Coverage for tdom/callables.py: 100%

43 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-17 19:54 +0000

1import sys 

2import typing as t 

3from dataclasses import dataclass 

4from functools import lru_cache 

5 

6 

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

8class CallableInfo: 

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

10 

11 id: int 

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

13 

14 named_params: frozenset[str] 

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

16 

17 required_named_params: frozenset[str] 

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

19 

20 requires_positional: bool 

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

22 

23 kwargs: bool 

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

25 

26 @classmethod 

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

28 """Create a CallableInfo from a callable.""" 

29 import inspect 

30 

31 sig = inspect.signature(c) 

32 named_params = [] 

33 required_named_params = [] 

34 requires_positional = False 

35 kwargs = False 

36 

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

38 match param.kind: 

39 case inspect.Parameter.POSITIONAL_ONLY: 

40 if param.default is param.empty: 

41 requires_positional = True 

42 case inspect.Parameter.POSITIONAL_OR_KEYWORD: 

43 named_params.append(param.name) 

44 if param.default is param.empty: 

45 required_named_params.append(param.name) 

46 case inspect.Parameter.VAR_POSITIONAL: 

47 # Does this necessarily mean it requires positional args? 

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

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

50 # that it does. 

51 requires_positional = True 

52 case inspect.Parameter.KEYWORD_ONLY: 

53 named_params.append(param.name) 

54 if param.default is param.empty: 

55 required_named_params.append(param.name) 

56 case inspect.Parameter.VAR_KEYWORD: 

57 kwargs = True 

58 

59 return cls( 

60 id=id(c), 

61 named_params=frozenset(named_params), 

62 required_named_params=frozenset(required_named_params), 

63 requires_positional=requires_positional, 

64 kwargs=kwargs, 

65 ) 

66 

67 @property 

68 def supports_zero_args(self) -> bool: 

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

70 return not self.requires_positional and not self.required_named_params 

71 

72 

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

74def get_callable_info(c: t.Callable) -> CallableInfo: 

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

76 return CallableInfo.from_callable(c)