Skip to content

Commit

Permalink
Merge pull request #35 from sphinx-contrib/download_pdf
Browse files Browse the repository at this point in the history
Add feature to download remote pdf files or copy local pdf files
  • Loading branch information
JasperCraeghs authored Sep 27, 2021
2 parents 0d681bb + e3695d5 commit 0c8bda7
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 13 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
1.11 (Sep 22, 2021)
==================

- Add feature to download remote and copy local pdf files [PR #35]

1.10 (Sep 10, 2021)
==================

Expand Down
31 changes: 28 additions & 3 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,19 +131,44 @@ Configuration values
.. confval:: doxylink

The environment is set up with a dictionary mapping the interpreted text role to a tuple of tag file and prefix.
The keys of this dictionary must be lower-case.
The keys of this dictionary must be lower-case. The prefix can be an absolute path or a path relative to `Sphinx'
output directory`_. An optional third element with the name of a Doxygen pdf file may be added. This will be used
when Sphinx uses the LaTeX builder. Otherwise, the second element of the tuple will be used to link to.

.. code-block:: python
doxylink = {
'polyvox' : ('/home/matt/PolyVox.tag', '/home/matt/PolyVox/html/'),
'qtogre' : ('/home/matt/QtOgre.tag', '/home/matt/QtOgre/html/'),
'polyvox' : ('/home/matt/PolyVox.tag', '/home/matt/PolyVox/html/', 'polyvox_doxygen.pdf'),
'qtogre' : ('/home/matt/QtOgre.tag', '/home/matt/QtOgre/html/', 'qtogre_doxygen.pdf'),
}
.. note::

The links in your pdf document to your Doxygen pdf file(s) may not work (properly) in a browser or a basic
PDF-reader. They should work in Adobe Reader for example.

.. _`Sphinx' output directory`: https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.outdir

.. confval:: add_function_parentheses

A boolean that decides whether parentheses are appended to function and method role text. Default is ``True``.

.. confval:: doxylink_pdf_files

Doxylink can be configured to download remote Doxygen pdf files or copy them from a local location.
You should use the output file name as the third
element of the value of the ``doxylink`` dictionary **and** as key in the ``doxylink_pdf_files`` dictionary,
which should contain the URL to the remote location or local location as value.
If the pdf file already exists locally in Sphinx' output directory, it will not be downloaded or overwritten.

.. code-block:: python
doxylink_pdf_files = {
'polyvox_doxygen.pdf': url_to_remote_doxygen_pdf,
'qtogre_doxygen.pdf': '/home/matt/qtogre/doxygen.pdf',
}
Bug reports
-----------

Expand Down
3 changes: 2 additions & 1 deletion sphinxcontrib/doxylink/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
__version__ = '1.10'
__version__ = '1.11'


def setup(app):
from .doxylink import setup_doxylink_roles
app.add_config_value('doxylink', {}, 'env')
app.add_config_value('doxylink_pdf_files', {}, 'env')
app.connect('builder-inited', setup_doxylink_roles)
106 changes: 97 additions & 9 deletions sphinxcontrib/doxylink/doxylink.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import re
import requests
import shutil
import time
import xml.etree.ElementTree as ET
import urllib.parse
Expand Down Expand Up @@ -258,9 +259,9 @@ def join(*args):
return ''.join(args)


def create_role(app, tag_filename, rootdir, cache_name):
def create_role(app, tag_filename, rootdir, cache_name, pdf=""):
# Tidy up the root directory path
if not rootdir.endswith(('/', '\\')) and not rootdir.endswith('.pdf'):
if not rootdir.endswith(('/', '\\')):
rootdir = join(rootdir, os.sep)

try:
Expand Down Expand Up @@ -332,14 +333,14 @@ def find_doxygen_link(name, rawtext, text, lineno, inliner, options={}, content=
'Error reported was: %s' % (part, error), line=lineno)
return [nodes.inline(title, title)], []

if pdf:
full_url = join(pdf, '#', url.file)
full_url = full_url.replace('.html#', '_') # for links to variables and functions
full_url = full_url.replace('.html', '') # for links to files
# If it's an absolute path then the link will work regardless of the document directory
# Also check if it is a URL (i.e. it has a 'scheme' like 'http' or 'file')
if os.path.isabs(rootdir) or urllib.parse.urlparse(rootdir).scheme:
elif os.path.isabs(rootdir) or urllib.parse.urlparse(rootdir).scheme:
full_url = join(rootdir, url.file)
elif rootdir.endswith('.pdf'):
full_url = join(rootdir, '#', url.file)
full_url = full_url.replace('.html#', '_') # for links to variables and functions
full_url = full_url.replace('.html', '') # for links to files
# But otherwise we need to add the relative path of the current document to the root source directory to the link
else:
relative_path_to_docsrc = os.path.relpath(app.env.srcdir, os.path.dirname(inliner.document.attributes['source']))
Expand All @@ -354,6 +355,93 @@ def find_doxygen_link(name, rawtext, text, lineno, inliner, options={}, content=
return find_doxygen_link


def extract_configuration(values):
if len(values) == 3:
tag_filename, rootdir, pdf_filename = values
elif len(values) == 2:
tag_filename = values[0]
if values[1].endswith('.pdf'):
pdf_filename = values[1]
rootdir = ""
else:
rootdir = values[1]
pdf_filename = ""
else:
raise ValueError("Config variable `doxylink` is incorrectly configured. Expected a tuple with 2 to 3 "
"elements; got %s" % values)
return tag_filename, rootdir, pdf_filename


def fetch_file(app, source, output_path):
"""Fetches file and puts it in the desired location if it does not exist yet.
Local files will be copied and remote files will be downloaded.
Directories in the ``output_path`` get created if needed.
Args:
app: Sphinx' application instance
source (str): Path to local file or URL to remote file
output_path (str): Path with filename to copy/download the source to, relative to Sphinx' output directory
"""
if not os.path.isabs(output_path):
output_path = os.path.join(app.outdir, output_path)
if os.path.exists(output_path):
return
os.makedirs(os.path.dirname(output_path), exist_ok=True)
if is_url(source):
response = requests.get(source, allow_redirects=True)
if response.status_code != 200:
report_warning(app.env,
standout("Could not find file %r. Make sure your `doxylink_pdf_files` config variable is "
"set correctly." % source))
return
with open(output_path, 'wb') as file:
file.write(response.content)
else:
if not os.path.isabs(source):
source = os.path.join(app.outdir, source)
if os.path.exists(source):
shutil.copy(source, output_path)
else:
report_warning(app.env,
standout("Expected a URL or a path that exists as value for `doxylink_pdf_files` "
"config variable; got %r" % source))


def process_configuration(app, tag_filename, rootdir, pdf_filename):
"""Processes the configured values for ``doxylink`` and ``doxylink_pdf_files`` and warns about potential issues.
The type of builder decides which values shall be used.
Args:
app: Sphinx' application instance
tag_filename (str): Path to the Doxygen tag file
rootdir (str): Path to the root directory of Doxygen HTML documentation
pdf_filename (str): Path to the pdf file; may be empty when LaTeX builder is not used
"""
if app.builder.format == 'latex':
if not pdf_filename:
if is_url(rootdir):
report_warning(app.env,
"Linking from PDF to remote Doxygen html is not supported yet; got %r."
"Consider linking to a Doxygen pdf file instead as "
"third element of the tuple in the `doxylink` config variable." % rootdir)
else:
report_warning(app.env,
"Linking from PDF to local Doxygen html is not possible; got %r."
"Consider linking to a Doxygen pdf file instead as third element of the tuple in the "
"`doxylink` config variable." % rootdir)
elif pdf_filename in app.config.doxylink_pdf_files:
source = app.config.doxylink_pdf_files[pdf_filename]
fetch_file(app, source, pdf_filename)
elif pdf_filename and not rootdir:
report_warning(app.env,
"Linking from HTML to Doxygen pdf (%r) is not supported. Consider setting "
"the root directory of Doxygen's HTML output as value instead." % pdf_filename)


def setup_doxylink_roles(app):
for name, (tag_filename, rootdir) in app.config.doxylink.items():
app.add_role(name, create_role(app, tag_filename, rootdir, name))
for name, values in app.config.doxylink.items():
tag_filename, rootdir, pdf_filename = extract_configuration(values)
process_configuration(app, tag_filename, rootdir, pdf_filename)
app.add_role(name, create_role(app, tag_filename, rootdir, name, pdf=pdf_filename))
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pytest
testfixtures
55 changes: 55 additions & 0 deletions tests/test_doxylink.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import os.path
import subprocess
import xml.etree.ElementTree as ET
from unittest.mock import MagicMock

import pytest
from testfixtures import LogCapture

from sphinxcontrib.doxylink import doxylink

Expand Down Expand Up @@ -131,3 +133,56 @@ def test_find_url_piecewise(examples_tag_file, symbol, expected_matches):
def test_is_url(str_to_validate, expected):
result = doxylink.is_url(str_to_validate)
assert result == expected


@pytest.mark.parametrize('values, out_rootdir, out_pdf', [
(['doxygen/project.tag', 'https://example.com'], 'https://example.com', ''),
(['doxygen/project.tag', 'https://example.com', ''], 'https://example.com', ''),
(['doxygen/project.tag', 'doxygen.pdf'], '', 'doxygen.pdf'),
(['doxygen/project.tag', 'https://example.com', 'doxygen.pdf'], 'https://example.com', 'doxygen.pdf'),
])
def test_extract_configuration_pass(values, out_rootdir, out_pdf):
tag_filename, rootdir, pdf_filename = doxylink.extract_configuration(values)
assert rootdir == out_rootdir
assert pdf_filename == out_pdf


@pytest.mark.parametrize('values', [
(['doxygen/project.tag']),
(['doxygen/project.tag', 'https://example.com', 'doxygen.pdf', 'fail']),
])
def test_extract_configuration_fail(values):
with pytest.raises(ValueError):
doxylink.extract_configuration(values)


@pytest.mark.parametrize('tag_filename, rootdir, pdf_filename, builder', [
('doxygen/project.tag', 'https://example.com', '', 'html'),
('doxygen/project.tag', '', 'doxygen.pdf', 'latex'),
('doxygen/project.tag', 'html/doxygen', 'doxygen.pdf', 'latex'),
])
def test_process_configuration_pass(tag_filename, rootdir, pdf_filename, builder):
app = MagicMock()
app.builder.format = builder
with LogCapture() as l:
doxylink.process_configuration(app, tag_filename, rootdir, pdf_filename)
l.check()


@pytest.mark.parametrize('rootdir, pdf_filename, builder, msg', [
('', 'doxygen.pdf', 'html',
"Linking from HTML to Doxygen pdf ('doxygen.pdf') is not supported. "
"Consider setting the root directory of Doxygen's HTML output as value instead."),
('https://example.com', '', 'latex',
"Linking from PDF to remote Doxygen html is not supported yet; got 'https://example.com'."
"Consider linking to a Doxygen pdf file instead as third element of the tuple in the `doxylink` config variable."),
('html/doxygen', '', 'latex',
"Linking from PDF to local Doxygen html is not possible; got 'html/doxygen'."
"Consider linking to a Doxygen pdf file instead as third element of the tuple in the `doxylink` config variable."),
])
def test_process_configuration_warn(rootdir, pdf_filename, builder, msg):
app = MagicMock()
app.builder.format = builder
with LogCapture() as l:
doxylink.process_configuration(app, 'doxygen/project.tag', rootdir, pdf_filename)
l.check(('sphinx.sphinxcontrib.doxylink.doxylink', 'WARNING', msg))

0 comments on commit 0c8bda7

Please sign in to comment.