From e182161cc35557de92a89f7e239cf4cbceebccce Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 12 Oct 2024 00:56:40 +0200 Subject: [PATCH 01/18] PEP 702 (@deprecated): consider type hints in function signatures --- mypy/checker.py | 11 +++++- test-data/unit/check-deprecated.test | 57 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index ca35144456fe..604624bb5a69 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -290,12 +290,17 @@ class PartialTypeScope(NamedTuple): class InstanceDeprecatedVisitor(TypeTraverserVisitor): """Visitor that recursively checks for deprecations in nested instances.""" - def __init__(self, typechecker: TypeChecker, context: Context) -> None: + def __init__( + self, typechecker: TypeChecker, context: Context, ignore: TypeInfo | None = None + ) -> None: self.typechecker = typechecker self.context = context + self.ignore = ignore def visit_instance(self, t: Instance) -> None: super().visit_instance(t) + if self.ignore and (t.type.fullname == self.ignore.fullname): + return self.typechecker.check_deprecated(t.type, self.context) @@ -1056,6 +1061,10 @@ def visit_func_def(self, defn: FuncDef) -> None: return with self.tscope.function_scope(defn): self._visit_func_def(defn) + if (typ := defn.type) is not None: + typ.accept( + InstanceDeprecatedVisitor(typechecker=self, context=defn, ignore=defn.info) + ) def _visit_func_def(self, defn: FuncDef) -> None: """Type check a function definition.""" diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 13cebc85513e..7159dd6864f4 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -152,6 +152,63 @@ x13: A3[C] # N: class __main__.C is deprecated: use C2 instead [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 From 5127b130f6ec0ffce68cec6bd70dbaa2e7570466 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:23:25 +0000 Subject: [PATCH 02/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 604624bb5a69..c513069962d3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1062,9 +1062,7 @@ def visit_func_def(self, defn: FuncDef) -> None: with self.tscope.function_scope(defn): self._visit_func_def(defn) if (typ := defn.type) is not None: - typ.accept( - InstanceDeprecatedVisitor(typechecker=self, context=defn, ignore=defn.info) - ) + typ.accept(InstanceDeprecatedVisitor(typechecker=self, context=defn, ignore=defn.info)) def _visit_func_def(self, defn: FuncDef) -> None: """Type check a function definition.""" From ed33bd72ffd1e5def4aad187bdb7f041c0adedea Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 17 Oct 2024 23:33:46 +0200 Subject: [PATCH 03/18] some test suite improvements --- test-data/unit/check-deprecated.test | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 7159dd6864f4..c98014356a48 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 @@ -205,7 +205,6 @@ class E(Generic[T]): 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] @@ -215,7 +214,8 @@ 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 * @@ -227,9 +227,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: ... From 9a3065a9756eebdeed6b072a0323eceb570306a2 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 17 Oct 2024 23:36:14 +0200 Subject: [PATCH 04/18] Generalise `InstanceDeprecatedVisitor` (Just a suggenstion that works for the current test suite, which we still need to extend.) --- mypy/checker.py | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c513069962d3..f04d1a83d9fa 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -56,6 +56,7 @@ make_inferred_type_note, pretty_seq, ) +from mypy.mixedtraverser import MixedTraverserVisitor from mypy.mro import MroError, calculate_mro from mypy.nodes import ( ARG_NAMED, @@ -287,21 +288,38 @@ class PartialTypeScope(NamedTuple): is_local: bool -class InstanceDeprecatedVisitor(TypeTraverserVisitor): +class InstanceDeprecatedVisitor(MixedTraverserVisitor): """Visitor that recursively checks for deprecations in nested instances.""" - def __init__( - self, typechecker: TypeChecker, context: Context, ignore: TypeInfo | None = None - ) -> None: + def __init__(self, typechecker: TypeChecker) -> None: self.typechecker = typechecker - self.context = context - self.ignore = ignore + self.context: Context | None = None + + @contextmanager + def _set_context(self, new: Context, /) -> Iterator[None]: + old = self.context + try: + self.context = new + yield None + finally: + self.context = old + + def visit_decorator(self, o: Decorator) -> None: + with self._set_context(o.func): + super().visit_decorator(o) + + def visit_func(self, o: FuncItem) -> None: + with self._set_context(o): + super().visit_func(o) def visit_instance(self, t: Instance) -> None: super().visit_instance(t) - if self.ignore and (t.type.fullname == self.ignore.fullname): - return - self.typechecker.check_deprecated(t.type, self.context) + if t.type and not ( + isinstance(defn := self.context, FuncDef) + and defn.info and + (defn.info.fullname == t.type.fullname) + ): + self.typechecker.check_deprecated(node=t.type, context=t) class TypeChecker(NodeVisitor[None], CheckerPluginInterface): @@ -498,6 +516,7 @@ def check_first_pass(self) -> None: break else: self.accept(d) + self.tree.accept(InstanceDeprecatedVisitor(typechecker=self)) assert not self.current_node_deferred @@ -1061,8 +1080,6 @@ def visit_func_def(self, defn: FuncDef) -> None: return with self.tscope.function_scope(defn): self._visit_func_def(defn) - if (typ := defn.type) is not None: - typ.accept(InstanceDeprecatedVisitor(typechecker=self, context=defn, ignore=defn.info)) def _visit_func_def(self, defn: FuncDef) -> None: """Type check a function definition.""" @@ -2949,15 +2966,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. From 37b9be79b7db80b26cdbd07ee5d97d059478bd33 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 18 Oct 2024 00:43:14 +0200 Subject: [PATCH 05/18] fix format --- mypy/checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f04d1a83d9fa..fb5f39e3e841 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -316,8 +316,8 @@ def visit_instance(self, t: Instance) -> None: super().visit_instance(t) if t.type and not ( isinstance(defn := self.context, FuncDef) - and defn.info and - (defn.info.fullname == t.type.fullname) + and defn.info + and (defn.info.fullname == t.type.fullname) ): self.typechecker.check_deprecated(node=t.type, context=t) From 36c190b44c736044a18fae25c4b446798bec233f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 18 Oct 2024 09:16:36 +0200 Subject: [PATCH 06/18] Add `deprecated` to the `TypeInfo` snapshot and add corresponding fine-grained tests (problem: `y: b.D`) --- mypy/server/astdiff.py | 1 + test-data/unit/fine-grained.test | 113 +++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) 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/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index d4c61cbf1d5b..e398b07425e7 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,115 @@ 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 testDeprecatedAddClassDeprecationIndirectImport] +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 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 +== From 4e34852b3e9952de1559a28dedb64fa15fc19a2d Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 18 Oct 2024 09:27:03 +0200 Subject: [PATCH 07/18] Add test case`testDeprecatedAddClassDeprecationIndirectImportAlreadyDecorated` --- test-data/unit/fine-grained.test | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index e398b07425e7..3fb4ccee5f05 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -11018,3 +11018,45 @@ 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 testDeprecatedAddClassDeprecationIndirectImportAlreadyDecorated] +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 From 414b5163d8513b963475be23a12d85a34bc30ce9 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 20 Oct 2024 23:49:31 +0200 Subject: [PATCH 08/18] fix: apply `InstanceDeprecatedVisitor` in `check_partial` --- mypy/checker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/checker.py b/mypy/checker.py index fb5f39e3e841..7100f18004b3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -574,6 +574,7 @@ def check_second_pass( def check_partial(self, node: DeferredNodeType | FineGrainedDeferredNodeType) -> None: if isinstance(node, MypyFile): self.check_top_level(node) + self.tree.accept(InstanceDeprecatedVisitor(typechecker=self)) else: self.recurse_into_functions = True with self.binder.top_frame_context(): From efdc893f21ee35dccdc8747901e3076ecacb2172 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 20 Oct 2024 23:50:11 +0200 Subject: [PATCH 09/18] fix: different output order for `cache` and `nocache` --- test-data/unit/fine-grained.test | 82 +++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 3fb4ccee5f05..a611320909e5 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10908,7 +10908,7 @@ main:4: note: function a.f is deprecated: deprecated decorated function b.py:1: note: function a.f is deprecated: deprecated decorated function -[case testDeprecatedAddClassDeprecationIndirectImport] +[case testDeprecatedAddClassDeprecationIndirectImport1-only_when_cache] from b import C x: C C() @@ -10943,6 +10943,41 @@ 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 @@ -10986,6 +11021,7 @@ 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 @@ -11020,7 +11056,7 @@ main:6: note: class a.D is deprecated: use D1 instead == -[case testDeprecatedAddClassDeprecationIndirectImportAlreadyDecorated] +[case testDeprecatedAddClassDeprecationIndirectImportAlreadyDecorated1-only_when_cache] from b import C x: C C() @@ -11060,3 +11096,45 @@ 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 From 3ba66842b389552b1d2d5da4accc3fe5f8225af7 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 21 Oct 2024 10:16:38 +0200 Subject: [PATCH 10/18] add test `testDeprecatedBaseClass` --- test-data/unit/check-deprecated.test | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index c98014356a48..1aca540ddbb2 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -152,6 +152,20 @@ 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 testDeprecatedInstanceInFunctionDefinition] from typing import Generic, List, Optional, TypeVar From a03f3183d6d773fd3c9891da8112b6262afc1902 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 21 Oct 2024 10:23:17 +0200 Subject: [PATCH 11/18] add test `testDeprecatedClassInTypeVar` --- test-data/unit/check-deprecated.test | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 1aca540ddbb2..019697ad9139 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -166,6 +166,26 @@ 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 testDeprecatedInstanceInFunctionDefinition] from typing import Generic, List, Optional, TypeVar From e221e46d61c11bb888647c73dee44844000333e5 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 21 Oct 2024 10:29:08 +0200 Subject: [PATCH 12/18] add test `testDeprecatedClassInCast` --- test-data/unit/check-deprecated.test | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 019697ad9139..fbfdfcce5a14 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -186,6 +186,21 @@ 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 fe71ad69851abd71d7136aecf2f3dbddf5c4dda3 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 22 Oct 2024 22:26:04 +0200 Subject: [PATCH 13/18] Do not warn when analysing a typeshed stub file. --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7100f18004b3..cf272dcda6d5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7620,7 +7620,7 @@ def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None: node = node.func 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) From a42b123649c0c8c32df79fc5d816b6809a7cfb96 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 22 Oct 2024 22:43:38 +0200 Subject: [PATCH 14/18] fix format --- mypy/checker.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index cf272dcda6d5..c881df851deb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7618,9 +7618,11 @@ 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 - ) and not self.is_typeshed_stub: + 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) From ede5ae1dc4e4ac44378a74869f54e6cff407e286 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 25 Oct 2024 09:26:46 +0200 Subject: [PATCH 15/18] Replace class `InstanceDeprecatedVisitor` (module `checker`) with method `TypeAnalyzer.check_and_warn_deprecated` (module `typeanal`). --- mypy/checker.py | 36 ------------------------------------ mypy/semanal.py | 2 ++ mypy/typeanal.py | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 36 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c881df851deb..332b73825394 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -288,40 +288,6 @@ class PartialTypeScope(NamedTuple): is_local: bool -class InstanceDeprecatedVisitor(MixedTraverserVisitor): - """Visitor that recursively checks for deprecations in nested instances.""" - - def __init__(self, typechecker: TypeChecker) -> None: - self.typechecker = typechecker - self.context: Context | None = None - - @contextmanager - def _set_context(self, new: Context, /) -> Iterator[None]: - old = self.context - try: - self.context = new - yield None - finally: - self.context = old - - def visit_decorator(self, o: Decorator) -> None: - with self._set_context(o.func): - super().visit_decorator(o) - - def visit_func(self, o: FuncItem) -> None: - with self._set_context(o): - super().visit_func(o) - - def visit_instance(self, t: Instance) -> None: - super().visit_instance(t) - if t.type and not ( - isinstance(defn := self.context, FuncDef) - and defn.info - and (defn.info.fullname == t.type.fullname) - ): - self.typechecker.check_deprecated(node=t.type, context=t) - - class TypeChecker(NodeVisitor[None], CheckerPluginInterface): """Mypy type checker. @@ -516,7 +482,6 @@ def check_first_pass(self) -> None: break else: self.accept(d) - self.tree.accept(InstanceDeprecatedVisitor(typechecker=self)) assert not self.current_node_deferred @@ -574,7 +539,6 @@ def check_second_pass( def check_partial(self, node: DeferredNodeType | FineGrainedDeferredNodeType) -> None: if isinstance(node, MypyFile): self.check_top_level(node) - self.tree.accept(InstanceDeprecatedVisitor(typechecker=self)) else: self.recurse_into_functions = True with self.binder.top_frame_context(): diff --git a/mypy/semanal.py b/mypy/semanal.py index 95efe2b0f30c..769dd68af261 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3859,6 +3859,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, @@ -7267,6 +7268,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/typeanal.py b/mypy/typeanal.py index 0a6b7689136e..cd480d7b8ae1 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) From c8fe1f95bdf4fc0b81658c1903acee2a26eca57b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 07:52:48 +0000 Subject: [PATCH 16/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 332b73825394..56690e9db40f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -56,7 +56,6 @@ make_inferred_type_note, pretty_seq, ) -from mypy.mixedtraverser import MixedTraverserVisitor from mypy.mro import MroError, calculate_mro from mypy.nodes import ( ARG_NAMED, From cec1a0c5c14e7684ff0dbb40fe141f1e77f66def Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 25 Oct 2024 09:59:00 +0200 Subject: [PATCH 17/18] restart tests --- mypy/checker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/checker.py b/mypy/checker.py index 56690e9db40f..1d7b0daa710b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -45,6 +45,7 @@ from mypy.maptype import map_instance_to_supertype from mypy.meet import is_overlapping_erased_types, is_overlapping_types, meet_types from mypy.message_registry import ErrorMessage +test from mypy.messages import ( SUGGESTED_TEST_FIXTURES, MessageBuilder, From 82e0ae876f186818cc0ce8ca3121d15da84ec21d Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 25 Oct 2024 09:59:10 +0200 Subject: [PATCH 18/18] restart tests --- mypy/checker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1d7b0daa710b..56690e9db40f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -45,7 +45,6 @@ from mypy.maptype import map_instance_to_supertype from mypy.meet import is_overlapping_erased_types, is_overlapping_types, meet_types from mypy.message_registry import ErrorMessage -test from mypy.messages import ( SUGGESTED_TEST_FIXTURES, MessageBuilder,