Skip to content
Merged
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
6 changes: 4 additions & 2 deletions mypy/solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None:
uppers = new_uppers

# ...unless this is the only information we have, then we just pass it on.
lowers = list(lowers)
if not uppers and not lowers:
candidate = UninhabitedType()
candidate.ambiguous = True
Expand All @@ -281,10 +282,11 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None:
# Process each bound separately, and calculate the lower and upper
# bounds based on constraints. Note that we assume that the constraint
# targets do not have constraint references.
if type_state.infer_unions:
if type_state.infer_unions and lowers:
# This deviates from the general mypy semantics because
# recursive types are union-heavy in 95% of cases.
bottom = UnionType.make_union(list(lowers))
# Retain `None` when no bottoms were provided to avoid bogus `Never` inference.
bottom = UnionType.make_union(lowers)
else:
# The order of lowers is non-deterministic.
# We attempt to sort lowers because joins are non-associative. For instance:
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-recursive-types.test
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ reveal_type(flatten([1, [2, [3]]])) # N: Revealed type is "builtins.list[builti

class Bad: ...
x: Nested[int] = [1, [2, [3]]]
x = [1, [Bad()]] # E: List item 1 has incompatible type "list[Bad]"; expected "Union[int, Nested[int]]"
x = [1, [Bad()]] # E: List item 0 has incompatible type "Bad"; expected "Union[int, Nested[int]]"
[builtins fixtures/isinstancelist.pyi]

[case testRecursiveAliasGenericInferenceNested]
Expand Down Expand Up @@ -605,7 +605,7 @@ class NT(NamedTuple, Generic[T]):
class A: ...
class B(A): ...

nti: NT[int] = NT(key=0, value=NT(key=1, value=A())) # E: Argument "value" to "NT" has incompatible type "NT[A]"; expected "Union[int, NT[int]]"
nti: NT[int] = NT(key=0, value=NT(key=1, value=A())) # E: Argument "value" to "NT" has incompatible type "A"; expected "Union[int, NT[int]]"
reveal_type(nti) # N: Revealed type is "tuple[builtins.int, Union[builtins.int, ...], fallback=__main__.NT[builtins.int]]"

nta: NT[A]
Expand Down
18 changes: 18 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -4271,3 +4271,21 @@ reveal_type(dicts.TF) # N: Revealed type is "def (*, user_id: builtins.int =) -
reveal_type(dicts.TotalFalse) # N: Revealed type is "def (*, user_id: builtins.int =) -> TypedDict('__main__.Dicts.TF', {'user_id'?: builtins.int})"
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testRecursiveNestedTypedDictInference]
from typing import TypedDict, Sequence
from typing_extensions import NotRequired

class Component(TypedDict):
type: str
components: NotRequired[Sequence['Component']]

inputs: Sequence[Component] = [{
'type': 'tuple',
'components': [
{'type': 'uint256'},
{'type': 'address'},
]
}]
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]