diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index d86818adf2a43..aab57350c738c 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -860,7 +860,11 @@ def generate_class_stub( ro_properties, class_info, ) - elif inspect.isclass(value) and self.is_defined_in_module(value): + elif ( + inspect.isclass(value) + and self.is_defined_in_module(value) + and not is_enclosing_class(value, class_info) + ): self.generate_class_stub(attr, value, types, parent_class=class_info) else: attrs.append((attr, value)) @@ -920,6 +924,19 @@ def generate_variable_stub(self, name: str, obj: object, output: list[str]) -> N output.append(f"{name}: {type_str}") +def is_enclosing_class(cls: type, class_info: ClassInfo | None) -> bool: + """Is 'cls' the class we are generating a stub for, or one that encloses it? + + Such an attribute is a back reference (e.g. 'C.C = C', or two classes in the + same module exposing each other). Recursing into it would never terminate. + """ + while class_info is not None: + if class_info.cls is cls: + return True + class_info = class_info.parent + return False + + def method_name_sort_key(name: str) -> tuple[int, str]: """Sort methods in classes in a typical order. diff --git a/mypy/test/teststubgen.py b/mypy/test/teststubgen.py index 605409f995232..802ea743b36e4 100644 --- a/mypy/test/teststubgen.py +++ b/mypy/test/teststubgen.py @@ -953,6 +953,25 @@ class TestClass(TestBaseClass): pass +class SelfReferential: + """A class that exposes itself as one of its own attributes.""" + + +SelfReferential.SelfReferential = SelfReferential # type: ignore[attr-defined] + + +class MutualParent: + """A class that exposes a sibling which points back at it.""" + + +class MutualChild: + """The sibling of MutualParent.""" + + +MutualParent.MutualChild = MutualChild # type: ignore[attr-defined] +MutualChild.MutualParent = MutualParent # type: ignore[attr-defined] + + class StubgencSuite(unittest.TestCase): """Unit tests for stub generation from C modules using introspection. @@ -1004,6 +1023,22 @@ def test_generate_class_stub_no_crash_for_object(self) -> None: assert_equal(gen.get_imports().splitlines(), []) assert_equal(output[0], "class alias:") + def test_generate_class_stub_no_crash_for_self_referential_class(self) -> None: + output: list[str] = [] + mod = ModuleType(SelfReferential.__module__, "") + gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod) + + gen.generate_class_stub("SelfReferential", SelfReferential, output) + assert_equal(output[0], "class SelfReferential:") + + def test_generate_class_stub_no_crash_for_mutually_referential_classes(self) -> None: + output: list[str] = [] + mod = ModuleType(MutualParent.__module__, "") + gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod) + + gen.generate_class_stub("MutualParent", MutualParent, output) + assert_equal(output[0], "class MutualParent:") + def test_generate_class_stub_variable_type_annotation(self) -> None: # This class mimics the stubgen unit test 'testClassVariable' class TestClassVariableCls: