Skip to content

Commit f9f257c

Browse files
Fix deprecating a mixin; warn when inheriting from a deprecated class (#294)
Co-authored-by: Alex Waygood <[email protected]>
1 parent fc9acbd commit f9f257c

File tree

4 files changed

+122
-4
lines changed

4 files changed

+122
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
- All parameters on `NewType.__call__` are now positional-only. This means that
44
the signature of `typing_extensions.NewType.__call__` now exactly matches the
55
signature of `typing.NewType.__call__`. Patch by Alex Waygood.
6-
- `typing.deprecated` now gives a better error message if you pass a non-`str`
6+
- Fix bug with using `@deprecated` on a mixin class. Inheriting from a
7+
deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra.
8+
- `@deprecated` now gives a better error message if you pass a non-`str`
79
argument to the `msg` parameter. Patch by Alex Waygood.
810
- Exclude `__match_args__` from `Protocol` members,
911
this is a backport of https://github.com/python/cpython/pull/110683

doc/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,11 @@ Decorators
553553

554554
.. versionadded:: 4.5.0
555555

556+
.. versionchanged:: 4.9.0
557+
558+
Inheriting from a deprecated class now also raises a runtime
559+
:py:exc:`DeprecationWarning`.
560+
556561
.. decorator:: final
557562

558563
See :py:func:`typing.final` and :pep:`591`. In ``typing`` since 3.8.

src/test_typing_extensions.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,93 @@ def __new__(cls, x):
418418
self.assertEqual(instance.x, 42)
419419
self.assertTrue(new_called)
420420

421+
def test_mixin_class(self):
422+
@deprecated("Mixin will go away soon")
423+
class Mixin:
424+
pass
425+
426+
class Base:
427+
def __init__(self, a) -> None:
428+
self.a = a
429+
430+
with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"):
431+
class Child(Base, Mixin):
432+
pass
433+
434+
instance = Child(42)
435+
self.assertEqual(instance.a, 42)
436+
437+
def test_existing_init_subclass(self):
438+
@deprecated("C will go away soon")
439+
class C:
440+
def __init_subclass__(cls) -> None:
441+
cls.inited = True
442+
443+
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
444+
C()
445+
446+
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
447+
class D(C):
448+
pass
449+
450+
self.assertTrue(D.inited)
451+
self.assertIsInstance(D(), D) # no deprecation
452+
453+
def test_existing_init_subclass_in_base(self):
454+
class Base:
455+
def __init_subclass__(cls, x) -> None:
456+
cls.inited = x
457+
458+
@deprecated("C will go away soon")
459+
class C(Base, x=42):
460+
pass
461+
462+
self.assertEqual(C.inited, 42)
463+
464+
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
465+
C()
466+
467+
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
468+
class D(C, x=3):
469+
pass
470+
471+
self.assertEqual(D.inited, 3)
472+
473+
def test_init_subclass_has_correct_cls(self):
474+
init_subclass_saw = None
475+
476+
@deprecated("Base will go away soon")
477+
class Base:
478+
def __init_subclass__(cls) -> None:
479+
nonlocal init_subclass_saw
480+
init_subclass_saw = cls
481+
482+
self.assertIsNone(init_subclass_saw)
483+
484+
with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
485+
class C(Base):
486+
pass
487+
488+
self.assertIs(init_subclass_saw, C)
489+
490+
def test_init_subclass_with_explicit_classmethod(self):
491+
init_subclass_saw = None
492+
493+
@deprecated("Base will go away soon")
494+
class Base:
495+
@classmethod
496+
def __init_subclass__(cls) -> None:
497+
nonlocal init_subclass_saw
498+
init_subclass_saw = cls
499+
500+
self.assertIsNone(init_subclass_saw)
501+
502+
with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
503+
class C(Base):
504+
pass
505+
506+
self.assertIs(init_subclass_saw, C)
507+
421508
def test_function(self):
422509
@deprecated("b will go away soon")
423510
def b():

src/typing_extensions.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2343,21 +2343,45 @@ def decorator(arg: _T, /) -> _T:
23432343
return arg
23442344
elif isinstance(arg, type):
23452345
original_new = arg.__new__
2346-
has_init = arg.__init__ is not object.__init__
23472346

23482347
@functools.wraps(original_new)
23492348
def __new__(cls, *args, **kwargs):
2350-
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
2349+
if cls is arg:
2350+
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
23512351
if original_new is not object.__new__:
23522352
return original_new(cls, *args, **kwargs)
23532353
# Mirrors a similar check in object.__new__.
2354-
elif not has_init and (args or kwargs):
2354+
elif cls.__init__ is object.__init__ and (args or kwargs):
23552355
raise TypeError(f"{cls.__name__}() takes no arguments")
23562356
else:
23572357
return original_new(cls)
23582358

23592359
arg.__new__ = staticmethod(__new__)
2360+
2361+
original_init_subclass = arg.__init_subclass__
2362+
# We need slightly different behavior if __init_subclass__
2363+
# is a bound method (likely if it was implemented in Python)
2364+
if isinstance(original_init_subclass, _types.MethodType):
2365+
original_init_subclass = original_init_subclass.__func__
2366+
2367+
@functools.wraps(original_init_subclass)
2368+
def __init_subclass__(*args, **kwargs):
2369+
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
2370+
return original_init_subclass(*args, **kwargs)
2371+
2372+
arg.__init_subclass__ = classmethod(__init_subclass__)
2373+
# Or otherwise, which likely means it's a builtin such as
2374+
# object's implementation of __init_subclass__.
2375+
else:
2376+
@functools.wraps(original_init_subclass)
2377+
def __init_subclass__(*args, **kwargs):
2378+
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
2379+
return original_init_subclass(*args, **kwargs)
2380+
2381+
arg.__init_subclass__ = __init_subclass__
2382+
23602383
arg.__deprecated__ = __new__.__deprecated__ = msg
2384+
__init_subclass__.__deprecated__ = msg
23612385
return arg
23622386
elif callable(arg):
23632387
@functools.wraps(arg)

0 commit comments

Comments
 (0)