Skip to content

Commit 270082e

Browse files
committed
stubgen: don't recurse forever on a class that refers back to itself
generate_class_stub() recurses into every attribute whose value is a class defined in the same module, so a class that exposes itself (C.C = C), or a pair of classes that expose each other, makes it recurse until Python raises RecursionError. Skip an attribute whose value is the class currently being generated or any class enclosing it: such an attribute is a back reference, not a nested class. Fixes #11989
1 parent 5bb72b7 commit 270082e

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

mypy/stubgenc.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,11 @@ def generate_class_stub(
860860
ro_properties,
861861
class_info,
862862
)
863-
elif inspect.isclass(value) and self.is_defined_in_module(value):
863+
elif (
864+
inspect.isclass(value)
865+
and self.is_defined_in_module(value)
866+
and not is_enclosing_class(value, class_info)
867+
):
864868
self.generate_class_stub(attr, value, types, parent_class=class_info)
865869
else:
866870
attrs.append((attr, value))
@@ -920,6 +924,19 @@ def generate_variable_stub(self, name: str, obj: object, output: list[str]) -> N
920924
output.append(f"{name}: {type_str}")
921925

922926

927+
def is_enclosing_class(cls: type, class_info: ClassInfo | None) -> bool:
928+
"""Is 'cls' the class we are generating a stub for, or one that encloses it?
929+
930+
Such an attribute is a back reference (e.g. 'C.C = C', or two classes in the
931+
same module exposing each other). Recursing into it would never terminate.
932+
"""
933+
while class_info is not None:
934+
if class_info.cls is cls:
935+
return True
936+
class_info = class_info.parent
937+
return False
938+
939+
923940
def method_name_sort_key(name: str) -> tuple[int, str]:
924941
"""Sort methods in classes in a typical order.
925942

mypy/test/teststubgen.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,25 @@ class TestClass(TestBaseClass):
953953
pass
954954

955955

956+
class SelfReferential:
957+
"""A class that exposes itself as one of its own attributes."""
958+
959+
960+
SelfReferential.SelfReferential = SelfReferential # type: ignore[attr-defined]
961+
962+
963+
class MutualParent:
964+
"""A class that exposes a sibling which points back at it."""
965+
966+
967+
class MutualChild:
968+
"""The sibling of MutualParent."""
969+
970+
971+
MutualParent.MutualChild = MutualChild # type: ignore[attr-defined]
972+
MutualChild.MutualParent = MutualParent # type: ignore[attr-defined]
973+
974+
956975
class StubgencSuite(unittest.TestCase):
957976
"""Unit tests for stub generation from C modules using introspection.
958977
@@ -1004,6 +1023,22 @@ def test_generate_class_stub_no_crash_for_object(self) -> None:
10041023
assert_equal(gen.get_imports().splitlines(), [])
10051024
assert_equal(output[0], "class alias:")
10061025

1026+
def test_generate_class_stub_no_crash_for_self_referential_class(self) -> None:
1027+
output: list[str] = []
1028+
mod = ModuleType(SelfReferential.__module__, "")
1029+
gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod)
1030+
1031+
gen.generate_class_stub("SelfReferential", SelfReferential, output)
1032+
assert_equal(output[0], "class SelfReferential:")
1033+
1034+
def test_generate_class_stub_no_crash_for_mutually_referential_classes(self) -> None:
1035+
output: list[str] = []
1036+
mod = ModuleType(MutualParent.__module__, "")
1037+
gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod)
1038+
1039+
gen.generate_class_stub("MutualParent", MutualParent, output)
1040+
assert_equal(output[0], "class MutualParent:")
1041+
10071042
def test_generate_class_stub_variable_type_annotation(self) -> None:
10081043
# This class mimics the stubgen unit test 'testClassVariable'
10091044
class TestClassVariableCls:

0 commit comments

Comments
 (0)