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
Prev Previous commit
Next Next commit
Adjust error handling for literals in NewType and TypeVars
This commit modifies mypy so that the caller of TypeAnalyzer can
optionally suppress "Invalid type" related error messages.

This resolves #5989: mypy will
now stop recommending using Literal[...] when doing
`A = NewType('A', 4)` or `T = TypeVar('T', bound=4)`.

(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
doing just `T = Literal[4].
  • Loading branch information
Michael0x2a committed Jan 6, 2019
commit 07b0d80e01e21d5f1e9cda3ae8bd1fdf09cd5ffd
1 change: 1 addition & 0 deletions mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ def anal_type(self, t: Type, *,
tvar_scope: Optional[TypeVarScope] = None,
allow_tuple_literal: bool = False,
allow_unbound_tvars: bool = False,
report_invalid_types: bool = True,
third_pass: bool = False) -> Type:
"""Analyze an unbound type."""
raise NotImplementedError
Expand Down
15 changes: 12 additions & 3 deletions mypy/semanal.py
10000
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,7 @@ def update_metaclass(self, defn: ClassDef) -> None:
return
defn.metaclass = metas.pop()

def expr_to_analyzed_type(self, expr: Expression) -> Type:
def expr_to_analyzed_type(self, expr: Expression, report_invalid_types: bool = True) -> Type:
if isinstance(expr, CallExpr):
expr.accept(self)
info = self.named_tuple_analyzer.check_namedtuple(expr, None, self.is_func_scope())
Expand All @@ -1295,7 +1295,7 @@ def expr_to_analyzed_type(self, expr: Expression) -> Type:
fallback = Instance(info, [])
return TupleType(info.tuple_type.items, fallback=fallback)
typ = expr_to_unanalyzed_type(expr)
return self.anal_type(typ)
return self.anal_type(typ, report_invalid_types=report_invalid_types)

def verify_base_classes(self, defn: ClassDef) -> bool:
info = defn.info
Expand Down Expand Up @@ -1686,6 +1686,7 @@ def type_analyzer(self, *,
tvar_scope: Optional[TypeVarScope] = None,
allow_tuple_literal: bool = False,
allow_unbound_tvars: bool = False,
report_invalid_types: bool = True,
third_pass: bool = False) -> TypeAnalyser:
if tvar_scope is None:
tvar_scope = self.tvar_scope
Expand All @@ -1696,6 +1697,7 @@ def type_analyzer(self, *,
self.is_typeshed_stub_file,
allow_unbound_tvars=allow_unbound_tvars,
allow_tuple_literal=allow_tuple_literal,
report_invalid_types=report_invalid_types,
allow_unnormalized=self.is_stub_file,
third_pass=third_pass)
tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic())
Expand All @@ -1706,10 +1708,12 @@ def anal_type(self, t: Type, *,
tvar_scope: Optional[TypeVarScope] = None,
allow_tuple_literal: bool = False,
allow_unbound_tvars: bool = False,
report_invalid_types: bool = True,
third_pass: bool = False) -> Type:
a = self.type_analyzer(tvar_scope=tvar_scope,
allow_unbound_tvars=allow_unbound_tvars,
allow_tuple_literal=allow_tuple_literal,
report_invalid_types=report_invalid_types,
third_pass=third_pass)
typ = t.accept(a)
self.add_type_alias_deps(a.aliases_used)
Expand Down Expand Up @@ -2394,7 +2398,12 @@ def process_typevar_parameters(self, args: List[Expression],
self.fail("TypeVar cannot have both values and an upper bound", context)
return None
try:
upper_bound = self.expr_to_analyzed_type(param_value)
upper_bound = self.expr_to_analyzed_type(param_value,
report_invalid_types=False)
if isinstance(upper_bound, AnyType) and upper_bound.is_from_error:
self.fail("TypeVar 'bound' must be a type", param_value)
# Note: we do not return 'None' here -- we want to continue
# using the AnyType as the upper bound.
except TypeTranslationError:
self.fail("TypeVar 'bound' must be a type", param_value)
return None
Expand Down
4 changes: 2 additions & 2 deletions mypy/semanal_newtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ def check_newtype_args(self, name: str, call: CallExpr, context: Context) -> Opt
self.fail(msg, context)
return None

old_type = self.api.anal_type(unanalyzed_type)
old_type = self.api.anal_type(unanalyzed_type, report_invalid_types=False)

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

Expand Down
1 change: 1 addition & 0 deletions mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def anal_type(self, t: Type, *,
tvar_scope: Optional[TypeVarScope] = None,
allow_tuple_literal: bool = False,
allow_unbound_tvars: bool = False,
report_invalid_types: bool = True,
third_pass: bool = False) -> Type:
raise NotImplementedError

Expand Down
43 changes: 25 additions & 18 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def __init__(self,
allow_tuple_literal: bool = False,
allow_unnormalized: bool = False,
allow_unbound_tvars: bool = False,
report_invalid_types: bool = True,
third_pass: bool = False) -> None:
self.api = api
self.lookup = api.lookup_qualified
Expand All @@ -174,6 +175,11 @@ def __init__(self,
self.allow_unnormalized = allow_unnormalized
# Should we accept unbound type variables (always OK in aliases)?
self.allow_unbound_tvars = allow_unbound_tvars or defining_alias
# Should we report an error whenever we encounter a RawExpressionType outside
# of a Literal context: e.g. whenever we encounter an invalid type? Normally,
# we want to report an error, but the caller may want to do more specialized
# error handling.
self.report_invalid_types = report_invalid_types
self.plugin = plugin
self.options = options
self.is_typeshed_stub = is_typeshed_stub
Expand Down Expand Up @@ -499,24 +505,25 @@ def visit_raw_expression_type(self, t: RawExpressionType) -> Type:
# this method so it generates and returns an actual LiteralType
# instead.

if t.base_type_name in ('builtins.int', 'builtins.bool'):
# The only time it makes sense to use an int or bool is inside of
# a literal type.
msg = "Invalid type: try using Literal[{}] instead?".format(repr(t.literal_value))
elif t.base_type_name in ('builtins.float', 'builtins.complex'):
# We special-case warnings for floats and complex numbers.
msg = "Invalid type: {} literals cannot be used as a type".format(t.simple_name())
else:
# And in all other cases, we default to a generic error message.
# Note: the reason why we use a generic error message for strings
# but not ints or bools is because whenever we see an out-of-place
# string, it's unclear if the user meant to construct a literal type
# or just misspelled a regular type. So we avoid guessing.
msg = 'Invalid type comment or annotation'

self.fail(msg, t)
if t.note is not None:
self.note_func(t.note, t)
if self.report_invalid_types:
if t.base_type_name in ('builtins.int', 'builtins.bool'):
# The only time it makes sense to use an int or bool is inside of
# a literal type.
msg = "Invalid type: try using Literal[{}] instead?".format(repr(t.literal_value))
elif t.base_type_name in ('builtins.float', 'builtins.complex'):
# We special-case warnings for floats and complex numbers.
msg = "Invalid type: {} literals cannot be used as a type".format(t.simple_name())
else:
# And in all other cases, we default to a generic error message.
# Note: the reason why we use a generic error message for strings
# but not ints or bools is because whenever we see an out-of-place
# string, it's unclear if the user meant to construct a literal type
# or just misspelled a regular type. So we avoid guessing.
msg = 'Invalid type comment or annotation'

self.fail(msg, t)
if t.note is not None:
self.note_func(t.note, t)

return AnyType(TypeOfAny.from_error, line=t.line, column=t.column)

Expand Down
3 changes: 1 addition & 2 deletions test-data/unit/check-newtype.test
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,7 @@ tmp/m.py:14: error: Revealed type is 'builtins.int'
from typing import NewType

a = NewType('b', int) # E: String argument 1 'b' to NewType(...) does not match variable name 'a'
b = NewType('b', 3) # E: Argument 2 to NewType(...) must be a valid type \
# E: Invalid type: try using Literal[3] instead?
b = NewType('b', 3) # E: Argument 2 to NewType(...) must be a valid type
c = NewType(2, int) # E: Argument 1 to NewType(...) must be a string literal
foo = "d"
d = NewType(foo, int) # E: Argument 1 to NewType(...) must be a string literal
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/semanal-errors.test
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ e = TypeVar('e', int, str, x=1) # E: Unexpected argument to TypeVar(): x
f = TypeVar('f', (int, str), int) # E: Type expected
g = TypeVar('g', int) # E: TypeVar cannot have only a single constraint
h = TypeVar('h', x=(int, str)) # E: Unexpected argument to TypeVar(): x
i = TypeVar('i', bound=1) # E: Invalid type: try using Literal[1] instead?
i = TypeVar('i', bound=1) # E: TypeVar 'bound' must be a type
[out]

[case testMoreInvalidTypevarArguments]
Expand Down
0