Skip to content

Commit 1987d17

Browse files
Reject direct union expressions in Literal
1 parent 21e2859 commit 1987d17

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

mypy/typeanal.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,14 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type]
17301730
)
17311731
]
17321732

1733+
if self.is_direct_union_literal_param(arg):
1734+
self.fail(
1735+
f"Parameter {idx} of Literal[...] cannot be a union expression",
1736+
ctx,
1737+
code=codes.VALID_TYPE,
1738+
)
1739+
return None
1740+
17331741
# If arg is an UnboundType that was *not* originally defined as
17341742
# a string, try expanding it in case it's a type alias or something.
17351743
if isinstance(arg, UnboundType):
@@ -1786,6 +1794,9 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type]
17861794
# Types generated from declarations like "var: Final = 4".
17871795
return [arg.last_known_value]
17881796
elif isinstance(arg, UnionType):
1797+
# This handles unions produced by expanding aliases or nested Literal types,
1798+
# such as Literal[Alias] where Alias = Literal[1, None]. Direct union
1799+
# expressions inside Literal[...] are rejected before alias expansion above.
17891800
out = []
17901801
for union_arg in arg.items:
17911802
union_result = self.analyze_literal_param(idx, union_arg, ctx)
@@ -1797,6 +1808,21 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type]
17971808
self.fail(f"Parameter {idx} of Literal[...] is invalid", ctx, code=codes.VALID_TYPE)
17981809
return None
17991810

1811+
def is_direct_union_literal_param(self, arg: Type) -> bool:
1812+
"""Is this a direct Union expression inside Literal[...]?"""
1813+
if (
1814+
isinstance(arg, ProperType)
1815+
and isinstance(arg, UnionType)
1816+
and arg.uses_pep604_syntax
1817+
and arg.original_str_expr is None
1818+
):
1819+
return True
1820+
if isinstance(arg, UnboundType) and arg.args:
1821+
sym = self.lookup_qualified(arg.name, arg, suppress_errors=True)
1822+
if sym is not None and sym.node is not None:
1823+
return sym.node.fullname in ("typing.Union", "typing.Optional")
1824+
return False
1825+
18001826
def analyze_type(self, typ: Type) -> Type:
18011827
return typ.accept(self)
18021828

test-data/unit/check-literal.test

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,21 @@ e: Literal[dummy()] # E: Invalid type: Literal[...] cannot contain a
611611
[builtins fixtures/tuple.pyi]
612612
[out]
613613

614+
[case testLiteralDisallowUnionExpressions]
615+
from typing import Literal, Optional, Union
616+
from typing import Literal as L, Optional as O, Union as U
617+
618+
a: Literal[1 | None] # E: Parameter 1 of Literal[...] cannot be a union expression
619+
b: Literal[1 | 2] # E: Parameter 1 of Literal[...] cannot be a union expression
620+
c: Literal[Literal[1] | None] # E: Parameter 1 of Literal[...] cannot be a union expression
621+
d: Literal[Union[Literal[1], None]] # E: Parameter 1 of Literal[...] cannot be a union expression
622+
e: Literal[Optional[Literal[1]]] # E: Parameter 1 of Literal[...] cannot be a union expression
623+
f: L[1 | None] # E: Parameter 1 of Literal[...] cannot be a union expression
624+
g: Literal[U[Literal[1], None]] # E: Parameter 1 of Literal[...] cannot be a union expression
625+
h: Literal[O[Literal[1]]] # E: Parameter 1 of Literal[...] cannot be a union expression
626+
[builtins fixtures/tuple.pyi]
627+
[out]
628+
614629
[case testLiteralDisallowCollections]
615630
from typing import Literal
616631
a: Literal[{"a": 1, "b": 2}] # E: Parameter 1 of Literal[...] is invalid
@@ -688,6 +703,32 @@ reveal_type(e) # N: Revealed type is "None | None | None"
688703
[builtins fixtures/bool.pyi]
689704
[out]
690705

706+
[case testLiteralValidNoneUnionAlias]
707+
from typing import Literal, Union
708+
709+
a: Literal[1, None]
710+
b: Literal[Literal[1, None]]
711+
712+
Alias1 = Literal[1, None]
713+
c: Literal[Alias1]
714+
715+
Alias2 = Literal[1] | None
716+
d: Literal[Alias2]
717+
718+
Alias3 = Union[Literal[1], None]
719+
e: Literal[Alias3]
720+
721+
f: Literal["1 | None"]
722+
723+
reveal_type(a) # N: Revealed type is "Literal[1] | None"
724+
reveal_type(b) # N: Revealed type is "Literal[1] | None"
725+
reveal_type(c) # N: Revealed type is "Literal[1] | None"
726+
reveal_type(d) # N: Revealed type is "Literal[1] | None"
727+
reveal_type(e) # N: Revealed type is "Literal[1] | None"
728+
reveal_type(f) # N: Revealed type is "Literal['1 | None']"
729+
[builtins fixtures/tuple.pyi]
730+
[out]
731+
691732
[case testLiteralMultipleValuesExplicitTuple]
692733
from typing import Literal
693734
# Unfortunately, it seems like typed_ast is unable to distinguish this from

0 commit comments

Comments
 (0)