Skip to content

Commit b21e566

Browse files
authored
python/apigen: Fix inheritance of sibling/alias members (#358)
Previously, if class ``A`` defined a member ``p`` with an alias ``q``, and then ``B`` inherits from ``A``, the ``q`` alias would be listed twice, since it would be added when processing the members of ``A`` and then again when processing the members of ``B``..
1 parent 6a933e6 commit b21e566

File tree

3 files changed

+70
-33
lines changed

3 files changed

+70
-33
lines changed

sphinx_immaterial/apidoc/python/apigen.py

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,8 @@ class _ApiEntityMemberReference(NamedTuple):
310310
name: str
311311
canonical_object_name: str
312312
parent_canonical_object_name: str
313-
inherited: bool = False
313+
inherited: bool
314+
siblings: List["_ApiEntityMemberReference"]
314315

315316

316317
@dataclasses.dataclass
@@ -357,8 +358,10 @@ def overload_suffix(self) -> str:
357358
base_classes: Optional[List[str]] = None
358359
"""List of base classes, as rST cross references."""
359360

360-
siblings: Optional[List[_ApiEntityMemberReference]] = None
361-
"""List of siblings that should be documented as aliases."""
361+
siblings: Optional[Dict[str, bool]] = None
362+
"""List of siblings that should be documented as aliases.
363+
364+
The key is the canonical_object_name of the sibling. The value is always `True`."""
362365

363366
primary_entity: bool = True
364367
"""Indicates if this is the primary sibling and should be documented."""
@@ -593,18 +596,24 @@ def object_description_transform(
593596
content = entity.content
594597
options = dict(entity.options)
595598
options["nonodeid"] = ""
596-
all_entities_and_members = [
597-
(entity, member),
598-
*[
599-
(
600-
api_data.entities[sibling_member.canonical_object_name],
601-
sibling_member if member is not None else None,
602-
)
603-
for sibling_member in (entity.siblings or [])
604-
],
605-
]
599+
all_members: List[Optional[_ApiEntityMemberReference]]
600+
if member is not None:
601+
all_members = cast(
602+
List[Optional[_ApiEntityMemberReference]], [member] + member.siblings
603+
)
604+
all_entities = [
605+
api_data.entities[cast(_ApiEntityMemberReference, m).canonical_object_name]
606+
for m in all_members
607+
]
608+
else:
609+
all_entities = [
610+
entity,
611+
*(api_data.entities[s] for s in (entity.siblings or {})),
612+
]
613+
all_members = [None] * len(all_entities)
614+
606615
options["object-ids"] = json.dumps(
607-
[e.object_name for e, _ in all_entities_and_members for _ in e.signatures]
616+
[e.object_name for e in all_entities for _ in e.signatures]
608617
)
609618
if summary:
610619
content = _summarize_rst_content(content)
@@ -627,7 +636,7 @@ def object_description_transform(
627636
)
628637

629638
signatures: List[str] = []
630-
for e, m in all_entities_and_members:
639+
for e, m in zip(all_entities, all_members):
631640
name = api_data.get_name_for_signature(e, m)
632641
signatures.extend(name + sig for sig in e.signatures)
633642

@@ -666,7 +675,7 @@ def object_description_transform(
666675
if not summary:
667676
py = cast(PythonDomain, env.get_domain("py"))
668677

669-
for e, _ in all_entities_and_members:
678+
for e in all_entities:
670679
py.objects.setdefault(
671680
e.canonical_object_name,
672681
py.objects[e.object_name]._replace(aliased=True),
@@ -1472,11 +1481,15 @@ def __init__(
14721481
def collect_entity_recursively(
14731482
self,
14741483
entry: _MemberDocumenterEntry,
1475-
primary_sibling: Optional[_ApiEntity] = None,
1484+
primary_entity: Optional[_ApiEntity] = None,
14761485
) -> str:
14771486
canonical_full_name = None
14781487
if isinstance(entry.documenter, sphinx.ext.autodoc.ClassDocumenter):
14791488
canonical_full_name = entry.documenter.get_canonical_fullname()
1489+
elif isinstance(entry.documenter, sphinx.ext.autodoc.FunctionDocumenter):
1490+
canonical_full_name = sphinx.ext.autodoc.ClassDocumenter.get_canonical_fullname(
1491+
entry.documenter # type: ignore[arg-type]
1492+
)
14801493
if canonical_full_name is None:
14811494
canonical_full_name = f"{entry.parent_canonical_full_name}.{entry.name}"
14821495

@@ -1492,7 +1505,7 @@ def collect_entity_recursively(
14921505
):
14931506
logger.warning("Unspecified overload id: %s", canonical_object_name)
14941507

1495-
if primary_sibling is None:
1508+
if primary_entity is None:
14961509
rst_strings = docutils.statemachine.StringList()
14971510
entry.documenter.directive.result = rst_strings
14981511
_prepare_documenter_docstring(entry)
@@ -1514,11 +1527,11 @@ def document_members(*args, **kwargs):
15141527
options = split_result.options
15151528
content = split_result.content
15161529
else:
1517-
group_name = primary_sibling.group_name
1518-
order = primary_sibling.order
1519-
directive = primary_sibling.directive
1520-
options = primary_sibling.options
1521-
content = primary_sibling.content
1530+
group_name = primary_entity.group_name
1531+
order = primary_entity.order
1532+
directive = primary_entity.directive
1533+
options = primary_entity.options
1534+
content = primary_entity.content
15221535

15231536
base_classes: Optional[List[str]] = None
15241537

@@ -1570,6 +1583,7 @@ def document_members(*args, **kwargs):
15701583
subscript=entry.subscript,
15711584
overload_id=overload_id or "",
15721585
base_classes=base_classes,
1586+
primary_entity=primary_entity is None,
15731587
)
15741588

15751589
self.entities[canonical_object_name] = entity
@@ -1579,8 +1593,8 @@ def document_members(*args, **kwargs):
15791593
entry.documenter,
15801594
canonical_object_name=canonical_object_name,
15811595
)
1582-
if primary_sibling is None
1583-
else primary_sibling.members
1596+
if primary_entity is None
1597+
else primary_entity.members
15841598
)
15851599

15861600
return canonical_object_name
@@ -1613,30 +1627,36 @@ def collect_documenter_members(
16131627
Tuple[Any, _ApiEntityMemberReference]
16141628
] = None
16151629
primary_sibling_entity: Optional[_ApiEntity] = None
1630+
primary_sibling_member: Optional[_ApiEntityMemberReference] = None
16161631
if obj is not None:
16171632
obj_and_primary_sibling_member = object_to_api_entity_member_map.get(
16181633
id(obj)
16191634
)
16201635
if obj_and_primary_sibling_member is not None:
1636+
primary_sibling_member = obj_and_primary_sibling_member[1]
16211637
primary_sibling_entity = self.entities[
1622-
obj_and_primary_sibling_member[1].canonical_object_name
1638+
primary_sibling_member.canonical_object_name
16231639
]
16241640
member_canonical_object_name = self.collect_entity_recursively(
1625-
entry, primary_sibling=primary_sibling_entity
1641+
entry, primary_entity=primary_sibling_entity
16261642
)
16271643
child = self.entities[member_canonical_object_name]
16281644
member = _ApiEntityMemberReference(
16291645
name=entry.name,
16301646
parent_canonical_object_name=canonical_object_name,
16311647
canonical_object_name=member_canonical_object_name,
16321648
inherited=entry.is_inherited,
1649+
siblings=[],
16331650
)
16341651

1635-
if primary_sibling_entity is not None:
1636-
child.primary_entity = False
1652+
if primary_sibling_member is not None:
1653+
primary_sibling_member.siblings.append(member)
1654+
assert primary_sibling_entity is not None
16371655
if primary_sibling_entity.siblings is None:
1638-
primary_sibling_entity.siblings = []
1639-
primary_sibling_entity.siblings.append(member)
1656+
primary_sibling_entity.siblings = {}
1657+
primary_sibling_entity.siblings.setdefault(
1658+
member_canonical_object_name, True
1659+
)
16401660
else:
16411661
if obj is not None:
16421662
object_to_api_entity_member_map[id(obj)] = (obj, member)

tests/python_apigen_test.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,18 @@ def test_pure_python_property(apigen_make_app):
142142
assert entity.primary_entity
143143
assert entity.siblings is not None
144144
assert len(entity.siblings) == 1
145-
assert entity.siblings[0].name == "bar"
146-
options = entity.options
145+
assert list(entity.siblings) == [f"{testmod}.Example.bar"]
147146

147+
options = entity.options
148148
assert options["type"] == "int"
149+
150+
entity = data.entities[f"{testmod}.InheritsFromExample"]
151+
assert len(entity.members) == 2
152+
member = entity.members[0]
153+
assert member.name == "foo"
154+
assert len(member.siblings) == 0
155+
156+
member = entity.members[1]
157+
assert member.name == "baz"
158+
assert len(member.siblings) == 1
159+
assert member.siblings[0].name == "bar"

tests/python_apigen_test_modules/property.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ def foo(self) -> int:
44
return 42
55

66
bar = foo
7+
8+
9+
class InheritsFromExample(Example):
10+
foo = "abc" # type: ignore[assignment]
11+
12+
baz = Example.bar

0 commit comments

Comments
 (0)