Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Contributors
* \A. Rafey Khan -- improved intersphinx typing
* Rui Pinheiro -- Python 3.14 forward references support
* Roland Meister -- epub builder
* Samuel Dowling -- Intersphinx request header support
* Sebastian Wiesner -- image handling, distutils support
* Slawek Figiel -- additional warning suppression
* Stefan Seefeld -- toctree improvements
Expand Down
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Dependencies
Features added
--------------

* Add :confval:`intersphinx_request_headers` to allow custom HTTP headers to
be set when resolving objects.inv on remote hosts. In particular, this
enables Bearer authorization to allow access to private GitLab pages.
* Add :meth:`~sphinx.application.Sphinx.add_static_dir` for copying static
assets from extensions to the build output.
Patch by Jared Dillard
Expand Down
18 changes: 18 additions & 0 deletions doc/usage/extensions/intersphinx.rst
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,24 @@ linking:
If ``*`` is in the list of domains, then no non-:rst:role:`external`
references will be resolved by intersphinx.

.. confval:: intersphinx_request_headers
:type: :code-py:`dict[str, dict[str, str]]`
:default: :code-py:`{}`

Headers to pass to the HTTP request for a project's :file:`objects.inv` file.
For example:

.. code-block:: python

token = "abcde"
intersphinx_request_headers = {
'python': {
"Authorization": f"Bearer {token}",
}
}

.. versionadded:: 9.2.0

Explicitly Reference External Objects
-------------------------------------

Expand Down
3 changes: 3 additions & 0 deletions sphinx/ext/intersphinx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@

def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('intersphinx_mapping', {}, 'env', types=frozenset({dict}))
app.add_config_value(
'intersphinx_request_headers', {}, 'env', types=frozenset({dict})
)
app.add_config_value('intersphinx_resolve_self', '', 'env', types=frozenset({str}))
app.add_config_value('intersphinx_cache_limit', 5, '', types=frozenset({int}))
app.add_config_value(
Expand Down
1 change: 1 addition & 0 deletions sphinx/ext/intersphinx/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def inspect_main(argv: list[str], /) -> int:
try:
raw_data, _ = _fetch_inventory_data(
target_uri='',
headers={},
inv_location=filename,
config=config,
srcdir=Path(),
Expand Down
27 changes: 23 additions & 4 deletions sphinx/ext/intersphinx/_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,18 @@ def load_mappings(app: Sphinx) -> None:
inventories = InventoryAdapter(env)
intersphinx_cache: dict[InventoryURI, InventoryCacheEntry] = inventories.cache
intersphinx_mapping: IntersphinxMapping = app.config.intersphinx_mapping
intersphinx_request_headers: dict[str, dict[str, str]] = (
app.config.intersphinx_request_headers
)

projects = []
for name, (uri, locations) in intersphinx_mapping.values():
try:
project = _IntersphinxProject(
name=name, target_uri=uri, locations=locations
name=name,
target_uri=uri,
headers=intersphinx_request_headers.get(name, {}),
locations=locations,
)
except ValueError as err:
msg = __(
Expand Down Expand Up @@ -302,6 +308,7 @@ def _fetch_inventory_group(
try:
raw_data, target_uri = _fetch_inventory_data(
target_uri=project.target_uri,
headers=project.headers,
inv_location=inv_location,
config=config,
srcdir=srcdir,
Expand Down Expand Up @@ -335,10 +342,13 @@ def _fetch_inventory_group(
return updated


def fetch_inventory(app: Sphinx, uri: InventoryURI, inv: str) -> Inventory:
def fetch_inventory(
app: Sphinx, uri: InventoryURI, headers: dict[str, str], inv: str
) -> Inventory:
"""Fetch, parse and return an intersphinx inventory file."""
raw_data, uri = _fetch_inventory_data(
target_uri=uri,
headers=headers,
inv_location=inv,
config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
Expand All @@ -350,6 +360,7 @@ def fetch_inventory(app: Sphinx, uri: InventoryURI, inv: str) -> Inventory:
def _fetch_inventory_data(
*,
target_uri: InventoryURI,
headers: dict[str, str],
inv_location: str,
config: _InvConfig,
srcdir: Path,
Expand All @@ -364,7 +375,10 @@ def _fetch_inventory_data(
target_uri = _strip_basic_auth(target_uri)
if '://' in inv_location:
raw_data, target_uri = _fetch_inventory_url(
target_uri=target_uri, inv_location=inv_location, config=config
target_uri=target_uri,
headers=headers,
inv_location=inv_location,
config=config,
)
if cache_path is not None:
cache_path.parent.mkdir(parents=True, exist_ok=True)
Expand All @@ -386,14 +400,19 @@ def _load_inventory(raw_data: bytes, /, *, target_uri: InventoryURI) -> _Invento


def _fetch_inventory_url(
*, target_uri: InventoryURI, inv_location: str, config: _InvConfig
*,
target_uri: InventoryURI,
headers: dict[str, str],
inv_location: str,
config: _InvConfig,
) -> tuple[bytes, str]:
try:
with requests.get(
inv_location,
timeout=config.intersphinx_timeout,
_user_agent=config.user_agent,
_tls_info=(config.tls_verify, config.tls_cacerts),
headers=headers,
) as r:
r.raise_for_status()
raw_data = r.content
Expand Down
5 changes: 5 additions & 0 deletions sphinx/ext/intersphinx/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@
class _IntersphinxProject:
name: InventoryName
target_uri: InventoryURI
headers: dict[str, str]
locations: tuple[InventoryLocation, ...]

__slots__ = {
'name': 'The inventory name. '
'It is unique and in bijection with an remote inventory URL.',
'target_uri': 'The inventory project URL to which links are resolved. '
'It is unique and in bijection with an inventory name.',
'headers': 'The headers to use in the HTTP request',
'locations': 'A tuple of local or remote targets containing '
'the inventory data to fetch. '
'None indicates the default inventory file name.',
Expand All @@ -61,6 +63,7 @@ def __init__(
*,
name: InventoryName,
target_uri: InventoryURI,
headers: dict[str, str],
locations: Sequence[InventoryLocation],
) -> None:
if not name or not isinstance(name, str):
Expand All @@ -80,13 +83,15 @@ def __init__(
raise ValueError(msg)
object.__setattr__(self, 'name', name)
object.__setattr__(self, 'target_uri', target_uri)
object.__setattr__(self, 'headers', headers)
object.__setattr__(self, 'locations', tuple(locations))

def __repr__(self) -> str:
return (
f'{self.__class__.__name__}('
f'name={self.name!r}, '
f'target_uri={self.target_uri!r}, '
f'headers={self.headers!r}, '
f'locations={self.locations!r})'
)

Expand Down
23 changes: 20 additions & 3 deletions tests/test_ext_intersphinx/test_ext_intersphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ def reference_check(app, *args, **kwds):
def set_config(app, mapping):
# copy *mapping* so that normalization does not alter it
app.config.intersphinx_mapping = mapping.copy()
app.config.intersphinx_request_headers = {
'python': {
'Authorization': 'Bearer abcde',
}
}
app.config.intersphinx_cache_limit = 0
app.config.intersphinx_disabled_reftypes = []
app.config.intersphinx_resolve_self = ''
Expand All @@ -89,8 +94,10 @@ def test_fetch_inventory_redirection(get_request, InventoryFile, app):
# same uri and inv, not redirected
mocked_get.url = 'https://hostname/' + INVENTORY_FILENAME
target_uri = 'https://hostname/'
headers = {}
raw_data, target_uri = _fetch_inventory_data(
target_uri=target_uri,
headers=headers,
inv_location='https://hostname/' + INVENTORY_FILENAME,
config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
Expand All @@ -108,6 +115,7 @@ def test_fetch_inventory_redirection(get_request, InventoryFile, app):
target_uri = 'https://hostname/'
raw_data, target_uri = _fetch_inventory_data(
target_uri=target_uri,
headers=headers,
inv_location='https://hostname/' + INVENTORY_FILENAME,
config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
Expand All @@ -129,6 +137,7 @@ def test_fetch_inventory_redirection(get_request, InventoryFile, app):
target_uri = 'https://hostname/'
raw_data, target_uri = _fetch_inventory_data(
target_uri=target_uri,
headers=headers,
inv_location='https://hostname/new/' + INVENTORY_FILENAME,
config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
Expand All @@ -146,6 +155,7 @@ def test_fetch_inventory_redirection(get_request, InventoryFile, app):
target_uri = 'https://hostname/'
raw_data, target_uri = _fetch_inventory_data(
target_uri=target_uri,
headers=headers,
inv_location='https://hostname/new/' + INVENTORY_FILENAME,
config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
Expand Down Expand Up @@ -802,7 +812,9 @@ def test_intersphinx_cache_limit(app, monkeypatch, cache_limit, expected_expired
)

for name, (uri, locations) in app.config.intersphinx_mapping.values():
project = _IntersphinxProject(name=name, target_uri=uri, locations=locations)
project = _IntersphinxProject(
name=name, target_uri=uri, headers={}, locations=locations
)
updated = _fetch_inventory_group(
project=project,
cache=intersphinx_cache,
Expand Down Expand Up @@ -836,6 +848,7 @@ def log_message(*args, **kwargs):
with http_server(InventoryHandler) as server:
url1 = f'http://localhost:{server.server_port}'
url2 = f'http://localhost:{server.server_port}/'
headers = {'Authorization': 'Bearer abcde'}

config = Config()
config.intersphinx_cache_limit = -1
Expand All @@ -858,7 +871,7 @@ def log_message(*args, **kwargs):
side_effect = ValueError('')

project1 = _IntersphinxProject(
name='1', target_uri=url1, locations=(url1, None)
name='1', target_uri=url1, headers=headers, locations=(url1, None)
)
with mock.patch(
'sphinx.ext.intersphinx._load._fetch_inventory_data',
Expand All @@ -867,21 +880,23 @@ def log_message(*args, **kwargs):
assert not _fetch_inventory_group(project=project1, **kwds)
mockfn.assert_any_call(
target_uri=url1,
headers=headers,
inv_location=url1,
config=config,
srcdir=None,
cache_path=None,
)
mockfn.assert_any_call(
target_uri=url1,
headers=headers,
inv_location=url1 + '/' + INVENTORY_FILENAME,
config=config,
srcdir=None,
cache_path=None,
)

project2 = _IntersphinxProject(
name='2', target_uri=url2, locations=(url2, None)
name='2', target_uri=url2, headers=headers, locations=(url2, None)
)
with mock.patch(
'sphinx.ext.intersphinx._load._fetch_inventory_data',
Expand All @@ -890,13 +905,15 @@ def log_message(*args, **kwargs):
assert not _fetch_inventory_group(project=project2, **kwds)
mockfn.assert_any_call(
target_uri=url2,
headers=headers,
inv_location=url2,
config=config,
srcdir=None,
cache_path=None,
)
mockfn.assert_any_call(
target_uri=url2,
headers=headers,
inv_location=url2 + INVENTORY_FILENAME,
config=config,
srcdir=None,
Expand Down
Loading