peritype 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.3
2
+ Name: peritype
3
+ Version: 0.4.0
4
+ Summary: Peritype helps you explore Python types at runtime with ease.
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Peritype
9
+
10
+ Peritype helps you navigate Python types and annotations at runtime with ease.
11
+ It provides a standard interface to inspect the mess of types, generics, `TypeVar`s, `Annotated`s, and more.
12
+
13
+ ## Installation
14
+
15
+ ```shell
16
+ $ pip install peritype # or use your preferred package manager
17
+ ```
18
+
19
+ ## Features
20
+
21
+ ### Simple type wrapping
22
+
23
+ ```python
24
+ from peritype import wrap_type
25
+
26
+ class MyClass:
27
+ attr: int
28
+
29
+ def __init__(self, x: int, y: str) -> None:
30
+ self.x = x
31
+ self.y = y
32
+
33
+ def my_method(self, z: float) -> bool:
34
+ return z > 0.0
35
+
36
+ wrapped = wrap_type(MyClass)
37
+
38
+ # Test if the type can match another type
39
+ assert wrapped.match(MyClass)
40
+ assert not wrapped.match(int)
41
+
42
+ # Access attribute type hints
43
+ hints = wrapped.attribute_hints
44
+ assert hints['attr'].match(int)
45
+
46
+ # Access the __init__ method's signature hints
47
+ init_signature = wrapped.init.get_signature_hints()
48
+ assert init_signature['x'].match(int)
49
+ assert init_signature['y'].match(str)
50
+
51
+ # Access method signatures
52
+ method_wrap = wrapped.get_method_hints('my_method')
53
+ method_signature = method_wrap.get_signature_hints()
54
+ assert method_signature['z'].match(float)
55
+ assert method_wrap.return_hint.match(bool)
56
+ ```
57
+
58
+ ### Generic type wrapping
59
+
60
+ ```python
61
+ from peritype import wrap_type
62
+
63
+ class GenericParent[T]:
64
+ def get_value(self) -> T:
65
+ ...
66
+
67
+ class GenericChild[T, U](GenericParent[U]):
68
+ def get_other_value(self) -> T:
69
+ ...
70
+
71
+ wrapped_child = wrap_type(GenericChild[int, str])
72
+
73
+ # Access method signatures with resolved generics
74
+ get_value_wrap = wrapped_child.get_method_hints('get_value')
75
+ get_value_signature = get_value_wrap.get_signature_hints()
76
+ assert get_value_wrap.get_return_hint().match(str)
77
+
78
+ get_other_value_wrap = wrapped_child.get_method_hints('get_other_value')
79
+ get_other_value_signature = get_other_value_wrap.get_signature_hints()
80
+ assert get_other_value_wrap.get_return_hint().match(int)
81
+ ```
82
+
83
+ ### Union and `Any` handling
84
+
85
+ ```python
86
+ from peritype import wrap_type
87
+
88
+ int_wrap = wrap_type(int)
89
+ # A simple type can match itself and unions including itself
90
+ assert int_wrap.match(int)
91
+ assert int_wrap.match(int | str)
92
+ assert not int_wrap.match(str)
93
+
94
+ union_wrap = wrap_type(int | str)
95
+ # A union type can match any of its member types and unions including them
96
+ assert union_wrap.match(int | str)
97
+ assert union_wrap.match(int)
98
+ assert union_wrap.match(str)
99
+ assert union_wrap.match(int | float)
100
+ assert not union_wrap.match(float)
101
+
102
+ int_list_wrap = wrap_type(list[int])
103
+ # A generic type with parameters can match the same generic with compatible parameters, including Any or Ellipsis
104
+ assert int_list_wrap.match(list[int])
105
+ assert int_list_wrap.match(list[int | str])
106
+ assert int_list_wrap.match(list[Any])
107
+ ```
108
+
109
+ ### Type metadata, `Annotated` and more
110
+
111
+ ```python
112
+ from peritype import wrap_type
113
+ from typing import Annotated, NotRequired, TypedDict
114
+
115
+ annotated_wrap = wrap_type(Annotated[int | None, "metadata"])
116
+
117
+ assert annotated_wrap.nullable # None in the union
118
+ assert not annotated_wrap.union # int | None is not considered a union, the None part is handled separately
119
+ assert annotated_wrap.annotations == ("metadata",)
120
+
121
+ class MyTypedDict(TypedDict, total=False):
122
+ x: int
123
+ y: NotRequired[str]
124
+
125
+ typed_dict_wrap = wrap_type(MyTypedDict)
126
+ assert not typed_dict_wrap.total
127
+
128
+ attrs = typed_dict_wrap.attribute_hints
129
+ assert attrs["x"].match(int)
130
+ assert attrs["y"].match(str)
131
+ assert attrs["x"].required
132
+ assert not attrs["y"].required
133
+ ```
134
+
135
+ ### Function wrapping
136
+
137
+ ```python
138
+ from peritype import wrap_func
139
+
140
+ def my_function(a: int, b: str) -> bool:
141
+ return str(a) == b
142
+
143
+ wrapped_func = wrap_func(my_function)
144
+
145
+ # Access function signature hints
146
+ signature_hints = wrapped_func.get_signature_hints()
147
+ assert signature_hints['a'].match(int)
148
+ assert signature_hints['b'].match(str)
149
+ assert wrapped_func.get_return_hint().match(bool)
150
+ ```
@@ -0,0 +1,143 @@
1
+ # Peritype
2
+
3
+ Peritype helps you navigate Python types and annotations at runtime with ease.
4
+ It provides a standard interface to inspect the mess of types, generics, `TypeVar`s, `Annotated`s, and more.
5
+
6
+ ## Installation
7
+
8
+ ```shell
9
+ $ pip install peritype # or use your preferred package manager
10
+ ```
11
+
12
+ ## Features
13
+
14
+ ### Simple type wrapping
15
+
16
+ ```python
17
+ from peritype import wrap_type
18
+
19
+ class MyClass:
20
+ attr: int
21
+
22
+ def __init__(self, x: int, y: str) -> None:
23
+ self.x = x
24
+ self.y = y
25
+
26
+ def my_method(self, z: float) -> bool:
27
+ return z > 0.0
28
+
29
+ wrapped = wrap_type(MyClass)
30
+
31
+ # Test if the type can match another type
32
+ assert wrapped.match(MyClass)
33
+ assert not wrapped.match(int)
34
+
35
+ # Access attribute type hints
36
+ hints = wrapped.attribute_hints
37
+ assert hints['attr'].match(int)
38
+
39
+ # Access the __init__ method's signature hints
40
+ init_signature = wrapped.init.get_signature_hints()
41
+ assert init_signature['x'].match(int)
42
+ assert init_signature['y'].match(str)
43
+
44
+ # Access method signatures
45
+ method_wrap = wrapped.get_method_hints('my_method')
46
+ method_signature = method_wrap.get_signature_hints()
47
+ assert method_signature['z'].match(float)
48
+ assert method_wrap.return_hint.match(bool)
49
+ ```
50
+
51
+ ### Generic type wrapping
52
+
53
+ ```python
54
+ from peritype import wrap_type
55
+
56
+ class GenericParent[T]:
57
+ def get_value(self) -> T:
58
+ ...
59
+
60
+ class GenericChild[T, U](GenericParent[U]):
61
+ def get_other_value(self) -> T:
62
+ ...
63
+
64
+ wrapped_child = wrap_type(GenericChild[int, str])
65
+
66
+ # Access method signatures with resolved generics
67
+ get_value_wrap = wrapped_child.get_method_hints('get_value')
68
+ get_value_signature = get_value_wrap.get_signature_hints()
69
+ assert get_value_wrap.get_return_hint().match(str)
70
+
71
+ get_other_value_wrap = wrapped_child.get_method_hints('get_other_value')
72
+ get_other_value_signature = get_other_value_wrap.get_signature_hints()
73
+ assert get_other_value_wrap.get_return_hint().match(int)
74
+ ```
75
+
76
+ ### Union and `Any` handling
77
+
78
+ ```python
79
+ from peritype import wrap_type
80
+
81
+ int_wrap = wrap_type(int)
82
+ # A simple type can match itself and unions including itself
83
+ assert int_wrap.match(int)
84
+ assert int_wrap.match(int | str)
85
+ assert not int_wrap.match(str)
86
+
87
+ union_wrap = wrap_type(int | str)
88
+ # A union type can match any of its member types and unions including them
89
+ assert union_wrap.match(int | str)
90
+ assert union_wrap.match(int)
91
+ assert union_wrap.match(str)
92
+ assert union_wrap.match(int | float)
93
+ assert not union_wrap.match(float)
94
+
95
+ int_list_wrap = wrap_type(list[int])
96
+ # A generic type with parameters can match the same generic with compatible parameters, including Any or Ellipsis
97
+ assert int_list_wrap.match(list[int])
98
+ assert int_list_wrap.match(list[int | str])
99
+ assert int_list_wrap.match(list[Any])
100
+ ```
101
+
102
+ ### Type metadata, `Annotated` and more
103
+
104
+ ```python
105
+ from peritype import wrap_type
106
+ from typing import Annotated, NotRequired, TypedDict
107
+
108
+ annotated_wrap = wrap_type(Annotated[int | None, "metadata"])
109
+
110
+ assert annotated_wrap.nullable # None in the union
111
+ assert not annotated_wrap.union # int | None is not considered a union, the None part is handled separately
112
+ assert annotated_wrap.annotations == ("metadata",)
113
+
114
+ class MyTypedDict(TypedDict, total=False):
115
+ x: int
116
+ y: NotRequired[str]
117
+
118
+ typed_dict_wrap = wrap_type(MyTypedDict)
119
+ assert not typed_dict_wrap.total
120
+
121
+ attrs = typed_dict_wrap.attribute_hints
122
+ assert attrs["x"].match(int)
123
+ assert attrs["y"].match(str)
124
+ assert attrs["x"].required
125
+ assert not attrs["y"].required
126
+ ```
127
+
128
+ ### Function wrapping
129
+
130
+ ```python
131
+ from peritype import wrap_func
132
+
133
+ def my_function(a: int, b: str) -> bool:
134
+ return str(a) == b
135
+
136
+ wrapped_func = wrap_func(my_function)
137
+
138
+ # Access function signature hints
139
+ signature_hints = wrapped_func.get_signature_hints()
140
+ assert signature_hints['a'].match(int)
141
+ assert signature_hints['b'].match(str)
142
+ assert wrapped_func.get_return_hint().match(bool)
143
+ ```
@@ -0,0 +1,4 @@
1
+ from peritype.twrap import TWrap as TWrap
2
+ from peritype.fwrap import FWrap as FWrap
3
+ from peritype.wrap import wrap_type as wrap_type, wrap_func as wrap_func
4
+ from peritype.utils import use_cache as use_cache
@@ -0,0 +1,3 @@
1
+ from peritype.collections.bag import TypeBag as TypeBag
2
+ from peritype.collections.map import TypeMap as TypeMap, TypeSetMap as TypeSetMap
3
+ from peritype.collections.tree import TypeSuperTree as TypeSuperTree
@@ -0,0 +1,52 @@
1
+ from typing import Any
2
+
3
+ from peritype import TWrap
4
+
5
+
6
+ class TypeBag:
7
+ def __init__(self) -> None:
8
+ self._bag = set[TWrap[Any]]()
9
+ self._raw_types = dict[type[Any], set[TWrap[Any]]]()
10
+
11
+ def add(self, twrap: TWrap[Any]) -> None:
12
+ self._bag.add(twrap)
13
+ for node in twrap.nodes:
14
+ raw_type = node.inner_type
15
+ if raw_type not in self._raw_types:
16
+ self._raw_types[raw_type] = set()
17
+ self._raw_types[raw_type].add(twrap)
18
+
19
+ def __contains__(self, twrap: TWrap[Any]) -> bool:
20
+ return twrap in self._bag
21
+
22
+ def get_matching(self, twrap: TWrap[Any]) -> TWrap[Any] | None:
23
+ if twrap in self._bag:
24
+ return twrap
25
+ for node in twrap.nodes:
26
+ raw_type = node.inner_type
27
+ if raw_type in self._raw_types:
28
+ for wrap in self._raw_types[raw_type]:
29
+ if twrap.match(wrap):
30
+ return wrap
31
+ return None
32
+
33
+ def contains_matching(self, twrap: TWrap[Any]) -> bool:
34
+ return self.get_matching(twrap) is not None
35
+
36
+ def get_all(self, twrap: TWrap[Any]) -> set[TWrap[Any]]:
37
+ if not twrap.contains_any:
38
+ return {twrap} if twrap in self._bag else set()
39
+ result = set[TWrap[Any]]()
40
+ for node in twrap.nodes:
41
+ raw_type = node.inner_type
42
+ if raw_type in self._raw_types:
43
+ for wrap in self._raw_types[raw_type]:
44
+ if twrap.match(wrap):
45
+ result.add(wrap)
46
+ return result
47
+
48
+ def copy(self) -> "TypeBag":
49
+ new_bag = TypeBag()
50
+ new_bag._bag = self._bag.copy()
51
+ new_bag._raw_types = {k: v.copy() for k, v in self._raw_types.items()}
52
+ return new_bag
@@ -0,0 +1,64 @@
1
+ from collections.abc import Iterator
2
+ from typing import Any, overload
3
+
4
+ from peritype.twrap import TWrap
5
+
6
+
7
+ class TypeMap[K, V]:
8
+ def __init__(self) -> None:
9
+ self._content: dict[TWrap[K], V] = {}
10
+
11
+ def __contains__(self, twrap: TWrap[Any], /) -> bool:
12
+ return twrap in self._content
13
+
14
+ def __getitem__(self, twrap: TWrap[K], /) -> V:
15
+ return self._content[twrap]
16
+
17
+ def __setitem__(self, twrap: TWrap[K], value: V, /) -> None:
18
+ self._content[twrap] = value
19
+
20
+ def __delitem__(self, twrap: TWrap[K], /) -> None:
21
+ del self._content[twrap]
22
+
23
+ def __len__(self) -> int:
24
+ return len(self._content)
25
+
26
+ def __iter__(self) -> Iterator[tuple[TWrap[K], V]]:
27
+ yield from self._content.items()
28
+
29
+ @overload
30
+ def get[D](self, twrap: TWrap[K], /, *, default: D) -> V | D: ...
31
+ @overload
32
+ def get(
33
+ self,
34
+ twrap: TWrap[K],
35
+ /,
36
+ ) -> V | None: ...
37
+ def get(
38
+ self,
39
+ twrap: TWrap[K],
40
+ /,
41
+ *,
42
+ default: Any = None,
43
+ ) -> Any:
44
+ return self._content.get(twrap, default)
45
+
46
+ def add(self, twrap: TWrap[K], value: V, /) -> None:
47
+ self._content[twrap] = value
48
+
49
+ def copy(self) -> "TypeMap[K, V]":
50
+ new_map = TypeMap[K, V]()
51
+ new_map._content = self._content.copy()
52
+ return new_map
53
+
54
+
55
+ class TypeSetMap[K, V](TypeMap[K, set[V]]):
56
+ def push(self, twrap: TWrap[K], value: V, /) -> None:
57
+ if twrap not in self._content:
58
+ self._content[twrap] = set()
59
+ self._content[twrap].add(value)
60
+
61
+ def count(self, twrap: TWrap[K], /) -> int:
62
+ if twrap not in self._content:
63
+ return 0
64
+ return len(self._content[twrap])
@@ -0,0 +1,47 @@
1
+ from typing import Any, ForwardRef, Generic, get_origin
2
+
3
+ from peritype.collections import TypeSetMap
4
+ from peritype.wrap import TWrap
5
+
6
+
7
+ class TypeSuperTree:
8
+ def __init__(self) -> None:
9
+ self._content = TypeSetMap[Any, TWrap[Any]]()
10
+
11
+ def add(self, twrap: TWrap[Any]) -> None:
12
+ bases = set[TWrap[Any]]()
13
+ self._recurse_all_bases(twrap, bases)
14
+ for base in bases:
15
+ self._add_type(base, twrap)
16
+
17
+ def __contains__(self, twrap: TWrap[Any]) -> bool:
18
+ return twrap in self._content
19
+
20
+ def __getitem__(self, twrap: TWrap[Any]) -> set[TWrap[Any]]:
21
+ return self._content[twrap]
22
+
23
+ def __delitem__(self, twrap: TWrap[Any]) -> None:
24
+ del self._content[twrap]
25
+
26
+ def _add_type(self, base: TWrap[Any], derived: TWrap[Any]) -> None:
27
+ self._content.push(base, derived)
28
+
29
+ @staticmethod
30
+ def _recurse_all_bases(
31
+ twrap: TWrap[Any],
32
+ seen: set[TWrap[Any]],
33
+ ) -> None:
34
+ if twrap in seen:
35
+ return
36
+ seen.add(twrap)
37
+ for node in twrap.nodes:
38
+ for base in node.bases:
39
+ origin = get_origin(base.origin)
40
+ if (origin or base.origin) in (object, Generic, ForwardRef):
41
+ continue
42
+ TypeSuperTree._recurse_all_bases(base, seen)
43
+
44
+ def copy(self) -> "TypeSuperTree":
45
+ new_tree = TypeSuperTree()
46
+ new_tree._content = self._content.copy()
47
+ return new_tree
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+
3
+
4
+ class PeritypeError(Exception):
5
+ def __init__(self, message: str, cls: type[Any] | None = None) -> None:
6
+ if cls is not None:
7
+ message = f"{cls.__qualname__}: {message}"
8
+ super().__init__(message)
9
+ self.cls = cls
10
+
11
+
12
+ class UnresolvedForwardRefError(PeritypeError):
13
+ def __init__(self, name: str, cls: type[Any] | None = None) -> None:
14
+ super().__init__(f"Parameter {name} could not be resolved from context", cls=cls)
15
+ self.name = name
16
+
17
+
18
+ class UnresolvedTypeVarError(PeritypeError):
19
+ def __init__(self, typevar_name: str, cls: type[Any] | None = None) -> None:
20
+ super().__init__(f"TypeVar {typevar_name} could not be inferred from context", cls=cls)
21
+ self.typevar_name = typevar_name
@@ -0,0 +1,92 @@
1
+ import inspect
2
+ from collections.abc import Callable
3
+ from functools import cached_property
4
+ from typing import TYPE_CHECKING, Any, get_type_hints, override
5
+
6
+ from peritype.twrap import TWrap
7
+
8
+ if TYPE_CHECKING:
9
+ from peritype.twrap import TWrap, TypeVarLookup
10
+
11
+
12
+ class FWrap[**FuncP, FuncT]:
13
+ def __init__(self, func: Callable[FuncP, FuncT]) -> None:
14
+ if isinstance(func, FWrap):
15
+ raise TypeError(f"Cannot wrap {func}, already wrapped")
16
+ self.func = func
17
+ self.bound_to = getattr(self.func, "__self__", None)
18
+ self._signature_hints: dict[str, Any] | None = None
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ return self.func.__name__ if hasattr(self.func, "__name__") else str(self.func)
23
+
24
+ @cached_property
25
+ def signature(self) -> inspect.Signature:
26
+ return inspect.signature(self.func)
27
+
28
+ @cached_property
29
+ def parameters(self) -> dict[str, inspect.Parameter]:
30
+ return {**self.signature.parameters}
31
+
32
+ def param_at(self, index: int) -> inspect.Parameter:
33
+ all_params = [*self.parameters.values()]
34
+ return all_params[index]
35
+
36
+ def get_signature_hints(self, belongs_to: "TWrap[Any] | None" = None) -> "dict[str, TWrap[Any]]":
37
+ if self._signature_hints is None:
38
+ self._signature_hints = {
39
+ n: self._transform_annotation(c, belongs_to.type_var_lookup if belongs_to else None)
40
+ for n, c in get_type_hints(self.func, include_extras=True).items()
41
+ }
42
+ return self._signature_hints
43
+
44
+ def get_signature_hint(self, index: int, belongs_to: "TWrap[Any] | None" = None) -> "TWrap[Any]":
45
+ return self.get_signature_hints(belongs_to)[self.param_at(index).name]
46
+
47
+ def get_return_hint(self, belongs_to: "TWrap[Any] | None" = None) -> "TWrap[FuncT]":
48
+ return self.get_signature_hints(belongs_to)["return"]
49
+
50
+ def __call__(self, *args: FuncP.args, **kwargs: FuncP.kwargs) -> FuncT:
51
+ return self.func(*args, **kwargs)
52
+
53
+ @staticmethod
54
+ def _transform_annotation(anno: Any, lookup: "TypeVarLookup | None") -> Any:
55
+ from peritype import wrap_type
56
+
57
+ if lookup is not None and anno in lookup:
58
+ return wrap_type(lookup[anno], lookup=lookup)
59
+ return wrap_type(anno, lookup=lookup)
60
+
61
+ @override
62
+ def __str__(self) -> str:
63
+ return f"{self.func.__qualname__}"
64
+
65
+ @override
66
+ def __repr__(self) -> str:
67
+ return f"<Function {self}>"
68
+
69
+ @override
70
+ def __hash__(self) -> int:
71
+ return hash(self.func)
72
+
73
+ def bind(self, belongs_to: "TWrap[Any]") -> "BoundFWrap[FuncP, FuncT]":
74
+ return BoundFWrap(self.func, belongs_to)
75
+
76
+
77
+ class BoundFWrap[**FuncP, FuncT](FWrap[FuncP, FuncT]):
78
+ def __init__(self, func: Callable[FuncP, FuncT], belongs_to: "TWrap[Any]") -> None:
79
+ super().__init__(func)
80
+ self._belongs_to = belongs_to
81
+
82
+ @override
83
+ def get_signature_hints(self, belongs_to: "TWrap[Any] | None" = None) -> "dict[str, TWrap[Any]]":
84
+ return super().get_signature_hints(belongs_to=belongs_to or self._belongs_to)
85
+
86
+ @override
87
+ def get_signature_hint(self, index: int, belongs_to: "TWrap[Any] | None" = None) -> "TWrap[Any]":
88
+ return super().get_signature_hint(index, belongs_to=belongs_to or self._belongs_to)
89
+
90
+ @override
91
+ def get_return_hint(self, belongs_to: "TWrap[Any] | None" = None) -> "TWrap[FuncT]":
92
+ return super().get_return_hint(belongs_to=belongs_to or self._belongs_to)
@@ -0,0 +1,10 @@
1
+ from collections.abc import Iterator
2
+ from typing import Any, Protocol, TypeVar
3
+
4
+
5
+ class TypeVarMapping(Protocol):
6
+ def __getitem__(self, key: TypeVar, /) -> type[Any]: ...
7
+
8
+ def __contains__(self, key: TypeVar, /) -> bool: ...
9
+
10
+ def __iter__(self, /) -> Iterator[TypeVar]: ...
File without changes
@@ -0,0 +1,413 @@
1
+ import inspect
2
+ from collections.abc import Iterator
3
+ from functools import cached_property
4
+ from types import NoneType
5
+ from typing import TYPE_CHECKING, Any, ForwardRef, Generic, Literal, TypeVar, cast, get_origin, get_type_hints, override
6
+
7
+ import peritype
8
+ from peritype.errors import UnresolvedTypeVarError
9
+
10
+ if TYPE_CHECKING:
11
+ from peritype.fwrap import BoundFWrap, FWrap
12
+
13
+
14
+ class TWrapMeta:
15
+ def __init__(
16
+ self,
17
+ *,
18
+ annotated: tuple[Any],
19
+ required: bool,
20
+ total: bool,
21
+ ) -> None:
22
+ self.annotated = annotated
23
+ self.required = required
24
+ self.total = total
25
+
26
+ @cached_property
27
+ def _hash(self) -> int:
28
+ return hash((self.annotated, self.required, self.total))
29
+
30
+ @override
31
+ def __hash__(self) -> int:
32
+ return self._hash
33
+
34
+
35
+ class TypeVarLookup:
36
+ def __init__(self, origins: dict[TypeVar, Any], twraps: dict[TypeVar, "TWrap[Any]"]) -> NoneType:
37
+ self.origin_mapping = origins
38
+ self.twrap_mapping = twraps
39
+
40
+ def __getitem__(self, key: TypeVar, /) -> Any:
41
+ if key not in self.twrap_mapping:
42
+ raise KeyError(key)
43
+ return self.origin_mapping[key]
44
+
45
+ def __contains__(self, key: TypeVar, /) -> bool:
46
+ return key in self.origin_mapping
47
+
48
+ def __iter__(self, /) -> Iterator[TypeVar]:
49
+ yield from self.origin_mapping
50
+
51
+ def __or__(self, other: "TypeVarLookup") -> "TypeVarLookup":
52
+ new_origins = self.origin_mapping | other.origin_mapping
53
+ new_twraps = self.twrap_mapping | other.twrap_mapping
54
+ return TypeVarLookup(new_origins, new_twraps)
55
+
56
+ def origin_items(self, /) -> Iterator[tuple[TypeVar, Any]]:
57
+ yield from self.origin_mapping.items()
58
+
59
+ def twrap_items(self, /) -> Iterator[tuple[TypeVar, "TWrap[Any]"]]:
60
+ yield from self.twrap_mapping.items()
61
+
62
+ def get_origin[DefT](self, key: TypeVar, /, default: DefT | None = None) -> Any | DefT | None:
63
+ if key not in self.twrap_mapping:
64
+ return default
65
+ return self.origin_mapping[key]
66
+
67
+ def get_twrap[DefT](self, key: TypeVar, /, default: DefT | None = None) -> Any | DefT | None:
68
+ if key not in self.twrap_mapping:
69
+ return default
70
+ return self.twrap_mapping[key]
71
+
72
+
73
+ class TypeNode[T]:
74
+ def __init__(
75
+ self,
76
+ origin: Any,
77
+ generic_params: "tuple[TWrap[Any], ...]",
78
+ inner_type: type[T],
79
+ origin_params: tuple[Any, ...],
80
+ ) -> None:
81
+ self._origin = origin
82
+ self._generic_params = generic_params
83
+ self._inner_type = inner_type
84
+ self._origin_params = origin_params
85
+
86
+ @property
87
+ def origin(self) -> Any:
88
+ return self._origin
89
+
90
+ @property
91
+ def generic_params(self) -> "tuple[TWrap[Any], ...]":
92
+ return self._generic_params
93
+
94
+ @property
95
+ def inner_type(self) -> type[T]:
96
+ return self._inner_type
97
+
98
+ @property
99
+ def origin_params(self) -> tuple[Any, ...]:
100
+ return self._origin_params
101
+
102
+ @staticmethod
103
+ def _format_type(v: Any) -> str:
104
+ if isinstance(v, type):
105
+ return v.__qualname__
106
+ if isinstance(v, TypeVar):
107
+ return f"~{v.__name__}"
108
+ if v is Ellipsis:
109
+ return "..."
110
+ if v is Literal:
111
+ return v.__name__
112
+ if isinstance(v, ForwardRef):
113
+ return f"'{v.__forward_arg__}'"
114
+ return str(v)
115
+
116
+ @cached_property
117
+ def _str(self) -> str:
118
+ if not self._origin_params:
119
+ return self._format_type(self._inner_type)
120
+ return f"{self._format_type(self._inner_type)}[{', '.join(map(self._format_type, self._origin_params))}]"
121
+
122
+ @override
123
+ def __str__(self) -> str:
124
+ return self._str
125
+
126
+ @override
127
+ def __repr__(self) -> str:
128
+ return f"<TypeNode {self._str}>"
129
+
130
+ @cached_property
131
+ def _hash(self) -> int:
132
+ return hash((self._inner_type, self._generic_params))
133
+
134
+ @override
135
+ def __hash__(self) -> int:
136
+ return self._hash
137
+
138
+ @override
139
+ def __eq__(self, value: object) -> bool:
140
+ if not isinstance(value, TypeNode):
141
+ return False
142
+ return hash(self) == hash(value) # pyright: ignore[reportUnknownArgumentType]
143
+
144
+ def __getitem__(self, index: int) -> "TWrap[Any]":
145
+ return self._generic_params[index]
146
+
147
+ @cached_property
148
+ def base_name(self) -> str:
149
+ return self._format_type(self._inner_type)
150
+
151
+ @cached_property
152
+ def contains_any(self) -> bool:
153
+ if self._inner_type is Any or self._inner_type is Ellipsis: # pyright: ignore[reportUnnecessaryComparison]
154
+ return True
155
+ if isinstance(self._inner_type, tuple) and Any in self._inner_type:
156
+ return True
157
+ for node in self._generic_params:
158
+ if node.contains_any:
159
+ return True
160
+ return False
161
+
162
+ @cached_property
163
+ def bases(self) -> tuple["TWrap[Any]", ...]:
164
+ bases: list[TWrap[Any]] = []
165
+ if hasattr(self._inner_type, "__orig_bases__"):
166
+ origin_bases: tuple[type[Any], ...] = getattr(self._inner_type, "__orig_bases__", ())
167
+ for base in origin_bases:
168
+ if (base_origin := get_origin(base)) and base_origin is Generic:
169
+ continue
170
+ bases.append(peritype.wrap_type(base, lookup=self.type_var_lookup))
171
+ elif hasattr(self._inner_type, "__bases__"):
172
+ cls_bases = getattr(self._inner_type, "__bases__", ())
173
+ for base in cls_bases:
174
+ if (base_origin := get_origin(base)) and base_origin is Generic:
175
+ continue
176
+ bases.append(peritype.wrap_type(base, lookup=self.type_var_lookup))
177
+ return (*bases,)
178
+
179
+ @cached_property
180
+ def type_var_lookup(self) -> TypeVarLookup:
181
+ parameters = getattr(self._inner_type, "__type_params__", None) or getattr(
182
+ self._inner_type, "__parameters__", None
183
+ )
184
+ origin_lookup = dict(zip(parameters, self._origin_params, strict=True)) if parameters else {}
185
+ twrap_lookup = dict(zip(parameters, self._generic_params, strict=True)) if parameters else {}
186
+ lookup = TypeVarLookup(origin_lookup, twrap_lookup)
187
+ base_lookup = TypeVarLookup({}, {})
188
+ if hasattr(self._inner_type, "__orig_bases__"):
189
+ origin_bases: tuple[type[Any], ...] = getattr(self._inner_type, "__orig_bases__", ())
190
+ for base in origin_bases:
191
+ if (base_origin := get_origin(base)) and base_origin is Generic:
192
+ continue
193
+ base_wrap = peritype.wrap_type(base, lookup=lookup)
194
+ base_lookup |= base_wrap.type_var_lookup
195
+ return base_lookup | lookup
196
+
197
+ @cached_property
198
+ def attribute_hints(self) -> "dict[str, TWrap[Any]]":
199
+ if self._inner_type is NoneType:
200
+ return {}
201
+ return self._get_recursive_attribute_hints(self._inner_type)
202
+
203
+ def _get_recursive_attribute_hints(self, cls: type[Any]) -> "dict[str, TWrap[Any]]":
204
+ attr_hints: dict[str, TWrap[Any]] = {}
205
+ try:
206
+ for base in cls.__bases__:
207
+ attr_hints |= self._get_recursive_attribute_hints(base)
208
+ raw_ints: dict[str, type[Any] | TypeVar] = get_type_hints(cls, include_extras=True)
209
+ for attr_name, hint in raw_ints.items():
210
+ if isinstance(hint, TypeVar):
211
+ if hint in self.type_var_lookup:
212
+ attr_hints[attr_name] = peritype.wrap_type(self.type_var_lookup[hint])
213
+ else:
214
+ raise UnresolvedTypeVarError(hint.__name__, cls=cls)
215
+ else:
216
+ attr_hints[attr_name] = peritype.wrap_type(hint, lookup=self.type_var_lookup)
217
+ except (AttributeError, TypeError, NameError):
218
+ return attr_hints
219
+ return attr_hints
220
+
221
+ @cached_property
222
+ def init(self) -> "FWrap[..., Any]":
223
+ if not hasattr(self._inner_type, "__init__"):
224
+ raise TypeError("No __init__ method found in type nodes")
225
+ init_func = self._inner_type.__init__
226
+ return peritype.wrap_func(init_func)
227
+
228
+ @cached_property
229
+ def signature(self) -> inspect.Signature:
230
+ return self.init.signature
231
+
232
+ @cached_property
233
+ def parameters(self) -> dict[str, inspect.Parameter]:
234
+ return {**self.signature.parameters}
235
+
236
+ def instantiate(self, /, *args: Any, **kwargs: Any) -> T:
237
+ return self._inner_type(*args, **kwargs)
238
+
239
+ def get_method(self, method_name: str) -> "FWrap[..., Any]":
240
+ if not hasattr(self._inner_type, method_name):
241
+ raise AttributeError(f"Method '{method_name}' not found")
242
+ method_func = getattr(self._inner_type, method_name)
243
+ return peritype.wrap_func(method_func)
244
+
245
+ def match(self, other: "TWrap[Any]") -> bool:
246
+ for other_node in other.nodes:
247
+ if self._nodes_intersect(other_node):
248
+ return True
249
+ return False
250
+
251
+ def _nodes_intersect(self, b: "TypeNode[Any]") -> bool:
252
+ if self._origin is Any or b._origin is Any:
253
+ return True
254
+ if self._origin is Ellipsis or b._origin is Ellipsis:
255
+ return True
256
+
257
+ if self._inner_type is not b._inner_type:
258
+ return False
259
+
260
+ if not self._generic_params and not b._generic_params:
261
+ return True
262
+
263
+ if len(self._generic_params) != len(b._generic_params):
264
+ return False
265
+
266
+ for i in range(len(self._generic_params)):
267
+ if not self._generic_params[i].match(b._generic_params[i]):
268
+ return False
269
+ return True
270
+
271
+
272
+ class TWrap[T]:
273
+ def __init__(
274
+ self,
275
+ *,
276
+ origin: Any,
277
+ nodes: tuple[TypeNode[Any], ...],
278
+ meta: TWrapMeta,
279
+ ) -> None:
280
+ self._origin = origin
281
+ self._nodes = nodes
282
+ self._meta = meta
283
+ self._method_cache: dict[str, BoundFWrap[..., Any]] = {}
284
+
285
+ @cached_property
286
+ def _hash(self) -> int:
287
+ return hash(((*sorted(self._nodes, key=str),), self._meta))
288
+
289
+ @override
290
+ def __hash__(self) -> int:
291
+ return self._hash
292
+
293
+ @override
294
+ def __eq__(self, value: object) -> bool:
295
+ if not isinstance(value, TWrap):
296
+ return False
297
+ return hash(self) == hash(value) # pyright: ignore[reportUnknownArgumentType]
298
+
299
+ @cached_property
300
+ def _str(self) -> str:
301
+ return " | ".join(str(n) for n in self._nodes)
302
+
303
+ @override
304
+ def __str__(self) -> str:
305
+ return self._str
306
+
307
+ @cached_property
308
+ def _repr(self) -> str:
309
+ return f"<Type {self}>"
310
+
311
+ @override
312
+ def __repr__(self) -> str:
313
+ return self._repr
314
+
315
+ def __getitem__(self, index: int) -> TypeNode[Any]:
316
+ return self.nodes[index]
317
+
318
+ @property
319
+ def origin(self) -> type[T]:
320
+ return self._origin
321
+
322
+ @property
323
+ def required(self) -> bool:
324
+ return self._meta.required
325
+
326
+ @property
327
+ def total(self) -> bool:
328
+ return self._meta.total
329
+
330
+ @cached_property
331
+ def annotations(self) -> tuple[Any, ...]:
332
+ return self._meta.annotated
333
+
334
+ @property
335
+ def nodes(self) -> tuple["TypeNode[Any]", ...]:
336
+ return self._nodes
337
+
338
+ @cached_property
339
+ def type_var_lookup(self) -> TypeVarLookup:
340
+ lookup = TypeVarLookup({}, {})
341
+ for node in self._nodes:
342
+ lookup |= node.type_var_lookup
343
+ return lookup
344
+
345
+ @cached_property
346
+ def contains_any(self) -> bool:
347
+ return any(node.contains_any for node in self._nodes)
348
+
349
+ @cached_property
350
+ def union(self) -> bool:
351
+ return len([n for n in self._nodes if n.inner_type is not NoneType]) > 1
352
+
353
+ @cached_property
354
+ def nullable(self) -> bool:
355
+ return any(n.inner_type is NoneType for n in self._nodes)
356
+
357
+ @cached_property
358
+ def attribute_hints(self) -> "dict[str, TWrap[Any]]":
359
+ if self.union:
360
+ raise TypeError("Cannot get attributes of union types")
361
+ return self._nodes[0].attribute_hints
362
+
363
+ @cached_property
364
+ def init(self) -> "BoundFWrap[..., Any]":
365
+ if self.union:
366
+ raise TypeError("Cannot get __init__ of union types")
367
+ return self._nodes[0].init.bind(self)
368
+
369
+ @cached_property
370
+ def signature(self) -> inspect.Signature:
371
+ if self.union:
372
+ raise TypeError("Cannot get signature of union types")
373
+ return inspect.signature(self._nodes[0].inner_type)
374
+
375
+ @cached_property
376
+ def parameters(self) -> dict[str, inspect.Parameter]:
377
+ return {**self.signature.parameters}
378
+
379
+ @cached_property
380
+ def inner_type(self) -> Any:
381
+ if self.union:
382
+ raise TypeError("Cannot get inner type of union types")
383
+ return self._nodes[0].inner_type
384
+
385
+ @cached_property
386
+ def generic_params(self) -> "tuple[TWrap[Any], ...]":
387
+ if self.union:
388
+ raise TypeError("Cannot get generic params of union types")
389
+ return self._nodes[0].generic_params
390
+
391
+ def instantiate(self, /, *args: Any, **kwargs: Any) -> T:
392
+ if self.union:
393
+ raise TypeError("Cannot instantiate union types")
394
+ return self._nodes[0].instantiate(*args, **kwargs)
395
+
396
+ def get_method(self, method_name: str) -> "BoundFWrap[..., Any]":
397
+ if self.union:
398
+ raise TypeError("Cannot get methods of union types")
399
+ if method_name in self._method_cache:
400
+ return self._method_cache[method_name]
401
+ return self._nodes[0].get_method(method_name).bind(self)
402
+
403
+ def match(self, other: Any) -> bool:
404
+ other_wrap: TWrap[Any]
405
+ if isinstance(other, TWrap):
406
+ other_wrap = cast(TWrap[Any], other)
407
+ else:
408
+ other_wrap = peritype.wrap_type(other)
409
+
410
+ for a in self._nodes:
411
+ if a.match(other_wrap):
412
+ return True
413
+ return False
@@ -0,0 +1,163 @@
1
+ import collections.abc
2
+ import contextlib
3
+ from types import UnionType
4
+ from typing import (
5
+ Annotated,
6
+ Any,
7
+ ForwardRef,
8
+ NotRequired,
9
+ ParamSpec,
10
+ TypeAliasType,
11
+ TypeVar,
12
+ Union, # pyright: ignore[reportDeprecated]
13
+ get_args,
14
+ get_origin,
15
+ )
16
+
17
+ from peritype.errors import UnresolvedForwardRefError, UnresolvedTypeVarError
18
+ from peritype.mapping import TypeVarMapping
19
+ from peritype.twrap import TWrapMeta
20
+
21
+
22
+ def unpack_annotations(cls: Any, meta: TWrapMeta) -> Any:
23
+ if isinstance(cls, TypeAliasType):
24
+ return unpack_annotations(cls.__value__, meta)
25
+ origin = get_origin(cls)
26
+ if origin is Annotated:
27
+ cls, *annotated = get_args(cls)
28
+ meta.annotated = (*annotated,)
29
+ return unpack_annotations(cls, meta)
30
+ if origin is NotRequired:
31
+ meta.required = False
32
+ return unpack_annotations(get_args(cls)[0], meta)
33
+ meta.total = getattr(cls, "__total__", True)
34
+ return cls
35
+
36
+
37
+ def unpack_union(cls: Any) -> tuple[Any, ...]:
38
+ origin = get_origin(cls)
39
+ if origin in (UnionType, Union): # pyright: ignore[reportDeprecated]
40
+ return get_args(cls)
41
+ else:
42
+ return (cls,)
43
+
44
+
45
+ def get_generics[GenT](
46
+ _cls: type[GenT],
47
+ *,
48
+ raise_on_forward: bool,
49
+ raise_on_typevar: bool,
50
+ ) -> tuple[type[GenT], tuple[Any, ...]]:
51
+ if origin := get_origin(_cls):
52
+ type_vars: list[Any] = []
53
+ for arg in get_args(_cls):
54
+ arg: Any
55
+ match arg:
56
+ case TypeVar() if raise_on_typevar:
57
+ raise UnresolvedTypeVarError(arg.__name__, cls=origin)
58
+ case ForwardRef() if raise_on_forward:
59
+ raise UnresolvedForwardRefError(arg.__forward_arg__, cls=origin)
60
+ case list():
61
+ arg = (*arg,)
62
+ case _:
63
+ pass
64
+ type_vars.append(arg)
65
+ return origin, (*type_vars,)
66
+ return _cls, ()
67
+
68
+
69
+ def specialize_type(
70
+ cls: Any,
71
+ lookup: TypeVarMapping,
72
+ *,
73
+ raise_on_forward: bool = True,
74
+ raise_on_typevar: bool = True,
75
+ ) -> Any:
76
+ origin = get_origin(cls)
77
+ if origin is None:
78
+ return cls
79
+ args = get_args(cls)
80
+ if not args:
81
+ return cls
82
+ new_args: list[Any] = []
83
+ specialized = False
84
+ for arg in args:
85
+ match arg:
86
+ case TypeVar() if arg in lookup:
87
+ new_args.append(lookup[arg])
88
+ specialized = True
89
+ case TypeVar() if raise_on_typevar:
90
+ raise UnresolvedTypeVarError(arg.__name__, cls=origin)
91
+ case ForwardRef() if raise_on_forward:
92
+ raise UnresolvedForwardRefError(arg.__forward_arg__, cls=origin)
93
+ case _:
94
+ new_args.append(arg)
95
+ vars = tuple(new_args)
96
+ if not specialized:
97
+ return cls
98
+ return origin[vars]
99
+
100
+
101
+ def use_cache(value: bool) -> None:
102
+ from peritype import wrap
103
+
104
+ wrap.USE_CACHE = value
105
+
106
+
107
+ def fill_params_in(cls_: type[Any], vars: tuple[Any, ...]) -> tuple[type[Any], tuple[Any, ...]]:
108
+ params: tuple[Any, ...] = getattr(cls_, "__type_params__", None) or getattr(cls_, "__parameters__", None) or ()
109
+ if cls_ in BUILTIN_PARAM_COUNT:
110
+ param_count = BUILTIN_PARAM_COUNT[cls_]
111
+ else:
112
+ param_count = len(params)
113
+ if len(vars) >= param_count:
114
+ return cls_, vars
115
+ new_vars: list[Any] = []
116
+ for i in range(len(vars), param_count):
117
+ if i < len(params):
118
+ if isinstance(params[i], ParamSpec):
119
+ new_vars.append(...)
120
+ else:
121
+ new_vars.append(Any)
122
+ else:
123
+ new_vars.append(Any)
124
+ return cls_, (*vars, *new_vars)
125
+
126
+
127
+ BUILTIN_PARAM_COUNT: dict[type[Any], int] = {
128
+ collections.abc.Hashable: 0,
129
+ collections.abc.Awaitable: 1,
130
+ collections.abc.Coroutine: 3,
131
+ collections.abc.AsyncIterable: 1,
132
+ collections.abc.AsyncIterator: 1,
133
+ collections.abc.Iterable: 1,
134
+ collections.abc.Iterator: 1,
135
+ collections.abc.Reversible: 1,
136
+ collections.abc.Sized: 0,
137
+ collections.abc.Container: 1,
138
+ collections.abc.Collection: 1,
139
+ collections.abc.Set: 1,
140
+ collections.abc.MutableSet: 1,
141
+ collections.abc.Mapping: 2,
142
+ collections.abc.MutableMapping: 2,
143
+ collections.abc.Sequence: 1,
144
+ collections.abc.MutableSequence: 1,
145
+ list: 1,
146
+ collections.deque: 1,
147
+ set: 1,
148
+ frozenset: 1,
149
+ collections.abc.MappingView: 1,
150
+ collections.abc.KeysView: 1,
151
+ collections.abc.ItemsView: 2,
152
+ collections.abc.ValuesView: 1,
153
+ contextlib.AbstractContextManager: 1,
154
+ contextlib.AbstractAsyncContextManager: 1,
155
+ dict: 2,
156
+ collections.defaultdict: 2,
157
+ collections.OrderedDict: 2,
158
+ collections.Counter: 1,
159
+ collections.ChainMap: 2,
160
+ collections.abc.Generator: 3,
161
+ collections.abc.AsyncGenerator: 2,
162
+ type: 1,
163
+ }
@@ -0,0 +1,68 @@
1
+ import weakref
2
+ from collections.abc import Callable
3
+ from typing import Any, cast, overload
4
+
5
+ from peritype import FWrap, TWrap
6
+ from peritype.mapping import TypeVarMapping
7
+ from peritype.twrap import TWrapMeta, TypeNode
8
+ from peritype.utils import (
9
+ fill_params_in,
10
+ get_generics,
11
+ specialize_type,
12
+ unpack_annotations,
13
+ unpack_union,
14
+ )
15
+
16
+ USE_CACHE = True
17
+ _TWRAP_CACHE: weakref.WeakValueDictionary[Any, TWrap[Any]] = weakref.WeakValueDictionary()
18
+ _FWRAP_CACHE: weakref.WeakValueDictionary[Any, FWrap[..., Any]] = weakref.WeakValueDictionary()
19
+
20
+
21
+ @overload
22
+ def wrap_type[T](
23
+ cls: type[T],
24
+ *,
25
+ lookup: TypeVarMapping | None = None,
26
+ ) -> TWrap[T]: ...
27
+ @overload
28
+ def wrap_type(
29
+ cls: Any,
30
+ *,
31
+ lookup: TypeVarMapping | None = None,
32
+ ) -> TWrap[Any]: ...
33
+ def wrap_type(
34
+ cls: Any,
35
+ *,
36
+ lookup: TypeVarMapping | None = None,
37
+ ) -> Any:
38
+ if lookup is not None:
39
+ cls = specialize_type(cls, lookup, raise_on_forward=True, raise_on_typevar=True)
40
+ if USE_CACHE and cls in _TWRAP_CACHE:
41
+ return _TWRAP_CACHE[cls]
42
+ meta = TWrapMeta(annotated=tuple[Any](), required=True, total=True)
43
+ unpacked: Any = unpack_annotations(cls, meta)
44
+ nodes = unpack_union(unpacked)
45
+ wrapped_nodes: list[Any] = []
46
+ for node in nodes:
47
+ if node in (None, type(None)):
48
+ node = type(None)
49
+ root, vars = get_generics(node, raise_on_forward=True, raise_on_typevar=True)
50
+ root, vars = fill_params_in(root, vars)
51
+ wrapped_vars = (*(wrap_type(var) for var in vars),)
52
+ wrapped_node = TypeNode(node, wrapped_vars, root, vars)
53
+ wrapped_nodes.append(wrapped_node)
54
+ twrap = cast(TWrap[Any], TWrap(origin=cls, nodes=(*wrapped_nodes,), meta=meta))
55
+ if USE_CACHE:
56
+ _TWRAP_CACHE[cls] = twrap
57
+ return twrap
58
+
59
+
60
+ def wrap_func[**FuncP, FuncT](
61
+ func: Callable[FuncP, FuncT],
62
+ ) -> FWrap[FuncP, FuncT]:
63
+ if USE_CACHE and func in _FWRAP_CACHE:
64
+ return _FWRAP_CACHE[func]
65
+ fwrap = FWrap(func)
66
+ if USE_CACHE:
67
+ _FWRAP_CACHE[func] = fwrap
68
+ return fwrap
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "peritype"
3
+ version = "0.4.0"
4
+ description = "Peritype helps you explore Python types at runtime with ease."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = []
8
+
9
+ [build-system]
10
+ requires = ["uv_build>=0.9.23,<0.10.0"]
11
+ build-backend = "uv_build"
12
+
13
+ [tool.uv.build-backend]
14
+ package = "peritype"
15
+ module-root = ""
16
+
17
+ [dependency-groups]
18
+ dev = ["ruff>=0.14.11"]
19
+ test = ["pytest>=9.0.2", "pytest-cov>=7.0.0"]
20
+
21
+ [tool.ruff]
22
+ line-length = 120
23
+
24
+ [tool.ruff.lint]
25
+ extend-select = ["E", "W", "F", "B", "Q", "I", "N", "RUF", "UP"]
26
+ fixable = ["ALL"]
27
+
28
+ [tool.ruff.lint.per-file-ignores]
29
+ "**/__init__.py" = ["I001"]