Skip to content

Commit

Permalink
allow literal types without Literal
Browse files Browse the repository at this point in the history
  • Loading branch information
KotlinIsland committed Jul 30, 2022
1 parent c92df4b commit ff66817
Show file tree
Hide file tree
Showing 19 changed files with 1,763 additions and 1,504 deletions.
2,978 changes: 1,518 additions & 1,460 deletions .mypy/baseline.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Basedmypy Changelog

## [Unreleased]
### Added
- Allow literal `int`, `bool` and `Enum`s without `Literal`
### Enhancements
- Unionize at type joins instead of common ancestor
- Render Literal types better in output messages
Expand Down
27 changes: 22 additions & 5 deletions mypy/exprtotype.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Translate an Expression to a Type value."""

from typing import Optional

from mypy.fastparse import parse_type_string
Expand Down Expand Up @@ -73,15 +72,27 @@ def expr_to_unanalyzed_type(
if isinstance(expr, NameExpr):
name = expr.name
if name == "True":
return RawExpressionType(True, "builtins.bool", line=expr.line, column=expr.column)
return RawExpressionType(
True,
"builtins.bool",
line=expr.line,
column=expr.column,
expression=not allow_new_syntax,
)
elif name == "False":
return RawExpressionType(False, "builtins.bool", line=expr.line, column=expr.column)
return RawExpressionType(
False,
"builtins.bool",
line=expr.line,
column=expr.column,
expression=not allow_new_syntax,
)
else:
return UnboundType(name, line=expr.line, column=expr.column)
elif isinstance(expr, MemberExpr):
fullname = get_member_expr_fullname(expr)
if fullname:
return UnboundType(fullname, line=expr.line, column=expr.column)
return UnboundType(fullname, line=expr.line, column=expr.column, expression=True)
else:
raise TypeTranslationError()
elif isinstance(expr, IndexExpr):
Expand Down Expand Up @@ -193,7 +204,13 @@ def expr_to_unanalyzed_type(
return typ
raise TypeTranslationError()
elif isinstance(expr, IntExpr):
return RawExpressionType(expr.value, "builtins.int", line=expr.line, column=expr.column)
return RawExpressionType(
expr.value,
"builtins.int",
line=expr.line,
column=expr.column,
expression=not allow_new_syntax,
)
elif isinstance(expr, FloatExpr):
# 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.
Expand Down
3 changes: 3 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,9 @@ def add_invertible_flag(
help="Partially typed/incomplete functions in this module are considered typed"
" for untyped call errors.",
)
add_invertible_flag(
"--bare-literals", default=True, help="Allow bare literals.", group=based_group
)

config_group = parser.add_argument_group(
title="Config file",
Expand Down
3 changes: 3 additions & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage":

# Invalid types
INVALID_TYPE_RAW_ENUM_VALUE: Final = "Invalid type: try using Literal[{}.{}] instead?"
INVALID_BARE_LITERAL: Final = (
'"{0}" is a bare literal and cannot be used here, try Literal[{0}] instead?'
)

# Type checker error message constants
NO_RETURN_VALUE_EXPECTED: Final = ErrorMessage("No return value expected", codes.RETURN_VALUE)
Expand Down
1 change: 1 addition & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def __init__(self) -> None:
self.targets: List[str] = []
self.ignore_any_from_error = True
self.incomplete_is_typed = flip_if_not_based(False)
self.bare_literals = flip_if_not_based(True)

# disallow_any options
self.disallow_any_generics = flip_if_not_based(True)
Expand Down
11 changes: 9 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3649,8 +3649,15 @@ def process_typevar_parameters(
upper_bound = get_proper_type(analyzed)
if isinstance(upper_bound, AnyType) and upper_bound.is_from_error:
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
# Note: we do not return 'None' here -- we want to continue
# using the AnyType as the upper bound.
elif isinstance(upper_bound, LiteralType) and upper_bound.bare_literal:
self.fail(
message_registry.INVALID_BARE_LITERAL.format(upper_bound.value_repr()),
param_value,
code=codes.VALID_TYPE,
)

# Note: we do not return 'None' here -- we want to continue
# using the AnyType as the upper bound.
except TypeTranslationError:
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
return None
Expand Down
12 changes: 10 additions & 2 deletions mypy/semanal_newtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from typing import Optional, Tuple

from mypy import errorcodes as codes
from mypy import errorcodes as codes, message_registry
from mypy.errorcodes import ErrorCode
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.messages import MessageBuilder, format_type
Expand Down Expand Up @@ -36,6 +36,7 @@
AnyType,
CallableType,
Instance,
LiteralType,
NoneType,
PlaceholderType,
TupleType,
Expand Down Expand Up @@ -202,10 +203,17 @@ def check_newtype_args(
should_defer = True

# 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.
# a valid one. So, we translate AnyTypes created from errors and bare literals into None.
if isinstance(old_type, AnyType) and old_type.is_from_error:
self.fail(msg, context)
return None, False
elif isinstance(old_type, LiteralType) and old_type.bare_literal:
self.fail(
message_registry.INVALID_BARE_LITERAL.format(old_type.value_repr()),
context,
code=codes.VALID_TYPE,
)
return None, False

return None if has_failed else old_type, should_defer

Expand Down
78 changes: 56 additions & 22 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,24 +682,32 @@ def analyze_unbound_type_without_type_info(
# a "Literal[...]" type. So, if `defining_literal` is not set,
# we bail out early with an error.
#
# If, in the distant future, we decide to permit things like
# `def foo(x: Color.RED) -> None: ...`, we can remove that
# check entirely.
# Based: we permit things like `def foo(x: Color.RED) -> None: ...`
if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum:
value = sym.node.name
base_enum_short_name = sym.node.info.name
# it's invalid to use in an expression, ie: a TypeAlias
if not defining_literal:
msg = message_registry.INVALID_TYPE_RAW_ENUM_VALUE.format(
base_enum_short_name, value
)
self.fail(msg, t)
return AnyType(TypeOfAny.from_error)
return LiteralType(
if not self.options.bare_literals:
base_enum_short_name = sym.node.info.name
msg = message_registry.INVALID_TYPE_RAW_ENUM_VALUE.format(
base_enum_short_name, value
)
self.fail(msg, t)
if t.expression:
base_enum_short_name = sym.node.info.name
name = f"{base_enum_short_name}.{value}"
msg = message_registry.INVALID_BARE_LITERAL.format(name)
self.fail(msg, t)
result = LiteralType(
value=value,
fallback=Instance(sym.node.info, [], line=t.line, column=t.column),
line=t.line,
column=t.column,
bare_literal=True,
)
if t.expression:
return result
return result.accept(self)

# None of the above options worked. We parse the args (if there are any)
# to make sure there are no remaining semanal-only types, then give up.
Expand Down Expand Up @@ -934,16 +942,18 @@ def visit_raw_expression_type(self, t: RawExpressionType) -> Type:
# "fake literals" should always be wrapped in an UnboundType
# corresponding to 'Literal'.
#
# Note: if at some point in the distant future, we decide to
# make signatures like "foo(x: 20) -> None" legal, we can change
# this method so it generates and returns an actual LiteralType
# instead.
# Based: signatures like "foo(x: 20) -> None" are legal, this method
# generates and returns an actual LiteralType instead.

if self.report_invalid_types:
msg = None
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 = f"Invalid type: try using Literal[{repr(t.literal_value)}] instead?"
if not self.options.bare_literals:
# The only time it makes sense to use an int or bool is inside of
# a literal type.
msg = f"Invalid type: try using Literal[{repr(t.literal_value)}] instead?"
if t.expression:
msg = message_registry.INVALID_BARE_LITERAL.format(t.literal_value)
elif t.base_type_name in ("builtins.float", "builtins.complex"):
# We special-case warnings for floats and complex numbers.
msg = f"Invalid type: {t.simple_name()} literals cannot be used as a type"
Expand All @@ -954,14 +964,38 @@ def visit_raw_expression_type(self, t: RawExpressionType) -> Type:
# 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, code=codes.VALID_TYPE)
if t.note is not None:
self.note(t.note, t, code=codes.VALID_TYPE)

if msg:
self.fail(msg, t, code=codes.VALID_TYPE)
if t.note is not None:
self.note(t.note, t, code=codes.VALID_TYPE)
if t.base_type_name in ("builtins.int", "builtins.bool"):
v = t.literal_value
assert v is not None
result = LiteralType(
v,
fallback=self.named_type(t.base_type_name),
line=t.line,
column=t.column,
bare_literal=True,
)
if t.expression:
return result
return result.accept(self)
return AnyType(TypeOfAny.from_error, line=t.line, column=t.column)

def visit_literal_type(self, t: LiteralType) -> Type:
if (
self.nesting_level
and t.bare_literal
and not (self.api.is_future_flag_set("annotations") or self.api.is_stub_file)
and self.options.bare_literals
):
self.fail(
f'"{t}" is a bare literal and shouldn\'t be used in a type operation without'
' "__future__.annotations"',
t,
code=codes.VALID_TYPE,
)
return t

def visit_star_type(self, t: StarType) -> Type:
Expand Down
19 changes: 16 additions & 3 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ class UnboundType(ProperType):
"empty_tuple_index",
"original_str_expr",
"original_str_fallback",
"expression",
)

def __init__(
Expand All @@ -774,6 +775,7 @@ def __init__(
empty_tuple_index: bool = False,
original_str_expr: Optional[str] = None,
original_str_fallback: Optional[str] = None,
expression=False,
) -> None:
super().__init__(line, column)
if not args:
Expand Down Expand Up @@ -801,6 +803,9 @@ def __init__(
self.original_str_expr = original_str_expr
self.original_str_fallback = original_str_fallback

self.expression = expression
"""This is used to ban bare Enum literals from expressions like ``TypeAlias``es"""

def copy_modified(self, args: Bogus[Optional[Sequence[Type]]] = _dummy) -> "UnboundType":
if args is _dummy:
args = self.args
Expand Down Expand Up @@ -2368,7 +2373,7 @@ class RawExpressionType(ProperType):
)
"""

__slots__ = ("literal_value", "base_type_name", "note")
__slots__ = ("literal_value", "base_type_name", "note", "expression")

def __init__(
self,
Expand All @@ -2377,11 +2382,13 @@ def __init__(
line: int = -1,
column: int = -1,
note: Optional[str] = None,
expression=False,
) -> None:
super().__init__(line, column)
self.literal_value = literal_value
self.base_type_name = base_type_name
self.note = note
self.expression = expression

def simple_name(self) -> str:
return self.base_type_name.replace("builtins.", "")
Expand Down Expand Up @@ -2422,15 +2429,21 @@ class LiteralType(ProperType):
represented as `LiteralType(value="RED", fallback=instance_of_color)'.
"""

__slots__ = ("value", "fallback", "_hash")
__slots__ = ("value", "fallback", "_hash", "bare_literal")

def __init__(
self, value: LiteralValue, fallback: Instance, line: int = -1, column: int = -1
self,
value: LiteralValue,
fallback: Instance,
line: int = -1,
column: int = -1,
bare_literal=False,
) -> None:
self.value = value
super().__init__(line, column)
self.fallback = fallback
self._hash = -1 # Cached hash value
self.bare_literal = bare_literal

def can_be_false_default(self) -> bool:
return not self.value
Expand Down
1 change: 1 addition & 0 deletions mypy_bootstrap.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ warn_unused_configs = True
show_traceback = True
always_true = MYPYC
infer_function_types = True
bare_literals = True

[mypy-mypy.*]
default_return = True
Expand Down
1 change: 1 addition & 0 deletions mypy_self_check.ini
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ no_warn_unreachable = True
implicit_reexport = True
disallow_redefinition = True
disable_error_code = truthy-bool, redundant-expr, ignore-without-code, no-untyped-usage
bare_literals = True

[mypy-mypy.*,mypyc.*]
default_return = True
Expand Down
Loading

0 comments on commit ff66817

Please sign in to comment.