Skip to content

Commit

Permalink
Merge pull request #19 from DavidCEllis/better_inspect_detection
Browse files Browse the repository at this point in the history
Fix the inspect bug by generating a __signature__ with a descriptor.
DavidCEllis authored Jun 25, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 8dd945a + 6a7d252 commit ce5297b
Showing 3 changed files with 51 additions and 12 deletions.
47 changes: 36 additions & 11 deletions src/ducktools/classbuilder/__init__.py
Original file line number Diff line number Diff line change
@@ -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"<MethodMaker for {self.funcname!r} method>"

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


13 changes: 12 additions & 1 deletion src/ducktools/classbuilder/__init__.pyi
Original file line number Diff line number Diff line change
@@ -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,
3 changes: 3 additions & 0 deletions src/ducktools/classbuilder/prefab.pyi
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ce5297b

Please sign in to comment.