Skip to content

Commit

Permalink
PEP 702 (@deprecated): consider all possible type positions (#17926)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
tyralla and pre-commit-ci[bot] authored Oct 26, 2024
1 parent 2b033cb commit e7db89c
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 26 deletions.
27 changes: 4 additions & 23 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions mypy/server/astdiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ArgKind,
Context,
Decorator,
ImportFrom,
MypyFile,
ParamSpecExpr,
PlaceholderNode,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
123 changes: 120 additions & 3 deletions test-data/unit/check-deprecated.test
Original file line number Diff line number Diff line change
Expand Up @@ -142,23 +142,129 @@ 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

[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 *

Expand All @@ -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: ...

Expand Down
Loading

0 comments on commit e7db89c

Please sign in to comment.