From e7db89cc41dd5c77a4ccc72695fabf4234a8b67b Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 26 Oct 2024 23:59:54 +0200 Subject: [PATCH] PEP 702 (@deprecated): consider all possible type positions (#17926) This pull request generalises #17899. Initially, it started with extending #17899 to function signatures only, as can be seen from the following initial comments and the subsequent discussions. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- mypy/checker.py | 27 +--- mypy/semanal.py | 2 + mypy/server/astdiff.py | 1 + mypy/typeanal.py | 22 +++ test-data/unit/check-deprecated.test | 123 +++++++++++++- test-data/unit/fine-grained.test | 233 +++++++++++++++++++++++++++ 6 files changed, 382 insertions(+), 26 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index dbc997ba33c6..8644e8d2e93e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -287,18 +287,6 @@ class PartialTypeScope(NamedTuple): is_local: bool -class InstanceDeprecatedVisitor(TypeTraverserVisitor): - """Visitor that recursively checks for deprecations in nested instances.""" - - def __init__(self, typechecker: TypeChecker, context: Context) -> None: - self.typechecker = typechecker - self.context = context - - def visit_instance(self, t: Instance) -> None: - super().visit_instance(t) - self.typechecker.check_deprecated(t.type, self.context) - - class TypeChecker(NodeVisitor[None], CheckerPluginInterface): """Mypy type checker. @@ -2958,15 +2946,6 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: Handle all kinds of assignment statements (simple, indexed, multiple). """ - if s.unanalyzed_type is not None: - for lvalue in s.lvalues: - if ( - isinstance(lvalue, NameExpr) - and isinstance(var := lvalue.node, Var) - and (var.type is not None) - ): - var.type.accept(InstanceDeprecatedVisitor(typechecker=self, context=s)) - # Avoid type checking type aliases in stubs to avoid false # positives about modern type syntax available in stubs such # as X | Y. @@ -7655,8 +7634,10 @@ def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None: """Warn if deprecated.""" if isinstance(node, Decorator): node = node.func - if isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( - (deprecated := node.deprecated) is not None + if ( + isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo)) + and ((deprecated := node.deprecated) is not None) + and not self.is_typeshed_stub ): 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/semanal.py b/mypy/semanal.py index 23310e0974cd..55eb9cbaf426 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3870,6 +3870,7 @@ def analyze_alias( self.tvar_scope, self.plugin, self.options, + self.cur_mod_node, self.is_typeshed_stub_file, allow_placeholder=allow_placeholder, in_dynamic_func=dynamic, @@ -7308,6 +7309,7 @@ def type_analyzer( tvar_scope, self.plugin, self.options, + self.cur_mod_node, self.is_typeshed_stub_file, allow_unbound_tvars=allow_unbound_tvars, allow_tuple_literal=allow_tuple_literal, diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 131a13ffd62d..85f77a269e43 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -303,6 +303,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb [snapshot_type(base) for base in node.bases], [snapshot_type(p) for p in node._promote], dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None, + node.deprecated, ) prefix = node.fullname symbol_table = snapshot_symbol_table(prefix, node.names) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 76ead74d38a8..0c241f5c0f99 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -34,6 +34,7 @@ ArgKind, Context, Decorator, + ImportFrom, MypyFile, ParamSpecExpr, PlaceholderNode, @@ -148,6 +149,7 @@ def analyze_type_alias( tvar_scope: TypeVarLikeScope, plugin: Plugin, options: Options, + cur_mod_node: MypyFile, is_typeshed_stub: bool, allow_placeholder: bool = False, in_dynamic_func: bool = False, @@ -167,6 +169,7 @@ def analyze_type_alias( tvar_scope, plugin, options, + cur_mod_node, is_typeshed_stub, defining_alias=True, allow_placeholder=allow_placeholder, @@ -213,6 +216,7 @@ def __init__( tvar_scope: TypeVarLikeScope, plugin: Plugin, options: Options, + cur_mod_node: MypyFile, is_typeshed_stub: bool, *, defining_alias: bool = False, @@ -266,6 +270,7 @@ def __init__( self.report_invalid_types = report_invalid_types self.plugin = plugin self.options = options + self.cur_mod_node = cur_mod_node self.is_typeshed_stub = is_typeshed_stub # Names of type aliases encountered while analysing a type will be collected here. self.aliases_used: set[str] = set() @@ -771,6 +776,21 @@ def get_omitted_any(self, typ: Type, fullname: str | None = None) -> AnyType: disallow_any = not self.is_typeshed_stub and self.options.disallow_any_generics return get_omitted_any(disallow_any, self.fail, self.note, typ, self.options, fullname) + def check_and_warn_deprecated(self, info: TypeInfo, ctx: Context) -> None: + """Similar logic to `TypeChecker.check_deprecated` and `TypeChecker.warn_deprecated.""" + + if ( + (deprecated := info.deprecated) + and not self.is_typeshed_stub + and not (self.api.type and (self.api.type.fullname == info.fullname)) + ): + for imp in self.cur_mod_node.imports: + if isinstance(imp, ImportFrom) and any(info.name == n[0] for n in imp.names): + break + else: + warn = self.fail if self.options.report_deprecated_as_error else self.note + warn(deprecated, ctx, code=codes.DEPRECATED) + def analyze_type_with_type_info( self, info: TypeInfo, args: Sequence[Type], ctx: Context, empty_tuple_index: bool ) -> Type: @@ -779,6 +799,8 @@ def analyze_type_with_type_info( This handles simple cases like 'int', 'modname.UserClass[str]', etc. """ + self.check_and_warn_deprecated(info, ctx) + if len(args) > 0 and info.fullname == "builtins.tuple": fallback = Instance(info, [AnyType(TypeOfAny.special_form)], ctx.line) return TupleType(self.anal_array(args, allow_unpack=True), fallback, ctx.line) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 13cebc85513e..fbfdfcce5a14 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -142,9 +142,9 @@ x9: Callable[[int], C] # N: class __main__.C is deprecated: use C2 instead x10: Callable[[int, C, int], int] # N: class __main__.C is deprecated: use C2 instead T = TypeVar("T") -A1: TypeAlias = Optional[C] # ToDo +A1: TypeAlias = Optional[C] # N: class __main__.C is deprecated: use C2 instead x11: A1 -A2: TypeAlias = List[Union[A2, C]] # ToDo +A2: TypeAlias = List[Union[A2, C]] # N: class __main__.C is deprecated: use C2 instead x12: A2 A3: TypeAlias = List[Optional[T]] x13: A3[C] # N: class __main__.C is deprecated: use C2 instead @@ -152,13 +152,119 @@ x13: A3[C] # N: class __main__.C is deprecated: use C2 instead [builtins fixtures/tuple.pyi] +[case testDeprecatedBaseClass] + +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +class D(C): ... # N: class __main__.C is deprecated: use C2 instead +class E(D): ... +class F(D, C): ... # N: class __main__.C is deprecated: use C2 instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClassInTypeVar] + +from typing import Generic, TypeVar +from typing_extensions import deprecated + +class B: ... +@deprecated("use C2 instead") +class C: ... + +T = TypeVar("T", bound=C) # N: class __main__.C is deprecated: use C2 instead +def f(x: T) -> T: ... +class D(Generic[T]): ... + +V = TypeVar("V", B, C) # N: class __main__.C is deprecated: use C2 instead +def g(x: V) -> V: ... +class E(Generic[V]): ... + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClassInCast] + +from typing import cast, Generic +from typing_extensions import deprecated + +class B: ... +@deprecated("use C2 instead") +class C: ... + +c = C() # N: class __main__.C is deprecated: use C2 instead +b = cast(B, c) + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedInstanceInFunctionDefinition] + +from typing import Generic, List, Optional, TypeVar +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +def f1(c: C) -> None: # N: class __main__.C is deprecated: use C2 instead + def g1() -> None: ... + +def f2(c: List[Optional[C]]) -> None: # N: class __main__.C is deprecated: use C2 instead + def g2() -> None: ... + +def f3() -> C: # N: class __main__.C is deprecated: use C2 instead + def g3() -> None: ... + return C() # N: class __main__.C is deprecated: use C2 instead + +def f4() -> List[Optional[C]]: # N: class __main__.C is deprecated: use C2 instead + def g4() -> None: ... + return [] + +def f5() -> None: + def g5(c: C) -> None: ... # N: class __main__.C is deprecated: use C2 instead + +def f6() -> None: + def g6() -> C: ... # N: class __main__.C is deprecated: use C2 instead + + +@deprecated("use D2 instead") +class D: + + def f1(self, c: C) -> None: # N: class __main__.C is deprecated: use C2 instead + def g1() -> None: ... + + def f2(self, c: List[Optional[C]]) -> None: # N: class __main__.C is deprecated: use C2 instead + def g2() -> None: ... + + def f3(self) -> None: + def g3(c: C) -> None: ... # N: class __main__.C is deprecated: use C2 instead + + def f4(self) -> None: + def g4() -> C: ... # N: class __main__.C is deprecated: use C2 instead + +T = TypeVar("T") + +@deprecated("use E2 instead") +class E(Generic[T]): + + def f1(self: E[C]) -> None: ... # N: class __main__.C is deprecated: use C2 instead + def f2(self, e: E[C]) -> None: ... # N: class __main__.C is deprecated: use C2 instead + def f3(self) -> E[C]: ... # N: class __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 # N: class m.C is deprecated: use C2 instead +from m import B, C # N: class m.B is deprecated: use B2 instead \ + # 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 * @@ -170,9 +276,20 @@ C() D() E() # N: class k.E is deprecated: use E2 instead +x1: m.A # N: class m.A is deprecated: use A2 instead +x2: m.A = m.A() # N: class m.A is deprecated: use A2 instead +y1: B +y2: B = B() + [file m.py] from typing_extensions import deprecated +@deprecated("use A2 instead") +class A: ... + +@deprecated("use B2 instead") +class B: ... + @deprecated("use C2 instead") class C: ... diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index d4c61cbf1d5b..a611320909e5 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10837,6 +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 testDeprecatedFunctionAlreadyDecorated1-only_when_cache] from b import f x: str = f() @@ -10905,3 +10906,235 @@ def f() -> str: ... 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 + + +[case testDeprecatedAddClassDeprecationIndirectImport1-only_when_cache] +from b import C +x: C +C() +import b +y: b.D +b.D() + +[file b.py] +from a import C +from a import D + +[file a.py] +class C: ... +class D: ... + +[file a.py.2] +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +@deprecated("use D2 instead") +class D: ... + +[builtins fixtures/tuple.pyi] +[out] +== +b.py:1: note: class a.C is deprecated: use C2 instead +b.py:2: note: class a.D is deprecated: use D2 instead +main:1: note: class a.C is deprecated: use C2 instead +main:5: note: class a.D is deprecated: use D2 instead +main:6: note: class a.D is deprecated: use D2 instead + + +[case testDeprecatedAddClassDeprecationIndirectImport2-only_when_nocache] +from b import C +x: C +C() +import b +y: b.D +b.D() + +[file b.py] +from a import C +from a import D + +[file a.py] +class C: ... +class D: ... + +[file a.py.2] +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +@deprecated("use D2 instead") +class D: ... + +[builtins fixtures/tuple.pyi] +[out] +== +main:1: note: class a.C is deprecated: use C2 instead +main:5: note: class a.D is deprecated: use D2 instead +main:6: note: class a.D is deprecated: use D2 instead +b.py:1: note: class a.C is deprecated: use C2 instead +b.py:2: note: class a.D is deprecated: use D2 instead + + +[case testDeprecatedChangeClassDeprecationIndirectImport] +from b import C +x: C +C() +import b +y: b.D +b.D() + +[file b.py] +from a import C +from a import D + +[file a.py] +from typing_extensions import deprecated + +@deprecated("use C1 instead") +class C: ... +@deprecated("use D1 instead") +class D: ... + +[file a.py.2] +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +@deprecated("use D2 instead") +class D: ... + +[builtins fixtures/tuple.pyi] +[out] +b.py:1: note: class a.C is deprecated: use C1 instead +b.py:2: note: class a.D is deprecated: use D1 instead +main:1: note: class a.C is deprecated: use C1 instead +main:5: note: class a.D is deprecated: use D1 instead +main:6: note: class a.D is deprecated: use D1 instead +== +b.py:1: note: class a.C is deprecated: use C2 instead +b.py:2: note: class a.D is deprecated: use D2 instead +main:1: note: class a.C is deprecated: use C2 instead +main:5: note: class a.D is deprecated: use D2 instead +main:6: note: class a.D is deprecated: use D2 instead + + +[case testDeprecatedRemoveClassDeprecationIndirectImport] +from b import C +x: C +C() +import b +y: b.D +b.D() + +[file b.py] +from a import C +from a import D + +[file a.py] +from typing_extensions import deprecated + +@deprecated("use C1 instead") +class C: ... +@deprecated("use D1 instead") +class D: ... + +[file a.py.2] +class C: ... +class D: ... + +[builtins fixtures/tuple.pyi] +[out] +b.py:1: note: class a.C is deprecated: use C1 instead +b.py:2: note: class a.D is deprecated: use D1 instead +main:1: note: class a.C is deprecated: use C1 instead +main:5: note: class a.D is deprecated: use D1 instead +main:6: note: class a.D is deprecated: use D1 instead +== + + +[case testDeprecatedAddClassDeprecationIndirectImportAlreadyDecorated1-only_when_cache] +from b import C +x: C +C() +import b +y: b.D +b.D() + +[file b.py] +from a import C +from a import D + +[file a.py] +from typing import TypeVar + +T = TypeVar("T") +def dec(x: T) -> T: ... + +@dec +class C: ... +@dec +class D: ... + +[file a.py.2] +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +@deprecated("use D2 instead") +class D: ... + +[builtins fixtures/tuple.pyi] +[out] +== +b.py:1: note: class a.C is deprecated: use C2 instead +b.py:2: note: class a.D is deprecated: use D2 instead +main:1: note: class a.C is deprecated: use C2 instead +main:5: note: class a.D is deprecated: use D2 instead +main:6: note: class a.D is deprecated: use D2 instead + + +[case testDeprecatedAddClassDeprecationIndirectImportAlreadyDecorated2-only_when_nocache] +from b import C +x: C +C() +import b +y: b.D +b.D() + +[file b.py] +from a import C +from a import D + +[file a.py] +from typing import TypeVar + +T = TypeVar("T") +def dec(x: T) -> T: ... + +@dec +class C: ... +@dec +class D: ... + +[file a.py.2] +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +@deprecated("use D2 instead") +class D: ... + +[builtins fixtures/tuple.pyi] +[out] +== +main:1: note: class a.C is deprecated: use C2 instead +main:5: note: class a.D is deprecated: use D2 instead +main:6: note: class a.D is deprecated: use D2 instead +b.py:1: note: class a.C is deprecated: use C2 instead +b.py:2: note: class a.D is deprecated: use D2 instead