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
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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. Inheriting from a
deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra.

# Release 4.8.0 (September 17, 2023)

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
52 changes: 52 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,58 @@ 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_function(self):
@deprecated("b will go away soon")
def b():
Expand Down
17 changes: 13 additions & 4 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2337,21 +2337,30 @@ 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):
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__ = arg
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
__init_subclass__.__deprecated__ = msg
return arg
elif callable(arg):
@functools.wraps(arg)
Expand Down