From e5a74959ddd42da93e0f17f2f2ed9ed2939318c2 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Mon, 17 Dec 2018 19:48:32 -0800 Subject: [PATCH 1/6] 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. --- mypy/checker.py | 6 +- mypy/checkexpr.py | 14 ++- mypy/defaults.py | 4 +- mypy/reachability.py | 2 +- mypy/semanal.py | 23 +++-- mypy/stubgen.py | 2 +- mypy/subtypes.py | 2 +- mypy/typeanal.py | 2 +- test-data/unit/check-final.test | 81 ++++++++++------- test-data/unit/check-incremental.test | 10 +-- test-data/unit/check-literal.test | 124 +++++++++++++++++++++++++- test-data/unit/diff.test | 34 +++++-- test-data/unit/fine-grained.test | 10 +-- 13 files changed, 243 insertions(+), 71 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a2f41ad1cec7..8fc000127635 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -174,7 +174,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 @@ -1809,8 +1809,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: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 88968f9735bb..a450d3a8a062 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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 @@ -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 @@ -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 @@ -3112,6 +3113,7 @@ 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 @@ -3119,6 +3121,8 @@ def accept(self, """ 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): @@ -3131,6 +3135,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) @@ -3376,6 +3381,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""" diff --git a/mypy/defaults.py b/mypy/defaults.py index 3169ec540e8d..af1f68e44627 100644 --- a/mypy/defaults.py +++ b/mypy/defaults.py @@ -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 diff --git a/mypy/reachability.py b/mypy/reachability.py index 4585c08c861a..f871680f3fb8 100644 --- a/mypy/reachability.py +++ b/mypy/reachability.py @@ -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): diff --git a/mypy/semanal.py b/mypy/semanal.py index f774e10b33cc..1c358ee8858b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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 @@ -1756,9 +1756,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: @@ -1896,8 +1896,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. @@ -1907,15 +1909,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], diff --git a/mypy/stubgen.py b/mypy/stubgen.py index a97ded77cf6b..a79dd6a8ff00 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -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 diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 227b3ba10d27..300295db74a6 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -589,7 +589,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 diff --git a/mypy/typeanal.py b/mypy/typeanal.py index a37aa4c6c03b..a870273ebcfd 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1073,7 +1073,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) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 7f895b97d277..ab9054c51bdb 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -338,7 +338,7 @@ class C: [case testFinalReassignModuleVar] from typing import Final -x: Final = 1 +x: Final[int] = 1 x # Dummy reference to allow renaming once implemented x = 2 # E: Cannot assign to final name "x" def f() -> int: @@ -358,7 +358,7 @@ y: Final = 2 # E: Name 'y' already defined on line 17 \ # E: Cannot redefine an existing name as final y = 3 # No error here, first definition wins -z: Final = 1 +z: Final[int] = 1 z: Final = 2 # E: Name 'z' already defined on line 22 \ # E: Cannot redefine an existing name as final z = 3 # E: Cannot assign to final name "z" @@ -395,11 +395,11 @@ main:8: error: Cannot assign to final name "ID" from typing import Final def f() -> None: - nl: Final = 0 + nl: Final[int] = 0 x: Final = 1 x = 1 # E: Cannot assign to final name "x" - y: Final = 1 + y: Final[int] = 1 y: Final = 2 # E: Cannot redefine an existing name as final def nested() -> None: nonlocal nl @@ -418,7 +418,7 @@ x: Final[int] from typing import Final class C: - x: Final = 1 + x: Final[int] = 1 x = 2 # E: Cannot assign to final name "x" y = 1 @@ -430,9 +430,9 @@ from typing import Final class C: def __init__(self) -> None: - self.x: Final = 1 + self.x: Final[int] = 1 self.y = 1 - self.y: Final = 2 # E: Cannot redefine an existing name as final + self.y: Final[int] = 2 # E: Cannot redefine an existing name as final def meth(self) -> None: self.x = 2 # E: Cannot assign to final attribute "x" [out] @@ -441,9 +441,9 @@ class C: from typing import Final class C: - y: Final = 1 + y: Final[int] = 1 def __init__(self) -> None: - self.x: Final = 1 + self.x: Final[int] = 1 self.y = 2 # E: Cannot assign to final attribute "y" x = 2 # E: Cannot assign to final name "x" [out] @@ -452,9 +452,9 @@ class C: from typing import Final class C: - x: Final = 1 + x: Final[int] = 1 def __init__(self) -> None: - self.y: Final = 1 + self.y: Final[int] = 1 def meth(self) -> None: self.x = 2 # E: Cannot assign to final attribute "x" self.y = 2 # E: Cannot assign to final attribute "y" @@ -472,9 +472,9 @@ class C: from typing import Final class C: - x: Final = 1 + x: Final[int] = 1 def __init__(self) -> None: - self.y: Final = 1 + self.y: Final[int] = 1 class D(C): pass @@ -494,9 +494,14 @@ class C: class D(C): pass -C().x = 2 # E: Cannot assign to final attribute "x" -D().x = 2 # E: Cannot assign to final attribute "x" -D().y = 2 # E: Cannot assign to final attribute "y" +C().x = 2 # E: Cannot assign to final attribute "x" \ + # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") + +D().x = 2 # E: Cannot assign to final attribute "x" \ + # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") + +D().y = 2 # E: Cannot assign to final attribute "y" \ + # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") [out] [case testFinalWorksWithComplexTargets] @@ -546,9 +551,9 @@ class A: def y(self) -> int: ... class B(A): - x: Final = 1 + x: Final[int] = 1 def __init__(self) -> None: - self.y: Final = 1 + self.y: Final[int] = 1 class C(B): x: int = 2 # E: Cannot assign to final name "x" y: int = 2 # E: Cannot assign to final name "y" @@ -570,9 +575,9 @@ class A: @property def y(self) -> int: ... class B(A): - x: Final = 1 + x: Final[int] = 1 def __init__(self) -> None: - self.y: Final = 1 + self.y: Final[int] = 1 class C(B): x: Final = 2 # E: Cannot override final attribute "x" (previously declared in base class "B") y: Final = 2 # E: Cannot override final attribute "y" (previously declared in base class "B") @@ -593,11 +598,15 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x = 2 # E: Cannot assign to final attribute "x" - self.y = 2 # E: Cannot assign to final attribute "y" + self.x = 2 # E: Cannot assign to final attribute "x" \ + # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") + self.y = 2 # E: Cannot assign to final attribute "y" \ + # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") def meth(self) -> None: - self.x = 3 # E: Cannot assign to final attribute "x" - self.y = 3 # E: Cannot assign to final attribute "y" + self.x = 3 # E: Cannot assign to final attribute "x" \ + # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1]") + self.y = 3 # E: Cannot assign to final attribute "y" \ + # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1]") [builtins fixtures/property.pyi] [out] @@ -615,8 +624,10 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x: Final = 2 # E: Cannot override final attribute "x" (previously declared in base class "B") - self.y: Final = 2 # E: Cannot override final attribute "y" (previously declared in base class "B") + self.x: Final = 2 # E: Cannot override final attribute "x" (previously declared in base class "B") \ + # E: Incompatible types in assignment (expression has type "Literal[2]", base class "B" defined the type as "Literal[1]") + self.y: Final = 2 # E: Cannot override final attribute "y" (previously declared in base class "B") \ + # E: Incompatible types in assignment (expression has type "Literal[2]", base class "B" defined the type as "Literal[1]") [builtins fixtures/property.pyi] [out] @@ -634,8 +645,10 @@ class B(A): self.y: Final = 1 class C(B): def meth(self) -> None: - self.x: int = 2 # E: Cannot assign to final attribute "x" - self.y: int = 2 # E: Cannot assign to final attribute "y" + self.x: int = 2 # E: Cannot assign to final attribute "x" \ + # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "Literal[1]") + self.y: int = 2 # E: Cannot assign to final attribute "y" \ + # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "Literal[1]") self.x = 3 # E: Cannot assign to final attribute "x" self.y = 3 # E: Cannot assign to final attribute "y" @@ -680,7 +693,7 @@ C().x = 4 # E: Cannot assign to final attribute "x" from typing import Final class A: - x: Final = 1 + x: Final[int] = 1 class B: def __init__(self) -> None: self.x = 2 @@ -962,11 +975,11 @@ class D(B): [case testFinalCanUseTypingExtensions] from typing_extensions import final, Final -x: Final = 1 +x: Final[int] = 1 x = 2 # E: Cannot assign to final name "x" class S: - x: Final = 1 + x: Final[int] = 1 S.x = 2 # E: Cannot assign to final attribute "x" class B: @@ -984,11 +997,13 @@ class E(F): ... # E: Cannot inherit from final class "F" from typing_extensions import final as f, Final as F x: F = 1 -x = 2 # E: Cannot assign to final name "x" +x = 2 # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") \ + # E: Cannot assign to final name "x" class S: x: F = 1 -S.x = 2 # E: Cannot assign to final attribute "x" +S.x = 2 # E: Cannot assign to final attribute "x" \ + # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") class B: @f diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index b3e70a12f70a..7233c320b4fb 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4239,11 +4239,11 @@ class C: [file mod.py.2] from typing import Final -x: Final = 1 +x: Final[int] = 1 class C: - y: Final = 1 + y: Final[int] = 1 def __init__(self) -> None: - self.z: Final = 1 + self.z: Final[int] = 1 [out] [out2] main:5: error: Cannot assign to final name "x" @@ -4274,9 +4274,9 @@ class C: from typing import Final class C: - x: Final = 1 + x: Final[int] = 1 def __init__(self) -> None: - self.y: Final = 1 + self.y: Final[int] = 1 [out] [out2] main:4: error: Cannot assign to final name "x" diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index fb3941c10b47..8e7aa79a8f71 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2012,7 +2012,7 @@ reveal_type(func1(identity(a))) # E: Revealed type is 'Literal[19]' reveal_type(func1(identity(b))) # E: Revealed type is 'builtins.int' -- --- Other misc interactions +-- Interactions with meets -- [case testLiteralMeets] @@ -2080,3 +2080,125 @@ def func(x: Literal[1], y: Literal[2]) -> None: pass reveal_type(unify(func)) # E: Revealed type is '' [builtins fixtures/list.pyi] [out] + + +-- +-- Interactions with 'Final' +-- + +[case testLiteralFinalInferredAsLiteral] +from typing_extensions import Final + +var1: Final = 1 +var2: Final = "foo" +var3: Final = True +var4: Final = None + +class Foo: + classvar1: Final = 1 + classvar2: Final = "foo" + classvar3: Final = True + classvar4: Final = None + + def __init__(self) -> None: + self.instancevar1: Final = 1 + self.instancevar2: Final = "foo" + self.instancevar3: Final = True + self.instancevar4: Final = None + +reveal_type(var1) # E: Revealed type is 'Literal[1]' +reveal_type(var2) # E: Revealed type is 'Literal['foo']' +reveal_type(var3) # E: Revealed type is 'Literal[True]' +reveal_type(var4) # E: Revealed type is 'None' + +reveal_type(Foo.classvar1) # E: Revealed type is 'Literal[1]' +reveal_type(Foo.classvar2) # E: Revealed type is 'Literal['foo']' +reveal_type(Foo.classvar3) # E: Revealed type is 'Literal[True]' +reveal_type(Foo.classvar4) # E: Revealed type is 'None' + +f = Foo() +reveal_type(f.instancevar1) # E: Revealed type is 'Literal[1]' +reveal_type(f.instancevar2) # E: Revealed type is 'Literal['foo']' +reveal_type(f.instancevar3) # E: Revealed type is 'Literal[True]' +reveal_type(f.instancevar4) # E: Revealed type is 'None' +[builtins fixtures/primitives.pyi] +[out] + +[case testLiteralFinalDirectTypesSupercededInferredLiteral] +from typing_extensions import Final + +var1: Final[int] = 1 +var2: Final[str] = "foo" +var3: Final[bool] = True +var4: Final[None] = None + +class Foo: + classvar1: Final[int] = 1 + classvar2: Final[str] = "foo" + classvar3: Final[bool] = True + classvar4: Final[None] = None + + def __init__(self) -> None: + self.instancevar1: Final[int] = 1 + self.instancevar2: Final[str] = "foo" + self.instancevar3: Final[bool] = True + self.instancevar4: Final[None] = None + +reveal_type(var1) # E: Revealed type is 'builtins.int' +reveal_type(var2) # E: Revealed type is 'builtins.str' +reveal_type(var3) # E: Revealed type is 'builtins.bool' +reveal_type(var4) # E: Revealed type is 'None' + +reveal_type(Foo.classvar1) # E: Revealed type is 'builtins.int' +reveal_type(Foo.classvar2) # E: Revealed type is 'builtins.str' +reveal_type(Foo.classvar3) # E: Revealed type is 'builtins.bool' +reveal_type(Foo.classvar4) # E: Revealed type is 'None' + +f = Foo() +reveal_type(f.instancevar1) # E: Revealed type is 'builtins.int' +reveal_type(f.instancevar2) # E: Revealed type is 'builtins.str' +reveal_type(f.instancevar3) # E: Revealed type is 'builtins.bool' +reveal_type(f.instancevar4) # E: Revealed type is 'None' +[builtins fixtures/primitives.pyi] +[out] + +[case testLiteralFinalMismatchCausesError] +from typing_extensions import Final, Literal + +var1: Final[Literal[4]] = 1 # E: Incompatible types in assignment (expression has type "Literal[1]", variable has type "Literal[4]") +var2: Final[Literal['bad']] = "foo" # E: Incompatible types in assignment (expression has type "Literal['foo']", variable has type "Literal['bad']") +var3: Final[Literal[False]] = True # E: Incompatible types in assignment (expression has type "Literal[True]", variable has type "Literal[False]") + +class Foo: + classvar1: Final[Literal[4]] = 1 # E: Incompatible types in assignment (expression has type "Literal[1]", variable has type "Literal[4]") + classvar2: Final[Literal['bad']] = "foo" # E: Incompatible types in assignment (expression has type "Literal['foo']", variable has type "Literal['bad']") + classvar3: Final[Literal[False]] = True # E: Incompatible types in assignment (expression has type "Literal[True]", variable has type "Literal[False]") + + def __init__(self) -> None: + self.instancevar1: Final[Literal[4]] = 1 # E: Incompatible types in assignment (expression has type "Literal[1]", variable has type "Literal[4]") + self.instancevar2: Final[Literal['bad']] = "foo" # E: Incompatible types in assignment (expression has type "Literal['foo']", variable has type "Literal['bad']") + self.instancevar3: Final[Literal[False]] = True # E: Incompatible types in assignment (expression has type "Literal[True]", variable has type "Literal[False]") + +# TODO: Fix the order in which these error messages are shown to be more consistent. +var1 = 10 # E: Incompatible types in assignment (expression has type "Literal[10]", variable has type "Literal[4]") \ + # E: Cannot assign to final name "var1" + +Foo.classvar1 = 10 # E: Cannot assign to final attribute "classvar1" \ + # E: Incompatible types in assignment (expression has type "Literal[10]", variable has type "Literal[4]") + +Foo().instancevar1 = 10 # E: Cannot assign to final attribute "instancevar1" \ + # E: Incompatible types in assignment (expression has type "Literal[10]", variable has type "Literal[4]") +[builtins fixtures/primitives.pyi] +[out] + +[case testLiteralFinalGoesOnlyOneLevelDown] +from typing import Tuple +from typing_extensions import Final + +a: Final = 1 +b: Final = (1, 2) + +reveal_type(a) # E: Revealed type is 'Literal[1]' +reveal_type(b) # E: Revealed type is 'Tuple[builtins.int, builtins.int]' +[builtins fixtures/tuple.pyi] +[out] diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index ef3718522ce3..b07f77111ac2 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -948,39 +948,57 @@ __main__.A.g [case testFinalFlagsTriggerVar] from typing import Final +w: Final = 1 x: Final = 1 y: Final[int] = 1 +z: Final[int] = 1 same: Final = 0 class C: + w: Final = 1 x: Final = 1 y: Final[int] = 1 + z: Final[int] = 1 same: Final = 0 def __init__(self) -> None: - self.z: Final = 1 - self.t: Final[int] = 1 + self.wi: Final = 1 + self.xi: Final = 1 + self.yi: Final[int] = 1 + self.zi: Final[int] = 1 self.also_same: Final[int] = 0 [file next.py] from typing import Final -x = 1 +w = 1 +x: Final[int] = 1 y: int = 1 +z: Final = 1 same: Final = 0 class C: - x = 1 + w = 1 + x: Final[int] = 1 y: int = 1 + z: Final = 1 same: Final = 0 def __init__(self) -> None: - self.z = 1 - self.t: int = 1 - self.also_same: Final = 0 + self.wi = 1 + self.xi: Final[int] = 1 + self.yi: int = 1 + self.zi: Final = 1 + self.also_same: Final[int] = 0 [out] -__main__.C.t +__main__.C.w +__main__.C.wi __main__.C.x +__main__.C.xi __main__.C.y +__main__.C.yi __main__.C.z +__main__.C.zi +__main__.w __main__.x __main__.y +__main__.z [case testFinalFlagsTriggerMethod] from typing import final diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 8da12a200df9..13d857ccd177 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -8007,11 +8007,11 @@ class C: [file mod.py.2] from typing import Final -x: Final = 1 +x: Final[int] = 1 class C: - y: Final = 1 + y: Final[int] = 1 def __init__(self) -> None: - self.z: Final = 1 + self.z: Final[int] = 1 [out] == main:5: error: Cannot assign to final name "x" @@ -8042,9 +8042,9 @@ class C: from typing import Final class C: - x: Final = 1 + x: Final[int] = 1 def __init__(self) -> None: - self.y: Final = 1 + self.y: Final[int] = 1 [out] == main:4: error: Cannot assign to final name "x" From 7335991d0767e8f3f4b640a58181832bab42c8ae Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 3 Jan 2019 17:08:25 -0800 Subject: [PATCH 2/6] Switch to making final variables context-sensitive This commit modifies this PR to make selecting the type of final variables context-sensitive. Now, when we do: x: Final = 1 ...the variable `x` is normally inferred to be of type `int`. However, if that variable is used in a context which expects `Literal`, we infer the literal type. This commit also removes some of the hacks to mypy and the tests that the first iteration added. --- mypy/checker.py | 7 +- mypy/checkexpr.py | 73 ++++---- mypy/checkmember.py | 9 +- mypy/defaults.py | 4 +- mypy/erasetype.py | 2 +- mypy/expandtype.py | 6 +- mypy/reachability.py | 2 +- mypy/sametypes.py | 3 +- mypy/semanal.py | 21 ++- mypy/server/astdiff.py | 3 +- mypy/server/astmerge.py | 2 + mypy/server/deps.py | 2 + mypy/stubgen.py | 2 +- mypy/subtypes.py | 2 +- mypy/type_visitor.py | 2 +- mypy/typeanal.py | 5 +- mypy/types.py | 44 ++++- test-data/unit/check-final.test | 81 ++++----- test-data/unit/check-incremental.test | 6 +- test-data/unit/check-literal.test | 234 +++++++++++++++++++++++--- test-data/unit/fine-grained.test | 10 +- 21 files changed, 378 insertions(+), 142 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8fc000127635..8e831f659786 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -174,7 +174,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 # type: int + last_pass = DEFAULT_LAST_PASS # Have we deferred the current function? If yes, don't infer additional # types during this pass within the function. current_node_deferred = False @@ -1809,7 +1809,10 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type self.check_indexed_assignment(index_lvalue, rvalue, lvalue) if inferred: - rvalue_type = self.expr_checker.accept(rvalue, infer_literal=inferred.is_final) + rvalue_type = self.expr_checker.accept( + rvalue, + in_final_declaration=inferred.is_final, + ) self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue) def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[Type], diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index a450d3a8a062..e69fcc696815 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -18,7 +18,7 @@ from mypy.types import ( Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef, TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType, - PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny, LiteralType, + PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny, LiteralType, LiteralValue, true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike, StarType, is_optional, remove_optional, is_generic_instance ) @@ -139,7 +139,7 @@ def __init__(self, self.msg = msg self.plugin = plugin self.type_context = [None] - self.infer_literal = False + self.in_final_declaration = 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 @@ -211,10 +211,12 @@ 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 self.is_literal_context() and var.name() in {'True', 'False'}: - return LiteralType(var.name() == 'True', self.named_type('builtins.bool')) - else: - return var.type + if isinstance(var.type, Instance): + if self._is_literal_context() and var.type.final_value is not None: + return var.type.final_value + if var.name() in {'True', 'False'}: + return self._handle_literal_expr(var.name() == 'True', 'builtins.bool') + return var.type else: if not var.is_ready and self.chk.in_checked_function(): self.chk.handle_cannot_determine_type(var.name(), context) @@ -693,7 +695,8 @@ def check_call(self, elif isinstance(callee, Instance): call_function = analyze_member_access('__call__', callee, context, False, False, False, self.msg, - original_type=callee, chk=self.chk) + original_type=callee, chk=self.chk, + in_literal_context=self._is_literal_context()) return self.check_call(call_function, args, arg_kinds, context, arg_names, callable_node, arg_messages) elif isinstance(callee, TypeVarType): @@ -1757,7 +1760,8 @@ def analyze_ordinary_member_access(self, e: MemberExpr, original_type = self.accept(e.expr) member_type = analyze_member_access( e.name, original_type, e, is_lvalue, False, False, - self.msg, original_type=original_type, chk=self.chk) + self.msg, original_type=original_type, chk=self.chk, + in_literal_context=self._is_literal_context()) return member_type def analyze_external_member_access(self, member: str, base_type: Type, @@ -1767,35 +1771,36 @@ def analyze_external_member_access(self, member: str, base_type: Type, """ # TODO remove; no private definitions in mypy return analyze_member_access(member, base_type, context, False, False, False, - self.msg, original_type=base_type, chk=self.chk) + self.msg, original_type=base_type, chk=self.chk, + in_literal_context=self._is_literal_context()) + + def _is_literal_context(self) -> bool: + return is_literal_type_like(self.type_context[-1]) + + def _handle_literal_expr(self, value: LiteralValue, fallback_name: str) -> Type: + typ = self.named_type(fallback_name) + if self._is_literal_context(): + return LiteralType(value=value, fallback=typ) + elif self.in_final_declaration: + return typ.copy_with_final_value(value) + else: + return typ def visit_int_expr(self, e: IntExpr) -> Type: """Type check an integer literal (trivial).""" - typ = self.named_type('builtins.int') - if self.is_literal_context(): - return LiteralType(value=e.value, fallback=typ) - return typ + return self._handle_literal_expr(e.value, 'builtins.int') def visit_str_expr(self, e: StrExpr) -> Type: """Type check a string literal (trivial).""" - typ = self.named_type('builtins.str') - if self.is_literal_context(): - return LiteralType(value=e.value, fallback=typ) - return typ + return self._handle_literal_expr(e.value, 'builtins.str') def visit_bytes_expr(self, e: BytesExpr) -> Type: """Type check a bytes literal (trivial).""" - typ = self.named_type('builtins.bytes') - if is_literal_type_like(self.type_context[-1]): - return LiteralType(value=e.value, fallback=typ) - return typ + return self._handle_literal_expr(e.value, 'builtins.bytes') def visit_unicode_expr(self, e: UnicodeExpr) -> Type: """Type check a unicode literal (trivial).""" - typ = self.named_type('builtins.unicode') - if is_literal_type_like(self.type_context[-1]): - return LiteralType(value=e.value, fallback=typ) - return typ + return self._handle_literal_expr(e.value, 'builtins.unicode') def visit_float_expr(self, e: FloatExpr) -> Type: """Type check a float literal (trivial).""" @@ -1932,7 +1937,8 @@ def check_method_call_by_name(self, """ local_errors = local_errors or self.msg method_type = analyze_member_access(method, base_type, context, False, False, True, - local_errors, original_type=base_type, chk=self.chk) + local_errors, original_type=base_type, chk=self.chk, + in_literal_context=self._is_literal_context()) return self.check_method_call( method, base_type, method_type, args, arg_kinds, context, local_errors) @@ -1996,6 +2002,7 @@ def lookup_operator(op_name: str, base_type: Type) -> Optional[Type]: context=context, msg=local_errors, chk=self.chk, + in_literal_context=self._is_literal_context() ) if local_errors.is_errors(): return None @@ -2946,7 +2953,8 @@ def analyze_super(self, e: SuperExpr, is_lvalue: bool) -> Type: override_info=base, context=e, msg=self.msg, - chk=self.chk) + chk=self.chk, + in_literal_context=self._is_literal_context()) assert False, 'unreachable' else: # Invalid super. This has been reported by the semantic analyzer. @@ -3113,7 +3121,7 @@ def accept(self, type_context: Optional[Type] = None, allow_none_return: bool = False, always_allow_any: bool = False, - infer_literal: bool = False, + in_final_declaration: 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 @@ -3121,8 +3129,8 @@ def accept(self, """ if node in self.type_overrides: return self.type_overrides[node] - old_infer_literal = self.infer_literal - self.infer_literal = infer_literal + old_in_final_declaration = self.in_final_declaration + self.in_final_declaration = in_final_declaration self.type_context.append(type_context) try: if allow_none_return and isinstance(node, CallExpr): @@ -3135,7 +3143,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 + self.in_final_declaration = old_in_final_declaration assert typ is not None self.chk.store_type(node, typ) @@ -3381,9 +3389,6 @@ 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""" diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 2829777eb848..b8826695f9f5 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -71,7 +71,8 @@ def analyze_member_access(name: str, msg: MessageBuilder, *, original_type: Type, chk: 'mypy.checker.TypeChecker', - override_info: Optional[TypeInfo] = None) -> Type: + override_info: Optional[TypeInfo] = None, + in_literal_context: bool = False) -> Type: """Return the type of attribute 'name' of 'typ'. The actual implementation is in '_analyze_member_access' and this docstring @@ -96,7 +97,11 @@ def analyze_member_access(name: str, context, msg, chk=chk) - return _analyze_member_access(name, typ, mx, override_info) + result = _analyze_member_access(name, typ, mx, override_info) + if in_literal_context and isinstance(result, Instance) and result.final_value is not None: + return result.final_value + else: + return result def _analyze_member_access(name: str, diff --git a/mypy/defaults.py b/mypy/defaults.py index af1f68e44627..3169ec540e8d 100644 --- a/mypy/defaults.py +++ b/mypy/defaults.py @@ -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[str] -CONFIG_FILE = 'mypy.ini' # type: Final[str] +CACHE_DIR = '.mypy_cache' # type: Final +CONFIG_FILE = 'mypy.ini' # type: Final 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 diff --git a/mypy/erasetype.py b/mypy/erasetype.py index fa3e4abf79b6..2f05892894c5 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -51,7 +51,7 @@ def visit_deleted_type(self, t: DeletedType) -> Type: return t def visit_instance(self, t: Instance) -> Type: - return Instance(t.type, [AnyType(TypeOfAny.special_form)] * len(t.args), t.line) + return t.copy_modified(args=[AnyType(TypeOfAny.special_form)] * len(t.args)) def visit_type_var(self, t: TypeVarType) -> Type: return AnyType(TypeOfAny.special_form) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index bf51dc2aa9a9..afe51a636b25 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -80,16 +80,14 @@ def visit_erased_type(self, t: ErasedType) -> Type: raise RuntimeError() def visit_instance(self, t: Instance) -> Type: - args = self.expand_types(t.args) - return Instance(t.type, args, t.line, t.column) + return t.copy_modified(args=self.expand_types(t.args)) def visit_type_var(self, t: TypeVarType) -> Type: repl = self.variables.get(t.id, t) if isinstance(repl, Instance): inst = repl # Return copy of instance with type erasure flag on. - return Instance(inst.type, inst.args, line=inst.line, - column=inst.column, erased=True) + return inst.copy_modified(erased=True) else: return repl diff --git a/mypy/reachability.py b/mypy/reachability.py index f871680f3fb8..4585c08c861a 100644 --- a/mypy/reachability.py +++ b/mypy/reachability.py @@ -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 # type: int + result = TRUTH_VALUE_UNKNOWN if isinstance(expr, NameExpr): name = expr.name elif isinstance(expr, MemberExpr): diff --git a/mypy/sametypes.py b/mypy/sametypes.py index 1cb826a5ec4f..1fec6d572fd4 100644 --- a/mypy/sametypes.py +++ b/mypy/sametypes.py @@ -77,7 +77,8 @@ def visit_deleted_type(self, left: DeletedType) -> bool: def visit_instance(self, left: Instance) -> bool: return (isinstance(self.right, Instance) and left.type == self.right.type and - is_same_types(left.args, self.right.args)) + is_same_types(left.args, self.right.args) and + left.final_value == self.right.final_value) def visit_type_var(self, left: TypeVarType) -> bool: return (isinstance(self.right, TypeVarType) and diff --git a/mypy/semanal.py b/mypy/semanal.py index 1c358ee8858b..4266ed5606bd 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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, LiteralType, + CallableType, Overloaded, Instance, Type, AnyType, TypeTranslator, TypeOfAny, TypeType, NoneTyp, ) from mypy.nodes import implicit_module_attrs @@ -1908,22 +1908,29 @@ def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Opt # inside type variables with value restrictions (like # AnyStr). return None + if isinstance(rvalue, FloatExpr): + return self.named_type_or_none('builtins.float') + if isinstance(rvalue, IntExpr): typ = self.named_type_or_none('builtins.int') if typ and is_final: - return LiteralType(rvalue.value, typ, rvalue.line, rvalue.column) + return typ.copy_with_final_value(rvalue.value) return typ - if isinstance(rvalue, FloatExpr): - return self.named_type_or_none('builtins.float') if isinstance(rvalue, StrExpr): typ = self.named_type_or_none('builtins.str') if typ and is_final: - return LiteralType(rvalue.value, typ, rvalue.line, rvalue.column) + return typ.copy_with_final_value(rvalue.value) return typ if isinstance(rvalue, BytesExpr): - return self.named_type_or_none('builtins.bytes') + typ = self.named_type_or_none('builtins.bytes') + if typ and is_final: + return typ.copy_with_final_value(rvalue.value) + return typ if isinstance(rvalue, UnicodeExpr): - return self.named_type_or_none('builtins.unicode') + typ = self.named_type_or_none('builtins.unicode') + if typ and is_final: + return typ.copy_with_final_value(rvalue.value) + return typ return None diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 8697358a4205..53dd2489e985 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -284,7 +284,8 @@ def visit_deleted_type(self, typ: DeletedType) -> SnapshotItem: def visit_instance(self, typ: Instance) -> SnapshotItem: return ('Instance', typ.type.fullname(), - snapshot_types(typ.args)) + snapshot_types(typ.args), + None if typ.final_value is None else snapshot_type(typ.final_value)) def visit_type_var(self, typ: TypeVarType) -> SnapshotItem: return ('TypeVar', diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index edfdc076e7d7..97cf2fdf50cc 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -337,6 +337,8 @@ def visit_instance(self, typ: Instance) -> None: typ.type = self.fixup(typ.type) for arg in typ.args: arg.accept(self) + if typ.final_value: + typ.final_value.accept(self) def visit_any(self, typ: AnyType) -> None: pass diff --git a/mypy/server/deps.py b/mypy/server/deps.py index ae2075d72fe7..43fb464fda5e 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -882,6 +882,8 @@ def visit_instance(self, typ: Instance) -> List[str]: triggers = [trigger] for arg in typ.args: triggers.extend(self.get_type_triggers(arg)) + if typ.final_value: + triggers.extend(self.get_type_triggers(typ.final_value)) return triggers def visit_any(self, typ: AnyType) -> List[str]: diff --git a/mypy/stubgen.py b/mypy/stubgen.py index a79dd6a8ff00..a97ded77cf6b 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -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 # type: str + self._state = EMPTY self._toplevel_names = [] # type: List[str] self._pyversion = pyversion self._include_private = include_private diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 300295db74a6..227b3ba10d27 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -589,7 +589,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} # type: Set[int] + flags = {IS_SETTABLE} if v.is_classvar: flags.add(IS_CLASSVAR) return flags diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 64ae75e174ee..0b8c5ca10d62 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -159,7 +159,7 @@ def visit_deleted_type(self, t: DeletedType) -> Type: return t def visit_instance(self, t: Instance) -> Type: - return Instance(t.type, self.translate_types(t.args), t.line, t.column) + return t.copy_modified(args=self.translate_types(t.args)) def visit_type_var(self, t: TypeVarType) -> Type: return t diff --git a/mypy/typeanal.py b/mypy/typeanal.py index a870273ebcfd..bfeffb8aa8df 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -678,6 +678,9 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[L elif isinstance(arg, (NoneTyp, LiteralType)): # Types that we can just add directly to the literal/potential union of literals. return [arg] + elif isinstance(arg, Instance) and arg.final_value is not None: + # Types generated from declarations like "var: Final = 4". + return [arg.final_value] elif isinstance(arg, UnionType): out = [] for union_arg in arg.items: @@ -1073,7 +1076,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: int + type_of_any = TypeOfAny.from_omitted_generics else: type_of_any = TypeOfAny.special_form any_type = AnyType(type_of_any, line=newline, column=newcolumn) diff --git a/mypy/types.py b/mypy/types.py index d21eabdd77f5..0d65afb8f5fb 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -561,37 +561,43 @@ class Instance(Type): The list of type variables may be empty. """ - __slots__ = ('type', 'args', 'erased', 'invalid', 'type_ref') + __slots__ = ('type', 'args', 'erased', 'invalid', 'type_ref', 'final_value') def __init__(self, typ: mypy.nodes.TypeInfo, args: List[Type], - line: int = -1, column: int = -1, erased: bool = False) -> None: + line: int = -1, column: int = -1, erased: bool = False, + final_value: Optional['LiteralType'] = None) -> None: super().__init__(line, column) self.type = typ self.args = args self.erased = erased # True if result of type variable substitution self.invalid = False # True if recovered after incorrect number of type arguments error self.type_ref = None # type: Optional[str] + self.final_value = final_value def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_instance(self) def __hash__(self) -> int: - return hash((self.type, tuple(self.args))) + return hash((self.type, tuple(self.args), self.final_value)) def __eq__(self, other: object) -> bool: if not isinstance(other, Instance): return NotImplemented - return self.type == other.type and self.args == other.args + return (self.type == other.type + and self.args == other.args + and self.final_value == other.final_value) def serialize(self) -> Union[JsonDict, str]: assert self.type is not None type_ref = self.type.fullname() - if not self.args: + if not self.args and not self.final_value: return type_ref data = {'.class': 'Instance', } # type: JsonDict data['type_ref'] = type_ref data['args'] = [arg.serialize() for arg in self.args] + if self.final_value is not None: + data['final_value'] = self.final_value.serialize() return data @classmethod @@ -608,10 +614,32 @@ def deserialize(cls, data: Union[JsonDict, str]) -> 'Instance': args = [deserialize_type(arg) for arg in args_list] inst = Instance(NOT_READY, args) inst.type_ref = data['type_ref'] # Will be fixed up by fixup.py later. + if 'final_value' in data: + inst.final_value = LiteralType.deserialize(data['final_value']) return inst - def copy_modified(self, *, args: List[Type]) -> 'Instance': - return Instance(self.type, args, self.line, self.column, self.erased) + def copy_modified(self, *, + args: Bogus[List[Type]] = _dummy, + erased: Bogus[bool] = _dummy) -> 'Instance': + return Instance( + self.type, + args if args is not _dummy else self.args, + self.line, + self.column, + erased if erased is not _dummy else self.erased, + self.final_value, + ) + + def copy_with_final_value(self, value: LiteralValue) -> 'Instance': + # Note: the fallback for this LiteralType is the *original* type, not the newly + # generated one. This helps prevent infinite loops when we traverse the type tree. + final_value = LiteralType( + value=value, + fallback=self, + line=self.line, + column=self.column, + ) + return Instance(self.type, self.args, self.line, self.column, self.erased, final_value) def has_readable_member(self, name: str) -> bool: return self.type.has_readable_member(name) @@ -2042,7 +2070,7 @@ def get_typ_args(tp: Type) -> List[Type]: def set_typ_args(tp: Type, new_args: List[Type], line: int = -1, column: int = -1) -> Type: """Return a copy of a parametrizable Type with arguments set to new_args.""" if isinstance(tp, Instance): - return Instance(tp.type, new_args, line, column) + return tp.copy_modified(args=new_args) if isinstance(tp, TupleType): return tp.copy_modified(items=new_args) if isinstance(tp, UnionType): diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index ab9054c51bdb..7f895b97d277 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -338,7 +338,7 @@ class C: [case testFinalReassignModuleVar] from typing import Final -x: Final[int] = 1 +x: Final = 1 x # Dummy reference to allow renaming once implemented x = 2 # E: Cannot assign to final name "x" def f() -> int: @@ -358,7 +358,7 @@ y: Final = 2 # E: Name 'y' already defined on line 17 \ # E: Cannot redefine an existing name as final y = 3 # No error here, first definition wins -z: Final[int] = 1 +z: Final = 1 z: Final = 2 # E: Name 'z' already defined on line 22 \ # E: Cannot redefine an existing name as final z = 3 # E: Cannot assign to final name "z" @@ -395,11 +395,11 @@ main:8: error: Cannot assign to final name "ID" from typing import Final def f() -> None: - nl: Final[int] = 0 + nl: Final = 0 x: Final = 1 x = 1 # E: Cannot assign to final name "x" - y: Final[int] = 1 + y: Final = 1 y: Final = 2 # E: Cannot redefine an existing name as final def nested() -> None: nonlocal nl @@ -418,7 +418,7 @@ x: Final[int] from typing import Final class C: - x: Final[int] = 1 + x: Final = 1 x = 2 # E: Cannot assign to final name "x" y = 1 @@ -430,9 +430,9 @@ from typing import Final class C: def __init__(self) -> None: - self.x: Final[int] = 1 + self.x: Final = 1 self.y = 1 - self.y: Final[int] = 2 # E: Cannot redefine an existing name as final + self.y: Final = 2 # E: Cannot redefine an existing name as final def meth(self) -> None: self.x = 2 # E: Cannot assign to final attribute "x" [out] @@ -441,9 +441,9 @@ class C: from typing import Final class C: - y: Final[int] = 1 + y: Final = 1 def __init__(self) -> None: - self.x: Final[int] = 1 + self.x: Final = 1 self.y = 2 # E: Cannot assign to final attribute "y" x = 2 # E: Cannot assign to final name "x" [out] @@ -452,9 +452,9 @@ class C: from typing import Final class C: - x: Final[int] = 1 + x: Final = 1 def __init__(self) -> None: - self.y: Final[int] = 1 + self.y: Final = 1 def meth(self) -> None: self.x = 2 # E: Cannot assign to final attribute "x" self.y = 2 # E: Cannot assign to final attribute "y" @@ -472,9 +472,9 @@ class C: from typing import Final class C: - x: Final[int] = 1 + x: Final = 1 def __init__(self) -> None: - self.y: Final[int] = 1 + self.y: Final = 1 class D(C): pass @@ -494,14 +494,9 @@ class C: class D(C): pass -C().x = 2 # E: Cannot assign to final attribute "x" \ - # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") - -D().x = 2 # E: Cannot assign to final attribute "x" \ - # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") - -D().y = 2 # E: Cannot assign to final attribute "y" \ - # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") +C().x = 2 # E: Cannot assign to final attribute "x" +D().x = 2 # E: Cannot assign to final attribute "x" +D().y = 2 # E: Cannot assign to final attribute "y" [out] [case testFinalWorksWithComplexTargets] @@ -551,9 +546,9 @@ class A: def y(self) -> int: ... class B(A): - x: Final[int] = 1 + x: Final = 1 def __init__(self) -> None: - self.y: Final[int] = 1 + self.y: Final = 1 class C(B): x: int = 2 # E: Cannot assign to final name "x" y: int = 2 # E: Cannot assign to final name "y" @@ -575,9 +570,9 @@ class A: @property def y(self) -> int: ... class B(A): - x: Final[int] = 1 + x: Final = 1 def __init__(self) -> None: - self.y: Final[int] = 1 + self.y: Final = 1 class C(B): x: Final = 2 # E: Cannot override final attribute "x" (previously declared in base class "B") y: Final = 2 # E: Cannot override final attribute "y" (previously declared in base class "B") @@ -598,15 +593,11 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x = 2 # E: Cannot assign to final attribute "x" \ - # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") - self.y = 2 # E: Cannot assign to final attribute "y" \ - # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") + self.x = 2 # E: Cannot assign to final attribute "x" + self.y = 2 # E: Cannot assign to final attribute "y" def meth(self) -> None: - self.x = 3 # E: Cannot assign to final attribute "x" \ - # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1]") - self.y = 3 # E: Cannot assign to final attribute "y" \ - # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1]") + self.x = 3 # E: Cannot assign to final attribute "x" + self.y = 3 # E: Cannot assign to final attribute "y" [builtins fixtures/property.pyi] [out] @@ -624,10 +615,8 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x: Final = 2 # E: Cannot override final attribute "x" (previously declared in base class "B") \ - # E: Incompatible types in assignment (expression has type "Literal[2]", base class "B" defined the type as "Literal[1]") - self.y: Final = 2 # E: Cannot override final attribute "y" (previously declared in base class "B") \ - # E: Incompatible types in assignment (expression has type "Literal[2]", base class "B" defined the type as "Literal[1]") + self.x: Final = 2 # E: Cannot override final attribute "x" (previously declared in base class "B") + self.y: Final = 2 # E: Cannot override final attribute "y" (previously declared in base class "B") [builtins fixtures/property.pyi] [out] @@ -645,10 +634,8 @@ class B(A): self.y: Final = 1 class C(B): def meth(self) -> None: - self.x: int = 2 # E: Cannot assign to final attribute "x" \ - # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "Literal[1]") - self.y: int = 2 # E: Cannot assign to final attribute "y" \ - # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "Literal[1]") + self.x: int = 2 # E: Cannot assign to final attribute "x" + self.y: int = 2 # E: Cannot assign to final attribute "y" self.x = 3 # E: Cannot assign to final attribute "x" self.y = 3 # E: Cannot assign to final attribute "y" @@ -693,7 +680,7 @@ C().x = 4 # E: Cannot assign to final attribute "x" from typing import Final class A: - x: Final[int] = 1 + x: Final = 1 class B: def __init__(self) -> None: self.x = 2 @@ -975,11 +962,11 @@ class D(B): [case testFinalCanUseTypingExtensions] from typing_extensions import final, Final -x: Final[int] = 1 +x: Final = 1 x = 2 # E: Cannot assign to final name "x" class S: - x: Final[int] = 1 + x: Final = 1 S.x = 2 # E: Cannot assign to final attribute "x" class B: @@ -997,13 +984,11 @@ class E(F): ... # E: Cannot inherit from final class "F" from typing_extensions import final as f, Final as F x: F = 1 -x = 2 # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") \ - # E: Cannot assign to final name "x" +x = 2 # E: Cannot assign to final name "x" class S: x: F = 1 -S.x = 2 # E: Cannot assign to final attribute "x" \ - # E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]") +S.x = 2 # E: Cannot assign to final attribute "x" class B: @f diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 7233c320b4fb..738f0b6242aa 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4239,11 +4239,11 @@ class C: [file mod.py.2] from typing import Final -x: Final[int] = 1 +x: Final = 1 class C: - y: Final[int] = 1 + y: Final = 1 def __init__(self) -> None: - self.z: Final[int] = 1 + self.z: Final = 1 [out] [out2] main:5: error: Cannot assign to final name "x" diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 8e7aa79a8f71..d00d7c4cbb67 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2087,7 +2087,7 @@ reveal_type(unify(func)) # E: Revealed type is '' -- [case testLiteralFinalInferredAsLiteral] -from typing_extensions import Final +from typing_extensions import Final, Literal var1: Final = 1 var2: Final = "foo" @@ -2106,26 +2106,43 @@ class Foo: self.instancevar3: Final = True self.instancevar4: Final = None -reveal_type(var1) # E: Revealed type is 'Literal[1]' -reveal_type(var2) # E: Revealed type is 'Literal['foo']' -reveal_type(var3) # E: Revealed type is 'Literal[True]' -reveal_type(var4) # E: Revealed type is 'None' - -reveal_type(Foo.classvar1) # E: Revealed type is 'Literal[1]' -reveal_type(Foo.classvar2) # E: Revealed type is 'Literal['foo']' -reveal_type(Foo.classvar3) # E: Revealed type is 'Literal[True]' -reveal_type(Foo.classvar4) # E: Revealed type is 'None' +def force1(x: Literal[1]) -> None: pass +def force2(x: Literal["foo"]) -> None: pass +def force3(x: Literal[True]) -> None: pass +def force4(x: Literal[None]) -> None: pass + +reveal_type(var1) # E: Revealed type is 'builtins.int' +reveal_type(var2) # E: Revealed type is 'builtins.str' +reveal_type(var3) # E: Revealed type is 'builtins.bool' +reveal_type(var4) # E: Revealed type is 'None' +force1(reveal_type(var1)) # E: Revealed type is 'Literal[1]' +force2(reveal_type(var2)) # E: Revealed type is 'Literal['foo']' +force3(reveal_type(var3)) # E: Revealed type is 'Literal[True]' +force4(reveal_type(var4)) # E: Revealed type is 'None' + +reveal_type(Foo.classvar1) # E: Revealed type is 'builtins.int' +reveal_type(Foo.classvar2) # E: Revealed type is 'builtins.str' +reveal_type(Foo.classvar3) # E: Revealed type is 'builtins.bool' +reveal_type(Foo.classvar4) # E: Revealed type is 'None' +force1(reveal_type(Foo.classvar1)) # E: Revealed type is 'Literal[1]' +force2(reveal_type(Foo.classvar2)) # E: Revealed type is 'Literal['foo']' +force3(reveal_type(Foo.classvar3)) # E: Revealed type is 'Literal[True]' +force4(reveal_type(Foo.classvar4)) # E: Revealed type is 'None' f = Foo() -reveal_type(f.instancevar1) # E: Revealed type is 'Literal[1]' -reveal_type(f.instancevar2) # E: Revealed type is 'Literal['foo']' -reveal_type(f.instancevar3) # E: Revealed type is 'Literal[True]' -reveal_type(f.instancevar4) # E: Revealed type is 'None' +reveal_type(f.instancevar1) # E: Revealed type is 'builtins.int' +reveal_type(f.instancevar2) # E: Revealed type is 'builtins.str' +reveal_type(f.instancevar3) # E: Revealed type is 'builtins.bool' +reveal_type(f.instancevar4) # E: Revealed type is 'None' +force1(reveal_type(f.instancevar1)) # E: Revealed type is 'Literal[1]' +force2(reveal_type(f.instancevar2)) # E: Revealed type is 'Literal['foo']' +force3(reveal_type(f.instancevar3)) # E: Revealed type is 'Literal[True]' +force4(reveal_type(f.instancevar4)) # E: Revealed type is 'None' [builtins fixtures/primitives.pyi] [out] -[case testLiteralFinalDirectTypesSupercededInferredLiteral] -from typing_extensions import Final +[case testLiteralFinalDirectInstanceTypesSupercedeInferredLiteral] +from typing_extensions import Final, Literal var1: Final[int] = 1 var2: Final[str] = "foo" @@ -2144,21 +2161,93 @@ class Foo: self.instancevar3: Final[bool] = True self.instancevar4: Final[None] = None +def force1(x: Literal[1]) -> None: pass +def force2(x: Literal["foo"]) -> None: pass +def force3(x: Literal[True]) -> None: pass +def force4(x: Literal[None]) -> None: pass + reveal_type(var1) # E: Revealed type is 'builtins.int' reveal_type(var2) # E: Revealed type is 'builtins.str' reveal_type(var3) # E: Revealed type is 'builtins.bool' reveal_type(var4) # E: Revealed type is 'None' +force1(var1) # E: Argument 1 to "force1" has incompatible type "int"; expected "Literal[1]" +force2(var2) # E: Argument 1 to "force2" has incompatible type "str"; expected "Literal['foo']" +force3(var3) # E: Argument 1 to "force3" has incompatible type "bool"; expected "Literal[True]" +force4(var4) reveal_type(Foo.classvar1) # E: Revealed type is 'builtins.int' reveal_type(Foo.classvar2) # E: Revealed type is 'builtins.str' reveal_type(Foo.classvar3) # E: Revealed type is 'builtins.bool' reveal_type(Foo.classvar4) # E: Revealed type is 'None' +force1(Foo.classvar1) # E: Argument 1 to "force1" has incompatible type "int"; expected "Literal[1]" +force2(Foo.classvar2) # E: Argument 1 to "force2" has incompatible type "str"; expected "Literal['foo']" +force3(Foo.classvar3) # E: Argument 1 to "force3" has incompatible type "bool"; expected "Literal[True]" +force4(Foo.classvar4) f = Foo() reveal_type(f.instancevar1) # E: Revealed type is 'builtins.int' reveal_type(f.instancevar2) # E: Revealed type is 'builtins.str' reveal_type(f.instancevar3) # E: Revealed type is 'builtins.bool' reveal_type(f.instancevar4) # E: Revealed type is 'None' +force1(f.instancevar1) # E: Argument 1 to "force1" has incompatible type "int"; expected "Literal[1]" +force2(f.instancevar2) # E: Argument 1 to "force2" has incompatible type "str"; expected "Literal['foo']" +force3(f.instancevar3) # E: Argument 1 to "force3" has incompatible type "bool"; expected "Literal[True]" +force4(f.instancevar4) +[builtins fixtures/primitives.pyi] +[out] + +[case testLiteralFinalDirectLiteralTypesForceLiteral] +from typing_extensions import Final, Literal + +var1: Final[Literal[1]] = 1 +var2: Final[Literal["foo"]] = "foo" +var3: Final[Literal[True]] = True +var4: Final[Literal[None]] = None + +class Foo: + classvar1: Final[Literal[1]] = 1 + classvar2: Final[Literal["foo"]] = "foo" + classvar3: Final[Literal[True]] = True + classvar4: Final[Literal[None]] = None + + def __init__(self) -> None: + self.instancevar1: Final[Literal[1]] = 1 + self.instancevar2: Final[Literal["foo"]] = "foo" + self.instancevar3: Final[Literal[True]] = True + self.instancevar4: Final[Literal[None]] = None + +def force1(x: Literal[1]) -> None: pass +def force2(x: Literal["foo"]) -> None: pass +def force3(x: Literal[True]) -> None: pass +def force4(x: Literal[None]) -> None: pass + +reveal_type(var1) # E: Revealed type is 'Literal[1]' +reveal_type(var2) # E: Revealed type is 'Literal['foo']' +reveal_type(var3) # E: Revealed type is 'Literal[True]' +reveal_type(var4) # E: Revealed type is 'None' +force1(reveal_type(var1)) # E: Revealed type is 'Literal[1]' +force2(reveal_type(var2)) # E: Revealed type is 'Literal['foo']' +force3(reveal_type(var3)) # E: Revealed type is 'Literal[True]' +force4(reveal_type(var4)) # E: Revealed type is 'None' + +reveal_type(Foo.classvar1) # E: Revealed type is 'Literal[1]' +reveal_type(Foo.classvar2) # E: Revealed type is 'Literal['foo']' +reveal_type(Foo.classvar3) # E: Revealed type is 'Literal[True]' +reveal_type(Foo.classvar4) # E: Revealed type is 'None' +force1(reveal_type(Foo.classvar1)) # E: Revealed type is 'Literal[1]' +force2(reveal_type(Foo.classvar2)) # E: Revealed type is 'Literal['foo']' +force3(reveal_type(Foo.classvar3)) # E: Revealed type is 'Literal[True]' +force4(reveal_type(Foo.classvar4)) # E: Revealed type is 'None' + +f = Foo() +reveal_type(f.instancevar1) # E: Revealed type is 'Literal[1]' +reveal_type(f.instancevar2) # E: Revealed type is 'Literal['foo']' +reveal_type(f.instancevar3) # E: Revealed type is 'Literal[True]' +reveal_type(f.instancevar4) # E: Revealed type is 'None' +force1(reveal_type(f.instancevar1)) # E: Revealed type is 'Literal[1]' +force2(reveal_type(f.instancevar2)) # E: Revealed type is 'Literal['foo']' +force3(reveal_type(f.instancevar3)) # E: Revealed type is 'Literal[True]' +force4(reveal_type(f.instancevar4)) # E: Revealed type is 'None' [builtins fixtures/primitives.pyi] [out] @@ -2193,12 +2282,119 @@ Foo().instancevar1 = 10 # E: Cannot assign to final attribute "instancevar1" \ [case testLiteralFinalGoesOnlyOneLevelDown] from typing import Tuple -from typing_extensions import Final +from typing_extensions import Final, Literal a: Final = 1 b: Final = (1, 2) -reveal_type(a) # E: Revealed type is 'Literal[1]' -reveal_type(b) # E: Revealed type is 'Tuple[builtins.int, builtins.int]' +def force1(x: Literal[1]) -> None: pass +def force2(x: Tuple[Literal[1], Literal[2]]) -> None: pass + +reveal_type(a) # E: Revealed type is 'builtins.int' +reveal_type(b) # E: Revealed type is 'Tuple[builtins.int, builtins.int]' + +force1(reveal_type(a)) # E: Revealed type is 'Literal[1]' +force2(reveal_type(b)) # E: Argument 1 to "force2" has incompatible type "Tuple[int, int]"; expected "Tuple[Literal[1], Literal[2]]" \ + # E: Revealed type is 'Tuple[builtins.int, builtins.int]' [builtins fixtures/tuple.pyi] [out] + +[case testLiteralFinalCollectionPropagation] +from typing import List +from typing_extensions import Final, Literal + +a: Final = 1 +implicit = [a] +explicit: List[Literal[1]] = [a] + +def force1(x: List[Literal[1]]) -> None: pass +def force2(x: Literal[1]) -> None: pass + +reveal_type(implicit) # E: Revealed type is 'builtins.list[builtins.int*]' +force1(reveal_type(implicit)) # E: Argument 1 to "force1" has incompatible type "List[int]"; expected "List[Literal[1]]" \ + # E: Revealed type is 'builtins.list[builtins.int*]' +force2(reveal_type(implicit[0])) # E: Argument 1 to "force2" has incompatible type "int"; expected "Literal[1]" \ + # E: Revealed type is 'builtins.int*' + +reveal_type(explicit) # E: Revealed type is 'builtins.list[Literal[1]]' +force1(reveal_type(explicit)) # E: Revealed type is 'builtins.list[Literal[1]]' +force2(reveal_type(explicit[0])) # E: Revealed type is 'Literal[1]' +[builtins fixtures/list.pyi] +[out] + +[case testLiteralFinalStringTypesPython3] +from typing_extensions import Final, Literal + +a: Final = u"foo" +b: Final = "foo" +c: Final = b"foo" + +def force_unicode(x: Literal[u"foo"]) -> None: pass +def force_bytes(x: Literal[b"foo"]) -> None: pass + +force_unicode(reveal_type(a)) # E: Revealed type is 'Literal['foo']' +force_unicode(reveal_type(b)) # E: Revealed type is 'Literal['foo']' +force_unicode(reveal_type(c)) # E: Argument 1 to "force_unicode" has incompatible type "Literal[b'foo']"; expected "Literal['foo']" \ + # E: Revealed type is 'Literal[b'foo']' + +force_bytes(reveal_type(a)) # E: Argument 1 to "force_bytes" has incompatible type "Literal['foo']"; expected "Literal[b'foo']" \ + # E: Revealed type is 'Literal['foo']' +force_bytes(reveal_type(b)) # E: Argument 1 to "force_bytes" has incompatible type "Literal['foo']"; expected "Literal[b'foo']" \ + # E: Revealed type is 'Literal['foo']' +force_bytes(reveal_type(c)) # E: Revealed type is 'Literal[b'foo']' +[out] + +[case testLiteralFinalStringTypesPython2UnicodeLiterals] +# flags: --python-version 2.7 +from __future__ import unicode_literals +from typing_extensions import Final, Literal + +a = u"foo" # type: Final +b = "foo" # type: Final +c = b"foo" # type: Final + +def force_unicode(x): + # type: (Literal[u"foo"]) -> None + pass +def force_bytes(x): + # type: (Literal[b"foo"]) -> None + pass + +force_unicode(reveal_type(a)) # E: Revealed type is 'Literal[u'foo']' +force_unicode(reveal_type(b)) # E: Revealed type is 'Literal[u'foo']' +force_unicode(reveal_type(c)) # E: Argument 1 to "force_unicode" has incompatible type "Literal['foo']"; expected "Literal[u'foo']" \ + # E: Revealed type is 'Literal['foo']' + +force_bytes(reveal_type(a)) # E: Argument 1 to "force_bytes" has incompatible type "Literal[u'foo']"; expected "Literal['foo']" \ + # E: Revealed type is 'Literal[u'foo']' +force_bytes(reveal_type(b)) # E: Argument 1 to "force_bytes" has incompatible type "Literal[u'foo']"; expected "Literal['foo']" \ + # E: Revealed type is 'Literal[u'foo']' +force_bytes(reveal_type(c)) # E: Revealed type is 'Literal['foo']' +[out] + +[case testLiteralFinalStringTypesPython2] +# flags: --python-version 2.7 +from typing_extensions import Final, Literal + +a = u"foo" # type: Final +b = "foo" # type: Final +c = b"foo" # type: Final + +def force_unicode(x): + # type: (Literal[u"foo"]) -> None + pass +def force_bytes(x): + # type: (Literal[b"foo"]) -> None + pass + +force_unicode(reveal_type(a)) # E: Revealed type is 'Literal[u'foo']' +force_unicode(reveal_type(b)) # E: Argument 1 to "force_unicode" has incompatible type "Literal['foo']"; expected "Literal[u'foo']" \ + # E: Revealed type is 'Literal['foo']' +force_unicode(reveal_type(c)) # E: Argument 1 to "force_unicode" has incompatible type "Literal['foo']"; expected "Literal[u'foo']" \ + # E: Revealed type is 'Literal['foo']' + +force_bytes(reveal_type(a)) # E: Argument 1 to "force_bytes" has incompatible type "Literal[u'foo']"; expected "Literal['foo']" \ + # E: Revealed type is 'Literal[u'foo']' +force_bytes(reveal_type(b)) # E: Revealed type is 'Literal['foo']' +force_bytes(reveal_type(c)) # E: Revealed type is 'Literal['foo']' +[out] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 13d857ccd177..8da12a200df9 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -8007,11 +8007,11 @@ class C: [file mod.py.2] from typing import Final -x: Final[int] = 1 +x: Final = 1 class C: - y: Final[int] = 1 + y: Final = 1 def __init__(self) -> None: - self.z: Final[int] = 1 + self.z: Final = 1 [out] == main:5: error: Cannot assign to final name "x" @@ -8042,9 +8042,9 @@ class C: from typing import Final class C: - x: Final[int] = 1 + x: Final = 1 def __init__(self) -> None: - self.y: Final[int] = 1 + self.y: Final = 1 [out] == main:4: error: Cannot assign to final name "x" From 85fab7d7922be9ee87ff32147b6914ca0a000bcb Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 3 Jan 2019 20:11:00 -0800 Subject: [PATCH 3/6] Revert overzealous Instance.copy_modified changes --- mypy/checkexpr.py | 7 +++- mypy/erasetype.py | 2 +- mypy/expandtype.py | 6 ++-- mypy/semanal.py | 31 ++++++++--------- mypy/type_visitor.py | 8 ++++- mypy/types.py | 23 +++++-------- test-data/unit/check-literal.test | 55 +++++++++++++++++++++++++++++++ 7 files changed, 97 insertions(+), 35 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e69fcc696815..1e080f38d002 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1782,7 +1782,12 @@ def _handle_literal_expr(self, value: LiteralValue, fallback_name: str) -> Type: if self._is_literal_context(): return LiteralType(value=value, fallback=typ) elif self.in_final_declaration: - return typ.copy_with_final_value(value) + return typ.copy_modified(final_value=LiteralType( + value=value, + fallback=typ, + line=typ.line, + column=typ.column, + )) else: return typ diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 2f05892894c5..fa3e4abf79b6 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -51,7 +51,7 @@ def visit_deleted_type(self, t: DeletedType) -> Type: return t def visit_instance(self, t: Instance) -> Type: - return t.copy_modified(args=[AnyType(TypeOfAny.special_form)] * len(t.args)) + return Instance(t.type, [AnyType(TypeOfAny.special_form)] * len(t.args), t.line) def visit_type_var(self, t: TypeVarType) -> Type: return AnyType(TypeOfAny.special_form) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index afe51a636b25..bf51dc2aa9a9 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -80,14 +80,16 @@ def visit_erased_type(self, t: ErasedType) -> Type: raise RuntimeError() def visit_instance(self, t: Instance) -> Type: - return t.copy_modified(args=self.expand_types(t.args)) + args = self.expand_types(t.args) + return Instance(t.type, args, t.line, t.column) def visit_type_var(self, t: TypeVarType) -> Type: repl = self.variables.get(t.id, t) if isinstance(repl, Instance): inst = repl # Return copy of instance with type erasure flag on. - return inst.copy_modified(erased=True) + return Instance(inst.type, inst.args, line=inst.line, + column=inst.column, erased=True) else: return repl diff --git a/mypy/semanal.py b/mypy/semanal.py index 4266ed5606bd..c1bb10b81186 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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, LiteralValue, TypeTranslator, TypeOfAny, TypeType, NoneTyp, ) from mypy.nodes import implicit_module_attrs @@ -1911,25 +1911,26 @@ def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Opt if isinstance(rvalue, FloatExpr): return self.named_type_or_none('builtins.float') + value = None # type: LiteralValue + type_name = None # type: Optional[str] if isinstance(rvalue, IntExpr): - typ = self.named_type_or_none('builtins.int') - if typ and is_final: - return typ.copy_with_final_value(rvalue.value) - return typ + value, type_name = rvalue.value, 'builtins.int' if isinstance(rvalue, StrExpr): - typ = self.named_type_or_none('builtins.str') - if typ and is_final: - return typ.copy_with_final_value(rvalue.value) - return typ + value, type_name = rvalue.value, 'builtins.str' if isinstance(rvalue, BytesExpr): - typ = self.named_type_or_none('builtins.bytes') - if typ and is_final: - return typ.copy_with_final_value(rvalue.value) - return typ + value, type_name = rvalue.value, 'builtins.bytes' if isinstance(rvalue, UnicodeExpr): - typ = self.named_type_or_none('builtins.unicode') + value, type_name = rvalue.value, 'builtins.unicode' + + if type_name is not None: + typ = self.named_type_or_none(type_name) if typ and is_final: - return typ.copy_with_final_value(rvalue.value) + return typ.copy_modified(final_value=LiteralType( + value=value, + fallback=typ, + line=typ.line, + column=typ.column, + )) return typ return None diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 0b8c5ca10d62..6ce54bca2ba9 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -159,7 +159,13 @@ def visit_deleted_type(self, t: DeletedType) -> Type: return t def visit_instance(self, t: Instance) -> Type: - return t.copy_modified(args=self.translate_types(t.args)) + return Instance( + typ=t.type, + args=self.translate_types(t.args), + line=t.line, + column=t.column, + final_value=None if t.final_value is None else t.final_value.accept(self), + ) def visit_type_var(self, t: TypeVarType) -> Type: return t diff --git a/mypy/types.py b/mypy/types.py index 0d65afb8f5fb..dd6706dab040 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -572,6 +572,10 @@ def __init__(self, typ: mypy.nodes.TypeInfo, args: List[Type], self.erased = erased # True if result of type variable substitution self.invalid = False # True if recovered after incorrect number of type arguments error self.type_ref = None # type: Optional[str] + + # The underlying Literal[...] value if this instance was created via a Final + # declaration. For example, if we did `x: Final = 3`, x would have an instance + # with a `final_value` of `LiteralType(3, int_fallback)`. self.final_value = final_value def accept(self, visitor: 'TypeVisitor[T]') -> T: @@ -620,26 +624,15 @@ def deserialize(cls, data: Union[JsonDict, str]) -> 'Instance': def copy_modified(self, *, args: Bogus[List[Type]] = _dummy, - erased: Bogus[bool] = _dummy) -> 'Instance': + final_value: Bogus[Optional['LiteralType']] = _dummy) -> 'Instance': return Instance( self.type, args if args is not _dummy else self.args, self.line, self.column, - erased if erased is not _dummy else self.erased, - self.final_value, - ) - - def copy_with_final_value(self, value: LiteralValue) -> 'Instance': - # Note: the fallback for this LiteralType is the *original* type, not the newly - # generated one. This helps prevent infinite loops when we traverse the type tree. - final_value = LiteralType( - value=value, - fallback=self, - line=self.line, - column=self.column, + self.erased, + final_value if final_value is not _dummy else self.final_value, ) - return Instance(self.type, self.args, self.line, self.column, self.erased, final_value) def has_readable_member(self, name: str) -> bool: return self.type.has_readable_member(name) @@ -2070,7 +2063,7 @@ def get_typ_args(tp: Type) -> List[Type]: def set_typ_args(tp: Type, new_args: List[Type], line: int = -1, column: int = -1) -> Type: """Return a copy of a parametrizable Type with arguments set to new_args.""" if isinstance(tp, Instance): - return tp.copy_modified(args=new_args) + return Instance(tp.type, new_args, line, column) if isinstance(tp, TupleType): return tp.copy_modified(items=new_args) if isinstance(tp, UnionType): diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index d00d7c4cbb67..78338bf7140b 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2398,3 +2398,58 @@ force_bytes(reveal_type(a)) # E: Argument 1 to "force_bytes" has incompatible force_bytes(reveal_type(b)) # E: Revealed type is 'Literal['foo']' force_bytes(reveal_type(c)) # E: Revealed type is 'Literal['foo']' [out] + +[case testLiteralFinalPropagatesThroughGenerics] +from typing import TypeVar, Generic +from typing_extensions import Final, Literal + + +T = TypeVar('T') + +class WrapperClass(Generic[T]): + def __init__(self, data: T) -> None: + self.data = data + +def wrapper_func(x: T) -> T: + return x + +def force(x: Literal[99]) -> None: pass +def over_int(x: WrapperClass[int]) -> None: pass +def over_literal(x: WrapperClass[Literal[99]]) -> None: pass + +var1: Final = 99 +w1 = WrapperClass(var1) +force(reveal_type(w1.data)) # E: Argument 1 to "force" has incompatible type "int"; expected "Literal[99]" \ + # E: Revealed type is 'builtins.int*' +force(reveal_type(WrapperClass(var1).data)) # E: Argument 1 to "force" has incompatible type "int"; expected "Literal[99]" \ + # E: Revealed type is 'builtins.int*' +force(reveal_type(wrapper_func(var1))) # E: Revealed type is 'Literal[99]' +over_int(reveal_type(w1)) # E: Revealed type is '__main__.WrapperClass[builtins.int*]' +over_literal(reveal_type(w1)) # E: Argument 1 to "over_literal" has incompatible type "WrapperClass[int]"; expected "WrapperClass[Literal[99]]" \ + # E: Revealed type is '__main__.WrapperClass[builtins.int*]' +over_int(reveal_type(WrapperClass(var1))) # E: Revealed type is '__main__.WrapperClass[builtins.int]' +over_literal(reveal_type(WrapperClass(var1))) # E: Revealed type is '__main__.WrapperClass[Literal[99]]' + +w2 = WrapperClass(99) +force(reveal_type(w2.data)) # E: Argument 1 to "force" has incompatible type "int"; expected "Literal[99]" \ + # E: Revealed type is 'builtins.int*' +force(reveal_type(WrapperClass(99).data)) # E: Argument 1 to "force" has incompatible type "int"; expected "Literal[99]" \ + # E: Revealed type is 'builtins.int*' +force(reveal_type(wrapper_func(99))) # E: Revealed type is 'Literal[99]' +over_int(reveal_type(w2)) # E: Revealed type is '__main__.WrapperClass[builtins.int*]' +over_literal(reveal_type(w2)) # E: Argument 1 to "over_literal" has incompatible type "WrapperClass[int]"; expected "WrapperClass[Literal[99]]" \ + # E: Revealed type is '__main__.WrapperClass[builtins.int*]' +over_int(reveal_type(WrapperClass(99))) # E: Revealed type is '__main__.WrapperClass[builtins.int]' +over_literal(reveal_type(WrapperClass(99))) # E: Revealed type is '__main__.WrapperClass[Literal[99]]' + +var3: Literal[99] = 99 +w3 = WrapperClass(var3) +force(reveal_type(w3.data)) # E: Revealed type is 'Literal[99]' +force(reveal_type(WrapperClass(var3).data)) # E: Revealed type is 'Literal[99]' +force(reveal_type(wrapper_func(var3))) # E: Revealed type is 'Literal[99]' +over_int(reveal_type(w3)) # E: Argument 1 to "over_int" has incompatible type "WrapperClass[Literal[99]]"; expected "WrapperClass[int]" \ + # E: Revealed type is '__main__.WrapperClass[Literal[99]]' +over_literal(reveal_type(w3)) # E: Revealed type is '__main__.WrapperClass[Literal[99]]' +over_int(reveal_type(WrapperClass(var3))) # E: Revealed type is '__main__.WrapperClass[builtins.int]' +over_literal(reveal_type(WrapperClass(var3))) # E: Revealed type is '__main__.WrapperClass[Literal[99]]' +[out] From dd6881623b5ab1e474fddd7e4a5648dbd0010882 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 3 Jan 2019 21:02:17 -0800 Subject: [PATCH 4/6] Fix self check --- mypy/type_visitor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 6ce54bca2ba9..eac1254de17a 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -13,7 +13,7 @@ from abc import abstractmethod from collections import OrderedDict -from typing import Generic, TypeVar, cast, Any, List, Callable, Iterable +from typing import Generic, TypeVar, cast, Any, List, Callable, Iterable, Optional from mypy_extensions import trait T = TypeVar('T') @@ -159,12 +159,17 @@ def visit_deleted_type(self, t: DeletedType) -> Type: return t def visit_instance(self, t: Instance) -> Type: + final_value = None # type: Optional[LiteralType] + if t.final_value is not None: + raw_final_value = t.final_value.accept(self) + assert isinstance(raw_final_value, LiteralType) + final_value = raw_final_value return Instance( typ=t.type, args=self.translate_types(t.args), line=t.line, column=t.column, - final_value=None if t.final_value is None else t.final_value.accept(self), + final_value=final_value, ) def visit_type_var(self, t: TypeVarType) -> Type: From 55a38cefe6248035ff73270ad62df74788eeb59f Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 3 Jan 2019 21:20:11 -0800 Subject: [PATCH 5/6] Remove one last lingering test change --- test-data/unit/check-incremental.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 738f0b6242aa..b3e70a12f70a 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4274,9 +4274,9 @@ class C: from typing import Final class C: - x: Final[int] = 1 + x: Final = 1 def __init__(self) -> None: - self.y: Final[int] = 1 + self.y: Final = 1 [out] [out2] main:4: error: Cannot assign to final name "x" From da6807f2b86933912282b129ae726b526c56c6ca Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 4 Jan 2019 11:46:02 -0800 Subject: [PATCH 6/6] Respond to code review; add a few more tests; add missing logic to fixup --- mypy/checkexpr.py | 55 ++++++++++++++++++++++--------- mypy/fixup.py | 2 ++ mypy/types.py | 40 +++++++++++++++++++--- test-data/unit/check-literal.test | 22 +++++++++++++ test-data/unit/diff.test | 27 +++++++++++---- test-data/unit/fine-grained.test | 26 ++++++++++++++- 6 files changed, 145 insertions(+), 27 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3c1c5750ee0e..b6bb5f95662d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -139,7 +139,16 @@ def __init__(self, self.msg = msg self.plugin = plugin self.type_context = [None] + + # Set to 'True' whenever we are checking the expression in some 'Final' declaration. + # For example, if we're checking the "3" in a statement like "var: Final = 3". + # + # This flag changes the type that eventually gets inferred for "var". Instead of + # inferring *just* a 'builtins.int' instance, we infer an instance that keeps track + # of the underlying literal value. See the comments in Instance's constructors for + # more details. self.in_final_declaration = 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 @@ -212,10 +221,10 @@ 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 isinstance(var.type, Instance): - if self._is_literal_context() and var.type.final_value is not None: + if self.is_literal_context() and var.type.final_value is not None: return var.type.final_value if var.name() in {'True', 'False'}: - return self._handle_literal_expr(var.name() == 'True', 'builtins.bool') + return self.infer_literal_expr_type(var.name() == 'True', 'builtins.bool') return var.type else: if not var.is_ready and self.chk.in_checked_function(): @@ -695,7 +704,7 @@ def check_call(self, call_function = analyze_member_access('__call__', callee, context, False, False, False, self.msg, original_type=callee, chk=self.chk, - in_literal_context=self._is_literal_context()) + in_literal_context=self.is_literal_context()) return self.check_call(call_function, args, arg_kinds, context, arg_names, callable_node, arg_messages) elif isinstance(callee, TypeVarType): @@ -1760,7 +1769,7 @@ def analyze_ordinary_member_access(self, e: MemberExpr, member_type = analyze_member_access( e.name, original_type, e, is_lvalue, False, False, self.msg, original_type=original_type, chk=self.chk, - in_literal_context=self._is_literal_context()) + in_literal_context=self.is_literal_context()) return member_type def analyze_external_member_access(self, member: str, base_type: Type, @@ -1771,14 +1780,30 @@ def analyze_external_member_access(self, member: str, base_type: Type, # TODO remove; no private definitions in mypy return analyze_member_access(member, base_type, context, False, False, False, self.msg, original_type=base_type, chk=self.chk, - in_literal_context=self._is_literal_context()) + in_literal_context=self.is_literal_context()) - def _is_literal_context(self) -> bool: + def is_literal_context(self) -> bool: return is_literal_type_like(self.type_context[-1]) - def _handle_literal_expr(self, value: LiteralValue, fallback_name: str) -> Type: + def infer_literal_expr_type(self, value: LiteralValue, fallback_name: str) -> Type: + """Analyzes the given literal expression and determines if we should be + inferring an Instance type, a Literal[...] type, or an Instance that + remembers the original literal. We... + + 1. ...Infer a normal Instance in most circumstances. + + 2. ...Infer a Literal[...] if we're in a literal context. For example, if we + were analyzing the "3" in "foo(3)" where "foo" has a signature of + "def foo(Literal[3]) -> None", we'd want to infer that the "3" has a + type of Literal[3] instead of Instance. + + 3. ...Infer an Instance that remembers the original Literal if we're declaring + a Final variable with an inferred type -- for example, "bar" in "bar: Final = 3" + would be assigned an Instance that remembers it originated from a '3'. See + the comments in Instance's constructor for more details. + """ typ = self.named_type(fallback_name) - if self._is_literal_context(): + if self.is_literal_context(): return LiteralType(value=value, fallback=typ) elif self.in_final_declaration: return typ.copy_modified(final_value=LiteralType( @@ -1792,19 +1817,19 @@ def _handle_literal_expr(self, value: LiteralValue, fallback_name: str) -> Type: def visit_int_expr(self, e: IntExpr) -> Type: """Type check an integer literal (trivial).""" - return self._handle_literal_expr(e.value, 'builtins.int') + return self.infer_literal_expr_type(e.value, 'builtins.int') def visit_str_expr(self, e: StrExpr) -> Type: """Type check a string literal (trivial).""" - return self._handle_literal_expr(e.value, 'builtins.str') + return self.infer_literal_expr_type(e.value, 'builtins.str') def visit_bytes_expr(self, e: BytesExpr) -> Type: """Type check a bytes literal (trivial).""" - return self._handle_literal_expr(e.value, 'builtins.bytes') + return self.infer_literal_expr_type(e.value, 'builtins.bytes') def visit_unicode_expr(self, e: UnicodeExpr) -> Type: """Type check a unicode literal (trivial).""" - return self._handle_literal_expr(e.value, 'builtins.unicode') + return self.infer_literal_expr_type(e.value, 'builtins.unicode') def visit_float_expr(self, e: FloatExpr) -> Type: """Type check a float literal (trivial).""" @@ -1942,7 +1967,7 @@ def check_method_call_by_name(self, local_errors = local_errors or self.msg method_type = analyze_member_access(method, base_type, context, False, False, True, local_errors, original_type=base_type, chk=self.chk, - in_literal_context=self._is_literal_context()) + in_literal_context=self.is_literal_context()) return self.check_method_call( method, base_type, method_type, args, arg_kinds, context, local_errors) @@ -2006,7 +2031,7 @@ def lookup_operator(op_name: str, base_type: Type) -> Optional[Type]: context=context, msg=local_errors, chk=self.chk, - in_literal_context=self._is_literal_context() + in_literal_context=self.is_literal_context() ) if local_errors.is_errors(): return None @@ -2964,7 +2989,7 @@ def analyze_super(self, e: SuperExpr, is_lvalue: bool) -> Type: context=e, msg=self.msg, chk=self.chk, - in_literal_context=self._is_literal_context()) + in_literal_context=self.is_literal_context()) assert False, 'unreachable' else: # Invalid super. This has been reported by the semantic analyzer. diff --git a/mypy/fixup.py b/mypy/fixup.py index 661d4f682476..37a723a3dbf7 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -155,6 +155,8 @@ def visit_instance(self, inst: Instance) -> None: base.accept(self) for a in inst.args: a.accept(self) + if inst.final_value is not None: + inst.final_value.accept(self) def visit_any(self, o: Any) -> None: pass # Nothing to descend into. diff --git a/mypy/types.py b/mypy/types.py index dd6706dab040..5166c6a8e086 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -569,13 +569,43 @@ def __init__(self, typ: mypy.nodes.TypeInfo, args: List[Type], super().__init__(line, column) self.type = typ self.args = args - self.erased = erased # True if result of type variable substitution - self.invalid = False # True if recovered after incorrect number of type arguments error self.type_ref = None # type: Optional[str] - # The underlying Literal[...] value if this instance was created via a Final - # declaration. For example, if we did `x: Final = 3`, x would have an instance - # with a `final_value` of `LiteralType(3, int_fallback)`. + # True if result of type variable substitution + self.erased = erased + + # True if recovered after incorrect number of type arguments error + self.invalid = False + + # This field keeps track of the underlying Literal[...] value if this instance + # was created via a Final declaration. For example, if we did `x: Final = 3`, x + # would have an instance with a `final_value` of `LiteralType(3, int_fallback)`. + # + # Or more broadly, this field lets this Instance "remember" its original declaration. + # We want this behavior because we want implicit Final declarations to act pretty + # much identically with constants: we should be able to replace any places where we + # use some Final variable with the original value and get the same type-checking + # behavior. For example, we want this program: + # + # def expects_literal(x: Literal[3]) -> None: pass + # var: Final = 3 + # expects_literal(var) + # + # ...to type-check in the exact same way as if we had written the program like this: + # + # def expects_literal(x: Literal[3]) -> None: pass + # expects_literal(3) + # + # In order to make this work (especially with literal types), we need var's type + # (an Instance) to remember the "original" value. + # + # This field is currently set only when we encounter an *implicit* final declaration + # like `x: Final = 3` where the RHS is some literal expression. This field remains 'None' + # when we do things like `x: Final[int] = 3` or `x: Final = foo + bar`. + # + # Currently most of mypy will ignore this field and will continue to treat this type like + # a regular Instance. We end up using this field only when we are explicitly within a + # Literal context. self.final_value = final_value def accept(self, visitor: 'TypeVisitor[T]') -> T: diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index e361ac73ae0c..6e612357880e 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2587,3 +2587,25 @@ over_literal(reveal_type(w3)) # E: Revealed type is '__main__. over_int(reveal_type(WrapperClass(var3))) # E: Revealed type is '__main__.WrapperClass[builtins.int]' over_literal(reveal_type(WrapperClass(var3))) # E: Revealed type is '__main__.WrapperClass[Literal[99]]' [out] + +[case testLiteralFinalUsedInLiteralType] +from typing_extensions import Literal, Final +a: Final[int] = 3 +b: Final = 3 +c: Final[Literal[3]] = 3 +d: Literal[3] + +# TODO: Consider if we want to support cases 'b' and 'd' or not. +# Probably not: we want to mostly keep the 'types' and 'value' worlds distinct. +# However, according to final semantics, we ought to be able to substitute "b" with +# "3" wherever it's used and get the same behavior -- so maybe we do need to support +# at least case "b" for consistency? +a_wrap: Literal[4, a] # E: Invalid type "__main__.a" \ + # E: Parameter 2 of Literal[...] is invalid +b_wrap: Literal[4, b] # E: Invalid type "__main__.b" \ + # E: Parameter 2 of Literal[...] is invalid +c_wrap: Literal[4, c] # E: Invalid type "__main__.c" \ + # E: Parameter 2 of Literal[...] is invalid +d_wrap: Literal[4, d] # E: Invalid type "__main__.d" \ + # E: Parameter 2 of Literal[...] is invalid +[out] diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index eb1023b6b9f7..274aa38fdbf4 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -948,45 +948,59 @@ __main__.A.g [case testFinalFlagsTriggerVar] from typing import Final +v: Final = 1 w: Final = 1 x: Final = 1 y: Final[int] = 1 z: Final[int] = 1 -same: Final = 0 +same1: Final = 1 +same2: Final[int] = 1 class C: + v: Final = 1 w: Final = 1 x: Final = 1 y: Final[int] = 1 z: Final[int] = 1 - same: Final = 0 + same1: Final = 1 + same2: Final[int] = 1 def __init__(self) -> None: + self.vi: Final = 1 self.wi: Final = 1 self.xi: Final = 1 self.yi: Final[int] = 1 self.zi: Final[int] = 1 - self.also_same: Final[int] = 0 + self.same1_instance: Final = 1 + self.same2_instance: Final[int] = 1 [file next.py] from typing import Final +v: Final = 0 w = 1 x: Final[int] = 1 y: int = 1 z: Final = 1 -same: Final = 0 +same1: Final = 1 +same2: Final[int] = 0 class C: + v: Final = 0 w = 1 x: Final[int] = 1 y: int = 1 z: Final = 1 - same: Final = 0 + same1: Final = 1 + same2: Final[int] = 0 def __init__(self) -> None: + self.vi: Final = 0 self.wi = 1 self.xi: Final[int] = 1 self.yi: int = 1 self.zi: Final = 1 - self.also_same: Final[int] = 0 + self.same1_instance: Final = 1 + self.same2_instance: Final[int] = 0 [out] +__main__.C.v +__main__.C.vi __main__.C.w __main__.C.wi __main__.C.x @@ -995,6 +1009,7 @@ __main__.C.y __main__.C.yi __main__.C.z __main__.C.zi +__main__.v __main__.w __main__.x __main__.y diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 68306f579273..e6f6c77016bd 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -8452,6 +8452,31 @@ main:2: error: Revealed type is 'builtins.int*' == main:2: error: Revealed type is 'Literal[3]' +[case testLiteralFineGrainedChainedViaFinal] +from mod1 import foo +from typing_extensions import Literal +def expect_3(x: Literal[3]) -> None: pass +expect_3(foo) +[file mod1.py] +from mod2 import bar +foo = bar +[file mod2.py] +from mod3 import qux as bar +[file mod3.py] +from typing_extensions import Final +qux: Final = 3 +[file mod3.py.2] +from typing_extensions import Final +qux: Final = 4 +[file mod3.py.3] +from typing_extensions import Final +qux: Final[int] = 4 +[out] +== +main:4: error: Argument 1 to "expect_3" has incompatible type "Literal[4]"; expected "Literal[3]" +== +main:4: error: Argument 1 to "expect_3" has incompatible type "int"; expected "Literal[3]" + [case testLiteralFineGrainedStringConversionPython3] from mod1 import foo reveal_type(foo) @@ -8518,4 +8543,3 @@ main:3: error: Revealed type is 'Literal[u'foo']' main:3: error: Revealed type is 'Literal['foo']' == main:3: error: Revealed type is 'Literal[u'foo']' -