intersphinx: Create an `_InventoryItem` type (#13248)

This commit is contained in:
Adam Turner
2025-01-17 00:41:11 +00:00
committed by GitHub
parent cfb47865d6
commit e5131ba1bd
9 changed files with 211 additions and 106 deletions

View File

@@ -37,9 +37,10 @@ def inspect_main(argv: list[str], /) -> int:
for key in sorted(inv_data or {}):
print(key)
inv_entries = sorted(inv_data[key].items())
for entry, (_proj, _ver, url_path, display_name) in inv_entries:
for entry, inv_item in inv_entries:
display_name = inv_item.display_name
display_name = display_name * (display_name != '-')
print(f' {entry:<40} {display_name:<40}: {url_path}')
print(f' {entry:<40} {display_name:<40}: {inv_item.uri}')
except ValueError as exc:
print(exc.args[0] % exc.args[1:], file=sys.stderr)
return 1

View File

@@ -30,30 +30,33 @@ if TYPE_CHECKING:
from sphinx.domains._domains_container import _DomainsContainer
from sphinx.environment import BuildEnvironment
from sphinx.ext.intersphinx._shared import InventoryName
from sphinx.util.typing import Inventory, InventoryItem, RoleFunction
from sphinx.util.inventory import _InventoryItem
from sphinx.util.typing import Inventory, RoleFunction
def _create_element_from_result(
domain_name: str,
inv_name: InventoryName | None,
data: InventoryItem,
inv_item: _InventoryItem,
node: pending_xref,
contnode: TextElement,
) -> nodes.reference:
proj, version, uri, dispname = data
uri = inv_item.uri
if '://' not in uri and node.get('refdoc'):
# get correct path in case of subdirectories
uri = (_relative_path(Path(), Path(node['refdoc']).parent) / uri).as_posix()
if version:
reftitle = _('(in %s v%s)') % (proj, version)
if inv_item.project_version:
reftitle = _('(in %s v%s)') % (inv_item.project_name, inv_item.project_version)
else:
reftitle = _('(in %s)') % (proj,)
reftitle = _('(in %s)') % (inv_item.project_name,)
newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle)
if node.get('refexplicit'):
# use whatever title was given
newnode.append(contnode)
elif dispname == '-' or (domain_name == 'std' and node['reftype'] == 'keyword'):
elif inv_item.display_name == '-' or (
domain_name == 'std' and node['reftype'] == 'keyword'
):
# use whatever title was given, but strip prefix
title = contnode.astext()
if inv_name is not None and title.startswith(inv_name + ':'):
@@ -66,7 +69,7 @@ def _create_element_from_result(
newnode.append(contnode)
else:
# else use the given display name (used for :ref:)
newnode.append(contnode.__class__(dispname, dispname))
newnode.append(contnode.__class__(inv_item.display_name, inv_item.display_name))
return newnode

View File

@@ -4,9 +4,11 @@ from __future__ import annotations
import posixpath
import re
import warnings
import zlib
from typing import TYPE_CHECKING
from sphinx.deprecation import RemovedInSphinx10Warning
from sphinx.locale import __
from sphinx.util import logging
@@ -15,12 +17,12 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING:
import os
from collections.abc import Callable, Sequence
from typing import Protocol
from collections.abc import Callable, Iterator, Sequence
from typing import NoReturn, Protocol
from sphinx.builders import Builder
from sphinx.environment import BuildEnvironment
from sphinx.util.typing import Inventory, InventoryItem
from sphinx.util.typing import Inventory
# Readable file stream for inventory loading
class _SupportsRead(Protocol):
@@ -82,8 +84,12 @@ class InventoryFile:
else:
item_type = f'py:{item_type}'
location += f'#{name}'
inv_item: InventoryItem = projname, version, location, '-'
invdata.setdefault(item_type, {})[name] = inv_item
invdata.setdefault(item_type, {})[name] = _InventoryItem(
project_name=projname,
project_version=version,
uri=location,
display_name='-',
)
return invdata
@classmethod
@@ -148,8 +154,12 @@ class InventoryFile:
if location.endswith('$'):
location = location[:-1] + name
location = posixpath.join(uri, location)
inv_item: InventoryItem = projname, version, location, dispname
invdata.setdefault(type, {})[name] = inv_item
invdata.setdefault(type, {})[name] = _InventoryItem(
project_name=projname,
project_version=version,
uri=location,
display_name=dispname,
)
for ambiguity in actual_ambiguities:
logger.info(
__('inventory <%s> contains multiple definitions for %s'),
@@ -194,3 +204,89 @@ class InventoryFile:
entry = f'{fullname} {domain.name}:{type} {prio} {uri} {dispname}\n'
f.write(compressor.compress(entry.encode()))
f.write(compressor.flush())
class _InventoryItem:
__slots__ = 'project_name', 'project_version', 'uri', 'display_name'
project_name: str
project_version: str
uri: str
display_name: str
def __init__(
self,
*,
project_name: str,
project_version: str,
uri: str,
display_name: str,
) -> None:
object.__setattr__(self, 'project_name', project_name)
object.__setattr__(self, 'project_version', project_version)
object.__setattr__(self, 'uri', uri)
object.__setattr__(self, 'display_name', display_name)
def __repr__(self) -> str:
return (
'_InventoryItem('
f'project_name={self.project_name!r}, '
f'project_version={self.project_version!r}, '
f'uri={self.uri!r}, '
f'display_name={self.display_name!r}'
')'
)
def __eq__(self, other: object) -> bool:
if not isinstance(other, _InventoryItem):
return NotImplemented
return (
self.project_name == other.project_name
and self.project_version == other.project_version
and self.uri == other.uri
and self.display_name == other.display_name
)
def __hash__(self) -> int:
return hash((
self.project_name,
self.project_version,
self.uri,
self.display_name,
))
def __setattr__(self, key: str, value: object) -> NoReturn:
msg = '_InventoryItem is immutable'
raise AttributeError(msg)
def __delattr__(self, key: str) -> NoReturn:
msg = '_InventoryItem is immutable'
raise AttributeError(msg)
def __getstate__(self) -> tuple[str, str, str, str]:
return self.project_name, self.project_version, self.uri, self.display_name
def __setstate__(self, state: tuple[str, str, str, str]) -> None:
project_name, project_version, uri, display_name = state
object.__setattr__(self, 'project_name', project_name)
object.__setattr__(self, 'project_version', project_version)
object.__setattr__(self, 'uri', uri)
object.__setattr__(self, 'display_name', display_name)
def __getitem__(self, key: int | slice) -> str | tuple[str, ...]:
warnings.warn(
'The tuple interface for _InventoryItem objects is deprecated.',
RemovedInSphinx10Warning,
stacklevel=2,
)
tpl = self.project_name, self.project_version, self.uri, self.display_name
return tpl[key]
def __iter__(self) -> Iterator[str]:
warnings.warn(
'The iter() interface for _InventoryItem objects is deprecated.',
RemovedInSphinx10Warning,
stacklevel=2,
)
tpl = self.project_name, self.project_version, self.uri, self.display_name
return iter(tpl)

View File

@@ -23,6 +23,7 @@ if TYPE_CHECKING:
from typing_extensions import TypeIs
from sphinx.application import Sphinx
from sphinx.util.inventory import _InventoryItem
_RestifyMode: TypeAlias = Literal[
'fully-qualified-except-typing',
@@ -110,13 +111,7 @@ OptionSpec: TypeAlias = dict[str, Callable[[str], typing.Any]]
TitleGetter: TypeAlias = Callable[[nodes.Node], str]
# inventory data on memory
InventoryItem: TypeAlias = tuple[
str, # project name
str, # project version
str, # URL
str, # display name
]
Inventory: TypeAlias = dict[str, dict[str, InventoryItem]]
Inventory: TypeAlias = dict[str, dict[str, '_InventoryItem']]
class ExtensionMetadata(typing.TypedDict, total=False):

View File

@@ -6,7 +6,7 @@ import posixpath
import pytest
from sphinx.util.inventory import InventoryFile
from sphinx.util.inventory import InventoryFile, _InventoryItem
@pytest.mark.sphinx('dirhtml', testroot='builder-dirhtml')
@@ -30,28 +30,33 @@ def test_dirhtml(app):
invdata = InventoryFile.load(f, 'path/to', posixpath.join)
assert 'index' in invdata.get('std:doc', {})
assert invdata['std:doc']['index'] == ('Project name not set', '', 'path/to/', '-')
assert invdata['std:doc']['index'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='path/to/',
display_name='-',
)
assert 'foo/index' in invdata.get('std:doc', {})
assert invdata['std:doc']['foo/index'] == (
'Project name not set',
'',
'path/to/foo/',
'-',
assert invdata['std:doc']['foo/index'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='path/to/foo/',
display_name='-',
)
assert 'index' in invdata.get('std:label', {})
assert invdata['std:label']['index'] == (
'Project name not set',
'',
'path/to/#index',
'-',
assert invdata['std:label']['index'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='path/to/#index',
display_name='-',
)
assert 'foo' in invdata.get('std:label', {})
assert invdata['std:label']['foo'] == (
'Project name not set',
'',
'path/to/foo/#foo',
'foo/index',
assert invdata['std:label']['foo'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='path/to/foo/#foo',
display_name='foo/index',
)

View File

@@ -12,7 +12,7 @@ import pytest
from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
from sphinx.errors import ConfigError
from sphinx.util.inventory import InventoryFile
from sphinx.util.inventory import InventoryFile, _InventoryItem
from tests.test_builders.xpath_data import FIGURE_CAPTION
from tests.test_builders.xpath_util import check_xpath
@@ -233,36 +233,36 @@ def test_html_inventory(app):
'genindex',
'search',
}
assert invdata['std:label']['modindex'] == (
'Project name not set',
'',
'https://www.google.com/py-modindex.html',
'Module Index',
assert invdata['std:label']['modindex'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='https://www.google.com/py-modindex.html',
display_name='Module Index',
)
assert invdata['std:label']['py-modindex'] == (
'Project name not set',
'',
'https://www.google.com/py-modindex.html',
'Python Module Index',
assert invdata['std:label']['py-modindex'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='https://www.google.com/py-modindex.html',
display_name='Python Module Index',
)
assert invdata['std:label']['genindex'] == (
'Project name not set',
'',
'https://www.google.com/genindex.html',
'Index',
assert invdata['std:label']['genindex'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='https://www.google.com/genindex.html',
display_name='Index',
)
assert invdata['std:label']['search'] == (
'Project name not set',
'',
'https://www.google.com/search.html',
'Search Page',
assert invdata['std:label']['search'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='https://www.google.com/search.html',
display_name='Search Page',
)
assert set(invdata['std:doc'].keys()) == {'index'}
assert invdata['std:doc']['index'] == (
'Project name not set',
'',
'https://www.google.com/index.html',
'The basic Sphinx documentation for testing',
assert invdata['std:doc']['index'] == _InventoryItem(
project_name='Project name not set',
project_version='',
uri='https://www.google.com/index.html',
display_name='The basic Sphinx documentation for testing',
)

View File

@@ -28,6 +28,7 @@ from sphinx.ext.intersphinx._load import (
)
from sphinx.ext.intersphinx._resolve import missing_reference
from sphinx.ext.intersphinx._shared import _IntersphinxProject
from sphinx.util.inventory import _InventoryItem
from tests.test_util.intersphinx_data import (
INVENTORY_V2,
@@ -155,11 +156,11 @@ def test_missing_reference(tmp_path, app):
load_mappings(app)
inv = app.env.intersphinx_inventory
assert inv['py:module']['module2'] == (
'foo',
'2.0',
'https://docs.python.org/foo.html#module-module2',
'-',
assert inv['py:module']['module2'] == _InventoryItem(
project_name='foo',
project_version='2.0',
uri='https://docs.python.org/foo.html#module-module2',
display_name='-',
)
# check resolution when a target is found

View File

@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
from sphinx.ext.intersphinx._shared import InventoryAdapter
from sphinx.testing.util import SphinxTestApp
from sphinx.util.inventory import _InventoryItem
from tests.utils import http_server
@@ -18,7 +19,6 @@ if TYPE_CHECKING:
from collections.abc import Iterable
from typing import BinaryIO
from sphinx.util.typing import InventoryItem
BASE_CONFIG = {
'extensions': ['sphinx.ext.intersphinx'],
@@ -109,10 +109,14 @@ class IntersphinxProject:
"""The :confval:`intersphinx_mapping` record for this project."""
return {self.name: (self.url, self.file)}
def normalise(self, entry: InventoryEntry) -> tuple[str, InventoryItem]:
def normalise(self, entry: InventoryEntry) -> tuple[str, _InventoryItem]:
"""Format an inventory entry as if it were part of this project."""
url = posixpath.join(self.url, entry.uri)
return entry.name, (self.safe_name, self.safe_version, url, entry.display_name)
return entry.name, _InventoryItem(
project_name=self.safe_name,
project_version=self.safe_version,
uri=posixpath.join(self.url, entry.uri),
display_name=entry.display_name,
)
class FakeInventory:

View File

@@ -8,7 +8,7 @@ import pytest
import sphinx.locale
from sphinx.testing.util import SphinxTestApp
from sphinx.util.inventory import InventoryFile
from sphinx.util.inventory import InventoryFile, _InventoryItem
from tests.test_util.intersphinx_data import (
INVENTORY_V1,
@@ -23,17 +23,17 @@ if TYPE_CHECKING:
def test_read_inventory_v1():
invdata = InventoryFile.loads(INVENTORY_V1, uri='/util')
assert invdata['py:module']['module'] == (
'foo',
'1.0',
'/util/foo.html#module-module',
'-',
assert invdata['py:module']['module'] == _InventoryItem(
project_name='foo',
project_version='1.0',
uri='/util/foo.html#module-module',
display_name='-',
)
assert invdata['py:class']['module.cls'] == (
'foo',
'1.0',
'/util/foo.html#module.cls',
'-',
assert invdata['py:class']['module.cls'] == _InventoryItem(
project_name='foo',
project_version='1.0',
uri='/util/foo.html#module.cls',
display_name='-',
)
@@ -41,35 +41,35 @@ def test_read_inventory_v2():
invdata = InventoryFile.loads(INVENTORY_V2, uri='/util')
assert len(invdata['py:module']) == 2
assert invdata['py:module']['module1'] == (
'foo',
'2.0',
'/util/foo.html#module-module1',
'Long Module desc',
assert invdata['py:module']['module1'] == _InventoryItem(
project_name='foo',
project_version='2.0',
uri='/util/foo.html#module-module1',
display_name='Long Module desc',
)
assert invdata['py:module']['module2'] == (
'foo',
'2.0',
'/util/foo.html#module-module2',
'-',
assert invdata['py:module']['module2'] == _InventoryItem(
project_name='foo',
project_version='2.0',
uri='/util/foo.html#module-module2',
display_name='-',
)
assert invdata['py:function']['module1.func'][2] == (
assert invdata['py:function']['module1.func'].uri == (
'/util/sub/foo.html#module1.func'
)
assert invdata['c:function']['CFunc'][2] == '/util/cfunc.html#CFunc'
assert invdata['std:term']['a term'][2] == '/util/glossary.html#term-a-term'
assert invdata['std:term']['a term including:colon'][2] == (
assert invdata['c:function']['CFunc'].uri == '/util/cfunc.html#CFunc'
assert invdata['std:term']['a term'].uri == '/util/glossary.html#term-a-term'
assert invdata['std:term']['a term including:colon'].uri == (
'/util/glossary.html#term-a-term-including-colon'
)
def test_read_inventory_v2_not_having_version():
invdata = InventoryFile.loads(INVENTORY_V2_NO_VERSION, uri='/util')
assert invdata['py:module']['module1'] == (
'foo',
'',
'/util/foo.html#module-module1',
'Long Module desc',
assert invdata['py:module']['module1'] == _InventoryItem(
project_name='foo',
project_version='',
uri='/util/foo.html#module-module1',
display_name='Long Module desc',
)