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