flextype 0.1.0__py3-none-any.whl
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.
- flextype/__init__.py +36 -0
- flextype/isinstance.py +90 -0
- flextype/load.py +70 -0
- flextype/registry_meta.py +493 -0
- flextype/registry_pickle.py +240 -0
- flextype/singledispatch.py +438 -0
- flextype-0.1.0.dist-info/METADATA +77 -0
- flextype-0.1.0.dist-info/RECORD +11 -0
- flextype-0.1.0.dist-info/WHEEL +5 -0
- flextype-0.1.0.dist-info/licenses/LICENSE +201 -0
- flextype-0.1.0.dist-info/top_level.txt +1 -0
flextype/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Lazy alternatives to singledispatch and isinstance."""
|
|
2
|
+
|
|
3
|
+
from . import registry_pickle
|
|
4
|
+
from .isinstance import LazyType, lazy_isinstance, lazy_issubclass
|
|
5
|
+
from .load import lazy_callable, lazy_import
|
|
6
|
+
from .registry_meta import (
|
|
7
|
+
ProtocolRegistry,
|
|
8
|
+
ProtocolRegistryMeta,
|
|
9
|
+
RegistrationError,
|
|
10
|
+
Registry,
|
|
11
|
+
RegistryMeta,
|
|
12
|
+
annotator,
|
|
13
|
+
copy_explicit_registry_classes,
|
|
14
|
+
get_explicit_registry_classes,
|
|
15
|
+
)
|
|
16
|
+
from .singledispatch import Flexdispatch, flexdispatch, is_valid_dispatch_type
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Flexdispatch",
|
|
20
|
+
"LazyType",
|
|
21
|
+
"ProtocolRegistry",
|
|
22
|
+
"ProtocolRegistryMeta",
|
|
23
|
+
"RegistrationError",
|
|
24
|
+
"Registry",
|
|
25
|
+
"RegistryMeta",
|
|
26
|
+
"annotator",
|
|
27
|
+
"copy_explicit_registry_classes",
|
|
28
|
+
"flexdispatch",
|
|
29
|
+
"get_explicit_registry_classes",
|
|
30
|
+
"is_valid_dispatch_type",
|
|
31
|
+
"lazy_callable",
|
|
32
|
+
"lazy_import",
|
|
33
|
+
"lazy_isinstance",
|
|
34
|
+
"lazy_issubclass",
|
|
35
|
+
"registry_pickle",
|
|
36
|
+
]
|
flextype/isinstance.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""A lazy version of isinstance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import UnionType
|
|
6
|
+
from typing import Any, Union, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
type LazyType = type | str | UnionType | tuple[LazyType, ...]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_union_type(cls: Any) -> bool: # noqa: ANN401
|
|
12
|
+
return get_origin(cls) in {Union, UnionType}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _split_lazy_type(lazy_type: LazyType) -> tuple[set[type], set[str]]:
|
|
16
|
+
"""Split classinfo into a set of types and a set of strings."""
|
|
17
|
+
if isinstance(lazy_type, str):
|
|
18
|
+
return set(), {t.strip().removeprefix("builtins.") for t in lazy_type.split("|")}
|
|
19
|
+
if isinstance(lazy_type, type):
|
|
20
|
+
return {lazy_type}, set()
|
|
21
|
+
if isinstance(lazy_type, tuple):
|
|
22
|
+
types: set[type] = set()
|
|
23
|
+
strings: set[str] = set()
|
|
24
|
+
for item in lazy_type:
|
|
25
|
+
t, s = _split_lazy_type(item)
|
|
26
|
+
types.update(t)
|
|
27
|
+
strings.update(s)
|
|
28
|
+
return types, strings
|
|
29
|
+
if _is_union_type(lazy_type):
|
|
30
|
+
types = set()
|
|
31
|
+
strings = set()
|
|
32
|
+
for arg in get_args(lazy_type):
|
|
33
|
+
t, s = _split_lazy_type(arg)
|
|
34
|
+
types.update(t)
|
|
35
|
+
strings.update(s)
|
|
36
|
+
return types, strings
|
|
37
|
+
|
|
38
|
+
msg = f"Invalid classinfo: {lazy_type!r}"
|
|
39
|
+
raise TypeError(msg)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _find_matching_string_type(cls: type, string_types: set[str] | dict[str, Any]) -> str | None:
|
|
43
|
+
"""Check if the type's name matches any of the strings."""
|
|
44
|
+
module = cls.__module__
|
|
45
|
+
qualname = cls.__qualname__
|
|
46
|
+
|
|
47
|
+
if module == "builtins":
|
|
48
|
+
for s in string_types:
|
|
49
|
+
if qualname == s:
|
|
50
|
+
return s
|
|
51
|
+
|
|
52
|
+
for s in string_types:
|
|
53
|
+
if f"{module}.{qualname}" == s:
|
|
54
|
+
return s
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _find_closest_string_type(cls: type, string_types: set[str] | dict[str, Any]) -> tuple[type, str] | None:
|
|
60
|
+
"""Check if any type in the MRO matches any of the strings."""
|
|
61
|
+
mro = cls.__mro__
|
|
62
|
+
for super_cls in mro:
|
|
63
|
+
matching_type = _find_matching_string_type(super_cls, string_types)
|
|
64
|
+
if matching_type is not None:
|
|
65
|
+
return super_cls, matching_type
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def lazy_isinstance(obj: object, class_or_tuple: LazyType, /) -> bool:
|
|
70
|
+
"""A lazy version of isinstance."""
|
|
71
|
+
types, strings = _split_lazy_type(class_or_tuple)
|
|
72
|
+
if len(types) > 0 and isinstance(obj, tuple(types)):
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
if len(strings) > 0:
|
|
76
|
+
return _find_closest_string_type(type(obj), strings) is not None
|
|
77
|
+
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def lazy_issubclass(cls: type, class_or_tuple: LazyType, /) -> bool:
|
|
82
|
+
"""A lazy version of issubclass."""
|
|
83
|
+
types, strings = _split_lazy_type(class_or_tuple)
|
|
84
|
+
if len(types) > 0 and issubclass(cls, tuple(types)):
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
if len(strings) > 0:
|
|
88
|
+
return _find_closest_string_type(cls, strings) is not None
|
|
89
|
+
|
|
90
|
+
return False
|
flextype/load.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Lazy import utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import sys
|
|
8
|
+
from typing import TYPE_CHECKING, Any, overload
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable, Iterable
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def lazy_import(name: str, package: str | None = None, register: bool = False) -> ModuleType:
|
|
16
|
+
"""Lazily import a module."""
|
|
17
|
+
if name in sys.modules:
|
|
18
|
+
return sys.modules[name]
|
|
19
|
+
|
|
20
|
+
spec = importlib.util.find_spec(name, package=package)
|
|
21
|
+
|
|
22
|
+
if spec is None or spec.loader is None:
|
|
23
|
+
msg = f"Module {name} not found"
|
|
24
|
+
raise ImportError(msg)
|
|
25
|
+
|
|
26
|
+
loader: importlib.util.LazyLoader = importlib.util.LazyLoader(spec.loader)
|
|
27
|
+
spec.loader = loader
|
|
28
|
+
module = importlib.util.module_from_spec(spec)
|
|
29
|
+
if register:
|
|
30
|
+
sys.modules[name] = module
|
|
31
|
+
loader.exec_module(module)
|
|
32
|
+
return module
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
def lazy_callable(
|
|
37
|
+
module: ModuleType | str,
|
|
38
|
+
attrs: str,
|
|
39
|
+
package: str | None = None,
|
|
40
|
+
register: bool = False,
|
|
41
|
+
) -> Callable: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@overload
|
|
45
|
+
def lazy_callable(
|
|
46
|
+
module: ModuleType | str,
|
|
47
|
+
attrs: Iterable[str],
|
|
48
|
+
package: str | None = None,
|
|
49
|
+
register: bool = False,
|
|
50
|
+
) -> list[Callable]: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def lazy_callable(
|
|
54
|
+
module: ModuleType | str,
|
|
55
|
+
attrs: str | Iterable[str],
|
|
56
|
+
package: str | None = None,
|
|
57
|
+
register: bool = False,
|
|
58
|
+
) -> Callable | list[Callable]:
|
|
59
|
+
"""Lazily get a callable attribute from a module or module name."""
|
|
60
|
+
if isinstance(module, str):
|
|
61
|
+
module = lazy_import(module, package=package, register=register)
|
|
62
|
+
|
|
63
|
+
if isinstance(attrs, str):
|
|
64
|
+
|
|
65
|
+
def fn(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
66
|
+
return getattr(module, attrs)(*args, **kwargs)
|
|
67
|
+
|
|
68
|
+
return fn
|
|
69
|
+
|
|
70
|
+
return [lazy_callable(module, attr) for attr in attrs]
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""RegistryMeta is a metaclass that allows instances and subclasses to be registered in a registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABCMeta
|
|
6
|
+
import functools
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Protocol, cast, is_protocol, overload, runtime_checkable
|
|
8
|
+
from weakref import ReferenceType, WeakSet, ref
|
|
9
|
+
|
|
10
|
+
from flextype.isinstance import _find_closest_string_type, _split_lazy_type
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
from flextype import LazyType
|
|
16
|
+
|
|
17
|
+
EXCLUDED_ATTRS = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"_subclass_registry",
|
|
20
|
+
"_instance_registry",
|
|
21
|
+
"_negative_instance_registry",
|
|
22
|
+
"_string_registry",
|
|
23
|
+
"_structural_checking",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _iter_registry_classes() -> list[RegistryMeta[Any]]:
|
|
29
|
+
"""Return all currently alive classes that use RegistryMeta."""
|
|
30
|
+
classes = [
|
|
31
|
+
registry_class
|
|
32
|
+
for registry_class in RegistryMeta.known_registry_classes
|
|
33
|
+
if isinstance(registry_class, RegistryMeta)
|
|
34
|
+
]
|
|
35
|
+
classes.sort(key=lambda cls: (cls.__module__, cls.__qualname__))
|
|
36
|
+
return classes
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_explicit_registry_classes(instance: object) -> list[RegistryMeta[Any]]:
|
|
40
|
+
"""Collect all registry classes where `instance` was explicitly registered."""
|
|
41
|
+
return [
|
|
42
|
+
registry_class
|
|
43
|
+
for registry_class in _iter_registry_classes()
|
|
44
|
+
if registry_class.is_explicit_instance_registered(instance)
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def copy_explicit_registry_classes(source: object, target: object) -> None:
|
|
49
|
+
"""Copy explicit registry registrations from `source` to `target`."""
|
|
50
|
+
for registry_class in get_explicit_registry_classes(source):
|
|
51
|
+
registry_class.register_instance(target)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RegistrationError(Exception):
|
|
55
|
+
"""Exception raised when an object cannot be registered."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, *, registry: type, target: object) -> None:
|
|
58
|
+
"""Create a registration error with contextual metadata."""
|
|
59
|
+
self.registry = registry
|
|
60
|
+
self.target_type = type(target)
|
|
61
|
+
super().__init__(
|
|
62
|
+
f"Registration failed for registry class {registry.__qualname__!r} and target type "
|
|
63
|
+
f"{self.target_type.__qualname__!r}: Registered instances must be weak-referenceable."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _IdentityWeakSet[T: object]:
|
|
68
|
+
"""Weak set that tracks objects by identity rather than equality/hash."""
|
|
69
|
+
|
|
70
|
+
__slots__ = (
|
|
71
|
+
"__weakref__",
|
|
72
|
+
"_refs",
|
|
73
|
+
)
|
|
74
|
+
_refs: dict[int, ReferenceType[T]]
|
|
75
|
+
|
|
76
|
+
def __init__(self) -> None:
|
|
77
|
+
self._refs = {}
|
|
78
|
+
|
|
79
|
+
def add(self, instance: T) -> None:
|
|
80
|
+
"""Add `instance` without requiring it to be hashable."""
|
|
81
|
+
key = id(instance)
|
|
82
|
+
registry_ref = ref(self)
|
|
83
|
+
|
|
84
|
+
def remove(instance_ref: ReferenceType[T], /, *, key: int = key) -> None:
|
|
85
|
+
registry = registry_ref()
|
|
86
|
+
if registry is not None:
|
|
87
|
+
registry.discard_ref(key, instance_ref)
|
|
88
|
+
|
|
89
|
+
self._refs[key] = ref(instance, remove)
|
|
90
|
+
|
|
91
|
+
def discard_ref(self, key: int, instance_ref: ReferenceType[T]) -> None:
|
|
92
|
+
"""Discard a weak reference if it is still the one registered for `key`."""
|
|
93
|
+
if self._refs.get(key) is instance_ref:
|
|
94
|
+
self._refs.pop(key, None)
|
|
95
|
+
|
|
96
|
+
def discard(self, instance: object) -> None:
|
|
97
|
+
"""Discard `instance` if this exact object is registered."""
|
|
98
|
+
key = id(instance)
|
|
99
|
+
instance_ref = self._refs.get(key)
|
|
100
|
+
if instance_ref is not None and instance_ref() is instance:
|
|
101
|
+
self._refs.pop(key, None)
|
|
102
|
+
|
|
103
|
+
def __contains__(self, instance: object) -> bool:
|
|
104
|
+
"""Return whether this exact object is registered."""
|
|
105
|
+
key = id(instance)
|
|
106
|
+
instance_ref = self._refs.get(key)
|
|
107
|
+
if instance_ref is None:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
registered_instance = instance_ref()
|
|
111
|
+
if registered_instance is instance:
|
|
112
|
+
return True
|
|
113
|
+
if registered_instance is None:
|
|
114
|
+
self._refs.pop(key, None)
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def _lazy_subclass_hook[T](cls: RegistryMeta[T], subclass: type, /) -> bool:
|
|
120
|
+
"""A __subclasshook__ that checks whether the ."""
|
|
121
|
+
string_registry = getattr(cls, "_string_registry", None)
|
|
122
|
+
|
|
123
|
+
if string_registry is not None and len(string_registry) > 0:
|
|
124
|
+
closest = _find_closest_string_type(subclass, string_registry)
|
|
125
|
+
if closest is not None:
|
|
126
|
+
real_type, string_type = closest
|
|
127
|
+
string_registry.remove(string_type)
|
|
128
|
+
cls._register(real_type)
|
|
129
|
+
|
|
130
|
+
return NotImplemented
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def _nop_instancehook[T](_cls: RegistryMeta[T], _instance: object, /) -> bool:
|
|
135
|
+
"""A __instancehook__ that does nothing."""
|
|
136
|
+
return NotImplemented
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _lazy_subclass_hook_with_pre_hook[T](
|
|
140
|
+
pre_hook: Callable[[RegistryMeta[T], type], bool],
|
|
141
|
+
) -> Callable[[RegistryMeta[T], type], bool]:
|
|
142
|
+
"""A __subclasshook__ that checks the pre_hook before checking the string registry."""
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def hook(cls: RegistryMeta[T], subclass: type, /) -> bool:
|
|
146
|
+
pre_res = pre_hook(cls, subclass)
|
|
147
|
+
|
|
148
|
+
if pre_res is not NotImplemented:
|
|
149
|
+
return pre_res
|
|
150
|
+
|
|
151
|
+
return _lazy_subclass_hook.__func__(cls, subclass)
|
|
152
|
+
|
|
153
|
+
return hook
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class List(list):
|
|
157
|
+
"""A wrapper around list that allows weak references, to allow lists to be registered in registries."""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Dict(dict):
|
|
161
|
+
"""A wrapper around dict that allows weak references, to allow dicts to be registered in registries."""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Set(set):
|
|
165
|
+
"""A wrapper around set that allows weak references, to allow sets to be registered in registries."""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def make_builtin_weakrefable[T: object](obj: T) -> T:
|
|
169
|
+
"""Wrap built-in types in a weak-referenceable wrapper to allow them to be registered in registries."""
|
|
170
|
+
if type(obj) is list:
|
|
171
|
+
return List(obj) # ty:ignore[invalid-return-type]
|
|
172
|
+
if type(obj) is dict:
|
|
173
|
+
return Dict(obj) # ty:ignore[invalid-return-type]
|
|
174
|
+
if type(obj) is set:
|
|
175
|
+
return Set(obj) # ty:ignore[invalid-return-type]
|
|
176
|
+
return obj
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class RegistryMeta[T: object](ABCMeta):
|
|
180
|
+
"""Metaclass for registry classes."""
|
|
181
|
+
|
|
182
|
+
_subclass_registry: WeakSet[type]
|
|
183
|
+
_instance_registry: _IdentityWeakSet[T]
|
|
184
|
+
_negative_instance_registry: _IdentityWeakSet[object]
|
|
185
|
+
_string_registry: set[str]
|
|
186
|
+
known_registry_classes: WeakSet[type] = WeakSet()
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
cls,
|
|
190
|
+
name: str,
|
|
191
|
+
bases: tuple[type, ...],
|
|
192
|
+
namespace: dict[str, Any],
|
|
193
|
+
/,
|
|
194
|
+
**kwargs: Any, # noqa: ANN401
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Create a new class with a registry."""
|
|
197
|
+
super().__init__(name, bases, namespace, **kwargs)
|
|
198
|
+
cls._subclass_registry: WeakSet[type] = WeakSet()
|
|
199
|
+
cls._instance_registry: _IdentityWeakSet[T] = _IdentityWeakSet()
|
|
200
|
+
cls._negative_instance_registry: _IdentityWeakSet[object] = _IdentityWeakSet()
|
|
201
|
+
cls._string_registry: set[str] = set()
|
|
202
|
+
|
|
203
|
+
if not is_protocol(cls):
|
|
204
|
+
subclasshook = namespace.get("__subclasshook__")
|
|
205
|
+
if subclasshook is None:
|
|
206
|
+
subclasshook = _lazy_subclass_hook
|
|
207
|
+
else:
|
|
208
|
+
if isinstance(subclasshook, classmethod):
|
|
209
|
+
subclasshook = subclasshook.__func__
|
|
210
|
+
subclasshook = _lazy_subclass_hook_with_pre_hook(subclasshook)
|
|
211
|
+
|
|
212
|
+
cls.__subclasshook__ = subclasshook # ty: ignore[invalid-assignment]
|
|
213
|
+
|
|
214
|
+
RegistryMeta.known_registry_classes.add(cls)
|
|
215
|
+
|
|
216
|
+
def is_explicit_instance_registered(cls, instance: object) -> bool:
|
|
217
|
+
"""Return whether `instance` was explicitly registered via register_instance."""
|
|
218
|
+
try:
|
|
219
|
+
return instance in cls._instance_registry
|
|
220
|
+
except TypeError:
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def _register(cls, subclass: type) -> type:
|
|
224
|
+
"""Register a subclass in the registry."""
|
|
225
|
+
res = super().register(subclass)
|
|
226
|
+
cls._subclass_registry.add(subclass)
|
|
227
|
+
return res
|
|
228
|
+
|
|
229
|
+
def _register_lazy(cls, subclass_strings: set[str]) -> None:
|
|
230
|
+
if is_protocol(cls) and len(subclass_strings) > 0:
|
|
231
|
+
msg = (
|
|
232
|
+
"Lazy subclass registration not supported for Protocols. Use ProtocolRegistry "
|
|
233
|
+
"with structural_checking=False instead if you want to use lazy subclass registration."
|
|
234
|
+
)
|
|
235
|
+
raise RuntimeError(msg)
|
|
236
|
+
cls._string_registry.update(subclass_strings)
|
|
237
|
+
|
|
238
|
+
def register(cls, subclass: LazyType) -> type:
|
|
239
|
+
"""Register a (lazy) subclass or a set of subclasses in registry."""
|
|
240
|
+
types, strings = _split_lazy_type(subclass)
|
|
241
|
+
for t in types:
|
|
242
|
+
cls._register(t)
|
|
243
|
+
|
|
244
|
+
cls._register_lazy(strings)
|
|
245
|
+
cls._negative_instance_registry = _IdentityWeakSet()
|
|
246
|
+
|
|
247
|
+
if isinstance(subclass, type):
|
|
248
|
+
return subclass
|
|
249
|
+
return cls
|
|
250
|
+
|
|
251
|
+
def _register_instance[Q](cls: RegistryMeta[T], instance: Q) -> Q:
|
|
252
|
+
try:
|
|
253
|
+
cls._instance_registry.add(instance) # ty:ignore[invalid-argument-type]
|
|
254
|
+
cls._negative_instance_registry.discard(instance)
|
|
255
|
+
except TypeError as err:
|
|
256
|
+
raise RegistrationError(
|
|
257
|
+
registry=cls,
|
|
258
|
+
target=instance,
|
|
259
|
+
) from err
|
|
260
|
+
return instance
|
|
261
|
+
|
|
262
|
+
def _negative_register_instance[Q](cls: RegistryMeta[T], instance: Q) -> Q:
|
|
263
|
+
try:
|
|
264
|
+
cls._negative_instance_registry.add(instance)
|
|
265
|
+
cls._instance_registry.discard(instance)
|
|
266
|
+
except TypeError as err:
|
|
267
|
+
raise RegistrationError(
|
|
268
|
+
registry=cls,
|
|
269
|
+
target=instance,
|
|
270
|
+
) from err
|
|
271
|
+
return instance
|
|
272
|
+
|
|
273
|
+
def register_instance[Q](cls: RegistryMeta[T], instance: Q, autocast_builtins: bool = False) -> Q:
|
|
274
|
+
"""Register an instance in the registry."""
|
|
275
|
+
if isinstance(instance, cls):
|
|
276
|
+
return instance
|
|
277
|
+
|
|
278
|
+
if autocast_builtins:
|
|
279
|
+
instance = make_builtin_weakrefable(instance)
|
|
280
|
+
|
|
281
|
+
return cls._register_instance(instance)
|
|
282
|
+
|
|
283
|
+
@overload
|
|
284
|
+
def register_factory[**In, Q](
|
|
285
|
+
cls: RegistryMeta[T],
|
|
286
|
+
func: Callable[In, Q],
|
|
287
|
+
*,
|
|
288
|
+
autocast_builtins: bool = False,
|
|
289
|
+
raise_on_failure: bool = True,
|
|
290
|
+
) -> Callable[In, Q]: ...
|
|
291
|
+
|
|
292
|
+
@overload
|
|
293
|
+
def register_factory[**In, Q](
|
|
294
|
+
cls: RegistryMeta[T],
|
|
295
|
+
*,
|
|
296
|
+
autocast_builtins: bool = False,
|
|
297
|
+
raise_on_failure: bool = True,
|
|
298
|
+
) -> Callable[[Callable[In, Q]], Callable[In, Q]]: ...
|
|
299
|
+
|
|
300
|
+
def register_factory[**In, Q](
|
|
301
|
+
cls: RegistryMeta[T],
|
|
302
|
+
func: Callable[In, Q] | None = None,
|
|
303
|
+
*,
|
|
304
|
+
autocast_builtins: bool = False,
|
|
305
|
+
raise_on_failure: bool = True,
|
|
306
|
+
) -> Callable[In, Q] | Callable[[Callable[In, Q]], Callable[In, Q]]:
|
|
307
|
+
"""Decorator to annotate the results of a function with the registry type."""
|
|
308
|
+
if func is None:
|
|
309
|
+
|
|
310
|
+
def decorator(func: Callable[In, Q]) -> Callable[In, Q]:
|
|
311
|
+
return cls.register_factory(
|
|
312
|
+
func, autocast_builtins=autocast_builtins, raise_on_failure=raise_on_failure
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return decorator
|
|
316
|
+
|
|
317
|
+
return annotator(cls, autocast_builtins=autocast_builtins, raise_on_failure=raise_on_failure)(func)
|
|
318
|
+
|
|
319
|
+
def _non_registered_instancecheck(cls, instance: object) -> bool:
|
|
320
|
+
"""Check if an instance is an instance of cls without checking the registry."""
|
|
321
|
+
return super().__instancecheck__(instance)
|
|
322
|
+
|
|
323
|
+
def __instancecheck__(cls, instance: object) -> bool:
|
|
324
|
+
"""Check if an instance is in the registry."""
|
|
325
|
+
try:
|
|
326
|
+
if instance in cls._instance_registry:
|
|
327
|
+
return True
|
|
328
|
+
if instance in cls._negative_instance_registry:
|
|
329
|
+
return False
|
|
330
|
+
except TypeError:
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
instancehook = getattr(cls, "__instancehook__", None)
|
|
334
|
+
if instancehook is not None:
|
|
335
|
+
res = instancehook(instance)
|
|
336
|
+
if res is not NotImplemented:
|
|
337
|
+
try:
|
|
338
|
+
if res:
|
|
339
|
+
cls._register_instance(instance)
|
|
340
|
+
else:
|
|
341
|
+
cls._negative_register_instance(instance)
|
|
342
|
+
except RegistrationError:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
return res
|
|
346
|
+
|
|
347
|
+
for subclass in cls._subclass_registry:
|
|
348
|
+
if isinstance(instance, subclass):
|
|
349
|
+
return True
|
|
350
|
+
for subclass in cls.__subclasses__():
|
|
351
|
+
if isinstance(instance, subclass):
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
return cls._non_registered_instancecheck(instance)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class ProtocolRegistryMeta[T](RegistryMeta[T], type(Protocol)):
|
|
358
|
+
"""Metaclass for protocol registry classes.
|
|
359
|
+
|
|
360
|
+
Takes an additional keyword argument `structural_checking`
|
|
361
|
+
which controls whether structural checking is performed, as is the default for protocols.
|
|
362
|
+
If `structural_checking` is False, ProtocolRegistry classes will only consider regular inheritance and
|
|
363
|
+
explicit registration, not structural compatibility, when checking for instance and subclass relationships.
|
|
364
|
+
`_structural_checking` is True by default, so that ProtocolRegistryMeta behaves like Protocol by default.
|
|
365
|
+
The chosen checking behavior is inherited by subclasses, but can be overridden by passing `structural_checking`
|
|
366
|
+
to the class definition.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
_structural_checking: bool = True
|
|
370
|
+
|
|
371
|
+
def __new__(
|
|
372
|
+
mcls,
|
|
373
|
+
name: str,
|
|
374
|
+
bases: tuple[type, ...],
|
|
375
|
+
namespace: dict[str, Any],
|
|
376
|
+
/,
|
|
377
|
+
structural_checking: bool | None = None,
|
|
378
|
+
**kwargs: Any, # noqa: ANN401
|
|
379
|
+
) -> ProtocolRegistryMeta[T]:
|
|
380
|
+
"""Create a new protocol registry class."""
|
|
381
|
+
del structural_checking
|
|
382
|
+
if Protocol not in bases and ProtocolRegistry in bases:
|
|
383
|
+
bases: tuple[type, ...] = (*bases, Protocol)
|
|
384
|
+
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
|
|
385
|
+
return cls
|
|
386
|
+
|
|
387
|
+
def __init__(
|
|
388
|
+
cls,
|
|
389
|
+
name: str,
|
|
390
|
+
bases: tuple[type, ...],
|
|
391
|
+
namespace: dict[str, Any],
|
|
392
|
+
/,
|
|
393
|
+
structural_checking: bool | None = None,
|
|
394
|
+
**kwargs: Any, # noqa: ANN401
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Initialize the protocol registry class."""
|
|
397
|
+
super().__init__(name, bases, namespace, **kwargs)
|
|
398
|
+
if structural_checking is not None:
|
|
399
|
+
cls._structural_checking = structural_checking
|
|
400
|
+
else:
|
|
401
|
+
structural_checking = cls._structural_checking
|
|
402
|
+
|
|
403
|
+
if not structural_checking:
|
|
404
|
+
# Override Protocol's __subclasshook__ with one that disables structural checking
|
|
405
|
+
# and instead adds lazy subclass registration support to the hook.
|
|
406
|
+
|
|
407
|
+
# This has to be done here, because Protocol inspects the callstack of __subclasscheck__ to determine
|
|
408
|
+
# whether the subclass check is being called from an isinstance check coming from ABCMeta;
|
|
409
|
+
# see __allow_reckless_class_checks and _ProtocolMeta in typing for details.
|
|
410
|
+
# To align the behavior of ProtocolRegistry and Protocol, __subclasshook__ has to be used.
|
|
411
|
+
subclasshook: Callable[[RegistryMeta[T], type], bool] | classmethod | None = namespace.get(
|
|
412
|
+
"__subclasshook__"
|
|
413
|
+
)
|
|
414
|
+
if subclasshook is None:
|
|
415
|
+
subclasshook = _lazy_subclass_hook
|
|
416
|
+
else:
|
|
417
|
+
if isinstance(subclasshook, classmethod):
|
|
418
|
+
subclasshook = cast(
|
|
419
|
+
"Callable[[RegistryMeta[T], type], bool]",
|
|
420
|
+
subclasshook.__func__,
|
|
421
|
+
)
|
|
422
|
+
subclasshook = _lazy_subclass_hook_with_pre_hook(subclasshook)
|
|
423
|
+
cls.__subclasshook__ = subclasshook # ty:ignore[invalid-assignment]
|
|
424
|
+
|
|
425
|
+
if is_protocol(cls):
|
|
426
|
+
# Disable the gate that prevents Protocols from being checked via isinstance()
|
|
427
|
+
# In other words: Non-structural ProtocolRegistries should be runtime_checkable by default.
|
|
428
|
+
# For potential downsides of this approach, please consider cpython issue gh-113320.
|
|
429
|
+
runtime_checkable(cls)
|
|
430
|
+
|
|
431
|
+
protocol_attrs: set[str] | None = getattr(cls, "__protocol_attrs__", None)
|
|
432
|
+
|
|
433
|
+
if protocol_attrs is not None:
|
|
434
|
+
cls.__protocol_attrs__ = protocol_attrs - EXCLUDED_ATTRS
|
|
435
|
+
|
|
436
|
+
if hasattr(cls, "__instancehook__") and "__instancehook__" not in cls.__dict__:
|
|
437
|
+
# For Protocols, __instancehook__ should not be inherited to derived ProtocolRegistries.
|
|
438
|
+
# This mirrors the behavior of __subclasshook__ in Protocols.
|
|
439
|
+
cls.__instancehook__ = _nop_instancehook
|
|
440
|
+
|
|
441
|
+
def _register_lazy(cls, subclass_strings: set[str]) -> None:
|
|
442
|
+
if len(subclass_strings) > 0 and cls._structural_checking:
|
|
443
|
+
msg = "Lazy subclass registration not supported for ProtocolRegistry with structural_checking=True."
|
|
444
|
+
raise RuntimeError(msg)
|
|
445
|
+
cls._string_registry.update(subclass_strings)
|
|
446
|
+
|
|
447
|
+
def _non_registered_instancecheck(cls, instance: object) -> bool:
|
|
448
|
+
"""Check if an instance is an instance of cls without checking the registry."""
|
|
449
|
+
if not cls._structural_checking:
|
|
450
|
+
return ABCMeta.__instancecheck__(cls, instance)
|
|
451
|
+
return super()._non_registered_instancecheck(instance)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class Registry[T](metaclass=RegistryMeta):
|
|
455
|
+
"""Helper class to create registries without needing to use the metaclass mechanism explicitly."""
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class ProtocolRegistry[T](Protocol, metaclass=ProtocolRegistryMeta):
|
|
459
|
+
"""Helper class to create protocol registries without needing to use the metaclass mechanism explicitly."""
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class _RegistryAnnotator[T: object](Protocol):
|
|
463
|
+
"""Callable protocol for decorators that preserve input signature and change return type."""
|
|
464
|
+
|
|
465
|
+
def __call__[**In, Q](self, func: Callable[In, Q], /) -> Callable[In, Q]:
|
|
466
|
+
"""Decorate `func` while preserving its parameters."""
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def annotator[T: object](
|
|
470
|
+
registry_type: RegistryMeta[T],
|
|
471
|
+
autocast_builtins: bool = False,
|
|
472
|
+
raise_on_failure: bool = True,
|
|
473
|
+
) -> _RegistryAnnotator[T]:
|
|
474
|
+
"""Decorator to annotate the result of a function with a registry type.
|
|
475
|
+
|
|
476
|
+
This is useful for functions that return instances of a registry, but where the return type is not known statically,
|
|
477
|
+
e.g. because the function is a lazy dispatch function that can return different types.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
def decorator[**In, Q](func: Callable[In, Q]) -> Callable[In, Q]:
|
|
481
|
+
@functools.wraps(func)
|
|
482
|
+
def wrapper(*args: In.args, **kwargs: In.kwargs) -> Q:
|
|
483
|
+
res = func(*args, **kwargs)
|
|
484
|
+
try:
|
|
485
|
+
return registry_type.register_instance(res, autocast_builtins=autocast_builtins)
|
|
486
|
+
except RegistrationError:
|
|
487
|
+
if raise_on_failure:
|
|
488
|
+
raise
|
|
489
|
+
return res # If the result cannot be registered, return it as is.
|
|
490
|
+
|
|
491
|
+
return wrapper
|
|
492
|
+
|
|
493
|
+
return decorator
|