Skip to content

Commit f8d71f1

Browse files
authored
Make is_recursive and has_recursive_types() more consistent (#14147)
While working on another PR I noticed that current behavior of `has_recursive_types()` is inconsistent, it returns `False` is there is a recursive type nested as an argument to a generic non-recursive alias. I wasn't able to find any situation where this actually matters, but I think it is better if this function behaves consistently.
1 parent e814c47 commit f8d71f1

File tree

3 files changed

+33
-10
lines changed

3 files changed

+33
-10
lines changed

mypy/test/testtypes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
UninhabitedType,
3232
UnionType,
3333
get_proper_type,
34+
has_recursive_types,
3435
)
3536

3637

@@ -157,6 +158,12 @@ def test_type_alias_expand_all(self) -> None:
157158
[self.fx.a, self.fx.a], Instance(self.fx.std_tuplei, [self.fx.a])
158159
)
159160

161+
def test_recursive_nested_in_non_recursive(self) -> None:
162+
A, _ = self.fx.def_alias_1(self.fx.a)
163+
NA = self.fx.non_rec_alias(Instance(self.fx.gi, [UnboundType("T")]), ["T"], [A])
164+
assert not NA.is_recursive
165+
assert has_recursive_types(NA)
166+
160167
def test_indirection_no_infinite_recursion(self) -> None:
161168
A, _ = self.fx.def_alias_1(self.fx.a)
162169
visitor = TypeIndirectionVisitor()

mypy/test/typefixture.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,13 @@ def def_alias_2(self, base: Instance) -> tuple[TypeAliasType, Type]:
339339
A.alias = AN
340340
return A, target
341341

342-
def non_rec_alias(self, target: Type) -> TypeAliasType:
343-
AN = TypeAlias(target, "__main__.A", -1, -1)
344-
return TypeAliasType(AN, [])
342+
def non_rec_alias(
343+
self, target: Type, alias_tvars: list[str] | None = None, args: list[Type] | None = None
344+
) -> TypeAliasType:
345+
AN = TypeAlias(target, "__main__.A", -1, -1, alias_tvars=alias_tvars)
346+
if args is None:
347+
args = []
348+
return TypeAliasType(AN, args)
345349

346350

347351
class InterfaceTypeFixture(TypeFixture):

mypy/types.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,30 +278,42 @@ def _expand_once(self) -> Type:
278278
self.alias.target, self.alias.alias_tvars, self.args, self.line, self.column
279279
)
280280

281-
def _partial_expansion(self) -> tuple[ProperType, bool]:
281+
def _partial_expansion(self, nothing_args: bool = False) -> tuple[ProperType, bool]:
282282
# Private method mostly for debugging and testing.
283283
unroller = UnrollAliasVisitor(set())
284-
unrolled = self.accept(unroller)
284+
if nothing_args:
285+
alias = self.copy_modified(args=[UninhabitedType()] * len(self.args))
286+
else:
287+
alias = self
288+
unrolled = alias.accept(unroller)
285289
assert isinstance(unrolled, ProperType)
286290
return unrolled, unroller.recursed
287291

288-
def expand_all_if_possible(self) -> ProperType | None:
292+
def expand_all_if_possible(self, nothing_args: bool = False) -> ProperType | None:
289293
"""Attempt a full expansion of the type alias (including nested aliases).
290294
291295
If the expansion is not possible, i.e. the alias is (mutually-)recursive,
292-
return None.
296+
return None. If nothing_args is True, replace all type arguments with an
297+
UninhabitedType() (used to detect recursively defined aliases).
293298
"""
294-
unrolled, recursed = self._partial_expansion()
299+
unrolled, recursed = self._partial_expansion(nothing_args=nothing_args)
295300
if recursed:
296301
return None
297302
return unrolled
298303

299304
@property
300305
def is_recursive(self) -> bool:
306+
"""Whether this type alias is recursive.
307+
308+
Note this doesn't check generic alias arguments, but only if this alias
309+
*definition* is recursive. The property value thus can be cached on the
310+
underlying TypeAlias node. If you want to include all nested types, use
311+
has_recursive_types() function.
312+
"""
301313
assert self.alias is not None, "Unfixed type alias"
302314
is_recursive = self.alias._is_recursive
303315
if is_recursive is None:
304-
is_recursive = self.expand_all_if_possible() is None
316+
is_recursive = self.expand_all_if_possible(nothing_args=True) is None
305317
# We cache the value on the underlying TypeAlias node as an optimization,
306318
# since the value is the same for all instances of the same alias.
307319
self.alias._is_recursive = is_recursive
@@ -3259,7 +3271,7 @@ def __init__(self) -> None:
32593271
super().__init__(any)
32603272

32613273
def visit_type_alias_type(self, t: TypeAliasType) -> bool:
3262-
return t.is_recursive
3274+
return t.is_recursive or self.query_types(t.args)
32633275

32643276

32653277
def has_recursive_types(typ: Type) -> bool:

0 commit comments

Comments
 (0)