diff --git a/mypy/checker.py b/mypy/checker.py index 2638f0e768c7..2b3ba74e8e46 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1810,8 +1810,11 @@ 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, + 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], rvalue: Expression) -> bool: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 2415049837e8..b6bb5f95662d 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,6 +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 @@ -210,10 +220,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 is_literal_type_like(self.type_context[-1]) 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.infer_literal_expr_type(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) @@ -691,7 +703,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): @@ -1755,7 +1768,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, @@ -1765,35 +1779,57 @@ 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 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(): + return LiteralType(value=value, fallback=typ) + elif self.in_final_declaration: + return typ.copy_modified(final_value=LiteralType( + value=value, + fallback=typ, + line=typ.line, + column=typ.column, + )) + else: + return typ 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]): - return LiteralType(value=e.value, fallback=typ) - return typ + return self.infer_literal_expr_type(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 is_literal_type_like(self.type_context[-1]): - return LiteralType(value=e.value, fallback=typ) - return typ + return self.infer_literal_expr_type(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.infer_literal_expr_type(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.infer_literal_expr_type(e.value, 'builtins.unicode') def visit_float_expr(self, e: FloatExpr) -> Type: """Type check a float literal (trivial).""" @@ -1930,7 +1966,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) @@ -1994,6 +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() ) if local_errors.is_errors(): return None @@ -2950,7 +2988,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. @@ -3117,6 +3156,7 @@ def accept(self, type_context: Optional[Type] = None, allow_none_return: bool = False, always_allow_any: 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 @@ -3124,6 +3164,8 @@ def accept(self, """ if node in self.type_overrides: return self.type_overrides[node] + 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): @@ -3136,6 +3178,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.in_final_declaration = old_in_final_declaration assert typ is not None self.chk.store_type(node, typ) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index c1254feb4a67..c0bed7810308 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/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/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 b2537a551f1f..6999b5431199 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 @@ -1760,9 +1760,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: @@ -1900,8 +1900,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. @@ -1910,16 +1912,31 @@ def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]: # inside type variables with value restrictions (like # AnyStr). return None - if isinstance(rvalue, IntExpr): - return self.named_type_or_none('builtins.int') 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): + value, type_name = rvalue.value, 'builtins.int' if isinstance(rvalue, StrExpr): - return self.named_type_or_none('builtins.str') + value, type_name = rvalue.value, 'builtins.str' if isinstance(rvalue, BytesExpr): - return self.named_type_or_none('builtins.bytes') + value, type_name = rvalue.value, 'builtins.bytes' if isinstance(rvalue, UnicodeExpr): - return 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_modified(final_value=LiteralType( + value=value, + fallback=typ, + line=typ.line, + column=typ.column, + )) + return typ + return None def analyze_alias(self, rvalue: Expression) -> Tuple[Optional[Type], List[str], 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 81319bedb480..39865efa2faf 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -342,6 +342,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/type_visitor.py b/mypy/type_visitor.py index 79318f56baa2..4d65ea7d4e51 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,7 +159,18 @@ 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) + 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=final_value, + ) def visit_type_var(self, t: TypeVarType) -> Type: return t diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 5c8d6770d95a..f6b36782b231 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -697,6 +697,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: diff --git a/mypy/types.py b/mypy/types.py index 776fee220b7a..ed3097d7e59e 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -569,37 +569,77 @@ 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] + # 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: 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 @@ -616,10 +656,21 @@ 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, + final_value: Bogus[Optional['LiteralType']] = _dummy) -> 'Instance': + return Instance( + self.type, + args if args is not _dummy else self.args, + self.line, + self.column, + self.erased, + final_value if final_value is not _dummy else self.final_value, + ) def has_readable_member(self, name: str) -> bool: return self.type.has_readable_member(name) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 0168c691e514..a57f93f7d9ab 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2007,7 +2007,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] @@ -2209,4 +2209,398 @@ UnicodeDict = TypedDict(b'UnicodeDict', {'key': int}) [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] + + +-- +-- Interactions with 'Final' +-- + +[case testLiteralFinalInferredAsLiteral] +from typing_extensions import Final, Literal + +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 + +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 '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 testLiteralFinalDirectInstanceTypesSupercedeInferredLiteral] +from typing_extensions import Final, Literal + +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 + +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] + +[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, Literal + +a: Final = 1 +b: Final = (1, 2) + +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] + +[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] + +[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 88067e28362a..274aa38fdbf4 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -948,39 +948,72 @@ __main__.A.g [case testFinalFlagsTriggerVar] from typing import Final +v: Final = 1 +w: Final = 1 x: Final = 1 y: Final[int] = 1 -same: Final = 0 +z: Final[int] = 1 +same1: Final = 1 +same2: Final[int] = 1 class C: + v: Final = 1 + w: Final = 1 x: Final = 1 y: Final[int] = 1 - same: Final = 0 + z: Final[int] = 1 + same1: Final = 1 + same2: Final[int] = 1 def __init__(self) -> None: - self.z: Final = 1 - self.t: Final[int] = 1 - self.also_same: Final[int] = 0 + self.vi: Final = 1 + self.wi: Final = 1 + self.xi: Final = 1 + self.yi: Final[int] = 1 + self.zi: Final[int] = 1 + self.same1_instance: Final = 1 + self.same2_instance: Final[int] = 1 [file next.py] from typing import Final -x = 1 +v: Final = 0 +w = 1 +x: Final[int] = 1 y: int = 1 -same: Final = 0 +z: Final = 1 +same1: Final = 1 +same2: Final[int] = 0 class C: - x = 1 + v: Final = 0 + w = 1 + x: Final[int] = 1 y: int = 1 - same: Final = 0 + z: Final = 1 + same1: Final = 1 + same2: Final[int] = 0 def __init__(self) -> None: - self.z = 1 - self.t: int = 1 - self.also_same: Final = 0 -[out] -__main__.C.t + self.vi: Final = 0 + self.wi = 1 + self.xi: Final[int] = 1 + self.yi: int = 1 + self.zi: Final = 1 + 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 +__main__.C.xi __main__.C.y +__main__.C.yi __main__.C.z +__main__.C.zi +__main__.v +__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 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']' -