From d91f56af2673a9431579cf4aaff47c64985b7fd8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 28 Oct 2023 08:21:58 -0700 Subject: [PATCH 1/7] Fix deprecating a mixin --- CHANGELOG.md | 1 + src/test_typing_extensions.py | 17 +++++++++++++++++ src/typing_extensions.py | 3 +++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ecde9cf..5558569c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - All parameters on `NewType.__call__` are now positional-only. This means that the signature of `typing_extensions.NewType.__call__` now exactly matches the signature of `typing.NewType.__call__`. Patch by Alex Waygood. +- Fix bug with using `@deprecated` on a mixin class. Patch by Jelle Zijlstra. # Release 4.8.0 (September 17, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7d8e2553..438a0316 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,3 +1,4 @@ +from ast import Pass import sys import os import abc @@ -418,6 +419,22 @@ def __new__(cls, x): self.assertEqual(instance.x, 42) self.assertTrue(new_called) + def test_mixin_class(self): + @deprecated("Mixin will go away soon") + class Mixin: + pass + + class Base: + def __init__(self, a) -> None: + self.a = a + + class Child(Base, Mixin): + pass + + with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"): + instance = Child(42) + self.assertEqual(instance.a, 42) + def test_function(self): @deprecated("b will go away soon") def b(): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 58706dc9..2bcdaedf 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2346,6 +2346,9 @@ def __new__(cls, *args, **kwargs): return original_new(cls, *args, **kwargs) # Mirrors a similar check in object.__new__. elif not has_init and (args or kwargs): + # Non-deprecated child class of a deprecated class + if cls.__init__ is not arg.__init__: + return original_new(cls) raise TypeError(f"{cls.__name__}() takes no arguments") else: return original_new(cls) From 6883ffe6755e4680c6900adc5396c72073c863c4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 28 Oct 2023 08:24:04 -0700 Subject: [PATCH 2/7] oops --- src/test_typing_extensions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 438a0316..52e61c94 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,4 +1,3 @@ -from ast import Pass import sys import os import abc From ddb1fc9942f38f5f99502bef18650071640c5a7a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 28 Oct 2023 08:37:30 -0700 Subject: [PATCH 3/7] Alternative solution --- CHANGELOG.md | 3 ++- doc/index.rst | 5 ++++ src/test_typing_extensions.py | 44 +++++++++++++++++++++++++++++++---- src/typing_extensions.py | 19 +++++++++------ 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5558569c..418280d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ - All parameters on `NewType.__call__` are now positional-only. This means that the signature of `typing_extensions.NewType.__call__` now exactly matches the signature of `typing.NewType.__call__`. Patch by Alex Waygood. -- Fix bug with using `@deprecated` on a mixin class. Patch by Jelle Zijlstra. +- Fix bug with using `@deprecated` on a mixin class. Inheriting from a + deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra. # Release 4.8.0 (September 17, 2023) diff --git a/doc/index.rst b/doc/index.rst index 28b795a3..39885861 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -553,6 +553,11 @@ Decorators .. versionadded:: 4.5.0 + .. versionchanged:: 4.9.0 + + Inheriting from a deprecated class now also raises a runtime + :py:exc:`DeprecationWarning`. + .. decorator:: final See :py:func:`typing.final` and :pep:`591`. In ``typing`` since 3.8. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 52e61c94..fcd04932 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -427,13 +427,49 @@ class Base: def __init__(self, a) -> None: self.a = a - class Child(Base, Mixin): - pass - with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"): - instance = Child(42) + class Child(Base, Mixin): + pass + + instance = Child(42) self.assertEqual(instance.a, 42) + def test_existing_init_subclass(self): + @deprecated("C will go away soon") + class C: + def __init_subclass__(cls) -> None: + cls.inited = True + + with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"): + C() + + with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"): + class D(C): + pass + + self.assertTrue(D.inited) + self.assertIsInstance(D(), D) # no deprecation + + def test_existing_init_subclass_in_base(self): + class Base: + def __init_subclass__(cls, x) -> None: + cls.inited = x + + @deprecated("C will go away soon") + class C(Base, x=42): + pass + + self.assertEqual(C.inited, 42) + + with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"): + C() + + with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"): + class D(C, x=3): + pass + + self.assertEqual(D.inited, 3) + def test_function(self): @deprecated("b will go away soon") def b(): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 2bcdaedf..ec8656a6 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2337,24 +2337,29 @@ def decorator(arg: _T, /) -> _T: return arg elif isinstance(arg, type): original_new = arg.__new__ - has_init = arg.__init__ is not object.__init__ + original_init_subclass = arg.__init_subclass__ @functools.wraps(original_new) def __new__(cls, *args, **kwargs): - warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + if cls is arg: + warnings.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 not has_init and (args or kwargs): - # Non-deprecated child class of a deprecated class - if cls.__init__ is not arg.__init__: - return original_new(cls) + 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__) - arg.__deprecated__ = __new__.__deprecated__ = msg + + @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__ = __init_subclass__.__deprecated__ = msg return arg elif callable(arg): @functools.wraps(arg) From 57ba348402cc75cc998a369dc46dd8f7ee9fee5e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 28 Oct 2023 08:40:22 -0700 Subject: [PATCH 4/7] long line --- src/typing_extensions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ec8656a6..7f34713e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2359,7 +2359,8 @@ def __init_subclass__(*args, **kwargs): return original_init_subclass(*args, **kwargs) arg.__init_subclass__ = __init_subclass__ - arg.__deprecated__ = __new__.__deprecated__ = __init_subclass__.__deprecated__ = msg + arg.__deprecated__ = __new__.__deprecated__ = arg + __init_subclass__.__deprecated__ = msg return arg elif callable(arg): @functools.wraps(arg) From 6809f6ac756b8e94c64baa369ef28602e05ec131 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 28 Oct 2023 09:43:59 -0700 Subject: [PATCH 5/7] Update src/typing_extensions.py Co-authored-by: Alex Waygood --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 7f34713e..6a0015ae 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2359,7 +2359,7 @@ def __init_subclass__(*args, **kwargs): return original_init_subclass(*args, **kwargs) arg.__init_subclass__ = __init_subclass__ - arg.__deprecated__ = __new__.__deprecated__ = arg + arg.__deprecated__ = __new__.__deprecated__ = msg __init_subclass__.__deprecated__ = msg return arg elif callable(arg): From 6504048b3956d28eec6c8db87d5140659f09c97d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Nov 2023 20:49:19 -0700 Subject: [PATCH 6/7] Fix __init_subclass__ --- src/test_typing_extensions.py | 35 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 27 +++++++++++++++++++++------ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index fcd04932..33724b1a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -470,6 +470,41 @@ class D(C, x=3): self.assertEqual(D.inited, 3) + def test_init_subclass_has_correct_cls(self): + init_subclass_saw = None + + @deprecated("Base will go away soon") + class Base: + def __init_subclass__(cls) -> None: + nonlocal init_subclass_saw + init_subclass_saw = cls + + self.assertIsNone(init_subclass_saw) + + with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"): + class C(Base): + pass + + self.assertIs(init_subclass_saw, C) + + def test_init_subclass_with_explicit_classmethod(self): + init_subclass_saw = None + + @deprecated("Base will go away soon") + class Base: + @classmethod + def __init_subclass__(cls) -> None: + nonlocal init_subclass_saw + init_subclass_saw = cls + + self.assertIsNone(init_subclass_saw) + + with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"): + class C(Base): + pass + + self.assertIs(init_subclass_saw, C) + def test_function(self): @deprecated("b will go away soon") def b(): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 6a0015ae..6245a3c9 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2337,7 +2337,6 @@ def decorator(arg: _T, /) -> _T: return arg elif isinstance(arg, type): original_new = arg.__new__ - original_init_subclass = arg.__init_subclass__ @functools.wraps(original_new) def __new__(cls, *args, **kwargs): @@ -2353,12 +2352,28 @@ def __new__(cls, *args, **kwargs): arg.__new__ = staticmethod(__new__) - @functools.wraps(original_init_subclass) - def __init_subclass__(*args, **kwargs): - warnings.warn(msg, category=category, stacklevel=stacklevel + 1) - return original_init_subclass(*args, **kwargs) + 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, _types.MethodType): + original_init_subclass = original_init_subclass.__func__ + + @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__ = classmethod(__init_subclass__) + # Or otherwise, which likely means it's a builtin such as + # type's implementation of __init_subclass__. + else: + @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.__init_subclass__ = __init_subclass__ arg.__deprecated__ = __new__.__deprecated__ = msg __init_subclass__.__deprecated__ = msg return arg From 01529c986fccd475849b548b286881e73bc23e8a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 4 Nov 2023 13:51:38 -0700 Subject: [PATCH 7/7] Update src/typing_extensions.py Co-authored-by: Alex Waygood --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 36075441..c8c6853b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2371,7 +2371,7 @@ def __init_subclass__(*args, **kwargs): arg.__init_subclass__ = classmethod(__init_subclass__) # Or otherwise, which likely means it's a builtin such as - # type's implementation of __init_subclass__. + # object's implementation of __init_subclass__. else: @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs):