diff --git a/requirements/dev-mypy.txt b/requirements/dev-mypy.txt index 4fc6daeb..b2a8f056 100644 --- a/requirements/dev-mypy.txt +++ b/requirements/dev-mypy.txt @@ -10,3 +10,4 @@ pytest pydantic>=2.0 pydantic-extra-types sphinx>=4.5 +-r black.txt diff --git a/sphinx_immaterial/apidoc/python/object_ids.py b/sphinx_immaterial/apidoc/python/object_ids.py index e6435374..0f95c788 100644 --- a/sphinx_immaterial/apidoc/python/object_ids.py +++ b/sphinx_immaterial/apidoc/python/object_ids.py @@ -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 symbol = (modname + "." if modname else "") + fullname if nonodeid and signode["ids"]: orig_node_id = signode["ids"][0] @@ -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)[ + "entries" + ] = new_entries PyObject.after_content = after_content # type: ignore[assignment] diff --git a/sphinx_immaterial/apidoc/python/parameter_objects.py b/sphinx_immaterial/apidoc/python/parameter_objects.py index 8543e786..3c8bce8a 100644 --- a/sphinx_immaterial/apidoc/python/parameter_objects.py +++ b/sphinx_immaterial/apidoc/python/parameter_objects.py @@ -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 symbol = (modname + "." if modname else "") + fullname symbols.append(symbol) + valid_signodes.append(signode) if not symbols: return @@ -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, diff --git a/sphinx_immaterial/apidoc/python/synopses.py b/sphinx_immaterial/apidoc/python/synopses.py index b7f262af..c155b1b6 100644 --- a/sphinx_immaterial/apidoc/python/synopses.py +++ b/sphinx_immaterial/apidoc/python/synopses.py @@ -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 @@ -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 symbol = (modname + "." if modname else "") + fullname symbols.append(symbol) diff --git a/tests/python_object_ids_test.py b/tests/python_object_ids_test.py index 98871639..c9c9fb57 100644 --- a/tests/python_object_ids_test.py +++ b/tests/python_object_ids_test.py @@ -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"] == []