Coverage for tdom/callables.py: 100%
43 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-17 19:54 +0000
« 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
7@dataclass(slots=True, frozen=True)
8class CallableInfo:
9 """Information about a callable necessary for `tdom` to safely invoke it."""
11 id: int
12 """The unique identifier of the callable (from id())."""
14 named_params: frozenset[str]
15 """The names of the callable's named arguments."""
17 required_named_params: frozenset[str]
18 """The names of the callable's required named arguments."""
20 requires_positional: bool
21 """Whether the callable requires positional-only argument values."""
23 kwargs: bool
24 """Whether the callable accepts **kwargs."""
26 @classmethod
27 def from_callable(cls, c: t.Callable) -> t.Self:
28 """Create a CallableInfo from a callable."""
29 import inspect
31 sig = inspect.signature(c)
32 named_params = []
33 required_named_params = []
34 requires_positional = False
35 kwargs = False
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
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 )
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
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)