Skip to content

Commit 34fe00b

Browse files
ilai-deutelMichael0x2a
authored andcommitted
Support for singleton types in unions with Enum (#7693)
This PR adds supports for singleton types in unions using `Enum`s as described in [PEP 484][0] (without using `Final`). As suggested by @Michael0x2a in [the corresponding issue][1], adding another case to the `is_singleton_type` and `coerce_to_literal`functions allows mypy to recognize an `Enum` with 1 value as a singleton type. Fixes #7279 [0]: https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions [1]: #7279 (comment)
1 parent a3d48cd commit 34fe00b

File tree

2 files changed

+77
-10
lines changed

2 files changed

+77
-10
lines changed

mypy/checker.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4743,6 +4743,11 @@ def is_private(node_name: str) -> bool:
47434743
return node_name.startswith('__') and not node_name.endswith('__')
47444744

47454745

4746+
def get_enum_values(typ: Instance) -> List[str]:
4747+
"""Return the list of values for an Enum."""
4748+
return [name for name, sym in typ.type.names.items() if isinstance(sym.node, Var)]
4749+
4750+
47464751
def is_singleton_type(typ: Type) -> bool:
47474752
"""Returns 'true' if this type is a "singleton type" -- if there exists
47484753
exactly only one runtime value associated with this type.
@@ -4751,7 +4756,8 @@ def is_singleton_type(typ: Type) -> bool:
47514756
'is_singleton_type(t)' returns True if and only if the expression 'a is b' is
47524757
always true.
47534758
4754-
Currently, this returns True when given NoneTypes and enum LiteralTypes.
4759+
Currently, this returns True when given NoneTypes, enum LiteralTypes and
4760+
enum types with a single value.
47554761
47564762
Note that other kinds of LiteralTypes cannot count as singleton types. For
47574763
example, suppose we do 'a = 100000 + 1' and 'b = 100001'. It is not guaranteed
@@ -4761,7 +4767,10 @@ def is_singleton_type(typ: Type) -> bool:
47614767
typ = get_proper_type(typ)
47624768
# TODO: Also make this return True if the type is a bool LiteralType.
47634769
# Also make this return True if the type corresponds to ... (ellipsis) or NotImplemented?
4764-
return isinstance(typ, NoneType) or (isinstance(typ, LiteralType) and typ.is_enum_literal())
4770+
return (
4771+
isinstance(typ, NoneType) or (isinstance(typ, LiteralType) and typ.is_enum_literal())
4772+
or (isinstance(typ, Instance) and typ.type.is_enum and len(get_enum_values(typ)) == 1)
4773+
)
47654774

47664775

47674776
def try_expanding_enum_to_union(typ: Type, target_fullname: str) -> ProperType:
@@ -4808,17 +4817,21 @@ class Status(Enum):
48084817

48094818

48104819
def coerce_to_literal(typ: Type) -> ProperType:
4811-
"""Recursively converts any Instances that have a last_known_value into the
4812-
corresponding LiteralType.
4820+
"""Recursively converts any Instances that have a last_known_value or are
4821+
instances of enum types with a single value into the corresponding LiteralType.
48134822
"""
48144823
typ = get_proper_type(typ)
48154824
if isinstance(typ, UnionType):
48164825
new_items = [coerce_to_literal(item) for item in typ.items]
48174826
return make_simplified_union(new_items)
4818-
elif isinstance(typ, Instance) and typ.last_known_value:
4819-
return typ.last_known_value
4820-
else:
4821-
return typ
4827+
elif isinstance(typ, Instance):
4828+
if typ.last_known_value:
4829+
return typ.last_known_value
4830+
elif typ.type.is_enum:
4831+
enum_values = get_enum_values(typ)
4832+
if len(enum_values) == 1:
4833+
return LiteralType(value=enum_values[0], fallback=typ)
4834+
return typ
48224835

48234836

48244837
def has_bool_item(typ: ProperType) -> bool:

test-data/unit/check-enum.test

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ else:
808808

809809
[builtins fixtures/bool.pyi]
810810

811-
[case testEnumReachabilityPEP484Example1]
811+
[case testEnumReachabilityPEP484ExampleWithFinal]
812812
# flags: --strict-optional
813813
from typing import Union
814814
from typing_extensions import Final
@@ -833,7 +833,7 @@ def func(x: Union[int, None, Empty] = _empty) -> int:
833833
return x + 2
834834
[builtins fixtures/primitives.pyi]
835835

836-
[case testEnumReachabilityPEP484Example2]
836+
[case testEnumReachabilityPEP484ExampleWithMultipleValues]
837837
from typing import Union
838838
from enum import Enum
839839

@@ -852,5 +852,59 @@ def process(response: Union[str, Reason] = '') -> str:
852852
# response can be only str, all other possible values exhausted
853853
reveal_type(response) # N: Revealed type is 'builtins.str'
854854
return 'PROCESSED: ' + response
855+
[builtins fixtures/primitives.pyi]
856+
857+
858+
[case testEnumReachabilityPEP484ExampleSingleton]
859+
# flags: --strict-optional
860+
from typing import Union
861+
from typing_extensions import Final
862+
from enum import Enum
863+
864+
class Empty(Enum):
865+
token = 0
866+
_empty = Empty.token
867+
868+
def func(x: Union[int, None, Empty] = _empty) -> int:
869+
boom = x + 42 # E: Unsupported left operand type for + ("None") \
870+
# E: Unsupported left operand type for + ("Empty") \
871+
# N: Left operand is of type "Union[int, None, Empty]"
872+
if x is _empty:
873+
reveal_type(x) # N: Revealed type is 'Literal[__main__.Empty.token]'
874+
return 0
875+
elif x is None:
876+
reveal_type(x) # N: Revealed type is 'None'
877+
return 1
878+
else: # At this point typechecker knows that x can only have type int
879+
reveal_type(x) # N: Revealed type is 'builtins.int'
880+
return x + 2
881+
[builtins fixtures/primitives.pyi]
882+
883+
[case testEnumReachabilityPEP484ExampleSingletonWithMethod]
884+
# flags: --strict-optional
885+
from typing import Union
886+
from typing_extensions import Final
887+
from enum import Enum
855888

889+
class Empty(Enum):
890+
token = lambda x: x
891+
892+
def f(self) -> int:
893+
return 1
894+
895+
_empty = Empty.token
896+
897+
def func(x: Union[int, None, Empty] = _empty) -> int:
898+
boom = x + 42 # E: Unsupported left operand type for + ("None") \
899+
# E: Unsupported left operand type for + ("Empty") \
900+
# N: Left operand is of type "Union[int, None, Empty]"
901+
if x is _empty:
902+
reveal_type(x) # N: Revealed type is 'Literal[__main__.Empty.token]'
903+
return 0
904+
elif x is None:
905+
reveal_type(x) # N: Revealed type is 'None'
906+
return 1
907+
else: # At this point typechecker knows that x can only have type int
908+
reveal_type(x) # N: Revealed type is 'builtins.int'
909+
return x + 2
856910
[builtins fixtures/primitives.pyi]

0 commit comments

Comments
 (0)