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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy