peritype 0.0.1__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.0.1
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,25 @@
1
+ [project]
2
+ name = "peritype"
3
+ version = "0.0.1"
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.8.4,<0.9.0"]
11
+ build-backend = "uv_build"
12
+
13
+ [dependency-groups]
14
+ dev = ["ruff>=0.12.7"]
15
+ test = ["pytest>=8.4.1", "pytest-cov>=6.2.1"]
16
+
17
+ [tool.ruff]
18
+ line-length = 120
19
+
20
+ [tool.ruff.lint]
21
+ extend-select = ["E", "W", "F", "B", "Q", "I", "N", "RUF", "UP"]
22
+ fixable = ["ALL"]
23
+
24
+ [tool.ruff.lint.per-file-ignores]
25
+ "**/__init__.py" = ["I001"]
@@ -0,0 +1,12 @@
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
5
+
6
+ __all__ = [
7
+ "FWrap",
8
+ "TWrap",
9
+ "use_cache",
10
+ "wrap_func",
11
+ "wrap_type",
12
+ ]
@@ -0,0 +1,9 @@
1
+ from peritype.collections.bag import TypeBag as TypeBag
2
+ from peritype.collections.map import TypeMap as TypeMap
3
+ from peritype.collections.tree import TypeSuperTree as TypeSuperTree
4
+
5
+ __all__ = [
6
+ "TypeBag",
7
+ "TypeMap",
8
+ "TypeSuperTree",
9
+ ]
@@ -0,0 +1,42 @@
1
+ from typing import Any
2
+
3
+ from peritype import TWrap
4
+ from peritype.twrap import TypeNode
5
+
6
+
7
+ class TypeBag:
8
+ def __init__(self) -> None:
9
+ self._bag = set[TWrap[Any]]()
10
+ self._raw_types = dict[type[Any], set[TWrap[Any]]]()
11
+
12
+ def add(self, twrap: TWrap[Any]) -> None:
13
+ self._bag.add(twrap)
14
+
15
+ def _for_nodes(node: TypeNode[Any]) -> None:
16
+ raw_type = node.cls
17
+ if raw_type not in self._raw_types:
18
+ self._raw_types[raw_type] = set()
19
+ self._raw_types[raw_type].add(twrap)
20
+
21
+ if not twrap.contains_any:
22
+ twrap.__visit__(for_nodes=_for_nodes)
23
+
24
+ def __contains__(self, twrap: TWrap[Any]) -> bool:
25
+ return twrap in self._bag
26
+
27
+ def get_all(self, twrap: TWrap[Any]) -> set[TWrap[Any]]:
28
+ if not twrap.contains_any:
29
+ return {twrap} if twrap in self._bag else set()
30
+ candidates = set[TWrap[Any]]()
31
+
32
+ def _for_nodes(node: TypeNode[Any]) -> None:
33
+ raw_type = node.cls
34
+ if raw_type in self._raw_types:
35
+ candidates.update(self._raw_types[raw_type])
36
+
37
+ twrap.__visit__(for_nodes=_for_nodes)
38
+ result = set[TWrap[Any]]()
39
+ for candidate in candidates:
40
+ if twrap.match(candidate):
41
+ result.add(candidate)
42
+ return result
@@ -0,0 +1,73 @@
1
+ from typing import Any, Literal, overload
2
+
3
+ from peritype.twrap import TWrap
4
+
5
+
6
+ class TypeMap:
7
+ def __init__(self) -> None:
8
+ self._content: dict[TWrap[Any], set[Any]] = {}
9
+
10
+ def __contains__(self, twrap: TWrap[Any], /) -> bool:
11
+ return twrap in self._content
12
+
13
+ def __getitem__(self, twrap: TWrap[Any], /) -> set[Any]:
14
+ return self._content[twrap]
15
+
16
+ def __setitem__(self, twrap: TWrap[Any], /, value: set[Any]) -> None:
17
+ self._content[twrap] = value
18
+
19
+ def __delitem__(self, twrap: TWrap[Any], /) -> None:
20
+ del self._content[twrap]
21
+
22
+ @overload
23
+ def get[T](
24
+ self,
25
+ twrap: TWrap[Any],
26
+ /,
27
+ *,
28
+ not_none: Literal[False] = False,
29
+ hint: type[T] | TWrap[T],
30
+ ) -> set[T] | None: ...
31
+ @overload
32
+ def get[T](
33
+ self,
34
+ twrap: TWrap[Any],
35
+ /,
36
+ *,
37
+ not_none: Literal[True],
38
+ hint: type[T] | TWrap[T],
39
+ ) -> set[T]: ...
40
+ @overload
41
+ def get(
42
+ self,
43
+ twrap: TWrap[Any],
44
+ /,
45
+ *,
46
+ not_none: Literal[False] = False,
47
+ hint: type[Any] | TWrap[Any] | None = None,
48
+ ) -> set[Any] | None: ...
49
+ @overload
50
+ def get(
51
+ self,
52
+ twrap: TWrap[Any],
53
+ /,
54
+ *,
55
+ not_none: Literal[True],
56
+ hint: type[Any] | TWrap[Any] | None = None,
57
+ ) -> set[Any] | None: ...
58
+ def get(
59
+ self,
60
+ twrap: TWrap[Any],
61
+ /,
62
+ *,
63
+ not_none: bool = False,
64
+ hint: type[Any] | TWrap[Any] | None = None,
65
+ ) -> set[Any] | None:
66
+ if not_none and twrap not in self._content:
67
+ return set()
68
+ return self._content.get(twrap)
69
+
70
+ def push(self, twrap: TWrap[Any], value: Any) -> None:
71
+ if twrap not in self._content:
72
+ self._content[twrap] = set()
73
+ self._content[twrap].add(value)
@@ -0,0 +1,47 @@
1
+ from typing import Any, ForwardRef, Generic, get_origin
2
+
3
+ from peritype.collections import TypeMap
4
+ from peritype.wrap import TWrap, TypeNode
5
+
6
+
7
+ class TypeSuperTree:
8
+ def __init__(self) -> None:
9
+ self._content: TypeMap = TypeMap()
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
+ if base is not twrap:
17
+ self.add(base)
18
+
19
+ def __contains__(self, twrap: TWrap[Any]) -> bool:
20
+ return twrap in self._content
21
+
22
+ def __getitem__(self, twrap: TWrap[Any]) -> set[TWrap[Any]]:
23
+ return self._content[twrap]
24
+
25
+ def __delitem__(self, twrap: TWrap[Any]) -> None:
26
+ del self._content[twrap]
27
+
28
+ def _add_type(self, base: TWrap[Any], derived: TWrap[Any]) -> None:
29
+ self._content.push(base, derived)
30
+
31
+ @staticmethod
32
+ def _recurse_all_bases(
33
+ twrap: TWrap[Any],
34
+ seen: set[TWrap[Any]],
35
+ ) -> None:
36
+ if twrap in seen:
37
+ return
38
+ seen.add(twrap)
39
+
40
+ def _for_nodes(node: TypeNode[Any]) -> None:
41
+ for base in node.bases:
42
+ origin = get_origin(base.origin)
43
+ if (origin or base.origin) in (object, Generic, ForwardRef):
44
+ continue
45
+ TypeSuperTree._recurse_all_bases(base, seen)
46
+
47
+ twrap.__visit__(for_nodes=_for_nodes)
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,128 @@
1
+ import inspect
2
+ from collections.abc import Callable
3
+ from typing import TYPE_CHECKING, Any, get_type_hints, override
4
+
5
+ from peritype.twrap import TWrap
6
+
7
+ if TYPE_CHECKING:
8
+ from peritype.twrap import TWrap, TypeVarLookup
9
+
10
+
11
+ class FWrap[**FuncP, FuncT]:
12
+ def __init__(self, func: Callable[FuncP, FuncT]) -> None:
13
+ if isinstance(func, FWrap):
14
+ raise TypeError(f"Cannot wrap {func}, already wrapped")
15
+ self.func = func
16
+ self.bound_to = getattr(self.func, "__self__", None)
17
+ self._signature_hints: dict[str, Any] | None = None
18
+ self._signature: inspect.Signature | None = None
19
+ self._parameters: dict[str, inspect.Parameter] | None = None
20
+
21
+ @property
22
+ def name(self) -> str:
23
+ return self.func.__name__ if hasattr(self.func, "__name__") else str(self.func)
24
+
25
+ @property
26
+ def parameters(self) -> dict[str, inspect.Parameter]:
27
+ if self._parameters is None:
28
+ if self._signature is None:
29
+ self._signature = inspect.signature(self.func)
30
+ self._parameters = {**self._signature.parameters}
31
+ return self._parameters
32
+
33
+ def param_at(self, index: int) -> inspect.Parameter:
34
+ all_params = [*self.parameters.values()]
35
+ return all_params[index]
36
+
37
+ def get_signature_hints(self, belongs_to: "TWrap[Any] | None" = None) -> "dict[str, TWrap[Any]]":
38
+ if self._signature_hints is None:
39
+ self._signature_hints = {
40
+ n: self._transform_annotation(c, belongs_to.type_var_lookup if belongs_to else None)
41
+ for n, c in get_type_hints(self.func, include_extras=True).items()
42
+ }
43
+ return self._signature_hints
44
+
45
+ def get_signature_hint(self, index: int, belongs_to: "TWrap[Any] | None" = None) -> "TWrap[Any]":
46
+ return self.get_signature_hints(belongs_to)[self.param_at(index).name]
47
+
48
+ def get_return_hint(self, belongs_to: "TWrap[Any] | None" = None) -> "TWrap[FuncT]":
49
+ return self.get_signature_hints(belongs_to)["return"]
50
+
51
+ def __call__(self, *args: FuncP.args, **kwargs: FuncP.kwargs) -> FuncT:
52
+ return self.func(*args, **kwargs)
53
+
54
+ @staticmethod
55
+ def _transform_annotation(anno: Any, lookup: "TypeVarLookup | None") -> Any:
56
+ from peritype import wrap_type
57
+
58
+ if lookup is not None and anno in lookup:
59
+ return wrap_type(lookup[anno], lookup=lookup)
60
+ return wrap_type(anno, lookup=lookup)
61
+
62
+ @override
63
+ def __str__(self) -> str:
64
+ return f"{self.func.__qualname__}"
65
+
66
+ @override
67
+ def __repr__(self) -> str:
68
+ return f"<Function {self}>"
69
+
70
+ @override
71
+ def __hash__(self) -> int:
72
+ return hash(self.func)
73
+
74
+ def bind(self, bound_to: "TWrap[Any]") -> "BoundFWrap[FuncP, FuncT]":
75
+ return BoundFWrap(self, bound_to)
76
+
77
+
78
+ class BoundFWrap[**FWrapP, FWrapT]:
79
+ def __init__(self, fwrap: FWrap[FWrapP, FWrapT], bound_to: "TWrap[Any]") -> None:
80
+ self._fwrap = fwrap
81
+ self._bound_to = bound_to
82
+
83
+ @property
84
+ def name(self) -> str:
85
+ return self.inner.name
86
+
87
+ @property
88
+ def inner(self) -> FWrap[FWrapP, FWrapT]:
89
+ return self._fwrap
90
+
91
+ @property
92
+ def bound_to(self) -> "TWrap[Any]":
93
+ return self._bound_to
94
+
95
+ @property
96
+ def func(self) -> Callable[FWrapP, FWrapT]:
97
+ return self.inner.func
98
+
99
+ @property
100
+ def parameters(self) -> dict[str, inspect.Parameter]:
101
+ return self.inner.parameters
102
+
103
+ def param_at(self, index: int) -> inspect.Parameter:
104
+ return self.inner.param_at(index)
105
+
106
+ def get_signature_hints(self) -> dict[str, TWrap[Any]]:
107
+ return self._fwrap.get_signature_hints(self._bound_to)
108
+
109
+ def get_signature_hint(self, index: int) -> "TWrap[Any]":
110
+ return self.get_signature_hints()[self.param_at(index).name]
111
+
112
+ def get_return_hint(self) -> "TWrap[FWrapT]":
113
+ return self.inner.get_return_hint(self.bound_to)
114
+
115
+ def __call__(self, *args: FWrapP.args, **kwargs: FWrapP.kwargs) -> FWrapT: # type: ignore[misc]
116
+ return self.inner(*args, **kwargs)
117
+
118
+ @override
119
+ def __str__(self) -> str:
120
+ return str(self.inner)
121
+
122
+ @override
123
+ def __repr__(self) -> str:
124
+ return repr(self.inner)
125
+
126
+ @override
127
+ def __hash__(self) -> int:
128
+ return hash(self.inner)
@@ -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,350 @@
1
+ from collections.abc import Callable, Iterator
2
+ from functools import cached_property
3
+ from types import NoneType
4
+ from typing import TYPE_CHECKING, Any, ForwardRef, Literal, TypeVar, cast, get_type_hints, override
5
+
6
+ import peritype
7
+ from peritype.errors import PeritypeError
8
+
9
+ if TYPE_CHECKING:
10
+ from peritype.fwrap import BoundFWrap
11
+
12
+
13
+ class TWrapMeta:
14
+ def __init__(
15
+ self,
16
+ *,
17
+ annotated: tuple[Any],
18
+ required: bool,
19
+ total: bool,
20
+ ) -> None:
21
+ self.annotated = annotated
22
+ self.required = required
23
+ self.total = total
24
+
25
+ @cached_property
26
+ def _hash(self) -> int:
27
+ return hash((self.annotated, self.required, self.total))
28
+
29
+ @override
30
+ def __hash__(self) -> int:
31
+ return self._hash
32
+
33
+
34
+ class TypeVarLookup:
35
+ def __init__(self, origins: dict[TypeVar, Any], twraps: dict[TypeVar, "TWrap[Any]"]) -> NoneType:
36
+ self.origin_mapping = origins
37
+ self.twrap_mapping = twraps
38
+
39
+ def __getitem__(self, key: TypeVar, /) -> Any:
40
+ if key not in self.twrap_mapping:
41
+ raise KeyError(key)
42
+ return self.origin_mapping[key]
43
+
44
+ def __contains__(self, key: TypeVar, /) -> bool:
45
+ return key in self.origin_mapping
46
+
47
+ def __iter__(self, /) -> Iterator[TypeVar]:
48
+ yield from self.origin_mapping
49
+
50
+ def __or__(self, other: "TypeVarLookup") -> "TypeVarLookup":
51
+ new_origins = self.origin_mapping | other.origin_mapping
52
+ new_twraps = self.twrap_mapping | other.twrap_mapping
53
+ return TypeVarLookup(new_origins, new_twraps)
54
+
55
+ def origin_items(self, /) -> Iterator[tuple[TypeVar, Any]]:
56
+ yield from self.origin_mapping.items()
57
+
58
+ def twrap_items(self, /) -> Iterator[tuple[TypeVar, "TWrap[Any]"]]:
59
+ yield from self.twrap_mapping.items()
60
+
61
+ def get_origin[DefT](self, key: TypeVar, /, default: DefT | None = None) -> Any | DefT | None:
62
+ if key not in self.twrap_mapping:
63
+ return default
64
+ return self.origin_mapping[key]
65
+
66
+ def get_twrap[DefT](self, key: TypeVar, /, default: DefT | None = None) -> Any | DefT | None:
67
+ if key not in self.twrap_mapping:
68
+ return default
69
+ return self.twrap_mapping[key]
70
+
71
+
72
+ class TypeNode[T]:
73
+ def __init__(
74
+ self,
75
+ origin: Any,
76
+ nodes: "tuple[TWrap[Any], ...]",
77
+ cls: type[T],
78
+ vars: tuple[Any, ...],
79
+ ) -> None:
80
+ self.origin = origin
81
+ self.nodes = nodes
82
+ self.cls = cls
83
+ self.vars = vars
84
+
85
+ @staticmethod
86
+ def _format_type(v: Any) -> str:
87
+ if isinstance(v, type):
88
+ return v.__qualname__
89
+ if isinstance(v, TypeVar):
90
+ return f"~{v.__name__}"
91
+ if v is Ellipsis:
92
+ return "..."
93
+ if v is Literal:
94
+ return v.__name__
95
+ if isinstance(v, ForwardRef):
96
+ return f"'{v.__forward_arg__}'"
97
+ return str(v)
98
+
99
+ @cached_property
100
+ def _str(self) -> str:
101
+ if not self.vars:
102
+ return self._format_type(self.cls)
103
+ return f"{self._format_type(self.cls)}[{', '.join(map(self._format_type, self.vars))}]"
104
+
105
+ @override
106
+ def __str__(self) -> str:
107
+ return self._str
108
+
109
+ @override
110
+ def __repr__(self) -> str:
111
+ return f"<TypeNode {self._str}>"
112
+
113
+ @cached_property
114
+ def _hash(self) -> int:
115
+ return hash((self.cls, self.nodes))
116
+
117
+ @override
118
+ def __hash__(self) -> int:
119
+ return self._hash
120
+
121
+ @override
122
+ def __eq__(self, value: object) -> bool:
123
+ if not isinstance(value, TypeNode):
124
+ return False
125
+ return hash(self) == hash(value) # pyright: ignore[reportUnknownArgumentType]
126
+
127
+ def __getitem__(self, index: int) -> "TWrap[Any]":
128
+ return self.nodes[index]
129
+
130
+ @cached_property
131
+ def base_name(self) -> str:
132
+ return self._format_type(self.cls)
133
+
134
+ @cached_property
135
+ def contains_any(self) -> bool:
136
+ if self.cls is Any or self.cls is Ellipsis: # pyright: ignore[reportUnnecessaryComparison]
137
+ return True
138
+ if isinstance(self.cls, tuple) and Any in self.cls:
139
+ return True
140
+ for node in self.nodes:
141
+ if node.contains_any:
142
+ return True
143
+ return False
144
+
145
+ @cached_property
146
+ def bases(self) -> tuple["TWrap[Any]", ...]:
147
+ bases: list[TWrap[Any]] = []
148
+ if hasattr(self.cls, "__orig_bases__"):
149
+ origin_bases: tuple[type[Any], ...] = getattr(self.cls, "__orig_bases__", ())
150
+ for base in origin_bases:
151
+ bases.append(peritype.wrap_type(base, lookup=self.type_var_lookup))
152
+ elif hasattr(self.cls, "__bases__"):
153
+ cls_bases = getattr(self.cls, "__bases__", ())
154
+ for base in cls_bases:
155
+ bases.append(peritype.wrap_type(base, lookup=self.type_var_lookup))
156
+ return (*bases,)
157
+
158
+ @cached_property
159
+ def type_var_lookup(self) -> TypeVarLookup:
160
+ parameters = getattr(self.cls, "__type_params__", None) or getattr(self.cls, "__parameters__", None)
161
+ origin_lookup = dict(zip(parameters, self.vars, strict=True)) if parameters else {}
162
+ twrap_lookup = dict(zip(parameters, self.nodes, strict=True)) if parameters else {}
163
+ lookup = TypeVarLookup(origin_lookup, twrap_lookup)
164
+ base_lookup = TypeVarLookup({}, {})
165
+ if hasattr(self.cls, "__orig_bases__"):
166
+ origin_bases: tuple[type[Any], ...] = getattr(self.cls, "__orig_bases__", ())
167
+ for base in origin_bases:
168
+ base_wrap = peritype.wrap_type(base, lookup=lookup)
169
+ base_lookup |= base_wrap.type_var_lookup
170
+ return base_lookup | lookup
171
+
172
+
173
+ class TWrap[T]:
174
+ def __init__(
175
+ self,
176
+ *,
177
+ origin: Any,
178
+ nodes: tuple[TypeNode[Any], ...],
179
+ meta: TWrapMeta,
180
+ ) -> None:
181
+ self._origin = origin
182
+ self._nodes = nodes
183
+ self._meta = meta
184
+ self._method_cache: dict[str, BoundFWrap[..., Any]] = {}
185
+
186
+ @cached_property
187
+ def _hash(self) -> int:
188
+ return hash(((*sorted(self._nodes, key=str),), self._meta))
189
+
190
+ @override
191
+ def __hash__(self) -> int:
192
+ return self._hash
193
+
194
+ @override
195
+ def __eq__(self, value: object) -> bool:
196
+ if not isinstance(value, TWrap):
197
+ return False
198
+ return hash(self) == hash(value) # pyright: ignore[reportUnknownArgumentType]
199
+
200
+ @cached_property
201
+ def _str(self) -> str:
202
+ return " | ".join(str(n) for n in self._nodes)
203
+
204
+ @override
205
+ def __str__(self) -> str:
206
+ return self._str
207
+
208
+ @cached_property
209
+ def _repr(self) -> str:
210
+ return f"<Type {self}>"
211
+
212
+ @override
213
+ def __repr__(self) -> str:
214
+ return self._repr
215
+
216
+ def __visit__(self, *, for_nodes: Callable[[TypeNode[Any]], None] | None = None) -> None:
217
+ if for_nodes:
218
+ for node in self._nodes:
219
+ for_nodes(node)
220
+
221
+ def __getitem__(self, index: int) -> TypeNode[Any]:
222
+ return self.nodes[index]
223
+
224
+ @property
225
+ def origin(self) -> type[T]:
226
+ return self._origin
227
+
228
+ @property
229
+ def required(self) -> bool:
230
+ return self._meta.required
231
+
232
+ @property
233
+ def total(self) -> bool:
234
+ return self._meta.total
235
+
236
+ @cached_property
237
+ def annotations(self) -> tuple[Any, ...]:
238
+ return self._meta.annotated
239
+
240
+ @property
241
+ def nodes(self) -> tuple["TypeNode[Any]", ...]:
242
+ return self._nodes
243
+
244
+ @cached_property
245
+ def type_var_lookup(self) -> TypeVarLookup:
246
+ lookup = TypeVarLookup({}, {})
247
+ for node in self._nodes:
248
+ lookup |= node.type_var_lookup
249
+ return lookup
250
+
251
+ @cached_property
252
+ def contains_any(self) -> bool:
253
+ return any(node.contains_any for node in self._nodes)
254
+
255
+ @cached_property
256
+ def union(self) -> bool:
257
+ return len([n for n in self._nodes if n.cls is not NoneType]) > 1
258
+
259
+ @cached_property
260
+ def nullable(self) -> bool:
261
+ return any(n.cls is NoneType for n in self._nodes)
262
+
263
+ @cached_property
264
+ def attribute_hints(self) -> "dict[str, TWrap[Any]]":
265
+ if self.union:
266
+ raise TypeError("Cannot get attributes of union types")
267
+ attrs: dict[str, TWrap[Any]] = {}
268
+ for node in self._nodes:
269
+ if node.cls is NoneType:
270
+ continue
271
+ attrs |= self._get_recursive_attribute_hints(node.cls)
272
+ return attrs
273
+
274
+ def _get_recursive_attribute_hints(self, cls: type[Any]) -> "dict[str, TWrap[Any]]":
275
+ attr_hints: dict[str, TWrap[Any]] = {}
276
+ try:
277
+ for base in cls.__bases__:
278
+ attr_hints |= self._get_recursive_attribute_hints(base)
279
+ raw_ints: dict[str, type[Any] | TypeVar] = get_type_hints(cls, include_extras=True)
280
+ for attr_name, hint in raw_ints.items():
281
+ if isinstance(hint, TypeVar):
282
+ if hint in self.type_var_lookup:
283
+ attr_hints[attr_name] = peritype.wrap_type(self.type_var_lookup[hint])
284
+ else:
285
+ raise PeritypeError(f"TypeVar ~{hint.__name__} could not be found in lookup", cls=cls)
286
+ else:
287
+ attr_hints[attr_name] = peritype.wrap_type(hint, lookup=self.type_var_lookup)
288
+ except (AttributeError, TypeError, NameError):
289
+ return attr_hints
290
+ return attr_hints
291
+
292
+ @cached_property
293
+ def init(self) -> "BoundFWrap[..., Any]":
294
+ if self.union:
295
+ raise TypeError("Cannot get __init__ of union types")
296
+ for node in self._nodes:
297
+ if hasattr(node.cls, "__init__"):
298
+ init_func = node.cls.__init__
299
+ fwrap = peritype.wrap_func(init_func)
300
+ bound_fwrap = fwrap.bind(self)
301
+ return bound_fwrap
302
+ raise TypeError("No __init__ method found in type nodes")
303
+
304
+ def get_method_hints(self, method_name: str) -> "BoundFWrap[..., Any]":
305
+ if self.union:
306
+ raise TypeError("Cannot get methods of union types")
307
+ if method_name in self._method_cache:
308
+ return self._method_cache[method_name]
309
+ for node in self._nodes:
310
+ if hasattr(node.cls, method_name):
311
+ method_func = getattr(node.cls, method_name)
312
+ fwrap = peritype.wrap_func(method_func)
313
+ bound_fwrap = fwrap.bind(self)
314
+ self._method_cache[method_name] = bound_fwrap
315
+ return bound_fwrap
316
+ raise AttributeError(f"Method '{method_name}' not found in type nodes")
317
+
318
+ def match(self, other: Any) -> bool:
319
+ other_wrap: TWrap[Any]
320
+ if isinstance(other, TWrap):
321
+ other_wrap = cast(TWrap[Any], other)
322
+ else:
323
+ other_wrap = peritype.wrap_type(other)
324
+
325
+ for a in self._nodes:
326
+ for b in other_wrap._nodes:
327
+ if self._nodes_intersect(a, b):
328
+ return True
329
+ return False
330
+
331
+ @staticmethod
332
+ def _nodes_intersect(a: "TypeNode[Any]", b: "TypeNode[Any]") -> bool:
333
+ if a.origin is Any or b.origin is Any:
334
+ return True
335
+ if a.origin is Ellipsis or b.origin is Ellipsis:
336
+ return True
337
+
338
+ if a.cls is not b.cls:
339
+ return False
340
+
341
+ if not a.nodes and not b.nodes:
342
+ return True
343
+
344
+ if len(a.nodes) != len(b.nodes):
345
+ return False
346
+
347
+ for i in range(len(a.nodes)):
348
+ if not a.nodes[i].match(b.nodes[i]):
349
+ return False
350
+ return True
@@ -0,0 +1,143 @@
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 PeritypeError
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
+ lookup: TypeVarMapping | None,
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
+ if isinstance(arg, ForwardRef) and raise_on_forward:
56
+ raise PeritypeError(
57
+ f"Generic parameter '{arg.__forward_arg__}' cannot be a string",
58
+ cls=origin,
59
+ )
60
+ if isinstance(arg, TypeVar):
61
+ if lookup is not None:
62
+ if arg in lookup:
63
+ arg = lookup[arg]
64
+ elif raise_on_typevar:
65
+ raise PeritypeError(
66
+ f"TypeVar ~{arg.__name__} could not be found in lookup",
67
+ cls=origin,
68
+ )
69
+ elif raise_on_typevar:
70
+ raise PeritypeError(
71
+ f"Generic parameter ~{arg.__name__} cannot be a TypeVar",
72
+ cls=origin,
73
+ )
74
+ if isinstance(arg, list):
75
+ arg = (*arg,)
76
+ type_vars.append(arg)
77
+ return origin, (*type_vars,)
78
+ return _cls, ()
79
+
80
+
81
+ def use_cache(value: bool) -> None:
82
+ from peritype import wrap
83
+
84
+ wrap.USE_CACHE = value
85
+
86
+
87
+ def fill_params_in(cls_: type[Any], vars: tuple[Any, ...]) -> tuple[type[Any], tuple[Any, ...]]:
88
+ params: tuple[Any, ...] = getattr(cls_, "__type_params__", None) or getattr(cls_, "__parameters__", None) or ()
89
+ if cls_ in BUILTIN_PARAM_COUNT:
90
+ param_count = BUILTIN_PARAM_COUNT[cls_]
91
+ else:
92
+ param_count = len(params)
93
+ if len(vars) >= param_count:
94
+ return cls_, vars
95
+ new_vars: list[Any] = []
96
+ for i in range(len(vars), param_count):
97
+ if i < len(params):
98
+ if isinstance(params[i], ParamSpec):
99
+ new_vars.append(...)
100
+ else:
101
+ new_vars.append(Any)
102
+ else:
103
+ new_vars.append(Any)
104
+ return cls_, (*vars, *new_vars)
105
+
106
+
107
+ BUILTIN_PARAM_COUNT: dict[type[Any], int] = {
108
+ collections.abc.Hashable: 0,
109
+ collections.abc.Awaitable: 1,
110
+ collections.abc.Coroutine: 3,
111
+ collections.abc.AsyncIterable: 1,
112
+ collections.abc.AsyncIterator: 1,
113
+ collections.abc.Iterable: 1,
114
+ collections.abc.Iterator: 1,
115
+ collections.abc.Reversible: 1,
116
+ collections.abc.Sized: 0,
117
+ collections.abc.Container: 1,
118
+ collections.abc.Collection: 1,
119
+ collections.abc.Set: 1,
120
+ collections.abc.MutableSet: 1,
121
+ collections.abc.Mapping: 2,
122
+ collections.abc.MutableMapping: 2,
123
+ collections.abc.Sequence: 1,
124
+ collections.abc.MutableSequence: 1,
125
+ list: 1,
126
+ collections.deque: 1,
127
+ set: 1,
128
+ frozenset: 1,
129
+ collections.abc.MappingView: 1,
130
+ collections.abc.KeysView: 1,
131
+ collections.abc.ItemsView: 2,
132
+ collections.abc.ValuesView: 1,
133
+ contextlib.AbstractContextManager: 1,
134
+ contextlib.AbstractAsyncContextManager: 1,
135
+ dict: 2,
136
+ collections.defaultdict: 2,
137
+ collections.OrderedDict: 2,
138
+ collections.Counter: 1,
139
+ collections.ChainMap: 2,
140
+ collections.abc.Generator: 3,
141
+ collections.abc.AsyncGenerator: 2,
142
+ type: 1,
143
+ }
@@ -0,0 +1,59 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, cast, overload
3
+
4
+ from peritype import FWrap, TWrap
5
+ from peritype.mapping import TypeVarMapping
6
+ from peritype.twrap import TWrapMeta, TypeNode
7
+ from peritype.utils import fill_params_in, get_generics, unpack_annotations, unpack_union
8
+
9
+ USE_CACHE = True
10
+ _TWRAP_CACHE: dict[Any, TWrap[Any]] = {}
11
+ _FWRAP_CACHE: dict[Any, FWrap[..., Any]] = {}
12
+
13
+
14
+ @overload
15
+ def wrap_type[T](
16
+ cls: type[T],
17
+ *,
18
+ lookup: TypeVarMapping | None = None,
19
+ ) -> TWrap[T]: ...
20
+ @overload
21
+ def wrap_type(
22
+ cls: Any,
23
+ *,
24
+ lookup: TypeVarMapping | None = None,
25
+ ) -> TWrap[Any]: ...
26
+ def wrap_type(
27
+ cls: Any,
28
+ *,
29
+ lookup: TypeVarMapping | None = None,
30
+ ) -> Any:
31
+ if USE_CACHE and cls in _TWRAP_CACHE:
32
+ return _TWRAP_CACHE[cls]
33
+ meta = TWrapMeta(annotated=tuple[Any](), required=True, total=True)
34
+ unpacked: Any = unpack_annotations(cls, meta)
35
+ nodes = unpack_union(unpacked)
36
+ wrapped_nodes: list[Any] = []
37
+ for node in nodes:
38
+ if node in (None, type(None)):
39
+ node = type(None)
40
+ root, vars = get_generics(node, lookup, True, True)
41
+ root, vars = fill_params_in(root, vars)
42
+ wrapped_vars = (*(wrap_type(var) for var in vars),)
43
+ wrapped_node = TypeNode(node, wrapped_vars, root, vars)
44
+ wrapped_nodes.append(wrapped_node)
45
+ twrap = cast(TWrap[Any], TWrap(origin=cls, nodes=(*wrapped_nodes,), meta=meta))
46
+ if USE_CACHE:
47
+ _TWRAP_CACHE[cls] = twrap
48
+ return twrap
49
+
50
+
51
+ def wrap_func[**FuncP, FuncT](
52
+ func: Callable[FuncP, FuncT],
53
+ ) -> FWrap[FuncP, FuncT]:
54
+ if USE_CACHE and func in _FWRAP_CACHE:
55
+ return _FWRAP_CACHE[func]
56
+ fwrap = FWrap(func)
57
+ if USE_CACHE:
58
+ _FWRAP_CACHE[func] = fwrap
59
+ return fwrap