Skip to content

Commit e5131ba

Browse files
authored
intersphinx: Create an _InventoryItem type (#13248)
1 parent cfb4786 commit e5131ba

File tree

9 files changed

+211
-106
lines changed

9 files changed

+211
-106
lines changed

sphinx/ext/intersphinx/_cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ def inspect_main(argv: list[str], /) -> int:
3737
for key in sorted(inv_data or {}):
3838
print(key)
3939
inv_entries = sorted(inv_data[key].items())
40-
for entry, (_proj, _ver, url_path, display_name) in inv_entries:
40+
for entry, inv_item in inv_entries:
41+
display_name = inv_item.display_name
4142
display_name = display_name * (display_name != '-')
42-
print(f' {entry:<40} {display_name:<40}: {url_path}')
43+
print(f' {entry:<40} {display_name:<40}: {inv_item.uri}')
4344
except ValueError as exc:
4445
print(exc.args[0] % exc.args[1:], file=sys.stderr)
4546
return 1

sphinx/ext/intersphinx/_resolve.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,30 +30,33 @@
3030
from sphinx.domains._domains_container import _DomainsContainer
3131
from sphinx.environment import BuildEnvironment
3232
from sphinx.ext.intersphinx._shared import InventoryName
33-
from sphinx.util.typing import Inventory, InventoryItem, RoleFunction
33+
from sphinx.util.inventory import _InventoryItem
34+
from sphinx.util.typing import Inventory, RoleFunction
3435

3536

3637
def _create_element_from_result(
3738
domain_name: str,
3839
inv_name: InventoryName | None,
39-
data: InventoryItem,
40+
inv_item: _InventoryItem,
4041
node: pending_xref,
4142
contnode: TextElement,
4243
) -> nodes.reference:
43-
proj, version, uri, dispname = data
44+
uri = inv_item.uri
4445
if '://' not in uri and node.get('refdoc'):
4546
# get correct path in case of subdirectories
4647
uri = (_relative_path(Path(), Path(node['refdoc']).parent) / uri).as_posix()
47-
if version:
48-
reftitle = _('(in %s v%s)') % (proj, version)
48+
if inv_item.project_version:
49+
reftitle = _('(in %s v%s)') % (inv_item.project_name, inv_item.project_version)
4950
else:
50-
reftitle = _('(in %s)') % (proj,)
51+
reftitle = _('(in %s)') % (inv_item.project_name,)
5152

5253
newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle)
5354
if node.get('refexplicit'):
5455
# use whatever title was given
5556
newnode.append(contnode)
56-
elif dispname == '-' or (domain_name == 'std' and node['reftype'] == 'keyword'):
57+
elif inv_item.display_name == '-' or (
58+
domain_name == 'std' and node['reftype'] == 'keyword'
59+
):
5760
# use whatever title was given, but strip prefix
5861
title = contnode.astext()
5962
if inv_name is not None and title.startswith(inv_name + ':'):
@@ -66,7 +69,7 @@ def _create_element_from_result(
6669
newnode.append(contnode)
6770
else:
6871
# else use the given display name (used for :ref:)
69-
newnode.append(contnode.__class__(dispname, dispname))
72+
newnode.append(contnode.__class__(inv_item.display_name, inv_item.display_name))
7073
return newnode
7174

7275

sphinx/util/inventory.py

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import posixpath
66
import re
7+
import warnings
78
import zlib
89
from typing import TYPE_CHECKING
910

11+
from sphinx.deprecation import RemovedInSphinx10Warning
1012
from sphinx.locale import __
1113
from sphinx.util import logging
1214

@@ -15,12 +17,12 @@
1517

1618
if TYPE_CHECKING:
1719
import os
18-
from collections.abc import Callable, Sequence
19-
from typing import Protocol
20+
from collections.abc import Callable, Iterator, Sequence
21+
from typing import NoReturn, Protocol
2022

2123
from sphinx.builders import Builder
2224
from sphinx.environment import BuildEnvironment
23-
from sphinx.util.typing import Inventory, InventoryItem
25+
from sphinx.util.typing import Inventory
2426

2527
# Readable file stream for inventory loading
2628
class _SupportsRead(Protocol):
@@ -82,8 +84,12 @@ def _loads_v1(cls, lines: Sequence[str], *, uri: str) -> Inventory:
8284
else:
8385
item_type = f'py:{item_type}'
8486
location += f'#{name}'
85-
inv_item: InventoryItem = projname, version, location, '-'
86-
invdata.setdefault(item_type, {})[name] = inv_item
87+
invdata.setdefault(item_type, {})[name] = _InventoryItem(
88+
project_name=projname,
89+
project_version=version,
90+
uri=location,
91+
display_name='-',
92+
)
8793
return invdata
8894

8995
@classmethod
@@ -148,8 +154,12 @@ def _loads_v2(cls, inv_data: bytes, *, uri: str) -> Inventory:
148154
if location.endswith('$'):
149155
location = location[:-1] + name
150156
location = posixpath.join(uri, location)
151-
inv_item: InventoryItem = projname, version, location, dispname
152-
invdata.setdefault(type, {})[name] = inv_item
157+
invdata.setdefault(type, {})[name] = _InventoryItem(
158+
project_name=projname,
159+
project_version=version,
160+
uri=location,
161+
display_name=dispname,
162+
)
153163
for ambiguity in actual_ambiguities:
154164
logger.info(
155165
__('inventory <%s> contains multiple definitions for %s'),
@@ -194,3 +204,89 @@ def escape(string: str) -> str:
194204
entry = f'{fullname} {domain.name}:{type} {prio} {uri} {dispname}\n'
195205
f.write(compressor.compress(entry.encode()))
196206
f.write(compressor.flush())
207+
208+
209+
class _InventoryItem:
210+
__slots__ = 'project_name', 'project_version', 'uri', 'display_name'
211+
212+
project_name: str
213+
project_version: str
214+
uri: str
215+
display_name: str
216+
217+
def __init__(
218+
self,
219+
*,
220+
project_name: str,
221+
project_version: str,
222+
uri: str,
223+
display_name: str,
224+
) -> None:
225+
object.__setattr__(self, 'project_name', project_name)
226+
object.__setattr__(self, 'project_version', project_version)
227+
object.__setattr__(self, 'uri', uri)
228+
object.__setattr__(self, 'display_name', display_name)
229+
230+
def __repr__(self) -> str:
231+
return (
232+
'_InventoryItem('
233+
f'project_name={self.project_name!r}, '
234+
f'project_version={self.project_version!r}, '
235+
f'uri={self.uri!r}, '
236+
f'display_name={self.display_name!r}'
237+
')'
238+
)
239+
240+
def __eq__(self, other: object) -> bool:
241+
if not isinstance(other, _InventoryItem):
242+
return NotImplemented
243+
return (
244+
self.project_name == other.project_name
245+
and self.project_version == other.project_version
246+
and self.uri == other.uri
247+
and self.display_name == other.display_name
248+
)
249+
250+
def __hash__(self) -> int:
251+
return hash((
252+
self.project_name,
253+
self.project_version,
254+
self.uri,
255+
self.display_name,
256+
))
257+
258+
def __setattr__(self, key: str, value: object) -> NoReturn:
259+
msg = '_InventoryItem is immutable'
260+
raise AttributeError(msg)
261+
262+
def __delattr__(self, key: str) -> NoReturn:
263+
msg = '_InventoryItem is immutable'
264+
raise AttributeError(msg)
265+
266+
def __getstate__(self) -> tuple[str, str, str, str]:
267+
return self.project_name, self.project_version, self.uri, self.display_name
268+
269+
def __setstate__(self, state: tuple[str, str, str, str]) -> None:
270+
project_name, project_version, uri, display_name = state
271+
object.__setattr__(self, 'project_name', project_name)
272+
object.__setattr__(self, 'project_version', project_version)
273+
object.__setattr__(self, 'uri', uri)
274+
object.__setattr__(self, 'display_name', display_name)
275+
276+
def __getitem__(self, key: int | slice) -> str | tuple[str, ...]:
277+
warnings.warn(
278+
'The tuple interface for _InventoryItem objects is deprecated.',
279+
RemovedInSphinx10Warning,
280+
stacklevel=2,
281+
)
282+
tpl = self.project_name, self.project_version, self.uri, self.display_name
283+
return tpl[key]
284+
285+
def __iter__(self) -> Iterator[str]:
286+
warnings.warn(
287+
'The iter() interface for _InventoryItem objects is deprecated.',
288+
RemovedInSphinx10Warning,
289+
stacklevel=2,
290+
)
291+
tpl = self.project_name, self.project_version, self.uri, self.display_name
292+
return iter(tpl)

sphinx/util/typing.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from typing_extensions import TypeIs
2424

2525
from sphinx.application import Sphinx
26+
from sphinx.util.inventory import _InventoryItem
2627

2728
_RestifyMode: TypeAlias = Literal[
2829
'fully-qualified-except-typing',
@@ -110,13 +111,7 @@ def __call__(
110111
TitleGetter: TypeAlias = Callable[[nodes.Node], str]
111112

112113
# inventory data on memory
113-
InventoryItem: TypeAlias = tuple[
114-
str, # project name
115-
str, # project version
116-
str, # URL
117-
str, # display name
118-
]
119-
Inventory: TypeAlias = dict[str, dict[str, InventoryItem]]
114+
Inventory: TypeAlias = dict[str, dict[str, '_InventoryItem']]
120115

121116

122117
class ExtensionMetadata(typing.TypedDict, total=False):

tests/test_builders/test_build_dirhtml.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pytest
88

9-
from sphinx.util.inventory import InventoryFile
9+
from sphinx.util.inventory import InventoryFile, _InventoryItem
1010

1111

1212
@pytest.mark.sphinx('dirhtml', testroot='builder-dirhtml')
@@ -30,28 +30,33 @@ def test_dirhtml(app):
3030
invdata = InventoryFile.load(f, 'path/to', posixpath.join)
3131

3232
assert 'index' in invdata.get('std:doc', {})
33-
assert invdata['std:doc']['index'] == ('Project name not set', '', 'path/to/', '-')
33+
assert invdata['std:doc']['index'] == _InventoryItem(
34+
project_name='Project name not set',
35+
project_version='',
36+
uri='path/to/',
37+
display_name='-',
38+
)
3439

3540
assert 'foo/index' in invdata.get('std:doc', {})
36-
assert invdata['std:doc']['foo/index'] == (
37-
'Project name not set',
38-
'',
39-
'path/to/foo/',
40-
'-',
41+
assert invdata['std:doc']['foo/index'] == _InventoryItem(
42+
project_name='Project name not set',
43+
project_version='',
44+
uri='path/to/foo/',
45+
display_name='-',
4146
)
4247

4348
assert 'index' in invdata.get('std:label', {})
44-
assert invdata['std:label']['index'] == (
45-
'Project name not set',
46-
'',
47-
'path/to/#index',
48-
'-',
49+
assert invdata['std:label']['index'] == _InventoryItem(
50+
project_name='Project name not set',
51+
project_version='',
52+
uri='path/to/#index',
53+
display_name='-',
4954
)
5055

5156
assert 'foo' in invdata.get('std:label', {})
52-
assert invdata['std:label']['foo'] == (
53-
'Project name not set',
54-
'',
55-
'path/to/foo/#foo',
56-
'foo/index',
57+
assert invdata['std:label']['foo'] == _InventoryItem(
58+
project_name='Project name not set',
59+
project_version='',
60+
uri='path/to/foo/#foo',
61+
display_name='foo/index',
5762
)

tests/test_builders/test_build_html.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from sphinx._cli.util.errors import strip_escape_sequences
1313
from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
1414
from sphinx.errors import ConfigError
15-
from sphinx.util.inventory import InventoryFile
15+
from sphinx.util.inventory import InventoryFile, _InventoryItem
1616

1717
from tests.test_builders.xpath_data import FIGURE_CAPTION
1818
from tests.test_builders.xpath_util import check_xpath
@@ -233,36 +233,36 @@ def test_html_inventory(app):
233233
'genindex',
234234
'search',
235235
}
236-
assert invdata['std:label']['modindex'] == (
237-
'Project name not set',
238-
'',
239-
'https://www.google.com/py-modindex.html',
240-
'Module Index',
236+
assert invdata['std:label']['modindex'] == _InventoryItem(
237+
project_name='Project name not set',
238+
project_version='',
239+
uri='https://www.google.com/py-modindex.html',
240+
display_name='Module Index',
241241
)
242-
assert invdata['std:label']['py-modindex'] == (
243-
'Project name not set',
244-
'',
245-
'https://www.google.com/py-modindex.html',
246-
'Python Module Index',
242+
assert invdata['std:label']['py-modindex'] == _InventoryItem(
243+
project_name='Project name not set',
244+
project_version='',
245+
uri='https://www.google.com/py-modindex.html',
246+
display_name='Python Module Index',
247247
)
248-
assert invdata['std:label']['genindex'] == (
249-
'Project name not set',
250-
'',
251-
'https://www.google.com/genindex.html',
252-
'Index',
248+
assert invdata['std:label']['genindex'] == _InventoryItem(
249+
project_name='Project name not set',
250+
project_version='',
251+
uri='https://www.google.com/genindex.html',
252+
display_name='Index',
253253
)
254-
assert invdata['std:label']['search'] == (
255-
'Project name not set',
256-
'',
257-
'https://www.google.com/search.html',
258-
'Search Page',
254+
assert invdata['std:label']['search'] == _InventoryItem(
255+
project_name='Project name not set',
256+
project_version='',
257+
uri='https://www.google.com/search.html',
258+
display_name='Search Page',
259259
)
260260
assert set(invdata['std:doc'].keys()) == {'index'}
261-
assert invdata['std:doc']['index'] == (
262-
'Project name not set',
263-
'',
264-
'https://www.google.com/index.html',
265-
'The basic Sphinx documentation for testing',
261+
assert invdata['std:doc']['index'] == _InventoryItem(
262+
project_name='Project name not set',
263+
project_version='',
264+
uri='https://www.google.com/index.html',
265+
display_name='The basic Sphinx documentation for testing',
266266
)
267267

268268

tests/test_extensions/test_ext_intersphinx.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929
from sphinx.ext.intersphinx._resolve import missing_reference
3030
from sphinx.ext.intersphinx._shared import _IntersphinxProject
31+
from sphinx.util.inventory import _InventoryItem
3132

3233
from tests.test_util.intersphinx_data import (
3334
INVENTORY_V2,
@@ -155,11 +156,11 @@ def test_missing_reference(tmp_path, app):
155156
load_mappings(app)
156157
inv = app.env.intersphinx_inventory
157158

158-
assert inv['py:module']['module2'] == (
159-
'foo',
160-
'2.0',
161-
'https://docs.python.org/foo.html#module-module2',
162-
'-',
159+
assert inv['py:module']['module2'] == _InventoryItem(
160+
project_name='foo',
161+
project_version='2.0',
162+
uri='https://docs.python.org/foo.html#module-module2',
163+
display_name='-',
163164
)
164165

165166
# check resolution when a target is found

0 commit comments

Comments
 (0)