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.
- peritype-0.4.0/PKG-INFO +150 -0
- peritype-0.4.0/README.md +143 -0
- peritype-0.4.0/peritype/__init__.py +4 -0
- peritype-0.4.0/peritype/collections/__init__.py +3 -0
- peritype-0.4.0/peritype/collections/bag.py +52 -0
- peritype-0.4.0/peritype/collections/map.py +64 -0
- peritype-0.4.0/peritype/collections/tree.py +47 -0
- peritype-0.4.0/peritype/errors.py +21 -0
- peritype-0.4.0/peritype/fwrap.py +92 -0
- peritype-0.4.0/peritype/mapping.py +10 -0
- peritype-0.4.0/peritype/py.typed +0 -0
- peritype-0.4.0/peritype/twrap.py +413 -0
- peritype-0.4.0/peritype/utils.py +163 -0
- peritype-0.4.0/peritype/wrap.py +68 -0
- peritype-0.4.0/pyproject.toml +29 -0
peritype-0.4.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
peritype-0.4.0/README.md
ADDED
|
@@ -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,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"]
|