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 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