diff --git a/src/ducktools/classbuilder/__init__.py b/src/ducktools/classbuilder/__init__.py index ee79e06..285ea21 100644 --- a/src/ducktools/classbuilder/__init__.py +++ b/src/ducktools/classbuilder/__init__.py @@ -34,7 +34,7 @@ from .annotations import get_ns_annotations, is_classvar -__version__ = "v0.6.1" +__version__ = "v0.6.2" # Change this name if you make heavy modifications INTERNALS_DICT = "__classbuilder_internals__" @@ -158,14 +158,7 @@ def __init__(self, funcname, code_generator): def __repr__(self): return f"" - def __get__(self, obj, objtype=None): - if objtype is None or issubclass(objtype, type): - # Called with get(ourclass, type(ourclass)) - cls = obj - else: - # Called with get(inst | None, ourclass) - cls = objtype - + def __get__(self, inst, cls): local_vars = {} gen = self.code_generator(cls, self.funcname) exec(gen.source_code, gen.globs, local_vars) @@ -183,7 +176,31 @@ def __get__(self, obj, objtype=None): # Use 'get' to return the generated function as a bound method # instead of as a regular function for first usage. - return method.__get__(obj, objtype) + return method.__get__(inst, cls) + + +class _SignatureMaker: + # 'inspect.signature' calls the `__get__` method of the `__init__` methodmaker with + # the wrong arguments. + # Instead of __get__(None, cls) or __get__(inst, type(inst)) + # it uses __get__(cls, type(cls)). + # + # If this is done before `__init__` has been generated then + # help(cls) will fail along with inspect.signature(cls) + # This signature maker descriptor is placed to override __signature__ and force + # the `__init__` signature to be generated first if the signature is requested. + def __get__(self, instance, cls): + import inspect # Deferred inspect import + _ = cls.__init__ # force generation of `__init__` function + # Remove this attribute from the class + # This prevents recursion back into this __get__ method. + delattr(cls, "__signature__") + sig = inspect.signature(cls) + setattr(cls, "__signature__", sig) + return sig + + +signature_maker = _SignatureMaker() def get_init_generator(null=NOTHING, extra_code=None): @@ -402,7 +419,7 @@ def frozen_delattr_generator(cls, funcname="__delattr__"): ) -def builder(cls=None, /, *, gatherer, methods, flags=None): +def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True): """ The main builder for class generation @@ -413,6 +430,10 @@ def builder(cls=None, /, *, gatherer, methods, flags=None): :type methods: set[MethodMaker] :param flags: additional flags to store in the internals dictionary for use by method generators. + :type flags: None | dict[str, bool] + :param fix_signature: Add a __signature__ attribute to work-around an issue with + inspect.signature incorrectly handling __init__ descriptors. + :type fix_signature: bool :return: The modified class (the class itself is modified, but this is expected). """ # Handle `None` to make wrapping with a decorator easier. @@ -459,6 +480,10 @@ def builder(cls=None, /, *, gatherer, methods, flags=None): internals["methods"] = _MappingProxyType(internal_methods) + # Fix for inspect.signature(cls) + if fix_signature: + setattr(cls, "__signature__", signature_maker) + return cls diff --git a/src/ducktools/classbuilder/__init__.pyi b/src/ducktools/classbuilder/__init__.pyi index 5e8e9e1..eb25f10 100644 --- a/src/ducktools/classbuilder/__init__.pyi +++ b/src/ducktools/classbuilder/__init__.pyi @@ -1,6 +1,8 @@ import types import typing +import inspect + from collections.abc import Callable from types import MappingProxyType from typing_extensions import dataclass_transform @@ -49,7 +51,12 @@ class MethodMaker: code_generator: _CodegenType def __init__(self, funcname: str, code_generator: _CodegenType) -> None: ... def __repr__(self) -> str: ... - def __get__(self, instance, cls=None) -> Callable: ... + def __get__(self, instance, cls) -> Callable: ... + +class _SignatureMaker: + def __get__(self, instance, cls) -> inspect.Signature: ... + +signature_maker: _SignatureMaker def get_init_generator( null: _NothingType = NOTHING, @@ -86,6 +93,7 @@ def builder( gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]], methods: frozenset[MethodMaker] | set[MethodMaker], flags: dict[str, bool] | None = None, + fix_signature: bool = ..., ) -> type[_T]: ... @typing.overload @@ -96,6 +104,7 @@ def builder( gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]], methods: frozenset[MethodMaker] | set[MethodMaker], flags: dict[str, bool] | None = None, + fix_signature: bool = ..., ) -> Callable[[type[_T]], type[_T]]: ... @@ -126,6 +135,7 @@ class Field(metaclass=SlotMakerMeta): __slots__: dict[str, str] __classbuilder_internals__: dict + __signature__: inspect.Signature def __init__( self, @@ -249,6 +259,7 @@ class GatheredFields: modifications: dict[str, typing.Any] __classbuilder_internals__: dict + __signature__: inspect.Signature def __init__( self, diff --git a/src/ducktools/classbuilder/prefab.pyi b/src/ducktools/classbuilder/prefab.pyi index 0c54a7a..da9718e 100644 --- a/src/ducktools/classbuilder/prefab.pyi +++ b/src/ducktools/classbuilder/prefab.pyi @@ -2,6 +2,8 @@ import typing from types import MappingProxyType from typing_extensions import dataclass_transform +import inspect + from collections.abc import Callable from . import ( @@ -42,6 +44,7 @@ asdict_maker: MethodMaker class Attribute(Field): __slots__: dict + __signature__: inspect.Signature iter: bool serialize: bool