Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,14 @@ def check_callable_call(

See the docstring of check_call for more information.
"""
# Check implicit calls to deprecated class constructors.
# Only the non-overload case is handled here. Overloaded constructors are handled
# separately during overload resolution. `callable_node` is `None` for an overload
# item so deprecation checks are not duplicated.
if isinstance(callable_node, RefExpr) and isinstance(callable_node.node, TypeInfo):
self.chk.check_deprecated(callable_node.node.get_method("__new__"), context)
self.chk.check_deprecated(callable_node.node.get_method("__init__"), context)

# Always unpack **kwargs before checking a call.
callee = callee.with_unpacked_kwargs().with_normalized_var_args()
if callable_name is None and callee.name:
Expand Down
43 changes: 38 additions & 5 deletions test-data/unit/check-deprecated.test
Original file line number Diff line number Diff line change
Expand Up @@ -315,18 +315,51 @@ class E: ...
[builtins fixtures/tuple.pyi]


[case testDeprecatedClassInitMethod]
[case testDeprecatedClassConstructor]
# flags: --enable-error-code=deprecated

from typing_extensions import deprecated

@deprecated("use C2 instead")
class C:
@deprecated("call `make_c()` instead")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a similar test (if there isn't one) in incremental mode? .definition is a little magic (it isn't serialized in cache and is restored in fixup.py instead) - that should not be problematic here, but better have it than discover yet another cache trouble a week later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5302af3

def __init__(self) -> None: ...
@classmethod
def make_c(cls) -> C: ...

c: C # E: class __main__.C is deprecated: use C2 instead
C() # E: class __main__.C is deprecated: use C2 instead
C.__init__(c) # E: class __main__.C is deprecated: use C2 instead
C() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead

class D:
@deprecated("call `make_d()` instead")
def __new__(cls) -> D: ...
@classmethod
def make_d(cls) -> D: ...

D() # E: function __main__.D.__new__ is deprecated: call `make_d()` instead

[builtins fixtures/tuple.pyi]


[case testDeprecatedSuperClassConstructor]
# flags: --enable-error-code=deprecated

from typing_extensions import deprecated, Self

class A:
@deprecated("call `self.initialise()` instead")
def __init__(self) -> None: ...
def initialise(self) -> None: ...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also check self.__init__() inside some A (should be flagged) and B (should not be flagged) methods? And maybe also standalone

a = A()
a.__init__()

Copy link
Contributor Author

@bzoracler bzoracler Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed 4be62c6 to address this...well kind of. I did not add checks for <instance>.__init__ or <instance>.__new__ (only <Class>.__init__ and <Class>.__new__):

  • <instance>.__init__ doesn't activate the code path for @deprecated() at all, mypy complains that accessing <instance>.__init__ is unsound and doesn't analyse the call expression further.
  • <instance>.__new__ has some weird complaint when any decorator is added; it looks like mypy thinks <instance>.__new__ is an instance method after decoration.

See mypy Playground.


In any case, this is still simply testing for deprecation warnings upon attribute access (the attribute just happens to be a class constructor method, and we can't Deprecate[] class/instance variables yet). I added tests covering general attribute access of methods accessed internally in 9e49aa1. The cases for accessing them externally already exist here:

[case testDeprecatedMethod]
# flags: --enable-error-code=deprecated
from typing_extensions import deprecated
class C:
@deprecated("use g instead")
def f(self) -> None: ...
def g(self) -> None: ...
@staticmethod
@deprecated("use g instead")
def h() -> None: ...
@deprecated("use g instead")
@staticmethod
def k() -> None: ...
C.f # E: function __main__.C.f is deprecated: use g instead
C().f # E: function __main__.C.f is deprecated: use g instead
C().f() # E: function __main__.C.f is deprecated: use g instead
C().f(1) # E: function __main__.C.f is deprecated: use g instead \
# E: Too many arguments for "f" of "C"
f = C().f # E: function __main__.C.f is deprecated: use g instead
f()
t = (C.f, C.f, C.g) # E: function __main__.C.f is deprecated: use g instead
C().g()
C().h() # E: function __main__.C.h is deprecated: use g instead
C().k() # E: function __main__.C.k is deprecated: use g instead


class B(A):
def __init__(self) -> None:
super().__init__() # E: function __main__.A.__init__ is deprecated: call `self.initialise()` instead

class C:
@deprecated("call `object.__new__(cls)` instead")
def __new__(cls) -> Self: ...

class D(C):
def __new__(cls) -> Self:
return super().__new__(cls) # E: function __main__.C.__new__ is deprecated: call `object.__new__(cls)` instead

[builtins fixtures/tuple.pyi]

Expand Down
Loading