From 270082edab2a89dcbad6ff1b41da411e0b9535de Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Sun, 9 Aug 2026 16:54:12 +0300 Subject: [PATCH] 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 --- mypy/stubgenc.py | 19 ++++++++++++++++++- mypy/test/teststubgen.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) 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: