-
-
Notifications
You must be signed in to change notification settings - Fork 111
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
Conversation
I realized we could do something smarter: inject |
Co-authored-by: Alex Waygood <[email protected]>
src/typing_extensions.py
Outdated
@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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
@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__ |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
Co-authored-by: Alex Waygood <[email protected]>
Fixes #251