From 0332484ba82821653db7913955804448419906ca Mon Sep 17 00:00:00 2001 From: bzoracler <50305397+bzoracler@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:42:30 +1300 Subject: [PATCH 01/11] feat: report `@deprecated()` on non-overloaded class constructors --- mypy/checkexpr.py | 8 ++++++ test-data/unit/check-deprecated.test | 43 ++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3eb54579a050..a0c2283ebf59 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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: diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 607e9d767956..98d0324faf10 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -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") 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: ... + +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] From d497f37d0119064db50628fbb67aeaa94eb834b8 Mon Sep 17 00:00:00 2001 From: bzoracler <50305397+bzoracler@users.noreply.github.com> Date: Sun, 26 Oct 2025 22:36:30 +1300 Subject: [PATCH 02/11] Add simple subclass tests --- test-data/unit/check-deprecated.test | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 98d0324faf10..384b0bc27d2c 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -326,7 +326,10 @@ class C: @classmethod def make_c(cls) -> C: ... +class C2(C): ... + C() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead +C2() # E: function.__main__.C.__init__ is deprecated: call `make_c()` instead class D: @deprecated("call `make_d()` instead") @@ -334,7 +337,10 @@ class D: @classmethod def make_d(cls) -> D: ... +class D2(D): ... + D() # E: function __main__.D.__new__ is deprecated: call `make_d()` instead +D2() # E: function __main__.D.__new__ is deprecated: call `make_d()` instead [builtins fixtures/tuple.pyi] From 53622b1413b7623ec57fd2d5dbf2eed4f5cc8726 Mon Sep 17 00:00:00 2001 From: bzoracler <50305397+bzoracler@users.noreply.github.com> Date: Sun, 26 Oct 2025 23:04:31 +1300 Subject: [PATCH 03/11] fix error message typo --- test-data/unit/check-deprecated.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 384b0bc27d2c..c32d8a70a5b6 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -329,7 +329,7 @@ class C: class C2(C): ... C() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead -C2() # E: function.__main__.C.__init__ is deprecated: call `make_c()` instead +C2() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead class D: @deprecated("call `make_d()` instead") From a473e52aeef05115c1b92ce3404417a07e22c9ae Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 08:46:24 +1300 Subject: [PATCH 04/11] Save an unnecessary instance check --- mypy/checkexpr.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index a0c2283ebf59..ee0d7ed88761 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1664,14 +1664,6 @@ 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: @@ -1679,9 +1671,21 @@ def check_callable_call( ret_type = get_proper_type(callee.ret_type) if callee.is_type_obj() and isinstance(ret_type, Instance): callable_name = ret_type.type.fullname - if isinstance(callable_node, RefExpr) and callable_node.fullname in ENUM_BASES: - # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). - return callee.ret_type, callee + if isinstance(callable_node, RefExpr): + # 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. + callable_info: TypeInfo | None = None + if isinstance(callable_node.node, TypeInfo): + callable_info = callable_node.node + if callable_info is not None: + self.chk.check_deprecated(callable_node.node.get_method("__new__"), context) + self.chk.check_deprecated(callable_node.node.get_method("__init__"), context) + + if callable_node.fullname in ENUM_BASES: + # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). + return callee.ret_type, callee if ( callee.is_type_obj() From 8f08c60d9146857a2e7d7e91491bedfde218c2f3 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 08:47:02 +1300 Subject: [PATCH 05/11] Use the constructor definition chosen by mypy rather than walking 2 MROs --- mypy/checkexpr.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ee0d7ed88761..3cc08ef7b97c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1680,8 +1680,7 @@ def check_callable_call( if isinstance(callable_node.node, TypeInfo): callable_info = callable_node.node if callable_info is not None: - self.chk.check_deprecated(callable_node.node.get_method("__new__"), context) - self.chk.check_deprecated(callable_node.node.get_method("__init__"), context) + self.chk.check_deprecated(callee.definition, context) if callable_node.fullname in ENUM_BASES: # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). From b0ffd6b0acdb9d38e539093941a2d8c957e9fe8a Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 08:51:55 +1300 Subject: [PATCH 06/11] Warn deprecated constructor calls from old-style type aliases --- mypy/checkexpr.py | 6 ++++++ test-data/unit/check-deprecated.test | 29 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3cc08ef7b97c..cffee9cf1b0e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1679,6 +1679,12 @@ def check_callable_call( callable_info: TypeInfo | None = None if isinstance(callable_node.node, TypeInfo): callable_info = callable_node.node + elif ( + isinstance(callable_node.node, TypeAlias) + and isinstance(callable_node.node.target, Instance) + and isinstance(callable_node.node.target.type, TypeInfo) + ): + callable_info = callable_node.node.target.type if callable_info is not None: self.chk.check_deprecated(callee.definition, context) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index c32d8a70a5b6..8381fa25e2cd 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -370,6 +370,35 @@ class D(C): [builtins fixtures/tuple.pyi] +[case testDeprecatedClassConstructorCalledFromTypeAlias] +# flags: --enable-error-code=deprecated + +from typing_extensions import deprecated, TypeAlias + +class A: + @deprecated("do not use") + def __init__(self) -> None: ... + +class B(A): ... + +A_alias = A +A_explicit_alias: TypeAlias = A +B_alias = B +B_explicit_alias: TypeAlias = B + +A_alias() # E: function __main__.A.__init__ is deprecated: do not use +A_explicit_alias() # E: function __main__.A.__init__ is deprecated: do not use +B_alias() # E: function __main__.A.__init__ is deprecated: do not use +B_explicit_alias() # E: function __main__.A.__init__ is deprecated: do not use + +A_alias +A_explicit_alias +B_alias +B_explicit_alias + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedSpecialMethods] # flags: --enable-error-code=deprecated From c954f78ba4cb219e160feae22e0242b52c538baa Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 08:59:26 +1300 Subject: [PATCH 07/11] fix unexpanded type check error --- mypy/checkexpr.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index cffee9cf1b0e..7b4fbdcd0820 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1679,12 +1679,10 @@ def check_callable_call( callable_info: TypeInfo | None = None if isinstance(callable_node.node, TypeInfo): callable_info = callable_node.node - elif ( - isinstance(callable_node.node, TypeAlias) - and isinstance(callable_node.node.target, Instance) - and isinstance(callable_node.node.target.type, TypeInfo) - ): - callable_info = callable_node.node.target.type + elif isinstance(callable_node.node, TypeAlias): + alias_target = get_proper_type(callable_node.node.target) + if isinstance(alias_target, Instance) and isinstance(alias_target.type, TypeInfo): + callable_info = callable_node.node.target.type if callable_info is not None: self.chk.check_deprecated(callee.definition, context) From 00897abdb5021129db91a6c7b91d3c2d850823e0 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 09:00:04 +1300 Subject: [PATCH 08/11] fix unexpanded type check error --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 7b4fbdcd0820..c0edb7780cfd 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1682,7 +1682,7 @@ def check_callable_call( elif isinstance(callable_node.node, TypeAlias): alias_target = get_proper_type(callable_node.node.target) if isinstance(alias_target, Instance) and isinstance(alias_target.type, TypeInfo): - callable_info = callable_node.node.target.type + callable_info = alias_target.type if callable_info is not None: self.chk.check_deprecated(callee.definition, context) From 39e752f1c7bb0e5aa6f7417015f9c31db7b6ea24 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 12:02:15 +1300 Subject: [PATCH 09/11] Warn usage of types with deprecated constructors in callable-like contexts --- mypy/checkexpr.py | 62 ++++++++- test-data/unit/check-deprecated.test | 201 +++++++++++++++++++++++++++ 2 files changed, 262 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c0edb7780cfd..0d07ecf81cce 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -102,7 +102,7 @@ YieldExpr, YieldFromExpr, ) -from mypy.options import PRECISE_TUPLE_TYPES +from mypy.options import PRECISE_TUPLE_TYPES, Options from mypy.plugin import ( FunctionContext, FunctionSigContext, @@ -376,6 +376,16 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: # generating extra type errors. result = AnyType(TypeOfAny.from_error) if isinstance(node, TypeInfo): + if self.type_context[-1] is not None: + proper_result = get_proper_type(result) + if isinstance(proper_result, (CallableType, Overloaded)): + ctor_type = constructor_type_in_callable_context( + proper_result, + get_proper_type(self.type_context[-1]), + self.chk.options, + ) + if ctor_type is not None: + self.chk.check_deprecated(ctor_type.definition, e) if isinstance(result, CallableType) and isinstance( # type: ignore[misc] result.ret_type, Instance ): @@ -6782,3 +6792,53 @@ def is_type_type_context(context: Type | None) -> bool: if isinstance(context, UnionType): return any(is_type_type_context(item) for item in context.items) return False + + +def constructor_type_in_callable_context( + constructor_type: CallableType | Overloaded, + context: ProperType, + options: Options, + /, + *, + _check_subtyping: bool = False, +) -> CallableType | None: + """ + Gets a class constructor type if it's used in a valid callable type context. + Considers the following cases as valid contexts: + + * A plain `Callable` context is always treated as a valid context. + * A union type context requires at least one of the union items to be a supertype of + the class type, in addition to being a `Callable` or callable `Protocol`. + * A callable `Protocol` context is only treated as a valid context if the + constructor type is a subtype of the protocol or overloaded type. + + If the class type is overloaded, use the first overload which is in a valid context. + """ + + item: Type + if isinstance(constructor_type, Overloaded): + for item in constructor_type.items: + result = constructor_type_in_callable_context( + item, context, options, _check_subtyping=True + ) + if result is not None: + return result + elif isinstance(context, CallableType): + if (not _check_subtyping) or is_subtype(constructor_type, context, options=options): + return constructor_type + elif isinstance(context, UnionType): + for item in context.items: + result = constructor_type_in_callable_context( + constructor_type, get_proper_type(item), options, _check_subtyping=True + ) + if result is not None: + return result + elif isinstance(context, Instance): + if ( + context.type.is_protocol + and ("__call__" in context.type.protocol_members) + and is_subtype(constructor_type, context, options=options) + ): + return constructor_type + + return None diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 8381fa25e2cd..dc9e4d3d12a4 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -399,6 +399,207 @@ B_explicit_alias [builtins fixtures/tuple.pyi] +[case testDeprecatedClassConstructorInCallableTypeContext] +# flags: --enable-error-code=deprecated + +from typing import Any, Callable, ClassVar, TypeVar, Generic +from typing_extensions import deprecated + +class A: + @deprecated("do not use") + def __init__(self) -> None: ... + +def receives_callable(c: Callable[..., Any], /) -> None: ... +callable_receiver: Callable[[Callable[..., Any]], None] + +T = TypeVar("T") + +class CallableAttr(Generic[T]): + def __get__(self, instance: Settable[T], owner: type[Settable[T]], /) -> T: ... + def __set__(self, instance: Settable[T], value: T, /) -> None: ... + +class Settable(Generic[T]): + instance: T + attr = CallableAttr[T]() + + @property + def prop(self) -> T: ... + @prop.setter + def prop(self, c: T, /) -> None: ... + +# Simple assignment +A_callable: Callable[..., Any] = ( + A # E: function __main__.A.__init__ is deprecated: do not use +) + +# Multiple assignments +A_multi_callable: Callable[..., Any] +A_multi_callable, var = ( + A, # E: function __main__.A.__init__ is deprecated: do not use + 1, +) + +# Function argument +receives_callable( + A # E: function __main__.A.__init__ is deprecated: do not use +) + +# Callable type argument +callable_receiver( + A # E: function __main__.A.__init__ is deprecated: do not use +) + +# Function return type +def func_returns_callable(arg: int) -> Callable[..., Any]: + return A # E: function __main__.A.__init__ is deprecated: do not use + +# Typed lambda return type +lambda_returns_callable_1: Callable[[], Callable[..., Any]] = ( + lambda: A # E: function __main__.A.__init__ is deprecated: do not use +) +lambda_returns_callable_2: Callable[[], Callable[..., Any]] +lambda_returns_callable_2 = lambda: ( + A # E: function __main__.A.__init__ is deprecated: do not use +) + +# Class and instance attributes +settable: Settable[Callable[..., Any]] +settable.instance = ( + A # E: function __main__.A.__init__ is deprecated: do not use +) +settable.attr = ( + A # E: function __main__.A.__init__ is deprecated: do not use +) +settable.prop = ( + A # E: function __main__.A.__init__ is deprecated: do not use +) +class SettableChild(Settable[Callable[..., Any]]): + class_: ClassVar[Callable[..., Any]] = ( + A # E: function __main__.A.__init__ is deprecated: do not use + ) + +# Checks for false positives + +def receives_type(t: type[A], /) -> None: ... +def receives_object(o: object) -> None: ... +def receives_any(o: Any) -> None: ... +type_receiver: Callable[[type[A]], None] +object_receiver: Callable[[object], None] +any_receiver: Callable[[Any], None] + +A_type: type[A] = A +A_object: object = A +A_any: Any = A +receives_type(A_type) +receives_object(A_object) +receives_any(A_any) +type_receiver(A_type) +object_receiver(A_object) +any_receiver(A_any) + +def func_returns_type(arg: int) -> type[A]: return A +def func_returns_object(arg: int) -> object: return A +def func_returns_any(arg: int) -> Any: return A +lambda_returns_type: Callable[[], type[A]] = lambda: A +lambda_returns_object: Callable[[], object] = lambda: A +lambda_returns_any: Callable[[], Any] = lambda: A + +settable2: Settable[type[A]] +settable2.instance = A +settable2.attr = A +settable2.prop = A + +class SettableChild2(Settable[type[A]]): + class_: ClassVar[type[A]] = A + +[builtins fixtures/property.pyi] + + +[case testDeprecatedClassConstructorInUnionTypeContext] +# flags: --enable-error-code=deprecated + +from typing import Any, Callable, Union, Optional +from typing_extensions import deprecated + +class Dummy: ... + +class A: + @deprecated("do not use") + def __init__(self) -> None: ... + +callable_or_dummy: Union[Callable[..., Any], Dummy] = A # E: function __main__.A.__init__ is deprecated: do not use +maybe_callable: Optional[Callable[..., Any]] = A # E: function __main__.A.__init__ is deprecated: do not use + +type_or_dummy: Union[type[A], Dummy] = A +maybe_type: Optional[type[A]] = A + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClassConstructorInProtocolTypeContext] +# flags: --enable-error-code=deprecated + +from typing import Protocol, Union +from typing_extensions import deprecated + +class CompatibleProto(Protocol): + def __call__(self) -> A: ... + +class IncompatibleProto1(Protocol): + def __call__(self, a: int, /) -> A: ... + +class IncompatibleProto2(Protocol): + var: int + def __call__(self) -> A: ... + +class A: + @deprecated("do not use") + def __init__(self) -> None: ... + +CallableA: Union[CompatibleProto, type[A]] = ( + A # E: function __main__.A.__init__ is deprecated: do not use +) +AType1: Union[IncompatibleProto1, type[A]] = A +AType2: Union[IncompatibleProto2, type[A]] = A + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedOverloadedClassConstructorInCallableTypeContext] +# flags: --enable-error-code=deprecated --disable-error-code=no-overload-impl + +from typing import Callable, overload +from typing_extensions import deprecated + +class A: + @overload + @deprecated("use `make_a` instead") + def __init__(self) -> None: ... + @overload + @deprecated("use `make_a_with_int()` instead") + def __init__(self, a: int) -> None: ... + @overload + def __init__(self, a: str) -> None: ... + @classmethod + def make_a(cls) -> A: ... + @classmethod + def make_a_with_int(cls, a: int) -> A: ... + +AFactory: Callable[[], A] = ( + A # E: overload def (self: __main__.A) of function __main__.A.__init__ is deprecated: use `make_a` instead +) +AFactoryWithInt: Callable[[int], A] = ( + A # E: overload def (self: __main__.A, a: builtins.int) of function __main__.A.__init__ is deprecated: use `make_a_with_int()` instead +) + +AFactoryWithStr: Callable[[str], A] = A +IncompatibleFactory: Callable[[bytes], A] = ( + A # E: Incompatible types in assignment (expression has type "type[A]", variable has type "Callable[[bytes], A]") +) + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedSpecialMethods] # flags: --enable-error-code=deprecated From 0950c975c07f09068abadedb1317a3822ee7e36f Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 12:15:16 +1300 Subject: [PATCH 10/11] Avoid invalid deprecation checks of in the `else` branch of conditional expressions --- mypy/checkexpr.py | 24 ++++++++++++++++++++---- test-data/unit/check-deprecated.test | 25 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0d07ecf81cce..fad89052a840 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -286,6 +286,8 @@ class ExpressionChecker(ExpressionVisitor[Type], ExpressionCheckerSharedApi): plugin: Plugin _arg_infer_context_cache: ArgumentInferContext | None + # Used to prevent generating redundant or invalid `@deprecated()` reports + _valid_pep702_type_context: bool def __init__( self, @@ -322,6 +324,7 @@ def __init__( type_state.infer_polymorphic = not self.chk.options.old_type_inference self._arg_infer_context_cache = None + self._valid_pep702_type_context = True self.expr_cache: dict[ tuple[Expression, Type | None], tuple[int, Type, list[ErrorInfo], dict[Expression, Type]], @@ -375,7 +378,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: # Unknown reference; use any type implicitly to avoid # generating extra type errors. result = AnyType(TypeOfAny.from_error) - if isinstance(node, TypeInfo): + if self._valid_pep702_type_context and isinstance(node, TypeInfo): if self.type_context[-1] is not None: proper_result = get_proper_type(result) if isinstance(proper_result, (CallableType, Overloaded)): @@ -5963,6 +5966,10 @@ def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = F e.else_expr, context=if_type_fallback, allow_none_return=allow_none_return, + # `@deprecated()` is already properly reported in the else branch when obtaining + # `full_context_else_type`. Reporting it again is redundant, and also invalid when + # analysing reference expressions here because the full type context is not used. + valid_pep702_type_context=False, ) # In most cases using if_type as a context for right branch gives better inferred types. @@ -5988,17 +5995,26 @@ def analyze_cond_branch( context: Type | None, allow_none_return: bool = False, suppress_unreachable_errors: bool = True, + valid_pep702_type_context: bool = True, ) -> Type: with self.chk.binder.frame_context(can_skip=True, fall_through=0): + _valid_pep702_context = self._valid_pep702_type_context + self._valid_pep702_type_context = valid_pep702_type_context + result: Type if map is None: # We still need to type check node, in case we want to # process it for isinstance checks later. Since the branch was # determined to be unreachable, any errors should be suppressed. with self.msg.filter_errors(filter_errors=suppress_unreachable_errors): self.accept(node, type_context=context, allow_none_return=allow_none_return) - return UninhabitedType() - self.chk.push_type_map(map) - return self.accept(node, type_context=context, allow_none_return=allow_none_return) + result = UninhabitedType() + else: + self.chk.push_type_map(map) + result = self.accept( + node, type_context=context, allow_none_return=allow_none_return + ) + self._valid_pep702_type_context = _valid_pep702_context + return result def _combined_context(self, ty: Type | None) -> Type | None: ctx_items = [] diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index dc9e4d3d12a4..12537d0c818a 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -515,6 +515,31 @@ class SettableChild2(Settable[type[A]]): [builtins fixtures/property.pyi] +[case testDeprecatedClassConstructorInConditionalExprCallableTypeContext] +# flags: --enable-error-code=deprecated + +from typing import Any, Callable +from typing_extensions import deprecated + +class A: + @deprecated("do not use") + def __init__(self) -> None: ... + +class B(A): ... + +var: object +callable_: Callable[..., Any] = ( + A # E: function __main__.A.__init__ is deprecated: do not use + if (var is None) else + B # E: function __main__.A.__init__ is deprecated: do not use +) + +TypeA1: type[A] = A if (var is None) else B +TypeA2 = A if (var is None) else B + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedClassConstructorInUnionTypeContext] # flags: --enable-error-code=deprecated From 56be189bf1cd0e9833e41c90044fbea9abb854f7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:33:51 +0000 Subject: [PATCH 11/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index fad89052a840..072cee0ac602 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -383,9 +383,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: proper_result = get_proper_type(result) if isinstance(proper_result, (CallableType, Overloaded)): ctor_type = constructor_type_in_callable_context( - proper_result, - get_proper_type(self.type_context[-1]), - self.chk.options, + proper_result, get_proper_type(self.type_context[-1]), self.chk.options ) if ctor_type is not None: self.chk.check_deprecated(ctor_type.definition, e)