Skip to content

Commit aa6ac71

Browse files
Yoshanuikabundipawamoy
authored andcommitted
fix: Load Sphinx inventories whose dispname spans multiple lines
When a display name spans multiple lines, both mkdocstrings and Sphinx simply print the newline character and then the continuation. This is because a display name spanning multiple lines is a rare edge case that was not accounted for in either software. However, while Sphinx discards any inventory lines that do not parse (such as the continuation line), mkdocstrings raises an error, causing the entire inventory file to fail to load. This continuation line behavior can be seen in the wild in the OpenEye toolkits inventory file and a few Open Force Field inventory files. These projects' inventories cannot be used with mkdocstrings because of the raised error. Note that in Sphinx, these inventory files are read succesfully, but the continuation lines are discarded, truncating the display name. In theory, a continuation line that by chance did parse correctly would be interpreted by both packages as a new inventory item. This commit appends the continuation line to the previous inventory item display name when it fails to parse. This allows these inventory files to be loaded while preserving the original display name. Note that the theoretical continuation line that parses by chance is not addressed. Round-trip tests and additional Sphinx tests are added to check the new behavior. OpenEye Toolkits inventory file: https://docs.eyesopen.com/toolkits/python/objects.inv Open Force Field Toolkit inventory file: https://docs.openforcefield.org/projects/toolkit/en/stable/objects.inv BespokeFit inventory file: https://docs.openforcefield.org/projects/bespokefit/en/stable/objects.inv
1 parent 3da907c commit aa6ac71

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

src/mkdocstrings/_internal/inventory.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import re
99
import zlib
1010
from textwrap import dedent
11-
from typing import TYPE_CHECKING, BinaryIO
11+
from typing import TYPE_CHECKING, BinaryIO, Literal, overload
1212

1313
if TYPE_CHECKING:
1414
from collections.abc import Collection
@@ -66,11 +66,21 @@ def format_sphinx(self) -> str:
6666
sphinx_item_regex = re.compile(r"^(.+?)\s+(\S+):(\S+)\s+(-?\d+)\s+(\S+)\s*(.*)$")
6767
"""Regex to parse a Sphinx v2 inventory line."""
6868

69+
@overload
6970
@classmethod
70-
def parse_sphinx(cls, line: str) -> InventoryItem:
71+
def parse_sphinx(cls, line: str, *, return_none: Literal[False]) -> InventoryItem: ...
72+
73+
@overload
74+
@classmethod
75+
def parse_sphinx(cls, line: str, *, return_none: Literal[True]) -> InventoryItem | None: ...
76+
77+
@classmethod
78+
def parse_sphinx(cls, line: str, *, return_none: bool = False) -> InventoryItem | None:
7179
"""Parse a line from a Sphinx v2 inventory file and return an `InventoryItem` from it."""
7280
match = cls.sphinx_item_regex.search(line)
7381
if not match:
82+
if return_none:
83+
return None
7484
raise ValueError(line)
7585
name, domain, role, priority, uri, dispname = match.groups()
7686
if uri.endswith("$"):
@@ -167,7 +177,12 @@ def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ())
167177
for _ in range(4):
168178
in_file.readline()
169179
lines = zlib.decompress(in_file.read()).splitlines()
170-
items = [InventoryItem.parse_sphinx(line.decode("utf8")) for line in lines]
180+
items: list[InventoryItem] = list(
181+
filter(
182+
bool, # type: ignore[arg-type]
183+
(InventoryItem.parse_sphinx(line.decode("utf8"), return_none=True) for line in lines),
184+
),
185+
)
171186
if domain_filter:
172187
items = [item for item in items if item.domain in domain_filter]
173188
return cls(items)

tests/test_inventory.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
from mkdocstrings import Inventory, InventoryItem
1313

14-
sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed")
15-
1614

1715
@pytest.mark.parametrize(
1816
"our_inv",
@@ -21,10 +19,13 @@
2119
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url")]),
2220
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#object_path")]),
2321
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#other_anchor")]),
22+
Inventory([InventoryItem(name="o", domain="py", role="obj", uri="u#o", dispname="first line\nsecond line")]),
2423
],
2524
)
2625
def test_sphinx_load_inventory_file(our_inv: Inventory) -> None:
2726
"""Perform the 'live' inventory load test."""
27+
sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed")
28+
2829
buffer = BytesIO(our_inv.format_sphinx())
2930
sphinx_inv = sphinx.InventoryFile.load(buffer, "", join)
3031

@@ -37,6 +38,8 @@ def test_sphinx_load_inventory_file(our_inv: Inventory) -> None:
3738

3839
def test_sphinx_load_mkdocstrings_inventory_file() -> None:
3940
"""Perform the 'live' inventory load test on mkdocstrings own inventory."""
41+
sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed")
42+
4043
mkdocs_config = load_config()
4144
mkdocs_config["plugins"].run_event("startup", command="build", dirty=False)
4245
try:
@@ -53,3 +56,29 @@ def test_sphinx_load_mkdocstrings_inventory_file() -> None:
5356

5457
for item in own_inv.values():
5558
assert item.name in sphinx_inv[f"{item.domain}:{item.role}"]
59+
60+
61+
@pytest.mark.parametrize(
62+
"our_inv",
63+
[
64+
Inventory(),
65+
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url")]),
66+
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#object_path")]),
67+
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#other_anchor")]),
68+
Inventory([InventoryItem(name="o", domain="py", role="obj", uri="u#o", dispname="first line\nsecond line")]),
69+
],
70+
)
71+
def test_mkdocstrings_roundtrip_inventory_file(our_inv: Inventory) -> None:
72+
"""Save some inventory files, then load them in again."""
73+
buffer = BytesIO(our_inv.format_sphinx())
74+
round_tripped = Inventory.parse_sphinx(buffer)
75+
76+
assert our_inv.keys() == round_tripped.keys()
77+
for key, value in our_inv.items():
78+
round_tripped_item = round_tripped[key]
79+
assert round_tripped_item.name == value.name
80+
assert round_tripped_item.domain == value.domain
81+
assert round_tripped_item.role == value.role
82+
assert round_tripped_item.uri == value.uri
83+
assert round_tripped_item.priority == value.priority
84+
assert round_tripped_item.dispname == value.dispname.splitlines()[0]

0 commit comments

Comments
 (0)