Skip to content

Commit

Permalink
Add interactions between Literal and Final
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Michael0x2a committed Dec 18, 2018
1 parent cf25319 commit 304f4c7
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 71 deletions.
6 changes: 3 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
# Type checking pass number (0 = first pass)
pass_num = 0
# Last pass number to take
last_pass = DEFAULT_LAST_PASS
last_pass = DEFAULT_LAST_PASS # type: int
# Have we deferred the current function? If yes, don't infer additional
# types during this pass within the function.
current_node_deferred = False
Expand Down Expand Up @@ -1810,8 +1810,8 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
self.check_indexed_assignment(index_lvalue, rvalue, lvalue)

if inferred:
self.infer_variable_type(inferred, lvalue, self.expr_checker.accept(rvalue),
rvalue)
rvalue_type = self.expr_checker.accept(rvalue, infer_literal=inferred.is_final)
self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue)

def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[Type],
rvalue: Expression) -> bool:
Expand Down
14 changes: 11 additions & 3 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def __init__(self,
self.msg = msg
self.plugin = plugin
self.type_context = [None]
self.infer_literal = False
# Temporary overrides for expression types. This is currently
# used by the union math in overloads.
# TODO: refactor this to use a pattern similar to one in
Expand Down Expand Up @@ -210,7 +211,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:

def analyze_var_ref(self, var: Var, context: Context) -> Type:
if var.type:
if is_literal_type_like(self.type_context[-1]) and var.name() in {'True', 'False'}:
if self.is_literal_context() and var.name() in {'True', 'False'}:
return LiteralType(var.name() == 'True', self.named_type('builtins.bool'))
else:
return var.type
Expand Down Expand Up @@ -1771,14 +1772,14 @@ def analyze_external_member_access(self, member: str, base_type: Type,
def visit_int_expr(self, e: IntExpr) -> Type:
"""Type check an integer literal (trivial)."""
typ = self.named_type('builtins.int')
if is_literal_type_like(self.type_context[-1]):
if self.is_literal_context():
return LiteralType(value=e.value, fallback=typ)
return typ

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

Expand Down Expand Up @@ -3106,13 +3107,16 @@ def accept(self,
type_context: Optional[Type] = None,
allow_none_return: bool = False,
always_allow_any: bool = False,
infer_literal: bool = False,
) -> Type:
"""Type check a node in the given type context. If allow_none_return
is True and this expression is a call, allow it to return None. This
applies only to this expression and not any subexpressions.
"""
if node in self.type_overrides:
return self.type_overrides[node]
old_infer_literal = self.infer_literal
self.infer_literal = infer_literal
self.type_context.append(type_context)
try:
if allow_none_return and isinstance(node, CallExpr):
Expand All @@ -3125,6 +3129,7 @@ def accept(self,
report_internal_error(err, self.chk.errors.file,
node.line, self.chk.errors, self.chk.options)
self.type_context.pop()
self.infer_literal = old_infer_literal
assert typ is not None
self.chk.store_type(node, typ)

Expand Down Expand Up @@ -3370,6 +3375,9 @@ def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type:
return ans
return known_type

def is_literal_context(self) -> bool:
return self.infer_literal or is_literal_type_like(self.type_context[-1])


def has_any_type(t: Type) -> bool:
"""Whether t contains an Any type"""
Expand Down
4 changes: 2 additions & 2 deletions mypy/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
PYTHON2_VERSION = (2, 7) # type: Final
PYTHON3_VERSION = (3, 6) # type: Final
PYTHON3_VERSION_MIN = (3, 4) # type: Final
CACHE_DIR = '.mypy_cache' # type: Final
CONFIG_FILE = 'mypy.ini' # type: Final
CACHE_DIR = '.mypy_cache' # type: Final[str]
CONFIG_FILE = 'mypy.ini' # type: Final[str]
SHARED_CONFIG_FILES = ('setup.cfg',) # type: Final
USER_CONFIG_FILES = ('~/.mypy.ini',) # type: Final
CONFIG_FILES = (CONFIG_FILE,) + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final
2 changes: 1 addition & 1 deletion mypy/reachability.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def infer_condition_value(expr: Expression, options: Options) -> int:
if alias.op == 'not':
expr = alias.expr
negated = True
result = TRUTH_VALUE_UNKNOWN
result = TRUTH_VALUE_UNKNOWN # type: int
if isinstance(expr, NameExpr):
name = expr.name
elif isinstance(expr, MemberExpr):
Expand Down
23 changes: 16 additions & 7 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
from mypy.messages import CANNOT_ASSIGN_TO_TYPE, MessageBuilder
from mypy.types import (
FunctionLike, UnboundType, TypeVarDef, TupleType, UnionType, StarType, function_type,
CallableType, Overloaded, Instance, Type, AnyType,
CallableType, Overloaded, Instance, Type, AnyType, LiteralType,
TypeTranslator, TypeOfAny, TypeType, NoneTyp,
)
from mypy.nodes import implicit_module_attrs
Expand Down Expand Up @@ -1755,9 +1755,9 @@ def final_cb(keep_final: bool) -> None:
self.type and self.type.is_protocol and not self.is_func_scope()):
self.fail('All protocol members must have explicitly declared types', s)
# Set the type if the rvalue is a simple literal (even if the above error occurred).
if len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr):
if len(s.lvalues) == 1 and isinstance(s.lvalues[0], RefExpr):
if s.lvalues[0].is_inferred_def:
s.type = self.analyze_simple_literal_type(s.rvalue)
s.type = self.analyze_simple_literal_type(s.rvalue, s.is_final_def)
if s.type:
# Store type into nodes.
for lvalue in s.lvalues:
Expand Down Expand Up @@ -1895,8 +1895,10 @@ def unbox_literal(self, e: Expression) -> Optional[Union[int, float, bool, str]]
return True if e.name == 'True' else False
return None

def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]:
"""Return builtins.int if rvalue is an int literal, etc."""
def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Optional[Type]:
"""Return builtins.int if rvalue is an int literal, etc.
If this is a 'Final' context, we return "Literal[...]" instead."""
if self.options.semantic_analysis_only or self.function_stack:
# Skip this if we're only doing the semantic analysis pass.
# This is mostly to avoid breaking unit tests.
Expand All @@ -1906,15 +1908,22 @@ def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]:
# AnyStr).
return None
if isinstance(rvalue, IntExpr):
return self.named_type_or_none('builtins.int')
typ = self.named_type_or_none('builtins.int')
if typ and is_final:
return LiteralType(rvalue.value, typ, rvalue.line, rvalue.column)
return typ
if isinstance(rvalue, FloatExpr):
return self.named_type_or_none('builtins.float')
if isinstance(rvalue, StrExpr):
return self.named_type_or_none('builtins.str')
typ = self.named_type_or_none('builtins.str')
if typ and is_final:
return LiteralType(rvalue.value, typ, rvalue.line, rvalue.column)
return typ
if isinstance(rvalue, BytesExpr):
return self.named_type_or_none('builtins.bytes')
if isinstance(rvalue, UnicodeExpr):
return self.named_type_or_none('builtins.unicode')

return None

def analyze_alias(self, rvalue: Expression) -> Tuple[Optional[Type], List[str],
Expand Down
2 changes: 1 addition & 1 deletion mypy/stubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ def __init__(self, _all_: Optional[List[str]], pyversion: Tuple[int, int],
self._import_lines = [] # type: List[str]
self._indent = ''
self._vars = [[]] # type: List[List[str]]
self._state = EMPTY
self._state = EMPTY # type: str
self._toplevel_names = [] # type: List[str]
self._pyversion = pyversion
self._include_private = include_private
Expand Down
2 changes: 1 addition & 1 deletion mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ def get_member_flags(name: str, info: TypeInfo) -> Set[int]:
return {IS_CLASS_OR_STATIC}
# just a variable
if isinstance(v, Var) and not v.is_property:
flags = {IS_SETTABLE}
flags = {IS_SETTABLE} # type: Set[int]
if v.is_classvar:
flags.add(IS_CLASSVAR)
return flags
Expand Down
2 changes: 1 addition & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ def replace_alias_tvars(tp: Type, vars: List[str], subs: List[Type],
def set_any_tvars(tp: Type, vars: List[str],
newline: int, newcolumn: int, implicit: bool = True) -> Type:
if implicit:
type_of_any = TypeOfAny.from_omitted_generics
type_of_any = TypeOfAny.from_omitted_generics # type: int
else:
type_of_any = TypeOfAny.special_form
any_type = AnyType(type_of_any, line=newline, column=newcolumn)
Expand Down
Loading

0 comments on commit 304f4c7

Please sign in to comment.