diff --git a/mypy/checker.py b/mypy/checker.py index adc7f33b5e37..91927609e163 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 2638e556499c..d858aa0b0b02 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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 @@ -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. @@ -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. @@ -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: @@ -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) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index a0d0ec8e34b0..663547a9679b 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -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) diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 33f34082cf83..f433ffe7aa34 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -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) diff --git a/mypy/nodes.py b/mypy/nodes.py index ac2dbf336634..1ba229d40de8 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -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) @@ -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 @@ -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, @@ -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 @@ -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): diff --git a/mypy/semanal.py b/mypy/semanal.py index 86183704f680..e7031b34cb48 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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], @@ -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 @@ -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 # @@ -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 @@ -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 @@ -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):