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

Fix deprecating a mixin #294

Merged
merged 8 commits into from
Nov 4, 2023
Merged

Conversation

JelleZijlstra
Copy link
Member

Fixes #251

@JelleZijlstra
Copy link
Member Author

I realized we could do something smarter: inject __init_subclass__ to warn when creating subclasses of a deprecated class, and then change the injected __new__ to warn only if the class being instantiated is the deprecated class, not a subclass.

Comment on lines 2356 to 2363
@functools.wraps(original_init_subclass)
def __init_subclass__(*args, **kwargs):
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
return original_init_subclass(*args, **kwargs)

arg.__init_subclass__ = __init_subclass__
arg.__deprecated__ = __new__.__deprecated__ = msg
__init_subclass__.__deprecated__ = msg
Copy link
Member

@AlexWaygood AlexWaygood Oct 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't always do the right thing, because of the fact that __init_subclass__ is an implicit classmethod. For example:

Python 3.11.5 (tags/v3.11.5:cce6ba9, Aug 24 2023, 14:38:34) [MSC v.1936 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import typing_extensions as t
>>> @t.deprecated('DEPRECATED')
... class Foo:
...     ATTR = 1
...     def __init_subclass__(cls, *args, **kwargs):
...         print(f"{cls=}, {cls.ATTR=}")
...
>>> class Bar:
...     ATTR = 2
...
>>> class Baz(Bar, Foo): ...
...
<stdin>:1: DeprecationWarning: DEPRECATED
cls=<class '__main__.Foo'>, cls.ATTR=1
>>> Baz.ATTR
2

You can fix it by doing this (diff is relative to your PR branch):

diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index 6a0015a..37ef575 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -2337,7 +2337,7 @@ else:
                 return arg
             elif isinstance(arg, type):
                 original_new = arg.__new__
-                original_init_subclass = arg.__init_subclass__
+                original_init_subclass = arg.__init_subclass__.__func__

                 @functools.wraps(original_new)
                 def __new__(cls, *args, **kwargs):
@@ -2358,7 +2358,7 @@ else:
                     warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
                     return original_init_subclass(*args, **kwargs)

-                arg.__init_subclass__ = __init_subclass__
+                arg.__init_subclass__ = classmethod(__init_subclass__)
                 arg.__deprecated__ = __new__.__deprecated__ = msg
                 __init_subclass__.__deprecated__ = msg
                 return arg

With this change, we get the correct behaviour:

>>> import typing_extensions as t
>>> @t.deprecated('DEPRECATED')
... class Foo:
...     ATTR = 1
...     def __init_subclass__(cls, *args, **kwargs):
...         print(f"{cls=}, {cls.ATTR=}")
...
>>> class Bar:
...     ATTR = 2
...
>>> class Baz(Bar, Foo): ...
...
<stdin>:1: DeprecationWarning: DEPRECATED
cls=<class '__main__.Baz'>, cls.ATTR=2

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. Your solution isn't quite right as it doesn't work for classes without a Python __init_subclass__, but I got something to work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, thanks -- I checked my solution worked in the REPL, but didn't actually run the test suite with my changes :) My bad!

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Comment on lines +2376 to +2381
@functools.wraps(original_init_subclass)
def __init_subclass__(*args, **kwargs):
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
return original_init_subclass(*args, **kwargs)

arg.__init_subclass__ = __init_subclass__
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how often we hit this branch? Is it only when original_init_subclass is object.__init_subclass__? But I have no actual problem to point out here, I guess I'm just slightly queasy about how well it's tested right now :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite a lot, I think five or so tests failed before I implemented this.

Copy link
Member

@AlexWaygood AlexWaygood Nov 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I know — my (poorly expressed) point was that they were all failing for the same reason (object.__init_subclass__ not being a classmethod) — I was curious whether there were any other realistic situations that would lead to us ending up in this branch :)

@JelleZijlstra JelleZijlstra merged commit f9f257c into python:main Nov 4, 2023
17 checks passed
@JelleZijlstra JelleZijlstra deleted the deprmixin branch November 4, 2023 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

@deprecated breaking __init__ inheritance when used with mixins/multiple inheritance
2 participants