Skip to content

Commit

Permalink
Make apidoc.python robust to invalid signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
jbms committed Jul 20, 2024
1 parent df43833 commit dd125ad
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 21 deletions.
1 change: 1 addition & 0 deletions requirements/dev-mypy.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pytest
pydantic>=2.0
pydantic-extra-types
sphinx>=4.5
-r black.txt
17 changes: 12 additions & 5 deletions sphinx_immaterial/apidoc/python/object_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,15 @@ def strip_object_entry_node_id(existing_node_id: str, object_id: str):
noindexentry = "noindexentry" in self.options

for signode in cast(List[docutils.nodes.Element], signodes):
modname = signode["module"]
fullname = signode["fullname"]
# If the Python signature could not be parsed by Sphinx, `module`
# and `fullname` won't be set. Such signatures should be gracefully
# ignored.
modname = signode.get("module", False)
if modname is False:
continue
fullname = signode.get("fullname", False)
if fullname is False:
continue

Check warning on line 109 in sphinx_immaterial/apidoc/python/object_ids.py

View check run for this annotation

Codecov / codecov/patch

sphinx_immaterial/apidoc/python/object_ids.py#L109

Added line #L109 was not covered by tests
symbol = (modname + "." if modname else "") + fullname
if nonodeid and signode["ids"]:
orig_node_id = signode["ids"][0]
Expand All @@ -116,9 +123,9 @@ def strip_object_entry_node_id(existing_node_id: str, object_id: str):
if new_entry[2] == orig_node_id:
new_entry[2] = ""
new_entries.append(tuple(new_entry))
cast(docutils.nodes.Element, self.indexnode)["entries"] = (
new_entries
)
cast(docutils.nodes.Element, self.indexnode)[

Check warning on line 126 in sphinx_immaterial/apidoc/python/object_ids.py

View check run for this annotation

Codecov / codecov/patch

sphinx_immaterial/apidoc/python/object_ids.py#L126

Added line #L126 was not covered by tests
"entries"
] = new_entries

PyObject.after_content = after_content # type: ignore[assignment]

Expand Down
16 changes: 11 additions & 5 deletions sphinx_immaterial/apidoc/python/parameter_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,16 +469,22 @@ def after_content(self: PyObject) -> None:
obj_desc = cast(
sphinx.addnodes.desc_content, getattr(self, "contentnode")
).parent
signodes = obj_desc.children[:-1]
signodes = cast(list[sphinx.addnodes.desc_signature], obj_desc.children[:-1])

noindex = "noindex" in self.options

symbols = []
for signode in cast(List[docutils.nodes.Element], signodes):
modname = signode["module"]
fullname = signode["fullname"]
valid_signodes: list[sphinx.addnodes.desc_signature] = []
for signode in signodes:
modname = signode.get("module", False)
if modname is False:
continue
fullname = signode.get("fullname", False)
if fullname is False:
continue

Check warning on line 484 in sphinx_immaterial/apidoc/python/parameter_objects.py

View check run for this annotation

Codecov / codecov/patch

sphinx_immaterial/apidoc/python/parameter_objects.py#L484

Added line #L484 was not covered by tests
symbol = (modname + "." if modname else "") + fullname
symbols.append(symbol)
valid_signodes.append(signode)

if not symbols:
return
Expand All @@ -492,7 +498,7 @@ def after_content(self: PyObject) -> None:
_cross_link_parameters(
directive=self,
app=self.env.app,
signodes=cast(List[sphinx.addnodes.desc_signature], signodes),
signodes=valid_signodes,
content=getattr(self, "contentnode"),
symbols=function_symbols,
noindex=noindex,
Expand Down
26 changes: 15 additions & 11 deletions sphinx_immaterial/apidoc/python/synopses.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ def _add_synopsis(
obj = self.objects.get(name)
if obj is None:
return
refnode["reftitle"] = (
object_description_options.format_object_description_tooltip(
env,
object_description_options.get_object_description_options(
env, self.name, obj.objtype
),
base_title=name,
synopsis=self.data["synopses"].get(name),
)
refnode[
"reftitle"
] = object_description_options.format_object_description_tooltip(
env,
object_description_options.get_object_description_options(
env, self.name, obj.objtype
),
base_title=name,
synopsis=self.data["synopses"].get(name),
)

orig_resolve_xref = PythonDomain.resolve_xref
Expand Down Expand Up @@ -100,8 +100,12 @@ def after_content(self: PyObject) -> None:

symbols = []
for signode in cast(List[docutils.nodes.Element], signodes):
modname = signode["module"]
fullname = signode["fullname"]
modname = signode.get("module", False)
if modname is False:
continue
fullname = signode.get("fullname", False)
if fullname is False:
continue

Check warning on line 108 in sphinx_immaterial/apidoc/python/synopses.py

View check run for this annotation

Codecov / codecov/patch

sphinx_immaterial/apidoc/python/synopses.py#L108

Added line #L108 was not covered by tests
symbol = (modname + "." if modname else "") + fullname
symbols.append(symbol)

Expand Down
22 changes: 22 additions & 0 deletions tests/python_object_ids_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,25 @@ def test_nonodeid(immaterial_make_app):

assert nodes[0]["ids"] == ["Bar"]
assert nodes[1]["ids"] == []


def test_invalid_signature(immaterial_make_app):
app = immaterial_make_app(
files={
"index.rst": """
.. py:function:: bar(
"""
},
)

app.build()

assert not app._warning.getvalue()

doc = app.env.get_and_resolve_doctree("index", app.builder)

nodes = list(doc.findall(condition=sphinx.addnodes.desc_signature))

assert len(nodes) == 1

assert nodes[0]["ids"] == []

0 comments on commit dd125ad

Please sign in to comment.