Skip to content

Add support for loading intersphinx inventory from file #882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions docs/source/sphinx-integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ with ``L{}`` tag using `epytext`, will be linked to the Python element. Example:
`datetime.datetime`
L{datetime.datetime}

Similarly, you can link external API documentation from a local directory
with the following cumulative configuration option::

--intersphinx-file=~/projects/myproject/docs/api/objects.inv

Simple as that!

Linking from Sphinx to your pydoctor API docs
Expand Down
3 changes: 3 additions & 0 deletions pydoctor/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,9 @@ def fetchIntersphinxInventories(self, cache: CacheT) -> None:
"""
for url in self.options.intersphinx:
self.intersphinx.update(cache, url)

for path in self.options.intersphinx_file:
self.intersphinx.update_file(path)

def defaultPostProcess(system:'System') -> None:

Expand Down
9 changes: 9 additions & 0 deletions pydoctor/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ def get_parser() -> ArgumentParser:
help=MAX_AGE_HELP,
metavar='DURATION',
)

parser.add_argument(
'--intersphinx-file', action='append', dest='intersphinx_file',
metavar='PATH_TO_OBJECTS.INV', default=[],
help=(
"Use Sphinx objects inventory file to generate links to external "
"documentation. Can be repeated."))

parser.add_argument(
'--pyval-repr-maxlines', dest='pyvalreprmaxlines', default=7, type=int, metavar='INT',
help='Maxinum number of lines for a constant value representation. Use 0 for unlimited.')
Expand Down Expand Up @@ -381,6 +389,7 @@ class Options:
intersphinx_cache_path: str = attr.ib()
clear_intersphinx_cache: bool = attr.ib()
intersphinx_cache_max_age: str = attr.ib()
intersphinx_file: List[str] = attr.ib()
pyvalreprlinelen: int = attr.ib()
pyvalreprmaxlines: int = attr.ib()
sidebarexpanddepth: int = attr.ib()
Expand Down
11 changes: 11 additions & 0 deletions pydoctor/sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ def update(self, cache: CacheT, url: str) -> None:
payload = self._getPayload(base_url, data)
self._links.update(self._parseInventory(base_url, payload))

def update_file(self, path: str) -> None:
"""
Update inventory from local path.
"""
with open(path, 'rb') as f:
data = f.read()

payload = self._getPayload(path, data)
root_dir = os.path.dirname(path)
self._links.update(self._parseInventory(root_dir, payload))

def _getPayload(self, base_url: str, data: bytes) -> str:
"""
Parse inventory and return clear text payload without comments.
Expand Down
42 changes: 42 additions & 0 deletions pydoctor/test/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pydoctor.test import CapSys
from pydoctor.test.test_astbuilder import fromText
from pydoctor.test.test_packages import processPackage
from pydoctor.test import FixtureRequest, TempPathFactory


class FakeOptions:
Expand All @@ -43,6 +44,11 @@ class FakeDocumentable:
filepath: str


@pytest.fixture(scope='module')
def tempDir(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
name = request.module.__name__.split('.')[-1]
return tmp_path_factory.mktemp(f'{name}-cache')


@pytest.mark.parametrize('projectBaseDir', [
PurePosixPath("/foo/bar/ProjectName"),
Expand Down Expand Up @@ -186,6 +192,42 @@ def close(self) -> None:
)


def test_fetchIntersphinxInventories_content_file(tempDir:Path) -> None:
"""
Read and parse intersphinx inventories from file for each configured
intersphix.
"""
root_dir = tempDir
path = root_dir / 'objects.inv'
with open(path, 'wb') as f:
f.write(zlib.compress(b'twisted.package py:module -1 tm.html -'))

with open(root_dir / 'tm.html', "w") as f:
pass

options = Options.defaults()
options.intersphinx_file = [path]

sut = model.System(options=options)
log = []
def log_msg(part: str, msg: str) -> None:
log.append((part, msg))
sut.msg = log_msg # type: ignore[assignment]

class Cache(CacheT):
"""Avoid touching the network."""
def get(self, url: str) -> bytes:
return b''
def close(self) -> None:
return None


sut.fetchIntersphinxInventories(Cache())

assert [] == log
assert ((root_dir / 'tm.html').samefile(sut.intersphinx.getLink('twisted.package')))


def test_docsources_class_attribute() -> None:
src = '''
class Base:
Expand Down
84 changes: 83 additions & 1 deletion pydoctor/test/test_sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io
import string
import zlib
import re
from contextlib import contextmanager
from pathlib import Path
from typing import Callable, Iterator, List, Optional, Tuple, cast
Expand All @@ -19,7 +20,7 @@
from hypothesis import strategies as st

from . import CapLog, FixtureRequest, MonkeyPatch, TempPathFactory
from pydoctor import model, sphinx
from pydoctor import model, sphinx, driver



Expand Down Expand Up @@ -757,3 +758,84 @@ def test_prepareCache(

if clearCache:
assert not cacheDirectory.exists()


def test_intersphinx_file(inv_reader_nolog: sphinx.SphinxInventory,
tmp_path_factory: TempPathFactory) -> None:
"""
Functional test for updating from a file
"""

payload = (
b'some.module1 py:module -1 module1.html -\n'
b'other.module2 py:module 0 module2.html Other description\n'
)
# Patch URL loader to avoid hitting the system.
content = b"""# Sphinx inventory version 2
# Project: some-name
# Version: 2.0
# The rest of this file is compressed with zlib.
""" + zlib.compress(payload)

root_dir = tmp_path_factory.mktemp('test_intersphinx_file')
path = root_dir / 'objects.inv'
with open(path, 'wb') as f:
f.write(content)

with open(root_dir / 'module1.html', "w") as f:
pass

with open(root_dir / 'module2.html', "w") as f:
pass

inv_reader_nolog.update_file(path)

assert (root_dir / 'module1.html').samefile(inv_reader_nolog.getLink('some.module1'))
assert (root_dir / 'module2.html').samefile(inv_reader_nolog.getLink('other.module2'))
Comment on lines +793 to +794
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... so if I understand correctly, your generating links that do not include a http scheme but are paths to files directly. Can you tell me more about your usage of this feature ? are you serving all pydoctor generated pages for many projects under the same folder, so these links are all relative in practice ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just pushed the test you requested. It needed some adjustment to work, but I hope it's now okay. i have also updated sphinx-integration.rst as you requested.

I am using pydoctor for local projects that are not hosted online, mostly because the source code includes confidential data and we don't want to upload the source code to github, etc.
These updates to pydoctor were implemented and tested for one project so far. Here, the links are absolute and point to a location on a shared file system. I don't see any reason why they couldn't be relative, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I understand your usage.

Another use case for loading a inventory files from disk is when the API documentation are actually served somewhere (but behind a login page of some sort or in a inaccessible network segment), in that situation pydoctor won't be able to retrieve the inventory file itself from the URL. To accommodate this kind of usage, the --intersphinx-file option could have the following format PATH[:BASE_URL] where the base url is optional, but when used the links generated will be actually HTTP links.

Tell me what you think,

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand how this would work in practice. Are you saying that the code documentation is accessible but the inventory file is not. The user would have to manually download the inventory file to the local file system and use this to build the links. The resulting http links would then point to the html pages that are not behind a security page.
If I understand correctly, this is doable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s say the documentation is behind a basic authenticated website. Pydoctor doesn’t support authentication to retrieve intersphinx inventory at the moment. So the solution would be to download objects.inv files withcurl -u user:password https://someprotectedsite/project1/api/objects.inv > project.inv before calling pydoctor with option —intersphinx-file=./project.inv:https://someprotectedsite/project1/api.

Tell me what you think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @idclifford, are you interested in implementing this feature?

If you do not wish to invest more time that this, I understand.

Many thanks



def test_generate_then_load_file(tmp_path_factory: TempPathFactory) -> None:
'''
Generate an inventory and save to file. Test --intersphinx-file to
correctly reproduce links to this.
'''
from pydoctor.test.test_astbuilder import fromText
from pydoctor.test.test_epydoc2stan import docstring2html

# Create a testing inventory file under tmp_path/objects.inv
src = '''
class C:
def __init__(self, a):
self.a = a
'''
system = fromText(src, modname='mylib').system
tmp_path = tmp_path_factory.mktemp('test_generate_then_load_file')

inv_writer, logger = get_inv_writer_with_logger(
name='project-name',
version='1.2.0rc1',
)
inv_writer.generate(system.rootobjects, tmp_path)

# Then load this inventory file in a new system, include links to elements
src = '''
from mylib import C
class Client:
"L{C}"
a: bytes
"L{C.a}"
'''
options2 = model.Options.from_args([f'--intersphinx-file={tmp_path / "objects.inv"}'])
model2 = fromText(src,
modname='myclient',
system=driver.get_system(options2))
system2 = model2.system

Client_doc = docstring2html(system2.allobjects['myclient.Client'])
Client_a_doc = docstring2html(system2.allobjects['myclient.Client.a'])

assert re.fullmatch('<div><p><code><a href=".*" class="intersphinx-link">C</a></code></p></div>',
Client_doc.replace('\n', '')) is not None
assert re.fullmatch('<div><p><code><a href=".*" class="intersphinx-link">C.a</a></code></p></div>',
Client_a_doc.replace('\n', '')) is not None

Loading