Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
- 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.
- `typing.deprecated` now gives a better error message if you pass a non-`str`
- Fix bug with using `@deprecated` on a mixin class. Inheriting from a
deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra.
- `@deprecated` now gives a better error message if you pass a non-`str`
argument to the `msg` parameter. Patch by Alex Waygood.
- Exclude `__match_args__` from `Protocol` members,
this is a backport of https://github.com/python/cpython/pull/110683
Expand Down
5 changes: 5 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
87 changes: 87 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,93 @@ 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

with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"):
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_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():
Expand Down
30 changes: 27 additions & 3 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2343,21 +2343,45 @@ def decorator(arg: _T, /) -> _T:
return arg
elif isinstance(arg, type):
original_new = arg.__new__
has_init = arg.__init__ is not object.__init__

@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):
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__)

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
# object'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__
Comment on lines +2376 to +2381
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 :)


arg.__deprecated__ = __new__.__deprecated__ = msg
__init_subclass__.__deprecated__ = msg
return arg
elif callable(arg):
@functools.wraps(arg)
Expand Down