Skip to content

Commit e69bd9a

Browse files
authored
Remove Optional[...] special-casing during semantic analysis (#13357)
This is another fix for recursive aliases. Ref #13297 Previously this special casing caused `Optional[...]` to fail with infinite recursion, where `Union[..., None]` worked. I also delete a duplicate helper, and replace it with another existing one that handles type aliases properly. (I have no idea why some TypeGuard test started passing, but we have `xfail-strict` so I am re-enabling this test.)
1 parent ae2fc03 commit e69bd9a

File tree

6 files changed

+32
-30
lines changed

6 files changed

+32
-30
lines changed

mypy/checker.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,14 @@
199199
UnboundType,
200200
UninhabitedType,
201201
UnionType,
202+
flatten_nested_unions,
202203
get_proper_type,
203204
get_proper_types,
204205
is_literal_type,
205206
is_named_instance,
206207
is_optional,
207208
remove_optional,
208209
strip_type,
209-
union_items,
210210
)
211211
from mypy.typetraverser import TypeTraverserVisitor
212212
from mypy.typevars import fill_typevars, fill_typevars_with_any, has_no_typevars
@@ -1464,7 +1464,8 @@ def check_overlapping_op_methods(
14641464
# inheritance. (This is consistent with how we handle overloads: we also
14651465
# do not try checking unsafe overlaps due to multiple inheritance there.)
14661466

1467-
for forward_item in union_items(forward_type):
1467+
for forward_item in flatten_nested_unions([forward_type]):
1468+
forward_item = get_proper_type(forward_item)
14681469
if isinstance(forward_item, CallableType):
14691470
if self.is_unsafe_overlapping_op(forward_item, forward_base, reverse_type):
14701471
self.msg.operator_method_signatures_overlap(
@@ -5320,8 +5321,8 @@ def replay_lookup(new_parent_type: ProperType) -> Optional[Type]:
53205321
# Take each element in the parent union and replay the original lookup procedure
53215322
# to figure out which parents are compatible.
53225323
new_parent_types = []
5323-
for item in union_items(parent_type):
5324-
member_type = replay_lookup(item)
5324+
for item in flatten_nested_unions(parent_type.items):
5325+
member_type = replay_lookup(get_proper_type(item))
53255326
if member_type is None:
53265327
# We were unable to obtain the member type. So, we give up on refining this
53275328
# parent type entirely and abort.

mypy/plugins/ctypes.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
Type,
1919
TypeOfAny,
2020
UnionType,
21+
flatten_nested_unions,
2122
get_proper_type,
22-
union_items,
2323
)
2424

2525

@@ -54,7 +54,8 @@ def _autoconvertible_to_cdata(tp: Type, api: "mypy.plugin.CheckerPluginInterface
5454
# items. This is not quite correct - strictly speaking, only types convertible to *all* of the
5555
# union items should be allowed. This may be worth changing in the future, but the more
5656
# correct algorithm could be too strict to be useful.
57-
for t in union_items(tp):
57+
for t in flatten_nested_unions([tp]):
58+
t = get_proper_type(t)
5859
# Every type can be converted from itself (obviously).
5960
allowed_types.append(t)
6061
if isinstance(t, Instance):
@@ -197,7 +198,8 @@ def array_value_callback(ctx: "mypy.plugin.AttributeContext") -> Type:
197198
et = _get_array_element_type(ctx.type)
198199
if et is not None:
199200
types: List[Type] = []
200-
for tp in union_items(et):
201+
for tp in flatten_nested_unions([et]):
202+
tp = get_proper_type(tp)
201203
if isinstance(tp, AnyType):
202204
types.append(AnyType(TypeOfAny.from_another_any, source_any=tp))
203205
elif isinstance(tp, Instance) and tp.type.fullname == "ctypes.c_char":
@@ -219,7 +221,8 @@ def array_raw_callback(ctx: "mypy.plugin.AttributeContext") -> Type:
219221
et = _get_array_element_type(ctx.type)
220222
if et is not None:
221223
types: List[Type] = []
222-
for tp in union_items(et):
224+
for tp in flatten_nested_unions([et]):
225+
tp = get_proper_type(tp)
223226
if (
224227
isinstance(tp, AnyType)
225228
or isinstance(tp, Instance)

mypy/typeanal.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@
8383
UnpackType,
8484
bad_type_type_item,
8585
callable_with_ellipsis,
86+
flatten_nested_unions,
8687
get_proper_type,
87-
union_items,
8888
)
8989
from mypy.typetraverser import TypeTraverserVisitor
9090

@@ -1739,11 +1739,16 @@ def make_optional_type(t: Type) -> Type:
17391739
is called during semantic analysis and simplification only works during
17401740
type checking.
17411741
"""
1742-
t = get_proper_type(t)
1743-
if isinstance(t, NoneType):
1742+
p_t = get_proper_type(t)
1743+
if isinstance(p_t, NoneType):
17441744
return t
1745-
elif isinstance(t, UnionType):
1746-
items = [item for item in union_items(t) if not isinstance(item, NoneType)]
1745+
elif isinstance(p_t, UnionType):
1746+
# Eagerly expanding aliases is not safe during semantic analysis.
1747+
items = [
1748+
item
1749+
for item in flatten_nested_unions(p_t.items, handle_type_alias_type=False)
1750+
if not isinstance(get_proper_type(item), NoneType)
1751+
]
17471752
return UnionType(items + [NoneType()], t.line, t.column)
17481753
else:
17491754
return UnionType([t, NoneType()], t.line, t.column)

mypy/types.py

+1-16
Original file line numberDiff line numberDiff line change
@@ -2524,7 +2524,7 @@ def relevant_items(self) -> List[Type]:
25242524
if state.strict_optional:
25252525
return self.items
25262526
else:
2527-
return [i for i in get_proper_types(self.items) if not isinstance(i, NoneType)]
2527+
return [i for i in self.items if not isinstance(get_proper_type(i), NoneType)]
25282528

25292529
def serialize(self) -> JsonDict:
25302530
return {".class": "UnionType", "items": [t.serialize() for t in self.items]}
@@ -3178,21 +3178,6 @@ def flatten_nested_unions(
31783178
return flat_items
31793179

31803180

3181-
def union_items(typ: Type) -> List[ProperType]:
3182-
"""Return the flattened items of a union type.
3183-
3184-
For non-union types, return a list containing just the argument.
3185-
"""
3186-
typ = get_proper_type(typ)
3187-
if isinstance(typ, UnionType):
3188-
items = []
3189-
for item in typ.items:
3190-
items.extend(union_items(item))
3191-
return items
3192-
else:
3193-
return [typ]
3194-
3195-
31963181
def invalid_recursive_alias(seen_nodes: Set[mypy.nodes.TypeAlias], target: Type) -> bool:
31973182
"""Flag aliases like A = Union[int, A] (and similar mutual aliases).
31983183

test-data/unit/check-recursive-types.test

+8
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,14 @@ reveal_type(bar(lla)) # N: Revealed type is "__main__.A"
389389
reveal_type(bar(llla)) # N: Revealed type is "__main__.A"
390390
[builtins fixtures/isinstancelist.pyi]
391391

392+
[case testRecursiveAliasesWithOptional]
393+
# flags: --enable-recursive-aliases
394+
from typing import Optional, Sequence
395+
396+
A = Sequence[Optional[A]]
397+
x: A
398+
y: str = x[0] # E: Incompatible types in assignment (expression has type "Optional[A]", variable has type "str")
399+
392400
[case testRecursiveAliasesProhibitBadAliases]
393401
# flags: --enable-recursive-aliases
394402
from typing import Union, Type, List, TypeVar

test-data/unit/check-typeguard.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ def foobar(items: List[object]):
421421
c: List[Bar] = [x for x in items if is_foo(x)] # E: List comprehension has incompatible type List[Foo]; expected List[Bar]
422422
[builtins fixtures/tuple.pyi]
423423

424-
[case testTypeGuardNestedRestrictionUnionIsInstance-xfail]
424+
[case testTypeGuardNestedRestrictionUnionIsInstance]
425425
from typing_extensions import TypeGuard
426426
from typing import Any, List
427427

0 commit comments

Comments
 (0)