I frequently develop ORM libraries using the peewee framework. On one particularly large ORM library, representing a proprietary data format, I have a lot of coupled ORM models representing different nodes in an XML document. The library also provides de-/serialization methods for each ORM model from/to JSON and PyXB DOMs.
Since subordinate models need to known superordinate models for the foreign keys to be defined, I have this one-way dependency. Alas, also superordinate models need to know the subordinate models for the de-/serializations. To avoid putting all > 10,000 LOC into one humongous Python unit, I outsourced the DOM de-/serialization into mixin classes for each of the models into separate units, accessing the subordinate modules via the foreign keys of the deferred implementation, which avoids the cyclic imports.
I have not yet found a better solution to this problem. However, this means, that I do not have the implementated classes available on the respective module level, which again means that I cannot use them for type hinting. Hence, I use strings of the respective class names for type hinting:
# Class Foo not available here, since it implements FooMixin later. class FooJSONMixin: @classmethod def from_json(...) -> 'Foo': ...
However, this causes, that I dont't get the actual types in the method signatures in
pydoc, which I want.
Hence, I implemented a small library exposing a decorator to resolve type hint strings to the actual objects. I'd appreciate your feedback on this, since I deem it rather hacky.
"""Enhanced type hinting hacks.""" from typing import _UnionGenericAlias from typing import Any from typing import ForwardRef from typing import Iterable __all__ = ['resolve_type_hints'] def _resolve_union_generic_alias( uga: _UnionGenericAlias, mapping: dict[str, Any] ) -> Any: """Resolve the type.""" return _UnionGenericAlias( uga.__origin__, tuple(_resolve_type_hint(a, mapping) for a in uga.__args__), inst=uga._inst, # pylint: disable=W0212 name=uga._name, # pylint: disable=W0212 _typevar_types=uga._typevar_types, # pylint: disable=W0212 _paramspec_tvars=uga._paramspec_tvars # pylint: disable=W0212 ) def _resolve_type_hint(value: Any, mapping: dict[str, Any]) -> Any: """Resolves a type hint.""" if isinstance(value, str): return mapping.get(value, value) if isinstance(value, ForwardRef): return _resolve_type_hint(value.__forward_arg__, mapping) if type(value) is _UnionGenericAlias: # pylint: disable=C0123 return _resolve_union_generic_alias(value, mapping) return value def _resolve_type_hints( annotations: dict[str, Any], mapping: dict[str, Any] ) -> None: """Resolves type hints of the given annotations dict.""" for key, value in dict(annotations).items(): annotations[key] = _resolve_type_hint(value, mapping) def _resolve_mro(mro: Iterable[type], mapping: dict[str, Any]) -> None: """Resolve type hints of a method resolution order.""" for cls in mro: for attribute in cls.__dict__: try: annotations = getattr(cls, attribute).__annotations__ except AttributeError: continue _resolve_type_hints(annotations, mapping) def resolve_type_hints(obj: type) -> type: """Decorator to resolves type hints on classes and functions.""" mapping = globals() mapping[obj.__name__] = obj try: mro = obj.__mro__ except AttributeError: _resolve_type_hints(obj.__annotations__, mapping) else: _resolve_mro(mro, mapping) return obj
from typing import Optional, Union class Model: pass class TestMixin: @classmethod def foo(cls) -> 'Test': return cls() @resolve_type_hints class Test(Model, TestMixin): pass @resolve_type_hints def test(a: Optional['Test'], b: Union['Test', int]) -> 'Test': pass