Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,14 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type]
)
]

if self.is_direct_union_literal_param(arg):
self.fail(
f"Parameter {idx} of Literal[...] cannot be a union expression",
ctx,
code=codes.VALID_TYPE,
)
return None

# If arg is an UnboundType that was *not* originally defined as
# a string, try expanding it in case it's a type alias or something.
if isinstance(arg, UnboundType):
Expand Down Expand Up @@ -1786,6 +1794,9 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type]
# Types generated from declarations like "var: Final = 4".
return [arg.last_known_value]
elif isinstance(arg, UnionType):
# This handles unions produced by expanding aliases or nested Literal types,
# such as Literal[Alias] where Alias = Literal[1, None]. Direct union
# expressions inside Literal[...] are rejected before alias expansion above.
out = []
for union_arg in arg.items:
union_result = self.analyze_literal_param(idx, union_arg, ctx)
Expand All @@ -1797,6 +1808,21 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type]
self.fail(f"Parameter {idx} of Literal[...] is invalid", ctx, code=codes.VALID_TYPE)
return None

def is_direct_union_literal_param(self, arg: Type) -> bool:
"""Is this a direct Union expression inside Literal[...]?"""
if (
isinstance(arg, ProperType)
and isinstance(arg, UnionType)
and arg.uses_pep604_syntax
and arg.original_str_expr is None
):
return True
if isinstance(arg, UnboundType) and arg.args:
sym = self.lookup_qualified(arg.name, arg, suppress_errors=True)
if sym is not None and sym.node is not None:
return sym.node.fullname in ("typing.Union", "typing.Optional")
return False

def analyze_type(self, typ: Type) -> Type:
return typ.accept(self)

Expand Down
41 changes: 41 additions & 0 deletions test-data/unit/check-literal.test
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,21 @@ e: Literal[dummy()] # E: Invalid type: Literal[...] cannot contain a
[builtins fixtures/tuple.pyi]
[out]

[case testLiteralDisallowUnionExpressions]
from typing import Literal, Optional, Union
from typing import Literal as L, Optional as O, Union as U

a: Literal[1 | None] # E: Parameter 1 of Literal[...] cannot be a union expression
b: Literal[1 | 2] # E: Parameter 1 of Literal[...] cannot be a union expression
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad case of error msg here. 1 | 2 should be recongized as bitwise operation?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll open a follow-up PR to fix parsing of expressions inside Literal once this has been merged.

c: Literal[Literal[1] | None] # E: Parameter 1 of Literal[...] cannot be a union expression
d: Literal[Union[Literal[1], None]] # E: Parameter 1 of Literal[...] cannot be a union expression
e: Literal[Optional[Literal[1]]] # E: Parameter 1 of Literal[...] cannot be a union expression
f: L[1 | None] # E: Parameter 1 of Literal[...] cannot be a union expression
g: Literal[U[Literal[1], None]] # E: Parameter 1 of Literal[...] cannot be a union expression
h: Literal[O[Literal[1]]] # E: Parameter 1 of Literal[...] cannot be a union expression
[builtins fixtures/tuple.pyi]
[out]

[case testLiteralDisallowCollections]
from typing import Literal
a: Literal[{"a": 1, "b": 2}] # E: Parameter 1 of Literal[...] is invalid
Expand Down Expand Up @@ -688,6 +703,32 @@ reveal_type(e) # N: Revealed type is "None | None | None"
[builtins fixtures/bool.pyi]
[out]

[case testLiteralValidNoneUnionAlias]
from typing import Literal, Union

a: Literal[1, None]
b: Literal[Literal[1, None]]

Alias1 = Literal[1, None]
c: Literal[Alias1]

Alias2 = Literal[1] | None
d: Literal[Alias2]

Alias3 = Union[Literal[1], None]
e: Literal[Alias3]

f: Literal["1 | None"]

reveal_type(a) # N: Revealed type is "Literal[1] | None"
reveal_type(b) # N: Revealed type is "Literal[1] | None"
reveal_type(c) # N: Revealed type is "Literal[1] | None"
reveal_type(d) # N: Revealed type is "Literal[1] | None"
reveal_type(e) # N: Revealed type is "Literal[1] | None"
reveal_type(f) # N: Revealed type is "Literal['1 | None']"
[builtins fixtures/tuple.pyi]
[out]

[case testLiteralMultipleValuesExplicitTuple]
from typing import Literal
# Unfortunately, it seems like typed_ast is unable to distinguish this from
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-unreachable-code.test
Original file line number Diff line number Diff line change
Expand Up @@ -1495,7 +1495,7 @@ class Lie:
def __bool__(self) -> Literal[False]: ...

class Maybe:
def __bool__(self) -> Literal[True | False]: ...
def __bool__(self) -> Literal[True, False]: ...

t = Truth()
if t:
Expand Down
Loading