From f04f4e3fdd639d61f859bf517a230a682e2fc366 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 3 Jul 2024 21:27:03 +0200 Subject: [PATCH 01/35] Add basic support for PEP 702 (@deprecated). --- mypy/checker.py | 80 ++++++ mypy/checkexpr.py | 17 +- mypy/checkmember.py | 29 ++- mypy/errorcodes.py | 6 + mypy/nodes.py | 7 + mypy/types.py | 18 +- test-data/unit/check-deprecated.test | 353 +++++++++++++++++++++++++++ 7 files changed, 502 insertions(+), 8 deletions(-) create mode 100644 test-data/unit/check-deprecated.test diff --git a/mypy/checker.py b/mypy/checker.py index 2df74cf7be8d..349cb221f407 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -190,6 +190,7 @@ BoolTypeQuery, CallableType, DeletedType, + DEPRECATED_TYPE_NAMES, ErasedType, FunctionLike, Instance, @@ -636,6 +637,10 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: if len(defn.items) == 1: self.fail(message_registry.MULTIPLE_OVERLOADS_REQUIRED, defn) + for item in defn.items: + if isinstance(item, Decorator) and isinstance(ct := item.func.type, CallableType): + ct.deprecated = self.get_deprecation_warning(item.decorators) + if defn.is_property: # HACK: Infer the type of the property. assert isinstance(defn.items[0], Decorator) @@ -654,6 +659,16 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: self.fail(message_registry.INCONSISTENT_ABSTRACT_OVERLOAD, defn) if defn.impl: defn.impl.accept(self) + if ( + isinstance(impl := defn.impl, Decorator) and + isinstance(ct := impl.func.type, CallableType) and + ((deprecated := ct.deprecated) is not None) + ): + if isinstance(ct := defn.type, (CallableType, Overloaded)): + ct.deprecated = deprecated + for subdef in defn.items: + if isinstance(ct := get_proper_type(subdef.type), (CallableType, Overloaded)): + ct.deprecated = deprecated if not defn.is_property: self.check_overlapping_overloads(defn) if defn.type is None: @@ -2406,6 +2421,7 @@ def check__exit__return_type(self, defn: FuncItem) -> None: def visit_class_def(self, defn: ClassDef) -> None: """Type check a class definition.""" typ = defn.info + typ.deprecated = self.get_deprecation_warning(defn.decorators) for base in typ.mro[1:]: if base.is_final: self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn) @@ -2835,6 +2851,15 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None: ) def visit_import_from(self, node: ImportFrom) -> None: + for name, _ in node.names: + if (sym := self.globals.get(name)) is not None: + if isinstance(sym.node, TypeInfo) and ((depr := sym.node.deprecated) is not None): + self.warn_deprecated(sym.node, depr, node) + elif ( + isinstance(co := get_proper_type(sym.type), (CallableType, Overloaded)) and + ((depr := co.deprecated) is not None) + ): + self.warn_deprecated(co, depr, node) self.check_import(node) def visit_import_all(self, node: ImportAll) -> None: @@ -2923,6 +2948,16 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: Handle all kinds of assignment statements (simple, indexed, multiple). """ + + if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs: + for lvalue in s.lvalues: + if ( + isinstance(lvalue, NameExpr) and + isinstance(var := lvalue.node, Var) and + isinstance(instance := get_proper_type(var.type), Instance) + ): + self.check_deprecated_class(instance.type, s, False) + # Avoid type checking type aliases in stubs to avoid false # positives about modern type syntax available in stubs such # as X | Y. @@ -4667,6 +4702,7 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) rvalue_type, method_type = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s) + self.check_deprecated_function(method_type, s, True) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: @@ -4992,6 +5028,8 @@ def visit_del_stmt(self, s: DelStmt) -> None: ) def visit_decorator(self, e: Decorator) -> None: + if isinstance(co := e.func.type, (CallableType, Overloaded)): + co.deprecated = self.get_deprecation_warning(e.decorators) for d in e.decorators: if isinstance(d, RefExpr): if d.fullname == "typing.no_type_check": @@ -7519,6 +7557,48 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type: return self.expr_checker.accept(node, type_context=type_context) + def get_deprecation_warning(self, decorators: Iterable[Expression]) -> str | None: + for decorator in decorators: + if ( + isinstance(decorator, CallExpr) and + isinstance(callee := decorator.callee, NameExpr) and + ((callee.fullname in DEPRECATED_TYPE_NAMES) or (not callee.fullname and callee.name == "deprecated")) and + (len(args := decorator.args) == 1) and + isinstance(arg := args[0], StrExpr) + ): + return arg.value + return None + + def check_deprecated_function(self, typ: Type, context: Context, memberaccess: bool) -> None: + if isinstance(typ := get_proper_type(typ), (CallableType, Overloaded)): + self._check_deprecated(typ, context, memberaccess) + + def check_deprecated_class(self, typ: TypeInfo, context: Context, memberaccess: bool) -> None: + self._check_deprecated(typ, context, memberaccess) + + def _check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context, memberaccess: bool) -> None: + if (depr := typ.deprecated) is not None: + if memberaccess: + self.warn_deprecated(typ, depr, context) + else: + for imp in self.tree.imports: + if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): + break + else: + self.warn_deprecated(typ, depr, context) + + def warn_deprecated(self, type_: CallableType | Overloaded | TypeInfo, deprecated: str, context: Context) -> None: + name = type_.name + if isinstance(type_, CallableType): + if (defn := type_.definition) is not None: + name = defn.fullname + elif isinstance(type_, Overloaded): + if isinstance(func := type_.items[0].definition, FuncDef): + name = func.fullname + else: + name = type_.fullname + self.msg.fail(f"{name} is deprecated - {deprecated}", context, code=codes.DEPRECATED) + class CollectArgTypeVarTypes(TypeTraverserVisitor): """Collects the non-nested argument types in a set.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3532e18b93b2..c690565a74e4 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -354,7 +354,12 @@ def visit_name_expr(self, e: NameExpr) -> Type: """ self.chk.module_refs.update(extract_refexpr_names(e)) result = self.analyze_ref_expr(e) - return self.narrow_type_from_binder(e, result) + narrowed = self.narrow_type_from_binder(e, result) + if isinstance(e.node, TypeInfo): + self.chk.check_deprecated_class(e.node, e, False) + else: + self.chk.check_deprecated_function(narrowed, e, False) + return narrowed def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: result: Type | None = None @@ -1474,6 +1479,7 @@ def check_call_expr_with_callee_type( callable_name=callable_name, object_type=object_type, ) + self.chk.check_deprecated_function(callee_type, e, False) proper_callee = get_proper_type(callee_type) if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType): # Cache it for find_isinstance_check() @@ -3252,7 +3258,12 @@ def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: """Visit member expression (of form e.id).""" self.chk.module_refs.update(extract_refexpr_names(e)) result = self.analyze_ordinary_member_access(e, is_lvalue) - return self.narrow_type_from_binder(e, result) + narrowed = self.narrow_type_from_binder(e, result) + if isinstance(e.node, TypeInfo): + self.chk.check_deprecated_class(e.node, e, True) + else: + self.chk.check_deprecated_function(narrowed, e, True) + return narrowed def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type: """Analyse member expression or member lvalue.""" @@ -3477,6 +3488,7 @@ def visit_op_expr(self, e: OpExpr) -> Type: else: assert_never(use_reverse) e.method_type = method_type + self.chk.check_deprecated_function(method_type, e, False) return result else: raise RuntimeError(f"Unknown operator {e.op}") @@ -3793,6 +3805,7 @@ def check_method_call_by_name( chk=self.chk, in_literal_context=self.is_literal_context(), ) + self.chk.check_deprecated_function(method_type, context, True) return self.check_method_call(method, base_type, method_type, args, arg_kinds, context) def check_union_method_call_by_name( diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 0f117f5475ed..1f4fb32579e1 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -316,9 +316,25 @@ def analyze_instance_member_access( if method.is_property: assert isinstance(method, OverloadedFuncDef) - first_item = method.items[0] - assert isinstance(first_item, Decorator) - return analyze_var(name, first_item.var, typ, info, mx) + getter = method.items[0] + assert isinstance(getter, Decorator) + if ( + mx.is_lvalue and + (len(items := method.items) > 1) and + isinstance(setter := items[1], Decorator) + ): + if ( + isinstance(co := setter.func.type, (CallableType, Overloaded)) and + ((deprecated := co.deprecated) is not None) + ): + mx.chk.warn_deprecated(co, deprecated, mx.context) + return analyze_var(name, getter.var, typ, info, mx) + elif ( + isinstance(co := method.type, (CallableType, Overloaded)) and + ((deprecated := co.deprecated) is not None) + ): + mx.chk.warn_deprecated(co, deprecated, mx.context) + if mx.is_lvalue: mx.msg.cant_assign_to_method(mx.context) if not isinstance(method, OverloadedFuncDef): @@ -773,6 +789,13 @@ def analyze_var( result = t typ = get_proper_type(typ) + if ( + var.is_property and + isinstance(typ, CallableType) and + ((deprecated := typ.deprecated) is not None) + ): + mx.chk.warn_deprecated(typ, deprecated, mx.context) + call_type: ProperType | None = None if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator): if isinstance(typ, FunctionLike) and not typ.is_type_obj(): diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 6e8763264ddd..308d2afed43c 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -293,5 +293,11 @@ def __hash__(self) -> int: "General", ) +DEPRECATED: Final = ErrorCode( + "deprecated", + "Warn when importing or using deprecated (overloaded) functions, methods or classes", + "General", +) + # This copy will not include any error codes defined later in the plugins. mypy_error_codes = error_codes.copy() diff --git a/mypy/nodes.py b/mypy/nodes.py index 2eb39d4baaf6..02f217e937dd 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2942,6 +2942,7 @@ class is generic then it will be a type constructor of higher kind. "self_type", "dataclass_transform_spec", "is_type_check_only", + "deprecated", ) _fullname: str # Fully qualified name @@ -3095,6 +3096,9 @@ class is generic then it will be a type constructor of higher kind. # Is set to `True` when class is decorated with `@typing.type_check_only` is_type_check_only: bool + # The type's deprecation message (in case it is deprecated) + deprecated: str | None + FLAGS: Final = [ "is_abstract", "is_enum", @@ -3152,6 +3156,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None self.self_type = None self.dataclass_transform_spec = None self.is_type_check_only = False + self.deprecated = None def add_type_vars(self) -> None: self.has_type_var_tuple_type = False @@ -3374,6 +3379,7 @@ def serialize(self) -> JsonDict: if self.dataclass_transform_spec is not None else None ), + "deprecated": self.deprecated, } return data @@ -3441,6 +3447,7 @@ def deserialize(cls, data: JsonDict) -> TypeInfo: ti.dataclass_transform_spec = DataclassTransformSpec.deserialize( data["dataclass_transform_spec"] ) + ti.deprecated = data.get("deprecated") return ti diff --git a/mypy/types.py b/mypy/types.py index 2e7cbfd4e733..73f9efb77ac4 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1789,6 +1789,7 @@ class CallableType(FunctionLike): # (this is used for error messages) "imprecise_arg_kinds", "unpack_kwargs", # Was an Unpack[...] with **kwargs used to define this callable? + "deprecated", # The callable's deprecation message (in case it is deprecated) ) def __init__( @@ -1815,6 +1816,7 @@ def __init__( from_concatenate: bool = False, imprecise_arg_kinds: bool = False, unpack_kwargs: bool = False, + deprecated: str | None = None, ) -> None: super().__init__(line, column) assert len(arg_types) == len(arg_kinds) == len(arg_names) @@ -1863,6 +1865,7 @@ def __init__( self.type_guard = type_guard self.type_is = type_is self.unpack_kwargs = unpack_kwargs + self.deprecated = deprecated def copy_modified( self: CT, @@ -1887,6 +1890,7 @@ def copy_modified( from_concatenate: Bogus[bool] = _dummy, imprecise_arg_kinds: Bogus[bool] = _dummy, unpack_kwargs: Bogus[bool] = _dummy, + deprecated: Bogus[str | None] = _dummy, ) -> CT: modified = CallableType( arg_types=arg_types if arg_types is not _dummy else self.arg_types, @@ -1918,6 +1922,7 @@ def copy_modified( else self.imprecise_arg_kinds ), unpack_kwargs=unpack_kwargs if unpack_kwargs is not _dummy else self.unpack_kwargs, + deprecated=deprecated if deprecated is not _dummy else self.deprecated, ) # Optimization: Only NewTypes are supported as subtypes since # the class is effectively final, so we can use a cast safely. @@ -2280,7 +2285,7 @@ class Overloaded(FunctionLike): implementation. """ - __slots__ = ("_items",) + __slots__ = ("_items", "deprecated") _items: list[CallableType] # Must not be empty @@ -2288,6 +2293,7 @@ def __init__(self, items: list[CallableType]) -> None: super().__init__(items[0].line, items[0].column) self._items = items self.fallback = items[0].fallback + self.deprecated: str | None = None @property def items(self) -> list[CallableType]: @@ -2332,12 +2338,18 @@ def __eq__(self, other: object) -> bool: return self.items == other.items def serialize(self) -> JsonDict: - return {".class": "Overloaded", "items": [t.serialize() for t in self.items]} + return { + ".class": "Overloaded", + "items": [t.serialize() for t in self.items], + "deprecated": self.deprecated + } @classmethod def deserialize(cls, data: JsonDict) -> Overloaded: assert data[".class"] == "Overloaded" - return Overloaded([CallableType.deserialize(t) for t in data["items"]]) + s = Overloaded([CallableType.deserialize(t) for t in data["items"]]) + s.deprecated = data.get("deprecated") + return s class TupleType(ProperType): diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test new file mode 100644 index 000000000000..3a3655b351a3 --- /dev/null +++ b/test-data/unit/check-deprecated.test @@ -0,0 +1,353 @@ +-- Type checker test cases for reporting deprecations. + +[case testDeprecatedFunction] + +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +f # E: __main__.f is deprecated - use f2 instead +f(1) # E: __main__.f is deprecated - use f2 instead \ + # E: Too many arguments for "f" +f[1] # E: __main__.f is deprecated - use f2 instead \ + # E: Value of type "Callable[[], None]" is not indexable +g = f # E: __main__.f is deprecated - use f2 instead +g() # E: __main__.f is deprecated - use f2 instead +t = (f, f, g) # E: __main__.f is deprecated - use f2 instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedFunctionDifferentModule] + +import m +import p.s +import m as n +import p.s as ps +from m import f # E: m.f is deprecated - use f2 instead +from p.s import g # E: p.s.g is deprecated - use g2 instead +from k import * + +m.f() # E: m.f is deprecated - use f2 instead +p.s.g() # E: p.s.g is deprecated - use g2 instead +n.f() # E: m.f is deprecated - use f2 instead +ps.g() # E: p.s.g is deprecated - use g2 instead +f() +g() +h() # E: k.h is deprecated - use h2 instead + +[file m.py] +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +[file p/s.py] +from typing_extensions import deprecated + +@deprecated("use g2 instead") +def g() -> None: ... + +[file k.py] +from typing_extensions import deprecated + +@deprecated("use h2 instead") +def h() -> None: ... + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClass] + +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +c: C # E: __main__.C is deprecated - use C2 instead +C() # E: __main__.C is deprecated - use C2 instead +C.missing() # E: __main__.C is deprecated - use C2 instead \ + # E: "Type[C]" has no attribute "missing" +C.__init__(c) # E: __main__.C is deprecated - use C2 instead +C(1) # E: __main__.C is deprecated - use C2 instead \ + # E: Too many arguments for "C" +D = C # E: __main__.C is deprecated - use C2 instead +D() +t = (C, C, D) # E: __main__.C is deprecated - use C2 instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClassDifferentModule] + +import m +import p.s +import m as n +import p.s as ps +from m import C # E: m.C is deprecated - use C2 instead +from p.s import D # E: p.s.D is deprecated - use D2 instead +from k import * + +m.C() # E: m.C is deprecated - use C2 instead +p.s.D() # E: p.s.D is deprecated - use D2 instead +n.C() # E: m.C is deprecated - use C2 instead +ps.D() # E: p.s.D is deprecated - use D2 instead +C() +D() +E() # E: k.E is deprecated - use E2 instead + +[file m.py] +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +[file p/s.py] +from typing_extensions import deprecated + +@deprecated("use D2 instead") +class D: ... + +[file k.py] +from typing_extensions import deprecated + +@deprecated("use E2 instead") +class E: ... + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClassInitMethod] + +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: + def __init__(self) -> None: ... + +c: C # E: __main__.C is deprecated - use C2 instead +C() # E: __main__.C is deprecated - use C2 instead +C.__init__(c) # E: __main__.C is deprecated - use C2 instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedSpecialMethods] + +from typing import Iterator +from typing_extensions import deprecated + +class A: + @deprecated("no A + int") + def __add__(self, v: int) -> None: ... + + @deprecated("no int + A") + def __radd__(self, v: int) -> None: ... + + @deprecated("no A = A + int") + def __iadd__(self, v: int) -> A: ... + + @deprecated("no iteration") + def __iter__(self) -> Iterator[int]: ... + + @deprecated("no in") + def __contains__(self, v: int) -> int: ... + + @deprecated("no integer") + def __int__(self) -> int: ... + + @deprecated("no inversion") + def __invert__(self) -> A: ... + +class B: + @deprecated("no in") + def __contains__(self, v: int) -> int: ... + +a = A() +b = B() +a + 1 # E: __main__.A.__add__ is deprecated - no A + int +1 + a # E: __main__.A.__radd__ is deprecated - no int + A +a += 1 # E: __main__.A.__iadd__ is deprecated - no A = A + int +for i in a: # E: __main__.A.__iter__ is deprecated - no iteration + reveal_type(i) # N: Revealed type is "builtins.int" +1 in a # E: __main__.A.__iter__ is deprecated - no iteration +1 in b # E: __main__.B.__contains__ is deprecated - no in +~a # E: __main__.A.__invert__ is deprecated - no inversion + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedOverloadedSpecialMethods] + +from typing import Iterator, Union +from typing_extensions import deprecated, overload + +class A: + @overload + @deprecated("no A + int") + def __add__(self, v: int) -> None: ... + @overload + def __add__(self, v: str) -> None: ... + def __add__(self, v: Union[int, str]) -> None: ... + + @overload + def __radd__(self, v: int) -> None: ... + @overload + @deprecated("no str + A") + def __radd__(self, v: str) -> None: ... + def __radd__(self, v: Union[int, str]) -> None: ... + + @overload + def __iadd__(self, v: int) -> A: ... + @overload + def __iadd__(self, v: str) -> A: ... + @deprecated("no A += Any") + def __iadd__(self, v: Union[int, str]) -> A: ... + +a = A() +a + 1 # E: __main__.A.__add__ is deprecated - no A + int +a + "x" +1 + a +"x" + a # E: __main__.A.__radd__ is deprecated - no str + A +a += 1 # E: __main__.A.__iadd__ is deprecated - no A += Any +a += "x" # E: __main__.A.__iadd__ is deprecated - no A += Any + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedMethod] + +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: __main__.C.f is deprecated - use g instead +C().f # E: __main__.C.f is deprecated - use g instead +C().f() # E: __main__.C.f is deprecated - use g instead +C().f(1) # E: __main__.C.f is deprecated - use g instead \ + # E: Too many arguments for "f" of "C" +f = C().f # E: __main__.C.f is deprecated - use g instead +f() # E: __main__.C.f is deprecated - use g instead +t = (C.f, C.f, C.g) # E: __main__.C.f is deprecated - use g instead + +C().g() +C().h() # E: __main__.C.h is deprecated - use g instead +C().k() # E: __main__.C.k is deprecated - use g instead + +[builtins fixtures/callable.pyi] + + +[case testDeprecatedClassWithDeprecatedMethod] + +from typing_extensions import deprecated + +@deprecated("use D instead") +class C: + @deprecated("use g instead") + def f(self) -> None: ... + def g(self) -> None: ... + +C().f() # E: __main__.C is deprecated - use D instead \ + # E: __main__.C.f is deprecated - use g instead +C().g() # E: __main__.C is deprecated - use D instead + +[builtins fixtures/callable.pyi] + + +[case testDeprecatedProperty] + +from typing_extensions import deprecated + +class C: + @property + @deprecated("use f2 instead") + def f(self) -> int: ... + + @property + def g(self) -> int: ... + @g.setter + @deprecated("use g2 instead") + def g(self, v: int) -> None: ... + + +C.f # E: __main__.C.f is deprecated - use f2 instead +C().f # E: __main__.C.f is deprecated - use f2 instead +C().f() # E: __main__.C.f is deprecated - use f2 instead \ + # E: "int" not callable +C().f = 1 # E: Property "f" defined in "C" is read-only \ + # E: __main__.C.f is deprecated - use f2 instead + +C.g +C().g +C().g = 1 # E: __main__.C.g is deprecated - use g2 instead +C().g = "x" # E: __main__.C.g is deprecated - use g2 instead \ + # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +[builtins fixtures/property.pyi] + + +[case testDeprecatedOverloadedFunction] + +from typing import Union +from typing_extensions import deprecated, overload + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +@deprecated("use f2 instead") +def f(x: Union[int, str]) -> Union[int, str]: ... + +f # E: __main__.f is deprecated - use f2 instead +f(1) # E: __main__.f is deprecated - use f2 instead +f("x") # E: __main__.f is deprecated - use f2 instead +f(1.0) # E: __main__.f is deprecated - use f2 instead \ + # E: No overload variant of "f" matches argument type "float" \ + # N: Possible overload variants: \ + # N: def f(x: int) -> int \ + # N: def f(x: str) -> str + +@overload +@deprecated("work with str instead") +def g(x: int) -> int: ... +@overload +def g(x: str) -> str: ... +def g(x: Union[int, str]) -> Union[int, str]: ... + +g +g(1) # E: __main__.g is deprecated - work with str instead +g("x") +g(1.0) # E: No overload variant of "g" matches argument type "float" \ + # N: Possible overload variants: \ + # N: def g(x: int) -> int \ + # N: def g(x: str) -> str + +@overload +def h(x: int) -> int: ... +@overload +@deprecated("work with int instead") +def h(x: str) -> str: ... +def h(x: Union[int, str]) -> Union[int, str]: ... + +h +h(1) +h("x") # E: __main__.h is deprecated - work with int instead +h(1.0) # E: No overload variant of "h" matches argument type "float" \ + # N: Possible overload variants: \ + # N: def h(x: int) -> int \ + # N: def h(x: str) -> str + +[builtins fixtures/tuple.pyi] From 973bf2d1e5257977a3915dc578fda96c66f37c00 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:51:55 +0000 Subject: [PATCH 02/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 40 +++++++++++++++++++++++----------------- mypy/checkmember.py | 22 ++++++++++------------ mypy/nodes.py | 2 +- mypy/types.py | 2 +- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 349cb221f407..a30d9b231fd3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -184,13 +184,13 @@ ) from mypy.types import ( ANY_STRATEGY, + DEPRECATED_TYPE_NAMES, MYPYC_NATIVE_INT_NAMES, OVERLOAD_NAMES, AnyType, BoolTypeQuery, CallableType, DeletedType, - DEPRECATED_TYPE_NAMES, ErasedType, FunctionLike, Instance, @@ -660,9 +660,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: if defn.impl: defn.impl.accept(self) if ( - isinstance(impl := defn.impl, Decorator) and - isinstance(ct := impl.func.type, CallableType) and - ((deprecated := ct.deprecated) is not None) + isinstance(impl := defn.impl, Decorator) + and isinstance(ct := impl.func.type, CallableType) + and ((deprecated := ct.deprecated) is not None) ): if isinstance(ct := defn.type, (CallableType, Overloaded)): ct.deprecated = deprecated @@ -2855,9 +2855,8 @@ def visit_import_from(self, node: ImportFrom) -> None: if (sym := self.globals.get(name)) is not None: if isinstance(sym.node, TypeInfo) and ((depr := sym.node.deprecated) is not None): self.warn_deprecated(sym.node, depr, node) - elif ( - isinstance(co := get_proper_type(sym.type), (CallableType, Overloaded)) and - ((depr := co.deprecated) is not None) + elif isinstance(co := get_proper_type(sym.type), (CallableType, Overloaded)) and ( + (depr := co.deprecated) is not None ): self.warn_deprecated(co, depr, node) self.check_import(node) @@ -2952,9 +2951,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs: for lvalue in s.lvalues: if ( - isinstance(lvalue, NameExpr) and - isinstance(var := lvalue.node, Var) and - isinstance(instance := get_proper_type(var.type), Instance) + isinstance(lvalue, NameExpr) + and isinstance(var := lvalue.node, Var) + and isinstance(instance := get_proper_type(var.type), Instance) ): self.check_deprecated_class(instance.type, s, False) @@ -7560,11 +7559,14 @@ def get_expression_type(self, node: Expression, type_context: Type | None = None def get_deprecation_warning(self, decorators: Iterable[Expression]) -> str | None: for decorator in decorators: if ( - isinstance(decorator, CallExpr) and - isinstance(callee := decorator.callee, NameExpr) and - ((callee.fullname in DEPRECATED_TYPE_NAMES) or (not callee.fullname and callee.name == "deprecated")) and - (len(args := decorator.args) == 1) and - isinstance(arg := args[0], StrExpr) + isinstance(decorator, CallExpr) + and isinstance(callee := decorator.callee, NameExpr) + and ( + (callee.fullname in DEPRECATED_TYPE_NAMES) + or (not callee.fullname and callee.name == "deprecated") + ) + and (len(args := decorator.args) == 1) + and isinstance(arg := args[0], StrExpr) ): return arg.value return None @@ -7576,7 +7578,9 @@ def check_deprecated_function(self, typ: Type, context: Context, memberaccess: b def check_deprecated_class(self, typ: TypeInfo, context: Context, memberaccess: bool) -> None: self._check_deprecated(typ, context, memberaccess) - def _check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context, memberaccess: bool) -> None: + def _check_deprecated( + self, typ: CallableType | Overloaded | TypeInfo, context: Context, memberaccess: bool + ) -> None: if (depr := typ.deprecated) is not None: if memberaccess: self.warn_deprecated(typ, depr, context) @@ -7587,7 +7591,9 @@ def _check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: else: self.warn_deprecated(typ, depr, context) - def warn_deprecated(self, type_: CallableType | Overloaded | TypeInfo, deprecated: str, context: Context) -> None: + def warn_deprecated( + self, type_: CallableType | Overloaded | TypeInfo, deprecated: str, context: Context + ) -> None: name = type_.name if isinstance(type_, CallableType): if (defn := type_.definition) is not None: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 1f4fb32579e1..4e801219f504 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -319,19 +319,17 @@ def analyze_instance_member_access( getter = method.items[0] assert isinstance(getter, Decorator) if ( - mx.is_lvalue and - (len(items := method.items) > 1) and - isinstance(setter := items[1], Decorator) + mx.is_lvalue + and (len(items := method.items) > 1) + and isinstance(setter := items[1], Decorator) ): - if ( - isinstance(co := setter.func.type, (CallableType, Overloaded)) and - ((deprecated := co.deprecated) is not None) + if isinstance(co := setter.func.type, (CallableType, Overloaded)) and ( + (deprecated := co.deprecated) is not None ): mx.chk.warn_deprecated(co, deprecated, mx.context) return analyze_var(name, getter.var, typ, info, mx) - elif ( - isinstance(co := method.type, (CallableType, Overloaded)) and - ((deprecated := co.deprecated) is not None) + elif isinstance(co := method.type, (CallableType, Overloaded)) and ( + (deprecated := co.deprecated) is not None ): mx.chk.warn_deprecated(co, deprecated, mx.context) @@ -790,9 +788,9 @@ def analyze_var( typ = get_proper_type(typ) if ( - var.is_property and - isinstance(typ, CallableType) and - ((deprecated := typ.deprecated) is not None) + var.is_property + and isinstance(typ, CallableType) + and ((deprecated := typ.deprecated) is not None) ): mx.chk.warn_deprecated(typ, deprecated, mx.context) diff --git a/mypy/nodes.py b/mypy/nodes.py index 02f217e937dd..abf311bcb7f2 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3379,7 +3379,7 @@ def serialize(self) -> JsonDict: if self.dataclass_transform_spec is not None else None ), - "deprecated": self.deprecated, + "deprecated": self.deprecated, } return data diff --git a/mypy/types.py b/mypy/types.py index 73f9efb77ac4..a957a10e478b 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2341,7 +2341,7 @@ def serialize(self) -> JsonDict: return { ".class": "Overloaded", "items": [t.serialize() for t in self.items], - "deprecated": self.deprecated + "deprecated": self.deprecated, } @classmethod From a88f4b432533280197d2d7fe006d523d9b7895bc Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 3 Jul 2024 22:37:31 +0200 Subject: [PATCH 03/35] use `type: ignore[deprecated]` when importing `abstractproperty` --- test-data/unit/pythoneval.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 222430c3ef55..e5eabf11f2c9 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -956,7 +956,7 @@ a = [] # type: List[Dict[str, str]] sorted(a, key=lambda y: y['']) [case testAbstractProperty] -from abc import abstractproperty, ABCMeta +from abc import abstractproperty, ABCMeta # type: ignore[deprecated] class A(metaclass=ABCMeta): @abstractproperty def x(self) -> int: pass From faa49111467cd37620d8b0cecf276dadf270cfd7 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 06:35:09 +0200 Subject: [PATCH 04/35] accept deprecated.args >= 1 --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index a30d9b231fd3..5dc19f954dc4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7565,7 +7565,7 @@ def get_deprecation_warning(self, decorators: Iterable[Expression]) -> str | Non (callee.fullname in DEPRECATED_TYPE_NAMES) or (not callee.fullname and callee.name == "deprecated") ) - and (len(args := decorator.args) == 1) + and (len(args := decorator.args) >= 1) and isinstance(arg := args[0], StrExpr) ): return arg.value From 101e9b8f85c6c6cb9c690508915e4675bcbc145a Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 06:39:25 +0200 Subject: [PATCH 05/35] ": " instead of " - " --- mypy/checker.py | 2 +- test-data/unit/check-deprecated.test | 130 +++++++++++++-------------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5dc19f954dc4..a61def898c85 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7603,7 +7603,7 @@ def warn_deprecated( name = func.fullname else: name = type_.fullname - self.msg.fail(f"{name} is deprecated - {deprecated}", context, code=codes.DEPRECATED) + self.msg.fail(f"{name} is deprecated: {deprecated}", context, code=codes.DEPRECATED) class CollectArgTypeVarTypes(TypeTraverserVisitor): diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 3a3655b351a3..93774e566f81 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -7,14 +7,14 @@ from typing_extensions import deprecated @deprecated("use f2 instead") def f() -> None: ... -f # E: __main__.f is deprecated - use f2 instead -f(1) # E: __main__.f is deprecated - use f2 instead \ +f # E: __main__.f is deprecated: use f2 instead # type: ignore[deprecated] +f(1) # E: __main__.f is deprecated: use f2 instead \ # E: Too many arguments for "f" -f[1] # E: __main__.f is deprecated - use f2 instead \ +f[1] # E: __main__.f is deprecated: use f2 instead \ # E: Value of type "Callable[[], None]" is not indexable -g = f # E: __main__.f is deprecated - use f2 instead -g() # E: __main__.f is deprecated - use f2 instead -t = (f, f, g) # E: __main__.f is deprecated - use f2 instead +g = f # E: __main__.f is deprecated: use f2 instead +g() # E: __main__.f is deprecated: use f2 instead +t = (f, f, g) # E: __main__.f is deprecated: use f2 instead [builtins fixtures/tuple.pyi] @@ -25,17 +25,17 @@ import m import p.s import m as n import p.s as ps -from m import f # E: m.f is deprecated - use f2 instead -from p.s import g # E: p.s.g is deprecated - use g2 instead +from m import f # E: m.f is deprecated: use f2 instead +from p.s import g # E: p.s.g is deprecated: use g2 instead from k import * -m.f() # E: m.f is deprecated - use f2 instead -p.s.g() # E: p.s.g is deprecated - use g2 instead -n.f() # E: m.f is deprecated - use f2 instead -ps.g() # E: p.s.g is deprecated - use g2 instead +m.f() # E: m.f is deprecated: use f2 instead +p.s.g() # E: p.s.g is deprecated: use g2 instead +n.f() # E: m.f is deprecated: use f2 instead +ps.g() # E: p.s.g is deprecated: use g2 instead f() g() -h() # E: k.h is deprecated - use h2 instead +h() # E: k.h is deprecated: use h2 instead [file m.py] from typing_extensions import deprecated @@ -65,16 +65,16 @@ from typing_extensions import deprecated @deprecated("use C2 instead") class C: ... -c: C # E: __main__.C is deprecated - use C2 instead -C() # E: __main__.C is deprecated - use C2 instead -C.missing() # E: __main__.C is deprecated - use C2 instead \ +c: C # E: __main__.C is deprecated: use C2 instead +C() # E: __main__.C is deprecated: use C2 instead +C.missing() # E: __main__.C is deprecated: use C2 instead \ # E: "Type[C]" has no attribute "missing" -C.__init__(c) # E: __main__.C is deprecated - use C2 instead -C(1) # E: __main__.C is deprecated - use C2 instead \ +C.__init__(c) # E: __main__.C is deprecated: use C2 instead +C(1) # E: __main__.C is deprecated: use C2 instead \ # E: Too many arguments for "C" -D = C # E: __main__.C is deprecated - use C2 instead +D = C # E: __main__.C is deprecated: use C2 instead D() -t = (C, C, D) # E: __main__.C is deprecated - use C2 instead +t = (C, C, D) # E: __main__.C is deprecated: use C2 instead [builtins fixtures/tuple.pyi] @@ -85,17 +85,17 @@ import m import p.s import m as n import p.s as ps -from m import C # E: m.C is deprecated - use C2 instead -from p.s import D # E: p.s.D is deprecated - use D2 instead +from m import C # E: m.C is deprecated: use C2 instead +from p.s import D # E: p.s.D is deprecated: use D2 instead from k import * -m.C() # E: m.C is deprecated - use C2 instead -p.s.D() # E: p.s.D is deprecated - use D2 instead -n.C() # E: m.C is deprecated - use C2 instead -ps.D() # E: p.s.D is deprecated - use D2 instead +m.C() # E: m.C is deprecated: use C2 instead +p.s.D() # E: p.s.D is deprecated: use D2 instead +n.C() # E: m.C is deprecated: use C2 instead +ps.D() # E: p.s.D is deprecated: use D2 instead C() D() -E() # E: k.E is deprecated - use E2 instead +E() # E: k.E is deprecated: use E2 instead [file m.py] from typing_extensions import deprecated @@ -126,9 +126,9 @@ from typing_extensions import deprecated class C: def __init__(self) -> None: ... -c: C # E: __main__.C is deprecated - use C2 instead -C() # E: __main__.C is deprecated - use C2 instead -C.__init__(c) # E: __main__.C is deprecated - use C2 instead +c: C # E: __main__.C is deprecated: use C2 instead +C() # E: __main__.C is deprecated: use C2 instead +C.__init__(c) # E: __main__.C is deprecated: use C2 instead [builtins fixtures/tuple.pyi] @@ -166,14 +166,14 @@ class B: a = A() b = B() -a + 1 # E: __main__.A.__add__ is deprecated - no A + int -1 + a # E: __main__.A.__radd__ is deprecated - no int + A -a += 1 # E: __main__.A.__iadd__ is deprecated - no A = A + int -for i in a: # E: __main__.A.__iter__ is deprecated - no iteration +a + 1 # E: __main__.A.__add__ is deprecated: no A + int +1 + a # E: __main__.A.__radd__ is deprecated: no int + A +a += 1 # E: __main__.A.__iadd__ is deprecated: no A = A + int +for i in a: # E: __main__.A.__iter__ is deprecated: no iteration reveal_type(i) # N: Revealed type is "builtins.int" -1 in a # E: __main__.A.__iter__ is deprecated - no iteration -1 in b # E: __main__.B.__contains__ is deprecated - no in -~a # E: __main__.A.__invert__ is deprecated - no inversion +1 in a # E: __main__.A.__iter__ is deprecated: no iteration +1 in b # E: __main__.B.__contains__ is deprecated: no in +~a # E: __main__.A.__invert__ is deprecated: no inversion [builtins fixtures/tuple.pyi] @@ -206,12 +206,12 @@ class A: def __iadd__(self, v: Union[int, str]) -> A: ... a = A() -a + 1 # E: __main__.A.__add__ is deprecated - no A + int +a + 1 # E: __main__.A.__add__ is deprecated: no A + int a + "x" 1 + a -"x" + a # E: __main__.A.__radd__ is deprecated - no str + A -a += 1 # E: __main__.A.__iadd__ is deprecated - no A += Any -a += "x" # E: __main__.A.__iadd__ is deprecated - no A += Any +"x" + a # E: __main__.A.__radd__ is deprecated: no str + A +a += 1 # E: __main__.A.__iadd__ is deprecated: no A += Any +a += "x" # E: __main__.A.__iadd__ is deprecated: no A += Any [builtins fixtures/tuple.pyi] @@ -234,18 +234,18 @@ class C: @staticmethod def k() -> None: ... -C.f # E: __main__.C.f is deprecated - use g instead -C().f # E: __main__.C.f is deprecated - use g instead -C().f() # E: __main__.C.f is deprecated - use g instead -C().f(1) # E: __main__.C.f is deprecated - use g instead \ +C.f # E: __main__.C.f is deprecated: use g instead +C().f # E: __main__.C.f is deprecated: use g instead +C().f() # E: __main__.C.f is deprecated: use g instead +C().f(1) # E: __main__.C.f is deprecated: use g instead \ # E: Too many arguments for "f" of "C" -f = C().f # E: __main__.C.f is deprecated - use g instead -f() # E: __main__.C.f is deprecated - use g instead -t = (C.f, C.f, C.g) # E: __main__.C.f is deprecated - use g instead +f = C().f # E: __main__.C.f is deprecated: use g instead +f() # E: __main__.C.f is deprecated: use g instead +t = (C.f, C.f, C.g) # E: __main__.C.f is deprecated: use g instead C().g() -C().h() # E: __main__.C.h is deprecated - use g instead -C().k() # E: __main__.C.k is deprecated - use g instead +C().h() # E: __main__.C.h is deprecated: use g instead +C().k() # E: __main__.C.k is deprecated: use g instead [builtins fixtures/callable.pyi] @@ -260,9 +260,9 @@ class C: def f(self) -> None: ... def g(self) -> None: ... -C().f() # E: __main__.C is deprecated - use D instead \ - # E: __main__.C.f is deprecated - use g instead -C().g() # E: __main__.C is deprecated - use D instead +C().f() # E: __main__.C is deprecated: use D instead \ + # E: __main__.C.f is deprecated: use g instead +C().g() # E: __main__.C is deprecated: use D instead [builtins fixtures/callable.pyi] @@ -283,17 +283,17 @@ class C: def g(self, v: int) -> None: ... -C.f # E: __main__.C.f is deprecated - use f2 instead -C().f # E: __main__.C.f is deprecated - use f2 instead -C().f() # E: __main__.C.f is deprecated - use f2 instead \ +C.f # E: __main__.C.f is deprecated: use f2 instead +C().f # E: __main__.C.f is deprecated: use f2 instead +C().f() # E: __main__.C.f is deprecated: use f2 instead \ # E: "int" not callable C().f = 1 # E: Property "f" defined in "C" is read-only \ - # E: __main__.C.f is deprecated - use f2 instead + # E: __main__.C.f is deprecated: use f2 instead C.g C().g -C().g = 1 # E: __main__.C.g is deprecated - use g2 instead -C().g = "x" # E: __main__.C.g is deprecated - use g2 instead \ +C().g = 1 # E: __main__.C.g is deprecated: use g2 instead +C().g = "x" # E: __main__.C.g is deprecated: use g2 instead \ # E: Incompatible types in assignment (expression has type "str", variable has type "int") [builtins fixtures/property.pyi] @@ -311,10 +311,10 @@ def f(x: str) -> str: ... @deprecated("use f2 instead") def f(x: Union[int, str]) -> Union[int, str]: ... -f # E: __main__.f is deprecated - use f2 instead -f(1) # E: __main__.f is deprecated - use f2 instead -f("x") # E: __main__.f is deprecated - use f2 instead -f(1.0) # E: __main__.f is deprecated - use f2 instead \ +f # E: __main__.f is deprecated: use f2 instead +f(1) # E: __main__.f is deprecated: use f2 instead +f("x") # E: __main__.f is deprecated: use f2 instead +f(1.0) # E: __main__.f is deprecated: use f2 instead \ # E: No overload variant of "f" matches argument type "float" \ # N: Possible overload variants: \ # N: def f(x: int) -> int \ @@ -328,7 +328,7 @@ def g(x: str) -> str: ... def g(x: Union[int, str]) -> Union[int, str]: ... g -g(1) # E: __main__.g is deprecated - work with str instead +g(1) # E: __main__.g is deprecated: work with str instead g("x") g(1.0) # E: No overload variant of "g" matches argument type "float" \ # N: Possible overload variants: \ @@ -344,7 +344,7 @@ def h(x: Union[int, str]) -> Union[int, str]: ... h h(1) -h("x") # E: __main__.h is deprecated - work with int instead +h("x") # E: __main__.h is deprecated: work with int instead h(1.0) # E: No overload variant of "h" matches argument type "float" \ # N: Possible overload variants: \ # N: def h(x: int) -> int \ From e3dfacb41edda2eb62766ec19da7a20c63427a09 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 06:58:28 +0200 Subject: [PATCH 06/35] only consider `warnings.deprecated` and `typing_extensions.deprecated`, not unresolved `deprecated` --- mypy/checker.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a61def898c85..a650a58a985f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7561,10 +7561,7 @@ def get_deprecation_warning(self, decorators: Iterable[Expression]) -> str | Non if ( isinstance(decorator, CallExpr) and isinstance(callee := decorator.callee, NameExpr) - and ( - (callee.fullname in DEPRECATED_TYPE_NAMES) - or (not callee.fullname and callee.name == "deprecated") - ) + and (callee.fullname in DEPRECATED_TYPE_NAMES) and (len(args := decorator.args) >= 1) and isinstance(arg := args[0], StrExpr) ): From cbf7574b851c7c53fb0db36c01741113df87ae8d Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 07:05:02 +0200 Subject: [PATCH 07/35] note instead of error --- mypy/checker.py | 2 +- test-data/unit/check-deprecated.test | 130 +++++++++++++-------------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a650a58a985f..1b221c33ec72 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7600,7 +7600,7 @@ def warn_deprecated( name = func.fullname else: name = type_.fullname - self.msg.fail(f"{name} is deprecated: {deprecated}", context, code=codes.DEPRECATED) + self.msg.note(f"{name} is deprecated: {deprecated}", context, code=codes.DEPRECATED) class CollectArgTypeVarTypes(TypeTraverserVisitor): diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 93774e566f81..a95d1b20eee4 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -7,14 +7,14 @@ from typing_extensions import deprecated @deprecated("use f2 instead") def f() -> None: ... -f # E: __main__.f is deprecated: use f2 instead # type: ignore[deprecated] -f(1) # E: __main__.f is deprecated: use f2 instead \ +f # N: __main__.f is deprecated: use f2 instead # type: ignore[deprecated] +f(1) # N: __main__.f is deprecated: use f2 instead \ # E: Too many arguments for "f" -f[1] # E: __main__.f is deprecated: use f2 instead \ +f[1] # N: __main__.f is deprecated: use f2 instead \ # E: Value of type "Callable[[], None]" is not indexable -g = f # E: __main__.f is deprecated: use f2 instead -g() # E: __main__.f is deprecated: use f2 instead -t = (f, f, g) # E: __main__.f is deprecated: use f2 instead +g = f # N: __main__.f is deprecated: use f2 instead +g() # N: __main__.f is deprecated: use f2 instead +t = (f, f, g) # N: __main__.f is deprecated: use f2 instead [builtins fixtures/tuple.pyi] @@ -25,17 +25,17 @@ import m import p.s import m as n import p.s as ps -from m import f # E: m.f is deprecated: use f2 instead -from p.s import g # E: p.s.g is deprecated: use g2 instead +from m import f # N: m.f is deprecated: use f2 instead +from p.s import g # N: p.s.g is deprecated: use g2 instead from k import * -m.f() # E: m.f is deprecated: use f2 instead -p.s.g() # E: p.s.g is deprecated: use g2 instead -n.f() # E: m.f is deprecated: use f2 instead -ps.g() # E: p.s.g is deprecated: use g2 instead +m.f() # N: m.f is deprecated: use f2 instead +p.s.g() # N: p.s.g is deprecated: use g2 instead +n.f() # N: m.f is deprecated: use f2 instead +ps.g() # N: p.s.g is deprecated: use g2 instead f() g() -h() # E: k.h is deprecated: use h2 instead +h() # N: k.h is deprecated: use h2 instead [file m.py] from typing_extensions import deprecated @@ -65,16 +65,16 @@ from typing_extensions import deprecated @deprecated("use C2 instead") class C: ... -c: C # E: __main__.C is deprecated: use C2 instead -C() # E: __main__.C is deprecated: use C2 instead -C.missing() # E: __main__.C is deprecated: use C2 instead \ +c: C # N: __main__.C is deprecated: use C2 instead +C() # N: __main__.C is deprecated: use C2 instead +C.missing() # N: __main__.C is deprecated: use C2 instead \ # E: "Type[C]" has no attribute "missing" -C.__init__(c) # E: __main__.C is deprecated: use C2 instead -C(1) # E: __main__.C is deprecated: use C2 instead \ +C.__init__(c) # N: __main__.C is deprecated: use C2 instead +C(1) # N: __main__.C is deprecated: use C2 instead \ # E: Too many arguments for "C" -D = C # E: __main__.C is deprecated: use C2 instead +D = C # N: __main__.C is deprecated: use C2 instead D() -t = (C, C, D) # E: __main__.C is deprecated: use C2 instead +t = (C, C, D) # N: __main__.C is deprecated: use C2 instead [builtins fixtures/tuple.pyi] @@ -85,17 +85,17 @@ import m import p.s import m as n import p.s as ps -from m import C # E: m.C is deprecated: use C2 instead -from p.s import D # E: p.s.D is deprecated: use D2 instead +from m import C # N: m.C is deprecated: use C2 instead +from p.s import D # N: p.s.D is deprecated: use D2 instead from k import * -m.C() # E: m.C is deprecated: use C2 instead -p.s.D() # E: p.s.D is deprecated: use D2 instead -n.C() # E: m.C is deprecated: use C2 instead -ps.D() # E: p.s.D is deprecated: use D2 instead +m.C() # N: m.C is deprecated: use C2 instead +p.s.D() # N: p.s.D is deprecated: use D2 instead +n.C() # N: m.C is deprecated: use C2 instead +ps.D() # N: p.s.D is deprecated: use D2 instead C() D() -E() # E: k.E is deprecated: use E2 instead +E() # N: k.E is deprecated: use E2 instead [file m.py] from typing_extensions import deprecated @@ -126,9 +126,9 @@ from typing_extensions import deprecated class C: def __init__(self) -> None: ... -c: C # E: __main__.C is deprecated: use C2 instead -C() # E: __main__.C is deprecated: use C2 instead -C.__init__(c) # E: __main__.C is deprecated: use C2 instead +c: C # N: __main__.C is deprecated: use C2 instead +C() # N: __main__.C is deprecated: use C2 instead +C.__init__(c) # N: __main__.C is deprecated: use C2 instead [builtins fixtures/tuple.pyi] @@ -166,14 +166,14 @@ class B: a = A() b = B() -a + 1 # E: __main__.A.__add__ is deprecated: no A + int -1 + a # E: __main__.A.__radd__ is deprecated: no int + A -a += 1 # E: __main__.A.__iadd__ is deprecated: no A = A + int -for i in a: # E: __main__.A.__iter__ is deprecated: no iteration +a + 1 # N: __main__.A.__add__ is deprecated: no A + int +1 + a # N: __main__.A.__radd__ is deprecated: no int + A +a += 1 # N: __main__.A.__iadd__ is deprecated: no A = A + int +for i in a: # N: __main__.A.__iter__ is deprecated: no iteration reveal_type(i) # N: Revealed type is "builtins.int" -1 in a # E: __main__.A.__iter__ is deprecated: no iteration -1 in b # E: __main__.B.__contains__ is deprecated: no in -~a # E: __main__.A.__invert__ is deprecated: no inversion +1 in a # N: __main__.A.__iter__ is deprecated: no iteration +1 in b # N: __main__.B.__contains__ is deprecated: no in +~a # N: __main__.A.__invert__ is deprecated: no inversion [builtins fixtures/tuple.pyi] @@ -206,12 +206,12 @@ class A: def __iadd__(self, v: Union[int, str]) -> A: ... a = A() -a + 1 # E: __main__.A.__add__ is deprecated: no A + int +a + 1 # N: __main__.A.__add__ is deprecated: no A + int a + "x" 1 + a -"x" + a # E: __main__.A.__radd__ is deprecated: no str + A -a += 1 # E: __main__.A.__iadd__ is deprecated: no A += Any -a += "x" # E: __main__.A.__iadd__ is deprecated: no A += Any +"x" + a # N: __main__.A.__radd__ is deprecated: no str + A +a += 1 # N: __main__.A.__iadd__ is deprecated: no A += Any +a += "x" # N: __main__.A.__iadd__ is deprecated: no A += Any [builtins fixtures/tuple.pyi] @@ -234,18 +234,18 @@ class C: @staticmethod def k() -> None: ... -C.f # E: __main__.C.f is deprecated: use g instead -C().f # E: __main__.C.f is deprecated: use g instead -C().f() # E: __main__.C.f is deprecated: use g instead -C().f(1) # E: __main__.C.f is deprecated: use g instead \ +C.f # N: __main__.C.f is deprecated: use g instead +C().f # N: __main__.C.f is deprecated: use g instead +C().f() # N: __main__.C.f is deprecated: use g instead +C().f(1) # N: __main__.C.f is deprecated: use g instead \ # E: Too many arguments for "f" of "C" -f = C().f # E: __main__.C.f is deprecated: use g instead -f() # E: __main__.C.f is deprecated: use g instead -t = (C.f, C.f, C.g) # E: __main__.C.f is deprecated: use g instead +f = C().f # N: __main__.C.f is deprecated: use g instead +f() # N: __main__.C.f is deprecated: use g instead +t = (C.f, C.f, C.g) # N: __main__.C.f is deprecated: use g instead C().g() -C().h() # E: __main__.C.h is deprecated: use g instead -C().k() # E: __main__.C.k is deprecated: use g instead +C().h() # N: __main__.C.h is deprecated: use g instead +C().k() # N: __main__.C.k is deprecated: use g instead [builtins fixtures/callable.pyi] @@ -260,9 +260,9 @@ class C: def f(self) -> None: ... def g(self) -> None: ... -C().f() # E: __main__.C is deprecated: use D instead \ - # E: __main__.C.f is deprecated: use g instead -C().g() # E: __main__.C is deprecated: use D instead +C().f() # N: __main__.C is deprecated: use D instead \ + # N: __main__.C.f is deprecated: use g instead +C().g() # N: __main__.C is deprecated: use D instead [builtins fixtures/callable.pyi] @@ -283,17 +283,17 @@ class C: def g(self, v: int) -> None: ... -C.f # E: __main__.C.f is deprecated: use f2 instead -C().f # E: __main__.C.f is deprecated: use f2 instead -C().f() # E: __main__.C.f is deprecated: use f2 instead \ +C.f # N: __main__.C.f is deprecated: use f2 instead +C().f # N: __main__.C.f is deprecated: use f2 instead +C().f() # N: __main__.C.f is deprecated: use f2 instead \ # E: "int" not callable C().f = 1 # E: Property "f" defined in "C" is read-only \ - # E: __main__.C.f is deprecated: use f2 instead + # N: __main__.C.f is deprecated: use f2 instead C.g C().g -C().g = 1 # E: __main__.C.g is deprecated: use g2 instead -C().g = "x" # E: __main__.C.g is deprecated: use g2 instead \ +C().g = 1 # N: __main__.C.g is deprecated: use g2 instead +C().g = "x" # N: __main__.C.g is deprecated: use g2 instead \ # E: Incompatible types in assignment (expression has type "str", variable has type "int") [builtins fixtures/property.pyi] @@ -311,10 +311,10 @@ def f(x: str) -> str: ... @deprecated("use f2 instead") def f(x: Union[int, str]) -> Union[int, str]: ... -f # E: __main__.f is deprecated: use f2 instead -f(1) # E: __main__.f is deprecated: use f2 instead -f("x") # E: __main__.f is deprecated: use f2 instead -f(1.0) # E: __main__.f is deprecated: use f2 instead \ +f # N: __main__.f is deprecated: use f2 instead +f(1) # N: __main__.f is deprecated: use f2 instead +f("x") # N: __main__.f is deprecated: use f2 instead +f(1.0) # N: __main__.f is deprecated: use f2 instead \ # E: No overload variant of "f" matches argument type "float" \ # N: Possible overload variants: \ # N: def f(x: int) -> int \ @@ -328,7 +328,7 @@ def g(x: str) -> str: ... def g(x: Union[int, str]) -> Union[int, str]: ... g -g(1) # E: __main__.g is deprecated: work with str instead +g(1) # N: __main__.g is deprecated: work with str instead g("x") g(1.0) # E: No overload variant of "g" matches argument type "float" \ # N: Possible overload variants: \ @@ -344,7 +344,7 @@ def h(x: Union[int, str]) -> Union[int, str]: ... h h(1) -h("x") # E: __main__.h is deprecated: work with int instead +h("x") # N: __main__.h is deprecated: work with int instead h(1.0) # E: No overload variant of "h" matches argument type "float" \ # N: Possible overload variants: \ # N: def h(x: int) -> int \ From 9a947a5360884c854c469024c87fe60de3b00c6c Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 07:42:44 +0200 Subject: [PATCH 08/35] remove walrusses --- mypy/checker.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1b221c33ec72..7bf58dd706f5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -660,15 +660,16 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: if defn.impl: defn.impl.accept(self) if ( - isinstance(impl := defn.impl, Decorator) - and isinstance(ct := impl.func.type, CallableType) - and ((deprecated := ct.deprecated) is not None) + isinstance(defn.impl, Decorator) + and isinstance(defn.impl.func.type, CallableType) + and ((deprecated := defn.impl.func.type.deprecated) is not None) ): - if isinstance(ct := defn.type, (CallableType, Overloaded)): - ct.deprecated = deprecated + if isinstance(defn.type, (CallableType, Overloaded)): + defn.type.deprecated = deprecated for subdef in defn.items: - if isinstance(ct := get_proper_type(subdef.type), (CallableType, Overloaded)): - ct.deprecated = deprecated + type_ = get_proper_type(subdef.type) + if isinstance(type_, (CallableType, Overloaded)): + type_.deprecated = deprecated if not defn.is_property: self.check_overlapping_overloads(defn) if defn.type is None: From 6d92318ee951bd42b01787109d385aa35dcbf3a1 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 09:18:35 +0200 Subject: [PATCH 09/35] document the new option --- docs/source/command_line.rst | 6 ++++++ docs/source/error_code_list2.rst | 34 ++++++++++++++++++++++++++++++++ mypy/main.py | 7 +++++++ 3 files changed, 47 insertions(+) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 50a6ef65f4d0..8a23ce8301e0 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -533,6 +533,12 @@ potentially problematic or redundant in some way. This limitation will be removed in future releases of mypy. +.. option:: --warn-deprecated + + This flag will make mypy report an error whenever your code imports a deprecated + function or class with a ``from mod import depr" statement or uses a deprecated + function, method or class imported otherwise or defined locally. Features are + considered deprecated when decorated with ``warnings.deprecated``. .. _miscellaneous-strictness-flags: diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 0655ef2d35d8..84edbe948cbf 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -231,6 +231,40 @@ incorrect control flow or conditional checks that are accidentally always true o # Error: Statement is unreachable [unreachable] print('unreachable') +.. _code-deprecated: + +Check that imported or used feature is deprecated [deprecated] +-------------------------------------------------------------- + +If you use :option:`--warn-deprecated `, mypy generates a note if +your code imports a deprecated function or class with a ``from mod import depr" statement +or uses a deprecated function, method or class imported otherwise or defined locally. +Features are considered deprecated when decorated with ``warnings.deprecated``, as +specified in `PEP 702 `_. + +.. note:: + + The ``warnings`` module provides the ``@deprecated`` decorator since Python 3.13. + To use it with older Python versions, import it from ``typing_extensions`` instead. + +Examples: + +.. code-block:: python + + # mypy: warn-deprecated + + # Note: abc.abstractproperty is deprecated: Deprecated, use 'property' with 'abstractmethod' instead + from abc import abstractproperty + + from typing_extensions import deprecated + + @deprecated("use new_function") + def old_function() -> None: + print("I am old") + + # Note: __main__.old_function is deprecated: use new_function + old_function() + .. _code-redundant-expr: Check that expression is redundant [redundant-expr] diff --git a/mypy/main.py b/mypy/main.py index 05044335ecee..514aa916dfa1 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -800,6 +800,13 @@ def add_invertible_flag( help="Warn about statements or expressions inferred to be unreachable", group=lint_group, ) + add_invertible_flag( + "--warn-deprecated", + default=False, + strict_flag=False, + help="Warn when importing or using deprecated features", + group=lint_group, + ) # Note: this group is intentionally added here even though we don't add # --strict to this group near the end. From afa0336bc6d3a695f1bd1dd456f2d23f6e872c35 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 13:11:48 +0200 Subject: [PATCH 10/35] motivate the semantic analyzer --- mypy/semanal.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index f857c3e73381..53047cad8ea4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1435,15 +1435,17 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) - for i, item in enumerate(items[1:]): if isinstance(item, Decorator): if len(item.decorators) >= 1: - node = item.decorators[0] - if isinstance(node, MemberExpr): - if node.name == "setter": + first_node = item.decorators[0] + if isinstance(first_node, MemberExpr): + if first_node.name == "setter": # The first item represents the entire property. first_item.var.is_settable_property = True # Get abstractness from the original definition. item.func.abstract_status = first_item.func.abstract_status - if node.name == "deleter": + if first_node.name == "deleter": item.func.abstract_status = first_item.func.abstract_status + for other_node in item.decorators[1:]: + other_node.accept(self) else: self.fail( f"Only supported top decorator is @{first_item.func.name}.setter", item From 7bfb534faf4af31af895044a8fca26f8c8e31108 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 18:35:28 +0200 Subject: [PATCH 11/35] `report-deprecated-as-error` instead of `warn-deprecated` and three new test cases --- docs/source/command_line.rst | 10 +++---- docs/source/error_code_list2.rst | 20 +++++++------ mypy/checker.py | 3 +- mypy/errors.py | 2 +- mypy/main.py | 4 +-- mypy/options.py | 3 ++ test-data/unit/check-deprecated.test | 42 ++++++++++++++++++++++++++++ 7 files changed, 67 insertions(+), 17 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 8a23ce8301e0..019e576b6e79 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -533,12 +533,12 @@ potentially problematic or redundant in some way. This limitation will be removed in future releases of mypy. -.. option:: --warn-deprecated +.. option:: --report-deprecated-as-error - This flag will make mypy report an error whenever your code imports a deprecated - function or class with a ``from mod import depr" statement or uses a deprecated - function, method or class imported otherwise or defined locally. Features are - considered deprecated when decorated with ``warnings.deprecated``. + By default, mypy emits notes if your code imports or uses deprecated + features. This flag converts such notes to errors, causing mypy to + eventually finish with a non-zero exit code. Features are considered + deprecated when decorated with ``warnings.deprecated``. .. _miscellaneous-strictness-flags: diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 84edbe948cbf..28ab8fdfabac 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -236,11 +236,13 @@ incorrect control flow or conditional checks that are accidentally always true o Check that imported or used feature is deprecated [deprecated] -------------------------------------------------------------- -If you use :option:`--warn-deprecated `, mypy generates a note if -your code imports a deprecated function or class with a ``from mod import depr" statement -or uses a deprecated function, method or class imported otherwise or defined locally. -Features are considered deprecated when decorated with ``warnings.deprecated``, as -specified in `PEP 702 `_. +By default, mypy generates a note if your code imports a deprecated feature explicitly with a +``from mod import depr" statement or uses a deprecated feature imported otherwise or defined +locally. Features are considered deprecated when decorated with ``warnings.deprecated``, as +specified in `PEP 702 `_. You can silence single notes via +``# type: ignore[deprecated]`` or turn off this check completely via ``--disable-error-code=deprecated``. +Use the :option:`--report-deprecated-as-error ` option for +more strictness, which turns all such notes into errors. .. note:: @@ -251,9 +253,9 @@ Examples: .. code-block:: python - # mypy: warn-deprecated + # mypy: report-deprecated-as-error - # Note: abc.abstractproperty is deprecated: Deprecated, use 'property' with 'abstractmethod' instead + # Error: abc.abstractproperty is deprecated: Deprecated, use 'property' with 'abstractmethod' instead from abc import abstractproperty from typing_extensions import deprecated @@ -262,8 +264,10 @@ Examples: def old_function() -> None: print("I am old") - # Note: __main__.old_function is deprecated: use new_function + # Error: __main__.old_function is deprecated: use new_function old_function() + old_function() # type: ignore[deprecated] + .. _code-redundant-expr: diff --git a/mypy/checker.py b/mypy/checker.py index 7bf58dd706f5..d9f5cfcc18bc 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7601,7 +7601,8 @@ def warn_deprecated( name = func.fullname else: name = type_.fullname - self.msg.note(f"{name} is deprecated: {deprecated}", context, code=codes.DEPRECATED) + warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note + warn(f"{name} is deprecated: {deprecated}", context, code=codes.DEPRECATED) class CollectArgTypeVarTypes(TypeTraverserVisitor): diff --git a/mypy/errors.py b/mypy/errors.py index d6dcd4e49e13..a4fbbd79b367 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -20,7 +20,7 @@ # Show error codes for some note-level messages (these usually appear alone # and not as a comment for a previous error-level message). -SHOW_NOTE_CODES: Final = {codes.ANNOTATION_UNCHECKED} +SHOW_NOTE_CODES: Final = {codes.ANNOTATION_UNCHECKED, codes.DEPRECATED} # Do not add notes with links to error code docs to errors with these codes. # We can tweak this set as we get more experience about what is helpful and what is not. diff --git a/mypy/main.py b/mypy/main.py index 514aa916dfa1..a0b925230702 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -801,10 +801,10 @@ def add_invertible_flag( group=lint_group, ) add_invertible_flag( - "--warn-deprecated", + "--report-deprecated-as-error", default=False, strict_flag=False, - help="Warn when importing or using deprecated features", + help="Report importing or using deprecated features as errors instead of notes", group=lint_group, ) diff --git a/mypy/options.py b/mypy/options.py index 5ef6bc2a35e7..afee07bb3c0b 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -172,6 +172,9 @@ def __init__(self) -> None: # declared with a precise type self.warn_return_any = False + # Report importing or using deprecated features as errors instead of notes. + self.report_deprecated_as_error = False + # Warn about unused '# type: ignore' comments self.warn_unused_ignores = False diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index a95d1b20eee4..1499ea2f30b2 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -1,5 +1,47 @@ -- Type checker test cases for reporting deprecations. + +[case testDeprecatedDisableNotes] +# flags: --disable-error-code=deprecated + +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +f() + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedAsNoteWithErrorCode] +# flags: --show-error-codes + +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +f() # type: ignore[deprecated] +f() # N: __main__.f is deprecated: use f2 instead [deprecated] + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedAsErrorWithErrorCode] +# flags: --report-deprecated-as-error --show-error-codes + +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +f() # type: ignore[deprecated] +f() # E: __main__.f is deprecated: use f2 instead [deprecated] + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedFunction] from typing_extensions import deprecated From 1042c6531d8e7701bf65e41bceb7c0cd8adc4c22 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 19:18:10 +0200 Subject: [PATCH 12/35] fix a typo in error_code_list2.rst Co-authored-by: Ivan Levkivskyi --- docs/source/error_code_list2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 28ab8fdfabac..aa45fe2ecbdf 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -237,7 +237,7 @@ Check that imported or used feature is deprecated [deprecated] -------------------------------------------------------------- By default, mypy generates a note if your code imports a deprecated feature explicitly with a -``from mod import depr" statement or uses a deprecated feature imported otherwise or defined +``from mod import depr`` statement or uses a deprecated feature imported otherwise or defined locally. Features are considered deprecated when decorated with ``warnings.deprecated``, as specified in `PEP 702 `_. You can silence single notes via ``# type: ignore[deprecated]`` or turn off this check completely via ``--disable-error-code=deprecated``. From 978b1a29dbd0280473ff60d33fc540c68e4128e0 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 21:53:52 +0200 Subject: [PATCH 13/35] additional note when PEP 702 is unsatisfied with the order of `@deprecated` and `@overload` --- mypy/checker.py | 11 ++++++++--- test-data/unit/check-deprecated.test | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index d9f5cfcc18bc..1c9ddef1feac 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7558,16 +7558,21 @@ def get_expression_type(self, node: Expression, type_context: Type | None = None return self.expr_checker.accept(node, type_context=type_context) def get_deprecation_warning(self, decorators: Iterable[Expression]) -> str | None: + deprecation = None for decorator in decorators: - if ( + if (isinstance(decorator, NameExpr) and (decorator.fullname in OVERLOAD_NAMES)): + if deprecation is not None: + self.msg.note("@overload should be placed before @deprecated", decorator) + elif ( isinstance(decorator, CallExpr) and isinstance(callee := decorator.callee, NameExpr) and (callee.fullname in DEPRECATED_TYPE_NAMES) and (len(args := decorator.args) >= 1) and isinstance(arg := args[0], StrExpr) + and ((value := arg.value) is not None) ): - return arg.value - return None + deprecation = value + return deprecation def check_deprecated_function(self, typ: Type, context: Context, memberaccess: bool) -> None: if isinstance(typ := get_proper_type(typ), (CallableType, Overloaded)): diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 1499ea2f30b2..93a2e1664910 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -379,8 +379,8 @@ g(1.0) # E: No overload variant of "g" matches argument type "float" \ @overload def h(x: int) -> int: ... -@overload @deprecated("work with int instead") +@overload # N: @overload should be placed before @deprecated def h(x: str) -> str: ... def h(x: Union[int, str]) -> Union[int, str]: ... From b0ced073f1701f987e9a0359587e07a5a5297598 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 22:21:32 +0200 Subject: [PATCH 14/35] mention the affected overload --- mypy/checker.py | 20 ++++++++++++++------ test-data/unit/check-deprecated.test | 8 ++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1c9ddef1feac..6d00ac822b97 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -639,7 +639,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: for item in defn.items: if isinstance(item, Decorator) and isinstance(ct := item.func.type, CallableType): - ct.deprecated = self.get_deprecation_warning(item.decorators) + ct.deprecated = self.get_deprecation_warning(item.decorators, ct) if defn.is_property: # HACK: Infer the type of the property. @@ -5028,8 +5028,8 @@ def visit_del_stmt(self, s: DelStmt) -> None: ) def visit_decorator(self, e: Decorator) -> None: - if isinstance(co := e.func.type, (CallableType, Overloaded)): - co.deprecated = self.get_deprecation_warning(e.decorators) + if isinstance(ct := e.func.type, CallableType): + ct.deprecated = self.get_deprecation_warning(e.decorators, ct) for d in e.decorators: if isinstance(d, RefExpr): if d.fullname == "typing.no_type_check": @@ -7557,10 +7557,14 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type: return self.expr_checker.accept(node, type_context=type_context) - def get_deprecation_warning(self, decorators: Iterable[Expression]) -> str | None: + def get_deprecation_warning( + self, decorators: Iterable[Expression], callable: CallableType | None = None + ) -> str | None: + overload = False deprecation = None for decorator in decorators: if (isinstance(decorator, NameExpr) and (decorator.fullname in OVERLOAD_NAMES)): + overload = True if deprecation is not None: self.msg.note("@overload should be placed before @deprecated", decorator) elif ( @@ -7572,7 +7576,11 @@ def get_deprecation_warning(self, decorators: Iterable[Expression]) -> str | Non and ((value := arg.value) is not None) ): deprecation = value - return deprecation + if deprecation is None: + return None + if not overload: + return f": {deprecation}" + return f" [overload {callable}]: {deprecation}" def check_deprecated_function(self, typ: Type, context: Context, memberaccess: bool) -> None: if isinstance(typ := get_proper_type(typ), (CallableType, Overloaded)): @@ -7607,7 +7615,7 @@ def warn_deprecated( else: name = type_.fullname warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note - warn(f"{name} is deprecated: {deprecated}", context, code=codes.DEPRECATED) + warn(f"{name} is deprecated{deprecated}", context, code=codes.DEPRECATED) class CollectArgTypeVarTypes(TypeTraverserVisitor): diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 93a2e1664910..37a6878ab554 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -248,10 +248,10 @@ class A: def __iadd__(self, v: Union[int, str]) -> A: ... a = A() -a + 1 # N: __main__.A.__add__ is deprecated: no A + int +a + 1 # N: __main__.A.__add__ is deprecated [overload def (__main__.A, builtins.int)]: no A + int a + "x" 1 + a -"x" + a # N: __main__.A.__radd__ is deprecated: no str + A +"x" + a # N: __main__.A.__radd__ is deprecated [overload def (__main__.A, builtins.str)]: no str + A a += 1 # N: __main__.A.__iadd__ is deprecated: no A += Any a += "x" # N: __main__.A.__iadd__ is deprecated: no A += Any @@ -370,7 +370,7 @@ def g(x: str) -> str: ... def g(x: Union[int, str]) -> Union[int, str]: ... g -g(1) # N: __main__.g is deprecated: work with str instead +g(1) # N: __main__.g is deprecated [overload def (x: builtins.int) -> builtins.int]: work with str instead g("x") g(1.0) # E: No overload variant of "g" matches argument type "float" \ # N: Possible overload variants: \ @@ -386,7 +386,7 @@ def h(x: Union[int, str]) -> Union[int, str]: ... h h(1) -h("x") # N: __main__.h is deprecated: work with int instead +h("x") # N: __main__.h is deprecated [overload def (x: builtins.str) -> builtins.str]: work with int instead h(1.0) # E: No overload variant of "h" matches argument type "float" \ # N: Possible overload variants: \ # N: def h(x: int) -> int \ From b527250929d3a9b0badb852058feac39d9d3cb29 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:26:27 +0000 Subject: [PATCH 15/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6d00ac822b97..8b7cd7b4f965 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7563,7 +7563,7 @@ def get_deprecation_warning( overload = False deprecation = None for decorator in decorators: - if (isinstance(decorator, NameExpr) and (decorator.fullname in OVERLOAD_NAMES)): + if isinstance(decorator, NameExpr) and (decorator.fullname in OVERLOAD_NAMES): overload = True if deprecation is not None: self.msg.note("@overload should be placed before @deprecated", decorator) From f636024bc532de119a18b66f6b104c1bea97d4dc Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jul 2024 22:36:33 +0200 Subject: [PATCH 16/35] add an annotation required by mypyc --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6d00ac822b97..fc8acb2478e2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7561,7 +7561,7 @@ def get_deprecation_warning( self, decorators: Iterable[Expression], callable: CallableType | None = None ) -> str | None: overload = False - deprecation = None + deprecation: str | None = None for decorator in decorators: if (isinstance(decorator, NameExpr) and (decorator.fullname in OVERLOAD_NAMES)): overload = True From 966ac8bd82f216d9ed82f7ae2fa3d08a970570c6 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Jul 2024 06:46:03 +0200 Subject: [PATCH 17/35] refactor: create the whole deprecation warning in one place --- mypy/checker.py | 70 ++++++++++++++++++++++++--------------------- mypy/checkmember.py | 20 ++++--------- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 17d393dba9a2..c0b9346147ff 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -639,7 +639,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: for item in defn.items: if isinstance(item, Decorator) and isinstance(ct := item.func.type, CallableType): - ct.deprecated = self.get_deprecation_warning(item.decorators, ct) + ct.deprecated = self.get_deprecation_warning(ct, item.decorators) if defn.is_property: # HACK: Infer the type of the property. @@ -2422,7 +2422,7 @@ def check__exit__return_type(self, defn: FuncItem) -> None: def visit_class_def(self, defn: ClassDef) -> None: """Type check a class definition.""" typ = defn.info - typ.deprecated = self.get_deprecation_warning(defn.decorators) + typ.deprecated = self.get_deprecation_warning(typ, defn.decorators) for base in typ.mro[1:]: if base.is_final: self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn) @@ -2854,12 +2854,10 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None: def visit_import_from(self, node: ImportFrom) -> None: for name, _ in node.names: if (sym := self.globals.get(name)) is not None: - if isinstance(sym.node, TypeInfo) and ((depr := sym.node.deprecated) is not None): - self.warn_deprecated(sym.node, depr, node) - elif isinstance(co := get_proper_type(sym.type), (CallableType, Overloaded)) and ( - (depr := co.deprecated) is not None - ): - self.warn_deprecated(co, depr, node) + if isinstance(sym.node, TypeInfo): + self.warn_deprecated(sym.node, node) + elif isinstance(co := get_proper_type(sym.type), (CallableType, Overloaded)): + self.warn_deprecated(co, node) self.check_import(node) def visit_import_all(self, node: ImportAll) -> None: @@ -5029,7 +5027,7 @@ def visit_del_stmt(self, s: DelStmt) -> None: def visit_decorator(self, e: Decorator) -> None: if isinstance(ct := e.func.type, CallableType): - ct.deprecated = self.get_deprecation_warning(e.decorators, ct) + ct.deprecated = self.get_deprecation_warning(ct, e.decorators) for d in e.decorators: if isinstance(d, RefExpr): if d.fullname == "typing.no_type_check": @@ -7558,12 +7556,26 @@ def get_expression_type(self, node: Expression, type_context: Type | None = None return self.expr_checker.accept(node, type_context=type_context) def get_deprecation_warning( - self, decorators: Iterable[Expression], callable: CallableType | None = None + self, typ: CallableType | Overloaded | TypeInfo, decorators: Iterable[Expression] ) -> str | None: + + name = typ.name + if isinstance(typ, CallableType): + if (defn := typ.definition) is not None: + name = defn.fullname + elif isinstance(typ, Overloaded): + if isinstance(func := typ.items[0].definition, FuncDef): + name = func.fullname + else: + name = typ.fullname + overload = False deprecation: str | None = None for decorator in decorators: - if isinstance(decorator, NameExpr) and (decorator.fullname in OVERLOAD_NAMES): + if (isinstance(typ, CallableType) + and isinstance(decorator, NameExpr) + and (decorator.fullname in OVERLOAD_NAMES) + ): overload = True if deprecation is not None: self.msg.note("@overload should be placed before @deprecated", decorator) @@ -7576,11 +7588,12 @@ def get_deprecation_warning( and ((value := arg.value) is not None) ): deprecation = value + if deprecation is None: return None if not overload: - return f": {deprecation}" - return f" [overload {callable}]: {deprecation}" + return f"{name} is deprecated: {deprecation}" + return f"{name} is deprecated [overload {typ}]: {deprecation}" def check_deprecated_function(self, typ: Type, context: Context, memberaccess: bool) -> None: if isinstance(typ := get_proper_type(typ), (CallableType, Overloaded)): @@ -7592,30 +7605,21 @@ def check_deprecated_class(self, typ: TypeInfo, context: Context, memberaccess: def _check_deprecated( self, typ: CallableType | Overloaded | TypeInfo, context: Context, memberaccess: bool ) -> None: - if (depr := typ.deprecated) is not None: - if memberaccess: - self.warn_deprecated(typ, depr, context) + if memberaccess: + self.warn_deprecated(typ, context) + elif typ.deprecated is not None: + for imp in self.tree.imports: + if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): + break else: - for imp in self.tree.imports: - if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): - break - else: - self.warn_deprecated(typ, depr, context) + self.warn_deprecated(typ, context) def warn_deprecated( - self, type_: CallableType | Overloaded | TypeInfo, deprecated: str, context: Context + self, typ: CallableType | Overloaded | TypeInfo, context: Context ) -> None: - name = type_.name - if isinstance(type_, CallableType): - if (defn := type_.definition) is not None: - name = defn.fullname - elif isinstance(type_, Overloaded): - if isinstance(func := type_.items[0].definition, FuncDef): - name = func.fullname - else: - name = type_.fullname - warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note - warn(f"{name} is deprecated{deprecated}", context, code=codes.DEPRECATED) + if (deprecated := typ.deprecated) is not None: + warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note + warn(deprecated, context, code=codes.DEPRECATED) class CollectArgTypeVarTypes(TypeTraverserVisitor): diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 4e801219f504..8f490ed99ac9 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -323,15 +323,11 @@ def analyze_instance_member_access( and (len(items := method.items) > 1) and isinstance(setter := items[1], Decorator) ): - if isinstance(co := setter.func.type, (CallableType, Overloaded)) and ( - (deprecated := co.deprecated) is not None - ): - mx.chk.warn_deprecated(co, deprecated, mx.context) + if isinstance(co := setter.func.type, (CallableType, Overloaded)): + mx.chk.warn_deprecated(co, mx.context) return analyze_var(name, getter.var, typ, info, mx) - elif isinstance(co := method.type, (CallableType, Overloaded)) and ( - (deprecated := co.deprecated) is not None - ): - mx.chk.warn_deprecated(co, deprecated, mx.context) + elif isinstance(co := method.type, (CallableType, Overloaded)): + mx.chk.warn_deprecated(co, mx.context) if mx.is_lvalue: mx.msg.cant_assign_to_method(mx.context) @@ -787,12 +783,8 @@ def analyze_var( result = t typ = get_proper_type(typ) - if ( - var.is_property - and isinstance(typ, CallableType) - and ((deprecated := typ.deprecated) is not None) - ): - mx.chk.warn_deprecated(typ, deprecated, mx.context) + if var.is_property and isinstance(typ, CallableType): + mx.chk.warn_deprecated(typ, mx.context) call_type: ProperType | None = None if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator): From 6a7dfe03add4b99fc1871427d08089940f757cdc Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Jul 2024 07:10:52 +0200 Subject: [PATCH 18/35] refactor: get rid of the `memberaccess` parameter --- mypy/checker.py | 25 +++++++++++-------------- mypy/checkexpr.py | 17 +++++++++-------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c0b9346147ff..8a46c87f7974 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2856,8 +2856,8 @@ def visit_import_from(self, node: ImportFrom) -> None: if (sym := self.globals.get(name)) is not None: if isinstance(sym.node, TypeInfo): self.warn_deprecated(sym.node, node) - elif isinstance(co := get_proper_type(sym.type), (CallableType, Overloaded)): - self.warn_deprecated(co, node) + elif isinstance(typ := get_proper_type(sym.type), (CallableType, Overloaded)): + self.warn_deprecated(typ, node) self.check_import(node) def visit_import_all(self, node: ImportAll) -> None: @@ -2954,7 +2954,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: and isinstance(var := lvalue.node, Var) and isinstance(instance := get_proper_type(var.type), Instance) ): - self.check_deprecated_class(instance.type, s, False) + self.check_deprecated_class(instance.type, s) # Avoid type checking type aliases in stubs to avoid false # positives about modern type syntax available in stubs such @@ -4700,7 +4700,8 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) rvalue_type, method_type = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s) - self.check_deprecated_function(method_type, s, True) + if isinstance(method_type := get_proper_type(method_type), (CallableType, Overloaded)): + self.warn_deprecated(method_type, s) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: @@ -7595,19 +7596,15 @@ def get_deprecation_warning( return f"{name} is deprecated: {deprecation}" return f"{name} is deprecated [overload {typ}]: {deprecation}" - def check_deprecated_function(self, typ: Type, context: Context, memberaccess: bool) -> None: + def check_deprecated_function(self, typ: Type, context: Context) -> None: if isinstance(typ := get_proper_type(typ), (CallableType, Overloaded)): - self._check_deprecated(typ, context, memberaccess) + self._check_deprecated(typ, context) - def check_deprecated_class(self, typ: TypeInfo, context: Context, memberaccess: bool) -> None: - self._check_deprecated(typ, context, memberaccess) + def check_deprecated_class(self, typ: TypeInfo, context: Context) -> None: + self._check_deprecated(typ, context) - def _check_deprecated( - self, typ: CallableType | Overloaded | TypeInfo, context: Context, memberaccess: bool - ) -> None: - if memberaccess: - self.warn_deprecated(typ, context) - elif typ.deprecated is not None: + def _check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context) -> None: + if typ.deprecated is not None: for imp in self.tree.imports: if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): break diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c690565a74e4..29e19c351eaa 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -356,9 +356,9 @@ def visit_name_expr(self, e: NameExpr) -> Type: result = self.analyze_ref_expr(e) narrowed = self.narrow_type_from_binder(e, result) if isinstance(e.node, TypeInfo): - self.chk.check_deprecated_class(e.node, e, False) + self.chk.check_deprecated_class(e.node, e) else: - self.chk.check_deprecated_function(narrowed, e, False) + self.chk.check_deprecated_function(narrowed, e) return narrowed def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: @@ -1479,7 +1479,7 @@ def check_call_expr_with_callee_type( callable_name=callable_name, object_type=object_type, ) - self.chk.check_deprecated_function(callee_type, e, False) + self.chk.check_deprecated_function(callee_type, e) proper_callee = get_proper_type(callee_type) if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType): # Cache it for find_isinstance_check() @@ -3260,9 +3260,9 @@ def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: result = self.analyze_ordinary_member_access(e, is_lvalue) narrowed = self.narrow_type_from_binder(e, result) if isinstance(e.node, TypeInfo): - self.chk.check_deprecated_class(e.node, e, True) - else: - self.chk.check_deprecated_function(narrowed, e, True) + self.chk.warn_deprecated(e.node, e) + elif isinstance(typ := get_proper_type(narrowed), (CallableType, Overloaded)): + self.chk.warn_deprecated(typ, e) return narrowed def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type: @@ -3488,7 +3488,7 @@ def visit_op_expr(self, e: OpExpr) -> Type: else: assert_never(use_reverse) e.method_type = method_type - self.chk.check_deprecated_function(method_type, e, False) + self.chk.check_deprecated_function(method_type, e) return result else: raise RuntimeError(f"Unknown operator {e.op}") @@ -3805,7 +3805,8 @@ def check_method_call_by_name( chk=self.chk, in_literal_context=self.is_literal_context(), ) - self.chk.check_deprecated_function(method_type, context, True) + if isinstance(mt := get_proper_type(method_type), (CallableType, Overloaded)): + self.chk.warn_deprecated(mt, context) return self.check_method_call(method, base_type, method_type, args, arg_kinds, context) def check_union_method_call_by_name( From 1a409537a8178e6b4f3258cdd1fee6cc8dfee288 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Jul 2024 07:37:32 +0200 Subject: [PATCH 19/35] refactor: merge `check_deprecated_function` and `check_deprecated_class` into one method (`check_deprecated`) --- mypy/checker.py | 13 ++++--------- mypy/checkexpr.py | 12 +++++++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8a46c87f7974..c937ab0622b1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2954,7 +2954,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: and isinstance(var := lvalue.node, Var) and isinstance(instance := get_proper_type(var.type), Instance) ): - self.check_deprecated_class(instance.type, s) + self.check_deprecated(instance.type, s) # Avoid type checking type aliases in stubs to avoid false # positives about modern type syntax available in stubs such @@ -7596,14 +7596,8 @@ def get_deprecation_warning( return f"{name} is deprecated: {deprecation}" return f"{name} is deprecated [overload {typ}]: {deprecation}" - def check_deprecated_function(self, typ: Type, context: Context) -> None: - if isinstance(typ := get_proper_type(typ), (CallableType, Overloaded)): - self._check_deprecated(typ, context) - - def check_deprecated_class(self, typ: TypeInfo, context: Context) -> None: - self._check_deprecated(typ, context) - - def _check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context) -> None: + def check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context) -> None: + """Warn if deprecated and not directly imported with a `from` statement.""" if typ.deprecated is not None: for imp in self.tree.imports: if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): @@ -7614,6 +7608,7 @@ def _check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: def warn_deprecated( self, typ: CallableType | Overloaded | TypeInfo, context: Context ) -> None: + """Warn if deprecated.""" if (deprecated := typ.deprecated) is not None: warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note warn(deprecated, context, code=codes.DEPRECATED) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 29e19c351eaa..f7c0ad709b9b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -356,9 +356,9 @@ def visit_name_expr(self, e: NameExpr) -> Type: result = self.analyze_ref_expr(e) narrowed = self.narrow_type_from_binder(e, result) if isinstance(e.node, TypeInfo): - self.chk.check_deprecated_class(e.node, e) - else: - self.chk.check_deprecated_function(narrowed, e) + self.chk.check_deprecated(e.node, e) + elif isinstance(typ := get_proper_type(narrowed), (CallableType, Overloaded)): + self.chk.check_deprecated(typ, e) return narrowed def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: @@ -1479,8 +1479,9 @@ def check_call_expr_with_callee_type( callable_name=callable_name, object_type=object_type, ) - self.chk.check_deprecated_function(callee_type, e) proper_callee = get_proper_type(callee_type) + if isinstance(proper_callee, (CallableType, Overloaded)): + self.chk.check_deprecated(proper_callee, e) if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType): # Cache it for find_isinstance_check() if proper_callee.type_guard is not None: @@ -3488,7 +3489,8 @@ def visit_op_expr(self, e: OpExpr) -> Type: else: assert_never(use_reverse) e.method_type = method_type - self.chk.check_deprecated_function(method_type, e) + if isinstance(mt := get_proper_type(method_type), (CallableType, Overloaded)): + self.chk.check_deprecated(mt, e) return result else: raise RuntimeError(f"Unknown operator {e.op}") From 1372e66a9fde1a4163ba585c8e424e5f65e91e71 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Jul 2024 07:48:03 +0200 Subject: [PATCH 20/35] refactor: convert `get_deprecation_warning` to `create_deprecation_warning` and document it --- mypy/checker.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c937ab0622b1..58d4c4f671e9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -639,7 +639,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: for item in defn.items: if isinstance(item, Decorator) and isinstance(ct := item.func.type, CallableType): - ct.deprecated = self.get_deprecation_warning(ct, item.decorators) + self.create_deprecation_warning(ct, item.decorators) if defn.is_property: # HACK: Infer the type of the property. @@ -2422,7 +2422,7 @@ def check__exit__return_type(self, defn: FuncItem) -> None: def visit_class_def(self, defn: ClassDef) -> None: """Type check a class definition.""" typ = defn.info - typ.deprecated = self.get_deprecation_warning(typ, defn.decorators) + self.create_deprecation_warning(typ, defn.decorators) for base in typ.mro[1:]: if base.is_final: self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn) @@ -5028,7 +5028,7 @@ def visit_del_stmt(self, s: DelStmt) -> None: def visit_decorator(self, e: Decorator) -> None: if isinstance(ct := e.func.type, CallableType): - ct.deprecated = self.get_deprecation_warning(ct, e.decorators) + self.create_deprecation_warning(ct, e.decorators) for d in e.decorators: if isinstance(d, RefExpr): if d.fullname == "typing.no_type_check": @@ -7556,9 +7556,11 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type: return self.expr_checker.accept(node, type_context=type_context) - def get_deprecation_warning( + def create_deprecation_warning( self, typ: CallableType | Overloaded | TypeInfo, decorators: Iterable[Expression] - ) -> str | None: + ) -> None: + """Create a warning when visiting a deprecated (decorated) callable, overload or + class that may be used later if the deprecated feature is used.""" name = typ.name if isinstance(typ, CallableType): @@ -7590,11 +7592,11 @@ def get_deprecation_warning( ): deprecation = value - if deprecation is None: - return None - if not overload: - return f"{name} is deprecated: {deprecation}" - return f"{name} is deprecated [overload {typ}]: {deprecation}" + if deprecation is not None: + if overload: + typ.deprecated = f"{name} is deprecated [overload {typ}]: {deprecation}" + else: + typ.deprecated = f"{name} is deprecated: {deprecation}" def check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context) -> None: """Warn if deprecated and not directly imported with a `from` statement.""" From 286371f9e5aed8ff9883e4f811a8cacd872b7a6a Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Jul 2024 08:04:02 +0200 Subject: [PATCH 21/35] prefix the warnings with `class ...`, `function ...`, or `overload ... of function ...` --- mypy/checker.py | 15 +-- test-data/unit/check-deprecated.test | 134 +++++++++++++-------------- 2 files changed, 76 insertions(+), 73 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 58d4c4f671e9..cc721c9b0a5c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7562,15 +7562,18 @@ def create_deprecation_warning( """Create a warning when visiting a deprecated (decorated) callable, overload or class that may be used later if the deprecated feature is used.""" - name = typ.name if isinstance(typ, CallableType): - if (defn := typ.definition) is not None: - name = defn.fullname + if (defn := typ.definition) is None: + name = f"function {typ.name}" + else: + name = f"function {defn.fullname}" elif isinstance(typ, Overloaded): if isinstance(func := typ.items[0].definition, FuncDef): - name = func.fullname + name = f"function {func.fullname}" + else: + name = f"function {typ.name}" else: - name = typ.fullname + name = f"class {typ.fullname}" overload = False deprecation: str | None = None @@ -7594,7 +7597,7 @@ class that may be used later if the deprecated feature is used.""" if deprecation is not None: if overload: - typ.deprecated = f"{name} is deprecated [overload {typ}]: {deprecation}" + typ.deprecated = f"overload {typ} of {name} is deprecated: {deprecation}" else: typ.deprecated = f"{name} is deprecated: {deprecation}" diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 37a6878ab554..94af203d5a82 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -23,7 +23,7 @@ from typing_extensions import deprecated def f() -> None: ... f() # type: ignore[deprecated] -f() # N: __main__.f is deprecated: use f2 instead [deprecated] +f() # N: function __main__.f is deprecated: use f2 instead [deprecated] [builtins fixtures/tuple.pyi] @@ -37,7 +37,7 @@ from typing_extensions import deprecated def f() -> None: ... f() # type: ignore[deprecated] -f() # E: __main__.f is deprecated: use f2 instead [deprecated] +f() # E: function __main__.f is deprecated: use f2 instead [deprecated] [builtins fixtures/tuple.pyi] @@ -49,14 +49,14 @@ from typing_extensions import deprecated @deprecated("use f2 instead") def f() -> None: ... -f # N: __main__.f is deprecated: use f2 instead # type: ignore[deprecated] -f(1) # N: __main__.f is deprecated: use f2 instead \ +f # N: function __main__.f is deprecated: use f2 instead # type: ignore[deprecated] +f(1) # N: function __main__.f is deprecated: use f2 instead \ # E: Too many arguments for "f" -f[1] # N: __main__.f is deprecated: use f2 instead \ +f[1] # N: function __main__.f is deprecated: use f2 instead \ # E: Value of type "Callable[[], None]" is not indexable -g = f # N: __main__.f is deprecated: use f2 instead -g() # N: __main__.f is deprecated: use f2 instead -t = (f, f, g) # N: __main__.f is deprecated: use f2 instead +g = f # N: function __main__.f is deprecated: use f2 instead +g() # N: function __main__.f is deprecated: use f2 instead +t = (f, f, g) # N: function __main__.f is deprecated: use f2 instead [builtins fixtures/tuple.pyi] @@ -67,17 +67,17 @@ import m import p.s import m as n import p.s as ps -from m import f # N: m.f is deprecated: use f2 instead -from p.s import g # N: p.s.g is deprecated: use g2 instead +from m import f # N: function m.f is deprecated: use f2 instead +from p.s import g # N: function p.s.g is deprecated: use g2 instead from k import * -m.f() # N: m.f is deprecated: use f2 instead -p.s.g() # N: p.s.g is deprecated: use g2 instead -n.f() # N: m.f is deprecated: use f2 instead -ps.g() # N: p.s.g is deprecated: use g2 instead +m.f() # N: function m.f is deprecated: use f2 instead +p.s.g() # N: function p.s.g is deprecated: use g2 instead +n.f() # N: function m.f is deprecated: use f2 instead +ps.g() # N: function p.s.g is deprecated: use g2 instead f() g() -h() # N: k.h is deprecated: use h2 instead +h() # N: function k.h is deprecated: use h2 instead [file m.py] from typing_extensions import deprecated @@ -107,16 +107,16 @@ from typing_extensions import deprecated @deprecated("use C2 instead") class C: ... -c: C # N: __main__.C is deprecated: use C2 instead -C() # N: __main__.C is deprecated: use C2 instead -C.missing() # N: __main__.C is deprecated: use C2 instead \ +c: C # N: class __main__.C is deprecated: use C2 instead +C() # N: class __main__.C is deprecated: use C2 instead +C.missing() # N: class __main__.C is deprecated: use C2 instead \ # E: "Type[C]" has no attribute "missing" -C.__init__(c) # N: __main__.C is deprecated: use C2 instead -C(1) # N: __main__.C is deprecated: use C2 instead \ +C.__init__(c) # N: class __main__.C is deprecated: use C2 instead +C(1) # N: class __main__.C is deprecated: use C2 instead \ # E: Too many arguments for "C" -D = C # N: __main__.C is deprecated: use C2 instead +D = C # N: class __main__.C is deprecated: use C2 instead D() -t = (C, C, D) # N: __main__.C is deprecated: use C2 instead +t = (C, C, D) # N: class __main__.C is deprecated: use C2 instead [builtins fixtures/tuple.pyi] @@ -127,17 +127,17 @@ import m import p.s import m as n import p.s as ps -from m import C # N: m.C is deprecated: use C2 instead -from p.s import D # N: p.s.D is deprecated: use D2 instead +from m import C # N: class m.C is deprecated: use C2 instead +from p.s import D # N: class p.s.D is deprecated: use D2 instead from k import * -m.C() # N: m.C is deprecated: use C2 instead -p.s.D() # N: p.s.D is deprecated: use D2 instead -n.C() # N: m.C is deprecated: use C2 instead -ps.D() # N: p.s.D is deprecated: use D2 instead +m.C() # N: class m.C is deprecated: use C2 instead +p.s.D() # N: class p.s.D is deprecated: use D2 instead +n.C() # N: class m.C is deprecated: use C2 instead +ps.D() # N: class p.s.D is deprecated: use D2 instead C() D() -E() # N: k.E is deprecated: use E2 instead +E() # N: class k.E is deprecated: use E2 instead [file m.py] from typing_extensions import deprecated @@ -168,9 +168,9 @@ from typing_extensions import deprecated class C: def __init__(self) -> None: ... -c: C # N: __main__.C is deprecated: use C2 instead -C() # N: __main__.C is deprecated: use C2 instead -C.__init__(c) # N: __main__.C is deprecated: use C2 instead +c: C # N: class __main__.C is deprecated: use C2 instead +C() # N: class __main__.C is deprecated: use C2 instead +C.__init__(c) # N: class __main__.C is deprecated: use C2 instead [builtins fixtures/tuple.pyi] @@ -208,14 +208,14 @@ class B: a = A() b = B() -a + 1 # N: __main__.A.__add__ is deprecated: no A + int -1 + a # N: __main__.A.__radd__ is deprecated: no int + A -a += 1 # N: __main__.A.__iadd__ is deprecated: no A = A + int -for i in a: # N: __main__.A.__iter__ is deprecated: no iteration +a + 1 # N: function __main__.A.__add__ is deprecated: no A + int +1 + a # N: function __main__.A.__radd__ is deprecated: no int + A +a += 1 # N: function __main__.A.__iadd__ is deprecated: no A = A + int +for i in a: # N: function __main__.A.__iter__ is deprecated: no iteration reveal_type(i) # N: Revealed type is "builtins.int" -1 in a # N: __main__.A.__iter__ is deprecated: no iteration -1 in b # N: __main__.B.__contains__ is deprecated: no in -~a # N: __main__.A.__invert__ is deprecated: no inversion +1 in a # N: function __main__.A.__iter__ is deprecated: no iteration +1 in b # N: function __main__.B.__contains__ is deprecated: no in +~a # N: function __main__.A.__invert__ is deprecated: no inversion [builtins fixtures/tuple.pyi] @@ -248,12 +248,12 @@ class A: def __iadd__(self, v: Union[int, str]) -> A: ... a = A() -a + 1 # N: __main__.A.__add__ is deprecated [overload def (__main__.A, builtins.int)]: no A + int +a + 1 # N: overload def (__main__.A, builtins.int) of function __main__.A.__add__ is deprecated: no A + int a + "x" 1 + a -"x" + a # N: __main__.A.__radd__ is deprecated [overload def (__main__.A, builtins.str)]: no str + A -a += 1 # N: __main__.A.__iadd__ is deprecated: no A += Any -a += "x" # N: __main__.A.__iadd__ is deprecated: no A += Any +"x" + a # N: overload def (__main__.A, builtins.str) of function __main__.A.__radd__ is deprecated: no str + A +a += 1 # N: function __main__.A.__iadd__ is deprecated: no A += Any +a += "x" # N: function __main__.A.__iadd__ is deprecated: no A += Any [builtins fixtures/tuple.pyi] @@ -276,18 +276,18 @@ class C: @staticmethod def k() -> None: ... -C.f # N: __main__.C.f is deprecated: use g instead -C().f # N: __main__.C.f is deprecated: use g instead -C().f() # N: __main__.C.f is deprecated: use g instead -C().f(1) # N: __main__.C.f is deprecated: use g instead \ +C.f # N: function __main__.C.f is deprecated: use g instead +C().f # N: function __main__.C.f is deprecated: use g instead +C().f() # N: function __main__.C.f is deprecated: use g instead +C().f(1) # N: function __main__.C.f is deprecated: use g instead \ # E: Too many arguments for "f" of "C" -f = C().f # N: __main__.C.f is deprecated: use g instead -f() # N: __main__.C.f is deprecated: use g instead -t = (C.f, C.f, C.g) # N: __main__.C.f is deprecated: use g instead +f = C().f # N: function __main__.C.f is deprecated: use g instead +f() # N: function __main__.C.f is deprecated: use g instead +t = (C.f, C.f, C.g) # N: function __main__.C.f is deprecated: use g instead C().g() -C().h() # N: __main__.C.h is deprecated: use g instead -C().k() # N: __main__.C.k is deprecated: use g instead +C().h() # N: function __main__.C.h is deprecated: use g instead +C().k() # N: function __main__.C.k is deprecated: use g instead [builtins fixtures/callable.pyi] @@ -302,9 +302,9 @@ class C: def f(self) -> None: ... def g(self) -> None: ... -C().f() # N: __main__.C is deprecated: use D instead \ - # N: __main__.C.f is deprecated: use g instead -C().g() # N: __main__.C is deprecated: use D instead +C().f() # N: class __main__.C is deprecated: use D instead \ + # N: function __main__.C.f is deprecated: use g instead +C().g() # N: class __main__.C is deprecated: use D instead [builtins fixtures/callable.pyi] @@ -325,17 +325,17 @@ class C: def g(self, v: int) -> None: ... -C.f # N: __main__.C.f is deprecated: use f2 instead -C().f # N: __main__.C.f is deprecated: use f2 instead -C().f() # N: __main__.C.f is deprecated: use f2 instead \ +C.f # N: function __main__.C.f is deprecated: use f2 instead +C().f # N: function __main__.C.f is deprecated: use f2 instead +C().f() # N: function __main__.C.f is deprecated: use f2 instead \ # E: "int" not callable C().f = 1 # E: Property "f" defined in "C" is read-only \ - # N: __main__.C.f is deprecated: use f2 instead + # N: function __main__.C.f is deprecated: use f2 instead C.g C().g -C().g = 1 # N: __main__.C.g is deprecated: use g2 instead -C().g = "x" # N: __main__.C.g is deprecated: use g2 instead \ +C().g = 1 # N: function __main__.C.g is deprecated: use g2 instead +C().g = "x" # N: function __main__.C.g is deprecated: use g2 instead \ # E: Incompatible types in assignment (expression has type "str", variable has type "int") [builtins fixtures/property.pyi] @@ -353,10 +353,10 @@ def f(x: str) -> str: ... @deprecated("use f2 instead") def f(x: Union[int, str]) -> Union[int, str]: ... -f # N: __main__.f is deprecated: use f2 instead -f(1) # N: __main__.f is deprecated: use f2 instead -f("x") # N: __main__.f is deprecated: use f2 instead -f(1.0) # N: __main__.f is deprecated: use f2 instead \ +f # N: function __main__.f is deprecated: use f2 instead +f(1) # N: function __main__.f is deprecated: use f2 instead +f("x") # N: function __main__.f is deprecated: use f2 instead +f(1.0) # N: function __main__.f is deprecated: use f2 instead \ # E: No overload variant of "f" matches argument type "float" \ # N: Possible overload variants: \ # N: def f(x: int) -> int \ @@ -370,7 +370,7 @@ def g(x: str) -> str: ... def g(x: Union[int, str]) -> Union[int, str]: ... g -g(1) # N: __main__.g is deprecated [overload def (x: builtins.int) -> builtins.int]: work with str instead +g(1) # N: overload def (x: builtins.int) -> builtins.int of function __main__.g is deprecated: work with str instead g("x") g(1.0) # E: No overload variant of "g" matches argument type "float" \ # N: Possible overload variants: \ @@ -386,7 +386,7 @@ def h(x: Union[int, str]) -> Union[int, str]: ... h h(1) -h("x") # N: __main__.h is deprecated [overload def (x: builtins.str) -> builtins.str]: work with int instead +h("x") # N: overload def (x: builtins.str) -> builtins.str of function __main__.h is deprecated: work with int instead h(1.0) # E: No overload variant of "h" matches argument type "float" \ # N: Possible overload variants: \ # N: def h(x: int) -> int \ From 6f54dab827f7482490d575c5f889139797b48975 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 06:04:34 +0000 Subject: [PATCH 22/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index cc721c9b0a5c..b997e9598a96 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7578,7 +7578,8 @@ class that may be used later if the deprecated feature is used.""" overload = False deprecation: str | None = None for decorator in decorators: - if (isinstance(typ, CallableType) + if ( + isinstance(typ, CallableType) and isinstance(decorator, NameExpr) and (decorator.fullname in OVERLOAD_NAMES) ): @@ -7601,7 +7602,9 @@ class that may be used later if the deprecated feature is used.""" else: typ.deprecated = f"{name} is deprecated: {deprecation}" - def check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context) -> None: + def check_deprecated( + self, typ: CallableType | Overloaded | TypeInfo, context: Context + ) -> None: """Warn if deprecated and not directly imported with a `from` statement.""" if typ.deprecated is not None: for imp in self.tree.imports: @@ -7610,9 +7613,7 @@ def check_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: C else: self.warn_deprecated(typ, context) - def warn_deprecated( - self, typ: CallableType | Overloaded | TypeInfo, context: Context - ) -> None: + def warn_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context) -> None: """Warn if deprecated.""" if (deprecated := typ.deprecated) is not None: warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note From 8c0260e8b4f529fa9c84cf648c815cf8bcc20e16 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 27 Aug 2024 21:31:27 +0200 Subject: [PATCH 23/35] Consider `CallableType.deprecated` in `__hash__`, `__eq__`, `serialize`, and `deserialize`. --- mypy/types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy/types.py b/mypy/types.py index a957a10e478b..5b2a5e3a6f4d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2202,6 +2202,7 @@ def __hash__(self) -> int: tuple(self.arg_names), tuple(self.arg_kinds), self.fallback, + self.deprecated, ) ) @@ -2216,6 +2217,7 @@ def __eq__(self, other: object) -> bool: and self.is_type_obj() == other.is_type_obj() and self.is_ellipsis_args == other.is_ellipsis_args and self.fallback == other.fallback + and self.deprecated == other.deprecated ) else: return NotImplemented @@ -2242,6 +2244,7 @@ def serialize(self) -> JsonDict: "from_concatenate": self.from_concatenate, "imprecise_arg_kinds": self.imprecise_arg_kinds, "unpack_kwargs": self.unpack_kwargs, + "deprecated": self.deprecated, } @classmethod @@ -2267,6 +2270,7 @@ def deserialize(cls, data: JsonDict) -> CallableType: from_concatenate=data["from_concatenate"], imprecise_arg_kinds=data["imprecise_arg_kinds"], unpack_kwargs=data["unpack_kwargs"], + deprecated=data["deprecated"], ) From cf2dcaf41ddff5a5df2777911675d4bc3f1206f4 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 27 Aug 2024 21:38:18 +0200 Subject: [PATCH 24/35] Add a few fine-grained tests (of which `testAddFunctionDeprecationIndirectImport1-only_when_nocache`) fails due to currently unknown reasons. --- test-data/unit/fine-grained.test | 136 +++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 2ad31311a402..2d6675a5316b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10545,3 +10545,139 @@ m.py:9: error: Argument 1 to "foo" has incompatible type "int"; expected "str" m.py:9: error: Argument 2 to "foo" has incompatible type "str"; expected "int" m.py:10: error: Unexpected keyword argument "a" for "foo" partial.py:4: note: "foo" defined here + +[case testAddKeepChangeAndRemoveFunctionDeprecation] +from a import f +f() +import a +a.f() + +[file a.py] +def f() -> None: ... + +[file a.py.2] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[file a.py.3] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[file a.py.4] +from typing_extensions import deprecated +@deprecated("use f3 instead") +def f() -> None: ... + +[file a.py.5] +def f() -> None: ... + +[builtins fixtures/tuple.pyi] +[out] +== +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +== +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +== +main:1: note: function a.f is deprecated: use f3 instead +main:4: note: function a.f is deprecated: use f3 instead +== + + +[case testRemoveFunctionDeprecation] +from a import f +f() +import a +a.f() + +[file a.py] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[file a.py.2] +def f() -> None: ... + +[builtins fixtures/tuple.pyi] +[out] +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +== + +[case testKeepFunctionDeprecation] +from a import f +f() +import a +a.f() + +[file a.py] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[file a.py.2] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[builtins fixtures/tuple.pyi] +[out] +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +== +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead + + +[case testAddFunctionDeprecationIndirectImport1-only_when_nocache] +from b import f +f() +import b +b.f() + +[file b.py] +from a import f + +[file a.py] +def f() -> int: ... + +[file a.py.2] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> int: ... + + +[builtins fixtures/tuple.pyi] +[out] +== +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +b.py:1: note: function a.f is deprecated: use f2 instead + +[case testAddFunctionDeprecationIndirectImport2-only_when_cache] +from b import f +f() +import b +b.f() + +[file b.py] +from a import f + +[file a.py] +def f() -> int: ... + +[file a.py.2] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> int: ... + + +[builtins fixtures/tuple.pyi] +[out] +== +b.py:1: note: function a.f is deprecated: use f2 instead +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead From a6d0e59e29523f0493a4a7433c42fd173d262ea7 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 22 Sep 2024 19:34:13 +0200 Subject: [PATCH 25/35] Move the complete creation process of warning notes from `checker.py` to `semanal.py`. --- mypy/checker.py | 65 ------------------------------------------------- mypy/semanal.py | 58 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 65 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b997e9598a96..9726e1c0f75c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -184,7 +184,6 @@ ) from mypy.types import ( ANY_STRATEGY, - DEPRECATED_TYPE_NAMES, MYPYC_NATIVE_INT_NAMES, OVERLOAD_NAMES, AnyType, @@ -637,10 +636,6 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: if len(defn.items) == 1: self.fail(message_registry.MULTIPLE_OVERLOADS_REQUIRED, defn) - for item in defn.items: - if isinstance(item, Decorator) and isinstance(ct := item.func.type, CallableType): - self.create_deprecation_warning(ct, item.decorators) - if defn.is_property: # HACK: Infer the type of the property. assert isinstance(defn.items[0], Decorator) @@ -659,17 +654,6 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: self.fail(message_registry.INCONSISTENT_ABSTRACT_OVERLOAD, defn) if defn.impl: defn.impl.accept(self) - if ( - isinstance(defn.impl, Decorator) - and isinstance(defn.impl.func.type, CallableType) - and ((deprecated := defn.impl.func.type.deprecated) is not None) - ): - if isinstance(defn.type, (CallableType, Overloaded)): - defn.type.deprecated = deprecated - for subdef in defn.items: - type_ = get_proper_type(subdef.type) - if isinstance(type_, (CallableType, Overloaded)): - type_.deprecated = deprecated if not defn.is_property: self.check_overlapping_overloads(defn) if defn.type is None: @@ -2422,7 +2406,6 @@ def check__exit__return_type(self, defn: FuncItem) -> None: def visit_class_def(self, defn: ClassDef) -> None: """Type check a class definition.""" typ = defn.info - self.create_deprecation_warning(typ, defn.decorators) for base in typ.mro[1:]: if base.is_final: self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn) @@ -5027,8 +5010,6 @@ def visit_del_stmt(self, s: DelStmt) -> None: ) def visit_decorator(self, e: Decorator) -> None: - if isinstance(ct := e.func.type, CallableType): - self.create_deprecation_warning(ct, e.decorators) for d in e.decorators: if isinstance(d, RefExpr): if d.fullname == "typing.no_type_check": @@ -7556,52 +7537,6 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type: return self.expr_checker.accept(node, type_context=type_context) - def create_deprecation_warning( - self, typ: CallableType | Overloaded | TypeInfo, decorators: Iterable[Expression] - ) -> None: - """Create a warning when visiting a deprecated (decorated) callable, overload or - class that may be used later if the deprecated feature is used.""" - - if isinstance(typ, CallableType): - if (defn := typ.definition) is None: - name = f"function {typ.name}" - else: - name = f"function {defn.fullname}" - elif isinstance(typ, Overloaded): - if isinstance(func := typ.items[0].definition, FuncDef): - name = f"function {func.fullname}" - else: - name = f"function {typ.name}" - else: - name = f"class {typ.fullname}" - - overload = False - deprecation: str | None = None - for decorator in decorators: - if ( - isinstance(typ, CallableType) - and isinstance(decorator, NameExpr) - and (decorator.fullname in OVERLOAD_NAMES) - ): - overload = True - if deprecation is not None: - self.msg.note("@overload should be placed before @deprecated", decorator) - elif ( - isinstance(decorator, CallExpr) - and isinstance(callee := decorator.callee, NameExpr) - and (callee.fullname in DEPRECATED_TYPE_NAMES) - and (len(args := decorator.args) >= 1) - and isinstance(arg := args[0], StrExpr) - and ((value := arg.value) is not None) - ): - deprecation = value - - if deprecation is not None: - if overload: - typ.deprecated = f"overload {typ} of {name} is deprecated: {deprecation}" - else: - typ.deprecated = f"{name} is deprecated: {deprecation}" - def check_deprecated( self, typ: CallableType | Overloaded | TypeInfo, context: Context ) -> None: diff --git a/mypy/semanal.py b/mypy/semanal.py index 53047cad8ea4..d2eb1fde0d2e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -249,6 +249,7 @@ from mypy.types import ( ASSERT_TYPE_NAMES, DATACLASS_TRANSFORM_NAMES, + DEPRECATED_TYPE_NAMES, FINAL_DECORATOR_NAMES, FINAL_TYPE_NAMES, IMPORTED_REVEAL_TYPE_NAMES, @@ -1255,10 +1256,51 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: return # We know this is an overload def. Infer properties and perform some checks. + self.process_deprecated_overload(defn) self.process_final_in_overload(defn) self.process_static_or_class_method_in_overload(defn) self.process_overload_impl(defn) + def process_deprecated_overload(self, defn: OverloadedFuncDef) -> None: + if defn.is_property: + return + + if isinstance(impl := defn.impl, Decorator) and isinstance( + type_ := impl.func.type, CallableType + ) and ( + (deprecated := type_.deprecated) is not None + ): + if isinstance(defn.type, Overloaded): + defn.type.deprecated = deprecated + for item in defn.items: + if isinstance(item, Decorator) and isinstance(item.func.type, Overloaded): + item.func.type.deprecated = deprecated + + for item in defn.items: + deprecation = False + if isinstance(item, Decorator): + for d in item.decorators: + if deprecation and refers_to_fullname(d, OVERLOAD_NAMES): + self.msg.note("@overload should be placed before @deprecated", d) + elif (deprecated := self.get_deprecated(d)) is not None: + deprecation = True + if isinstance(type_ := item.func.type, CallableType): + type_.deprecated = ( + f"overload {type_} of function {defn.fullname} is deprecated: " + f"{deprecated}" + ) + + @staticmethod + def get_deprecated(expression: Expression) -> str | None: + if ( + isinstance(expression, CallExpr) + and refers_to_fullname(expression.callee, DEPRECATED_TYPE_NAMES) + and (len(args := expression.args) >= 1) + and isinstance(deprecated := args[0], StrExpr) + ): + return deprecated.value + return None + def process_overload_impl(self, defn: OverloadedFuncDef) -> None: """Set flags for an overload implementation. @@ -1457,6 +1499,15 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) - for i in reversed(deleted_items): del items[i] + for item in items[1:]: + if isinstance(item, Decorator): + for d in item.decorators: + if (deprecated := self.get_deprecated(d)) is not None: + if isinstance(type_ := item.func.type, CallableType): + type_.deprecated = ( + f"function {item.fullname} is deprecated: {deprecated}" + ) + def add_function_to_symbol_table(self, func: FuncDef | OverloadedFuncDef) -> None: if self.is_class_scope(): assert self.type is not None @@ -1660,6 +1711,11 @@ def visit_decorator(self, dec: Decorator) -> None: d.callee, DATACLASS_TRANSFORM_NAMES ): dec.func.dataclass_transform_spec = self.parse_dataclass_transform_spec(d) + elif (deprecated := self.get_deprecated(d)) is not None: + if isinstance(type_ := dec.func.type, CallableType): + type_.deprecated = ( + f"function {dec.fullname} is deprecated: {deprecated}" + ) elif not dec.var.is_property: # We have seen a "non-trivial" decorator before seeing @property, if # we will see a @property later, give an error, as we don't support this. @@ -2081,6 +2137,8 @@ def analyze_class_decorator_common( info.is_final = True elif refers_to_fullname(decorator, TYPE_CHECK_ONLY_NAMES): info.is_type_check_only = True + elif (deprecated := self.get_deprecated(decorator)) is not None: + info.deprecated = f"class {defn.fullname} is deprecated: {deprecated}" def clean_up_bases_and_infer_type_variables( self, defn: ClassDef, base_type_exprs: list[Expression], context: Context From 6163787eb3d0f8c7a2be0e4f0d1bbab1198ae292 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 17:34:51 +0000 Subject: [PATCH 26/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/semanal.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index d2eb1fde0d2e..56d5174b05da 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1265,10 +1265,10 @@ def process_deprecated_overload(self, defn: OverloadedFuncDef) -> None: if defn.is_property: return - if isinstance(impl := defn.impl, Decorator) and isinstance( - type_ := impl.func.type, CallableType - ) and ( - (deprecated := type_.deprecated) is not None + if ( + isinstance(impl := defn.impl, Decorator) + and isinstance(type_ := impl.func.type, CallableType) + and ((deprecated := type_.deprecated) is not None) ): if isinstance(defn.type, Overloaded): defn.type.deprecated = deprecated @@ -1713,9 +1713,7 @@ def visit_decorator(self, dec: Decorator) -> None: dec.func.dataclass_transform_spec = self.parse_dataclass_transform_spec(d) elif (deprecated := self.get_deprecated(d)) is not None: if isinstance(type_ := dec.func.type, CallableType): - type_.deprecated = ( - f"function {dec.fullname} is deprecated: {deprecated}" - ) + type_.deprecated = f"function {dec.fullname} is deprecated: {deprecated}" elif not dec.var.is_property: # We have seen a "non-trivial" decorator before seeing @property, if # we will see a @property later, give an error, as we don't support this. From 49ee7a8ee53907b8b65665adcc756e811780811b Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 28 Sep 2024 23:46:04 +0200 Subject: [PATCH 27/35] Move the `deprecation` attribute from the nodes `CallableType` and `Overloaded` to the symbol nodes `FuncDef` and `OverloadedFuncDef` --- mypy/checker.py | 38 +++++++++++++++------- mypy/checkexpr.py | 48 +++++++++++++++++----------- mypy/checkmember.py | 18 ++++------- mypy/errors.py | 3 ++ mypy/nodes.py | 10 +++++- mypy/semanal.py | 31 +++++++++--------- mypy/types.py | 17 ++-------- test-data/unit/check-deprecated.test | 15 +++++---- 8 files changed, 98 insertions(+), 82 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9726e1c0f75c..42ba11e32194 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2837,10 +2837,7 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None: def visit_import_from(self, node: ImportFrom) -> None: for name, _ in node.names: if (sym := self.globals.get(name)) is not None: - if isinstance(sym.node, TypeInfo): - self.warn_deprecated(sym.node, node) - elif isinstance(typ := get_proper_type(sym.type), (CallableType, Overloaded)): - self.warn_deprecated(typ, node) + self.warn_deprecated(sym.node, node) self.check_import(node) def visit_import_all(self, node: ImportAll) -> None: @@ -4683,8 +4680,17 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) rvalue_type, method_type = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s) - if isinstance(method_type := get_proper_type(method_type), (CallableType, Overloaded)): - self.warn_deprecated(method_type, s) + if ( + isinstance(inst := get_proper_type(lvalue_type), Instance) + and isinstance(defn := inst.type.get_method(method), OverloadedFuncDef) + ): + for item in defn.items: + if ( + isinstance(item, Decorator) + and isinstance(typ := item.func.type, CallableType) + and (bind_self(typ) == method_type) + ): + self.warn_deprecated(item.func, s) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: @@ -7537,20 +7543,28 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type: return self.expr_checker.accept(node, type_context=type_context) - def check_deprecated( - self, typ: CallableType | Overloaded | TypeInfo, context: Context - ) -> None: + def check_deprecated(self, typ: SymbolNode | None, context: Context) -> None: """Warn if deprecated and not directly imported with a `from` statement.""" - if typ.deprecated is not None: + if isinstance(typ, Decorator): + typ = typ.func + if ( + isinstance(typ, (FuncDef, OverloadedFuncDef, TypeInfo)) + and (typ.deprecated is not None) + ): for imp in self.tree.imports: if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): break else: self.warn_deprecated(typ, context) - def warn_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context) -> None: + def warn_deprecated(self, typ: SymbolNode | None, context: Context) -> None: """Warn if deprecated.""" - if (deprecated := typ.deprecated) is not None: + if isinstance(typ, Decorator): + typ = typ.func + if ( + isinstance(typ, (FuncDef, OverloadedFuncDef, TypeInfo)) + and ((deprecated := typ.deprecated) is not None) + ): warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note warn(deprecated, context, code=codes.DEPRECATED) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f7c0ad709b9b..fc40e5e8a939 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -126,6 +126,7 @@ validate_instance, ) from mypy.typeops import ( + bind_self, callable_type, custom_special_method, erase_to_union_or_bound, @@ -355,10 +356,7 @@ def visit_name_expr(self, e: NameExpr) -> Type: self.chk.module_refs.update(extract_refexpr_names(e)) result = self.analyze_ref_expr(e) narrowed = self.narrow_type_from_binder(e, result) - if isinstance(e.node, TypeInfo): - self.chk.check_deprecated(e.node, e) - elif isinstance(typ := get_proper_type(narrowed), (CallableType, Overloaded)): - self.chk.check_deprecated(typ, e) + self.chk.check_deprecated(e.node, e) return narrowed def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: @@ -1480,8 +1478,10 @@ def check_call_expr_with_callee_type( object_type=object_type, ) proper_callee = get_proper_type(callee_type) - if isinstance(proper_callee, (CallableType, Overloaded)): - self.chk.check_deprecated(proper_callee, e) + if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, OverloadedFuncDef): + for item in e.callee.node.items: + if isinstance(item, Decorator) and (item.func.type == callee_type): + self.chk.check_deprecated(item.func, e) if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType): # Cache it for find_isinstance_check() if proper_callee.type_guard is not None: @@ -3260,10 +3260,7 @@ def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: self.chk.module_refs.update(extract_refexpr_names(e)) result = self.analyze_ordinary_member_access(e, is_lvalue) narrowed = self.narrow_type_from_binder(e, result) - if isinstance(e.node, TypeInfo): - self.chk.warn_deprecated(e.node, e) - elif isinstance(typ := get_proper_type(narrowed), (CallableType, Overloaded)): - self.chk.warn_deprecated(typ, e) + self.chk.warn_deprecated(e.node, e) return narrowed def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type: @@ -3489,8 +3486,6 @@ def visit_op_expr(self, e: OpExpr) -> Type: else: assert_never(use_reverse) e.method_type = method_type - if isinstance(mt := get_proper_type(method_type), (CallableType, Overloaded)): - self.chk.check_deprecated(mt, e) return result else: raise RuntimeError(f"Unknown operator {e.op}") @@ -3807,8 +3802,6 @@ def check_method_call_by_name( chk=self.chk, in_literal_context=self.is_literal_context(), ) - if isinstance(mt := get_proper_type(method_type), (CallableType, Overloaded)): - self.chk.warn_deprecated(mt, context) return self.check_method_call(method, base_type, method_type, args, arg_kinds, context) def check_union_method_call_by_name( @@ -3957,7 +3950,7 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None: # This is the case even if the __add__ method is completely missing and the __radd__ # method is defined. - variants_raw = [(left_op, left_type, right_expr)] + variants_raw = [(op_name, left_op, left_type, right_expr)] elif ( is_subtype(right_type, left_type) and isinstance(left_type, Instance) @@ -3978,19 +3971,25 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None: # As a special case, the alt_promote check makes sure that we don't use the # __radd__ method of int if the LHS is a native int type. - variants_raw = [(right_op, right_type, left_expr), (left_op, left_type, right_expr)] + variants_raw = [ + (rev_op_name, right_op, right_type, left_expr), + (op_name, left_op, left_type, right_expr), + ] else: # In all other cases, we do the usual thing and call __add__ first and # __radd__ second when doing "A() + B()". - variants_raw = [(left_op, left_type, right_expr), (right_op, right_type, left_expr)] + variants_raw = [ + (op_name, left_op, left_type, right_expr), + (rev_op_name, right_op, right_type, left_expr), + ] # STEP 3: # We now filter out all non-existent operators. The 'variants' list contains # all operator methods that are actually present, in the order that Python # attempts to invoke them. - variants = [(op, obj, arg) for (op, obj, arg) in variants_raw if op is not None] + variants = [(na, op, obj, arg) for (na, op, obj, arg) in variants_raw if op is not None] # STEP 4: # We now try invoking each one. If an operation succeeds, end early and return @@ -3999,13 +3998,24 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None: errors = [] results = [] - for method, obj, arg in variants: + for name, method, obj, arg in variants: with self.msg.filter_errors(save_filtered_errors=True) as local_errors: result = self.check_method_call(op_name, obj, method, [arg], [ARG_POS], context) if local_errors.has_new_errors(): errors.append(local_errors.filtered_errors()) results.append(result) else: + if ( + isinstance(obj, Instance) + and isinstance(defn := obj.type.get_method(name), OverloadedFuncDef) + ): + for item in defn.items: + if ( + isinstance(item, Decorator) + and isinstance(typ := item.func.type, CallableType) + and bind_self(typ) == result[1] + ): + self.chk.check_deprecated(item.func, context) return result # We finish invoking above operators and no early return happens. Therefore, diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 8f490ed99ac9..f9c771d3742f 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -318,16 +318,9 @@ def analyze_instance_member_access( assert isinstance(method, OverloadedFuncDef) getter = method.items[0] assert isinstance(getter, Decorator) - if ( - mx.is_lvalue - and (len(items := method.items) > 1) - and isinstance(setter := items[1], Decorator) - ): - if isinstance(co := setter.func.type, (CallableType, Overloaded)): - mx.chk.warn_deprecated(co, mx.context) + if mx.is_lvalue and (len(items := method.items) > 1): + mx.chk.warn_deprecated(items[1], mx.context) return analyze_var(name, getter.var, typ, info, mx) - elif isinstance(co := method.type, (CallableType, Overloaded)): - mx.chk.warn_deprecated(co, mx.context) if mx.is_lvalue: mx.msg.cant_assign_to_method(mx.context) @@ -503,6 +496,8 @@ def analyze_member_var_access( # It was not a method. Try looking up a variable. v = lookup_member_var_or_accessor(info, name, mx.is_lvalue) + mx.chk.warn_deprecated(v, mx.context) + vv = v if isinstance(vv, Decorator): # The associated Var node of a decorator contains the type. @@ -783,9 +778,6 @@ def analyze_var( result = t typ = get_proper_type(typ) - if var.is_property and isinstance(typ, CallableType): - mx.chk.warn_deprecated(typ, mx.context) - call_type: ProperType | None = None if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator): if isinstance(typ, FunctionLike) and not typ.is_type_obj(): @@ -1017,6 +1009,8 @@ def analyze_class_attribute_access( # on the class object itself rather than the instance. return None + mx.chk.warn_deprecated(node.node, mx.context) + is_decorated = isinstance(node.node, Decorator) is_method = is_decorated or isinstance(node.node, FuncBase) if mx.is_lvalue: diff --git a/mypy/errors.py b/mypy/errors.py index a4fbbd79b367..13452b14a237 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -194,6 +194,9 @@ def on_error(self, file: str, info: ErrorInfo) -> bool: Return True to filter out the error, preventing it from being seen by other ErrorWatcher further down the stack and from being recorded by Errors """ + if info.code == codes.DEPRECATED: + return False + self._has_new_errors = True if isinstance(self._filter, bool): should_filter = self._filter diff --git a/mypy/nodes.py b/mypy/nodes.py index abf311bcb7f2..087672b92ee4 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -561,17 +561,19 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement): Overloaded variants must be consecutive in the source file. """ - __slots__ = ("items", "unanalyzed_items", "impl") + __slots__ = ("items", "unanalyzed_items", "impl", "deprecated") items: list[OverloadPart] unanalyzed_items: list[OverloadPart] impl: OverloadPart | None + deprecated: str | None def __init__(self, items: list[OverloadPart]) -> None: super().__init__() self.items = items self.unanalyzed_items = items.copy() self.impl = None + self.deprecated = None if items: # TODO: figure out how to reliably set end position (we don't know the impl here). self.set_line(items[0].line, items[0].column) @@ -596,6 +598,7 @@ def serialize(self) -> JsonDict: "fullname": self._fullname, "impl": None if self.impl is None else self.impl.serialize(), "flags": get_flags(self, FUNCBASE_FLAGS), + "deprecated": self.deprecated, } @classmethod @@ -615,6 +618,7 @@ def deserialize(cls, data: JsonDict) -> OverloadedFuncDef: res.type = typ res._fullname = data["fullname"] set_flags(res, data["flags"]) + res.deprecated = data["deprecated"] # NOTE: res.info will be set in the fixup phase. return res @@ -781,6 +785,7 @@ class FuncDef(FuncItem, SymbolNode, Statement): # Present only when a function is decorated with @typing.datasclass_transform or similar "dataclass_transform_spec", "docstring", + "deprecated", ) __match_args__ = ("name", "arguments", "type", "body") @@ -810,6 +815,7 @@ def __init__( self.is_mypy_only = False self.dataclass_transform_spec: DataclassTransformSpec | None = None self.docstring: str | None = None + self.deprecated: str | None = None @property def name(self) -> str: @@ -840,6 +846,7 @@ def serialize(self) -> JsonDict: if self.dataclass_transform_spec is None else self.dataclass_transform_spec.serialize() ), + "deprecated": self.deprecated, } @classmethod @@ -867,6 +874,7 @@ def deserialize(cls, data: JsonDict) -> FuncDef: if data["dataclass_transform_spec"] is not None else None ) + ret.deprecated = data["deprecated"] # Leave these uninitialized so that future uses will trigger an error del ret.arguments del ret.max_pos diff --git a/mypy/semanal.py b/mypy/semanal.py index 56d5174b05da..9efc8eece7a8 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1267,14 +1267,12 @@ def process_deprecated_overload(self, defn: OverloadedFuncDef) -> None: if ( isinstance(impl := defn.impl, Decorator) - and isinstance(type_ := impl.func.type, CallableType) - and ((deprecated := type_.deprecated) is not None) + and ((deprecated := impl.func.deprecated) is not None) ): - if isinstance(defn.type, Overloaded): - defn.type.deprecated = deprecated + defn.deprecated = deprecated for item in defn.items: - if isinstance(item, Decorator) and isinstance(item.func.type, Overloaded): - item.func.type.deprecated = deprecated + if isinstance(item, Decorator): + item.func.deprecated = deprecated for item in defn.items: deprecation = False @@ -1285,10 +1283,13 @@ def process_deprecated_overload(self, defn: OverloadedFuncDef) -> None: elif (deprecated := self.get_deprecated(d)) is not None: deprecation = True if isinstance(type_ := item.func.type, CallableType): - type_.deprecated = ( - f"overload {type_} of function {defn.fullname} is deprecated: " - f"{deprecated}" - ) + typestr = f" {type_} " + else: + typestr = " " + item.func.deprecated = ( + f"overload{typestr}of function {defn.fullname} is deprecated: " + f"{deprecated}" + ) @staticmethod def get_deprecated(expression: Expression) -> str | None: @@ -1503,10 +1504,9 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) - if isinstance(item, Decorator): for d in item.decorators: if (deprecated := self.get_deprecated(d)) is not None: - if isinstance(type_ := item.func.type, CallableType): - type_.deprecated = ( - f"function {item.fullname} is deprecated: {deprecated}" - ) + item.func.deprecated = ( + f"function {item.fullname} is deprecated: {deprecated}" + ) def add_function_to_symbol_table(self, func: FuncDef | OverloadedFuncDef) -> None: if self.is_class_scope(): @@ -1712,8 +1712,7 @@ def visit_decorator(self, dec: Decorator) -> None: ): dec.func.dataclass_transform_spec = self.parse_dataclass_transform_spec(d) elif (deprecated := self.get_deprecated(d)) is not None: - if isinstance(type_ := dec.func.type, CallableType): - type_.deprecated = f"function {dec.fullname} is deprecated: {deprecated}" + dec.func.deprecated = f"function {dec.fullname} is deprecated: {deprecated}" elif not dec.var.is_property: # We have seen a "non-trivial" decorator before seeing @property, if # we will see a @property later, give an error, as we don't support this. diff --git a/mypy/types.py b/mypy/types.py index 5b2a5e3a6f4d..98dbef0d0cc7 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1789,7 +1789,6 @@ class CallableType(FunctionLike): # (this is used for error messages) "imprecise_arg_kinds", "unpack_kwargs", # Was an Unpack[...] with **kwargs used to define this callable? - "deprecated", # The callable's deprecation message (in case it is deprecated) ) def __init__( @@ -1816,7 +1815,6 @@ def __init__( from_concatenate: bool = False, imprecise_arg_kinds: bool = False, unpack_kwargs: bool = False, - deprecated: str | None = None, ) -> None: super().__init__(line, column) assert len(arg_types) == len(arg_kinds) == len(arg_names) @@ -1865,7 +1863,6 @@ def __init__( self.type_guard = type_guard self.type_is = type_is self.unpack_kwargs = unpack_kwargs - self.deprecated = deprecated def copy_modified( self: CT, @@ -1890,7 +1887,6 @@ def copy_modified( from_concatenate: Bogus[bool] = _dummy, imprecise_arg_kinds: Bogus[bool] = _dummy, unpack_kwargs: Bogus[bool] = _dummy, - deprecated: Bogus[str | None] = _dummy, ) -> CT: modified = CallableType( arg_types=arg_types if arg_types is not _dummy else self.arg_types, @@ -1922,7 +1918,6 @@ def copy_modified( else self.imprecise_arg_kinds ), unpack_kwargs=unpack_kwargs if unpack_kwargs is not _dummy else self.unpack_kwargs, - deprecated=deprecated if deprecated is not _dummy else self.deprecated, ) # Optimization: Only NewTypes are supported as subtypes since # the class is effectively final, so we can use a cast safely. @@ -2202,7 +2197,6 @@ def __hash__(self) -> int: tuple(self.arg_names), tuple(self.arg_kinds), self.fallback, - self.deprecated, ) ) @@ -2217,7 +2211,6 @@ def __eq__(self, other: object) -> bool: and self.is_type_obj() == other.is_type_obj() and self.is_ellipsis_args == other.is_ellipsis_args and self.fallback == other.fallback - and self.deprecated == other.deprecated ) else: return NotImplemented @@ -2244,7 +2237,6 @@ def serialize(self) -> JsonDict: "from_concatenate": self.from_concatenate, "imprecise_arg_kinds": self.imprecise_arg_kinds, "unpack_kwargs": self.unpack_kwargs, - "deprecated": self.deprecated, } @classmethod @@ -2270,7 +2262,6 @@ def deserialize(cls, data: JsonDict) -> CallableType: from_concatenate=data["from_concatenate"], imprecise_arg_kinds=data["imprecise_arg_kinds"], unpack_kwargs=data["unpack_kwargs"], - deprecated=data["deprecated"], ) @@ -2289,7 +2280,7 @@ class Overloaded(FunctionLike): implementation. """ - __slots__ = ("_items", "deprecated") + __slots__ = ("_items",) _items: list[CallableType] # Must not be empty @@ -2297,7 +2288,6 @@ def __init__(self, items: list[CallableType]) -> None: super().__init__(items[0].line, items[0].column) self._items = items self.fallback = items[0].fallback - self.deprecated: str | None = None @property def items(self) -> list[CallableType]: @@ -2345,15 +2335,12 @@ def serialize(self) -> JsonDict: return { ".class": "Overloaded", "items": [t.serialize() for t in self.items], - "deprecated": self.deprecated, } @classmethod def deserialize(cls, data: JsonDict) -> Overloaded: assert data[".class"] == "Overloaded" - s = Overloaded([CallableType.deserialize(t) for t in data["items"]]) - s.deprecated = data.get("deprecated") - return s + return Overloaded([CallableType.deserialize(t) for t in data["items"]]) class TupleType(ProperType): diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 94af203d5a82..f587034d8059 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -55,7 +55,7 @@ f(1) # N: function __main__.f is deprecated: use f2 instead \ f[1] # N: function __main__.f is deprecated: use f2 instead \ # E: Value of type "Callable[[], None]" is not indexable g = f # N: function __main__.f is deprecated: use f2 instead -g() # N: function __main__.f is deprecated: use f2 instead +g() t = (f, f, g) # N: function __main__.f is deprecated: use f2 instead [builtins fixtures/tuple.pyi] @@ -203,7 +203,7 @@ class A: def __invert__(self) -> A: ... class B: - @deprecated("no in") + @deprecated("still no in") def __contains__(self, v: int) -> int: ... a = A() @@ -213,8 +213,8 @@ a + 1 # N: function __main__.A.__add__ is deprecated: no A + int a += 1 # N: function __main__.A.__iadd__ is deprecated: no A = A + int for i in a: # N: function __main__.A.__iter__ is deprecated: no iteration reveal_type(i) # N: Revealed type is "builtins.int" -1 in a # N: function __main__.A.__iter__ is deprecated: no iteration -1 in b # N: function __main__.B.__contains__ is deprecated: no in +1 in a # N: function __main__.A.__contains__ is deprecated: no in +1 in b # N: function __main__.B.__contains__ is deprecated: still no in ~a # N: function __main__.A.__invert__ is deprecated: no inversion [builtins fixtures/tuple.pyi] @@ -282,7 +282,7 @@ C().f() # N: function __main__.C.f is deprecated: use g instead C().f(1) # N: function __main__.C.f is deprecated: use g instead \ # E: Too many arguments for "f" of "C" f = C().f # N: function __main__.C.f is deprecated: use g instead -f() # N: function __main__.C.f is deprecated: use g instead +f() t = (C.f, C.f, C.g) # N: function __main__.C.f is deprecated: use g instead C().g() @@ -329,8 +329,9 @@ C.f # N: function __main__.C.f is deprecated: use f2 instead C().f # N: function __main__.C.f is deprecated: use f2 instead C().f() # N: function __main__.C.f is deprecated: use f2 instead \ # E: "int" not callable -C().f = 1 # E: Property "f" defined in "C" is read-only \ - # N: function __main__.C.f is deprecated: use f2 instead +C().f = 1 # N: function __main__.C.f is deprecated: use f2 instead \ + # E: Property "f" defined in "C" is read-only + C.g C().g From 250e1719bbc92b99abda969a051c7937fb794df3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 21:46:54 +0000 Subject: [PATCH 28/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 15 ++++++--------- mypy/checkexpr.py | 5 ++--- mypy/semanal.py | 5 ++--- mypy/types.py | 5 +---- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 42ba11e32194..0b537b022e59 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4680,9 +4680,8 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) rvalue_type, method_type = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s) - if ( - isinstance(inst := get_proper_type(lvalue_type), Instance) - and isinstance(defn := inst.type.get_method(method), OverloadedFuncDef) + if isinstance(inst := get_proper_type(lvalue_type), Instance) and isinstance( + defn := inst.type.get_method(method), OverloadedFuncDef ): for item in defn.items: if ( @@ -7547,9 +7546,8 @@ def check_deprecated(self, typ: SymbolNode | None, context: Context) -> None: """Warn if deprecated and not directly imported with a `from` statement.""" if isinstance(typ, Decorator): typ = typ.func - if ( - isinstance(typ, (FuncDef, OverloadedFuncDef, TypeInfo)) - and (typ.deprecated is not None) + if isinstance(typ, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( + typ.deprecated is not None ): for imp in self.tree.imports: if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): @@ -7561,9 +7559,8 @@ def warn_deprecated(self, typ: SymbolNode | None, context: Context) -> None: """Warn if deprecated.""" if isinstance(typ, Decorator): typ = typ.func - if ( - isinstance(typ, (FuncDef, OverloadedFuncDef, TypeInfo)) - and ((deprecated := typ.deprecated) is not None) + if isinstance(typ, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( + (deprecated := typ.deprecated) is not None ): warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note warn(deprecated, context, code=codes.DEPRECATED) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index fc40e5e8a939..6ae60b9a8759 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4005,9 +4005,8 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None: errors.append(local_errors.filtered_errors()) results.append(result) else: - if ( - isinstance(obj, Instance) - and isinstance(defn := obj.type.get_method(name), OverloadedFuncDef) + if isinstance(obj, Instance) and isinstance( + defn := obj.type.get_method(name), OverloadedFuncDef ): for item in defn.items: if ( diff --git a/mypy/semanal.py b/mypy/semanal.py index 9efc8eece7a8..e32d0b0ec66c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1265,9 +1265,8 @@ def process_deprecated_overload(self, defn: OverloadedFuncDef) -> None: if defn.is_property: return - if ( - isinstance(impl := defn.impl, Decorator) - and ((deprecated := impl.func.deprecated) is not None) + if isinstance(impl := defn.impl, Decorator) and ( + (deprecated := impl.func.deprecated) is not None ): defn.deprecated = deprecated for item in defn.items: diff --git a/mypy/types.py b/mypy/types.py index 98dbef0d0cc7..2e7cbfd4e733 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2332,10 +2332,7 @@ def __eq__(self, other: object) -> bool: return self.items == other.items def serialize(self) -> JsonDict: - return { - ".class": "Overloaded", - "items": [t.serialize() for t in self.items], - } + return {".class": "Overloaded", "items": [t.serialize() for t in self.items]} @classmethod def deserialize(cls, data: JsonDict) -> Overloaded: From 09a53d5b9a4663ed924204e344fdfc2bd34eae0a Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 29 Sep 2024 20:38:33 +0200 Subject: [PATCH 29/35] Modify function`snapshot_symbol_table` by removing the "CrossRef" special case so that `testDeprecatedAddFunctionDeprecationIndirectImport` passes both with `cache` and `nocache` and add some more tests. --- mypy/server/astdiff.py | 6 +---- test-data/unit/fine-grained.test | 44 ++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index f8a874005adb..79d4a4dbd06e 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -217,11 +217,7 @@ def snapshot_symbol_table(name_prefix: str, table: SymbolTable) -> dict[str, Sym ) else: assert symbol.kind != UNBOUND_IMPORTED - if node and get_prefix(node.fullname) != name_prefix: - # This is a cross-reference to a node defined in another module. - result[name] = ("CrossRef", common) - else: - result[name] = snapshot_definition(node, common) + result[name] = snapshot_definition(node, common) return result diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 2d6675a5316b..91aae6e2800e 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10546,7 +10546,7 @@ m.py:9: error: Argument 2 to "foo" has incompatible type "str"; expected "int" m.py:10: error: Unexpected keyword argument "a" for "foo" partial.py:4: note: "foo" defined here -[case testAddKeepChangeAndRemoveFunctionDeprecation] +[case testDeprecatedAddKeepChangeAndRemoveFunctionDeprecation] from a import f f() import a @@ -10587,7 +10587,7 @@ main:4: note: function a.f is deprecated: use f3 instead == -[case testRemoveFunctionDeprecation] +[case testDeprecatedRemoveFunctionDeprecation] from a import f f() import a @@ -10607,7 +10607,7 @@ main:1: note: function a.f is deprecated: use f2 instead main:4: note: function a.f is deprecated: use f2 instead == -[case testKeepFunctionDeprecation] +[case testDeprecatedKeepFunctionDeprecation] from a import f f() import a @@ -10632,7 +10632,7 @@ main:1: note: function a.f is deprecated: use f2 instead main:4: note: function a.f is deprecated: use f2 instead -[case testAddFunctionDeprecationIndirectImport1-only_when_nocache] +[case testDeprecatedAddFunctionDeprecationIndirectImport] from b import f f() import b @@ -10649,15 +10649,15 @@ from typing_extensions import deprecated @deprecated("use f2 instead") def f() -> int: ... - [builtins fixtures/tuple.pyi] [out] == +b.py:1: note: function a.f is deprecated: use f2 instead main:1: note: function a.f is deprecated: use f2 instead main:4: note: function a.f is deprecated: use f2 instead -b.py:1: note: function a.f is deprecated: use f2 instead -[case testAddFunctionDeprecationIndirectImport2-only_when_cache] + +[case testDeprecatedChangeFunctionDeprecationIndirectImport] from b import f f() import b @@ -10667,6 +10667,8 @@ b.f() from a import f [file a.py] +from typing_extensions import deprecated +@deprecated("use f1 instead") def f() -> int: ... [file a.py.2] @@ -10674,10 +10676,36 @@ from typing_extensions import deprecated @deprecated("use f2 instead") def f() -> int: ... - [builtins fixtures/tuple.pyi] [out] +b.py:1: note: function a.f is deprecated: use f1 instead +main:1: note: function a.f is deprecated: use f1 instead +main:4: note: function a.f is deprecated: use f1 instead == b.py:1: note: function a.f is deprecated: use f2 instead main:1: note: function a.f is deprecated: use f2 instead main:4: note: function a.f is deprecated: use f2 instead + +[case testDeprecatedRemoveFunctionDeprecationIndirectImport] +from b import f +f() +import b +b.f() + +[file b.py] +from a import f + +[file a.py] +from typing_extensions import deprecated +@deprecated("use f1 instead") +def f() -> int: ... + +[file a.py.2] +def f() -> int: ... + +[builtins fixtures/tuple.pyi] +[out] +b.py:1: note: function a.f is deprecated: use f1 instead +main:1: note: function a.f is deprecated: use f1 instead +main:4: note: function a.f is deprecated: use f1 instead +== From 31c429633fedff46aa92a8bc5e11f4dae6230f56 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 29 Sep 2024 20:47:31 +0200 Subject: [PATCH 30/35] `typ: SymbolNode` -> `node: SymbolNode` --- mypy/checker.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0b537b022e59..415a1a9149d5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7542,25 +7542,25 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type: return self.expr_checker.accept(node, type_context=type_context) - def check_deprecated(self, typ: SymbolNode | None, context: Context) -> None: + def check_deprecated(self, node: SymbolNode | None, context: Context) -> None: """Warn if deprecated and not directly imported with a `from` statement.""" - if isinstance(typ, Decorator): - typ = typ.func - if isinstance(typ, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( - typ.deprecated is not None + if isinstance(node, Decorator): + node = node.func + if isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( + node.deprecated is not None ): for imp in self.tree.imports: - if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): + if isinstance(imp, ImportFrom) and any(node.name == n[0] for n in imp.names): break else: - self.warn_deprecated(typ, context) + self.warn_deprecated(node, context) - def warn_deprecated(self, typ: SymbolNode | None, context: Context) -> None: + def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None: """Warn if deprecated.""" - if isinstance(typ, Decorator): - typ = typ.func - if isinstance(typ, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( - (deprecated := typ.deprecated) is not None + if isinstance(node, Decorator): + node = node.func + if isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( + (deprecated := node.deprecated) is not None ): warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note warn(deprecated, context, code=codes.DEPRECATED) From 90fb06d86032ab90966bd6cb3f1003f136fea103 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 29 Sep 2024 20:54:12 +0200 Subject: [PATCH 31/35] `type_` -> typ --- mypy/semanal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index e32d0b0ec66c..9daa2bdf5c14 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1281,8 +1281,8 @@ def process_deprecated_overload(self, defn: OverloadedFuncDef) -> None: self.msg.note("@overload should be placed before @deprecated", d) elif (deprecated := self.get_deprecated(d)) is not None: deprecation = True - if isinstance(type_ := item.func.type, CallableType): - typestr = f" {type_} " + if isinstance(typ := item.func.type, CallableType): + typestr = f" {typ} " else: typestr = " " item.func.deprecated = ( From beed6a548d19bb11d0c856910a9cfd9ad6a9a1bd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Sep 2024 18:55:36 +0000 Subject: [PATCH 32/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/server/astdiff.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 79d4a4dbd06e..72ff47f61a03 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -102,7 +102,6 @@ class level -- these are handled at attribute level (say, 'mod.Cls.method' UnionType, UnpackType, ) -from mypy.util import get_prefix # Snapshot representation of a symbol table node or type. The representation is # opaque -- the only supported operations are comparing for equality and From 8eb2e77a0e8b092833e67f350b4c80d244b820aa Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 6 Oct 2024 21:38:12 +0200 Subject: [PATCH 33/35] Revert commit 09a53d5b (regarding `astdiff.py` but not `fine-grained.test`) --- mypy/server/astdiff.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 72ff47f61a03..f8a874005adb 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -102,6 +102,7 @@ class level -- these are handled at attribute level (say, 'mod.Cls.method' UnionType, UnpackType, ) +from mypy.util import get_prefix # Snapshot representation of a symbol table node or type. The representation is # opaque -- the only supported operations are comparing for equality and @@ -216,7 +217,11 @@ def snapshot_symbol_table(name_prefix: str, table: SymbolTable) -> dict[str, Sym ) else: assert symbol.kind != UNBOUND_IMPORTED - result[name] = snapshot_definition(node, common) + if node and get_prefix(node.fullname) != name_prefix: + # This is a cross-reference to a node defined in another module. + result[name] = ("CrossRef", common) + else: + result[name] = snapshot_definition(node, common) return result From 80ddc68abd6160f840a59ce4de412d51839d5930 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 6 Oct 2024 21:49:05 +0200 Subject: [PATCH 34/35] Add `testDeprecateFunctionAlreadyDecorated`. --- test-data/unit/fine-grained.test | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index f5204251304e..370ec744f4d3 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10836,3 +10836,36 @@ b.py:1: note: function a.f is deprecated: use f1 instead main:1: note: function a.f is deprecated: use f1 instead main:4: note: function a.f is deprecated: use f1 instead == + +[case testDeprecateFunctionAlreadyDecorated] +from b import f +x: str = f() +import b +y: str = b.f() + +[file b.py] +from a import f + +[file a.py] +from typing import Callable + +def d(t: Callable[[], str]) -> Callable[[], str]: ... + +@d +def f() -> str: ... + +[file a.py.2] +from typing import Callable +from typing_extensions import deprecated + +def d(t: Callable[[], str]) -> Callable[[], str]: ... + +@deprecated("deprecated decorated function") +@d +def f() -> str: ... + +[builtins fixtures/tuple.pyi] +[out] +== +main:2: note: deprecated decorated function +main:4: note: deprecated decorated function From d104f2615889db7b2e9efbfa217d0349790976f4 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 6 Oct 2024 23:31:28 +0200 Subject: [PATCH 35/35] Add `deprecated` to the `Func` snapshot and adjust test `testDeprecatedFunctionAlreadyDecorated`. --- mypy/server/astdiff.py | 1 + test-data/unit/fine-grained.test | 42 +++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index d5a303128126..131a13ffd62d 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -256,6 +256,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb signature, is_trivial_body, dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None, + node.deprecated if isinstance(node, FuncDef) else None, ) elif isinstance(node, Var): return ("Var", common, snapshot_optional_type(node.type), node.is_final) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 370ec744f4d3..d4c61cbf1d5b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10837,7 +10837,7 @@ main:1: note: function a.f is deprecated: use f1 instead main:4: note: function a.f is deprecated: use f1 instead == -[case testDeprecateFunctionAlreadyDecorated] +[case testDeprecatedFunctionAlreadyDecorated1-only_when_cache] from b import f x: str = f() import b @@ -10867,5 +10867,41 @@ def f() -> str: ... [builtins fixtures/tuple.pyi] [out] == -main:2: note: deprecated decorated function -main:4: note: deprecated decorated function +b.py:1: note: function a.f is deprecated: deprecated decorated function +main:1: note: function a.f is deprecated: deprecated decorated function +main:4: note: function a.f is deprecated: deprecated decorated function + + +[case testDeprecatedFunctionAlreadyDecorated2-only_when_nocache] +from b import f +x: str = f() +import b +y: str = b.f() + +[file b.py] +from a import f + +[file a.py] +from typing import Callable + +def d(t: Callable[[], str]) -> Callable[[], str]: ... + +@d +def f() -> str: ... + +[file a.py.2] +from typing import Callable +from typing_extensions import deprecated + +def d(t: Callable[[], str]) -> Callable[[], str]: ... + +@deprecated("deprecated decorated function") +@d +def f() -> str: ... + +[builtins fixtures/tuple.pyi] +[out] +== +main:1: note: function a.f is deprecated: deprecated decorated function +main:4: note: function a.f is deprecated: deprecated decorated function +b.py:1: note: function a.f is deprecated: deprecated decorated function