Skip to content

PEP 702 (@deprecated): improve the handling of explicit type annotations of assignment statements #17899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 96 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
)
from mypy.traverser import TraverserVisitor, all_return_statements, has_return_statement
from mypy.treetransform import TransformVisitor
from mypy.type_visitor import TypeVisitor
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type, make_optional_type
from mypy.typeops import (
bind_self,
Expand Down Expand Up @@ -196,6 +197,8 @@
LiteralType,
NoneType,
Overloaded,
Parameters,
ParamSpecType,
PartialType,
ProperType,
TupleType,
Expand Down Expand Up @@ -287,6 +290,96 @@ class PartialTypeScope(NamedTuple):
is_local: bool


class InstanceDeprecatedVisitor(TypeVisitor[None]):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this is not what I meant, you should inherit TypeTraverserVisitor instead, and override only visit_instance() and maybe visit_type_alias_type() (the latter is only needed if you want to show an error at each use place, instead of only at the type alias definition r.h.s.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem. TypeTraverserVisitor is the more convenient base class, of course. Now I know it exists.

I also changed the name visited to seen_aliases (in agreement with TypeArgumentAnalyzer).

I was unsure whether emitting deprecation notes due to using type alias definitions is a good idea. I had a slight tendency towards it, so I implemented it this way for now. Maybe @JelleZijlstra has a better thought-out opinion on this?

"""Visitor that recursively checks for deprecations in nested instances."""

def __init__(self, typechecker: TypeChecker, context: Context) -> None:
self.typechecker = typechecker
self.context = context
self.visited: set[Type] = set()

def _already_visited(self, t: Type, /) -> bool:
if t in self.visited:
return True
self.visited.add(t)
return False

def visit_any(self, t: AnyType) -> None:
pass

def visit_callable_type(self, t: CallableType) -> None:
if not self._already_visited(t):
for arg_type in t.arg_types:
arg_type.accept(self)
t.ret_type.accept(self)

def visit_deleted_type(self, t: DeletedType) -> None:
pass

def visit_erased_type(self, t: ErasedType) -> None:
pass

def visit_instance(self, t: Instance) -> None:
if not self._already_visited(t):
self.typechecker.check_deprecated(t.type, self.context)
for arg in t.args:
arg.accept(self)

def visit_literal_type(self, t: LiteralType) -> None:
pass

def visit_none_type(self, t: NoneType) -> None:
pass

def visit_overloaded(self, t: Overloaded) -> None:
pass

def visit_param_spec(self, t: ParamSpecType) -> None:
pass

def visit_parameters(self, t: Parameters) -> None:
pass

def visit_partial_type(self, t: PartialType) -> None:
pass

def visit_tuple_type(self, t: TupleType) -> None:
if not self._already_visited(t):
for item in t.items:
item.accept(self)

def visit_type_alias_type(self, t: TypeAliasType) -> None:
if not self._already_visited(t):
if ((alias := t.alias) is not None) and ((target := alias.target) is not None):
target.accept(self)

def visit_type_type(self, t: TypeType) -> None:
pass

def visit_type_var(self, t: TypeVarType) -> None:
pass

def visit_type_var_tuple(self, t: TypeVarTupleType) -> None:
pass

def visit_typeddict_type(self, t: TypedDictType) -> None:
pass

def visit_unbound_type(self, t: UnboundType) -> None:
pass

def visit_uninhabited_type(self, t: UninhabitedType) -> None:
pass

def visit_union_type(self, t: UnionType) -> None:
if not self._already_visited(t):
for item in t.items:
item.accept(self)

def visit_unpack_type(self, t: UnpackType) -> None:
pass


class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
"""Mypy type checker.

Expand Down Expand Up @@ -2930,14 +3023,14 @@ 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:
if s.unanalyzed_type is not None:
for lvalue in s.lvalues:
if (
isinstance(lvalue, NameExpr)
and isinstance(var := lvalue.node, Var)
and isinstance(instance := get_proper_type(var.type), Instance)
and (var.type is not None)
):
self.check_deprecated(instance.type, s)
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
Expand Down
31 changes: 30 additions & 1 deletion test-data/unit/check-deprecated.test
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def h() -> None: ...

[case testDeprecatedClass]

from typing_extensions import deprecated
from typing import Callable, List, Optional, Tuple, Union
from typing_extensions import deprecated, TypeAlias

@deprecated("use C2 instead")
class C: ...
Expand All @@ -114,10 +115,38 @@ C.missing() # N: class __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: class __main__.C is deprecated: use C2 instead
D()
t = (C, C, D) # N: class __main__.C is deprecated: use C2 instead

u1: Union[C, int] = 1 # N: class __main__.C is deprecated: use C2 instead
u1 = 1
u2 = 1 # type: Union[C, int] # N: class __main__.C is deprecated: use C2 instead
u2 = 1

c1 = c2 = C() # N: class __main__.C is deprecated: use C2 instead
i, c3 = 1, C() # N: class __main__.C is deprecated: use C2 instead

class E: ...

x1: Optional[C] # N: class __main__.C is deprecated: use C2 instead
x2: Union[D, C, E] # N: class __main__.C is deprecated: use C2 instead
x3: Union[D, Optional[C], E] # N: class __main__.C is deprecated: use C2 instead
x4: Tuple[D, C, E] # N: class __main__.C is deprecated: use C2 instead
x5: Tuple[Tuple[D, C], E] # N: class __main__.C is deprecated: use C2 instead
x6: List[C] # N: class __main__.C is deprecated: use C2 instead
x7: List[List[C]] # N: class __main__.C is deprecated: use C2 instead
x8: List[Optional[Tuple[Union[List[C], int]]]] # N: class __main__.C is deprecated: use C2 instead
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a test case involving type aliases (including generic and/or recursive ones).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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

A1: TypeAlias = Optional[C] # ToDo
x11: A1 # N: class __main__.C is deprecated: use C2 instead

A2: TypeAlias = List[Union[A2, C]] # ToDo
x12: A2 # N: class __main__.C is deprecated: use C2 instead

[builtins fixtures/tuple.pyi]


Expand Down
Loading