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.
- peritype-0.0.1/PKG-INFO +150 -0
- peritype-0.0.1/README.md +143 -0
- peritype-0.0.1/pyproject.toml +25 -0
- peritype-0.0.1/src/peritype/__init__.py +12 -0
- peritype-0.0.1/src/peritype/collections/__init__.py +9 -0
- peritype-0.0.1/src/peritype/collections/bag.py +42 -0
- peritype-0.0.1/src/peritype/collections/map.py +73 -0
- peritype-0.0.1/src/peritype/collections/tree.py +47 -0
- peritype-0.0.1/src/peritype/errors.py +9 -0
- peritype-0.0.1/src/peritype/fwrap.py +128 -0
- peritype-0.0.1/src/peritype/mapping.py +10 -0
- peritype-0.0.1/src/peritype/py.typed +0 -0
- peritype-0.0.1/src/peritype/twrap.py +350 -0
- peritype-0.0.1/src/peritype/utils.py +143 -0
- peritype-0.0.1/src/peritype/wrap.py +59 -0
peritype-0.0.1/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
peritype-0.0.1/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,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,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,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
|