Skip to content

Commit 8dafdc8

Browse files
authored
Add support for literal fallbacks (#5993)
This pull request modifies the 'checkmember' related code to use the LiteralType fallback as appropriate. This lets us use the underlying int, bool, str, etc methods for Literals. Note: I'm not 100% sure whether or not I've modified all the correct places. I used PyCharm to detect all case where we used TypedDictType's "fallback" field and attempted to add similar code for LiteralType, but I don't think this technique was necessarily foolproof.
1 parent 164cf7c commit 8dafdc8

File tree

5 files changed

+71
-22
lines changed

5 files changed

+71
-22
lines changed

mypy/checker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType,
3232
Instance, NoneTyp, strip_type, TypeType, TypeOfAny,
3333
UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef,
34-
true_only, false_only, function_type, is_named_instance, union_items, TypeQuery,
34+
true_only, false_only, function_type, is_named_instance, union_items, TypeQuery, LiteralType,
3535
is_optional, remove_optional
3636
)
3737
from mypy.sametypes import is_same_type, is_same_types
@@ -1065,7 +1065,7 @@ def check_reverse_op_method(self, defn: FuncItem,
10651065
forward_inst = reverse_type.arg_types[1]
10661066
if isinstance(forward_inst, TypeVarType):
10671067
forward_inst = forward_inst.upper_bound
1068-
if isinstance(forward_inst, (FunctionLike, TupleType, TypedDictType)):
1068+
if isinstance(forward_inst, (FunctionLike, TupleType, TypedDictType, LiteralType)):
10691069
forward_inst = forward_inst.fallback
10701070
if isinstance(forward_inst, TypeType):
10711071
item = forward_inst.item

mypy/checkexpr.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from mypy.types import (
1919
Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef,
2020
TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType,
21-
PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny,
21+
PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny, LiteralType,
2222
true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike,
2323
StarType, is_optional, remove_optional, is_invariant_instance
2424
)
@@ -323,7 +323,7 @@ def method_fullname(self, object_type: Type, method_name: str) -> Optional[str]:
323323
type_name = None
324324
if isinstance(object_type, Instance):
325325
type_name = object_type.type.fullname()
326-
elif isinstance(object_type, TypedDictType):
326+
elif isinstance(object_type, (TypedDictType, LiteralType)):
327327
info = object_type.fallback.type.get_containing_type_info(method_name)
328328
type_name = info.fullname() if info is not None else None
329329
elif isinstance(object_type, TupleType):
@@ -3121,7 +3121,7 @@ def has_member(self, typ: Type, member: str) -> bool:
31213121
# these two should be carefully kept in sync.
31223122
if isinstance(typ, TypeVarType):
31233123
typ = typ.upper_bound
3124-
if isinstance(typ, TupleType):
3124+
if isinstance(typ, (TupleType, LiteralType)):
31253125
typ = typ.fallback
31263126
if isinstance(typ, Instance):
31273127
return typ.type.has_readable_member(member)

mypy/checkmember.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from mypy.types import (
66
Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike, TypeVarDef,
7-
Overloaded, TypeVarType, UnionType, PartialType, UninhabitedType, TypeOfAny,
7+
Overloaded, TypeVarType, UnionType, PartialType, UninhabitedType, TypeOfAny, LiteralType,
88
DeletedType, NoneTyp, TypeType, function_type, get_type_vars,
99
)
1010
from mypy.nodes import (
@@ -131,12 +131,7 @@ def analyze_member_access(name: str,
131131
for subtype in typ.relevant_items()]
132132
msg.disable_type_names -= 1
133133
return UnionType.make_simplified_union(results)
134-
elif isinstance(typ, TupleType):
135-
# Actually look up from the fallback instance type.
136-
return analyze_member_access(name, typ.fallback, node, is_lvalue, is_super,
137-
is_operator, builtin_type, not_ready_callback, msg,
138-
original_type=original_type, chk=chk)
139-
elif isinstance(typ, TypedDictType):
134+
elif isinstance(typ, (TupleType, TypedDictType, LiteralType)):
140135
# Actually look up from the fallback instance type.
141136
return analyze_member_access(name, typ.fallback, node, is_lvalue, is_super,
142137
is_operator, builtin_type, not_ready_callback, msg,

test-data/unit/check-literal.test

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -640,23 +640,76 @@ c: Literal[15]
640640
-- to rewrite them in the future.
641641
--
642642

643-
[case testLiteralInheritedMethodsInteractCorrectly-skip]
644-
# TODO: fix this test. The method calls are not using the fallbacks.
643+
[case testLiteralActualAssignment-skip]
644+
# TODO: fix this test. The 1 is currently always given a type of 'int'
645+
from typing_extensions import Literal
646+
647+
a: Literal[1] = 1
648+
[out]
649+
650+
--
651+
-- Tests that make sure we're correctly using the fallback
652+
--
653+
654+
[case testLiteralFallbackOperatorsWorkCorrectly]
645655
from typing_extensions import Literal
646656

647657
a: Literal[3]
648658
b: int
649-
c: Literal['foo']
659+
c: Literal[4]
660+
d: Literal['foo']
661+
e: str
662+
663+
reveal_type(a + a) # E: Revealed type is 'builtins.int'
664+
reveal_type(a + b) # E: Revealed type is 'builtins.int'
665+
reveal_type(b + a) # E: Revealed type is 'builtins.int'
666+
reveal_type(a + 1) # E: Revealed type is 'builtins.int'
667+
reveal_type(1 + a) # E: Revealed type is 'builtins.int'
668+
reveal_type(a + c) # E: Revealed type is 'builtins.int'
669+
reveal_type(c + a) # E: Revealed type is 'builtins.int'
670+
671+
reveal_type(d + d) # E: Revealed type is 'builtins.str'
672+
reveal_type(d + e) # E: Revealed type is 'builtins.str'
673+
reveal_type(e + d) # E: Revealed type is 'builtins.str'
674+
reveal_type(d + 'foo') # E: Revealed type is 'builtins.str'
675+
reveal_type('foo' + d) # E: Revealed type is 'builtins.str'
676+
677+
reveal_type(a.__add__(b)) # E: Revealed type is 'builtins.int'
678+
reveal_type(b.__add__(a)) # E: Revealed type is 'builtins.int'
679+
680+
a *= b # E: Incompatible types in assignment (expression has type "int", variable has type "Literal[3]")
681+
b *= a
650682

651-
reveal_type(a + a) # E: Revealed type is 'builtins.int'
652-
reveal_type(a + b) # E: Revealed type is 'builtins.int'
653-
reveal_type(b + a) # E: Revealed type is 'builtins.int'
654-
reveal_type(c.strip()) # E: Revealed type is 'builtins.str'
683+
reveal_type(b) # E: Revealed type is 'builtins.int'
655684
[out]
656685

657-
[case testLiteralActualAssignment-skip]
658-
# TODO: fix this test. The 1 is currently always given a type of 'int'
686+
[case testLiteralFallbackInheritedMethodsWorkCorrectly]
659687
from typing_extensions import Literal
688+
a: Literal['foo']
689+
b: str
660690

661-
a: Literal[1] = 1
691+
reveal_type(a.startswith(a)) # E: Revealed type is 'builtins.bool'
692+
reveal_type(b.startswith(a)) # E: Revealed type is 'builtins.bool'
693+
reveal_type(a.startswith(b)) # E: Revealed type is 'builtins.bool'
694+
reveal_type(a.strip()) # E: Revealed type is 'builtins.str'
695+
[builtins fixtures/ops.pyi]
696+
[out]
697+
698+
[case testLiteralFallbackMethodsDoNotCoerceToLiteral]
699+
from typing_extensions import Literal
700+
701+
a: Literal[3]
702+
b: int
703+
c: Literal["foo"]
704+
705+
a = a * a # E: Incompatible types in assignment (expression has type "int", variable has type "Literal[3]")
706+
a = a * b # E: Incompatible types in assignment (expression has type "int", variable has type "Literal[3]")
707+
a = b * a # E: Incompatible types in assignment (expression has type "int", variable has type "Literal[3]")
708+
709+
b = a * a
710+
b = a * b
711+
b = b * a
712+
713+
c = c.strip() # E: Incompatible types in assignment (expression has type "str", variable has type "Literal['foo']")
714+
[builtins fixtures/ops.pyi]
662715
[out]

test-data/unit/fixtures/ops.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class str:
3131
def __add__(self, x: 'str') -> 'str': pass
3232
def __eq__(self, x: object) -> bool: pass
3333
def startswith(self, x: 'str') -> bool: pass
34+
def strip(self) -> 'str': pass
3435

3536
class unicode: pass
3637

0 commit comments

Comments
 (0)