Skip to content

WIP: checks annotations of assignments, refs #11582 #11611

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2073,6 +2073,13 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
with self.enter_final_context(s.is_final_def):
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)

# If this assignment is annotated and we are not in stubs,
# then we need to be sure that this annotation will work in runtime.
# For example `x: 'int' | str` is a valid annotation for mypy, but it fails in runtime.
if not self.is_stub and s.annotation is not None:
with self.expr_checker.with_annotation_context(True):
self.expr_checker.accept(s.annotation)

if s.is_alias_def:
self.check_type_alias_rvalue(s)

Expand Down
25 changes: 22 additions & 3 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ class ExpressionChecker(ExpressionVisitor[Type]):
msg: MessageBuilder
# Type context for type inference
type_context: List[Optional[Type]]
# Do we check annotation? If we do, rules are slightly different.
annotation_context: bool

strfrm_checker: StringFormatterChecker
plugin: Plugin
Expand All @@ -179,6 +181,7 @@ def __init__(self,
self.msg = msg
self.plugin = plugin
self.type_context = [None]
self.annotation_context = False

# Temporary overrides for expression types. This is currently
# used by the union math in overloads.
Expand All @@ -187,6 +190,13 @@ def __init__(self,
self.type_overrides: Dict[Expression, Type] = {}
self.strfrm_checker = StringFormatterChecker(self, self.chk, self.msg)

@contextmanager
def with_annotation_context(self, context: bool) -> Iterator[None]:
old = self.annotation_context
self.annotation_context = context
yield
self.annotation_context = old

def visit_name_expr(self, e: NameExpr) -> Type:
"""Type check a name expression.

Expand Down Expand Up @@ -246,9 +256,10 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
# Something that refers to a type alias appears in runtime context.
# Note that we suppress bogus errors for alias redefinitions,
# they are already reported in semanal.py.
result = self.alias_type_in_runtime_context(node, node.no_args, e,
alias_definition=e.is_alias_rvalue
or lvalue)
result = self.alias_type_in_runtime_context(
node, node.no_args, e,
alias_definition=e.is_alias_rvalue or lvalue,
)
elif isinstance(node, (TypeVarExpr, ParamSpecExpr)):
result = self.object_type()
else:
Expand Down Expand Up @@ -3221,6 +3232,14 @@ class LongName(Generic[T]): ...
x = A()
y = cast(A, ...)
"""
if self.annotation_context and alias.is_forward_ref:
# This means that we have `X: TypeAlias = 'SomeType'`.
# When checking annotations, we need to get runtime type,
# not the one analyzed by `semanal`. This helps us to solve
# runtime problems like `X | int`:
# `unsupported operand type(s) for |: 'str' and 'type'`
return self.named_type('builtins.str')

if isinstance(alias.target, Instance) and alias.target.invalid: # type: ignore
# An invalid alias, error already has been reported
return AnyType(TypeOfAny.from_error)
Expand Down
8 changes: 7 additions & 1 deletion mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,13 @@ def visit_AnnAssign(self, n: ast3.AnnAssign) -> AssignmentStmt:
typ = TypeConverter(self.errors, line=line).visit(n.annotation)
assert typ is not None
typ.column = n.annotation.col_offset
s = AssignmentStmt([self.visit(n.target)], rvalue, type=typ, new_syntax=True)
s = AssignmentStmt(
lvalues=[self.visit(n.target)],
rvalue=rvalue,
annotation=self.visit(n.annotation),
type=typ,
new_syntax=True,
)
return self.set_line(s, n)

# AugAssign(expr target, operator op, expr value)
Expand Down
1 change: 1 addition & 0 deletions mypy/fastparse2.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ def visit_Delete(self, n: ast27.Delete) -> DelStmt:
# Assign(expr* targets, expr value, string? type_comment)
def visit_Assign(self, n: ast27.Assign) -> AssignmentStmt:
typ = self.translate_type_comment(n, n.type_comment)
# AssignmentStmt cannot have `annotation` expression in python2.
stmt = AssignmentStmt(self.translate_expr_list(n.targets),
self.visit(n.value),
type=typ)
Expand Down
35 changes: 28 additions & 7 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,11 +1099,14 @@ class AssignmentStmt(Statement):
"""

__slots__ = ('lvalues', 'rvalue', 'type', 'unanalyzed_type', 'new_syntax',
'is_alias_def', 'is_final_def')
'is_alias_def', 'is_final_def', 'annotation')

lvalues: List[Lvalue]
# This is a TempNode if and only if no rvalue (x: t).
rvalue: Expression
# Annotation expr if present. We only count real `: Type` annotations.
# Might be `None` when type comment or no explicit annotation is used.
annotation: Optional[Expression]
# Declared type in a comment, may be None.
type: Optional["mypy.types.Type"]
# Original, not semantically analyzed type in annotation (used for reprocessing)
Expand All @@ -1121,13 +1124,18 @@ class AssignmentStmt(Statement):
is_final_def: bool

def __init__(self, lvalues: List[Lvalue], rvalue: Expression,
type: 'Optional[mypy.types.Type]' = None, new_syntax: bool = False) -> None:
type: 'Optional[mypy.types.Type]' = None,
annotation: Optional[Expression] = None,
new_syntax: bool = False) -> None:
super().__init__()
self.lvalues = lvalues
self.rvalue = rvalue
self.type = type
self.annotation = annotation
self.unanalyzed_type = type
self.new_syntax = new_syntax
if self.new_syntax is True:
assert self.annotation is not None, 'annotation is required when using new syntax'
self.is_alias_def = False
self.is_final_def = False

Expand Down Expand Up @@ -2971,19 +2979,25 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here
within functions that can't be looked up from the symbol table)
"""
__slots__ = ('target', '_fullname', 'alias_tvars', 'no_args', 'normalized',
'line', 'column', '_is_recursive', 'eager')
'line', 'column', '_is_recursive', 'eager', 'is_forward_ref')

def __init__(self, target: 'mypy.types.Type', fullname: str, line: int, column: int,
def __init__(self, target: 'mypy.types.Type', fullname: str,
line: int, column: int,
*,
alias_tvars: Optional[List[str]] = None,
no_args: bool = False,
normalized: bool = False,
eager: bool = False) -> None:
eager: bool = False,
is_forward_ref: bool = False) -> None:
self._fullname = fullname
self.target = target
if alias_tvars is None:
alias_tvars = []
self.alias_tvars = alias_tvars
# Used to differentiate cases like `X: TypeAlias = int` and `X: TypeAlias = 'int'`
# This is important for cases like `X | bool`. The first one will work in runtime,
# but not the second one.
self.is_forward_ref = is_forward_ref
self.no_args = no_args
self.normalized = normalized
# This attribute is manipulated by TypeAliasType. If non-None,
Expand All @@ -3010,6 +3024,7 @@ def serialize(self) -> JsonDict:
"normalized": self.normalized,
"line": self.line,
"column": self.column,
"is_forward_ref": self.is_forward_ref,
}
return data

Expand All @@ -3026,8 +3041,14 @@ def deserialize(cls, data: JsonDict) -> 'TypeAlias':
normalized = data['normalized']
line = data['line']
column = data['column']
return cls(target, fullname, line, column, alias_tvars=alias_tvars,
no_args=no_args, normalized=normalized)
is_forward_ref = data['is_forward_ref']
return cls(
target, fullname, line, column,
alias_tvars=alias_tvars,
no_args=no_args,
normalized=normalized,
is_forward_ref=is_forward_ref,
)


class PlaceholderNode(SymbolNode):
Expand Down
21 changes: 19 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ class SemanticAnalyzer(NodeVisitor[None],
# type is stored in this mapping and that it still matches.
wrapped_coro_return_types: Dict[FuncDef, Type] = {}

# Do we check annotation or a regular expr / statement? Rules are slightly different.
annotation_context: bool

def __init__(self,
modules: Dict[str, MypyFile],
missing_modules: Set[str],
Expand Down Expand Up @@ -285,6 +288,7 @@ def __init__(self,
self.deferral_debug_context: List[Tuple[str, int]] = []

self.future_import_flags: Set[str] = set()
self.annotation_context = False

# mypyc doesn't properly handle implementing an abstractproperty
# with a regular attribute so we make them properties
Expand Down Expand Up @@ -588,6 +592,13 @@ def file_context(self,
self.incomplete_type_stack.pop()
del self.options

@contextmanager
def with_annotation_context(self, context: bool) -> Iterator[None]:
old = self.annotation_context
self.annotation_context = context
yield
self.annotation_context = old

#
# Functions
#
Expand Down Expand Up @@ -2050,6 +2061,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:

tag = self.track_incomplete_refs()
s.rvalue.accept(self)
if s.annotation is not None:
with self.with_annotation_context(True):
s.annotation.accept(self)
if self.found_incomplete_ref(tag) or self.should_wait_rhs(s.rvalue):
# Initializer couldn't be fully analyzed. Defer the current node and give up.
# Make sure that if we skip the definition of some local names, they can't be
Expand Down Expand Up @@ -2732,7 +2746,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
s.column,
alias_tvars=alias_tvars,
no_args=no_args,
eager=eager)
eager=eager,
is_forward_ref=isinstance(rvalue, StrExpr))
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
s.rvalue.analyzed = TypeAliasExpr(alias_node)
s.rvalue.analyzed.line = s.line
Expand Down Expand Up @@ -3739,7 +3754,9 @@ def visit_name_expr(self, expr: NameExpr) -> None:

def bind_name_expr(self, expr: NameExpr, sym: SymbolTableNode) -> None:
"""Bind name expression to a symbol table node."""
if isinstance(sym.node, TypeVarExpr) and self.tvar_scope.get_binding(sym):
if (isinstance(sym.node, TypeVarExpr)
and self.tvar_scope.get_binding(sym)
and not self.annotation_context):
self.fail('"{}" is a type variable and only valid in type '
'context'.format(expr.name), expr)
elif isinstance(sym.node, PlaceholderNode):
Expand Down