Skip to content

Commit e7db89c

Browse files
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>
1 parent 2b033cb commit e7db89c

File tree

6 files changed

+382
-26
lines changed

6 files changed

+382
-26
lines changed

mypy/checker.py

+4-23
Original file line numberDiff line numberDiff line change
@@ -287,18 +287,6 @@ class PartialTypeScope(NamedTuple):
287287
is_local: bool
288288

289289

290-
class InstanceDeprecatedVisitor(TypeTraverserVisitor):
291-
"""Visitor that recursively checks for deprecations in nested instances."""
292-
293-
def __init__(self, typechecker: TypeChecker, context: Context) -> None:
294-
self.typechecker = typechecker
295-
self.context = context
296-
297-
def visit_instance(self, t: Instance) -> None:
298-
super().visit_instance(t)
299-
self.typechecker.check_deprecated(t.type, self.context)
300-
301-
302290
class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
303291
"""Mypy type checker.
304292
@@ -2958,15 +2946,6 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
29582946
Handle all kinds of assignment statements (simple, indexed, multiple).
29592947
"""
29602948

2961-
if s.unanalyzed_type is not None:
2962-
for lvalue in s.lvalues:
2963-
if (
2964-
isinstance(lvalue, NameExpr)
2965-
and isinstance(var := lvalue.node, Var)
2966-
and (var.type is not None)
2967-
):
2968-
var.type.accept(InstanceDeprecatedVisitor(typechecker=self, context=s))
2969-
29702949
# Avoid type checking type aliases in stubs to avoid false
29712950
# positives about modern type syntax available in stubs such
29722951
# as X | Y.
@@ -7655,8 +7634,10 @@ def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None:
76557634
"""Warn if deprecated."""
76567635
if isinstance(node, Decorator):
76577636
node = node.func
7658-
if isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo)) and (
7659-
(deprecated := node.deprecated) is not None
7637+
if (
7638+
isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo))
7639+
and ((deprecated := node.deprecated) is not None)
7640+
and not self.is_typeshed_stub
76607641
):
76617642
warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note
76627643
warn(deprecated, context, code=codes.DEPRECATED)

mypy/semanal.py

+2
Original file line numberDiff line numberDiff line change
@@ -3870,6 +3870,7 @@ def analyze_alias(
38703870
self.tvar_scope,
38713871
self.plugin,
38723872
self.options,
3873+
self.cur_mod_node,
38733874
self.is_typeshed_stub_file,
38743875
allow_placeholder=allow_placeholder,
38753876
in_dynamic_func=dynamic,
@@ -7308,6 +7309,7 @@ def type_analyzer(
73087309
tvar_scope,
73097310
self.plugin,
73107311
self.options,
7312+
self.cur_mod_node,
73117313
self.is_typeshed_stub_file,
73127314
allow_unbound_tvars=allow_unbound_tvars,
73137315
allow_tuple_literal=allow_tuple_literal,

mypy/server/astdiff.py

+1
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
303303
[snapshot_type(base) for base in node.bases],
304304
[snapshot_type(p) for p in node._promote],
305305
dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
306+
node.deprecated,
306307
)
307308
prefix = node.fullname
308309
symbol_table = snapshot_symbol_table(prefix, node.names)

mypy/typeanal.py

+22
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
ArgKind,
3535
Context,
3636
Decorator,
37+
ImportFrom,
3738
MypyFile,
3839
ParamSpecExpr,
3940
PlaceholderNode,
@@ -148,6 +149,7 @@ def analyze_type_alias(
148149
tvar_scope: TypeVarLikeScope,
149150
plugin: Plugin,
150151
options: Options,
152+
cur_mod_node: MypyFile,
151153
is_typeshed_stub: bool,
152154
allow_placeholder: bool = False,
153155
in_dynamic_func: bool = False,
@@ -167,6 +169,7 @@ def analyze_type_alias(
167169
tvar_scope,
168170
plugin,
169171
options,
172+
cur_mod_node,
170173
is_typeshed_stub,
171174
defining_alias=True,
172175
allow_placeholder=allow_placeholder,
@@ -213,6 +216,7 @@ def __init__(
213216
tvar_scope: TypeVarLikeScope,
214217
plugin: Plugin,
215218
options: Options,
219+
cur_mod_node: MypyFile,
216220
is_typeshed_stub: bool,
217221
*,
218222
defining_alias: bool = False,
@@ -266,6 +270,7 @@ def __init__(
266270
self.report_invalid_types = report_invalid_types
267271
self.plugin = plugin
268272
self.options = options
273+
self.cur_mod_node = cur_mod_node
269274
self.is_typeshed_stub = is_typeshed_stub
270275
# Names of type aliases encountered while analysing a type will be collected here.
271276
self.aliases_used: set[str] = set()
@@ -771,6 +776,21 @@ def get_omitted_any(self, typ: Type, fullname: str | None = None) -> AnyType:
771776
disallow_any = not self.is_typeshed_stub and self.options.disallow_any_generics
772777
return get_omitted_any(disallow_any, self.fail, self.note, typ, self.options, fullname)
773778

779+
def check_and_warn_deprecated(self, info: TypeInfo, ctx: Context) -> None:
780+
"""Similar logic to `TypeChecker.check_deprecated` and `TypeChecker.warn_deprecated."""
781+
782+
if (
783+
(deprecated := info.deprecated)
784+
and not self.is_typeshed_stub
785+
and not (self.api.type and (self.api.type.fullname == info.fullname))
786+
):
787+
for imp in self.cur_mod_node.imports:
788+
if isinstance(imp, ImportFrom) and any(info.name == n[0] for n in imp.names):
789+
break
790+
else:
791+
warn = self.fail if self.options.report_deprecated_as_error else self.note
792+
warn(deprecated, ctx, code=codes.DEPRECATED)
793+
774794
def analyze_type_with_type_info(
775795
self, info: TypeInfo, args: Sequence[Type], ctx: Context, empty_tuple_index: bool
776796
) -> Type:
@@ -779,6 +799,8 @@ def analyze_type_with_type_info(
779799
This handles simple cases like 'int', 'modname.UserClass[str]', etc.
780800
"""
781801

802+
self.check_and_warn_deprecated(info, ctx)
803+
782804
if len(args) > 0 and info.fullname == "builtins.tuple":
783805
fallback = Instance(info, [AnyType(TypeOfAny.special_form)], ctx.line)
784806
return TupleType(self.anal_array(args, allow_unpack=True), fallback, ctx.line)

test-data/unit/check-deprecated.test

+120-3
Original file line numberDiff line numberDiff line change
@@ -142,23 +142,129 @@ x9: Callable[[int], C] # N: class __main__.C is deprecated: use C2 instead
142142
x10: Callable[[int, C, int], int] # N: class __main__.C is deprecated: use C2 instead
143143

144144
T = TypeVar("T")
145-
A1: TypeAlias = Optional[C] # ToDo
145+
A1: TypeAlias = Optional[C] # N: class __main__.C is deprecated: use C2 instead
146146
x11: A1
147-
A2: TypeAlias = List[Union[A2, C]] # ToDo
147+
A2: TypeAlias = List[Union[A2, C]] # N: class __main__.C is deprecated: use C2 instead
148148
x12: A2
149149
A3: TypeAlias = List[Optional[T]]
150150
x13: A3[C] # N: class __main__.C is deprecated: use C2 instead
151151

152152
[builtins fixtures/tuple.pyi]
153153

154154

155+
[case testDeprecatedBaseClass]
156+
157+
from typing_extensions import deprecated
158+
159+
@deprecated("use C2 instead")
160+
class C: ...
161+
162+
class D(C): ... # N: class __main__.C is deprecated: use C2 instead
163+
class E(D): ...
164+
class F(D, C): ... # N: class __main__.C is deprecated: use C2 instead
165+
166+
[builtins fixtures/tuple.pyi]
167+
168+
169+
[case testDeprecatedClassInTypeVar]
170+
171+
from typing import Generic, TypeVar
172+
from typing_extensions import deprecated
173+
174+
class B: ...
175+
@deprecated("use C2 instead")
176+
class C: ...
177+
178+
T = TypeVar("T", bound=C) # N: class __main__.C is deprecated: use C2 instead
179+
def f(x: T) -> T: ...
180+
class D(Generic[T]): ...
181+
182+
V = TypeVar("V", B, C) # N: class __main__.C is deprecated: use C2 instead
183+
def g(x: V) -> V: ...
184+
class E(Generic[V]): ...
185+
186+
[builtins fixtures/tuple.pyi]
187+
188+
189+
[case testDeprecatedClassInCast]
190+
191+
from typing import cast, Generic
192+
from typing_extensions import deprecated
193+
194+
class B: ...
195+
@deprecated("use C2 instead")
196+
class C: ...
197+
198+
c = C() # N: class __main__.C is deprecated: use C2 instead
199+
b = cast(B, c)
200+
201+
[builtins fixtures/tuple.pyi]
202+
203+
204+
[case testDeprecatedInstanceInFunctionDefinition]
205+
206+
from typing import Generic, List, Optional, TypeVar
207+
from typing_extensions import deprecated
208+
209+
@deprecated("use C2 instead")
210+
class C: ...
211+
212+
def f1(c: C) -> None: # N: class __main__.C is deprecated: use C2 instead
213+
def g1() -> None: ...
214+
215+
def f2(c: List[Optional[C]]) -> None: # N: class __main__.C is deprecated: use C2 instead
216+
def g2() -> None: ...
217+
218+
def f3() -> C: # N: class __main__.C is deprecated: use C2 instead
219+
def g3() -> None: ...
220+
return C() # N: class __main__.C is deprecated: use C2 instead
221+
222+
def f4() -> List[Optional[C]]: # N: class __main__.C is deprecated: use C2 instead
223+
def g4() -> None: ...
224+
return []
225+
226+
def f5() -> None:
227+
def g5(c: C) -> None: ... # N: class __main__.C is deprecated: use C2 instead
228+
229+
def f6() -> None:
230+
def g6() -> C: ... # N: class __main__.C is deprecated: use C2 instead
231+
232+
233+
@deprecated("use D2 instead")
234+
class D:
235+
236+
def f1(self, c: C) -> None: # N: class __main__.C is deprecated: use C2 instead
237+
def g1() -> None: ...
238+
239+
def f2(self, c: List[Optional[C]]) -> None: # N: class __main__.C is deprecated: use C2 instead
240+
def g2() -> None: ...
241+
242+
def f3(self) -> None:
243+
def g3(c: C) -> None: ... # N: class __main__.C is deprecated: use C2 instead
244+
245+
def f4(self) -> None:
246+
def g4() -> C: ... # N: class __main__.C is deprecated: use C2 instead
247+
248+
T = TypeVar("T")
249+
250+
@deprecated("use E2 instead")
251+
class E(Generic[T]):
252+
253+
def f1(self: E[C]) -> None: ... # N: class __main__.C is deprecated: use C2 instead
254+
def f2(self, e: E[C]) -> None: ... # N: class __main__.C is deprecated: use C2 instead
255+
def f3(self) -> E[C]: ... # N: class __main__.C is deprecated: use C2 instead
256+
257+
[builtins fixtures/tuple.pyi]
258+
259+
155260
[case testDeprecatedClassDifferentModule]
156261

157262
import m
158263
import p.s
159264
import m as n
160265
import p.s as ps
161-
from m import C # N: class m.C is deprecated: use C2 instead
266+
from m import B, C # N: class m.B is deprecated: use B2 instead \
267+
# N: class m.C is deprecated: use C2 instead
162268
from p.s import D # N: class p.s.D is deprecated: use D2 instead
163269
from k import *
164270

@@ -170,9 +276,20 @@ C()
170276
D()
171277
E() # N: class k.E is deprecated: use E2 instead
172278

279+
x1: m.A # N: class m.A is deprecated: use A2 instead
280+
x2: m.A = m.A() # N: class m.A is deprecated: use A2 instead
281+
y1: B
282+
y2: B = B()
283+
173284
[file m.py]
174285
from typing_extensions import deprecated
175286

287+
@deprecated("use A2 instead")
288+
class A: ...
289+
290+
@deprecated("use B2 instead")
291+
class B: ...
292+
176293
@deprecated("use C2 instead")
177294
class C: ...
178295

0 commit comments

Comments
 (0)