Skip to content

Commit ff66817

Browse files
committed
allow literal types without Literal
1 parent c92df4b commit ff66817

19 files changed

+1763
-1504
lines changed

.mypy/baseline.json

Lines changed: 1518 additions & 1460 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Basedmypy Changelog
22

33
## [Unreleased]
4+
### Added
5+
- Allow literal `int`, `bool` and `Enum`s without `Literal`
46
### Enhancements
57
- Unionize at type joins instead of common ancestor
68
- Render Literal types better in output messages

mypy/exprtotype.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Translate an Expression to a Type value."""
2-
32
from typing import Optional
43

54
from mypy.fastparse import parse_type_string
@@ -73,15 +72,27 @@ def expr_to_unanalyzed_type(
7372
if isinstance(expr, NameExpr):
7473
name = expr.name
7574
if name == "True":
76-
return RawExpressionType(True, "builtins.bool", line=expr.line, column=expr.column)
75+
return RawExpressionType(
76+
True,
77+
"builtins.bool",
78+
line=expr.line,
79+
column=expr.column,
80+
expression=not allow_new_syntax,
81+
)
7782
elif name == "False":
78-
return RawExpressionType(False, "builtins.bool", line=expr.line, column=expr.column)
83+
return RawExpressionType(
84+
False,
85+
"builtins.bool",
86+
line=expr.line,
87+
column=expr.column,
88+
expression=not allow_new_syntax,
89+
)
7990
else:
8091
return UnboundType(name, line=expr.line, column=expr.column)
8192
elif isinstance(expr, MemberExpr):
8293
fullname = get_member_expr_fullname(expr)
8394
if fullname:
84-
return UnboundType(fullname, line=expr.line, column=expr.column)
95+
return UnboundType(fullname, line=expr.line, column=expr.column, expression=True)
8596
else:
8697
raise TypeTranslationError()
8798
elif isinstance(expr, IndexExpr):
@@ -193,7 +204,13 @@ def expr_to_unanalyzed_type(
193204
return typ
194205
raise TypeTranslationError()
195206
elif isinstance(expr, IntExpr):
196-
return RawExpressionType(expr.value, "builtins.int", line=expr.line, column=expr.column)
207+
return RawExpressionType(
208+
expr.value,
209+
"builtins.int",
210+
line=expr.line,
211+
column=expr.column,
212+
expression=not allow_new_syntax,
213+
)
197214
elif isinstance(expr, FloatExpr):
198215
# Floats are not valid parameters for RawExpressionType , so we just
199216
# pass in 'None' for now. We'll report the appropriate error at a later stage.

mypy/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,9 @@ def add_invertible_flag(
608608
help="Partially typed/incomplete functions in this module are considered typed"
609609
" for untyped call errors.",
610610
)
611+
add_invertible_flag(
612+
"--bare-literals", default=True, help="Allow bare literals.", group=based_group
613+
)
611614

612615
config_group = parser.add_argument_group(
613616
title="Config file",

mypy/message_registry.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage":
2323

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

2730
# Type checker error message constants
2831
NO_RETURN_VALUE_EXPECTED: Final = ErrorMessage("No return value expected", codes.RETURN_VALUE)

mypy/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def __init__(self) -> None:
131131
self.targets: List[str] = []
132132
self.ignore_any_from_error = True
133133
self.incomplete_is_typed = flip_if_not_based(False)
134+
self.bare_literals = flip_if_not_based(True)
134135

135136
# disallow_any options
136137
self.disallow_any_generics = flip_if_not_based(True)

mypy/semanal.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3649,8 +3649,15 @@ def process_typevar_parameters(
36493649
upper_bound = get_proper_type(analyzed)
36503650
if isinstance(upper_bound, AnyType) and upper_bound.is_from_error:
36513651
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
3652-
# Note: we do not return 'None' here -- we want to continue
3653-
# using the AnyType as the upper bound.
3652+
elif isinstance(upper_bound, LiteralType) and upper_bound.bare_literal:
3653+
self.fail(
3654+
message_registry.INVALID_BARE_LITERAL.format(upper_bound.value_repr()),
3655+
param_value,
3656+
code=codes.VALID_TYPE,
3657+
)
3658+
3659+
# Note: we do not return 'None' here -- we want to continue
3660+
# using the AnyType as the upper bound.
36543661
except TypeTranslationError:
36553662
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
36563663
return None

mypy/semanal_newtype.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from typing import Optional, Tuple
77

8-
from mypy import errorcodes as codes
8+
from mypy import errorcodes as codes, message_registry
99
from mypy.errorcodes import ErrorCode
1010
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
1111
from mypy.messages import MessageBuilder, format_type
@@ -36,6 +36,7 @@
3636
AnyType,
3737
CallableType,
3838
Instance,
39+
LiteralType,
3940
NoneType,
4041
PlaceholderType,
4142
TupleType,
@@ -202,10 +203,17 @@ def check_newtype_args(
202203
should_defer = True
203204

204205
# The caller of this function assumes that if we return a Type, it's always
205-
# a valid one. So, we translate AnyTypes created from errors into None.
206+
# a valid one. So, we translate AnyTypes created from errors and bare literals into None.
206207
if isinstance(old_type, AnyType) and old_type.is_from_error:
207208
self.fail(msg, context)
208209
return None, False
210+
elif isinstance(old_type, LiteralType) and old_type.bare_literal:
211+
self.fail(
212+
message_registry.INVALID_BARE_LITERAL.format(old_type.value_repr()),
213+
context,
214+
code=codes.VALID_TYPE,
215+
)
216+
return None, False
209217

210218
return None if has_failed else old_type, should_defer
211219

mypy/typeanal.py

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -682,24 +682,32 @@ def analyze_unbound_type_without_type_info(
682682
# a "Literal[...]" type. So, if `defining_literal` is not set,
683683
# we bail out early with an error.
684684
#
685-
# If, in the distant future, we decide to permit things like
686-
# `def foo(x: Color.RED) -> None: ...`, we can remove that
687-
# check entirely.
685+
# Based: we permit things like `def foo(x: Color.RED) -> None: ...`
688686
if isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum:
689687
value = sym.node.name
690-
base_enum_short_name = sym.node.info.name
688+
# it's invalid to use in an expression, ie: a TypeAlias
691689
if not defining_literal:
692-
msg = message_registry.INVALID_TYPE_RAW_ENUM_VALUE.format(
693-
base_enum_short_name, value
694-
)
695-
self.fail(msg, t)
696-
return AnyType(TypeOfAny.from_error)
697-
return LiteralType(
690+
if not self.options.bare_literals:
691+
base_enum_short_name = sym.node.info.name
692+
msg = message_registry.INVALID_TYPE_RAW_ENUM_VALUE.format(
693+
base_enum_short_name, value
694+
)
695+
self.fail(msg, t)
696+
if t.expression:
697+
base_enum_short_name = sym.node.info.name
698+
name = f"{base_enum_short_name}.{value}"
699+
msg = message_registry.INVALID_BARE_LITERAL.format(name)
700+
self.fail(msg, t)
701+
result = LiteralType(
698702
value=value,
699703
fallback=Instance(sym.node.info, [], line=t.line, column=t.column),
700704
line=t.line,
701705
column=t.column,
706+
bare_literal=True,
702707
)
708+
if t.expression:
709+
return result
710+
return result.accept(self)
703711

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

942948
if self.report_invalid_types:
949+
msg = None
943950
if t.base_type_name in ("builtins.int", "builtins.bool"):
944-
# The only time it makes sense to use an int or bool is inside of
945-
# a literal type.
946-
msg = f"Invalid type: try using Literal[{repr(t.literal_value)}] instead?"
951+
if not self.options.bare_literals:
952+
# The only time it makes sense to use an int or bool is inside of
953+
# a literal type.
954+
msg = f"Invalid type: try using Literal[{repr(t.literal_value)}] instead?"
955+
if t.expression:
956+
msg = message_registry.INVALID_BARE_LITERAL.format(t.literal_value)
947957
elif t.base_type_name in ("builtins.float", "builtins.complex"):
948958
# We special-case warnings for floats and complex numbers.
949959
msg = f"Invalid type: {t.simple_name()} literals cannot be used as a type"
@@ -954,14 +964,38 @@ def visit_raw_expression_type(self, t: RawExpressionType) -> Type:
954964
# string, it's unclear if the user meant to construct a literal type
955965
# or just misspelled a regular type. So we avoid guessing.
956966
msg = "Invalid type comment or annotation"
957-
958-
self.fail(msg, t, code=codes.VALID_TYPE)
959-
if t.note is not None:
960-
self.note(t.note, t, code=codes.VALID_TYPE)
961-
967+
if msg:
968+
self.fail(msg, t, code=codes.VALID_TYPE)
969+
if t.note is not None:
970+
self.note(t.note, t, code=codes.VALID_TYPE)
971+
if t.base_type_name in ("builtins.int", "builtins.bool"):
972+
v = t.literal_value
973+
assert v is not None
974+
result = LiteralType(
975+
v,
976+
fallback=self.named_type(t.base_type_name),
977+
line=t.line,
978+
column=t.column,
979+
bare_literal=True,
980+
)
981+
if t.expression:
982+
return result
983+
return result.accept(self)
962984
return AnyType(TypeOfAny.from_error, line=t.line, column=t.column)
963985

964986
def visit_literal_type(self, t: LiteralType) -> Type:
987+
if (
988+
self.nesting_level
989+
and t.bare_literal
990+
and not (self.api.is_future_flag_set("annotations") or self.api.is_stub_file)
991+
and self.options.bare_literals
992+
):
993+
self.fail(
994+
f'"{t}" is a bare literal and shouldn\'t be used in a type operation without'
995+
' "__future__.annotations"',
996+
t,
997+
code=codes.VALID_TYPE,
998+
)
965999
return t
9661000

9671001
def visit_star_type(self, t: StarType) -> Type:

mypy/types.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,7 @@ class UnboundType(ProperType):
762762
"empty_tuple_index",
763763
"original_str_expr",
764764
"original_str_fallback",
765+
"expression",
765766
)
766767

767768
def __init__(
@@ -774,6 +775,7 @@ def __init__(
774775
empty_tuple_index: bool = False,
775776
original_str_expr: Optional[str] = None,
776777
original_str_fallback: Optional[str] = None,
778+
expression=False,
777779
) -> None:
778780
super().__init__(line, column)
779781
if not args:
@@ -801,6 +803,9 @@ def __init__(
801803
self.original_str_expr = original_str_expr
802804
self.original_str_fallback = original_str_fallback
803805

806+
self.expression = expression
807+
"""This is used to ban bare Enum literals from expressions like ``TypeAlias``es"""
808+
804809
def copy_modified(self, args: Bogus[Optional[Sequence[Type]]] = _dummy) -> "UnboundType":
805810
if args is _dummy:
806811
args = self.args
@@ -2368,7 +2373,7 @@ class RawExpressionType(ProperType):
23682373
)
23692374
"""
23702375

2371-
__slots__ = ("literal_value", "base_type_name", "note")
2376+
__slots__ = ("literal_value", "base_type_name", "note", "expression")
23722377

23732378
def __init__(
23742379
self,
@@ -2377,11 +2382,13 @@ def __init__(
23772382
line: int = -1,
23782383
column: int = -1,
23792384
note: Optional[str] = None,
2385+
expression=False,
23802386
) -> None:
23812387
super().__init__(line, column)
23822388
self.literal_value = literal_value
23832389
self.base_type_name = base_type_name
23842390
self.note = note
2391+
self.expression = expression
23852392

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

2425-
__slots__ = ("value", "fallback", "_hash")
2432+
__slots__ = ("value", "fallback", "_hash", "bare_literal")
24262433

24272434
def __init__(
2428-
self, value: LiteralValue, fallback: Instance, line: int = -1, column: int = -1
2435+
self,
2436+
value: LiteralValue,
2437+
fallback: Instance,
2438+
line: int = -1,
2439+
column: int = -1,
2440+
bare_literal=False,
24292441
) -> None:
24302442
self.value = value
24312443
super().__init__(line, column)
24322444
self.fallback = fallback
24332445
self._hash = -1 # Cached hash value
2446+
self.bare_literal = bare_literal
24342447

24352448
def can_be_false_default(self) -> bool:
24362449
return not self.value

0 commit comments

Comments
 (0)