Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Failed to inspect __new__ and __init_subclass__ methods generated by warnings.deprecated #119605

Open
XuehaiPan opened this issue May 27, 2024 · 2 comments
Labels
3.13 bugs and security fixes 3.14 new features, bugs and security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@XuehaiPan
Copy link
Contributor

XuehaiPan commented May 27, 2024

Bug report

Bug description:

PEP 702 – Marking deprecations using the type system introduces a new API warnings.deprecated for deprecation.

While decorating a class object, it will update the __new__ method:

cpython/Lib/warnings.py

Lines 589 to 603 in 2268289

original_new = arg.__new__
@functools.wraps(original_new)
def __new__(cls, *args, **kwargs):
if cls is arg:
warn(msg, category=category, stacklevel=stacklevel + 1)
if original_new is not object.__new__:
return original_new(cls, *args, **kwargs)
# Mirrors a similar check in object.__new__.
elif cls.__init__ is object.__init__ and (args or kwargs):
raise TypeError(f"{cls.__name__}() takes no arguments")
else:
return original_new(cls)
arg.__new__ = staticmethod(__new__)

and the __init_subclass__ method:

cpython/Lib/warnings.py

Lines 605 to 625 in 2268289

original_init_subclass = arg.__init_subclass__
# We need slightly different behavior if __init_subclass__
# is a bound method (likely if it was implemented in Python)
if isinstance(original_init_subclass, MethodType):
original_init_subclass = original_init_subclass.__func__
@functools.wraps(original_init_subclass)
def __init_subclass__(*args, **kwargs):
warn(msg, category=category, stacklevel=stacklevel + 1)
return original_init_subclass(*args, **kwargs)
arg.__init_subclass__ = classmethod(__init_subclass__)
# Or otherwise, which likely means it's a builtin such as
# object's implementation of __init_subclass__.
else:
@functools.wraps(original_init_subclass)
def __init_subclass__(*args, **kwargs):
warn(msg, category=category, stacklevel=stacklevel + 1)
return original_init_subclass(*args, **kwargs)
arg.__init_subclass__ = __init_subclass__

For a class (cls) that does not implement the __new__ and __init_subclass__ methods, the warnings.deprecated decorator will generate these two methods (based on object.__new__ and type.__init_subclass__ ) and assign them to cls.__dict__.

However, if users want to inspect the methods in cls.__dict__, the inspect module fails to get the correct definition of cls.__new__. It is defined in warnings.py rather than object.__new__.

# test.py

from warnings import deprecated  # or from typing_extensions import deprecated


class Foo:
    def __new__(cls):
        return super().__new__(cls)


@deprecated("test")
class Bar:
    pass
In [1]: import inspect

In [2]: from test import Foo, Bar

In [3]: inspect.getsourcelines(Foo.__new__)
Out[3]: (['    def __new__(cls):\n', '        return super().__new__(cls)\n'], 7)

In [4]: inspect.getsourcelines(Bar.__new__) 
TypeError: module, class, method, function, traceback, frame, or code object was expected, got builtin_function_or_method

Expected to have inspect.getsourcelines(Bar.__new__) to be source code in /.../lib/python3.13/warnings.py.

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Linked PRs

@XuehaiPan XuehaiPan added the type-bug An unexpected behavior, bug, or error label May 27, 2024
@AlexWaygood AlexWaygood added stdlib Python modules in the Lib dir 3.13 bugs and security fixes 3.14 new features, bugs and security fixes labels May 27, 2024
@XuehaiPan
Copy link
Contributor Author

inspect.signature(DeprecatedClass) is also related.

cpython/Lib/inspect.py

Lines 2574 to 2588 in 2268289

new = _signature_get_user_defined_method(obj, '__new__')
init = _signature_get_user_defined_method(obj, '__init__')
# Go through the MRO and see if any class has user-defined
# pure Python __new__ or __init__ method
for base in obj.__mro__:
# Now we check if the 'obj' class has an own '__new__' method
if new is not None and '__new__' in base.__dict__:
sig = _get_signature_of(new)
if skip_bound_arg:
sig = _signature_bound_method(sig)
return sig
# or an own '__init__' method
elif init is not None and '__init__' in base.__dict__:
return _get_signature_of(init)

Reproduce snippet:

# test.py

from warnings import deprecated  # or from typing_extensions import deprecated


class Foo:
    def __init__(self, x, y):
        self.x = x
        self.y = y


@deprecated("test")
class Bar:
    def __init__(self, x, y):
        self.x = x
        self.y = y
In [1]: import inspect

In [2]: from test import Foo, Bar

In [3]: inspect.signature(Foo)
Out[3]: <Signature (x, y)>

In [4]: inspect.signature(Bar)
Out[4]: <Signature (*args, **kwargs)>

@XuehaiPan
Copy link
Contributor Author

I got a workaround to patch the __text_signature__ attribute of the generated __new__ and __init_subclass__ methods:

+ def update_signature(original_func):
+     # Ensure that the signature of the decorated callable matches the original one
+
+     def wrapper(func):
+         import inspect
+
+         try:
+             original_signature = inspect.signature(original_func)
+         except ValueError:
+             pass
+         else:
+             signature = inspect.signature(func)
+             if signature != original_signature:
+                 try:
+                     func.__text_signature__ = str(original_signature)
+                 except (AttributeError, TypeError):
+                     pass
+
+         return func
+
+     return wrapper

  if isinstance(arg, type):
      import functools
      from types import MethodType

      original_new = arg.__new__

+     @update_signature(original_new)
      @functools.wraps(original_new)
      def __new__(cls, *args, **kwargs):
          if cls is arg:
              warn(msg, category=category, stacklevel=stacklevel + 1)
          if original_new is not object.__new__:
              return original_new(cls, *args, **kwargs)
          # Mirrors a similar check in object.__new__.
          elif cls.__init__ is object.__init__ and (args or kwargs):
              raise TypeError(f"{cls.__name__}() takes no arguments")
          else:
              return original_new(cls)

      arg.__new__ = staticmethod(__new__)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.13 bugs and security fixes 3.14 new features, bugs and security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

2 participants