Skip to content

Commit fd048ab

Browse files
authored
Improve error messages related to literal types (#6149)
This pull request improves how we handle error messages with Literal types in three different ways: 1. When the user tries constructing types like `Literal[3 + a]`, we now report an error message that says 'Invalid type: Literal[...] cannot contain arbitrary expressions'. 2. We no longer recommend using Literal[...] when doing `A = NewType('A', 4)` or `T = TypeVar('T', bound=4)`. This resolves #5989. (The former suggestion is a bad one: you can't create a NewType of a Literal[...] type. The latter suggestion is a valid but stupid one: `T = TypeVar('T', bound=Literal[4])` is basically the same thing as `T = Literal[4]`.) 3. When the user tries using complex numbers inside Literals (e.g. `Literal[3j]`), we now report an error message that says 'Parameter 1 of Literal[...] cannot be of type "complex"'. This is the same kind of error message we previously used to report when the user tried using floats inside of literals. In order to accomplish bullet point 1, moved the "invalid type comment or annotation" checks from the parsing layer to the semantic analysis layer. This lets us customize which error message we report depending on whether or not the invalid type appears in the context of a Literal[...] type. In order to accomplish this, I repurposed RawLiteralType so it can represent any arbitrary expression that does not convert directly into a type (and renamed 'RawLiteralType' to 'RawExpressionType' to better reflect this new usage). I also added an optional "note" field to that class: this lets the parsing layer attach some extra context that would be difficult to obtain up in the semantic analysis layer. In order to accomplish bullet point 2, I modified the type analyzer so that the caller can optionally suppress the error messages that would otherwise be generated when a RawExpressionType appears outside of a Literal context. Bullet point 3 only required a minor tweak to the parsing and error handling code.
1 parent 80ca871 commit fd048ab

15 files changed

+247
-168
lines changed

mypy/exprtotype.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
from mypy.nodes import (
44
Expression, NameExpr, MemberExpr, IndexExpr, TupleExpr, IntExpr, FloatExpr, UnaryExpr,
5-
ListExpr, StrExpr, BytesExpr, UnicodeExpr, EllipsisExpr, CallExpr,
5+
ComplexExpr, ListExpr, StrExpr, BytesExpr, UnicodeExpr, EllipsisExpr, CallExpr,
66
get_member_expr_fullname
77
)
88
from mypy.fastparse import parse_type_string
99
from mypy.types import (
1010
Type, UnboundType, TypeList, EllipsisType, AnyType, Optional, CallableArgument, TypeOfAny,
11-
RawLiteralType,
11+
RawExpressionType,
1212
)
1313

1414

@@ -39,9 +39,9 @@ def expr_to_unanalyzed_type(expr: Expression, _parent: Optional[Expression] = No
3939
if isinstance(expr, NameExpr):
4040
name = expr.name
4141
if name == 'True':
42-
return RawLiteralType(True, 'builtins.bool', line=expr.line, column=expr.column)
42+
return RawExpressionType(True, 'builtins.bool', line=expr.line, column=expr.column)
4343
elif name == 'False':
44-
return RawLiteralType(False, 'builtins.bool', line=expr.line, column=expr.column)
44+
return RawExpressionType(False, 'builtins.bool', line=expr.line, column=expr.column)
4545
else:
4646
return UnboundType(name, line=expr.line, column=expr.column)
4747
elif isinstance(expr, MemberExpr):
@@ -122,17 +122,20 @@ def expr_to_unanalyzed_type(expr: Expression, _parent: Optional[Expression] = No
122122
assume_str_is_unicode=True)
123123
elif isinstance(expr, UnaryExpr):
124124
typ = expr_to_unanalyzed_type(expr.expr)
125-
if isinstance(typ, RawLiteralType) and isinstance(typ.value, int) and expr.op == '-':
126-
typ.value *= -1
127-
return typ
128-
else:
129-
raise TypeTranslationError()
125+
if isinstance(typ, RawExpressionType):
126+
if isinstance(typ.literal_value, int) and expr.op == '-':
127+
typ.literal_value *= -1
128+
return typ
129+
raise TypeTranslationError()
130130
elif isinstance(expr, IntExpr):
131-
return RawLiteralType(expr.value, 'builtins.int', line=expr.line, column=expr.column)
131+
return RawExpressionType(expr.value, 'builtins.int', line=expr.line, column=expr.column)
132132
elif isinstance(expr, FloatExpr):
133-
# Floats are not valid parameters for RawLiteralType, so we just
133+
# Floats are not valid parameters for RawExpressionType , so we just
134134
# pass in 'None' for now. We'll report the appropriate error at a later stage.
135-
return RawLiteralType(None, 'builtins.float', line=expr.line, column=expr.column)
135+
return RawExpressionType(None, 'builtins.float', line=expr.line, column=expr.column)
136+
elif isinstance(expr, ComplexExpr):
137+
# Same thing as above with complex numbers.
138+
return RawExpressionType(None, 'builtins.complex', line=expr.line, column=expr.column)
136139
elif isinstance(expr, EllipsisExpr):
137140
return EllipsisType(expr.line)
138141
else:

mypy/fastparse.py

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
)
3232
from mypy.types import (
3333
Type, CallableType, AnyType, UnboundType, TupleType, TypeList, EllipsisType, CallableArgument,
34-
TypeOfAny, Instance, RawLiteralType,
34+
TypeOfAny, Instance, RawExpressionType,
3535
)
3636
from mypy import defaults
3737
from mypy import messages
@@ -83,7 +83,6 @@
8383
_dummy_fallback = Instance(MISSING_FALLBACK, [], -1) # type: Final
8484

8585
TYPE_COMMENT_SYNTAX_ERROR = 'syntax error in type comment' # type: Final
86-
TYPE_COMMENT_AST_ERROR = 'invalid type comment or annotation' # type: Final
8786

8887

8988
# Older versions of typing don't allow using overload outside stubs,
@@ -184,11 +183,11 @@ def parse_type_string(expr_string: str, expr_fallback_name: str,
184183
node.original_str_fallback = expr_fallback_name
185184
return node
186185
else:
187-
return RawLiteralType(expr_string, expr_fallback_name, line, column)
186+
return RawExpressionType(expr_string, expr_fallback_name, line, column)
188187
except (SyntaxError, ValueError):
189188
# Note: the parser will raise a `ValueError` instead of a SyntaxError if
190189
# the string happens to contain things like \x00.
191-
return RawLiteralType(expr_string, expr_fallback_name, line, column)
190+
return RawExpressionType(expr_string, expr_fallback_name, line, column)
192191

193192

194193
def is_no_type_check_decorator(expr: ast3.expr) -> bool:
@@ -1069,6 +1068,24 @@ def __init__(self,
10691068
self.node_stack = [] # type: List[AST]
10701069
self.assume_str_is_unicode = assume_str_is_unicode
10711070

1071+
def invalid_type(self, node: AST, note: Optional[str] = None) -> RawExpressionType:
1072+
"""Constructs a type representing some expression that normally forms an invalid type.
1073+
For example, if we see a type hint that says "3 + 4", we would transform that
1074+
expression into a RawExpressionType.
1075+
1076+
The semantic analysis layer will report an "Invalid type" error when it
1077+
encounters this type, along with the given note if one is provided.
1078+
1079+
See RawExpressionType's docstring for more details on how it's used.
1080+
"""
1081+
return RawExpressionType(
1082+
None,
1083+
'typing.Any',
1084+
line=self.line,
1085+
column=getattr(node, 'col_offset', -1),
1086+
note=note,
1087+
)
1088+
10721089
@overload
10731090
def visit(self, node: ast3.expr) -> Type: ...
10741091

@@ -1086,8 +1103,7 @@ def visit(self, node: Optional[AST]) -> Optional[Type]: # noqa
10861103
if visitor is not None:
10871104
return visitor(node)
10881105
else:
1089-
self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(node, 'col_offset', -1))
1090-
return AnyType(TypeOfAny.from_error)
1106+
return self.invalid_type(node)
10911107
finally:
10921108
self.node_stack.pop()
10931109

@@ -1124,12 +1140,10 @@ def visit_Call(self, e: Call) -> Type:
11241140
constructor = stringify_name(f)
11251141

11261142
if not isinstance(self.parent(), ast3.List):
1127-
self.fail(TYPE_COMMENT_AST_ERROR, self.line, e.col_offset)
1143+
note = None
11281144
if constructor:
1129-
self.note("Suggestion: use {}[...] instead of {}(...)".format(
1130-
constructor, constructor),
1131-
self.line, e.col_offset)
1132-
return AnyType(TypeOfAny.from_error)
1145+
note = "Suggestion: use {0}[...] instead of {0}(...)".format(constructor)
1146+
return self.invalid_type(e, note=note)
11331147
if not constructor:
11341148
self.fail("Expected arg constructor name", e.lineno, e.col_offset)
11351149

@@ -1183,7 +1197,7 @@ def visit_Name(self, n: Name) -> Type:
11831197

11841198
def visit_NameConstant(self, n: NameConstant) -> Type:
11851199
if isinstance(n.value, bool):
1186-
return RawLiteralType(n.value, 'builtins.bool', line=self.line)
1200+
return RawExpressionType(n.value, 'builtins.bool', line=self.line)
11871201
else:
11881202
return UnboundType(str(n.value), line=self.line)
11891203

@@ -1192,26 +1206,29 @@ def visit_UnaryOp(self, n: UnaryOp) -> Type:
11921206
# We support specifically Literal[-4] and nothing else.
11931207
# For example, Literal[+4] or Literal[~6] is not supported.
11941208
typ = self.visit(n.operand)
1195-
if isinstance(typ, RawLiteralType) and isinstance(n.op, USub):
1196-
if isinstance(typ.value, int):
1197-
typ.value *= -1
1209+
if isinstance(typ, RawExpressionType) and isinstance(n.op, USub):
1210+
if isinstance(typ.literal_value, int):
1211+
typ.literal_value *= -1
11981212
return typ
1199-
self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(n, 'col_offset', -1))
1200-
return AnyType(TypeOfAny.from_error)
1213+
return self.invalid_type(n)
12011214

12021215
# Num(number n)
12031216
def visit_Num(self, n: Num) -> Type:
1204-
# Could be either float or int
1205-
numeric_value = n.n
1206-
if isinstance(numeric_value, int):
1207-
return RawLiteralType(numeric_value, 'builtins.int', line=self.line)
1208-
elif isinstance(numeric_value, float):
1209-
# Floats and other numbers are not valid parameters for RawLiteralType, so we just
1210-
# pass in 'None' for now. We'll report the appropriate error at a later stage.
1211-
return RawLiteralType(None, 'builtins.float', line=self.line)
1217+
if isinstance(n.n, int):
1218+
numeric_value = n.n
1219+
type_name = 'builtins.int'
12121220
else:
1213-
self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(n, 'col_offset', -1))
1214-
return AnyType(TypeOfAny.from_error)
1221+
# Other kinds of numbers (floats, complex) are not valid parameters for
1222+
# RawExpressionType so we just pass in 'None' for now. We'll report the
1223+
# appropriate error at a later stage.
1224+
numeric_value = None
1225+
type_name = 'builtins.{}'.format(type(n.n).__name__)
1226+
return RawExpressionType(
1227+
numeric_value,
1228+
type_name,
1229+
line=self.line,
1230+
column=getattr(n, 'col_offset', -1),
1231+
)
12151232

12161233
# Str(string s)
12171234
def visit_Str(self, n: Str) -> Type:
@@ -1230,7 +1247,7 @@ def visit_Str(self, n: Str) -> Type:
12301247
# Bytes(bytes s)
12311248
def visit_Bytes(self, n: Bytes) -> Type:
12321249
contents = bytes_to_human_readable_repr(n.s)
1233-
return RawLiteralType(contents, 'builtins.bytes', self.line, column=n.col_offset)
1250+
return RawExpressionType(contents, 'builtins.bytes', self.line, column=n.col_offset)
12341251

12351252
# Subscript(expr value, slice slice, expr_context ctx)
12361253
def visit_Subscript(self, n: ast3.Subscript) -> Type:
@@ -1251,8 +1268,7 @@ def visit_Subscript(self, n: ast3.Subscript) -> Type:
12511268
return UnboundType(value.name, params, line=self.line,
12521269
empty_tuple_index=empty_tuple_index)
12531270
else:
1254-
self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(n, 'col_offset', -1))
1255-
return AnyType(TypeOfAny.from_error)
1271+
return self.invalid_type(n)
12561272

12571273
def visit_Tuple(self, n: ast3.Tuple) -> Type:
12581274
return TupleType(self.translate_expr_list(n.elts), _dummy_fallback,
@@ -1265,8 +1281,7 @@ def visit_Attribute(self, n: Attribute) -> Type:
12651281
if isinstance(before_dot, UnboundType) and not before_dot.args:
12661282
return UnboundType("{}.{}".format(before_dot.name, n.attr), line=self.line)
12671283
else:
1268-
self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(n, 'col_offset', -1))
1269-
return AnyType(TypeOfAny.from_error)
1284+
return self.invalid_type(n)
12701285

12711286
# Ellipsis
12721287
def visit_Ellipsis(self, n: ast3_Ellipsis) -> Type:

mypy/indirection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ def visit_tuple_type(self, t: types.TupleType) -> Set[str]:
9090
def visit_typeddict_type(self, t: types.TypedDictType) -> Set[str]:
9191
return self._visit(t.items.values()) | self._visit(t.fallback)
9292

93-
def visit_raw_literal_type(self, t: types.RawLiteralType) -> Set[str]:
94-
assert False, "Unexpected RawLiteralType after semantic analysis phase"
93+
def visit_raw_expression_type(self, t: types.RawExpressionType) -> Set[str]:
94+
assert False, "Unexpected RawExpressionType after semantic analysis phase"
9595

9696
def visit_literal_type(self, t: types.LiteralType) -> Set[str]:
9797
return self._visit(t.fallback)

mypy/plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ def anal_type(self, t: Type, *,
180180
tvar_scope: Optional[TypeVarScope] = None,
181181
allow_tuple_literal: bool = False,
182182
allow_unbound_tvars: bool = False,
183+
report_invalid_types: bool = True,
183184
third_pass: bool = False) -> Type:
184185
"""Analyze an unbound type."""
185186
raise NotImplementedError

mypy/semanal.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,7 +1283,7 @@ def update_metaclass(self, defn: ClassDef) -> None:
12831283
return
12841284
defn.metaclass = metas.pop()
12851285

1286-
def expr_to_analyzed_type(self, expr: Expression) -> Type:
1286+
def expr_to_analyzed_type(self, expr: Expression, report_invalid_types: bool = True) -> Type:
12871287
if isinstance(expr, CallExpr):
12881288
expr.accept(self)
12891289
info = self.named_tuple_analyzer.check_namedtuple(expr, None, self.is_func_scope())
@@ -1295,7 +1295,7 @@ def expr_to_analyzed_type(self, expr: Expression) -> Type:
12951295
fallback = Instance(info, [])
12961296
return TupleType(info.tuple_type.items, fallback=fallback)
12971297
typ = expr_to_unanalyzed_type(expr)
1298-
return self.anal_type(typ)
1298+
return self.anal_type(typ, report_invalid_types=report_invalid_types)
12991299

13001300
def verify_base_classes(self, defn: ClassDef) -> bool:
13011301
info = defn.info
@@ -1686,6 +1686,7 @@ def type_analyzer(self, *,
16861686
tvar_scope: Optional[TypeVarScope] = None,
16871687
allow_tuple_literal: bool = False,
16881688
allow_unbound_tvars: bool = False,
1689+
report_invalid_types: bool = True,
16891690
third_pass: bool = False) -> TypeAnalyser:
16901691
if tvar_scope is None:
16911692
tvar_scope = self.tvar_scope
@@ -1696,6 +1697,7 @@ def type_analyzer(self, *,
16961697
self.is_typeshed_stub_file,
16971698
allow_unbound_tvars=allow_unbound_tvars,
16981699
allow_tuple_literal=allow_tuple_literal,
1700+
report_invalid_types=report_invalid_types,
16991701
allow_unnormalized=self.is_stub_file,
17001702
third_pass=third_pass)
17011703
tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic())
@@ -1706,10 +1708,12 @@ def anal_type(self, t: Type, *,
17061708
tvar_scope: Optional[TypeVarScope] = None,
17071709
allow_tuple_literal: bool = False,
17081710
allow_unbound_tvars: bool = False,
1711+
report_invalid_types: bool = True,
17091712
third_pass: bool = False) -> Type:
17101713
a = self.type_analyzer(tvar_scope=tvar_scope,
17111714
allow_unbound_tvars=allow_unbound_tvars,
17121715
allow_tuple_literal=allow_tuple_literal,
1716+
report_invalid_types=report_invalid_types,
17131717
third_pass=third_pass)
17141718
typ = t.accept(a)
17151719
self.add_type_alias_deps(a.aliases_used)
@@ -2394,7 +2398,14 @@ def process_typevar_parameters(self, args: List[Expression],
23942398
self.fail("TypeVar cannot have both values and an upper bound", context)
23952399
return None
23962400
try:
2397-
upper_bound = self.expr_to_analyzed_type(param_value)
2401+
# We want to use our custom error message below, so we suppress
2402+
# the default error message for invalid types here.
2403+
upper_bound = self.expr_to_analyzed_type(param_value,
2404+
report_invalid_types=False)
2405+
if isinstance(upper_bound, AnyType) and upper_bound.is_from_error:
2406+
self.fail("TypeVar 'bound' must be a type", param_value)
2407+
# Note: we do not return 'None' here -- we want to continue
2408+
# using the AnyType as the upper bound.
23982409
except TypeTranslationError:
23992410
self.fail("TypeVar 'bound' must be a type", param_value)
24002411
return None

mypy/semanal_newtype.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,13 @@ def check_newtype_args(self, name: str, call: CallExpr, context: Context) -> Opt
114114
self.fail(msg, context)
115115
return None
116116

117-
old_type = self.api.anal_type(unanalyzed_type)
117+
# We want to use our custom error message (see above), so we suppress
118+
# the default error message for invalid types here.
119+
old_type = self.api.anal_type(unanalyzed_type, report_invalid_types=False)
118120

119121
# The caller of this function assumes that if we return a Type, it's always
120122
# a valid one. So, we translate AnyTypes created from errors into None.
121-
if isinstance(old_type, AnyType) and old_type.type_of_any == TypeOfAny.from_error:
123+
if isinstance(old_type, AnyType) and old_type.is_from_error:
122124
self.fail(msg, context)
123125
return None
124126

mypy/semanal_shared.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def anal_type(self, t: Type, *,
9191
tvar_scope: Optional[TypeVarScope] = None,
9292
allow_tuple_literal: bool = False,
9393
allow_unbound_tvars: bool = False,
94+
report_invalid_types: bool = True,
9495
third_pass: bool = False) -> Type:
9596
raise NotImplementedError
9697

mypy/server/astmerge.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
Type, SyntheticTypeVisitor, Instance, AnyType, NoneTyp, CallableType, DeletedType, PartialType,
6060
TupleType, TypeType, TypeVarType, TypedDictType, UnboundType, UninhabitedType, UnionType,
6161
Overloaded, TypeVarDef, TypeList, CallableArgument, EllipsisType, StarType, LiteralType,
62-
RawLiteralType,
62+
RawExpressionType,
6363
)
6464
from mypy.util import get_prefix, replace_object_state
6565
from mypy.typestate import TypeState
@@ -331,7 +331,7 @@ class TypeReplaceVisitor(SyntheticTypeVisitor[None]):
331331
"""Similar to NodeReplaceVisitor, but for type objects.
332332
333333
Note: this visitor may sometimes visit unanalyzed types
334-
such as 'UnboundType' and 'RawLiteralType' For example, see
334+
such as 'UnboundType' and 'RawExpressionType' For example, see
335335
NodeReplaceVisitor.process_base_func.
336336
"""
337337

@@ -397,7 +397,7 @@ def visit_typeddict_type(self, typ: TypedDictType) -> None:
397397
value_type.accept(self)
398398
typ.fallback.accept(self)
399399

400-
def visit_raw_literal_type(self, t: RawLiteralType) -> None:
400+
def visit_raw_expression_type(self, t: RawExpressionType) -> None:
401401
pass
402402

403403
def visit_literal_type(self, typ: LiteralType) -> None:

mypy/type_visitor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from mypy.types import (
2222
Type, AnyType, CallableType, Overloaded, TupleType, TypedDictType, LiteralType,
23-
RawLiteralType, Instance, NoneTyp, TypeType,
23+
RawExpressionType, Instance, NoneTyp, TypeType,
2424
UnionType, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef,
2525
UnboundType, ErasedType, ForwardRef, StarType, EllipsisType, TypeList, CallableArgument,
2626
)
@@ -128,7 +128,7 @@ def visit_ellipsis_type(self, t: EllipsisType) -> T:
128128
pass
129129

130130
@abstractmethod
131-
def visit_raw_literal_type(self, t: RawLiteralType) -> T:
131+
def visit_raw_expression_type(self, t: RawExpressionType) -> T:
132132
pass
133133

134134

@@ -282,7 +282,7 @@ def visit_tuple_type(self, t: TupleType) -> T:
282282
def visit_typeddict_type(self, t: TypedDictType) -> T:
283283
return self.query_types(t.items.values())
284284

285-
def visit_raw_literal_type(self, t: RawLiteralType) -> T:
285+
def visit_raw_expression_type(self, t: RawExpressionType) -> T:
286286
return self.strategy([])
287287

288288
def visit_literal_type(self, t: LiteralType) -> T:

0 commit comments

Comments
 (0)