Coverage for tdom / callables.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-03 21:23 +0000

1import sys 

2import typing as t 

3from collections.abc import Callable 

4from dataclasses import dataclass 

5from functools import lru_cache 

6 

7 

8@dataclass(slots=True, frozen=True) 

9class CallableInfo: 

10 """Information about a callable necessary for `tdom` to safely invoke it.""" 

11 

12 id: int 

13 """The unique identifier of the callable (from id()).""" 

14 

15 named_params: frozenset[str] 

16 """The names of the callable's named arguments.""" 

17 

18 required_named_params: frozenset[str] 

19 """The names of the callable's required named arguments.""" 

20 

21 requires_positional: bool 

22 """Whether the callable requires positional-only argument values.""" 

23 

24 kwargs: bool 

25 """Whether the callable accepts **kwargs.""" 

26 

27 @classmethod 

28 def from_callable(cls, c: Callable) -> t.Self: 

29 """Create a CallableInfo from a callable.""" 

30 import inspect 

31 

32 sig = inspect.signature(c) 

33 named_params = [] 

34 required_named_params = [] 

35 requires_positional = False 

36 kwargs = False 

37 

38 for param in sig.parameters.values(): 

39 match param.kind: 

40 case inspect.Parameter.POSITIONAL_ONLY: 

41 if param.default is param.empty: 

42 requires_positional = True 

43 case inspect.Parameter.POSITIONAL_OR_KEYWORD: 

44 named_params.append(param.name) 

45 if param.default is param.empty: 

46 required_named_params.append(param.name) 

47 case inspect.Parameter.VAR_POSITIONAL: 

48 # Does this necessarily mean it requires positional args? 

49 # Answer: No, but we have no way of knowing how many 

50 # positional args it *might* expect, so we have to assume 

51 # that it does. 

52 requires_positional = True 

53 case inspect.Parameter.KEYWORD_ONLY: 

54 named_params.append(param.name) 

55 if param.default is param.empty: 

56 required_named_params.append(param.name) 

57 case inspect.Parameter.VAR_KEYWORD: 

58 kwargs = True 

59 

60 return cls( 

61 id=id(c), 

62 named_params=frozenset(named_params), 

63 required_named_params=frozenset(required_named_params), 

64 requires_positional=requires_positional, 

65 kwargs=kwargs, 

66 ) 

67 

68 @property 

69 def supports_zero_args(self) -> bool: 

70 """Whether the callable can be called with zero arguments.""" 

71 return not self.requires_positional and not self.required_named_params 

72 

73 

74@lru_cache(maxsize=0 if "pytest" in sys.modules else 512) 

75def get_callable_info(c: Callable) -> CallableInfo: 

76 """Get the CallableInfo for a callable, caching the result.""" 

77 return CallableInfo.from_callable(c)