diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3eb54579a050..072cee0ac602 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, @@ -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,15 @@ 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)): + 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 ): @@ -1671,9 +1682,24 @@ 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 + 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 = alias_target.type + if callable_info is not None: + self.chk.check_deprecated(callee.definition, 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() @@ -5938,6 +5964,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. @@ -5963,17 +5993,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 = [] @@ -6767,3 +6806,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 607e9d767956..12537d0c818a 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -315,18 +315,312 @@ 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 +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") + def __new__(cls) -> 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] + + +[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] + + +[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 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 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 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]