Skip to content

Commit

Permalink
python/apigen: Fix inheritance of sibling/alias members (#358)
Browse files Browse the repository at this point in the history
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``..
  • Loading branch information
jbms committed Jul 3, 2024
1 parent 6a933e6 commit b21e566
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 33 deletions.
82 changes: 51 additions & 31 deletions sphinx_immaterial/apidoc/python/apigen.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ class _ApiEntityMemberReference(NamedTuple):
name: str
canonical_object_name: str
parent_canonical_object_name: str
inherited: bool = False
inherited: bool
siblings: List["_ApiEntityMemberReference"]


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

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

primary_entity: bool = True
"""Indicates if this is the primary sibling and should be documented."""
Expand Down Expand Up @@ -593,18 +596,24 @@ def object_description_transform(
content = entity.content
options = dict(entity.options)
options["nonodeid"] = ""
all_entities_and_members = [
(entity, member),
*[
(
api_data.entities[sibling_member.canonical_object_name],
sibling_member if member is not None else None,
)
for sibling_member in (entity.siblings or [])
],
]
all_members: List[Optional[_ApiEntityMemberReference]]
if member is not None:
all_members = cast(
List[Optional[_ApiEntityMemberReference]], [member] + member.siblings
)
all_entities = [
api_data.entities[cast(_ApiEntityMemberReference, m).canonical_object_name]
for m in all_members
]
else:
all_entities = [
entity,
*(api_data.entities[s] for s in (entity.siblings or {})),
]
all_members = [None] * len(all_entities)

options["object-ids"] = json.dumps(
[e.object_name for e, _ in all_entities_and_members for _ in e.signatures]
[e.object_name for e in all_entities for _ in e.signatures]
)
if summary:
content = _summarize_rst_content(content)
Expand All @@ -627,7 +636,7 @@ def object_description_transform(
)

signatures: List[str] = []
for e, m in all_entities_and_members:
for e, m in zip(all_entities, all_members):
name = api_data.get_name_for_signature(e, m)
signatures.extend(name + sig for sig in e.signatures)

Expand Down Expand Up @@ -666,7 +675,7 @@ def object_description_transform(
if not summary:
py = cast(PythonDomain, env.get_domain("py"))

for e, _ in all_entities_and_members:
for e in all_entities:
py.objects.setdefault(
e.canonical_object_name,
py.objects[e.object_name]._replace(aliased=True),
Expand Down Expand Up @@ -1472,11 +1481,15 @@ def __init__(
def collect_entity_recursively(
self,
entry: _MemberDocumenterEntry,
primary_sibling: Optional[_ApiEntity] = None,
primary_entity: Optional[_ApiEntity] = None,
) -> str:
canonical_full_name = None
if isinstance(entry.documenter, sphinx.ext.autodoc.ClassDocumenter):
canonical_full_name = entry.documenter.get_canonical_fullname()
elif isinstance(entry.documenter, sphinx.ext.autodoc.FunctionDocumenter):
canonical_full_name = sphinx.ext.autodoc.ClassDocumenter.get_canonical_fullname(
entry.documenter # type: ignore[arg-type]
)
if canonical_full_name is None:
canonical_full_name = f"{entry.parent_canonical_full_name}.{entry.name}"

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

if primary_sibling is None:
if primary_entity is None:
rst_strings = docutils.statemachine.StringList()
entry.documenter.directive.result = rst_strings
_prepare_documenter_docstring(entry)
Expand All @@ -1514,11 +1527,11 @@ def document_members(*args, **kwargs):
options = split_result.options
content = split_result.content
else:
group_name = primary_sibling.group_name
order = primary_sibling.order
directive = primary_sibling.directive
options = primary_sibling.options
content = primary_sibling.content
group_name = primary_entity.group_name
order = primary_entity.order
directive = primary_entity.directive
options = primary_entity.options
content = primary_entity.content

base_classes: Optional[List[str]] = None

Expand Down Expand Up @@ -1570,6 +1583,7 @@ def document_members(*args, **kwargs):
subscript=entry.subscript,
overload_id=overload_id or "",
base_classes=base_classes,
primary_entity=primary_entity is None,
)

self.entities[canonical_object_name] = entity
Expand All @@ -1579,8 +1593,8 @@ def document_members(*args, **kwargs):
entry.documenter,
canonical_object_name=canonical_object_name,
)
if primary_sibling is None
else primary_sibling.members
if primary_entity is None
else primary_entity.members
)

return canonical_object_name
Expand Down Expand Up @@ -1613,30 +1627,36 @@ def collect_documenter_members(
Tuple[Any, _ApiEntityMemberReference]
] = None
primary_sibling_entity: Optional[_ApiEntity] = None
primary_sibling_member: Optional[_ApiEntityMemberReference] = None
if obj is not None:
obj_and_primary_sibling_member = object_to_api_entity_member_map.get(
id(obj)
)
if obj_and_primary_sibling_member is not None:
primary_sibling_member = obj_and_primary_sibling_member[1]
primary_sibling_entity = self.entities[
obj_and_primary_sibling_member[1].canonical_object_name
primary_sibling_member.canonical_object_name
]
member_canonical_object_name = self.collect_entity_recursively(
entry, primary_sibling=primary_sibling_entity
entry, primary_entity=primary_sibling_entity
)
child = self.entities[member_canonical_object_name]
member = _ApiEntityMemberReference(
name=entry.name,
parent_canonical_object_name=canonical_object_name,
canonical_object_name=member_canonical_object_name,
inherited=entry.is_inherited,
siblings=[],
)

if primary_sibling_entity is not None:
child.primary_entity = False
if primary_sibling_member is not None:
primary_sibling_member.siblings.append(member)
assert primary_sibling_entity is not None
if primary_sibling_entity.siblings is None:
primary_sibling_entity.siblings = []
primary_sibling_entity.siblings.append(member)
primary_sibling_entity.siblings = {}
primary_sibling_entity.siblings.setdefault(
member_canonical_object_name, True
)
else:
if obj is not None:
object_to_api_entity_member_map[id(obj)] = (obj, member)
Expand Down
15 changes: 13 additions & 2 deletions tests/python_apigen_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,18 @@ def test_pure_python_property(apigen_make_app):
assert entity.primary_entity
assert entity.siblings is not None
assert len(entity.siblings) == 1
assert entity.siblings[0].name == "bar"
options = entity.options
assert list(entity.siblings) == [f"{testmod}.Example.bar"]

options = entity.options
assert options["type"] == "int"

entity = data.entities[f"{testmod}.InheritsFromExample"]
assert len(entity.members) == 2
member = entity.members[0]
assert member.name == "foo"
assert len(member.siblings) == 0

member = entity.members[1]
assert member.name == "baz"
assert len(member.siblings) == 1
assert member.siblings[0].name == "bar"
6 changes: 6 additions & 0 deletions tests/python_apigen_test_modules/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ def foo(self) -> int:
return 42

bar = foo


class InheritsFromExample(Example):
foo = "abc" # type: ignore[assignment]

baz = Example.bar

0 comments on commit b21e566

Please sign in to comment.