diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 02b96afa8c17..d928580f5625 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -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): @@ -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) @@ -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) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index f795f1f5b354..15c24c257818 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -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 +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 @@ -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 diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 93bb6e4e52f2..8f37ebd0eea9 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -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: