8000 Improve error messages related to literal types by Michael0x2a · Pull Request #6149 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Improve error messages related to literal types #6149

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
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Rename RawLiteralType to RawExpressionType
This commit renames "RawLiteralType" to "RawExpressionType". This is to
prepare for an upcoming commit, where we repurpose RawExpressionType to
represent any expression that does not convert cleanly into a type.

This lets us defer almost all error handling to the type analysis phase,
which makes it easier for us to generate cleaner error messages when
using Literal types.
  • Loading branch information
Michael0x2a committed Jan 6, 2019
commit 0afaf6ef3b73083572aa94cf8baf007585fd205b
22 changes: 11 additions & 11 deletions mypy/exprtotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from mypy.fastparse import parse_type_string
from mypy.types import (
Type, UnboundType, TypeList, EllipsisType, AnyType, Optional, CallableArgument, TypeOfAny,
RawLiteralType,
RawExpressionType,
)


Expand Down Expand Up @@ -39,9 +39,9 @@ def expr_to_unanalyzed_type(expr: Expression, _parent: Optional[Expression] = No
if isinstance(expr, NameExpr):
name = expr.name
if name == 'True':
return RawLiteralType(True, 'builtins.bool', line=expr.line, column=expr.column)
return RawExpressionType(True, 'builtins.bool', line=expr.line, column=expr.column)
elif name == 'False':
return RawLiteralType(False, 'builtins.bool', line=expr.line, column=expr.column)
return RawExpressionType(False, 'builtins.bool', line=expr.line, column=expr.column)
else:
return UnboundType(name, line=expr.line, column=expr.column)
elif isinstance(expr, MemberExpr):
Expand Down Expand Up @@ -122,17 +122,17 @@ def expr_to_unanalyzed_type(expr: Expression, _parent: Optional[Expression] = No
assume_str_is_unicode=True)
elif isinstance(expr, UnaryExpr):
typ = expr_to_unanalyzed_type(expr.expr)
if isinstance(typ, RawLiteralType) and isinstance(typ.value, int) and expr.op == '-':
typ.value *= -1
return typ
else:
raise TypeTranslationError()
if isinstance(typ, RawExpressionType):
if isinstance(typ.literal_value, int) and expr.op == '-':
typ.literal_value *= -1
return typ
raise TypeTranslationError()
elif isinstance(expr, IntExpr):
return RawLiteralType(expr.value, 'builtins.int', line=expr.line, column=expr.column)
return RawExpressionType(expr.value, 'builtins.int', line=expr.line, column=expr.column)
elif isinstance(expr, FloatExpr):
# Floats are not valid parameters for RawLiteralType, so we just
# Floats are not valid parameters for RawExpressionType , so we just
# pass in 'None' for now. We'll report the appropriate error at a later stage.
return RawLiteralType(None, 'builtins.float', line=expr.line, column=expr.column)
return RawExpressionType(None, 'builtins.float', line=expr.line, column=expr.column)
elif isinstance(expr, EllipsisExpr):
return EllipsisType(expr.line)
else:
Expand Down
20 changes: 10 additions & 10 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
)
from mypy.types import (
Type, CallableType, AnyType, UnboundType, TupleType, TypeList, EllipsisType, CallableArgument,
TypeOfAny, Instance, RawLiteralType,
TypeOfAny, Instance, RawExpressionType,
)
from mypy import defaults
from mypy import messages
Expand Down Expand Up @@ -184,11 +184,11 @@ def parse 10000 _type_string(expr_string: str, expr_fallback_name: str,
node.original_str_fallback = expr_fallback_name
return node
else:
return RawLiteralType(expr_string, expr_fallback_name, line, column)
return RawExpressionType(expr_string, expr_fallback_name, line, column)
except (SyntaxError, ValueError):
# Note: the parser will raise a `ValueError` instead of a SyntaxError if
# the string happens to contain things like \x00.
return RawLiteralType(expr_string, expr_fallback_name, line, column)
return RawExpressionType(expr_string, expr_fallback_name, line, column)


def is_no_type_check_decorator(expr: ast3.expr) -> bool:
Expand Down Expand Up @@ -1183,7 +1183,7 @@ def visit_Name(self, n: Name) -> Type:

def visit_NameConstant(self, n: NameConstant) -> Type:
if isinstance(n.value, bool):
return RawLiteralType(n.value, 'builtins.bool', line=self.line)
return RawExpressionType(n.value, 'builtins.bool', line=self.line)
else:
return UnboundType(str(n.value), line=self.line)

Expand All @@ -1192,9 +1192,9 @@ def visit_UnaryOp(self, n: UnaryOp) -> Type:
# We support specifically Literal[-4] and nothing else.
# For example, Literal[+4] or Literal[~6] is not supported.
typ = self.visit(n.operand)
if isinstance(typ, RawLiteralType) and isinstance(n.op, USub):
if isinstance(typ.value, int):
typ.value *= -1
if isinstance(typ, RawExpressionType) and isinstance(n.op, USub):
if isinstance(typ.literal_value, int):
typ.literal_value *= -1
return typ
self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(n, 'col_offset', -1))
return AnyType(TypeOfAny.from_error)
Expand All @@ -1204,11 +1204,11 @@ def visit_Num(self, n: Num) -> Type:
# Could be either float or int
numeric_value = n.n
if isinstance(numeric_value, int):
return RawLiteralType(numeric_value, 'builtins.int', line=self.line)
return RawExpressionType(numeric_value, 'builtins.int', line=self.line)
elif isinstance(numeric_value, float):
# Floats and other numbers are not valid parameters for RawLiteralType, so we just
# pass in 'None' for now. We'll report the appropriate error at a later stage.
return RawLiteralType(None, 'builtins.float', line=self.line)
return RawExpressionType(None, 'builtins.float', line=self.line)
else:
self.fail(TYPE_COMMENT_AST_ERROR, self.line, getattr(n, 'col_offset', -1))
return AnyType(TypeOfAny.from_error)
Expand All @@ -1230,7 +1230,7 @@ def visit_Str(self, n: Str) -> Type:
# Bytes(bytes s)
def visit_Bytes(self, n: Bytes) -> Type:
contents = bytes_to_human_readable_repr(n.s)
return RawLiteralType(contents, 'builtins.bytes', self.line, column=n.col_offset)
return RawExpressionType(contents, 'builtins.bytes', self.line, column=n.col_offset)

# Subscript(expr value, slice slice, expr_context ctx)
def visit_Subscript(self, n: ast3.Subscript) -> Type:
Expand Down
4 changes: 2 additions & 2 deletions mypy/indirection.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ def visit_tuple_type(self, t: types.TupleType) -> Set[str]:
def visit_typeddict_type(self, t: types.TypedDictType) -> Set[str]:
return self._visit(t.items.values()) | self._visit(t.fallback)

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

def visit_literal_type(self, t: types.LiteralType) -> Set[str]:
return self._visit(t.fallback)
Expand Down
6 changes: 3 additions & 3 deletions mypy/server/astmerge.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
Type, SyntheticTypeVisitor, Instance, AnyType, NoneTyp, CallableType, DeletedType, PartialType,
TupleType, TypeType, TypeVarType, TypedDictType, UnboundType, UninhabitedType, UnionType,
Overloaded, TypeVarDef, TypeList, CallableArgument, EllipsisType, StarType, LiteralType,
RawLiteralType,
RawExpressionType,
)
from mypy.util import get_prefix, replace_object_state
from mypy.typestate import TypeState
Expand Down Expand Up @@ -331,7 +331,7 @@ class TypeReplaceVisitor(SyntheticTypeVisitor[None]):
"""Similar to NodeReplaceVisitor, but for type objects.

Note: this visitor may sometimes visit unanalyzed types
such as 'UnboundType' and 'RawLiteralType' For example, see
such as 'UnboundType' and 'RawExpressionType' For example, see
NodeReplaceVisitor.process_base_func.
"""

Expand Down Expand Up @@ -397,7 +397,7 @@ def visit_typeddict_type(self, typ: TypedDictType) -> None:
value_type.accept(self)
typ.fallback.accept(self)

def visit_raw_literal_type(self, t: RawLiteralType) -> None:
def visit_raw_expression_type(self, t: RawExpressionType) -> None:
pass

def visit_literal_type(self, typ: LiteralType) -> None:
Expand Down
6 changes: 3 additions & 3 deletions mypy/type_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

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

@abstractmethod
def visit_raw_literal_type(self, t: RawLiteralType) -> T:
def visit_raw_expression_type(self, t: RawExpressionType) -> T:
pass


Expand Down Expand Up @@ -282,7 +282,7 @@ def visit_tuple_type(self, t: TupleType) -> T:
def visit_typeddict_type(self, t: TypedDictType) -> T:
return self.query_types(t.items.values())

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

def visit_literal_type(self, t: LiteralType) -> T:
Expand Down
24 changes: 15 additions & 9 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
CallableType, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, SyntheticTypeVisitor,
StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args,
CallableArgument, get_type_vars, TypeQuery, union_items, TypeOfAny, ForwardRef, Overloaded,
LiteralType, RawLiteralType,
LiteralType, RawExpressionType,
)
from mypy.fastparse import TYPE_COMMENT_SYNTAX_ERROR

Expand Down Expand Up @@ -489,7 +489,7 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type:
])
return TypedDictType(items, set(t.required_keys), t.fallback)

def visit_raw_literal_type(self, t: RawLiteralType) -> Type:
def visit_raw_expression_type(self, t: RawExpressionType) -> Type:
# We should never see a bare Literal. We synthesize these raw literals
# in the earlier stages of semantic analysis, but those
# "fake literals" should always be wrapped in an UnboundType
Expand Down Expand Up @@ -663,18 +663,24 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[L
if arg.type_of_any != TypeOfAny.from_error:
self.fail('Parameter {} of Literal[...] cannot be of type "Any"'.format(idx), ctx)
return None
elif isinstance(arg, RawLiteralType):
# A raw literal. Convert it directly into a literal.
if arg.base_type_name == 'builtins.float':
self.fail(
'Parameter {} of Literal[...] cannot be of type "float"'.format(idx),
ctx)
elif isinstance(arg, RawExpressionType):
# A raw literal. Convert it directly into a literal if we can.
if arg.literal_value is None:
name = arg.simple_name()
if name == 'float':
msg = 'Parameter {} of Literal[...] cannot be of type "{}"'.format(idx, name)
else:
msg = 'Invalid type: Literal[...] cannot contain arbitrary expressions'

self.fail(msg, ctx)
if arg.note is not None:
self.note_func(arg.note, ctx)
return None

# Remap bytes and unicode into the appropriate type for the correct Python version
fallback = self.named_type_with_normalized_str(arg.base_type_name)
assert isinstance(fallback, Instance)
return [LiteralType(arg.value, fallback, line=arg.line, column=arg.column)]
return [LiteralType(arg.literal_value, fallback, line=arg.line, column=arg.column)]
elif isinstance(arg, (NoneTyp, LiteralType)):
# Types that we can just add directly to the literal/potential union of literals.
return [arg]
Expand Down
62 changes: 45 additions & 17 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@
# 3. server.astdiff.SnapshotTypeVisitor's visit_literal_type_method: this
# method assumes that the following types supports equality checks and
# hashability.
LiteralValue = Union[int, str, bool, None]
#
# Note: Although "Literal[None]" is a valid type, we internally always convert
# such a type directly into "None". So, "None" is not a valid parameter of
# LiteralType and is omitted from this list.
LiteralValue = Union[int, str, bool]


# If we only import type_visitor in the middle of the file, mypy
Expand Down Expand Up @@ -1299,20 +1303,20 @@ def zipall(self, right: 'TypedDictType') \
yield (item_name, None, right_item_type)


class RawLiteralType(Type):
"""A synthetic type representing any type that could plausibly be something
that lives inside of a literal.
class RawExpressionType(Type):
"""A synthetic type representing some arbitrary expression that does not cleanly
translate into a type.

This synthetic type is only used at the beginning stages of semantic analysis
and should be completely removing during the process for mapping UnboundTypes to
actual types.
actual types: we either turn it into a LiteralType or an AnyType.

For example, `Foo[1]` is initially represented as the following:
For example, suppose `Foo[1]` is initially represented as the following:

UnboundType(
name='Foo',
args=[
RawLiteralType(value=1, base_type_name='builtins.int'),
RawExpressionType(value=1, base_type_name='builtins.int'),
],
)

Expand All @@ -1326,27 +1330,51 @@ class RawLiteralType(Type):
Alternatively, if 'Foo' is an unrelated class, we report an error and instead
produce something like this:

Instance(type=typeinfo_for_foo, args=[AnyType(TypeOfAny.from_error))
Instance(type=typeinfo_for_foo, args=[AnyType(TypeOfAny.invalid_type))

If the "note" field is not None, the provided note will be reported alongside the
error at this point.

Note: if "literal_value" is None, that means this object is representing some
expression that cannot possibly be a parameter of Literal[...]. For example,
"Foo[3j]" would be represented as:

UnboundType(
name='Foo',
args=[
RawExpressionType(value=None, base_type_name='builtins.complex'),
],
)
"""
def __init__(self, value: LiteralValue, base_type_name: str,
line: int = -1, column: int = -1) -> None:
def __init__(self,
literal_value: Optional[LiteralValue],
base_type_name: str,
line: int = -1,
column: int = -1,
note: Optional[str] = None,
) -> None:
super().__init__(line, column)
self.value = value
self.literal_value = literal_value
self.base_type_name = base_type_name
self.note = note

def simple_name(self) -> str:
return self.base_type_name.replace("builtins.", "")

def accept(self, visitor: 'TypeVisitor[T]') -> T:
assert isinstance(visitor, SyntheticTypeVisitor)
return visitor.visit_raw_literal_type(self)
return visitor.visit_raw_expression_type(self)

def serialize(self) -> JsonDict:
assert False, "Synthetic types don't serialize"

def __hash__(self) -> int:
return hash((self.value, self.base_type_name))
return hash((self.literal_value, self.base_type_name))

def __eq__(self, other: object) -> bool:
if isinstance(other, RawLiteralType):
return self.base_type_name == other.base_type_name and self.value == other.value
if isinstance(other, RawExpressionType):
return (self.base_type_name == other.base_type_name
and self.literal_value == other.literal_value)
else:
return NotImplemented

Expand Down Expand Up @@ -1872,8 +1900,8 @@ def item_str(name: str, typ: str) -> str:
prefix = repr(t.fallback.type.fullname()) + ', '
return 'TypedDict({}{})'.format(prefix, s)

def visit_raw_literal_type(self, t: RawLiteralType) -> str:
return repr(t.value)
def visit_raw_expression 4AE0 _type(self, t: RawExpressionType) -> str:
return repr(t.literal_value)

def visit_literal_type(self, t: LiteralType) -> str:
return 'Literal[{}]'.format(t.value_repr())
Expand Down
0