Skip to content

Commit e5a7495

Browse files
committed
Add interactions between Literal and Final
This pull request adds logic to handle interactions between Literal and Final: for example, inferring that `foo` has type `Literal[3]` when doing `foo: Final = 3`. A few additional notes: 1. This unfortunately had the side-effect of causing some of the existing tests for `Final` become noiser. I decided to mostly bias towards preserving the original error messages by modifying many of the existing variable assignments to explicitly use things like `Final[int]`. I left in the new error messages in a few cases -- mostly in cases where I was able to add them in a relatively tidy way. Let me know if this needs to be handled differently. 2. Since mypy uses 'Final', this means that once this PR lands, mypy itself will actually be using Literal types (albeit somewhat indirectly) for the first time. I'm not fully sure what the ramifications of this are. For example, do we need to detour and add support for literal types to mypyc? 3. Are there any major users of `Final` other then mypy? It didn't seem like we were really using it in our internal codebase at least, but I could be wrong about that. If there *are* some people who have already started depending on 'Final', maybe we should defer landing this PR until Literal types are more stable to avoid disrupting them. I had to make a few changes to mypy's own source code to get it to type check under these new semantics, for example.
1 parent 1d40527 commit e5a7495

13 files changed

+243
-71
lines changed

mypy/checker.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
174174
# Type checking pass number (0 = first pass)
175175
pass_num = 0
176176
# Last pass number to take
177-
last_pass = DEFAULT_LAST_PASS
177+
last_pass = DEFAULT_LAST_PASS # type: int
178178
# Have we deferred the current function? If yes, don't infer additional
179179
# types during this pass within the function.
180180
current_node_deferred = False
@@ -1809,8 +1809,8 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
18091809
self.check_indexed_assignment(index_lvalue, rvalue, lvalue)
18101810

18111811
if inferred:
1812-
self.infer_variable_type(inferred, lvalue, self.expr_checker.accept(rvalue),
1813-
rvalue)
1812+
rvalue_type = self.expr_checker.accept(rvalue, infer_literal=inferred.is_final)
1813+
self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue)
18141814

18151815
def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[Type],
18161816
rvalue: Expression) -> bool:

mypy/checkexpr.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def __init__(self,
139139
self.msg = msg
140140
self.plugin = plugin
141141
self.type_context = [None]
142+
self.infer_literal = False
142143
# Temporary overrides for expression types. This is currently
143144
# used by the union math in overloads.
144145
# TODO: refactor this to use a pattern similar to one in
@@ -210,7 +211,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
210211

211212
def analyze_var_ref(self, var: Var, context: Context) -> Type:
212213
if var.type:
213-
if is_literal_type_like(self.type_context[-1]) and var.name() in {'True', 'False'}:
214+
if self.is_literal_context() and var.name() in {'True', 'False'}:
214215
return LiteralType(var.name() == 'True', self.named_type('builtins.bool'))
215216
else:
216217
return var.type
@@ -1771,14 +1772,14 @@ def analyze_external_member_access(self, member: str, base_type: Type,
17711772
def visit_int_expr(self, e: IntExpr) -> Type:
17721773
"""Type check an integer literal (trivial)."""
17731774
typ = self.named_type('builtins.int')
1774-
if is_literal_type_like(self.type_context[-1]):
1775+
if self.is_literal_context():
17751776
return LiteralType(value=e.value, fallback=typ)
17761777
return typ
17771778

17781779
def visit_str_expr(self, e: StrExpr) -> Type:
17791780
"""Type check a string literal (trivial)."""
17801781
typ = self.named_type('builtins.str')
1781-
if is_literal_type_like(self.type_context[-1]):
1782+
if self.is_literal_context():
17821783
return LiteralType(value=e.value, fallback=typ)
17831784
return typ
17841785

@@ -3112,13 +3113,16 @@ def accept(self,
31123113
type_context: Optional[Type] = None,
31133114
allow_none_return: bool = False,
31143115
always_allow_any: bool = False,
3116+
infer_literal: bool = False,
31153117
) -> Type:
31163118
"""Type check a node in the given type context. If allow_none_return
31173119
is True and this expression is a call, allow it to return None. This
31183120
applies only to this expression and not any subexpressions.
31193121
"""
31203122
if node in self.type_overrides:
31213123
return self.type_overrides[node]
3124+
old_infer_literal = self.infer_literal
3125+
self.infer_literal = infer_literal
31223126
self.type_context.append(type_context)
31233127
try:
31243128
if allow_none_return and isinstance(node, CallExpr):
@@ -3131,6 +3135,7 @@ def accept(self,
31313135
report_internal_error(err, self.chk.errors.file,
31323136
node.line, self.chk.errors, self.chk.options)
31333137
self.type_context.pop()
3138+
self.infer_literal = old_infer_literal
31343139
assert typ is not None
31353140
self.chk.store_type(node, typ)
31363141

@@ -3376,6 +3381,9 @@ def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type:
33763381
return ans
33773382
return known_type
33783383

3384+
def is_literal_context(self) -> bool:
3385+
return self.infer_literal or is_literal_type_like(self.type_context[-1])
3386+
33793387

33803388
def has_any_type(t: Type) -> bool:
33813389
"""Whether t contains an Any type"""

mypy/defaults.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
PYTHON2_VERSION = (2, 7) # type: Final
66
PYTHON3_VERSION = (3, 6) # type: Final
77
PYTHON3_VERSION_MIN = (3, 4) # type: Final
8-
CACHE_DIR = '.mypy_cache' # type: Final
9-
CONFIG_FILE = 'mypy.ini' # type: Final
8+
CACHE_DIR = '.mypy_cache' # type: Final[str]
9+
CONFIG_FILE = 'mypy.ini' # type: Final[str]
1010
SHARED_CONFIG_FILES = ('setup.cfg',) # type: Final
1111
USER_CONFIG_FILES = ('~/.mypy.ini',) # type: Final
1212
CONFIG_FILES = (CONFIG_FILE,) + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final

mypy/reachability.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def infer_condition_value(expr: Expression, options: Options) -> int:
7777
if alias.op == 'not':
7878
expr = alias.expr
7979
negated = True
80-
result = TRUTH_VALUE_UNKNOWN
80+
result = TRUTH_VALUE_UNKNOWN # type: int
8181
if isinstance(expr, NameExpr):
8282
name = expr.name
8383
elif isinstance(expr, MemberExpr):

mypy/semanal.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
from mypy.messages import CANNOT_ASSIGN_TO_TYPE, MessageBuilder
6666
from mypy.types import (
6767
FunctionLike, UnboundType, TypeVarDef, TupleType, UnionType, StarType, function_type,
68-
CallableType, Overloaded, Instance, Type, AnyType,
68+
CallableType, Overloaded, Instance, Type, AnyType, LiteralType,
6969
TypeTranslator, TypeOfAny, TypeType, NoneTyp,
7070
)
7171
from mypy.nodes import implicit_module_attrs
@@ -1756,9 +1756,9 @@ def final_cb(keep_final: bool) -> None:
17561756
self.type and self.type.is_protocol and not self.is_func_scope()):
17571757
self.fail('All protocol members must have explicitly declared types', s)
17581758
# Set the type if the rvalue is a simple literal (even if the above error occurred).
1759-
if len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr):
1759+
if len(s.lvalues) == 1 and isinstance(s.lvalues[0], RefExpr):
17601760
if s.lvalues[0].is_inferred_def:
1761-
s.type = self.analyze_simple_literal_type(s.rvalue)
1761+
s.type = self.analyze_simple_literal_type(s.rvalue, s.is_final_def)
17621762
if s.type:
17631763
# Store type into nodes.
17641764
for lvalue in s.lvalues:
@@ -1896,8 +1896,10 @@ def unbox_literal(self, e: Expression) -> Optional[Union[int, float, bool, str]]
18961896
return True if e.name == 'True' else False
18971897
return None
18981898

1899-
def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]:
1900-
"""Return builtins.int if rvalue is an int literal, etc."""
1899+
def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Optional[Type]:
1900+
"""Return builtins.int if rvalue is an int literal, etc.
1901+
1902+
If this is a 'Final' context, we return "Literal[...]" instead."""
19011903
if self.options.semantic_analysis_only or self.function_stack:
19021904
# Skip this if we're only doing the semantic analysis pass.
19031905
# This is mostly to avoid breaking unit tests.
@@ -1907,15 +1909,22 @@ def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]:
19071909
# AnyStr).
19081910
return None
19091911
if isinstance(rvalue, IntExpr):
1910-
return self.named_type_or_none('builtins.int')
1912+
typ = self.named_type_or_none('builtins.int')
1913+
if typ and is_final:
1914+
return LiteralType(rvalue.value, typ, rvalue.line, rvalue.column)
1915+
return typ
19111916
if isinstance(rvalue, FloatExpr):
19121917
return self.named_type_or_none('builtins.float')
19131918
if isinstance(rvalue, StrExpr):
1914-
return self.named_type_or_none('builtins.str')
1919+
typ = self.named_type_or_none('builtins.str')
1920+
if typ and is_final:
1921+
return LiteralType(rvalue.value, typ, rvalue.line, rvalue.column)
1922+
return typ
19151923
if isinstance(rvalue, BytesExpr):
19161924
return self.named_type_or_none('builtins.bytes')
19171925
if isinstance(rvalue, UnicodeExpr):
19181926
return self.named_type_or_none('builtins.unicode')
1927+
19191928
return None
19201929

19211930
def analyze_alias(self, rvalue: Expression) -> Tuple[Optional[Type], List[str],

mypy/stubgen.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def __init__(self, _all_: Optional[List[str]], pyversion: Tuple[int, int],
420420
self._import_lines = [] # type: List[str]
421421
self._indent = ''
422422
self._vars = [[]] # type: List[List[str]]
423-
self._state = EMPTY
423+
self._state = EMPTY # type: str
424424
self._toplevel_names = [] # type: List[str]
425425
self._pyversion = pyversion
426426
self._include_private = include_private

mypy/subtypes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ def get_member_flags(name: str, info: TypeInfo) -> Set[int]:
589589
return {IS_CLASS_OR_STATIC}
590590
# just a variable
591591
if isinstance(v, Var) and not v.is_property:
592-
flags = {IS_SETTABLE}
592+
flags = {IS_SETTABLE} # type: Set[int]
593593
if v.is_classvar:
594594
flags.add(IS_CLASSVAR)
595595
return flags

mypy/typeanal.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,7 @@ def replace_alias_tvars(tp: Type, vars: List[str], subs: List[Type],
10731073
def set_any_tvars(tp: Type, vars: List[str],
10741074
newline: int, newcolumn: int, implicit: bool = True) -> Type:
10751075
if implicit:
1076-
type_of_any = TypeOfAny.from_omitted_generics
1076+
type_of_any = TypeOfAny.from_omitted_generics # type: int
10771077
else:
10781078
type_of_any = TypeOfAny.special_form
10791079
any_type = AnyType(type_of_any, line=newline, column=newcolumn)

0 commit comments

Comments
 (0)