Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion mypy/stubgenc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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.

Expand Down
35 changes: 35 additions & 0 deletions mypy/test/teststubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
Loading