Skip to content

Commit 81caff5

Browse files
Yoshanuikabundipawamoy
authored andcommitted
fix: Ignore invalid inventory lines
Previously, inventory items whose `dispname` value contains multiple lines would prevent mkdocstrings from loading the whole inventory file. This change makes mkdocstrings ignore invalid lines in inventories so that the rest of the inventory can still be loaded. 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 too, these inventory files are read succesfully, and 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. 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 0bc4799 commit 81caff5

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

src/mkdocstrings/_internal/inventory.py

Lines changed: 15 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,9 @@ 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] = [
181+
item for line in lines if (item := InventoryItem.parse_sphinx(line.decode("utf8"), return_none=True))
182+
]
171183
if domain_filter:
172184
items = [item for item in items if item.domain in domain_filter]
173185
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)