Merge branch 'master' into doc-enhance_extension_dev_i18n

This commit is contained in:
Takeshi KOMIYA
2019-03-02 19:00:25 +09:00
224 changed files with 4948 additions and 4679 deletions

View File

@@ -13,13 +13,14 @@ matrix:
include:
- python: '3.5'
env:
- TOXENV=du13
- TOXENV=du12
- python: '3.6'
env:
- TOXENV=py36
- PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg"
- TOXENV=du13
- python: '3.7'
env: TOXENV=py37
env:
- TOXENV=py37
- PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg"
- python: 'nightly'
env: TOXENV=py38
- python: '3.6'

132
CHANGES
View File

@@ -1,9 +1,69 @@
Release 2.0.0 (in development)
Release 2.1.0 (in development)
==============================
Dependencies
------------
Incompatible changes
--------------------
* Ignore filenames without file extension given to ``Builder.build_specific()``
API directly
Deprecated
----------
* ``sphinx.ext.autodoc.importer.MockFinder``
* ``sphinx.ext.autodoc.importer.MockLoader``
* ``sphinx.ext.autodoc.importer.mock()``
* ``sphinx.ext.autosummary.autolink_role()``
* ``sphinx.util.i18n.find_catalog()``
* ``sphinx.util.i18n.find_catalog_files()``
* ``sphinx.util.i18n.find_catalog_source_files()``
Features added
--------------
Bugs fixed
----------
Testing
--------
Release 2.0.0 beta2 (in development)
====================================
Dependencies
------------
Incompatible changes
--------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
* #6096: html: Anchor links are not added to figures
* #3620: html: Defer searchindex.js rather than loading it via ajax
* #6113: html: Table cells and list items have large margins
* #5508: ``linenothreshold`` option for ``highlight`` directive was ignored
* texinfo: ``make install-info`` causes syntax error
* texinfo: ``make install-info`` fails on macOS
Testing
--------
Release 2.0.0 beta1 (in development)
====================================
Dependencies
------------
* LaTeX builder now depends on TeX Live 2015 or above.
* LaTeX builder (with ``'pdflatex'`` :confval:`latex_engine`) will process
Unicode Greek letters in text (not in math mark-up) via the text font and
@@ -21,8 +81,11 @@ Dependencies
* The sphinxcontrib-websupport package is no longer a dependency
* Some packages are separated to sub packages:
- sphinxcontrib.applehelp
- sphinxcontrib.devhelp
- sphinxcontrib.htmlhelp
- sphinxcontrib.jsmath
- sphinxcontrib.serializinghtml
- sphinxcontrib.qthelp
Incompatible changes
@@ -58,9 +121,17 @@ Incompatible changes
from LaTeX preamble now get overwritten. Use ``\sphinxtableofcontentshook``
to insert custom user definitions. See :ref:`latex-macros`.
* quickstart: Simplify generated ``conf.py``
* #4148: quickstart: some questions are removed. They are still able to specify
via command line options
* websupport: unbundled from sphinx core. Please use sphinxcontrib-websupport
* C++, the visibility of base classes is now always rendered as present in the
input. That is, ``private`` is now shown, where it was ellided before.
* LaTeX: graphics inclusion of oversized images rescales to not exceed
the text width and height, even if width and/or height option were used.
(refs: #5956)
* epub: ``epub_title`` defaults to the :confval:`project` option
* #4550: All tables and figures without ``align`` option are displayed to center
* #4587: html: Output HTML5 by default
Deprecated
----------
@@ -71,6 +142,10 @@ Deprecated
``EpubBuilder.build_container()``, ``EpubBuilder.bulid_content()``,
``EpubBuilder.build_toc()`` and ``EpubBuilder.build_epub()``
* The arguments of ``Epub3Builder.build_navigation_doc()``
* The config variables
- :confval:`html_experimental_html5_writer`
* The ``encoding`` argument of ``autodoc.Documenter.get_doc()``,
``autodoc.DocstringSignatureMixin.get_doc()``,
``autodoc.DocstringSignatureMixin._find_signature()``, and
@@ -104,21 +179,30 @@ Deprecated
* ``sphinx.io.SphinxFileInput.supported``
* ``sphinx.io.SphinxRSTFileInput``
* ``sphinx.registry.SphinxComponentRegistry.add_source_input()``
* ``sphinx.roles.abbr_role()``
* ``sphinx.roles.emph_literal_role()``
* ``sphinx.roles.menusel_role()``
* ``sphinx.roles.index_role()``
* ``sphinx.roles.indexmarkup_role()``
* ``sphinx.testing.util.remove_unicode_literal()``
* ``sphinx.util.attrdict``
* ``sphinx.util.force_decode()``
* ``sphinx.util.get_matching_docs()``
* ``sphinx.util.inspect.Parameter``
* ``sphinx.util.jsonimpl``
* ``sphinx.util.osutil.EEXIST``
* ``sphinx.util.osutil.EINVAL``
* ``sphinx.util.osutil.ENOENT``
* ``sphinx.util.osutil.EPIPE``
* ``sphinx.util.osutil.walk()``
* ``sphinx.util.PeekableIterator``
* ``sphinx.util.pycompat.NoneType``
* ``sphinx.util.pycompat.TextIOWrapper``
* ``sphinx.util.pycompat.UnicodeMixin``
* ``sphinx.util.pycompat.htmlescape``
* ``sphinx.util.pycompat.indent``
* ``sphinx.util.pycompat.sys_encoding``
* ``sphinx.util.pycompat.terminal_safe()``
* ``sphinx.util.pycompat.u``
* ``sphinx.writers.latex.ExtBabel``
* ``sphinx.writers.latex.LaTeXTranslator._make_visit_admonition()``
@@ -150,6 +234,11 @@ Features added
* #4182: autodoc: Support :confval:`suppress_warnings`
* #5533: autodoc: :confval:`autodoc_default_options` supports ``member-order``
* #5394: autodoc: Display readable names in type annotations for mocked objects
* #5459: autodoc: :confval:`autodoc_default_options` accepts ``True`` as a value
* #1148: autodoc: Add :rst:dir:`autodecorator` directive for decorators
* #5635: autosummary: Add :confval:`autosummary_mock_imports` to mock external
libraries on importing targets
* #4018: htmlhelp: Add :confval:`htmlhelp_file_suffix` and
:confval:`htmlhelp_link_suffix`
* #5559: text: Support complex tables (colspan and rowspan)
@@ -166,6 +255,19 @@ Features added
* C++: add ``cpp:struct`` to complement ``cpp:class``.
* #1341 the HTML search considers words that contain a search term of length
three or longer a match.
* #4611: epub: Show warning for duplicated ToC entries
* #1851: Allow to omit an argument for :rst:dir:`code-block` directive. If
omitted, it follows :rst:dir:`highlight` or :confval:`highlight_language`
* #4587: html: Add :confval:`html4_writer` to use old HTML4 writer
* #6016: HTML search: A placeholder for the search summary prevents search
result links from changing their position when the search terminates. This
makes navigating search results easier.
* #5196: linkcheck also checks remote images exist
* #5924: githubpages: create CNAME file for custom domains when
:confval:`html_baseurl` set
* #4261: autosectionlabel: restrict the labeled sections by new config value;
:confval:`autosectionlabel_maxdepth`
Bugs fixed
----------
@@ -196,7 +298,7 @@ Testing
* Stop to use ``SPHINX_TEST_TEMPDIR`` envvar
Release 1.8.4 (in development)
Release 1.8.5 (in development)
==============================
Dependencies
@@ -213,6 +315,26 @@ Features added
Bugs fixed
----------
* LaTeX: Remove extraneous space after author names on PDF title page (refs: #6004)
* #6026: LaTeX: A cross reference to definition list does not work
* #6046: LaTeX: ``TypeError`` is raised when invalid latex_elements given
* #6067: LaTeX: images having a target are concatenated to next line
* #6067: LaTeX: images having a target are not aligned even if specified
* #6019: imgconverter: Including multipage PDF fails
* #6047: autodoc: ``autofunction`` emits a warning for method objects
* #6028: graphviz: Ensure the graphviz filenames are reproducible
* #6068: doctest: ``skipif`` option may remove the code block from documentation
Testing
--------
Release 1.8.4 (released Feb 03, 2019)
=====================================
Bugs fixed
----------
* #3707: latex: no bold checkmark (✔) available.
* #5605: with the documentation language set to Chinese, English words could not
be searched.
@@ -231,10 +353,10 @@ Bugs fixed
* #5966: mathjax has not been loaded on incremental build
* #5960: LaTeX: modified PDF layout since September 2018 TeXLive update of
:file:`parskip.sty`
* #5948: LaTeX: duplicated labels are generated for sections
* #5958: versionadded directive causes crash with Python 3.5.0
Testing
--------
* #5995: autodoc: autodoc_mock_imports conflict with metaclass on Python 3.7
* #5871: texinfo: a section title ``.`` is not allowed
Release 1.8.3 (released Dec 26, 2018)
=====================================

View File

@@ -18,6 +18,7 @@ Documentation using the alabaster theme
* `Click <http://click.pocoo.org/>`__ (customized)
* `coala <https://docs.coala.io/>`__ (customized)
* `CodePy <https://documen.tician.de/codepy/>`__
* `Eve <https://docs.python-eve.org/>`__ (Python REST API framework)
* `Fabric <https://docs.fabfile.org/>`__
* `Fityk <https://fityk.nieto.pl/>`__
* `Flask <http://flask.pocoo.org/docs/>`__
@@ -110,6 +111,7 @@ Documentation using the classic theme
Documentation using the sphinxdoc theme
---------------------------------------
* `ABRT <https://abrt.readthedocs.io/>`__
* `cartopy <https://scitools.org.uk/cartopy/docs/latest/>`__
* `Jython <http://www.jython.org/docs/>`__
* `Matplotlib <https://matplotlib.org/>`__
@@ -183,11 +185,14 @@ Documentation using sphinx_rtd_theme
* `Elemental <http://libelemental.org/documentation/dev/>`__
* `ESWP3 <https://eswp3.readthedocs.io/>`__
* `Ethereum Homestead <http://www.ethdocs.org/>`__
* `Faker <https://faker.readthedocs.io/>`__
* `Fidimag <https://fidimag.readthedocs.io/>`__
* `Flake8 <http://flake8.pycqa.org/>`__
* `Flatpak <http://docs.flatpak.org/>`__
* `FluidDyn <https://fluiddyn.readthedocs.io/>`__
* `Fluidsim <https://fluidsim.readthedocs.io/>`__
* `GeoNode <http://docs.geonode.org/>`__
* `Glances <https://glances.readthedocs.io/>`__
* `Godot <https://godot.readthedocs.io/>`__
* `Graylog <http://docs.graylog.org/>`__
* `GPAW <https://wiki.fysik.dtu.dk/gpaw/>`__ (customized)
@@ -225,6 +230,7 @@ Documentation using sphinx_rtd_theme
* `Phinx <http://docs.phinx.org/>`__
* `phpMyAdmin <https://docs.phpmyadmin.net/>`__
* `PROS <https://pros.cs.purdue.edu/v5/>`__ (customized)
* `Pushkin <http://docs.pushkin.io/>`__
* `Pweave <http://mpastell.com/pweave/>`__
* `PyPy <http://doc.pypy.org/>`__
* `python-sqlparse <https://sqlparse.readthedocs.io/>`__
@@ -234,8 +240,10 @@ Documentation using sphinx_rtd_theme
* `Releases Sphinx extension <https://releases.readthedocs.io/>`__
* `Qtile <http://docs.qtile.org/>`__
* `Quex <http://quex.sourceforge.net/doc/html/main.html>`__
* `QuTiP <http://qutip.org/docs/latest/>`__
* `Satchmo <http://docs.satchmoproject.com/>`__
* `Scapy <https://scapy.readthedocs.io/>`__
* `SimGrid <http://simgrid.gforge.inria.fr/simgrid/latest/doc/>`__
* `SimPy <https://simpy.readthedocs.io/>`__
* `six <https://six.readthedocs.io/>`__
* `SlamData <https://newdocs.slamdata.com>`__
@@ -249,12 +257,14 @@ Documentation using sphinx_rtd_theme
* `Sublime Text Unofficial Documentation <http://docs.sublimetext.info/>`__
* `SunPy <https://docs.sunpy.org/>`__
* `Sylius <http://docs.sylius.org/>`__
* `Syncthing <https://docs.syncthing.net/>`__
* `Tango Controls <https://tango-controls.readthedocs.io/>`__ (customized)
* `Topshelf <http://docs.topshelf-project.com/>`__
* `Theano <http://www.deeplearning.net/software/theano/>`__
* `ThreatConnect <https://docs.threatconnect.com/>`__
* `Tuleap <https://tuleap.net/doc/en/>`__
* `TYPO3 <https://docs.typo3.org/>`__ (customized)
* `Veyon <https://docs.veyon.io/>`__
* `uWSGI <https://uwsgi-docs.readthedocs.io/>`__
* `virtualenv <https://virtualenv.readthedocs.io/>`__
* `Wagtail <https://docs.wagtail.io/>`__
@@ -262,6 +272,7 @@ Documentation using sphinx_rtd_theme
* `Weblate <https://docs.weblate.org/>`__
* `x265 <https://x265.readthedocs.io/>`__
* `ZeroNet <https://zeronet.readthedocs.io/>`__
* `Zulip <https://zulip.readthedocs.io/>`__
Documentation using sphinx_bootstrap_theme
------------------------------------------
@@ -301,6 +312,7 @@ Documentation using a custom theme or integrated in a website
* `GHC - Glasgow Haskell Compiler <https://downloads.haskell.org/~ghc/master/users-guide/>`__
* `Guzzle <http://docs.guzzlephp.org/>`__
* `H2O.ai <http://docs.h2o.ai/>`__
* `Heka <https://hekad.readthedocs.io/>`__
* `Istihza (Turkish Python documentation project) <https://belgeler.yazbel.com/python-istihza/>`__
* `Kombu <http://docs.kombu.me/>`__
* `Lasso <http://lassoguide.com/>`__
@@ -378,6 +390,7 @@ Books produced using Sphinx
* `"Mithril -- The fastest clientside MVC (Japanese)" <https://www.oreilly.co.jp/books/9784873117447/>`__
* `"Pioneers and Prominent Men of Utah" <http://pioneers.rstebbing.com/>`__
* `"Pomodoro Technique Illustrated" (Japanese translation) <https://www.amazon.co.jp/dp/4048689525/>`__
* `"Professional Software Development" <https://mixmastamyk.bitbucket.io/pro_soft_dev/>`__
* `"Python Professional Programming" (in Japanese) <http://www.amazon.co.jp/dp/4798032948/>`__
* `"Python Professional Programming 2nd Edition" (in Japanese) <https://www.amazon.co.jp/dp/479804315X/>`__
* `"Python Professional Programming 3rd Edition" (in Japanese) <https://www.amazon.co.jp/dp/4798053821/>`__

15
bindep.txt Normal file
View File

@@ -0,0 +1,15 @@
texlive [platform:rpm]
texlive-fncychap [platform:rpm]
texlive-titlesec [platform:rpm]
texlive-tabulary [platform:rpm]
texlive-framed [platform:rpm]
texlive-wrapfig [platform:rpm]
texlive-upquote [platform:rpm]
texlive-capt-of [platform:rpm]
texlive-needspace [platform:rpm]
texlive-polyglossia [platform:rpm]
texlive-luatex85 [platform:rpm]
texlive-anyfontsize [platform:rpm]
texlive-ctablestack [platform:rpm]
texlive-gnu-freefont [platform:rpm]
latexmk [platform:rpm]

View File

@@ -337,7 +337,7 @@ a tt:hover {
}
pre {
font-family: 'Consolas', 'DejaVu Sans Mono',
font-family: 'Consolas', 'Courier New', 'DejaVu Sans Mono',
'Bitstream Vera Sans Mono', monospace;
font-size: 13px;
letter-spacing: 0.015em;
@@ -388,32 +388,29 @@ div.admonition, div.warning {
padding: 0;
}
div.admonition p, div.warning p {
div.admonition > p, div.warning > p {
margin: 0.5em 1em 0.5em 1em;
padding: 0;
}
div.admonition pre, div.warning pre {
div.admonition > pre, div.warning > pre {
margin: 0.4em 1em 0.4em 1em;
}
div.admonition p.admonition-title,
div.warning p.admonition-title {
margin-top: 1em;
padding-top: 0.5em;
div.admonition > p.admonition-title,
div.warning > p.admonition-title {
margin-top: 0.5em;
font-weight: bold;
}
div.warning {
border: 1px solid #940000;
/* background-color: #FFCCCF;*/
}
div.warning p.admonition-title {
}
div.admonition ul, div.admonition ol,
div.warning ul, div.warning ol {
div.admonition > ul,
div.admonition > ol,
div.warning > ul,
div.warning > ol {
margin: 0.1em 0.5em 0.5em 3em;
padding: 0;
}

View File

@@ -0,0 +1,11 @@
:orphan:
Tutorial examples
=================
This directory contains a number of examples used in the tutorials. These are
intended to be increasingly complex to demonstrate the various features of
Sphinx, but should aim to be as complicated as necessary but no more.
Individual sections are referenced by line numbers, meaning if you make changes
to the source files, you should update the references in the documentation
accordingly.

View File

@@ -0,0 +1,19 @@
from docutils import nodes
from docutils.parsers.rst import Directive
class HelloWorld(Directive):
def run(self):
paragraph_node = nodes.paragraph(text='Hello World!')
return [paragraph_node]
def setup(app):
app.add_directive("helloworld", HelloWorld)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@@ -0,0 +1,161 @@
from collections import defaultdict
from docutils.parsers.rst import directives
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain
from sphinx.domains import Index
from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode
class RecipeDirective(ObjectDescription):
"""A custom directive that describes a recipe."""
has_content = True
required_arguments = 1
option_spec = {
'contains': directives.unchanged_required,
}
def handle_signature(self, sig, signode):
signode += addnodes.desc_name(text=sig)
return sig
def add_target_and_index(self, name_cls, sig, signode):
signode['ids'].append('recipe' + '-' + sig)
if 'noindex' not in self.options:
ingredients = [
x.strip() for x in self.options.get('contains').split(',')]
recipes = self.env.get_domain('recipe')
recipes.add_recipe(sig, ingredients)
class IngredientIndex(Index):
"""A custom index that creates an ingredient matrix."""
name = 'ingredient'
localname = 'Ingredient Index'
shortname = 'Ingredient'
def generate(self, docnames=None):
content = defaultdict(list)
recipes = {name: (dispname, typ, docname, anchor)
for name, dispname, typ, docname, anchor, _
in self.domain.get_objects()}
recipe_ingredients = self.domain.data['recipe_ingredients']
ingredient_recipes = defaultdict(list)
# flip from recipe_ingredients to ingredient_recipes
for recipe_name, ingredients in recipe_ingredients.items():
for ingredient in ingredients:
ingredient_recipes[ingredient].append(recipe_name)
# convert the mapping of ingredient to recipes to produce the expected
# output, shown below, using the ingredient name as a key to group
#
# name, subtype, docname, anchor, extra, qualifier, description
for ingredient, recipe_names in ingredient_recipes.items():
for recipe_name in recipe_names:
dispname, typ, docname, anchor = recipes[recipe_name]
content[ingredient].append(
(dispname, 0, docname, anchor, docname, '', typ))
# convert the dict to the sorted list of tuples expected
content = sorted(content.items())
return content, True
class RecipeIndex(Index):
"""A custom index that creates an recipe matrix."""
name = 'recipe'
localname = 'Recipe Index'
shortname = 'Recipe'
def generate(self, docnames=None):
content = defaultdict(list)
# sort the list of recipes in alphabetical order
recipes = self.domain.get_objects()
recipes = sorted(recipes, key=lambda recipe: recipe[0])
# generate the expected output, shown below, from the above using the
# first letter of the recipe as a key to group thing
#
# name, subtype, docname, anchor, extra, qualifier, description
for name, dispname, typ, docname, anchor, _ in recipes:
content[dispname[0].lower()].append(
(dispname, 0, docname, anchor, docname, '', typ))
# convert the dict to the sorted list of tuples expected
content = sorted(content.items())
return content, True
class RecipeDomain(Domain):
name = 'recipe'
label = 'Recipe Sample'
roles = {
'ref': XRefRole()
}
directives = {
'recipe': RecipeDirective,
}
indices = {
RecipeIndex,
IngredientIndex
}
initial_data = {
'recipes': [], # object list
'recipe_ingredients': {}, # name -> object
}
def get_full_qualified_name(self, node):
return '{}.{}'.format('recipe', node.arguments[0])
def get_objects(self):
for obj in self.data['recipes']:
yield(obj)
def resolve_xref(self, env, fromdocname, builder, typ, target, node,
contnode):
match = [(docname, anchor)
for name, sig, typ, docname, anchor, prio
in self.get_objects() if sig == target]
if len(match) > 0:
todocname = match[0][0]
targ = match[0][1]
return make_refnode(builder, fromdocname, todocname, targ,
contnode, targ)
else:
print('Awww, found nothing')
return None
def add_recipe(self, signature, ingredients):
"""Add a new recipe to the domain."""
name = '{}.{}'.format('recipe', signature)
anchor = 'recipe-{}'.format(signature)
self.data['recipe_ingredients'][name] = ingredients
# name, dispname, type, docname, anchor, priority
self.data['recipes'].append(
(name, signature, 'Recipe', self.env.docname, anchor, 0))
def setup(app):
app.add_domain(RecipeDomain)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@@ -0,0 +1,124 @@
from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx.locale import _
from sphinx.util.docutils import SphinxDirective
class todo(nodes.Admonition, nodes.Element):
pass
class todolist(nodes.General, nodes.Element):
pass
def visit_todo_node(self, node):
self.visit_admonition(node)
def depart_todo_node(self, node):
self.depart_admonition(node)
class TodolistDirective(Directive):
def run(self):
return [todolist('')]
class TodoDirective(SphinxDirective):
# this enables content in the directive
has_content = True
def run(self):
targetid = 'todo-%d' % self.env.new_serialno('todo')
targetnode = nodes.target('', '', ids=[targetid])
todo_node = todo('\n'.join(self.content))
todo_node += nodes.title(_('Todo'), _('Todo'))
self.state.nested_parse(self.content, self.content_offset, todo_node)
if not hasattr(self.env, 'todo_all_todos'):
self.env.todo_all_todos = []
self.env.todo_all_todos.append({
'docname': self.env.docname,
'lineno': self.lineno,
'todo': todo_node.deepcopy(),
'target': targetnode,
})
return [targetnode, todo_node]
def purge_todos(app, env, docname):
if not hasattr(env, 'todo_all_todos'):
return
env.todo_all_todos = [todo for todo in env.todo_all_todos
if todo['docname'] != docname]
def process_todo_nodes(app, doctree, fromdocname):
if not app.config.todo_include_todos:
for node in doctree.traverse(todo):
node.parent.remove(node)
# Replace all todolist nodes with a list of the collected todos.
# Augment each todo with a backlink to the original location.
env = app.builder.env
for node in doctree.traverse(todolist):
if not app.config.todo_include_todos:
node.replace_self([])
continue
content = []
for todo_info in env.todo_all_todos:
para = nodes.paragraph()
filename = env.doc2path(todo_info['docname'], base=None)
description = (
_('(The original entry is located in %s, line %d and can be found ') %
(filename, todo_info['lineno']))
para += nodes.Text(description, description)
# Create a reference
newnode = nodes.reference('', '')
innernode = nodes.emphasis(_('here'), _('here'))
newnode['refdocname'] = todo_info['docname']
newnode['refuri'] = app.builder.get_relative_uri(
fromdocname, todo_info['docname'])
newnode['refuri'] += '#' + todo_info['target']['refid']
newnode.append(innernode)
para += newnode
para += nodes.Text('.)', '.)')
# Insert into the todolist
content.append(todo_info['todo'])
content.append(para)
node.replace_self(content)
def setup(app):
app.add_config_value('todo_include_todos', False, 'html')
app.add_node(todolist)
app.add_node(todo,
html=(visit_todo_node, depart_todo_node),
latex=(visit_todo_node, depart_todo_node),
text=(visit_todo_node, depart_todo_node))
app.add_directive('todo', TodoDirective)
app.add_directive('todolist', TodolistDirective)
app.connect('doctree-resolved', process_todo_nodes)
app.connect('env-purge-doc', purge_todos)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@@ -1,85 +1,89 @@
Developing a "Hello world" directive
Developing a "Hello world" extension
====================================
The objective of this tutorial is to create a very basic extension that adds a new
directive that outputs a paragraph containing `hello world`.
The objective of this tutorial is to create a very basic extension that adds a
new directive. This directive will output a paragraph containing "hello world".
Only basic information is provided in this tutorial. For more information,
refer to the :doc:`other tutorials <index>` that go into more
details.
Only basic information is provided in this tutorial. For more information, refer
to the :doc:`other tutorials <index>` that go into more details.
.. warning:: For this extension, you will need some basic understanding of docutils_
.. warning::
For this extension, you will need some basic understanding of docutils_
and Python.
Creating a new extension file
-----------------------------
Your extension file could be in any folder of your project. In our case,
let's do the following:
Overview
--------
#. Create an :file:`_ext` folder in :file:`source`.
We want the extension to add the following to Sphinx:
* A ``helloworld`` directive, that will simply output the text "hello world".
Prerequisites
-------------
We will not be distributing this plugin via `PyPI`_ and will instead include it
as part of an existing project. This means you will need to use an existing
project or create a new one using :program:`sphinx-quickstart`.
We assume you are using separate source (:file:`source`) and build
(:file:`build`) folders. Your extension file could be in any folder of your
project. In our case, let's do the following:
#. Create an :file:`_ext` folder in :file:`source`
#. Create a new Python file in the :file:`_ext` folder called
:file:`helloworld.py`.
:file:`helloworld.py`
Here is an example of the folder structure you might obtain:
Here is an example of the folder structure you might obtain:
.. code-block:: text
.. code-block:: text
└── source
   ├── _ext
  └── helloworld.py
   ├── _static
   ├── conf.py
   ├── somefolder
   ├── index.rst
   ├── somefile.rst
   └── someotherfile.rst
└── source
   ├── _ext
  └── helloworld.py
   ├── _static
   ├── _themes
   ├── conf.py
   ├── somefolder
   ├── somefile.rst
   └── someotherfile.rst
Writing the extension
---------------------
Open :file:`helloworld.py` and paste the following code in it:
.. code-block:: python
.. literalinclude:: examples/helloworld.py
:language: python
:linenos:
from docutils import nodes
from docutils.parsers.rst import Directive
Some essential things are happening in this example, and you will see them for
all directives.
.. rubric:: The directive class
class HelloWorld(Directive):
def run(self):
paragraph_node = nodes.paragraph(text='Hello World!')
return [paragraph_node]
Our new directive is declared in the ``HelloWorld`` class.
.. literalinclude:: examples/helloworld.py
:language: python
:linenos:
:lines: 5-9
def setup(app):
app.add_directive("helloworld", HelloWorld)
Some essential things are happening in this example, and you will see them
in all directives:
.. rubric:: Directive declaration
Our new directive is declared in the ``HelloWorld`` class, it extends
docutils_' ``Directive`` class. All extensions that create directives
should extend this class.
.. rubric:: ``run`` method
This method is a requirement and it is part of every directive. It contains
the main logic of the directive and it returns a list of docutils nodes to
be processed by Sphinx.
This class extends the docutils_' ``Directive`` class. All extensions that
create directives should extend this class.
.. seealso::
:doc:`todo`
`The docutils documentation on creating directives <docutils directives>`_
.. rubric:: docutils nodes
The ``run`` method returns a list of nodes. Nodes are docutils' way of
representing the content of a document. There are many types of nodes
available: text, paragraph, reference, table, etc.
This class contains a ``run`` method. This method is a requirement and it is
part of every directive. It contains the main logic of the directive and it
returns a list of docutils nodes to be processed by Sphinx. These nodes are
docutils' way of representing the content of a document. There are many types of
nodes available: text, paragraph, reference, table, etc.
.. seealso::
@@ -89,74 +93,97 @@ The ``nodes.paragraph`` class creates a new paragraph node. A paragraph
node typically contains some text that we can set during instantiation using
the ``text`` parameter.
.. rubric:: ``setup`` function
.. rubric:: The ``setup`` function
.. currentmodule:: sphinx.application
This function is a requirement. We use it to plug our new directive into
Sphinx.
The simplest thing you can do it call the ``app.add_directive`` method.
.. note::
.. literalinclude:: examples/helloworld.py
:language: python
:linenos:
:lines: 12-
The first argument is the name of the directive itself as used in an rST file.
The simplest thing you can do it call the :meth:`~Sphinx.add_directive` method,
which is what we've done here. For this particular call, the first argument is
the name of the directive itself as used in a reST file. In this case, we would
use ``helloworld``. For example:
In our case, we would use ``helloworld``:
.. code-block:: rst
.. code-block:: rst
Some intro text here...
Some intro text here...
.. helloworld::
.. helloworld::
Some more text here...
Some more text here...
We also return the :ref:`extension metadata <ext-metadata>` that indicates the
version of our extension, along with the fact that it is safe to use the
extension for both parallel reading and writing.
Updating the conf.py file
-------------------------
Using the extension
-------------------
The extension file has to be declared in your :file:`conf.py` file to make
Sphinx aware of it:
The extension has to be declared in your :file:`conf.py` file to make Sphinx
aware of it. There are two steps necessary here:
#. Open :file:`conf.py`. It is in the :file:`source` folder by default.
#. Add ``sys.path.append(os.path.abspath("./_ext"))`` before
the ``extensions`` variable declaration (if it exists).
#. Update or create the ``extensions`` list and add the
extension file name to the list:
#. Add the :file:`_ext` directory to the `Python path`_ using
``sys.path.append``. This should be placed at the top of the file.
.. code-block:: python
#. Update or create the :confval:`extensions` list and add the extension file
name to the list
extensions.append('helloworld')
For example:
You can now use the extension.
.. code-block:: python
.. admonition:: Example
import os
import sys
.. code-block:: rst
sys.path.append(os.path.abspath("./_ext"))
Some intro text here...
extensions = ['helloworld']
.. helloworld::
.. tip::
Some more text here...
We're not distributing this extension as a `Python package`_, we need to
modify the `Python path`_ so Sphinx can find our extension. This is why we
need the call to ``sys.path.append``.
The sample above would generate:
You can now use the extension in a file. For example:
.. code-block:: text
.. code-block:: rst
Some intro text here...
Some intro text here...
Hello World!
.. helloworld::
Some more text here...
Some more text here...
The sample above would generate:
.. code-block:: text
Some intro text here...
Hello World!
Some more text here...
Further reading
---------------
This is the very basic principle of an extension that creates a new directive.
For a more advanced example, refer to :doc:`todo`.
Further reading
---------------
You can create your own nodes if needed, refer to the
:doc:`todo` for more information.
.. _docutils: http://docutils.sourceforge.net/
.. _`docutils nodes`: http://docutils.sourceforge.net/docs/ref/doctree.html
.. _docutils directives: http://docutils.sourceforge.net/docs/howto/rst-directives.html
.. _docutils nodes: http://docutils.sourceforge.net/docs/ref/doctree.html
.. _PyPI: https://pypi.org/
.. _Python package: https://packaging.python.org/
.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH

View File

@@ -9,3 +9,4 @@ Refer to the following tutorials to get started with extension development.
helloworld
todo
recipe

View File

@@ -0,0 +1,217 @@
Developing a "recipe" extension
===============================
The objective of this tutorial is to illustrate roles, directives and domains.
Once complete, we will be able to use this extension to describe a recipe and
reference that recipe from elsewhere in our documentation.
.. note::
This tutorial is based on a guide first published on `opensource.com`_ and
is provided here with the original author's permission.
.. _opensource.com: https://opensource.com/article/18/11/building-custom-workflows-sphinx
Overview
--------
We want the extension to add the following to Sphinx:
* A ``recipe`` :term:`directive`, containing some content describing the recipe
steps, along with a ``:contains:`` option highlighting the main ingredients
of the recipe.
* A ``ref`` :term:`role`, which provides a cross-reference to the recipe
itself.
* A ``recipe`` :term:`domain`, which allows us to tie together the above role
and domain, along with things like indices.
For that, we will need to add the following elements to Sphinx:
* A new directive called ``recipe``
* New indexes to allow us to reference ingredient and recipes
* A new domain called ``recipe``, which will contain the ``recipe`` directive
and ``ref`` role
Prerequisites
-------------
We need the same setup as in :doc:`the previous extensions <todo>`. This time,
we will be putting out extension in a file called :file:`recipe.py`.
Here is an example of the folder structure you might obtain:
.. code-block:: text
└── source
   ├── _ext
  └── recipe.py
   ├── conf.py
   └── index.rst
Writing the extension
---------------------
Open :file:`recipe.py` and paste the following code in it, all of which we will
explain in detail shortly:
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
Let's look at each piece of this extension step-by-step to explain what's going
on.
.. rubric:: The directive class
The first thing to examine is the ``RecipeDirective`` directive:
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
:lines: 17-37
Unlike :doc:`helloworld` and :doc:`todo`, this directive doesn't derive from
:class:`docutils.parsers.rst.Directive` and doesn't define a ``run`` method.
Instead, it derives from :class:`sphinx.directives.ObjectDescription` and
defines ``handle_signature`` and ``add_target_and_index`` methods. This is
because ``ObjectDescription`` is a special-purpose directive that's intended
for describing things like classes, functions, or, in our case, recipes. More
specifically, ``handle_signature`` implements parsing the signature of the
directive and passes on the object's name and type to its superclass, while
``add_taget_and_index`` adds a target (to link to) and an entry to the index
for this node.
We also see that this directive defines ``has_content``, ``required_arguments``
and ``option_spec``. Unlike the ``TodoDirective`` directive added in the
:doc:`previous tutorial <todo>`, this directive takes a single argument, the
recipe name, and an option, ``contains``, in addition to the nested
reStructuredText in the body.
.. rubric:: The index classes
.. currentmodule:: sphinx.domains
.. todo:: Add brief overview of indices
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
:lines: 40-102
Both ``IngredientIndex`` and ``RecipeIndex`` are derived from :class:`Index`.
They implement custom logic to generate a tuple of values that define the
index. Note that ``RecipeIndex`` is a simple index that has only one entry.
Extending it to cover more object types is not yet part of the code.
Both indices use the method :meth:`Index.generate` to do their work. This
method combines the information from our domain, sorts it, and returns it in a
list structure that will be accepted by Sphinx. This might look complicated but
all it really is is a list of tuples like ``('tomato', 'TomatoSoup', 'test',
'rec-TomatoSoup',...)``. Refer to the :doc:`domain API guide
</extdev/domainapi>` for more information on this API.
.. rubric:: The domain
A Sphinx domain is a specialized container that ties together roles,
directives, and indices, among other things. Let's look at the domain we're
creating here.
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
:lines: 105-155
There are some interesting things to note about this ``recipe`` domain and domains
in general. Firstly, we actually register our directives, roles and indices
here, via the ``directives``, ``roles`` and ``indices`` attributes, rather than
via calls later on in ``setup``. We can also note that we aren't actually
defining a custom role and are instead reusing the
:class:`sphinx.roles.XRefRole` role and defining the
:class:`sphinx.domains.Domain.resolve_xref` method. This method takes two
arguments, ``typ`` and ``target``, which refer to the cross-reference type and
its target name. We'll use ``target`` to resolve our destination from our
domain's ``recipes`` because we currently have only one type of node.
Moving on, we can see that we've defined ``initial_data``. The values defined in
``initial_data`` will be copied to ``env.domaindata[domain_name]`` as the
initial data of the domain, and domain instances can access it via
``self.data``. We see that we have defined two items in ``initial_data``:
``recipes`` and ``recipe2ingredient``. These contain a list of all objects
defined (i.e. all recipes) and a hash that maps a canonical ingredient name to
the list of objects. The way we name objects is common across our extension and
is defined in the ``get_full_qualified_name`` method. For each object created,
the canonical name is ``recipe.<recipename>``, where ``<recipename>`` is the
name the documentation writer gives the object (a recipe). This enables the
extension to use different object types that share the same name. Having a
canonical name and central place for our objects is a huge advantage. Both our
indices and our cross-referencing code use this feature.
.. rubric:: The ``setup`` function
.. currentmodule:: sphinx.application
:doc:`As always <todo>`, the ``setup`` function is a requirement and is used to
hook the various parts of our extension into Sphinx. Let's look at the
``setup`` function for this extension.
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
:lines: 158-
This looks a little different to what we're used to seeing. There are no calls
to :meth:`~Sphinx.add_directive` or even :meth:`~Sphinx.add_role`. Instead, we
have a single call to :meth:`~Sphinx.add_domain` followed by some
initialization of the :ref:`standard domain <domains-std>`. This is because we
had already registered our directives, roles and indexes as part of the
directive itself.
Using the extension
-------------------
You can now use the extension throughout your project. For example:
.. code-block:: rst
:caption: index.rst
Joe's Recipes
=============
Below are a collection of my favourite recipes. I highly recommend the
:recipe:ref:`TomatoSoup` recipe in particular!
.. toctree::
tomato-soup
.. code-block:: rst
:caption: tomato-soup.rst
The recipe contains `tomato` and `cilantro`.
.. recipe:recipe:: TomatoSoup
:contains: tomato cilantro salt pepper
This recipe is a tasty tomato soup, combine all ingredients
and cook.
The important things to note are the use of the ``:recipe:ref:`` role to
cross-reference the recipe actually defined elsewhere (using the
``:recipe:recipe:`` directive.
Further reading
---------------
For more information, refer to the `docutils`_ documentation and
:doc:`/extdev/index`.
.. _docutils: http://docutils.sourceforge.net/docs/

View File

@@ -1,114 +1,99 @@
Developing a "TODO" extension
=============================
This section is intended as a walkthrough for the creation of custom extensions.
It covers the basics of writing and activating an extension, as well as
commonly used features of extensions.
As an example, we will cover a "todo" extension that adds capabilities to
include todo entries in the documentation, and to collect these in a central
place. (A similar "todo" extension is distributed with Sphinx.)
The objective of this tutorial is to create a more comprehensive extension than
that created in :doc:`helloworld`. Whereas that guide just covered writing a
custom :term:`directive`, this guide adds multiple directives, along with custom
nodes, additional config values and custom event handlers. To this end, we will
cover a ``todo`` extension that adds capabilities to include todo entries in the
documentation, and to collect these in a central place. This is similar the
``sphinxext.todo`` extension distributed with Sphinx.
Extension Design
----------------
Overview
--------
.. note:: To understand the design this extension, refer to
.. note::
To understand the design of this extension, refer to
:ref:`important-objects` and :ref:`build-phases`.
We want the extension to add the following to Sphinx:
* A "todo" directive, containing some content that is marked with "TODO", and
only shown in the output if a new config value is set. (Todo entries should
not be in the output by default.)
* A ``todo`` directive, containing some content that is marked with "TODO" and
only shown in the output if a new config value is set. Todo entries should not
be in the output by default.
* A "todolist" directive that creates a list of all todo entries throughout the
* A ``todolist`` directive that creates a list of all todo entries throughout the
documentation.
For that, we will need to add the following elements to Sphinx:
* New directives, called ``todo`` and ``todolist``.
* New document tree nodes to represent these directives, conventionally also
called ``todo`` and ``todolist``. We wouldn't need new nodes if the new
directives only produced some content representable by existing nodes.
* A new config value ``todo_include_todos`` (config value names should start
with the extension name, in order to stay unique) that controls whether todo
entries make it into the output.
* New event handlers: one for the :event:`doctree-resolved` event, to replace
the todo and todolist nodes, and one for :event:`env-purge-doc` (the reason
for that will be covered later).
The Setup Function
------------------
Prerequisites
-------------
.. currentmodule:: sphinx.application
As with :doc:`helloworld`, we will not be distributing this plugin via PyPI so
once again we need a Sphinx project to call this from. You can use an existing
project or create a new one using :program:`sphinx-quickstart`.
The new elements are added in the extension's setup function. Let us create a
new Python module called :file:`todo.py` and add the setup function::
We assume you are using separate source (:file:`source`) and build
(:file:`build`) folders. Your extension file could be in any folder of your
project. In our case, let's do the following:
def setup(app):
app.add_config_value('todo_include_todos', False, 'html')
#. Create an :file:`_ext` folder in :file:`source`
#. Create a new Python file in the :file:`_ext` folder called :file:`todo.py`
app.add_node(todolist)
app.add_node(todo,
html=(visit_todo_node, depart_todo_node),
latex=(visit_todo_node, depart_todo_node),
text=(visit_todo_node, depart_todo_node))
Here is an example of the folder structure you might obtain:
app.add_directive('todo', TodoDirective)
app.add_directive('todolist', TodolistDirective)
app.connect('doctree-resolved', process_todo_nodes)
app.connect('env-purge-doc', purge_todos)
.. code-block:: text
return {'version': '0.1'} # identifies the version of our extension
The calls in this function refer to classes and functions not yet written. What
the individual calls do is the following:
* :meth:`~Sphinx.add_config_value` lets Sphinx know that it should recognize the
new *config value* ``todo_include_todos``, whose default value should be
``False`` (this also tells Sphinx that it is a boolean value).
If the third argument was ``'html'``, HTML documents would be full rebuild if the
config value changed its value. This is needed for config values that
influence reading (build :ref:`phase 1 <build-phases>`).
* :meth:`~Sphinx.add_node` adds a new *node class* to the build system. It also
can specify visitor functions for each supported output format. These visitor
functions are needed when the new nodes stay until :ref:`phase 4 <build-phases>`
-- since the ``todolist`` node is always replaced in :ref:`phase 3 <build-phases>`,
it doesn't need any.
We need to create the two node classes ``todo`` and ``todolist`` later.
* :meth:`~Sphinx.add_directive` adds a new *directive*, given by name and class.
The handler functions are created later.
* Finally, :meth:`~Sphinx.connect` adds an *event handler* to the event whose
name is given by the first argument. The event handler function is called
with several arguments which are documented with the event.
└── source
   ├── _ext
  └── todo.py
   ├── _static
   ├── conf.py
   ├── somefolder
   ├── index.rst
   ├── somefile.rst
   └── someotherfile.rst
The Node Classes
----------------
Writing the extension
---------------------
Let's start with the node classes::
Open :file:`todo.py` and paste the following code in it, all of which we will
explain in detail shortly:
from docutils import nodes
.. literalinclude:: examples/todo.py
:language: python
:linenos:
class todo(nodes.Admonition, nodes.Element):
pass
This is far more extensive extension than the one detailed in :doc:`helloworld`,
however, we will will look at each piece step-by-step to explain what's
happening.
class todolist(nodes.General, nodes.Element):
pass
.. rubric:: The node classes
def visit_todo_node(self, node):
self.visit_admonition(node)
Let's start with the node classes:
def depart_todo_node(self, node):
self.depart_admonition(node)
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 8-21
Node classes usually don't have to do anything except inherit from the standard
docutils classes defined in :mod:`docutils.nodes`. ``todo`` inherits from
@@ -122,81 +107,54 @@ is just a "general" node.
<http://docutils.sourceforge.net/docs/ref/doctree.html>`__ and :ref:`Sphinx
<nodes>`.
The Directive Classes
---------------------
.. rubric:: The directive classes
A directive class is a class deriving usually from
:class:`docutils.parsers.rst.Directive`. The directive interface is also
:class:`docutils.parsers.rst.Directive`. The directive interface is also
covered in detail in the `docutils documentation`_; the important thing is that
the class should have attributes that configure the allowed markup,
and a ``run`` method that returns a list of nodes.
the class should have attributes that configure the allowed markup, and a
``run`` method that returns a list of nodes.
The ``todolist`` directive is quite simple::
Looking first at the ``TodolistDirective`` directive:
from docutils.parsers.rst import Directive
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 24-27
class TodolistDirective(Directive):
It's very simple, creating and returning an instance of our ``todolist`` node
class. The ``TodolistDirective`` directive itself has neither content nor
arguments that need to be handled. That brings us to the ``TodoDirective``
directive:
def run(self):
return [todolist('')]
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 30-53
An instance of our ``todolist`` node class is created and returned. The
todolist directive has neither content nor arguments that need to be handled.
The ``todo`` directive function looks like this::
from sphinx.locale import _
class TodoDirective(Directive):
# this enables content in the directive
has_content = True
def run(self):
env = self.state.document.settings.env
targetid = "todo-%d" % env.new_serialno('todo')
targetnode = nodes.target('', '', ids=[targetid])
todo_node = todo('\n'.join(self.content))
todo_node += nodes.title(_('Todo'), _('Todo'))
self.state.nested_parse(self.content, self.content_offset, todo_node)
if not hasattr(env, 'todo_all_todos'):
env.todo_all_todos = []
env.todo_all_todos.append({
'docname': env.docname,
'lineno': self.lineno,
'todo': todo_node.deepcopy(),
'target': targetnode,
})
return [targetnode, todo_node]
Several important things are covered here. First, as you can see, you can refer
to the :ref:`build environment instance <important-objects>` using ``self.state.document.settings.env``.
Then, to act as a link target (from the todolist), the todo directive needs to
return a target node in addition to the todo node. The target ID (in HTML, this
will be the anchor name) is generated by using ``env.new_serialno`` which
returns a new unique integer on each call and therefore leads to unique target
names. The target node is instantiated without any text (the first two
arguments).
Several important things are covered here. First, as you can see, we're now
subclassing the :class:`~sphinx.util.docutils.SphinxDirective` helper class
instead of the usual :class:`~docutils.parsers.rst.Directive` class. This
gives us access to the :ref:`build environment instance <important-objects>`
using the ``self.env`` property. Without this, we'd have to use the rather
convoluted ``self.state.document.settings.env``. Then, to act as a link target
(from ``TodolistDirective``), the ``TodoDirective`` directive needs to return a
target node in addition to the ``todo`` node. The target ID (in HTML, this will
be the anchor name) is generated by using ``env.new_serialno`` which returns a
new unique integer on each call and therefore leads to unique target names. The
target node is instantiated without any text (the first two arguments).
On creating admonition node, the content body of the directive are parsed using
``self.state.nested_parse``. The first argument gives the content body, and
the second one gives content offset. The third argument gives the parent node
of parsed result, in our case the ``todo`` node.
Then, the todo node is added to the environment. This is needed to be able to
create a list of all todo entries throughout the documentation, in the place
where the author puts a ``todolist`` directive. For this case, the environment
attribute ``todo_all_todos`` is used (again, the name should be unique, so it is
prefixed by the extension name). It does not exist when a new environment is
created, so the directive must check and create it if necessary. Various
information about the todo entry's location are stored along with a copy of the
node.
of parsed result, in our case the ``todo`` node. Following this, the ``todo``
node is added to the environment. This is needed to be able to create a list of
all todo entries throughout the documentation, in the place where the author
puts a ``todolist`` directive. For this case, the environment attribute
``todo_all_todos`` is used (again, the name should be unique, so it is prefixed
by the extension name). It does not exist when a new environment is created, so
the directive must check and create it if necessary. Various information about
the todo entry's location are stored along with a copy of the node.
In the last line, the nodes that should be put into the doctree are returned:
the target node and the admonition node.
@@ -217,18 +175,20 @@ The node structure that the directive returns looks like this::
| ... |
+--------------------+
.. rubric:: The event handlers
The Event Handlers
------------------
Event handlers are one of Sphinx's most powerful features, providing a way to
do hook into any part of the documentation process. There are many events
provided by Sphinx itself, as detailed in :ref:`the API guide <events>`, and
we're going to use a subset of them here.
Finally, let's look at the event handlers. First, the one for the
:event:`env-purge-doc` event::
Let's look at the event handlers used in the above example. First, the one for
the :event:`env-purge-doc` event:
def purge_todos(app, env, docname):
if not hasattr(env, 'todo_all_todos'):
return
env.todo_all_todos = [todo for todo in env.todo_all_todos
if todo['docname'] != docname]
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 56-61
Since we store information from source files in the environment, which is
persistent, it may become out of date when the source file changes. Therefore,
@@ -238,62 +198,144 @@ Here we clear out all todos whose docname matches the given one from the
``todo_all_todos`` list. If there are todos left in the document, they will be
added again during parsing.
The other handler belongs to the :event:`doctree-resolved` event. This event is
emitted at the end of :ref:`phase 3 <build-phases>` and allows custom resolving
to be done::
The other handler belongs to the :event:`doctree-resolved` event:
def process_todo_nodes(app, doctree, fromdocname):
if not app.config.todo_include_todos:
for node in doctree.traverse(todo):
node.parent.remove(node)
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 64-103
# Replace all todolist nodes with a list of the collected todos.
# Augment each todo with a backlink to the original location.
env = app.builder.env
for node in doctree.traverse(todolist):
if not app.config.todo_include_todos:
node.replace_self([])
continue
content = []
for todo_info in env.todo_all_todos:
para = nodes.paragraph()
filename = env.doc2path(todo_info['docname'], base=None)
description = (
_('(The original entry is located in %s, line %d and can be found ') %
(filename, todo_info['lineno']))
para += nodes.Text(description, description)
# Create a reference
newnode = nodes.reference('', '')
innernode = nodes.emphasis(_('here'), _('here'))
newnode['refdocname'] = todo_info['docname']
newnode['refuri'] = app.builder.get_relative_uri(
fromdocname, todo_info['docname'])
newnode['refuri'] += '#' + todo_info['target']['refid']
newnode.append(innernode)
para += newnode
para += nodes.Text('.)', '.)')
# Insert into the todolist
content.append(todo_info['todo'])
content.append(para)
node.replace_self(content)
It is a bit more involved. If our new "todo_include_todos" config value is
false, all todo and todolist nodes are removed from the documents.
If not, todo nodes just stay where and how they are. Todolist nodes are
The :event:`doctree-resolved` event is emitted at the end of :ref:`phase 3
(resolving) <build-phases>` and allows custom resolving to be done. The handler
we have written for this event is a bit more involved. If the
``todo_include_todos`` config value (which we'll describe shortly) is false,
all ``todo`` and ``todolist`` nodes are removed from the documents. If not,
``todo`` nodes just stay where and how they are. ``todolist`` nodes are
replaced by a list of todo entries, complete with backlinks to the location
where they come from. The list items are composed of the nodes from the todo
entry and docutils nodes created on the fly: a paragraph for each entry,
containing text that gives the location, and a link (reference node containing
an italic node) with the backreference. The reference URI is built by
``app.builder.get_relative_uri`` which creates a suitable URI depending on the
used builder, and appending the todo node's (the target's) ID as the anchor
name.
where they come from. The list items are composed of the nodes from the
``todo`` entry and docutils nodes created on the fly: a paragraph for each
entry, containing text that gives the location, and a link (reference node
containing an italic node) with the backreference. The reference URI is built
by :meth:`sphinx.builders.Builder.get_relative_uri`` which creates a suitable
URI depending on the used builder, and appending the todo node's (the target's)
ID as the anchor name.
.. rubric:: The ``setup`` function
.. currentmodule:: sphinx.application
As noted :doc:`previously <helloworld>`, the ``setup`` function is a requirement
and is used to plug directives into Sphinx. However, we also use it to hook up
the other parts of our extension. Let's look at our ``setup`` function:
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 106-
The calls in this function refer to the classes and functions we added earlier.
What the individual calls do is the following:
* :meth:`~Sphinx.add_config_value` lets Sphinx know that it should recognize the
new *config value* ``todo_include_todos``, whose default value should be
``False`` (this also tells Sphinx that it is a boolean value).
If the third argument was ``'html'``, HTML documents would be full rebuild if the
config value changed its value. This is needed for config values that
influence reading (build :ref:`phase 1 (reading) <build-phases>`).
* :meth:`~Sphinx.add_node` adds a new *node class* to the build system. It also
can specify visitor functions for each supported output format. These visitor
functions are needed when the new nodes stay until :ref:`phase 4 (writing)
<build-phases>`. Since the ``todolist`` node is always replaced in
:ref:`phase 3 (resolving) <build-phases>`, it doesn't need any.
* :meth:`~Sphinx.add_directive` adds a new *directive*, given by name and class.
* Finally, :meth:`~Sphinx.connect` adds an *event handler* to the event whose
name is given by the first argument. The event handler function is called
with several arguments which are documented with the event.
With this, our extension is complete.
Using the extension
-------------------
As before, we need to enable the extension by declaring it in our
:file:`conf.py` file. There are two steps necessary here:
#. Add the :file:`_ext` directory to the `Python path`_ using
``sys.path.append``. This should be placed at the top of the file.
#. Update or create the :confval:`extensions` list and add the extension file
name to the list
In addition, we may wish to set the ``todo_include_todos`` config value. As
noted above, this defaults to ``False`` but we can set it explicitly.
For example:
.. code-block:: python
import os
import sys
sys.path.append(os.path.abspath("./_ext"))
extensions = ['todo']
todo_include_todos = False
You can now use the extension throughout your project. For example:
.. code-block:: rst
:caption: index.rst
Hello, world
============
.. toctree::
somefile.rst
someotherfile.rst
Hello world. Below is the list of TODOs.
.. todolist::
.. code-block:: rst
:caption: somefile.rst
foo
===
Some intro text here...
.. todo:: Fix this
.. code-block:: rst
:caption: someotherfile.rst
bar
===
Some more text here...
.. todo:: Fix that
Because we have configured ``todo_include_todos`` to ``False``, we won't
actually see anything rendered for the ``todo`` and ``todolist`` directives.
However, if we toggle this to true, we will see the output described
previously.
Further reading
---------------
For more information, refer to the `docutils`_ documentation and
:doc:`/extdev/index`.
.. _docutils: http://docutils.sourceforge.net/docs/
.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH
.. _docutils documentation: http://docutils.sourceforge.net/docs/ref/rst/directives.html

View File

@@ -312,6 +312,41 @@ The following is a list of deprecated interfaces.
- (will be) Removed
- Alternatives
* - ``sphinx.ext.autodoc.importer.MockFinder``
- 2.1
- 4.0
- ``sphinx.ext.autodoc.mock.MockFinder``
* - ``sphinx.ext.autodoc.importer.MockLoader``
- 2.1
- 4.0
- ``sphinx.ext.autodoc.mock.MockLoader``
* - ``sphinx.ext.autodoc.importer.mock()``
- 2.1
- 4.0
- ``sphinx.ext.autodoc.mock.mock()``
* - ``sphinx.ext.autosummary.autolink_role()``
- 2.1
- 4.0
- ``sphinx.ext.autosummary.AutoLink``
* - ``sphinx.util.i18n.find_catalog()``
- 2.1
- 4.0
- ``sphinx.util.i18n.docname_to_domain()``
* - ``sphinx.util.i18n.find_catalog_files()``
- 2.1
- 4.0
- ``sphinx.util.i18n.CatalogRepository``
* - ``sphinx.util.i18n.find_catalog_source_files()``
- 2.1
- 4.0
- ``sphinx.util.i18n.CatalogRepository``
* - ``encoding`` argument of ``autodoc.Documenter.get_doc()``,
``autodoc.DocstringSignatureMixin.get_doc()``,
``autodoc.DocstringSignatureMixin._find_signature()``, and
@@ -353,6 +388,11 @@ The following is a list of deprecated interfaces.
- 4.0
- ``docutils.nodes.abbreviation``
* - ``sphinx.builders.applehelp``
- 2.0
- 4.0
- ``sphinxcontrib.applehelp``
* - ``sphinx.builders.devhelp``
- 2.0
- 4.0
@@ -363,11 +403,36 @@ The following is a list of deprecated interfaces.
- 4.0
- ``sphinx.builders.epub3.validate_config_values()``
* - ``sphinx.builders.html.JSONHTMLBuilder``
- 2.0
- 4.0
- ``sphinx.builders.serializinghtml.JSONHTMLBuilder``
* - ``sphinx.builders.html.PickleHTMLBuilder``
- 2.0
- 4.0
- ``sphinx.builders.serializinghtml.PickleHTMLBuilder``
* - ``sphinx.builders.html.SerializingHTMLBuilder``
- 2.0
- 4.0
- ``sphinx.builders.serializinghtml.SerializingHTMLBuilder``
* - ``sphinx.builders.html.SingleFileHTMLBuilder``
- 2.0
- 4.0
- ``sphinx.builders.singlehtml.SingleFileHTMLBuilder``
* - ``sphinx.builders.html.WebHTMLBuilder``
- 2.0
- 4.0
- ``sphinx.builders.serializinghtml.PickleHTMLBuilder``
* - ``sphinx.builders.htmlhelp``
- 2.0
- 4.0
- ``sphinxcontrib.htmlhelp``
* - ``sphinx.builders.htmlhelp.HTMLHelpBuilder.open_file()``
- 2.0
- 4.0
@@ -443,6 +508,31 @@ The following is a list of deprecated interfaces.
- 4.0
- ``sphinxcontrib.jsmath``
* - ``sphinx.roles.abbr_role()``
- 2.0
- 4.0
- ``sphinx.roles.Abbreviation``
* - ``sphinx.roles.emph_literal_role()``
- 2.0
- 4.0
- ``sphinx.roles.EmphasizedLiteral``
* - ``sphinx.roles.menusel_role()``
- 2.0
- 4.0
- ``sphinx.roles.GUILabel`` or ``sphinx.roles.MenuSelection``
* - ``sphinx.roles.index_role()``
- 2.0
- 4.0
- ``sphinx.roles.Index``
* - ``sphinx.roles.indexmarkup_role()``
- 2.0
- 4.0
- ``sphinx.roles.PEP`` or ``sphinx.roles.RFC``
* - ``sphinx.testing.util.remove_unicode_literal()``
- 2.0
- 4.0
@@ -468,6 +558,11 @@ The following is a list of deprecated interfaces.
- 3.0
- N/A
* - ``sphinx.util.jsonimpl``
- 2.0
- 4.0
- ``sphinxcontrib.serializinghtml.jsonimpl``
* - ``sphinx.util.osutil.EEXIST``
- 2.0
- 4.0
@@ -493,6 +588,11 @@ The following is a list of deprecated interfaces.
- 4.0
- ``os.walk()``
* - ``sphinx.util.pycompat.NoneType``
- 2.0
- 4.0
- ``sphinx.util.typing.NoneType``
* - ``sphinx.util.pycompat.TextIOWrapper``
- 2.0
- 4.0
@@ -513,6 +613,16 @@ The following is a list of deprecated interfaces.
- 4.0
- ``textwrap.indent()``
* - ``sphinx.util.pycompat.sys_encoding``
- 2.0
- 4.0
- ``sys.getdefaultencoding()``
* - ``sphinx.util.pycompat.terminal_safe()``
- 2.0
- 4.0
- ``sphinx.util.console.terminal_safe()``
* - ``sphinx.util.pycompat.u``
- 2.0
- 4.0

View File

@@ -18,5 +18,11 @@ components (e.g. :class:`.Config`, :class:`.BuildEnvironment` and so on) easily.
.. autoclass:: sphinx.util.docutils.SphinxDirective
:members:
.. autoclass:: sphinx.util.docutils.SphinxRole
:members:
.. autoclass:: sphinx.util.docutils.ReferenceRole
:members:
.. autoclass:: sphinx.transforms.post_transforms.images.ImageConverter
:members:

View File

@@ -88,7 +88,7 @@ MediaWiki
Google Analytics
You can use a custom ``layout.html`` template, like this:
.. code-block:: html+django
.. code-block:: html+jinja
{% extends "!layout.html" %}
@@ -119,6 +119,36 @@ Google Analytics
{% endblock %}
Google Search
To replace Sphinx's built-in search function with Google Search, proceed as
follows:
1. Go to https://cse.google.com/cse/all to create the Google Search code
snippet.
2. Copy the code snippet and paste it into ``_templates/searchbox.html`` in
your Sphinx project:
.. code-block:: html+jinja
<div>
<h3>{{ _('Quick search') }}</h3>
<script>
(function() {
var cx = '......';
var gcse = document.createElement('script');
gcse.type = 'text/javascript';
gcse.async = true;
gcse.src = 'https://cse.google.com/cse.js?cx=' + cx;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(gcse, s);
})();
</script>
<gcse:search></gcse:search>
</div>
3. Add ``searchbox.html`` to the :confval:`html_sidebars` configuration value.
.. _api role: https://git.savannah.gnu.org/cgit/kenozooid.git/tree/doc/extapi.py
.. _xhtml to reST: http://docutils.sourceforge.net/sandbox/xhtml2rest/xhtml2rest.py

View File

@@ -70,10 +70,6 @@ Options
Master document name. (see :confval:`master_doc`).
.. option:: --epub
Use epub.
.. rubric:: Extension Options
.. option:: --ext-autodoc

View File

@@ -28,6 +28,7 @@ The builder's "name" must be given to the **-b** command-line option of
.. autoattribute:: supported_image_types
.. module:: sphinx.builders.dirhtml
.. class:: DirectoryHTMLBuilder
This is a subclass of the standard HTML builder. Its output is a directory
@@ -45,6 +46,7 @@ The builder's "name" must be given to the **-b** command-line option of
.. versionadded:: 0.6
.. module:: sphinx.builders.singlehtml
.. class:: SingleFileHTMLBuilder
This is an HTML builder that combines the whole project in one output file.
@@ -91,7 +93,7 @@ The builder's "name" must be given to the **-b** command-line option of
.. _Qt help: https://doc.qt.io/qt-4.8/qthelp-framework.html
.. module:: sphinx.builders.applehelp
.. module:: sphinxcontrib.applehelp
.. class:: AppleHelpBuilder
This builder produces an Apple Help Book based on the same output as the
@@ -117,6 +119,10 @@ The builder's "name" must be given to the **-b** command-line option of
.. versionadded:: 1.3
.. versionchanged:: 2.0
Moved to sphinxcontrib.applehelp from sphinx.builders package.
.. module:: sphinxcontrib.devhelp
.. class:: DevhelpBuilder
@@ -293,7 +299,7 @@ name is ``rinoh``. Refer to the `rinohtype manual`_ for details.
.. versionadded:: 1.1
.. currentmodule:: sphinx.builders.html
.. currentmodule:: sphinxcontrib.serializinghtml
.. class:: SerializingHTMLBuilder
This builder uses a module that implements the Python serialization API

View File

@@ -509,6 +509,13 @@ General configuration
.. versionadded:: 1.5
.. tip:: Sphinx uses requests_ as a HTTP library internally.
Therefore, Sphinx refers a certification file on the
directory pointed ``REQUESTS_CA_BUNDLE`` environment
variable if ``tls_cacerts`` not set.
.. _requests: http://docs.python-requests.org/en/master/
.. confval:: today
today_fmt
@@ -1331,6 +1338,12 @@ that use Sphinx's HTMLWriter class.
.. versionadded:: 1.6
.. deprecated:: 2.0
.. confval:: html4_writer
Output is processed with HTML4 writer. Default is ``False``.
Options for Single HTML output
-------------------------------
@@ -1561,7 +1574,11 @@ the `Dublin Core metadata <http://dublincore.org/>`_.
.. confval:: epub_title
The title of the document. It defaults to the :confval:`html_title` option
but can be set independently for epub creation.
but can be set independently for epub creation. It defaults to the
:confval:`project` option.
.. versionchanged:: 2.0
It defaults to the ``project`` option.
.. confval:: epub_description

View File

@@ -237,6 +237,7 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
.. rst:directive:: autofunction
autodecorator
autodata
automethod
autoattribute
@@ -293,10 +294,11 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
docstrings.
.. versionchanged:: 1.1
Comment docs are now allowed on the same line after an assignment.
.. versionchanged:: 1.2
:rst:dir:`autodata` and :rst:dir:`autoattribute` have an ``annotation``
option.
.. versionchanged:: 2.0
:rst:dir:`autodecorator` added.
.. note::
@@ -313,7 +315,7 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
Configuration
-------------
There are also new config values that you can set:
There are also config values that you can set:
.. confval:: autoclass_content
@@ -376,12 +378,12 @@ There are also new config values that you can set:
'members': 'var1, var2',
'member-order': 'bysource',
'special-members': '__init__',
'undoc-members': None,
'undoc-members': True,
'exclude-members': '__weakref__'
}
Setting ``None`` is equivalent to giving the option name in the list format
(i.e. it means "yes/true/on").
Setting ``None`` or ``True`` to the value is equivalent to giving only the
option name to the directives.
The supported options are ``'members'``, ``'member-order'``,
``'undoc-members'``, ``'private-members'``, ``'special-members'``,
@@ -390,6 +392,9 @@ There are also new config values that you can set:
.. versionadded:: 1.8
.. versionchanged:: 2.0
Accepts ``True`` as a value.
.. confval:: autodoc_docstring_signature
Functions imported from C modules cannot be introspected, and therefore the

View File

@@ -38,3 +38,10 @@ Configuration
called ``Introduction`` that appears in document ``index.rst``. Useful for
avoiding ambiguity when the same section heading appears in different
documents.
.. confval:: autosectionlabel_maxdepth
If set, autosectionlabel chooses the sections for labeling by its depth. For
example, when set 1 to ``autosectionlabel_maxdepth``, labels are generated
only for top level sections, and deeper sections are not labeled. It
defaults to ``None`` (disabled).

View File

@@ -131,7 +131,7 @@ Generating stub pages automatically
-----------------------------------
If you do not want to create stub pages with :program:`sphinx-autogen`, you can
also use this new config value:
also use these config values:
.. confval:: autosummary_generate
@@ -143,6 +143,11 @@ also use this new config value:
The new files will be placed in the directories specified in the
``:toctree:`` options of the directives.
.. confval:: autosummary_mock_imports
This value contains a list of modules to be mocked up. See
:confval:`autodoc_mock_imports` for more details. It defaults to
:confval:`autodoc_mock_imports`.
Customizing templates
---------------------

View File

@@ -13,7 +13,7 @@ This extension features one additional builder, the :class:`CoverageBuilder`.
.. todo:: Write this section.
Several new configuration values can be used to specify what the builder
Several configuration values can be used to specify what the builder
should check:
.. confval:: coverage_ignore_modules

View File

@@ -18,7 +18,7 @@ tracker, at :samp:`https://github.com/sphinx-doc/sphinx/issues/{num}`. Typing
this URL again and again is tedious, so you can use :mod:`~sphinx.ext.extlinks`
to avoid repeating yourself.
The extension adds one new config value:
The extension adds a config value:
.. confval:: extlinks

View File

@@ -6,5 +6,11 @@
.. versionadded:: 1.4
.. versionchanged:: 2.0
Support ``CNAME`` file
This extension creates ``.nojekyll`` file on generated HTML directory to publish
the document on GitHub Pages.
It also creates a ``CNAME`` file for custom domains when :confval:`html_baseurl`
set.

View File

@@ -90,7 +90,7 @@ It adds these directives:
.. versionadded:: 1.6
All three directives support a ``name`` option to set the label to graph.
There are also these new config values:
There are also these config values:
.. confval:: graphviz_dot

View File

@@ -43,7 +43,7 @@ Configuration
-------------
To use Intersphinx linking, add ``'sphinx.ext.intersphinx'`` to your
:confval:`extensions` config value, and use these new config values to activate
:confval:`extensions` config value, and use these config values to activate
linking:
.. confval:: intersphinx_mapping

View File

@@ -463,7 +463,7 @@ __ http://pygments.org/docs/lexers/
This will produce line numbers for all code blocks longer than five lines.
.. rst:directive:: .. code-block:: language
.. rst:directive:: .. code-block:: [language]
Example::
@@ -471,9 +471,11 @@ __ http://pygments.org/docs/lexers/
Some Ruby code.
The directive's alias name :rst:dir:`sourcecode` works as well. As with
:rst:dir:`highlight`\ 's ``language`` option, ``language`` can be any lexer
alias supported by Pygments.
The directive's alias name :rst:dir:`sourcecode` works as well. This
directive takes a language name as an argument. It can be any lexer alias
supported by Pygments. If it is not given, the setting of
:rst:dir:`highlight` directive will be used. If not set,
:confval:`highlight_language` will be used.
**Additional options**
@@ -523,15 +525,18 @@ __ http://pygments.org/docs/lexers/
some ruby code
.. versionchanged:: 1.1
The ``emphasize-lines`` option has been added.
.. versionchanged:: 1.1
The ``emphasize-lines`` option has been added.
.. versionchanged:: 1.3
The ``lineno-start``, ``caption``, ``name`` and ``dedent`` options have
been added.
.. versionchanged:: 1.3
The ``lineno-start``, ``caption``, ``name`` and ``dedent`` options have
been added.
.. versionchanged:: 1.6.6
LaTeX supports the ``emphasize-lines`` option.
.. versionchanged:: 1.6.6
LaTeX supports the ``emphasize-lines`` option.
.. versionchanged:: 2.0
The ``language`` argument becomes optional.
.. rst:directive:: .. literalinclude:: filename

View File

@@ -1195,6 +1195,7 @@ Configuration Variables
See :ref:`cpp-config`.
.. _domains-std:
The Standard Domain
-------------------

View File

@@ -15,8 +15,11 @@ if sys.version_info < (3, 5):
sys.exit(1)
install_requires = [
'sphinxcontrib-applehelp',
'sphinxcontrib-devhelp',
'sphinxcontrib-jsmath',
'sphinxcontrib-htmlhelp',
'sphinxcontrib-serializinghtml',
'sphinxcontrib-qthelp',
'Jinja2>=2.3',
'Pygments>=2.0',

View File

@@ -32,8 +32,8 @@ if 'PYTHONWARNINGS' not in os.environ:
warnings.filterwarnings('ignore', "'U' mode is deprecated",
DeprecationWarning, module='docutils.io')
__version__ = '2.0.0+'
__released__ = '2.0.0' # used when Sphinx builds its own docs
__version__ = '2.1.0+'
__released__ = '2.1.0' # used when Sphinx builds its own docs
#: Version info for better programmatic use.
#:
@@ -43,7 +43,7 @@ __released__ = '2.0.0' # used when Sphinx builds its own docs
#:
#: .. versionadded:: 1.2
#: Before version 1.2, check the string ``sphinx.__version__``.
version_info = (2, 0, 0, 'beta', 0)
version_info = (2, 1, 0, 'beta', 0)
package_dir = path.abspath(path.dirname(__file__))

View File

@@ -37,11 +37,10 @@ from sphinx.registry import SphinxComponentRegistry
from sphinx.util import docutils
from sphinx.util import import_object, progress_message
from sphinx.util import logging
from sphinx.util import pycompat # noqa: F401
from sphinx.util.build_phase import BuildPhase
from sphinx.util.console import bold # type: ignore
from sphinx.util.docutils import directive_helper
from sphinx.util.i18n import find_catalog_source_files
from sphinx.util.i18n import CatalogRepository
from sphinx.util.logging import prefixed_warnings
from sphinx.util.osutil import abspath, ensuredir, relpath
from sphinx.util.tags import Tags
@@ -62,13 +61,12 @@ if False:
builtin_extensions = (
'sphinx.addnodes',
'sphinx.builders.applehelp',
'sphinx.builders.changes',
'sphinx.builders.epub3',
'sphinx.builders.dirhtml',
'sphinx.builders.dummy',
'sphinx.builders.gettext',
'sphinx.builders.html',
'sphinx.builders.htmlhelp',
'sphinx.builders.latex',
'sphinx.builders.linkcheck',
'sphinx.builders.manpage',
@@ -106,7 +104,10 @@ builtin_extensions = (
'sphinx.environment.collectors.toctree',
'sphinx.environment.collectors.indexentries',
# 1st party extensions
'sphinxcontrib.applehelp',
'sphinxcontrib.devhelp',
'sphinxcontrib.htmlhelp',
'sphinxcontrib.serializinghtml',
'sphinxcontrib.qthelp',
# Strictly, alabaster theme is not a builtin extension,
# but it is loaded automatically to use it as default theme.
@@ -264,21 +265,21 @@ class Sphinx:
"""Load translated strings from the configured localedirs if enabled in
the configuration.
"""
if self.config.language is not None:
if self.config.language is None:
self.translator, has_translation = locale.init([], None)
else:
logger.info(bold(__('loading translations [%s]... ') % self.config.language),
nonl=True)
user_locale_dirs = [
path.join(self.srcdir, x) for x in self.config.locale_dirs]
# compile mo files if sphinx.po file in user locale directories are updated
for catinfo in find_catalog_source_files(
user_locale_dirs, self.config.language, domains=['sphinx'],
charset=self.config.source_encoding):
catinfo.write_mo(self.config.language)
locale_dirs = [None, path.join(package_dir, 'locale')] + user_locale_dirs
else:
locale_dirs = []
self.translator, has_translation = locale.init(locale_dirs, self.config.language)
if self.config.language is not None:
repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
self.config.language, self.config.source_encoding)
for catalog in repo.catalogs:
if catalog.domain == 'sphinx' and catalog.is_outdated():
catalog.write_mo(self.config.language)
locale_dirs = [None, path.join(package_dir, 'locale')] + list(repo.locale_dirs)
self.translator, has_translation = locale.init(locale_dirs, self.config.language)
if has_translation or self.config.language == 'en':
# "en" never needs to be translated
logger.info(__('done'))

View File

@@ -19,12 +19,11 @@ from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import SphinxError
from sphinx.io import read_doc
from sphinx.locale import __
from sphinx.util import i18n, import_object, logging, rst, status_iterator
from sphinx.util import import_object, logging, rst, progress_message, status_iterator
from sphinx.util.build_phase import BuildPhase
from sphinx.util.console import bold # type: ignore
from sphinx.util.docutils import sphinx_domains
from sphinx.util.i18n import find_catalog
from sphinx.util.matching import Matcher
from sphinx.util.i18n import CatalogRepository, docname_to_domain
from sphinx.util.osutil import SEP, ensuredir, relative_uri, relpath
from sphinx.util.parallel import ParallelTasks, SerialTasks, make_chunks, \
parallel_available
@@ -236,14 +235,10 @@ class Builder:
def compile_all_catalogs(self):
# type: () -> None
catalogs = i18n.find_catalog_source_files(
[path.join(self.srcdir, x) for x in self.config.locale_dirs],
self.config.language,
charset=self.config.source_encoding,
force_all=True,
excluded=Matcher(['**/.?**']))
message = __('all of %d po files') % len(catalogs)
self.compile_catalogs(catalogs, message)
repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
self.config.language, self.config.source_encoding)
message = __('all of %d po files') % len(list(repo.catalogs))
self.compile_catalogs(set(repo.catalogs), message)
def compile_specific_catalogs(self, specified_files):
# type: (List[str]) -> None
@@ -251,28 +246,25 @@ class Builder:
# type: (str) -> str
docname = self.env.path2doc(path.abspath(fpath))
if docname:
return find_catalog(docname, self.config.gettext_compact)
return docname_to_domain(docname, self.config.gettext_compact)
else:
return None
specified_domains = set(map(to_domain, specified_files))
specified_domains.discard(None)
catalogs = i18n.find_catalog_source_files(
[path.join(self.srcdir, x) for x in self.config.locale_dirs],
self.config.language,
domains=list(specified_domains),
charset=self.config.source_encoding,
excluded=Matcher(['**/.?**']))
catalogs = set()
domains = set(map(to_domain, specified_files))
repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
self.config.language, self.config.source_encoding)
for catalog in repo.catalogs:
if catalog.domain in domains and catalog.is_outdated():
catalogs.add(catalog)
message = __('targets for %d po files that are specified') % len(catalogs)
self.compile_catalogs(catalogs, message)
def compile_update_catalogs(self):
# type: () -> None
catalogs = i18n.find_catalog_source_files(
[path.join(self.srcdir, x) for x in self.config.locale_dirs],
self.config.language,
charset=self.config.source_encoding,
excluded=Matcher(['**/.?**']))
repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
self.config.language, self.config.source_encoding)
catalogs = {c for c in repo.catalogs if c.is_outdated()}
message = __('targets for %d po files that are out of date') % len(catalogs)
self.compile_catalogs(catalogs, message)
@@ -298,8 +290,7 @@ class Builder:
logger.warning(__('file %r given on command line is not under the '
'source directory, ignoring'), filename)
continue
if not (path.isfile(filename) or
any(path.isfile(filename + suffix) for suffix in suffixes)):
if not path.isfile(filename):
logger.warning(__('file %r given on command line does not exist, '
'ignoring'), filename)
continue
@@ -351,16 +342,14 @@ class Builder:
if updated_docnames:
# save the environment
from sphinx.application import ENV_PICKLE_FILENAME
logger.info(bold(__('pickling environment... ')), nonl=True)
with open(path.join(self.doctreedir, ENV_PICKLE_FILENAME), 'wb') as f:
pickle.dump(self.env, f, pickle.HIGHEST_PROTOCOL)
logger.info(__('done'))
with progress_message(__('pickling environment')):
with open(path.join(self.doctreedir, ENV_PICKLE_FILENAME), 'wb') as f:
pickle.dump(self.env, f, pickle.HIGHEST_PROTOCOL)
# global actions
self.app.phase = BuildPhase.CONSISTENCY_CHECK
logger.info(bold(__('checking consistency... ')), nonl=True)
self.env.check_consistency()
logger.info(__('done'))
with progress_message(__('checking consistency')):
self.env.check_consistency()
else:
if method == 'update' and not docnames:
logger.info(bold(__('no targets are out of date.')))
@@ -559,9 +548,8 @@ class Builder:
docnames.add(tocdocname)
docnames.add(self.config.master_doc)
logger.info(bold(__('preparing documents... ')), nonl=True)
self.prepare_writing(docnames)
logger.info(__('done'))
with progress_message(__('preparing documents')):
self.prepare_writing(docnames)
if self.parallel_ok:
# number of subprocesses is parallel-1 because the main process

View File

@@ -35,7 +35,7 @@ except ImportError:
if False:
# For type annotation
from typing import Any, Dict, List, Tuple # NOQA
from typing import Any, Dict, List, Set, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
@@ -134,8 +134,6 @@ class EpubBuilder(StandaloneHTMLBuilder):
html_scaled_image_link = False
# don't generate search index or include search page
search = False
# use html5 translator by default
default_html5_translator = True
coverpage_name = COVERPAGE_NAME
toctree_template = TOCTREE_TEMPLATE
@@ -213,6 +211,15 @@ class EpubBuilder(StandaloneHTMLBuilder):
result = self.get_refnodes(elem, result)
return result
def check_refnodes(self, nodes):
# type: (List[Dict[str, Any]]) -> None
appeared = set() # type: Set[str]
for node in nodes:
if node['refuri'] in appeared:
logger.warning(__('duplicated ToC entry found: %s'), node['refuri'])
else:
appeared.add(node['refuri'])
def get_toc(self):
# type: () -> None
"""Get the total table of contents, containing the master_doc
@@ -646,7 +653,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
if incr:
self.playorder += 1
self.tocid += 1
return NavPoint(self.esc('navPoint%d' % self.tocid), self.playorder,
return NavPoint('navPoint%d' % self.tocid, self.playorder,
node['text'], node['refuri'], [])
def build_navpoints(self, nodes):
@@ -726,6 +733,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
else:
# 'includehidden'
refnodes = self.refnodes
self.check_refnodes(refnodes)
navpoints = self.build_navpoints(refnodes)
level = max(item['level'] for item in self.refnodes)
level = min(level, self.config.epub_tocdepth)

View File

@@ -8,21 +8,15 @@
:license: BSD, see LICENSE for details.
"""
import plistlib
import shlex
import subprocess
from os import path, environ
from subprocess import CalledProcessError, PIPE, STDOUT
import warnings
from sphinx import package_dir
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.errors import SphinxError
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util import SkipProgressMessage, progress_message
from sphinx.util.fileutil import copy_asset, copy_asset_file
from sphinx.util.matching import Matcher
from sphinx.util.osutil import ensuredir, make_filename
from sphinxcontrib.applehelp import (
AppleHelpCodeSigningFailed,
AppleHelpIndexerFailed,
AppleHelpBuilder,
)
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
if False:
# For type annotation
@@ -30,240 +24,20 @@ if False:
from sphinx.application import Sphinx # NOQA
logger = logging.getLogger(__name__)
template_dir = path.join(package_dir, 'templates', 'applehelp')
class AppleHelpIndexerFailed(SphinxError):
category = __('Help indexer failed')
class AppleHelpCodeSigningFailed(SphinxError):
category = __('Code signing failed')
class AppleHelpBuilder(StandaloneHTMLBuilder):
"""
Builder that outputs an Apple help book. Requires Mac OS X as it relies
on the ``hiutil`` command line tool.
"""
name = 'applehelp'
epilog = __('The help book is in %(outdir)s.\n'
'Note that won\'t be able to view it unless you put it in '
'~/Library/Documentation/Help or install it in your application '
'bundle.')
# don't copy the reST source
copysource = False
supported_image_types = ['image/png', 'image/gif', 'image/jpeg',
'image/tiff', 'image/jp2', 'image/svg+xml']
# don't add links
add_permalinks = False
# this is an embedded HTML format
embedded = True
# don't generate the search index or include the search page
search = False
def init(self):
# type: () -> None
super().init()
# the output files for HTML help must be .html only
self.out_suffix = '.html'
self.link_suffix = '.html'
if self.config.applehelp_bundle_id is None:
raise SphinxError(__('You must set applehelp_bundle_id before '
'building Apple Help output'))
self.bundle_path = path.join(self.outdir,
self.config.applehelp_bundle_name + '.help')
self.outdir = path.join(self.bundle_path,
'Contents',
'Resources',
self.config.applehelp_locale + '.lproj')
def handle_finish(self):
# type: () -> None
super().handle_finish()
self.finish_tasks.add_task(self.copy_localized_files)
self.finish_tasks.add_task(self.build_helpbook)
@progress_message(__('copying localized files'))
def copy_localized_files(self):
# type: () -> None
source_dir = path.join(self.confdir, self.config.applehelp_locale + '.lproj')
target_dir = self.outdir
if path.isdir(source_dir):
excluded = Matcher(self.config.exclude_patterns + ['**/.*'])
copy_asset(source_dir, target_dir, excluded,
context=self.globalcontext, renderer=self.templates)
def build_helpbook(self):
# type: () -> None
contents_dir = path.join(self.bundle_path, 'Contents')
resources_dir = path.join(contents_dir, 'Resources')
language_dir = path.join(resources_dir,
self.config.applehelp_locale + '.lproj')
ensuredir(language_dir)
self.build_info_plist(contents_dir)
self.copy_applehelp_icon(resources_dir)
self.build_access_page(language_dir)
self.build_helpindex(language_dir)
if self.config.applehelp_codesign_identity:
self.do_codesign()
@progress_message(__('writing Info.plist'))
def build_info_plist(self, contents_dir):
# type: (str) -> None
"""Construct the Info.plist file."""
info_plist = {
'CFBundleDevelopmentRegion': self.config.applehelp_dev_region,
'CFBundleIdentifier': self.config.applehelp_bundle_id,
'CFBundleInfoDictionaryVersion': '6.0',
'CFBundlePackageType': 'BNDL',
'CFBundleShortVersionString': self.config.release,
'CFBundleSignature': 'hbwr',
'CFBundleVersion': self.config.applehelp_bundle_version,
'HPDBookAccessPath': '_access.html',
'HPDBookIndexPath': 'search.helpindex',
'HPDBookTitle': self.config.applehelp_title,
'HPDBookType': '3',
'HPDBookUsesExternalViewer': False,
}
if self.config.applehelp_icon is not None:
info_plist['HPDBookIconPath'] = path.basename(self.config.applehelp_icon)
if self.config.applehelp_kb_url is not None:
info_plist['HPDBookKBProduct'] = self.config.applehelp_kb_product
info_plist['HPDBookKBURL'] = self.config.applehelp_kb_url
if self.config.applehelp_remote_url is not None:
info_plist['HPDBookRemoteURL'] = self.config.applehelp_remote_url
with open(path.join(contents_dir, 'Info.plist'), 'wb') as f:
plistlib.dump(info_plist, f)
def copy_applehelp_icon(self, resources_dir):
# type: (str) -> None
"""Copy the icon, if one is supplied."""
if self.config.applehelp_icon:
try:
with progress_message(__('copying icon... ')):
applehelp_icon = path.join(self.srcdir, self.config.applehelp_icon)
copy_asset_file(applehelp_icon, resources_dir)
except Exception as err:
logger.warning(__('cannot copy icon file %r: %s'), applehelp_icon, err)
@progress_message(__('building access page'))
def build_access_page(self, language_dir):
# type: (str) -> None
"""Build the access page."""
context = {
'toc': self.config.master_doc + self.out_suffix,
'title': self.config.applehelp_title,
}
copy_asset_file(path.join(template_dir, '_access.html_t'), language_dir, context)
@progress_message(__('generating help index'))
def build_helpindex(self, language_dir):
# type: (str) -> None
"""Generate the help index."""
args = [
self.config.applehelp_indexer_path,
'-Cf',
path.join(language_dir, 'search.helpindex'),
language_dir
]
if self.config.applehelp_index_anchors is not None:
args.append('-a')
if self.config.applehelp_min_term_length is not None:
args += ['-m', '%s' % self.config.applehelp_min_term_length]
if self.config.applehelp_stopwords is not None:
args += ['-s', self.config.applehelp_stopwords]
if self.config.applehelp_locale is not None:
args += ['-l', self.config.applehelp_locale]
if self.config.applehelp_disable_external_tools:
raise SkipProgressMessage(__('you will need to index this help book with:\n %s'),
' '.join([shlex.quote(arg) for arg in args]))
else:
try:
subprocess.run(args, stdout=PIPE, stderr=STDOUT, check=True)
except OSError:
raise AppleHelpIndexerFailed(__('Command not found: %s') % args[0])
except CalledProcessError as exc:
raise AppleHelpIndexerFailed(exc.stdout)
@progress_message(__('signing help book'))
def do_codesign(self):
# type: () -> None
"""If we've been asked to, sign the bundle."""
args = [
self.config.applehelp_codesign_path,
'-s', self.config.applehelp_codesign_identity,
'-f'
]
args += self.config.applehelp_codesign_flags
args.append(self.bundle_path)
if self.config.applehelp_disable_external_tools:
raise SkipProgressMessage(__('you will need to sign this help book with:\n %s'),
' '.join([shlex.quote(arg) for arg in args]))
else:
try:
subprocess.run(args, stdout=PIPE, stderr=STDOUT, check=True)
except OSError:
raise AppleHelpCodeSigningFailed(__('Command not found: %s') % args[0])
except CalledProcessError as exc:
raise AppleHelpCodeSigningFailed(exc.stdout)
deprecated_alias('sphinx.builders.applehelp',
{
'AppleHelpCodeSigningFailed': AppleHelpCodeSigningFailed,
'AppleHelpIndexerFailed': AppleHelpIndexerFailed,
'AppleHelpBuilder': AppleHelpBuilder,
},
RemovedInSphinx40Warning)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.setup_extension('sphinx.builders.html')
app.add_builder(AppleHelpBuilder)
app.add_config_value('applehelp_bundle_name',
lambda self: make_filename(self.project), 'applehelp')
app.add_config_value('applehelp_bundle_id', None, 'applehelp', [str])
app.add_config_value('applehelp_dev_region', 'en-us', 'applehelp')
app.add_config_value('applehelp_bundle_version', '1', 'applehelp')
app.add_config_value('applehelp_icon', None, 'applehelp', [str])
app.add_config_value('applehelp_kb_product',
lambda self: '%s-%s' % (make_filename(self.project), self.release),
'applehelp')
app.add_config_value('applehelp_kb_url', None, 'applehelp', [str])
app.add_config_value('applehelp_remote_url', None, 'applehelp', [str])
app.add_config_value('applehelp_index_anchors', False, 'applehelp', [str])
app.add_config_value('applehelp_min_term_length', None, 'applehelp', [str])
app.add_config_value('applehelp_stopwords',
lambda self: self.language or 'en', 'applehelp')
app.add_config_value('applehelp_locale', lambda self: self.language or 'en', 'applehelp')
app.add_config_value('applehelp_title', lambda self: self.project + ' Help', 'applehelp')
app.add_config_value('applehelp_codesign_identity',
lambda self: environ.get('CODE_SIGN_IDENTITY', None),
'applehelp')
app.add_config_value('applehelp_codesign_flags',
lambda self: shlex.split(environ.get('OTHER_CODE_SIGN_FLAGS', '')),
'applehelp')
app.add_config_value('applehelp_indexer_path', '/usr/bin/hiutil', 'applehelp')
app.add_config_value('applehelp_codesign_path', '/usr/bin/codesign', 'applehelp')
app.add_config_value('applehelp_disable_external_tools', False, None)
warnings.warn('sphinx.builders.applehelp has been moved to sphinxcontrib-applehelp.',
RemovedInSphinx40Warning)
app.setup_extension('sphinxcontrib.applehelp')
return {
'version': 'builtin',

View File

@@ -0,0 +1,68 @@
"""
sphinx.builders.dirhtml
~~~~~~~~~~~~~~~~~~~~~~~
Directory HTML builders.
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from os import path
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.util import logging
from sphinx.util.osutil import SEP, os_path
if False:
# For type annotation
from typing import Any, Dict, Set # NOQA
from sphinx.application import Sphinx # NOQA
logger = logging.getLogger(__name__)
class DirectoryHTMLBuilder(StandaloneHTMLBuilder):
"""
A StandaloneHTMLBuilder that creates all HTML pages as "index.html" in
a directory given by their pagename, so that generated URLs don't have
``.html`` in them.
"""
name = 'dirhtml'
def get_target_uri(self, docname, typ=None):
# type: (str, str) -> str
if docname == 'index':
return ''
if docname.endswith(SEP + 'index'):
return docname[:-5] # up to sep
return docname + SEP
def get_outfilename(self, pagename):
# type: (str) -> str
if pagename == 'index' or pagename.endswith(SEP + 'index'):
outfilename = path.join(self.outdir, os_path(pagename) +
self.out_suffix)
else:
outfilename = path.join(self.outdir, os_path(pagename),
'index' + self.out_suffix)
return outfilename
def prepare_writing(self, docnames):
# type: (Set[str]) -> None
super().prepare_writing(docnames)
self.globalcontext['no_search_suffix'] = True
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.setup_extension('sphinx.builders.html')
app.add_builder(DirectoryHTMLBuilder)
return {
'version': 'builtin',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@@ -269,7 +269,7 @@ def setup(app):
app.add_config_value('epub_version', 3.0, 'epub') # experimental
app.add_config_value('epub_theme', 'epub', 'epub')
app.add_config_value('epub_theme_options', {}, 'epub')
app.add_config_value('epub_title', lambda self: self.html_title, 'epub')
app.add_config_value('epub_title', lambda self: self.project, 'epub')
app.add_config_value('epub_author', lambda self: self.author, 'epub')
app.add_config_value('epub_language', lambda self: self.language or 'en', 'epub')
app.add_config_value('epub_publisher', lambda self: self.author, 'epub')

View File

@@ -22,7 +22,7 @@ from sphinx.errors import ThemeError
from sphinx.locale import __
from sphinx.util import split_index_msg, logging, status_iterator
from sphinx.util.console import bold # type: ignore
from sphinx.util.i18n import find_catalog
from sphinx.util.i18n import docname_to_domain
from sphinx.util.nodes import extract_messages, traverse_translatable_index
from sphinx.util.osutil import relpath, ensuredir, canon_path
from sphinx.util.tags import Tags
@@ -140,7 +140,7 @@ class I18nBuilder(Builder):
def write_doc(self, docname, doctree):
# type: (str, nodes.document) -> None
catalog = self.catalogs[find_catalog(docname, self.config.gettext_compact)]
catalog = self.catalogs[docname_to_domain(docname, self.config.gettext_compact)]
for node, msg in extract_messages(doctree):
catalog.add(msg, node)

View File

@@ -9,16 +9,13 @@
"""
import html
import pickle
import posixpath
import re
import sys
import types
import warnings
from hashlib import md5
from os import path
import docutils
from docutils import nodes
from docutils.core import publish_parts
from docutils.frontend import OptionParser
@@ -26,7 +23,6 @@ from docutils.io import DocTreeInput, StringOutput
from docutils.utils import relative_path
from sphinx import package_dir, __display_version__
from sphinx.application import ENV_PICKLE_FILENAME
from sphinx.builders import Builder
from sphinx.deprecation import (
RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias
@@ -39,15 +35,14 @@ from sphinx.highlighting import PygmentsBridge
from sphinx.locale import _, __
from sphinx.search import js_index
from sphinx.theming import HTMLThemeFactory
from sphinx.util import jsonimpl, logging, status_iterator
from sphinx.util import logging, status_iterator
from sphinx.util.console import bold # type: ignore
from sphinx.util.docutils import is_html5_writer_available, new_document
from sphinx.util.fileutil import copy_asset
from sphinx.util.i18n import format_date
from sphinx.util.inventory import InventoryFile
from sphinx.util.matching import patmatch, Matcher, DOTFILES
from sphinx.util.osutil import SEP, os_path, relative_uri, ensuredir, \
movefile, copyfile
from sphinx.util.osutil import os_path, relative_uri, ensuredir, movefile, copyfile
from sphinx.writers.html import HTMLWriter, HTMLTranslator
if False:
@@ -58,7 +53,7 @@ if False:
from sphinx.domains import Domain, Index, IndexEntry # NOQA
from sphinx.util.tags import Tags # NOQA
# Experimental HTML5 Writer
# HTML5 Writer is avialable or not
if is_html5_writer_available():
from sphinx.writers.html5 import HTML5Translator
html5_ready = True
@@ -67,8 +62,6 @@ else:
#: the filename for the inventory of objects
INVENTORY_FILENAME = 'objects.inv'
#: the filename for the "last build" file (for serializing builders)
LAST_BUILD_FILENAME = 'last_build'
logger = logging.getLogger(__name__)
return_codes_re = re.compile('[\r\n]+')
@@ -116,24 +109,24 @@ class JSContainer(list):
"""The container for JavaScript scripts."""
def insert(self, index, obj):
# type: (int, str) -> None
warnings.warn('builder.script_files is deprecated. '
'Please use app.add_js_file() instead.',
RemovedInSphinx30Warning, stacklevel=2)
warnings.warn('To modify script_files in the theme is deprecated. '
'Please insert a <script> tag directly in your theme instead.',
RemovedInSphinx30Warning, stacklevel=3)
super().insert(index, obj)
def extend(self, other): # type: ignore
# type: (List[str]) -> None
warnings.warn('builder.script_files is deprecated. '
'Please use app.add_js_file() instead.',
RemovedInSphinx30Warning, stacklevel=2)
warnings.warn('To modify script_files in the theme is deprecated. '
'Please insert a <script> tag directly in your theme instead.',
RemovedInSphinx30Warning, stacklevel=3)
for item in other:
self.append(item)
def __iadd__(self, other): # type: ignore
# type: (List[str]) -> JSContainer
warnings.warn('builder.script_files is deprecated. '
'Please use app.add_js_file() instead.',
RemovedInSphinx30Warning, stacklevel=2)
warnings.warn('To modify script_files in the theme is deprecated. '
'Please insert a <script> tag directly in your theme instead.',
RemovedInSphinx30Warning, stacklevel=3)
for item in other:
self.append(item)
return self
@@ -242,8 +235,6 @@ class StandaloneHTMLBuilder(Builder):
search = True # for things like HTML help and Apple help: suppress search
use_index = False
download_support = True # enable download role
# use html5 translator by default
default_html5_translator = False
imgpath = None # type: str
domain_indices = [] # type: List[Tuple[str, Type[Index], List[Tuple[str, List[IndexEntry]]], bool]] # NOQA
@@ -285,11 +276,6 @@ class StandaloneHTMLBuilder(Builder):
self.use_index = self.get_builder_config('use_index', 'html')
if self.config.html_experimental_html5_writer and not html5_ready:
logger.warning(__('html_experimental_html5_writer is set, but current version '
'is old. Docutils\' version should be 0.13 or newer, but %s.'),
docutils.__version__)
def create_build_info(self):
# type: () -> BuildInfo
return BuildInfo(self.config, self.tags, ['html'])
@@ -374,14 +360,10 @@ class StandaloneHTMLBuilder(Builder):
@property
def default_translator_class(self): # type: ignore
# type: () -> Type[nodes.NodeVisitor]
use_html5_writer = self.config.html_experimental_html5_writer
if use_html5_writer is None:
use_html5_writer = self.default_html5_translator
if use_html5_writer and html5_ready:
return HTML5Translator
else:
if not html5_ready or self.config.html4_writer:
return HTMLTranslator
else:
return HTML5Translator
@property
def math_renderer_name(self):
@@ -562,7 +544,7 @@ class StandaloneHTMLBuilder(Builder):
'parents': [],
'logo': logo,
'favicon': favicon,
'html5_doctype': self.config.html_experimental_html5_writer and html5_ready,
'html5_doctype': html5_ready and not self.config.html4_writer
}
if self.theme:
self.globalcontext.update(
@@ -1169,173 +1151,6 @@ class StandaloneHTMLBuilder(Builder):
logger.info(__('done'))
class DirectoryHTMLBuilder(StandaloneHTMLBuilder):
"""
A StandaloneHTMLBuilder that creates all HTML pages as "index.html" in
a directory given by their pagename, so that generated URLs don't have
``.html`` in them.
"""
name = 'dirhtml'
def get_target_uri(self, docname, typ=None):
# type: (str, str) -> str
if docname == 'index':
return ''
if docname.endswith(SEP + 'index'):
return docname[:-5] # up to sep
return docname + SEP
def get_outfilename(self, pagename):
# type: (str) -> str
if pagename == 'index' or pagename.endswith(SEP + 'index'):
outfilename = path.join(self.outdir, os_path(pagename) +
self.out_suffix)
else:
outfilename = path.join(self.outdir, os_path(pagename),
'index' + self.out_suffix)
return outfilename
def prepare_writing(self, docnames):
# type: (Set[str]) -> None
super().prepare_writing(docnames)
self.globalcontext['no_search_suffix'] = True
class SerializingHTMLBuilder(StandaloneHTMLBuilder):
"""
An abstract builder that serializes the generated HTML.
"""
#: the serializing implementation to use. Set this to a module that
#: implements a `dump`, `load`, `dumps` and `loads` functions
#: (pickle, simplejson etc.)
implementation = None # type: Any
implementation_dumps_unicode = False
#: additional arguments for dump()
additional_dump_args = () # type: Tuple
#: the filename for the global context file
globalcontext_filename = None # type: str
supported_image_types = ['image/svg+xml', 'image/png',
'image/gif', 'image/jpeg']
def init(self):
# type: () -> None
self.build_info = BuildInfo(self.config, self.tags)
self.imagedir = '_images'
self.current_docname = None
self.theme = None # no theme necessary
self.templates = None # no template bridge necessary
self.init_templates()
self.init_highlighter()
self.init_css_files()
self.init_js_files()
self.use_index = self.get_builder_config('use_index', 'html')
def get_target_uri(self, docname, typ=None):
# type: (str, str) -> str
if docname == 'index':
return ''
if docname.endswith(SEP + 'index'):
return docname[:-5] # up to sep
return docname + SEP
def dump_context(self, context, filename):
# type: (Dict, str) -> None
if self.implementation_dumps_unicode:
with open(filename, 'w', encoding='utf-8') as ft:
self.implementation.dump(context, ft, *self.additional_dump_args)
else:
with open(filename, 'wb') as fb:
self.implementation.dump(context, fb, *self.additional_dump_args)
def handle_page(self, pagename, ctx, templatename='page.html',
outfilename=None, event_arg=None):
# type: (str, Dict, str, str, Any) -> None
ctx['current_page_name'] = pagename
self.add_sidebars(pagename, ctx)
if not outfilename:
outfilename = path.join(self.outdir,
os_path(pagename) + self.out_suffix)
# we're not taking the return value here, since no template is
# actually rendered
self.app.emit('html-page-context', pagename, templatename, ctx, event_arg)
# make context object serializable
for key in list(ctx):
if isinstance(ctx[key], types.FunctionType):
del ctx[key]
ensuredir(path.dirname(outfilename))
self.dump_context(ctx, outfilename)
# if there is a source file, copy the source file for the
# "show source" link
if ctx.get('sourcename'):
source_name = path.join(self.outdir, '_sources',
os_path(ctx['sourcename']))
ensuredir(path.dirname(source_name))
copyfile(self.env.doc2path(pagename), source_name)
def handle_finish(self):
# type: () -> None
# dump the global context
outfilename = path.join(self.outdir, self.globalcontext_filename)
self.dump_context(self.globalcontext, outfilename)
# super here to dump the search index
super().handle_finish()
# copy the environment file from the doctree dir to the output dir
# as needed by the web app
copyfile(path.join(self.doctreedir, ENV_PICKLE_FILENAME),
path.join(self.outdir, ENV_PICKLE_FILENAME))
# touch 'last build' file, used by the web application to determine
# when to reload its environment and clear the cache
open(path.join(self.outdir, LAST_BUILD_FILENAME), 'w').close()
class PickleHTMLBuilder(SerializingHTMLBuilder):
"""
A Builder that dumps the generated HTML into pickle files.
"""
name = 'pickle'
epilog = __('You can now process the pickle files in %(outdir)s.')
implementation = pickle
implementation_dumps_unicode = False
additional_dump_args = (pickle.HIGHEST_PROTOCOL,)
indexer_format = pickle
indexer_dumps_unicode = False
out_suffix = '.fpickle'
globalcontext_filename = 'globalcontext.pickle'
searchindex_filename = 'searchindex.pickle'
# compatibility alias
WebHTMLBuilder = PickleHTMLBuilder
class JSONHTMLBuilder(SerializingHTMLBuilder):
"""
A builder that dumps the generated HTML into JSON files.
"""
name = 'json'
epilog = __('You can now process the JSON files in %(outdir)s.')
implementation = jsonimpl
implementation_dumps_unicode = True
indexer_format = jsonimpl
indexer_dumps_unicode = True
out_suffix = '.fjson'
globalcontext_filename = 'globalcontext.json'
searchindex_filename = 'searchindex.json'
def convert_html_css_files(app, config):
# type: (Sphinx, Config) -> None
"""This converts string styled html_css_files to tuple styled one."""
@@ -1417,11 +1232,21 @@ def validate_math_renderer(app):
# for compatibility
from sphinx.builders.dirhtml import DirectoryHTMLBuilder # NOQA
from sphinx.builders.singlehtml import SingleFileHTMLBuilder # NOQA
from sphinxcontrib.serializinghtml import ( # NOQA
LAST_BUILD_FILENAME, JSONHTMLBuilder, PickleHTMLBuilder, SerializingHTMLBuilder
)
deprecated_alias('sphinx.builders.html',
{
'LAST_BUILD_FILENAME': LAST_BUILD_FILENAME,
'DirectoryHTMLBuilder': DirectoryHTMLBuilder,
'JSONHTMLBuilder': JSONHTMLBuilder,
'PickleHTMLBuilder': PickleHTMLBuilder,
'SerializingHTMLBuilder': SerializingHTMLBuilder,
'SingleFileHTMLBuilder': SingleFileHTMLBuilder,
'WebHTMLBuilder': PickleHTMLBuilder,
},
RemovedInSphinx40Warning)
@@ -1430,9 +1255,6 @@ def setup(app):
# type: (Sphinx) -> Dict[str, Any]
# builders
app.add_builder(StandaloneHTMLBuilder)
app.add_builder(DirectoryHTMLBuilder)
app.add_builder(PickleHTMLBuilder)
app.add_builder(JSONHTMLBuilder)
# config values
app.add_config_value('html_theme', 'alabaster', 'html')
@@ -1472,9 +1294,9 @@ def setup(app):
app.add_config_value('html_search_options', {}, 'html')
app.add_config_value('html_search_scorer', '', None)
app.add_config_value('html_scaled_image_link', True, 'html')
app.add_config_value('html_experimental_html5_writer', None, 'html')
app.add_config_value('html_baseurl', '', 'html')
app.add_config_value('html_math_renderer', None, 'env')
app.add_config_value('html4_writer', False, 'html')
# event handlers
app.connect('config-inited', convert_html_css_files)

View File

@@ -9,364 +9,36 @@
:license: BSD, see LICENSE for details.
"""
import html
import os
import warnings
from os import path
from docutils import nodes
from sphinxcontrib.htmlhelp import (
chm_locales, chm_htmlescape, HTMLHelpBuilder, default_htmlhelp_basename
)
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx import addnodes
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.environment.adapters.indexentries import IndexEntries
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.nodes import NodeMatcher
from sphinx.util.osutil import make_filename_from_project
if False:
# For type annotation
from typing import Any, Dict, IO, List, Match, Tuple # NOQA
from typing import Any, Dict # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.config import Config # NOQA
logger = logging.getLogger(__name__)
# Project file (*.hhp) template. 'outname' is the file basename (like
# the pythlp in pythlp.hhp); 'version' is the doc version number (like
# the 2.2 in Python 2.2).
# The magical numbers in the long line under [WINDOWS] set most of the
# user-visible features (visible buttons, tabs, etc).
# About 0x10384e: This defines the buttons in the help viewer. The
# following defns are taken from htmlhelp.h. Not all possibilities
# actually work, and not all those that work are available from the Help
# Workshop GUI. In particular, the Zoom/Font button works and is not
# available from the GUI. The ones we're using are marked with 'x':
#
# 0x000002 Hide/Show x
# 0x000004 Back x
# 0x000008 Forward x
# 0x000010 Stop
# 0x000020 Refresh
# 0x000040 Home x
# 0x000080 Forward
# 0x000100 Back
# 0x000200 Notes
# 0x000400 Contents
# 0x000800 Locate x
# 0x001000 Options x
# 0x002000 Print x
# 0x004000 Index
# 0x008000 Search
# 0x010000 History
# 0x020000 Favorites
# 0x040000 Jump 1
# 0x080000 Jump 2
# 0x100000 Zoom/Font x
# 0x200000 TOC Next
# 0x400000 TOC Prev
project_template = '''\
[OPTIONS]
Binary TOC=No
Binary Index=No
Compiled file=%(outname)s.chm
Contents file=%(outname)s.hhc
Default Window=%(outname)s
Default topic=%(master_doc)s
Display compile progress=No
Full text search stop list file=%(outname)s.stp
Full-text search=Yes
Index file=%(outname)s.hhk
Language=%(lcid)#x
Title=%(title)s
[WINDOWS]
%(outname)s="%(title)s","%(outname)s.hhc","%(outname)s.hhk",\
"%(master_doc)s","%(master_doc)s",,,,,0x63520,220,0x10384e,[0,0,1024,768],,,,,,,0
[FILES]
'''
contents_header = '''\
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<HTML>
<HEAD>
<meta name="GENERATOR" content="Microsoft&reg; HTML Help Workshop 4.1">
<!-- Sitemap 1.0 -->
</HEAD><BODY>
<OBJECT type="text/site properties">
<param name="Window Styles" value="0x801227">
<param name="ImageType" value="Folder">
</OBJECT>
<UL>
'''
contents_footer = '''\
</UL></BODY></HTML>
'''
object_sitemap = '''\
<OBJECT type="text/sitemap">
<param name="Name" value="%s">
<param name="Local" value="%s">
</OBJECT>
'''
# List of words the full text search facility shouldn't index. This
# becomes file outname.stp. Note that this list must be pretty small!
# Different versions of the MS docs claim the file has a maximum size of
# 256 or 512 bytes (including \r\n at the end of each line).
# Note that "and", "or", "not" and "near" are operators in the search
# language, so no point indexing them even if we wanted to.
stopwords = """
a and are as at
be but by
for
if in into is it
near no not
of on or
such
that the their then there these they this to
was will with
""".split()
# The following list includes only languages supported by Sphinx. See
# https://docs.microsoft.com/en-us/previous-versions/windows/embedded/ms930130(v=msdn.10)
# for more.
chm_locales = {
# lang: LCID, encoding
'ca': (0x403, 'cp1252'),
'cs': (0x405, 'cp1250'),
'da': (0x406, 'cp1252'),
'de': (0x407, 'cp1252'),
'en': (0x409, 'cp1252'),
'es': (0x40a, 'cp1252'),
'et': (0x425, 'cp1257'),
'fa': (0x429, 'cp1256'),
'fi': (0x40b, 'cp1252'),
'fr': (0x40c, 'cp1252'),
'hr': (0x41a, 'cp1250'),
'hu': (0x40e, 'cp1250'),
'it': (0x410, 'cp1252'),
'ja': (0x411, 'cp932'),
'ko': (0x412, 'cp949'),
'lt': (0x427, 'cp1257'),
'lv': (0x426, 'cp1257'),
'nl': (0x413, 'cp1252'),
'no_NB': (0x414, 'cp1252'),
'pl': (0x415, 'cp1250'),
'pt_BR': (0x416, 'cp1252'),
'ru': (0x419, 'cp1251'),
'sk': (0x41b, 'cp1250'),
'sl': (0x424, 'cp1250'),
'sv': (0x41d, 'cp1252'),
'tr': (0x41f, 'cp1254'),
'uk_UA': (0x422, 'cp1251'),
'zh_CN': (0x804, 'cp936'),
'zh_TW': (0x404, 'cp950'),
}
def chm_htmlescape(s, quote=True):
# type: (str, bool) -> str
"""
chm_htmlescape() is a wrapper of html.escape().
.hhc/.hhk files don't recognize hex escaping, we need convert
hex escaping to decimal escaping. for example: ``&#x27;`` -> ``&#39;``
html.escape() may generates a hex escaping ``&#x27;`` for single
quote ``'``, this wrapper fixes this.
"""
s = html.escape(s, quote)
s = s.replace('&#x27;', '&#39;') # re-escape as decimal
return s
class HTMLHelpBuilder(StandaloneHTMLBuilder):
"""
Builder that also outputs Windows HTML help project, contents and
index files. Adapted from the original Doc/tools/prechm.py.
"""
name = 'htmlhelp'
epilog = __('You can now run HTML Help Workshop with the .htp file in '
'%(outdir)s.')
# don't copy the reST source
copysource = False
supported_image_types = ['image/png', 'image/gif', 'image/jpeg']
# don't add links
add_permalinks = False
# don't add sidebar etc.
embedded = True
# don't generate search index or include search page
search = False
lcid = 0x409
encoding = 'cp1252'
def init(self):
# type: () -> None
# the output files for HTML help is .html by default
self.out_suffix = '.html'
self.link_suffix = '.html'
super().init()
# determine the correct locale setting
locale = chm_locales.get(self.config.language)
if locale is not None:
self.lcid, self.encoding = locale
def open_file(self, outdir, basename, mode='w'):
# type: (str, str, str) -> IO
# open a file with the correct encoding for the selected language
warnings.warn('HTMLHelpBuilder.open_file() is deprecated.',
RemovedInSphinx40Warning)
return open(path.join(outdir, basename), mode, encoding=self.encoding,
errors='xmlcharrefreplace')
def update_page_context(self, pagename, templatename, ctx, event_arg):
# type: (str, str, Dict, str) -> None
ctx['encoding'] = self.encoding
def handle_finish(self):
# type: () -> None
self.build_hhx(self.outdir, self.config.htmlhelp_basename)
def write_doc(self, docname, doctree):
# type: (str, nodes.document) -> None
for node in doctree.traverse(nodes.reference):
# add ``target=_blank`` attributes to external links
if node.get('internal') is None and 'refuri' in node:
node['target'] = '_blank'
super().write_doc(docname, doctree)
def build_hhx(self, outdir, outname):
# type: (str, str) -> None
logger.info(__('dumping stopword list...'))
filename = path.join(outdir, outname + '.stp')
with open(filename, 'w', encoding=self.encoding, errors='xmlcharrefreplace') as f:
for word in sorted(stopwords):
print(word, file=f)
logger.info(__('writing project file...'))
filename = path.join(outdir, outname + '.hhp')
with open(filename, 'w', encoding=self.encoding, errors='xmlcharrefreplace') as f:
f.write(project_template % {
'outname': outname,
'title': self.config.html_title,
'version': self.config.version,
'project': self.config.project,
'lcid': self.lcid,
'master_doc': self.config.master_doc + self.out_suffix
})
if not outdir.endswith(os.sep):
outdir += os.sep
olen = len(outdir)
for root, dirs, files in os.walk(outdir):
dirs.sort()
files.sort()
staticdir = root.startswith(path.join(outdir, '_static'))
for fn in sorted(files):
if (staticdir and not fn.endswith('.js')) or \
fn.endswith('.html'):
print(path.join(root, fn)[olen:].replace(os.sep, '\\'),
file=f)
logger.info(__('writing TOC file...'))
filename = path.join(outdir, outname + '.hhc')
with open(filename, 'w', encoding=self.encoding, errors='xmlcharrefreplace') as f:
f.write(contents_header)
# special books
f.write('<LI> ' + object_sitemap % (self.config.html_short_title,
self.config.master_doc + self.out_suffix))
for indexname, indexcls, content, collapse in self.domain_indices:
f.write('<LI> ' + object_sitemap % (indexcls.localname,
'%s.html' % indexname))
# the TOC
tocdoc = self.env.get_and_resolve_doctree(
self.config.master_doc, self, prune_toctrees=False)
def write_toc(node, ullevel=0):
# type: (nodes.Node, int) -> None
if isinstance(node, nodes.list_item):
f.write('<LI> ')
for subnode in node:
write_toc(subnode, ullevel)
elif isinstance(node, nodes.reference):
link = node['refuri']
title = chm_htmlescape(node.astext(), True)
f.write(object_sitemap % (title, link))
elif isinstance(node, nodes.bullet_list):
if ullevel != 0:
f.write('<UL>\n')
for subnode in node:
write_toc(subnode, ullevel + 1)
if ullevel != 0:
f.write('</UL>\n')
elif isinstance(node, addnodes.compact_paragraph):
for subnode in node:
write_toc(subnode, ullevel)
matcher = NodeMatcher(addnodes.compact_paragraph, toctree=True)
for node in tocdoc.traverse(matcher): # type: addnodes.compact_paragraph
write_toc(node)
f.write(contents_footer)
logger.info(__('writing index file...'))
index = IndexEntries(self.env).create_index(self)
filename = path.join(outdir, outname + '.hhk')
with open(filename, 'w', encoding=self.encoding, errors='xmlcharrefreplace') as f:
f.write('<UL>\n')
def write_index(title, refs, subitems):
# type: (str, List[Tuple[str, str]], List[Tuple[str, List[Tuple[str, str]]]]) -> None # NOQA
def write_param(name, value):
# type: (str, str) -> None
item = ' <param name="%s" value="%s">\n' % (name, value)
f.write(item)
title = chm_htmlescape(title, True)
f.write('<LI> <OBJECT type="text/sitemap">\n')
write_param('Keyword', title)
if len(refs) == 0:
write_param('See Also', title)
elif len(refs) == 1:
write_param('Local', refs[0][1])
else:
for i, ref in enumerate(refs):
# XXX: better title?
write_param('Name', '[%d] %s' % (i, ref[1]))
write_param('Local', ref[1])
f.write('</OBJECT>\n')
if subitems:
f.write('<UL> ')
for subitem in subitems:
write_index(subitem[0], subitem[1], [])
f.write('</UL>')
for (key, group) in index:
for title, (refs, subitems, key_) in group:
write_index(title, refs, subitems)
f.write('</UL>\n')
def default_htmlhelp_basename(config):
# type: (Config) -> str
"""Better default htmlhelp_basename setting."""
return make_filename_from_project(config.project) + 'doc'
deprecated_alias('sphinx.builders.devhelp',
{
'chm_locales': chm_locales,
'chm_htmlescape': chm_htmlescape,
'HTMLHelpBuilder': HTMLHelpBuilder,
'default_htmlhelp_basename': default_htmlhelp_basename,
},
RemovedInSphinx40Warning)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.setup_extension('sphinx.builders.html')
app.add_builder(HTMLHelpBuilder)
app.add_config_value('htmlhelp_basename', default_htmlhelp_basename, None)
app.add_config_value('htmlhelp_file_suffix', None, 'html', [str])
app.add_config_value('htmlhelp_link_suffix', None, 'html', [str])
warnings.warn('sphinx.builders.htmlhelp has been moved to sphinxcontrib-htmlhelp.',
RemovedInSphinx40Warning)
app.setup_extension('sphinxcontrib.htmlhelp')
return {
'version': 'builtin',

View File

@@ -28,7 +28,7 @@ from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import SphinxError
from sphinx.locale import _, __
from sphinx.transforms import SphinxTransformer
from sphinx.util import texescape, logging, status_iterator
from sphinx.util import texescape, logging, progress_message, status_iterator
from sphinx.util.console import bold, darkgreen # type: ignore
from sphinx.util.docutils import SphinxFileOutput, new_document
from sphinx.util.fileutil import copy_asset_file
@@ -250,33 +250,32 @@ class LaTeXBuilder(Builder):
toctree_only = entry[5]
destination = SphinxFileOutput(destination_path=path.join(self.outdir, targetname),
encoding='utf-8', overwrite_if_changed=True)
logger.info(__("processing %s..."), targetname, nonl=True)
toctrees = self.env.get_doctree(docname).traverse(addnodes.toctree)
if toctrees:
if toctrees[0].get('maxdepth') > 0:
tocdepth = toctrees[0].get('maxdepth')
with progress_message(__("processing %s") % targetname):
toctrees = self.env.get_doctree(docname).traverse(addnodes.toctree)
if toctrees:
if toctrees[0].get('maxdepth') > 0:
tocdepth = toctrees[0].get('maxdepth')
else:
tocdepth = None
else:
tocdepth = None
else:
tocdepth = None
doctree = self.assemble_doctree(
docname, toctree_only,
appendices=((docclass != 'howto') and self.config.latex_appendices or []))
doctree['tocdepth'] = tocdepth
self.apply_transforms(doctree)
self.post_process_images(doctree)
self.update_doc_context(title, author)
doctree = self.assemble_doctree(
docname, toctree_only,
appendices=((docclass != 'howto') and self.config.latex_appendices or []))
doctree['tocdepth'] = tocdepth
self.apply_transforms(doctree)
self.post_process_images(doctree)
self.update_doc_context(title, author)
logger.info(__("writing... "), nonl=True)
docsettings.author = author
docsettings.title = title
docsettings.contentsname = self.get_contentsname(docname)
docsettings.docname = docname
docsettings.docclass = docclass
with progress_message(__("writing")):
docsettings.author = author
docsettings.title = title
docsettings.contentsname = self.get_contentsname(docname)
docsettings.docname = docname
docsettings.docclass = docclass
doctree.settings = docsettings
docwriter.write(doctree, destination)
logger.info(__("done"))
doctree.settings = docsettings
docwriter.write(doctree, destination)
def get_contentsname(self, indexfile):
# type: (str) -> str
@@ -354,8 +353,15 @@ class LaTeXBuilder(Builder):
# type: () -> None
self.copy_image_files()
self.write_message_catalog()
self.copy_support_files()
# copy TeX support files from texinputs
if self.config.latex_additional_files:
self.copy_latex_additional_files()
@progress_message(__('copying TeX support files'))
def copy_support_files(self):
# type: () -> None
"""copy TeX support files from texinputs."""
# configure usage of xindy (impacts Makefile and latexmkrc)
# FIXME: convert this rather to a confval with suitable default
# according to language ? but would require extra documentation
@@ -386,21 +392,19 @@ class LaTeXBuilder(Builder):
copy_asset_file(path.join(staticdirname, 'Makefile_t'),
self.outdir, context=context)
# copy additional files
if self.config.latex_additional_files:
logger.info(bold(__('copying additional files...')), nonl=True)
for filename in self.config.latex_additional_files:
logger.info(' ' + filename, nonl=True)
copy_asset_file(path.join(self.confdir, filename), self.outdir)
logger.info('')
# the logo is handled differently
if self.config.latex_logo:
if not path.isfile(path.join(self.confdir, self.config.latex_logo)):
raise SphinxError(__('logo file %r does not exist') % self.config.latex_logo)
else:
copy_asset_file(path.join(self.confdir, self.config.latex_logo), self.outdir)
logger.info(__('done'))
@progress_message(__('copying additional files'))
def copy_latex_additional_files(self):
# type: () -> None
for filename in self.config.latex_additional_files:
logger.info(' ' + filename, nonl=True)
copy_asset_file(path.join(self.confdir, filename), self.outdir)
def copy_image_files(self):
# type: () -> None
@@ -439,7 +443,7 @@ def validate_config_values(app, config):
for key in list(config.latex_elements):
if key not in DEFAULT_SETTINGS:
msg = __("Unknown configure key: latex_elements[%r]. ignored.")
logger.warning(msg % key)
logger.warning(msg % (key,))
config.latex_elements.pop(key)

View File

@@ -25,7 +25,7 @@ from sphinx.util import encode_uri, requests, logging
from sphinx.util.console import ( # type: ignore
purple, red, darkgreen, darkgray, darkred, turquoise
)
from sphinx.util.nodes import traverse_parent
from sphinx.util.nodes import get_node_line
from sphinx.util.requests import is_ssl_error
if False:
@@ -271,17 +271,24 @@ class CheckExternalLinksBuilder(Builder):
# type: (str, nodes.Node) -> None
logger.info('')
n = 0
for node in doctree.traverse(nodes.reference):
if 'refuri' not in node:
# reference nodes
for refnode in doctree.traverse(nodes.reference):
if 'refuri' not in refnode:
continue
uri = node['refuri']
lineno = None
for parent in traverse_parent(node):
if parent.line:
lineno = parent.line
break
uri = refnode['refuri']
lineno = get_node_line(refnode)
self.wqueue.put((uri, docname, lineno), False)
n += 1
# image nodes
for imgnode in doctree.traverse(nodes.image):
uri = imgnode['candidates'].get('?')
if uri and '://' in uri:
lineno = get_node_line(imgnode)
self.wqueue.put((uri, docname, lineno), False)
n += 1
done = 0
while done < n:
self.process_result(self.rqueue.get())

View File

@@ -18,7 +18,8 @@ from sphinx.builders import Builder
from sphinx.environment import NoUri
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.console import bold, darkgreen # type: ignore
from sphinx.util import progress_message
from sphinx.util.console import darkgreen # type: ignore
from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.osutil import make_filename_from_project
from sphinx.writers.manpage import ManualPageWriter, ManualPageTranslator
@@ -60,6 +61,7 @@ class ManualPageBuilder(Builder):
return ''
raise NoUri
@progress_message(__('writing'))
def write(self, *ignored):
# type: (Any) -> None
docwriter = ManualPageWriter(self)
@@ -68,8 +70,6 @@ class ManualPageBuilder(Builder):
components=(docwriter,),
read_config_files=True).get_default_values() # type: Any
logger.info(bold(__('writing... ')), nonl=True)
for info in self.config.man_pages:
docname, name, description, authors, section = info
if docname not in self.env.all_docs:
@@ -105,7 +105,6 @@ class ManualPageBuilder(Builder):
pendingnode.replace_self(pendingnode.children)
docwriter.write(largetree, destination)
logger.info('')
def finish(self):
# type: () -> None

View File

@@ -16,7 +16,8 @@ from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment.adapters.toctree import TocTree
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.console import bold, darkgreen # type: ignore
from sphinx.util import progress_message
from sphinx.util.console import darkgreen # type: ignore
from sphinx.util.nodes import inline_all_toctrees
if False:
@@ -162,24 +163,32 @@ class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
# type: (Any) -> None
docnames = self.env.all_docs
logger.info(bold(__('preparing documents... ')), nonl=True)
self.prepare_writing(docnames) # type: ignore
logger.info(__('done'))
with progress_message(__('preparing documents')):
self.prepare_writing(docnames) # type: ignore
logger.info(bold(__('assembling single document... ')), nonl=True)
doctree = self.assemble_doctree()
self.env.toc_secnumbers = self.assemble_toc_secnumbers()
self.env.toc_fignumbers = self.assemble_toc_fignumbers()
logger.info('')
logger.info(bold(__('writing... ')), nonl=True)
self.write_doc_serialized(self.config.master_doc, doctree)
self.write_doc(self.config.master_doc, doctree)
logger.info(__('done'))
with progress_message(__('assembling single document')):
doctree = self.assemble_doctree()
self.env.toc_secnumbers = self.assemble_toc_secnumbers()
self.env.toc_fignumbers = self.assemble_toc_fignumbers()
with progress_message(__('writing')):
self.write_doc_serialized(self.config.master_doc, doctree)
self.write_doc(self.config.master_doc, doctree)
def finish(self):
# type: () -> None
self.write_additional_files()
self.copy_image_files()
self.copy_download_files()
self.copy_static_files()
self.copy_extra_files()
self.write_buildinfo()
self.dump_inventory()
@progress_message(__('writing additional files'))
def write_additional_files(self):
# type: () -> None
# no indices or search pages are supported
logger.info(bold(__('writing additional files...')), nonl=True)
# additional pages from conf.py
for pagename, template in self.config.html_additional_pages.items():
@@ -191,15 +200,6 @@ class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
fn = path.join(self.outdir, '_static', 'opensearch.xml')
self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn)
logger.info('')
self.copy_image_files()
self.copy_download_files()
self.copy_static_files()
self.copy_extra_files()
self.write_buildinfo()
self.dump_inventory()
def setup(app):
# type: (Sphinx) -> Dict[str, Any]

View File

@@ -22,8 +22,8 @@ from sphinx.environment import NoUri
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util import status_iterator
from sphinx.util.console import bold, darkgreen # type: ignore
from sphinx.util import progress_message, status_iterator
from sphinx.util.console import darkgreen # type: ignore
from sphinx.util.docutils import new_document
from sphinx.util.fileutil import copy_asset_file
from sphinx.util.nodes import inline_all_toctrees
@@ -113,28 +113,27 @@ class TexinfoBuilder(Builder):
destination = FileOutput(
destination_path=path.join(self.outdir, targetname),
encoding='utf-8')
logger.info(__("processing %s..."), targetname, nonl=True)
doctree = self.assemble_doctree(
docname, toctree_only,
appendices=(self.config.texinfo_appendices or []))
logger.info(__("writing... "), nonl=True)
self.post_process_images(doctree)
docwriter = TexinfoWriter(self)
settings = OptionParser(
defaults=self.env.settings,
components=(docwriter,),
read_config_files=True).get_default_values() # type: Any
settings.author = author
settings.title = title
settings.texinfo_filename = targetname[:-5] + '.info'
settings.texinfo_elements = self.config.texinfo_elements
settings.texinfo_dir_entry = direntry or ''
settings.texinfo_dir_category = category or ''
settings.texinfo_dir_description = description or ''
settings.docname = docname
doctree.settings = settings
docwriter.write(doctree, destination)
logger.info(__("done"))
with progress_message(__("processing %s") % targetname):
appendices = self.config.texinfo_appendices or []
doctree = self.assemble_doctree(docname, toctree_only, appendices=appendices)
with progress_message(__("writing")):
self.post_process_images(doctree)
docwriter = TexinfoWriter(self)
settings = OptionParser(
defaults=self.env.settings,
components=(docwriter,),
read_config_files=True).get_default_values() # type: Any
settings.author = author
settings.title = title
settings.texinfo_filename = targetname[:-5] + '.info'
settings.texinfo_elements = self.config.texinfo_elements
settings.texinfo_dir_entry = direntry or ''
settings.texinfo_dir_category = category or ''
settings.texinfo_dir_description = description or ''
settings.docname = docname
doctree.settings = settings
docwriter.write(doctree, destination)
def assemble_doctree(self, indexfile, toctree_only, appendices):
# type: (str, bool, List[str]) -> nodes.document
@@ -182,16 +181,7 @@ class TexinfoBuilder(Builder):
def finish(self):
# type: () -> None
self.copy_image_files()
logger.info(bold(__('copying Texinfo support files... ')), nonl=True)
# copy Makefile
fn = path.join(self.outdir, 'Makefile')
logger.info(fn, nonl=True)
try:
copy_asset_file(os.path.join(template_dir, 'Makefile'), fn)
except OSError as err:
logger.warning(__("error writing file %s: %s"), fn, err)
logger.info(__(' done'))
self.copy_support_files()
def copy_image_files(self):
# type: () -> None
@@ -208,6 +198,15 @@ class TexinfoBuilder(Builder):
logger.warning(__('cannot copy image file %r: %s'),
path.join(self.srcdir, src), err)
def copy_support_files(self):
# type: () -> None
try:
with progress_message(__('copying Texinfo support files')):
logger.info('Makefile ', nonl=True)
copy_asset_file(os.path.join(template_dir, 'Makefile'), self.outdir)
except OSError as err:
logger.warning(__("error writing file Makefile: %s"), err)
def default_texinfo_documents(config):
# type: (Config) -> List[Tuple[str, str, str, str, str, str, str]]

View File

@@ -23,9 +23,8 @@ from sphinx.application import Sphinx
from sphinx.errors import SphinxError
from sphinx.locale import __
from sphinx.util import Tee, format_exception_cut_frames, save_traceback
from sphinx.util.console import red, nocolor, color_terminal # type: ignore
from sphinx.util.console import red, nocolor, color_terminal, terminal_safe # type: ignore
from sphinx.util.docutils import docutils_namespace, patch_docutils
from sphinx.util.pycompat import terminal_safe
if False:
# For type annotation

View File

@@ -17,7 +17,6 @@ import time
import warnings
from collections import OrderedDict
from os import path
from urllib.parse import quote
# try to import readline, unix specific enhancement
try:
@@ -37,11 +36,10 @@ import sphinx.locale
from sphinx import __display_version__, package_dir
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.locale import __
from sphinx.util import texescape
from sphinx.util.console import ( # type: ignore
colorize, bold, red, turquoise, nocolor, color_terminal
)
from sphinx.util.osutil import ensuredir, make_filename
from sphinx.util.osutil import ensuredir
from sphinx.util.template import SphinxRenderer
if False:
@@ -375,25 +373,15 @@ def generate(d, overwrite=True, silent=False, templatedir=None):
"""Generate project based on values in *d*."""
template = QuickstartRenderer(templatedir=templatedir)
texescape.init()
if 'mastertoctree' not in d:
d['mastertoctree'] = ''
if 'mastertocmaxdepth' not in d:
d['mastertocmaxdepth'] = 2
d['PY3'] = True
d['project_fn'] = make_filename(d['project'])
d['project_url'] = quote(d['project'].encode('idna'))
d['project_manpage'] = d['project_fn'].lower()
d['now'] = time.asctime()
d['project_underline'] = column_width(d['project']) * '='
d.setdefault('extensions', [])
d['copyright'] = time.strftime('%Y') + ', ' + d['author']
d['author_texescaped'] = d['author'].translate(texescape.tex_escape_map)
d['project_doc'] = d['project'] + ' Documentation'
d['project_doc_texescaped'] = (d['project'] + ' Documentation').\
translate(texescape.tex_escape_map)
ensuredir(d['path'])
@@ -528,7 +516,7 @@ Makefile to be used with sphinx-build.
group = parser.add_argument_group(__('Structure options'))
group.add_argument('--sep', action='store_true', default=None,
help=__('if specified, separate source and build dirs'))
group.add_argument('--dot', metavar='DOT',
group.add_argument('--dot', metavar='DOT', default='_',
help=__('replacement for dot in _templates etc.'))
group = parser.add_argument_group(__('Project basic options'))
@@ -542,9 +530,9 @@ Makefile to be used with sphinx-build.
help=__('release of project'))
group.add_argument('-l', '--language', metavar='LANGUAGE', dest='language',
help=__('document language'))
group.add_argument('--suffix', metavar='SUFFIX',
group.add_argument('--suffix', metavar='SUFFIX', default='.rst',
help=__('source file suffix'))
group.add_argument('--master', metavar='MASTER',
group.add_argument('--master', metavar='MASTER', default='index',
help=__('master document name'))
group.add_argument('--epub', action='store_true', default=False,
help=__('use epub'))
@@ -558,11 +546,11 @@ Makefile to be used with sphinx-build.
action='append', help=__('enable arbitrary extensions'))
group = parser.add_argument_group(__('Makefile and Batchfile creation'))
group.add_argument('--makefile', action='store_true', dest='makefile', default=None,
group.add_argument('--makefile', action='store_true', dest='makefile', default=True,
help=__('create makefile'))
group.add_argument('--no-makefile', action='store_false', dest='makefile',
help=__('do not create makefile'))
group.add_argument('--batchfile', action='store_true', dest='batchfile', default=None,
group.add_argument('--batchfile', action='store_true', dest='batchfile', default=True,
help=__('create batchfile'))
group.add_argument('--no-batchfile', action='store_false',
dest='batchfile',
@@ -604,6 +592,13 @@ def main(argv=sys.argv[1:]):
# delete None or False value
d = dict((k, v) for k, v in d.items() if v is not None)
# handle use of CSV-style extension values
d.setdefault('extensions', [])
for ext in d['extensions'][:]:
if ',' in ext:
d['extensions'].remove(ext)
d['extensions'].extend(ext.split(','))
try:
if 'quiet' in d:
if not set(['project', 'author']).issubset(d):
@@ -633,13 +628,6 @@ def main(argv=sys.argv[1:]):
print('[Interrupted.]')
return 130 # 128 + SIGINT
# handle use of CSV-style extension values
d.setdefault('extensions', [])
for ext in d['extensions'][:]:
if ',' in ext:
d['extensions'].remove(ext)
d['extensions'].extend(ext.split(','))
for variable in d.get('variables', []):
try:
name, value = variable.split('=')

View File

@@ -22,7 +22,8 @@ from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util.i18n import format_date
from sphinx.util.osutil import cd
from sphinx.util.pycompat import execfile_, NoneType
from sphinx.util.pycompat import execfile_
from sphinx.util.typing import NoneType
if False:
# For type annotation

View File

@@ -48,7 +48,7 @@ class _ModuleWrapper(object):
def __getattr__(self, name):
# type: (str) -> Any
if name in self._objects:
warnings.warn("%s.%s is now deprecated. Please refer CHANGES to grasp"
warnings.warn("%s.%s is now deprecated. Please refer CHANGES to grasp "
"the changes of Sphinx API." % (self._modname, name),
self._warning, stacklevel=3)
return self._objects[name]

View File

@@ -47,9 +47,11 @@ class Highlight(SphinxDirective):
def run(self):
# type: () -> List[nodes.Node]
language = self.arguments[0].strip()
linenothreshold = self.options.get('linenothreshold', sys.maxsize)
return [addnodes.highlightlang(lang=self.arguments[0].strip(),
linenothreshold=linenothreshold)]
self.env.temp_data['highlight_language'] = language
return [addnodes.highlightlang(lang=language, linenothreshold=linenothreshold)]
class HighlightLang(Highlight):
@@ -110,8 +112,8 @@ class CodeBlock(SphinxDirective):
"""
has_content = True
required_arguments = 1
optional_arguments = 0
required_arguments = 0
optional_arguments = 1
final_argument_whitespace = False
option_spec = {
'linenos': directives.flag,
@@ -152,10 +154,20 @@ class CodeBlock(SphinxDirective):
code = '\n'.join(lines)
literal = nodes.literal_block(code, code) # type: nodes.Element
literal['language'] = self.arguments[0]
literal['linenos'] = 'linenos' in self.options or \
'lineno-start' in self.options
if 'linenos' in self.options or 'lineno-start' in self.options:
literal['linenos'] = True
literal['classes'] += self.options.get('class', [])
if self.arguments:
# highlight language specified
literal['language'] = self.arguments[0]
literal['force_highlighting'] = True
else:
# no highlight language specified. Then this directive refers the current
# highlight setting via ``highlight`` directive or ``highlight_language``
# configuration.
literal['language'] = self.env.temp_data.get('highlight_language',
self.config.highlight_language)
literal['force_highlighting'] = False
extra_args = literal['highlight_args'] = {}
if hl_lines is not None:
extra_args['hl_lines'] = hl_lines
@@ -439,9 +451,9 @@ class LiteralInclude(SphinxDirective):
retnode['language'] = 'udiff'
elif 'language' in self.options:
retnode['language'] = self.options['language']
retnode['linenos'] = ('linenos' in self.options or
'lineno-start' in self.options or
'lineno-match' in self.options)
if ('linenos' in self.options or 'lineno-start' in self.options or
'lineno-match' in self.options):
retnode['linenos'] = True
retnode['classes'] += self.options.get('class', [])
extra_args = retnode['highlight_args'] = {}
if 'emphasize-lines' in self.options:

View File

@@ -91,32 +91,54 @@ class Index:
def generate(self, docnames=None):
# type: (Iterable[str]) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]
"""Return entries for the index given by *name*. If *docnames* is
given, restrict to entries referring to these docnames.
"""Get entries for the index.
The return value is a tuple of ``(content, collapse)``, where *collapse*
is a boolean that determines if sub-entries should start collapsed (for
output formats that support collapsing sub-entries).
If ``docnames`` is given, restrict to entries referring to these
docnames.
*content* is a sequence of ``(letter, entries)`` tuples, where *letter*
is the "heading" for the given *entries*, usually the starting letter.
The return value is a tuple of ``(content, collapse)``:
*entries* is a sequence of single entries, where a single entry is a
sequence ``[name, subtype, docname, anchor, extra, qualifier, descr]``.
The items in this sequence have the following meaning:
``collapse``
A boolean that determines if sub-entries should start collapsed (for
output formats that support collapsing sub-entries).
- `name` -- the name of the index entry to be displayed
- `subtype` -- sub-entry related type:
0 -- normal entry
1 -- entry with sub-entries
2 -- sub-entry
- `docname` -- docname where the entry is located
- `anchor` -- anchor for the entry within `docname`
- `extra` -- extra info for the entry
- `qualifier` -- qualifier for the description
- `descr` -- description for the entry
``content``:
A sequence of ``(letter, entries)`` tuples, where ``letter`` is the
"heading" for the given ``entries``, usually the starting letter, and
``entries`` is a sequence of single entries. Each entry is a sequence
``[name, subtype, docname, anchor, extra, qualifier, descr]``. The
items in this sequence have the following meaning:
Qualifier and description are not rendered e.g. in LaTeX output.
``name``
The name of the index entry to be displayed.
``subtype``
The sub-entry related type. One of:
``0``
A normal entry.
``1``
An entry with sub-entries.
``2``
A sub-entry.
``docname``
*docname* where the entry is located.
``anchor``
Anchor for the entry within ``docname``
``extra``
Extra info for the entry.
``qualifier``
Qualifier for the description.
``descr``
Description for the entry.
Qualifier and description are not rendered for some output formats such
as LaTeX.
"""
raise NotImplementedError
@@ -318,21 +340,37 @@ class Domain:
def get_objects(self):
# type: () -> Iterable[Tuple[str, str, str, str, str, int]]
"""Return an iterable of "object descriptions", which are tuples with
five items:
"""Return an iterable of "object descriptions".
* `name` -- fully qualified name
* `dispname` -- name to display when searching/linking
* `type` -- object type, a key in ``self.object_types``
* `docname` -- the document where it is to be found
* `anchor` -- the anchor name for the object
* `priority` -- how "important" the object is (determines placement
in search results)
Object descriptions are tuples with six items:
- 1: default priority (placed before full-text matches)
- 0: object is important (placed before default-priority objects)
- 2: object is unimportant (placed after full-text matches)
- -1: object should not show up in search at all
``name``
Fully qualified name.
``dispname``
Name to display when searching/linking.
``type``
Object type, a key in ``self.object_types``.
``docname``
The document where it is to be found.
``anchor``
The anchor name for the object.
``priority``
How "important" the object is (determines placement in search
results). One of:
``1``
Default priority (placed before full-text matches).
``0``
Object is important (placed before default-priority objects).
``2``
Object is unimportant (placed after full-text matches).
``-1``
Object should not show up in search at all.
"""
return []

View File

@@ -7158,7 +7158,7 @@ class CPPDomain(Domain):
# the non-identifier refs are cross-references, which should be processed:
# - fix parenthesis due to operator() and add_function_parentheses
if typ != "identifier":
title = contnode.pop(0).astext() # type: ignore
title = contnode.pop(0).astext()
# If it's operator(), we need to add '()' if explicit function parens
# are requested. Then the Sphinx machinery will add another pair.
# Also, if it's an 'any' ref that resolves to a function, we need to add

View File

@@ -151,7 +151,7 @@ class PyXrefMixin:
delims_re = re.compile(delims)
sub_targets = re.split(delims, target)
split_contnode = bool(contnode and contnode.astext() == target) # type: ignore
split_contnode = bool(contnode and contnode.astext() == target)
results = []
for sub_target in filter(None, sub_targets):

View File

@@ -25,7 +25,7 @@ from sphinx.transforms import SphinxTransformer
from sphinx.util import DownloadFiles, FilenameUniqDict
from sphinx.util import logging
from sphinx.util.docutils import LoggingReporter
from sphinx.util.i18n import find_catalog_files
from sphinx.util.i18n import CatalogRepository, docname_to_domain
from sphinx.util.nodes import is_translatable
if False:
@@ -395,15 +395,13 @@ class BuildEnvironment:
# move i18n process into the writing phase, and remove these lines.
if builder.use_message_catalog:
# add catalog mo file dependency
repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
self.config.language, self.config.source_encoding)
for docname in self.found_docs:
catalog_files = find_catalog_files(
docname,
self.srcdir,
self.config.locale_dirs,
self.config.language,
self.config.gettext_compact)
for filename in catalog_files:
self.dependencies[docname].add(filename)
domain = docname_to_domain(docname, self.config.gettext_compact)
for catalog in repo.catalogs:
if catalog.domain == domain:
self.dependencies[docname].add(catalog.mo_path)
except OSError as exc:
raise DocumentError(__('Failed to scan documents in %s: %r') % (self.srcdir, exc))

View File

@@ -18,9 +18,11 @@ from typing import Any
from docutils.statemachine import StringList
import sphinx
from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning
from sphinx.ext.autodoc.importer import mock, import_object, get_object_members
from sphinx.ext.autodoc.importer import _MockImporter # to keep compatibility # NOQA
from sphinx.deprecation import (
RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias
)
from sphinx.ext.autodoc.importer import import_object, get_object_members
from sphinx.ext.autodoc.mock import mock
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import logging
@@ -72,7 +74,7 @@ INSTANCEATTR = object()
def members_option(arg):
# type: (Any) -> Union[object, List[str]]
"""Used to convert the :members: option to auto directives."""
if arg is None:
if arg is None or arg is True:
return ALL
return [x.strip() for x in arg.split(',')]
@@ -1002,6 +1004,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
return None
try:
if (not isfunction(self.object) and
not inspect.ismethod(self.object) and
not isbuiltin(self.object) and
not inspect.isclass(self.object) and
hasattr(self.object, '__call__')):
@@ -1032,6 +1035,23 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
pass
class DecoratorDocumenter(FunctionDocumenter):
"""
Specialized Documenter subclass for decorator functions.
"""
objtype = 'decorator'
# must be lower than FunctionDocumenter
priority = -1
def format_args(self):
args = super().format_args()
if ',' in args:
return args
else:
return None
class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for classes.
@@ -1454,6 +1474,15 @@ def merge_autodoc_default_flags(app, config):
)
from sphinx.ext.autodoc.mock import _MockImporter # NOQA
deprecated_alias('sphinx.ext.autodoc',
{
'_MockImporter': _MockImporter,
},
RemovedInSphinx40Warning)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.add_autodocumenter(ModuleDocumenter)
@@ -1461,6 +1490,7 @@ def setup(app):
app.add_autodocumenter(ExceptionDocumenter)
app.add_autodocumenter(DataDocumenter)
app.add_autodocumenter(FunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)
app.add_autodocumenter(AttributeDocumenter)
app.add_autodocumenter(InstanceAttributeDocumenter)

View File

@@ -8,199 +8,22 @@
:license: BSD, see LICENSE for details.
"""
import contextlib
import os
import sys
import traceback
import warnings
from collections import namedtuple
from importlib.abc import Loader, MetaPathFinder
from importlib.machinery import ModuleSpec
from types import FunctionType, MethodType, ModuleType
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.util import logging
from sphinx.util.inspect import isenumclass, safe_getattr
if False:
# For type annotation
from typing import Any, Callable, Dict, Generator, Iterator, List, Optional, Sequence, Tuple, Union # NOQA
from typing import Any, Callable, Dict, List # NOQA
logger = logging.getLogger(__name__)
class _MockObject:
"""Used by autodoc_mock_imports."""
def __new__(cls, *args, **kwargs):
# type: (Any, Any) -> Any
if len(args) == 3 and isinstance(args[1], tuple) and args[1][-1].__class__ is cls:
# subclassing MockObject
return type(args[0], (_MockObject,), args[2], **kwargs) # type: ignore
else:
return super(_MockObject, cls).__new__(cls)
def __init__(self, *args, **kwargs):
# type: (Any, Any) -> None
self.__qualname__ = ''
def __len__(self):
# type: () -> int
return 0
def __contains__(self, key):
# type: (str) -> bool
return False
def __iter__(self):
# type: () -> Iterator
return iter([])
def __mro_entries__(self, bases):
# type: (Tuple) -> Tuple
return bases
def __getitem__(self, key):
# type: (str) -> _MockObject
return self
def __getattr__(self, key):
# type: (str) -> _MockObject
return self
def __call__(self, *args, **kw):
# type: (Any, Any) -> Any
if args and type(args[0]) in [FunctionType, MethodType]:
# Appears to be a decorator, pass through unchanged
return args[0]
return self
class _MockModule(ModuleType):
"""Used by autodoc_mock_imports."""
__file__ = os.devnull
def __init__(self, name, loader=None):
# type: (str, _MockImporter) -> None
super().__init__(name)
self.__all__ = [] # type: List[str]
self.__path__ = [] # type: List[str]
if loader is not None:
warnings.warn('The loader argument for _MockModule is deprecated.',
RemovedInSphinx30Warning)
def __getattr__(self, name):
# type: (str) -> _MockObject
o = _MockObject()
o.__module__ = self.__name__
return o
class _MockImporter(MetaPathFinder):
def __init__(self, names):
# type: (List[str]) -> None
self.names = names
self.mocked_modules = [] # type: List[str]
# enable hook by adding itself to meta_path
sys.meta_path.insert(0, self)
warnings.warn('_MockImporter is now deprecated.',
RemovedInSphinx30Warning)
def disable(self):
# type: () -> None
# remove `self` from `sys.meta_path` to disable import hook
sys.meta_path = [i for i in sys.meta_path if i is not self]
# remove mocked modules from sys.modules to avoid side effects after
# running auto-documenter
for m in self.mocked_modules:
if m in sys.modules:
del sys.modules[m]
def find_module(self, name, path=None):
# type: (str, Sequence[Union[bytes, str]]) -> Any
# check if name is (or is a descendant of) one of our base_packages
for n in self.names:
if n == name or name.startswith(n + '.'):
return self
return None
def load_module(self, name):
# type: (str) -> ModuleType
if name in sys.modules:
# module has already been imported, return it
return sys.modules[name]
else:
logger.debug('[autodoc] adding a mock module %s!', name)
module = _MockModule(name, self)
sys.modules[name] = module
self.mocked_modules.append(name)
return module
class MockLoader(Loader):
"""A loader for mocking."""
def __init__(self, finder):
# type: (MockFinder) -> None
super().__init__()
self.finder = finder
def create_module(self, spec):
# type: (ModuleSpec) -> ModuleType
logger.debug('[autodoc] adding a mock module as %s!', spec.name)
self.finder.mocked_modules.append(spec.name)
return _MockModule(spec.name)
def exec_module(self, module):
# type: (ModuleType) -> None
pass # nothing to do
class MockFinder(MetaPathFinder):
"""A finder for mocking."""
def __init__(self, modnames):
# type: (List[str]) -> None
super().__init__()
self.modnames = modnames
self.loader = MockLoader(self)
self.mocked_modules = [] # type: List[str]
def find_spec(self, fullname, path, target=None):
# type: (str, Sequence[Union[bytes, str]], ModuleType) -> ModuleSpec
for modname in self.modnames:
# check if fullname is (or is a descendant of) one of our targets
if modname == fullname or fullname.startswith(modname + '.'):
return ModuleSpec(fullname, self.loader)
return None
def invalidate_caches(self):
# type: () -> None
"""Invalidate mocked modules on sys.modules."""
for modname in self.mocked_modules:
sys.modules.pop(modname, None)
@contextlib.contextmanager
def mock(modnames):
# type: (List[str]) -> Generator[None, None, None]
"""Insert mock modules during context::
with mock(['target.module.name']):
# mock modules are enabled here
...
"""
try:
finder = MockFinder(modnames)
sys.meta_path.insert(0, finder)
yield
finally:
sys.meta_path.remove(finder)
finder.invalidate_caches()
def import_module(modname, warningiserror=False):
# type: (str, bool) -> Any
"""
@@ -324,3 +147,19 @@ def get_object_members(subject, objpath, attrgetter, analyzer=None):
members[name] = Attribute(name, True, INSTANCEATTR)
return members
from sphinx.ext.autodoc.mock import ( # NOQA
_MockImporter, _MockModule, _MockObject, MockFinder, MockLoader, mock
)
deprecated_alias('sphinx.ext.autodoc.importer',
{
'_MockImporter': _MockImporter,
'_MockModule': _MockModule,
'_MockObject': _MockObject,
'MockFinder': MockFinder,
'MockLoader': MockLoader,
'mock': mock,
},
RemovedInSphinx40Warning)

217
sphinx/ext/autodoc/mock.py Normal file
View File

@@ -0,0 +1,217 @@
"""
sphinx.ext.autodoc.mock
~~~~~~~~~~~~~~~~~~~~~~~
mock for autodoc
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import contextlib
import os
import sys
import warnings
from importlib.abc import Loader, MetaPathFinder
from importlib.machinery import ModuleSpec
from types import FunctionType, MethodType, ModuleType
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.util import logging
if False:
# For type annotation
from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union # NOQA
logger = logging.getLogger(__name__)
class _MockObject:
"""Used by autodoc_mock_imports."""
__display_name__ = '_MockObject'
def __new__(cls, *args, **kwargs):
# type: (Any, Any) -> Any
if len(args) == 3 and isinstance(args[1], tuple):
superclass = args[1][-1].__class__
if superclass is cls:
# subclassing MockObject
return _make_subclass(args[0], superclass.__display_name__,
superclass=superclass, attributes=args[2])
return super(_MockObject, cls).__new__(cls)
def __init__(self, *args, **kwargs):
# type: (Any, Any) -> None
self.__qualname__ = ''
def __len__(self):
# type: () -> int
return 0
def __contains__(self, key):
# type: (str) -> bool
return False
def __iter__(self):
# type: () -> Iterator
return iter([])
def __mro_entries__(self, bases):
# type: (Tuple) -> Tuple
return (self.__class__,)
def __getitem__(self, key):
# type: (str) -> _MockObject
return _make_subclass(key, self.__display_name__, self.__class__)()
def __getattr__(self, key):
# type: (str) -> _MockObject
return _make_subclass(key, self.__display_name__, self.__class__)()
def __call__(self, *args, **kw):
# type: (Any, Any) -> Any
if args and type(args[0]) in [FunctionType, MethodType]:
# Appears to be a decorator, pass through unchanged
return args[0]
return self
def __repr__(self):
# type: () -> str
return self.__display_name__
def _make_subclass(name, module, superclass=_MockObject, attributes=None):
# type: (str, str, Any, dict) -> Any
attrs = {'__module__': module, '__display_name__': module + '.' + name}
attrs.update(attributes or {})
return type(name, (superclass,), attrs)
class _MockModule(ModuleType):
"""Used by autodoc_mock_imports."""
__file__ = os.devnull
def __init__(self, name, loader=None):
# type: (str, _MockImporter) -> None
super().__init__(name)
self.__all__ = [] # type: List[str]
self.__path__ = [] # type: List[str]
if loader is not None:
warnings.warn('The loader argument for _MockModule is deprecated.',
RemovedInSphinx30Warning)
def __getattr__(self, name):
# type: (str) -> _MockObject
return _make_subclass(name, self.__name__)()
def __repr__(self):
# type: () -> str
return self.__name__
class _MockImporter(MetaPathFinder):
def __init__(self, names):
# type: (List[str]) -> None
self.names = names
self.mocked_modules = [] # type: List[str]
# enable hook by adding itself to meta_path
sys.meta_path.insert(0, self)
warnings.warn('_MockImporter is now deprecated.',
RemovedInSphinx30Warning)
def disable(self):
# type: () -> None
# remove `self` from `sys.meta_path` to disable import hook
sys.meta_path = [i for i in sys.meta_path if i is not self]
# remove mocked modules from sys.modules to avoid side effects after
# running auto-documenter
for m in self.mocked_modules:
if m in sys.modules:
del sys.modules[m]
def find_module(self, name, path=None):
# type: (str, Sequence[Union[bytes, str]]) -> Any
# check if name is (or is a descendant of) one of our base_packages
for n in self.names:
if n == name or name.startswith(n + '.'):
return self
return None
def load_module(self, name):
# type: (str) -> ModuleType
if name in sys.modules:
# module has already been imported, return it
return sys.modules[name]
else:
logger.debug('[autodoc] adding a mock module %s!', name)
module = _MockModule(name, self)
sys.modules[name] = module
self.mocked_modules.append(name)
return module
class MockLoader(Loader):
"""A loader for mocking."""
def __init__(self, finder):
# type: (MockFinder) -> None
super().__init__()
self.finder = finder
def create_module(self, spec):
# type: (ModuleSpec) -> ModuleType
logger.debug('[autodoc] adding a mock module as %s!', spec.name)
self.finder.mocked_modules.append(spec.name)
return _MockModule(spec.name)
def exec_module(self, module):
# type: (ModuleType) -> None
pass # nothing to do
class MockFinder(MetaPathFinder):
"""A finder for mocking."""
def __init__(self, modnames):
# type: (List[str]) -> None
super().__init__()
self.modnames = modnames
self.loader = MockLoader(self)
self.mocked_modules = [] # type: List[str]
def find_spec(self, fullname, path, target=None):
# type: (str, Sequence[Union[bytes, str]], ModuleType) -> ModuleSpec
for modname in self.modnames:
# check if fullname is (or is a descendant of) one of our targets
if modname == fullname or fullname.startswith(modname + '.'):
return ModuleSpec(fullname, self.loader)
return None
def invalidate_caches(self):
# type: () -> None
"""Invalidate mocked modules on sys.modules."""
for modname in self.mocked_modules:
sys.modules.pop(modname, None)
@contextlib.contextmanager
def mock(modnames):
# type: (List[str]) -> Generator[None, None, None]
"""Insert mock modules during context::
with mock(['target.module.name']):
# mock modules are enabled here
...
"""
try:
finder = MockFinder(modnames)
sys.meta_path.insert(0, finder)
yield
finally:
sys.meta_path.remove(finder)
finder.invalidate_caches()

View File

@@ -30,11 +30,23 @@ if False:
from sphinx.application import Sphinx # NOQA
def get_node_depth(node):
i = 0
cur_node = node
while cur_node.parent != node.document:
cur_node = cur_node.parent
i += 1
return i
def register_sections_as_label(app, document):
# type: (Sphinx, nodes.Node) -> None
labels = app.env.domaindata['std']['labels']
anonlabels = app.env.domaindata['std']['anonlabels']
for node in document.traverse(nodes.section):
if (app.config.autosectionlabel_maxdepth and
get_node_depth(node) >= app.config.autosectionlabel_maxdepth):
continue
labelid = node['ids'][0]
docname = app.env.docname
title = cast(nodes.title, node[0])
@@ -57,6 +69,7 @@ def register_sections_as_label(app, document):
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.add_config_value('autosectionlabel_prefix_document', False, 'env')
app.add_config_value('autosectionlabel_maxdepth', None, 'env')
app.connect('doctree-read', register_sections_as_label)
return {

View File

@@ -73,11 +73,12 @@ from sphinx.environment.adapters.toctree import TocTree
from sphinx.ext.autodoc import get_documenters
from sphinx.ext.autodoc.directive import DocumenterBridge, Options
from sphinx.ext.autodoc.importer import import_module
from sphinx.ext.autodoc.mock import mock
from sphinx.locale import __
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import import_object, rst, logging
from sphinx.util.docutils import (
NullReporter, SphinxDirective, new_document, switch_source_input
NullReporter, SphinxDirective, SphinxRole, new_document, switch_source_input
)
from sphinx.util.matching import Matcher
@@ -286,7 +287,8 @@ class Autosummary(SphinxDirective):
display_name = name.split('.')[-1]
try:
real_name, obj, parent, modname = import_by_name(name, prefixes=prefixes)
with mock(self.config.autosummary_mock_imports):
real_name, obj, parent, modname = import_by_name(name, prefixes=prefixes)
except ImportError:
logger.warning(__('failed to import %s'), name)
items.append((name, '', '', name))
@@ -641,6 +643,7 @@ def autolink_role(typ, rawtext, etext, lineno, inliner, options={}, content=[]):
Expands to ':obj:`text`' if `text` is an object that can be imported;
otherwise expands to '*text*'.
"""
warnings.warn('autolink_role() is deprecated.', RemovedInSphinx40Warning)
env = inliner.document.settings.env
pyobj_role = env.get_domain('py').role('obj')
objects, msg = pyobj_role('obj', rawtext, etext, lineno, inliner, options, content)
@@ -659,6 +662,34 @@ def autolink_role(typ, rawtext, etext, lineno, inliner, options={}, content=[]):
return objects, msg
class AutoLink(SphinxRole):
"""Smart linking role.
Expands to ':obj:`text`' if `text` is an object that can be imported;
otherwise expands to '*text*'.
"""
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
pyobj_role = self.env.get_domain('py').role('obj')
objects, errors = pyobj_role('obj', self.rawtext, self.text, self.lineno,
self.inliner, self.options, self.content)
if errors:
return objects, errors
assert len(objects) == 1
pending_xref = cast(addnodes.pending_xref, objects[0])
try:
# try to import object by name
prefixes = get_import_prefixes_from_env(self.env)
import_by_name(pending_xref['reftarget'], prefixes)
except ImportError:
literal = cast(nodes.literal, pending_xref[0])
objects[0] = nodes.emphasis(self.rawtext, literal.astext(),
classes=literal['classes'])
return objects, errors
def get_rst_suffix(app):
# type: (Sphinx) -> str
def get_supported_format(suffix):
@@ -702,10 +733,11 @@ def process_generate_options(app):
'But your source_suffix does not contain .rst. Skipped.'))
return
generate_autosummary_docs(genfiles, builder=app.builder,
warn=logger.warning, info=logger.info,
suffix=suffix, base_path=app.srcdir,
app=app)
with mock(app.config.autosummary_mock_imports):
generate_autosummary_docs(genfiles, builder=app.builder,
warn=logger.warning, info=logger.info,
suffix=suffix, base_path=app.srcdir,
app=app)
def setup(app):
@@ -725,8 +757,11 @@ def setup(app):
man=(autosummary_noop, autosummary_noop),
texinfo=(autosummary_noop, autosummary_noop))
app.add_directive('autosummary', Autosummary)
app.add_role('autolink', autolink_role)
app.add_role('autolink', AutoLink())
app.connect('doctree-read', process_autosummary_toc)
app.connect('builder-inited', process_generate_options)
app.add_config_value('autosummary_generate', [], True, [bool])
app.add_config_value('autosummary_mock_imports',
lambda config: config.autodoc_mock_imports, 'env')
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@@ -83,16 +83,6 @@ class TestDirective(SphinxDirective):
def run(self):
# type: () -> List[nodes.Node]
if 'skipif' in self.options:
condition = self.options['skipif']
context = {} # type: Dict[str, Any]
if self.config.doctest_global_setup:
exec(self.config.doctest_global_setup, context)
should_skip = eval(condition, context)
if self.config.doctest_global_cleanup:
exec(self.config.doctest_global_cleanup, context)
if should_skip:
return []
# use ordinary docutils nodes for test code: they get special attributes
# so that our builder recognizes them, and the other builders are happy.
code = '\n'.join(self.content)
@@ -160,6 +150,8 @@ class TestDirective(SphinxDirective):
self.state.document.reporter.warning(
__("'%s' is not a valid pyversion option") % spec,
line=self.lineno)
if 'skipif' in self.options:
node['skipif'] = self.options['skipif']
return [node]
@@ -404,6 +396,20 @@ Doctest summary
return node.line - 1
return None
def skipped(self, node):
# type: (nodes.Element) -> bool
if 'skipif' not in node:
return False
else:
condition = node['skipif']
context = {} # type: Dict[str, Any]
if self.config.doctest_global_setup:
exec(self.config.doctest_global_setup, context)
should_skip = eval(condition, context)
if self.config.doctest_global_cleanup:
exec(self.config.doctest_global_cleanup, context)
return should_skip
def test_doc(self, docname, doctree):
# type: (str, nodes.Node) -> None
groups = {} # type: Dict[str, TestGroup]
@@ -429,8 +435,10 @@ Doctest summary
# type: (nodes.Node) -> bool
return isinstance(node, (nodes.literal_block, nodes.comment)) \
and 'testnodetype' in node
for node in doctree.traverse(condition): # type: nodes.Element
if self.skipped(node):
continue
source = node['test'] if 'test' in node else node.astext()
filename = self.get_filename_for_node(node, docname)
line_number = self.get_line_number(node)

View File

@@ -9,6 +9,7 @@
"""
import os
import urllib
import sphinx
@@ -19,14 +20,22 @@ if False:
from sphinx.environment import BuildEnvironment # NOQA
def create_nojekyll(app, env):
def create_nojekyll_and_cname(app, env):
# type: (Sphinx, BuildEnvironment) -> None
if app.builder.format == 'html':
path = os.path.join(app.builder.outdir, '.nojekyll')
open(path, 'wt').close()
open(os.path.join(app.builder.outdir, '.nojekyll'), 'wt').close()
html_baseurl = app.config.html_baseurl
if html_baseurl:
domain = urllib.parse.urlparse(html_baseurl).hostname
if domain and not domain.endswith(".github.io"):
with open(os.path.join(app.builder.outdir, 'CNAME'), 'wt') as f:
# NOTE: don't write a trailing newline. The `CNAME` file that's
# auto-generated by the Github UI doesn't have one.
f.write(domain)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.connect('env-updated', create_nojekyll)
app.connect('env-updated', create_nojekyll_and_cname)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@@ -196,9 +196,7 @@ class GraphvizSimple(SphinxDirective):
node = graphviz()
node['code'] = '%s %s {\n%s\n}\n' % \
(self.name, self.arguments[0], '\n'.join(self.content))
node['options'] = {
'docname': path.splitext(self.state.document.current_source)[0],
}
node['options'] = {'docname': self.env.docname}
if 'graphviz_dot' in self.options:
node['options']['graphviz_dot'] = self.options['graphviz_dot']
if 'alt' in self.options:

View File

@@ -54,9 +54,9 @@ class ImagemagickConverter(ImageConverter):
# type: (str, str) -> bool
"""Converts the image to expected one."""
try:
if _from.lower().endswith('.gif'):
# when target is GIF format, pick the first frame
_from += '[0]'
# append an index 0 to source filename to pick up the first frame
# (or first page) of image (ex. Animation GIF, PDF)
_from += '[0]'
args = ([self.config.image_converter] +
self.config.image_converter_args +

View File

@@ -12,6 +12,7 @@ import posixpath
import re
import shutil
import subprocess
import sys
import tempfile
from hashlib import sha1
from os import path
@@ -26,7 +27,6 @@ from sphinx.util import logging
from sphinx.util.math import get_node_equation_number, wrap_displaymath
from sphinx.util.osutil import ensuredir
from sphinx.util.png import read_png_depth, write_png_depth
from sphinx.util.pycompat import sys_encoding
if False:
# For type annotation
@@ -46,9 +46,9 @@ class MathExtError(SphinxError):
def __init__(self, msg, stderr=None, stdout=None):
# type: (str, bytes, bytes) -> None
if stderr:
msg += '\n[stderr]\n' + stderr.decode(sys_encoding, 'replace')
msg += '\n[stderr]\n' + stderr.decode(sys.getdefaultencoding(), 'replace')
if stdout:
msg += '\n[stdout]\n' + stdout.decode(sys_encoding, 'replace')
msg += '\n[stdout]\n' + stdout.decode(sys.getdefaultencoding(), 'replace')
super().__init__(msg)

View File

@@ -21,7 +21,7 @@ from typing import Any, Union # NOQA
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.transforms import (
ApplySourceWorkaround, ExtraTranslatableNodes, CitationReferences,
DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks, SortIds,
DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks, SortIds, FigureAligner,
AutoNumbering, AutoIndexUpgrader, FilterSystemMessages,
UnreferencedFootnotesDetector, SphinxSmartQuotes, DoctreeReadEvent, ManpageLink
)
@@ -96,7 +96,7 @@ class SphinxStandaloneReader(SphinxBaseReader):
"""
transforms = [ApplySourceWorkaround, ExtraTranslatableNodes, PreserveTranslatableMessages,
Locale, CitationReferences, DefaultSubstitutions, MoveModuleTargets,
HandleCodeBlocks, AutoNumbering, AutoIndexUpgrader, SortIds,
HandleCodeBlocks, AutoNumbering, AutoIndexUpgrader, SortIds, FigureAligner,
RemoveTranslatableInline, FilterSystemMessages, RefOnlyBulletListTransform,
UnreferencedFootnotesDetector, SphinxSmartQuotes, ManpageLink,
SphinxDomains, SubstitutionDefinitionsRemover, DoctreeReadEvent,

View File

@@ -267,7 +267,9 @@ def get_translation(catalog, namespace='general'):
With this code, sphinx searches a message catalog from
``${package_dir}/locales/${language}/LC_MESSAGES/myextension.mo``
The :confval:`language` is used for the searching.
(ex. ``sphinxcontrib.applehelp.mo``). Of course, you can use
arbitrary catalog name instead of ``__name__``. The
:confval:`language` is used for the searching.
.. versionadded:: 1.8
"""

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,15 @@
"""
import re
import warnings
from docutils import nodes, utils
from sphinx import addnodes
from sphinx.errors import SphinxError
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.locale import _
from sphinx.util import ws_re
from sphinx.util.docutils import ReferenceRole, SphinxRole
from sphinx.util.nodes import split_explicit_title, process_index_entry, \
set_role_source_info
@@ -44,7 +46,7 @@ generic_docroles = {
# -- generic cross-reference role ----------------------------------------------
class XRefRole:
class XRefRole(ReferenceRole):
"""
A generic cross-referencing role. To create a callable that can be used as
a role function, create an instance of this class.
@@ -82,8 +84,12 @@ class XRefRole:
if innernodeclass is not None:
self.innernodeclass = innernodeclass
super().__init__()
def _fix_parens(self, env, has_explicit_title, title, target):
# type: (BuildEnvironment, bool, str, str) -> Tuple[str, str]
warnings.warn('XRefRole._fix_parens() is deprecated.',
RemovedInSphinx40Warning, stacklevel=2)
if not has_explicit_title:
if title.endswith('()'):
# remove parentheses
@@ -96,55 +102,70 @@ class XRefRole:
target = target[:-2]
return title, target
def __call__(self, typ, rawtext, text, lineno, inliner,
options={}, content=[]):
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
env = inliner.document.settings.env
if not typ:
typ = env.temp_data.get('default_role')
if not typ:
typ = env.config.default_role
if not typ:
raise SphinxError('cannot determine default role!')
def update_title_and_target(self, title, target):
# type: (str, str) -> Tuple[str, str]
if not self.has_explicit_title:
if title.endswith('()'):
# remove parentheses
title = title[:-2]
if self.config.add_function_parentheses:
# add them back to all occurrences if configured
title += '()'
# remove parentheses from the target too
if target.endswith('()'):
target = target[:-2]
return title, target
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
if ':' not in self.name:
self.refdomain, self.reftype = '', self.name
self.classes = ['xref', self.reftype]
else:
typ = typ.lower()
if ':' not in typ:
domain, role = '', typ
classes = ['xref', role]
self.refdomain, self.reftype = self.name.split(':', 1)
self.classes = ['xref', self.refdomain, '%s-%s' % (self.refdomain, self.reftype)]
if self.text.startswith('!'):
# if the first character is a bang, don't cross-reference at all
return self.create_non_xref_node()
else:
domain, role = typ.split(':', 1)
classes = ['xref', domain, '%s-%s' % (domain, role)]
# if the first character is a bang, don't cross-reference at all
if text[0:1] == '!':
text = utils.unescape(text)[1:]
if self.fix_parens:
text, tgt = self._fix_parens(env, False, text, "")
innernode = self.innernodeclass(rawtext, text, classes=classes)
return self.result_nodes(inliner.document, env, innernode, is_ref=False)
# split title and target in role content
has_explicit_title, title, target = split_explicit_title(text)
title = utils.unescape(title)
target = utils.unescape(target)
# fix-up title and target
return self.create_xref_node()
def create_non_xref_node(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
text = utils.unescape(self.text[1:])
if self.fix_parens:
self.has_explicit_title = False # treat as implicit
text, target = self.update_title_and_target(text, "")
node = self.innernodeclass(self.rawtext, text, classes=self.classes)
return self.result_nodes(self.inliner.document, self.env, node, is_ref=False)
def create_xref_node(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
target = self.target
title = self.title
if self.lowercase:
target = target.lower()
if self.fix_parens:
title, target = self._fix_parens(
env, has_explicit_title, title, target)
title, target = self.update_title_and_target(title, target)
# create the reference node
refnode = self.nodeclass(rawtext, reftype=role, refdomain=domain,
refexplicit=has_explicit_title)
# we may need the line number for warnings
set_role_source_info(inliner, lineno, refnode)
title, target = self.process_link(env, refnode, has_explicit_title, title, target)
# now that the target and title are finally determined, set them
options = {'refdoc': self.env.docname,
'refdomain': self.refdomain,
'reftype': self.reftype,
'refexplicit': self.has_explicit_title,
'refwarn': self.warn_dangling}
refnode = self.nodeclass(self.rawtext, **options)
self.set_source_info(refnode)
# determine the target and title for the class
title, target = self.process_link(self.env, refnode, self.has_explicit_title,
title, target)
refnode['reftarget'] = target
refnode += self.innernodeclass(rawtext, title, classes=classes)
# we also need the source document
refnode['refdoc'] = env.docname
refnode['refwarn'] = self.warn_dangling
# result_nodes allow further modification of return values
return self.result_nodes(inliner.document, env, refnode, is_ref=True)
refnode += self.innernodeclass(self.rawtext, title, classes=self.classes)
return self.result_nodes(self.inliner.document, self.env, refnode, is_ref=True)
# methods that can be overwritten
@@ -179,6 +200,8 @@ class AnyXRefRole(XRefRole):
def indexmarkup_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
"""Role for PEP/RFC references that generate an index entry."""
warnings.warn('indexmarkup_role() is deprecated. Please use PEP or RFC class instead.',
RemovedInSphinx40Warning, stacklevel=2)
env = inliner.document.settings.env
if not typ:
assert env.temp_data['default_role']
@@ -242,11 +265,87 @@ def indexmarkup_role(typ, rawtext, text, lineno, inliner, options={}, content=[]
raise ValueError('unknown role type: %s' % typ)
class PEP(ReferenceRole):
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
target_id = 'index-%s' % self.env.new_serialno('index')
entries = [('single', _('Python Enhancement Proposals; PEP %s') % self.target,
target_id, '', None)]
index = addnodes.index(entries=entries)
target = nodes.target('', '', ids=[target_id])
self.inliner.document.note_explicit_target(target)
try:
refuri = self.build_uri()
reference = nodes.reference('', '', internal=False, refuri=refuri, classes=['pep'])
if self.has_explicit_title:
reference += nodes.strong(self.title, self.title)
else:
title = "PEP " + self.title
reference += nodes.strong(title, title)
except ValueError:
msg = self.inliner.reporter.error('invalid PEP number %s' % self.target,
line=self.lineno)
prb = self.inliner.problematic(self.rawtext, self.rawtext, msg)
return [prb], [msg]
return [index, target, reference], []
def build_uri(self):
# type: () -> str
base_url = self.inliner.document.settings.pep_base_url
ret = self.target.split('#', 1)
if len(ret) == 2:
return base_url + 'pep-%04d#%s' % (int(ret[0]), ret[1])
else:
return base_url + 'pep-%04d' % int(ret[0])
class RFC(ReferenceRole):
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
target_id = 'index-%s' % self.env.new_serialno('index')
entries = [('single', 'RFC; RFC %s' % self.target, target_id, '', None)]
index = addnodes.index(entries=entries)
target = nodes.target('', '', ids=[target_id])
self.inliner.document.note_explicit_target(target)
try:
refuri = self.build_uri()
reference = nodes.reference('', '', internal=False, refuri=refuri, classes=['rfc'])
if self.has_explicit_title:
reference += nodes.strong(self.title, self.title)
else:
title = "RFC " + self.title
reference += nodes.strong(title, title)
except ValueError:
msg = self.inliner.reporter.error('invalid RFC number %s' % self.target,
line=self.lineno)
prb = self.inliner.problematic(self.rawtext, self.rawtext, msg)
return [prb], [msg]
return [index, target, reference], []
def build_uri(self):
# type: () -> str
base_url = self.inliner.document.settings.rfc_base_url
ret = self.target.split('#', 1)
if len(ret) == 2:
return base_url + self.inliner.rfc_url % int(ret[0]) + '#' + ret[1]
else:
return base_url + self.inliner.rfc_url % int(ret[0])
_amp_re = re.compile(r'(?<!&)&(?![&\s])')
def menusel_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
warnings.warn('menusel_role() is deprecated. '
'Please use MenuSelection or GUILabel class instead.',
RemovedInSphinx40Warning, stacklevel=2)
env = inliner.document.settings.env
if not typ:
assert env.temp_data['default_role']
@@ -279,6 +378,32 @@ def menusel_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
return [node], []
class GUILabel(SphinxRole):
amp_re = re.compile(r'(?<!&)&(?![&\s])')
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
node = nodes.inline(rawtext=self.rawtext, classes=[self.name])
spans = self.amp_re.split(self.text)
node += nodes.Text(spans.pop(0))
for span in spans:
span = span.replace('&&', '&')
letter = nodes.Text(span[0])
accelerator = nodes.inline('', '', letter, classes=['accelerator'])
node += accelerator
node += nodes.Text(span[1:])
return [node], []
class MenuSelection(GUILabel):
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
self.text = self.text.replace('-->', '\N{TRIANGULAR BULLET}') # type: ignore
return super().run()
_litvar_re = re.compile('{([^}]+)}')
parens_re = re.compile(r'(\\*{|\\*})')
@@ -286,6 +411,9 @@ parens_re = re.compile(r'(\\*{|\\*})')
def emph_literal_role(typ, rawtext, text, lineno, inliner,
options={}, content=[]):
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
warnings.warn('emph_literal_role() is deprecated. '
'Please use EmphasizedLiteral class instead.',
RemovedInSphinx40Warning, stacklevel=2)
env = inliner.document.settings.env
if not typ:
assert env.temp_data['default_role']
@@ -333,11 +461,65 @@ def emph_literal_role(typ, rawtext, text, lineno, inliner,
return [retnode], []
class EmphasizedLiteral(SphinxRole):
parens_re = re.compile(r'(\\\\|\\{|\\}|{|})')
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
children = self.parse(self.text)
node = nodes.literal(self.rawtext, '', *children,
role=self.name.lower(), classes=[self.name])
return [node], []
def parse(self, text):
# type: (str) -> List[nodes.Node]
result = [] # type: List[nodes.Node]
stack = ['']
for part in self.parens_re.split(text):
if part == '\\\\': # escaped backslash
stack[-1] += '\\'
elif part == '{':
if len(stack) >= 2 and stack[-2] == "{": # nested
stack[-1] += "{"
else:
# start emphasis
stack.append('{')
stack.append('')
elif part == '}':
if len(stack) == 3 and stack[1] == "{" and len(stack[2]) > 0:
# emphasized word found
if stack[0]:
result.append(nodes.Text(stack[0], stack[0]))
result.append(nodes.emphasis(stack[2], stack[2]))
stack = ['']
else:
# emphasized word not found; the rparen is not a special symbol
stack.append('}')
stack = [''.join(stack)]
elif part == '\\{': # escaped left-brace
stack[-1] += '{'
elif part == '\\}': # escaped right-brace
stack[-1] += '}'
else: # others (containing escaped braces)
stack[-1] += part
if ''.join(stack):
# remaining is treated as Text
text = ''.join(stack)
result.append(nodes.Text(text, text))
return result
_abbr_re = re.compile(r'\((.*)\)$', re.S)
def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
warnings.warn('abbr_role() is deprecated. Please use Abbrevation class instead.',
RemovedInSphinx40Warning, stacklevel=2)
text = utils.unescape(text)
m = _abbr_re.search(text)
if m is None:
@@ -349,8 +531,25 @@ def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
return [nodes.abbreviation(abbr, abbr, **options)], []
class Abbreviation(SphinxRole):
abbr_re = re.compile(r'\((.*)\)$', re.S)
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
matched = self.abbr_re.search(self.text)
if matched:
text = self.text[:matched.start()].strip()
self.options['explanation'] = matched.group(1)
else:
text = self.text
return [nodes.abbreviation(self.rawtext, text, **self.options)], []
def index_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
warnings.warn('index_role() is deprecated. Please use Index class instead.',
RemovedInSphinx40Warning, stacklevel=2)
# create new reference target
env = inliner.document.settings.env
targetid = 'index-%s' % env.new_serialno('index')
@@ -378,20 +577,44 @@ def index_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
return [indexnode, targetnode, textnode], []
class Index(ReferenceRole):
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
target_id = 'index-%s' % self.env.new_serialno('index')
if self.has_explicit_title:
# if an explicit target is given, process it as a full entry
title = self.title
entries = process_index_entry(self.target, target_id)
else:
# otherwise we just create a single entry
if self.target.startswith('!'):
title = self.title[1:]
entries = [('single', self.target[1:], target_id, 'main', None)]
else:
title = self.title
entries = [('single', self.target, target_id, '', None)]
index = addnodes.index(entries=entries)
target = nodes.target('', '', ids=[target_id])
text = nodes.Text(title, title)
self.set_source_info(index)
return [index, target, text], []
specific_docroles = {
# links to download references
'download': XRefRole(nodeclass=addnodes.download_reference),
# links to anything
'any': AnyXRefRole(warn_dangling=True),
'pep': indexmarkup_role,
'rfc': indexmarkup_role,
'guilabel': menusel_role,
'menuselection': menusel_role,
'file': emph_literal_role,
'samp': emph_literal_role,
'abbr': abbr_role,
'index': index_role,
'pep': PEP(),
'rfc': RFC(),
'guilabel': GUILabel(),
'menuselection': MenuSelection(),
'file': EmphasizedLiteral(),
'samp': EmphasizedLiteral(),
'abbr': Abbreviation(),
'index': Index(),
} # type: Dict[str, RoleFunction]

View File

@@ -18,8 +18,8 @@ from docutils import nodes
from sphinx import addnodes
from sphinx import package_dir
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.util import jsdump, rpartition
from sphinx.search.jssplitter import splitter_code
from sphinx.util import jsdump, rpartition
if False:
# For type annotation

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{ title|e }}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<meta http-equiv="refresh" content="0;url={{ toc|e }}" />
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,31 @@
{%- macro sitemap(name, docname) -%}
<OBJECT type="text/sitemap">
<PARAM name="Name" value="{{ name|e }}" />
<PARAM name="Local" value="{{ docname|e }}{{ suffix }}" />
</OBJECT>
{%- endmacro -%}
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<HTML>
<HEAD>
<META name="GENERATOR" content="Microsoft&reg; HTML Help Workshop 4.1" />
<!-- Sitemap 1.0 -->
</HEAD>
<BODY>
<OBJECT type="text/site properties">
<PARAM name="Window Styles" value="0x801227" />
<PARAM name="ImageType" value="Folder" />
</OBJECT>
<UL>
<LI>
{{ sitemap(short_title, master_doc)|indent(8) }}
</LI>
{%- for indexname, indexcls, content, collapse in domain_indices %}
<LI>
{{ sitemap(indexcls.localname, indexname)|indent(8) }}
</LI>
{%- endfor %}
{{ body|indent(6) }}
</UL>
</BODY>
</HTML>

View File

@@ -0,0 +1,21 @@
[OPTIONS]
Binary TOC=No
Binary Index=No
Compiled file={{ outname }}.chm
Contents file={{ outname }}.hhc
Default Window={{ outname }}
Default topic={{ master_doc }}
Display compile progress=No
Full text search stop list file={{ outname }}.stp
Full-text search=Yes
Index file={{ outname }}.hhk
Language={{ "%#x"|format(lcid) }}
Title={{ title }}
[WINDOWS]
{{ outname }}="{{ title }}","{{ outname }}.hhc","{{ outname }}.hhk","{{ master_doc }}","{{ master_doc }}",,,,,0x63520,220,0x10384e,[0,0,1024,768],,,,,,,0
[FILES]
{%- for filename in files %}
{{ filename }}
{%- endfor %}

View File

@@ -0,0 +1,33 @@
a
and
are
as
at
be
but
by
for
if
in
into
is
it
near
no
not
of
on
or
such
that
the
their
then
there
these
they
this
to
was
will
with

View File

@@ -30,10 +30,16 @@ project = {{ project | repr }}
copyright = {{ copyright | repr }}
author = {{ author | repr }}
{%- if version %}
# The short X.Y version
version = {{ version | repr }}
{%- endif %}
{%- if release %}
# The full version, including alpha/beta/rc tags
release = {{ release | repr }}
{%- endif %}
# -- General configuration ---------------------------------------------------
@@ -50,17 +56,20 @@ extensions = [
# Add any paths that contain templates here, relative to this directory.
templates_path = ['{{ dot }}templates']
{% if suffix != '.rst' -%}
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = {{ suffix | repr }}
{% if master_doc != 'index' -%}
{% endif -%}
{% if master != 'index' -%}
# The master toctree document.
master_doc = {{ master | repr }}
{% endif -%}
{% if language -%}
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
@@ -68,6 +77,7 @@ master_doc = {{ master | repr }}
# Usually you set "language" from the command line for these cases.
language = {{ language | repr }}
{% endif -%}
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
@@ -81,44 +91,10 @@ exclude_patterns = [{{ exclude_patterns }}]
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['{{ dot }}static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
{%- if extensions %}

View File

@@ -17,15 +17,16 @@ html: $(addsuffix .html,$(ALLDOCS))
pdf: $(addsuffix .pdf,$(ALLDOCS))
install-info: info
for f in *.info; do \\
cp -t $(infodir) "$$f" && \\
$(INSTALL_INFO) --info-dir=$(infodir) "$$f" ; \\
for f in *.info; do \
mkdir -p $(infodir) && \
cp "$$f" $(infodir) && \
$(INSTALL_INFO) --info-dir=$(infodir) "$$f" ; \
done
uninstall-info: info
for f in *.info; do \\
rm -f "$(infodir)/$$f" ; \\
$(INSTALL_INFO) --delete --info-dir=$(infodir) "$$f" ; \\
for f in *.info; do \
rm -f "$(infodir)/$$f" ; \
$(INSTALL_INFO) --delete --info-dir=$(infodir) "$$f" ; \
done
%.info: %.texi

View File

@@ -707,53 +707,79 @@
%% GRAPHICS
%
% \sphinxincludegraphics defined to resize images larger than the line width,
% except if height or width option present.
% \sphinxincludegraphics resizes images larger than the TeX \linewidth (which
% is adjusted in indented environments), or taller than a certain maximal
% height (usually \textheight and this is reduced in the environments which use
% framed.sty to avoid infinite loop if image too tall).
%
% If scale is present, rescale before fitting to line width. (since 1.5)
\newbox\spx@image@box
\newcommand*{\sphinxincludegraphics}[2][]{%
\in@{height}{#1}\ifin@\else\in@{width}{#1}\fi
\ifin@ % height or width present
\includegraphics[#1]{#2}%
\else % no height nor width (but #1 may be "scale=...")
% In case height or width options are present the rescaling is done
% (since 2.0), in a way keeping the width:height ratio either native from
% image or from the width and height options if both were present.
%
\newdimen\spx@image@maxheight
\AtBeginDocument{\spx@image@maxheight\textheight}
% box scratch register
\newdimen\spx@image@box
\newcommand*{\sphinxsafeincludegraphics}[2][]{%
% #1 contains possibly width=, height=, but no scale= since 1.8.4
\setbox\spx@image@box\hbox{\includegraphics[#1,draft]{#2}}%
\in@false % use some handy boolean flag
\ifdim \wd\spx@image@box>\linewidth
\setbox\spx@image@box\box\voidb@x % clear memory
\includegraphics[#1,width=\linewidth]{#2}%
\in@true % flag to remember to adjust options and set box dimensions
% compute height which results from rescaling width to \linewidth
% and keep current aspect ratio. multiply-divide in \numexpr uses
% temporarily doubled precision, hence no overflow. (of course we
% assume \ht is not a few sp's below \maxdimen...(about 16384pt).
\edef\spx@image@rescaledheight % with sp units
{\the\numexpr\ht\spx@image@box
*\linewidth/\wd\spx@image@box sp}%
\ifdim\spx@image@rescaledheight>\spx@image@maxheight
% the rescaled height will be too big, so it is height which decides
% the rescaling factor
\def\spx@image@requiredheight{\spx@image@maxheight}% dimen register
\edef\spx@image@requiredwidth % with sp units
{\the\numexpr\wd\spx@image@box
*\spx@image@maxheight/\ht\spx@image@box sp}%
% TODO: decide if this commented-out block could be needed due to
% rounding in numexpr operations going up
% \ifdim\spx@image@requiredwidth>\linewidth
% \def\spx@image@requiredwidth{\linewidth}% dimen register
% \fi
\else
\def\spx@image@requiredwidth{\linewidth}% dimen register
\let\spx@image@requiredheight\spx@image@rescaledheight% sp units
\fi
\else
% width is ok, let's check height
\ifdim\ht\spx@image@box>\spx@image@maxheight
\in@true
\edef\spx@image@requiredwidth % with sp units
{\the\numexpr\wd\spx@image@box
*\spx@image@maxheight/\ht\spx@image@box sp}%
\def\spx@image@requiredheight{\spx@image@maxheight}% dimen register
\fi
\fi % end of check of width and height
\ifin@
\setbox\spx@image@box
\hbox{\includegraphics
[%#1,% contained only width and/or height and overruled anyhow
width=\spx@image@requiredwidth,height=\spx@image@requiredheight]%
{#2}}%
% \includegraphics does not set box dimensions to the exactly
% requested ones, see https://github.com/latex3/latex2e/issues/112
\wd\spx@image@box\spx@image@requiredwidth
\ht\spx@image@box\spx@image@requiredheight
\leavevmode\box\spx@image@box
\else
% here we do not modify the options, no need to adjust width and height
% on output, they will be computed exactly as with "draft" option
\setbox\spx@image@box\box\voidb@x % clear memory
\includegraphics[#1]{#2}%
\fi
\fi
}
% \sphinxsafeincludegraphics resizes images larger than the line width,
% or taller than about the text height (whether or not height/width options
% were used). This is requested to avoid a crash with \MakeFramed as used by
% sphinxShadowBox (topic/contents) and sphinxheavybox (admonitions), and also
% by sphinxVerbatim (but a priori no image inclusion there).
\newdimen\spx@image@maxheight
% default maximal setting will get reduced by sphinxShadowBox/sphinxheavybox
\AtBeginDocument{\spx@image@maxheight\textheight}
\newcommand*{\sphinxsafeincludegraphics}[2][]{%
\gdef\spx@includegraphics@options{#1}%
\setbox\spx@image@box\hbox{\includegraphics[#1,draft]{#2}}%
\in@false
\ifdim \wd\spx@image@box>\linewidth
\g@addto@macro\spx@includegraphics@options{,width=\linewidth}%
\in@true
\fi
% no rotation, no need to worry about depth
\ifdim \ht\spx@image@box>\spx@image@maxheight
\g@addto@macro\spx@includegraphics@options{,height=\spx@image@maxheight}%
\in@true
\fi
\ifin@
\g@addto@macro\spx@includegraphics@options{,keepaspectratio}%
\fi
\setbox\spx@image@box\box\voidb@x % clear memory
\expandafter\includegraphics\expandafter[\spx@includegraphics@options]{#2}%
}%
% Use the "safe" one by default (2.0)
\def\sphinxincludegraphics{\sphinxsafeincludegraphics}
%% FIGURE IN TABLE
@@ -1374,7 +1400,6 @@
+2\sphinxshadowsep
+\sphinxshadowsize
+\baselineskip\relax
\let\sphinxincludegraphics\sphinxsafeincludegraphics
% configure framed.sty not to add extra vertical spacing
\ltx@ifundefined{OuterFrameSep}{}{\OuterFrameSep\z@skip}%
% the \trivlist will add the vertical spacing on top and bottom which is
@@ -1461,7 +1486,6 @@
-\dimexpr2\FrameRule
+2\FrameSep
+\baselineskip\relax % will happen again if nested, needed indeed!
\let\sphinxincludegraphics\sphinxsafeincludegraphics
% configure framed.sty's parameters to obtain same vertical spacing
% as for "light" boxes. We need for this to manually insert parskip glue and
% revert a skip done by framed before the frame.

View File

@@ -46,7 +46,7 @@
{\Large
\begin{tabular}[t]{c}
\@author
\end{tabular}}\par
\end{tabular}\kern-\tabcolsep}\par
\vspace{25pt}
\@date \par
\py@authoraddress \par

View File

@@ -55,7 +55,7 @@
{\LARGE
\begin{tabular}[t]{c}
\@author
\end{tabular}
\end{tabular}\kern-\tabcolsep
\par}
\vfill\vfill
{\large

View File

@@ -14,12 +14,7 @@
<script type="text/javascript" src="{{ pathto('_static/searchtools.js', 1) }}"></script>
{%- endblock %}
{% block extrahead %}
<script type="text/javascript">
jQuery(function() { Search.loadIndex("{{ pathto('searchindex.js', 1) }}"); });
</script>
{# this is used when loading the search index using $.ajax fails,
such as on Chrome for documents on localhost #}
<script type="text/javascript" id="searchindexloader"></script>
<script type="text/javascript" src="{{ pathto('searchindex.js', 1) }}" defer></script>
{{ super() }}
{% endblock %}
{% block body %}

View File

@@ -231,6 +231,16 @@ a.headerlink {
visibility: hidden;
}
a.brackets:before,
span.brackets > a:before{
content: "[";
}
a.brackets:after,
span.brackets > a:after {
content: "]";
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
@@ -391,6 +401,16 @@ table.citation td {
border-bottom: none;
}
th > p:first-child,
td > p:first-child {
margin-top: 0px;
}
th > p:last-child,
td > p:last-child {
margin-bottom: 0px;
}
/* -- figures --------------------------------------------------------------- */
div.figure {
@@ -460,11 +480,57 @@ ol.upperroman {
list-style: upper-roman;
}
li > p:first-child {
margin-top: 0px;
}
li > p:last-child {
margin-bottom: 0px;
}
dl.footnote > dt,
dl.citation > dt {
float: left;
}
dl.footnote > dd,
dl.citation > dd {
margin-bottom: 0em;
}
dl.footnote > dd:after,
dl.citation > dd:after {
content: "";
clear: both;
}
dl.field-list {
display: flex;
flex-wrap: wrap;
}
dl.field-list > dt {
flex-basis: 20%;
font-weight: bold;
word-break: break-word;
}
dl.field-list > dt:after {
content: ":";
}
dl.field-list > dd {
flex-basis: 70%;
padding-left: 1em;
margin-left: 0em;
margin-bottom: 0em;
}
dl {
margin-bottom: 15px;
}
dd p {
dd > p:first-child {
margin-top: 0px;
}

View File

@@ -75,16 +75,6 @@ var Search = {
}
},
loadIndex : function(url) {
$.ajax({type: "GET", url: url, data: null,
dataType: "script", cache: true,
complete: function(jqxhr, textstatus) {
if (textstatus != "success") {
document.getElementById("searchindexloader").src = url;
}
}});
},
setIndex : function(index) {
var q;
this._index = index;
@@ -130,7 +120,7 @@ var Search = {
this.out = $('#search-results');
this.title = $('<h2>' + _('Searching') + '</h2>').appendTo(this.out);
this.dots = $('<span></span>').appendTo(this.title);
this.status = $('<p style="display: none"></p>').appendTo(this.out);
this.status = $('<p class="search-summary">&nbsp;</p>').appendTo(this.out);
this.output = $('<ul class="search"/>').appendTo(this.out);
$('#search-progress').text(_('Preparing search...'));

View File

@@ -23,7 +23,7 @@ from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util.docutils import new_document
from sphinx.util.i18n import format_date
from sphinx.util.nodes import apply_source_workaround, is_smartquotable
from sphinx.util.nodes import NodeMatcher, apply_source_workaround, is_smartquotable
if False:
# For type annotation
@@ -309,6 +309,19 @@ class UnreferencedFootnotesDetector(SphinxTransform):
location=node)
class FigureAligner(SphinxTransform):
"""
Align figures to center by default.
"""
default_priority = 700
def apply(self, **kwargs):
# type: (Any) -> None
matcher = NodeMatcher(nodes.table, nodes.figure)
for node in self.document.traverse(matcher): # type: nodes.Element
node.setdefault('align', 'center')
class FilterSystemMessages(SphinxTransform):
"""Filter system messages from a doctree."""
default_priority = 999

View File

@@ -21,7 +21,7 @@ from sphinx.domains.std import make_glossary_term, split_term_classifiers
from sphinx.locale import __, init as init_locale
from sphinx.transforms import SphinxTransform
from sphinx.util import split_index_msg, logging
from sphinx.util.i18n import find_catalog
from sphinx.util.i18n import docname_to_domain
from sphinx.util.nodes import (
LITERAL_TYPE_NODES, IMAGE_TYPE_NODES, NodeMatcher,
extract_messages, is_pending_meta, traverse_translatable_index,
@@ -94,7 +94,7 @@ class Locale(SphinxTransform):
assert source.startswith(self.env.srcdir)
docname = path.splitext(relative_path(path.join(self.env.srcdir, 'dummy'),
source))[0]
textdomain = find_catalog(docname, self.config.gettext_compact)
textdomain = docname_to_domain(docname, self.config.gettext_compact)
# fetch translations
dirs = [path.join(self.env.srcdir, directory)

View File

@@ -89,7 +89,7 @@ class HighlightLanguageVisitor(nodes.NodeVisitor):
if 'language' not in node:
node['language'] = setting.language
node['force_highlighting'] = False
else:
elif 'force_highlighting' not in node:
node['force_highlighting'] = True
if 'linenos' not in node:
lines = node.astext().count('\n')

View File

@@ -603,22 +603,26 @@ class PeekableIterator:
def import_object(objname, source=None):
# type: (str, str) -> Any
"""Import python object by qualname."""
try:
module, name = objname.rsplit('.', 1)
except ValueError as err:
raise ExtensionError('Invalid full object name %s' % objname +
(source and ' (needed for %s)' % source or ''),
err)
try:
return getattr(__import__(module, None, None, [name]), name)
except ImportError as err:
raise ExtensionError('Could not import %s' % module +
(source and ' (needed for %s)' % source or ''),
err)
except AttributeError as err:
raise ExtensionError('Could not find %s' % objname +
(source and ' (needed for %s)' % source or ''),
err)
objpath = objname.split('.')
modname = objpath.pop(0)
obj = __import__(modname)
for name in objpath:
modname += '.' + name
try:
obj = getattr(obj, name)
except AttributeError:
__import__(modname)
obj = getattr(obj, name)
return obj
except (AttributeError, ImportError) as exc:
if source:
raise ExtensionError('Could not import %s (needed for %s)' %
(objname, source), exc)
else:
raise ExtensionError('Could not import %s' % objname, exc)
def encode_uri(uri):

View File

@@ -27,6 +27,12 @@ _ansi_re = re.compile('\x1b\\[(\\d\\d;){0,2}\\d\\dm')
codes = {} # type: Dict[str, str]
def terminal_safe(s):
# type: (str) -> str
"""safely encode a string for printing to the terminal."""
return s.encode('ascii', 'backslashreplace').decode('ascii')
def get_terminal_width():
# type: () -> int
"""Borrowed from the py lib."""

View File

@@ -23,10 +23,10 @@ from docutils import nodes
from docutils.io import FileOutput
from docutils.parsers.rst import Directive, directives, roles, convert_directive_function
from docutils.statemachine import StateMachine
from docutils.utils import Reporter
from docutils.utils import Reporter, unescape
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.errors import ExtensionError
from sphinx.errors import ExtensionError, SphinxError
from sphinx.locale import __
from sphinx.util import logging
@@ -36,7 +36,8 @@ report_re = re.compile('^(.+?:(?:\\d+)?): \\((DEBUG|INFO|WARNING|ERROR|SEVERE)/(
if False:
# For type annotation
from types import ModuleType # NOQA
from typing import Any, Callable, Generator, List, Set, Tuple, Type # NOQA
from typing import Any, Callable, Dict, Generator, List, Set, Tuple, Type # NOQA
from docutils.parsers.rst.states import Inliner # NOQA
from docutils.statemachine import State, StringList # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.config import Config # NOQA
@@ -383,6 +384,99 @@ class SphinxDirective(Directive):
return self.env.config
class SphinxRole:
"""A base class for Sphinx roles.
This class provides helper methods for Sphinx roles.
.. note:: The subclasses of this class might not work with docutils.
This class is strongly coupled with Sphinx.
"""
name = None #: The role name actually used in the document.
rawtext = None #: A string containing the entire interpreted text input.
text = None #: The interpreted text content.
lineno = None #: The line number where the interpreted text begins.
inliner = None #: The ``docutils.parsers.rst.states.Inliner`` object.
options = None #: A dictionary of directive options for customization
#: (from the "role" directive).
content = None #: A list of strings, the directive content for customization
#: (from the "role" directive).
def __call__(self, name, rawtext, text, lineno, inliner, options={}, content=[]):
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
self.rawtext = rawtext
self.text = unescape(text)
self.lineno = lineno
self.inliner = inliner
self.options = options
self.content = content
# guess role type
if name:
self.name = name.lower()
else:
self.name = self.env.temp_data.get('default_role')
if not self.name:
self.name = self.env.config.default_role
if not self.name:
raise SphinxError('cannot determine default role!')
return self.run()
def run(self):
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
raise NotImplementedError
@property
def env(self):
# type: () -> BuildEnvironment
"""Reference to the :class:`.BuildEnvironment` object."""
return self.inliner.document.settings.env
@property
def config(self):
# type: () -> Config
"""Reference to the :class:`.Config` object."""
return self.env.config
def set_source_info(self, node, lineno=None):
# type: (nodes.Node, int) -> None
if lineno is None:
lineno = self.lineno
source_info = self.inliner.reporter.get_source_and_line(lineno) # type: ignore
node.source, node.line = source_info
class ReferenceRole(SphinxRole):
"""A base class for reference roles.
The reference roles can accpet ``link title <target>`` style as a text for
the role. The parsed result; link title and target will be stored to
``self.title`` and ``self.target``.
"""
has_explicit_title = None #: A boolean indicates the role has explicit title or not.
title = None #: The link title for the interpreted text.
target = None #: The link target for the interpreted text.
# \x00 means the "<" was backslash-escaped
explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL)
def __call__(self, name, rawtext, text, lineno, inliner, options={}, content=[]):
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA
matched = self.explicit_title_re.match(text)
if matched:
self.has_explicit_title = True
self.title = unescape(matched.group(1))
self.target = unescape(matched.group(2))
else:
self.has_explicit_title = False
self.title = unescape(text)
self.target = unescape(text)
return super().__call__(name, rawtext, text, lineno, inliner, options, content)
class SphinxTranslator(nodes.NodeVisitor):
"""A base class for Sphinx translators.

View File

@@ -19,19 +19,19 @@ import babel.dates
from babel.messages.mofile import write_mo
from babel.messages.pofile import read_po
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning
from sphinx.errors import SphinxError
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.matching import Matcher
from sphinx.util.osutil import SEP, relpath
from sphinx.util.osutil import SEP, canon_path, relpath
logger = logging.getLogger(__name__)
if False:
# For type annotation
from typing import Callable, List, Set # NOQA
from typing import Callable, Generator, List, Set, Tuple # NOQA
from sphinx.environment import BuildEnvironment # NOQA
LocaleFileInfoBase = namedtuple('CatalogInfo', 'base_dir,domain,charset')
@@ -81,8 +81,55 @@ class CatalogInfo(LocaleFileInfoBase):
logger.warning(__('writing error: %s, %s'), self.mo_path, exc)
class CatalogRepository:
"""A repository for message catalogs."""
def __init__(self, basedir, locale_dirs, language, encoding):
# type: (str, List[str], str, str) -> None
self.basedir = basedir
self._locale_dirs = locale_dirs
self.language = language
self.encoding = encoding
@property
def locale_dirs(self):
# type: () -> Generator[str, None, None]
if not self.language:
return
for locale_dir in self._locale_dirs:
locale_dir = path.join(self.basedir, locale_dir)
if path.exists(path.join(locale_dir, self.language, 'LC_MESSAGES')):
yield locale_dir
@property
def pofiles(self):
# type: () -> Generator[Tuple[str, str], None, None]
for locale_dir in self.locale_dirs:
basedir = path.join(locale_dir, self.language, 'LC_MESSAGES')
for root, dirnames, filenames in os.walk(basedir):
# skip dot-directories
for dirname in dirnames:
if dirname.startswith('.'):
dirnames.remove(dirname)
for filename in filenames:
if filename.endswith('.po'):
fullpath = path.join(root, filename)
yield basedir, relpath(fullpath, basedir)
@property
def catalogs(self):
# type: () -> Generator[CatalogInfo, None, None]
for basedir, filename in self.pofiles:
domain = canon_path(path.splitext(filename)[0])
yield CatalogInfo(basedir, domain, self.encoding)
def find_catalog(docname, compaction):
# type: (str, bool) -> str
warnings.warn('find_catalog() is deprecated.',
RemovedInSphinx40Warning, stacklevel=2)
if compaction:
ret = docname.split(SEP, 1)[0]
else:
@@ -91,8 +138,19 @@ def find_catalog(docname, compaction):
return ret
def docname_to_domain(docname, compation):
# type: (str, bool) -> str
"""Convert docname to domain for catalogs."""
if compation:
return docname.split(SEP, 1)[0]
else:
return docname
def find_catalog_files(docname, srcdir, locale_dirs, lang, compaction):
# type: (str, str, List[str], str, bool) -> List[str]
warnings.warn('find_catalog_files() is deprecated.',
RemovedInSphinx40Warning, stacklevel=2)
if not(lang and locale_dirs):
return []
@@ -120,6 +178,8 @@ def find_catalog_source_files(locale_dirs, locale, domains=None, gettext_compact
default is False.
:return: [CatalogInfo(), ...]
"""
warnings.warn('find_catalog_source_files() is deprecated.',
RemovedInSphinx40Warning, stacklevel=2)
if gettext_compact is not None:
warnings.warn('gettext_compact argument for find_catalog_source_files() '
'is deprecated.', RemovedInSphinx30Warning, stacklevel=2)

View File

@@ -20,7 +20,7 @@ from io import StringIO
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.util import logging
from sphinx.util.pycompat import NoneType
from sphinx.util.typing import NoneType
if False:
# For type annotation

View File

@@ -136,11 +136,11 @@ class InventoryFile:
if not m:
continue
name, type, prio, location, dispname = m.groups()
if type == 'py:module' and type in invdata and \
name in invdata[type]: # due to a bug in 1.1 and below,
# two inventory entries are created
# for Python modules, and the first
# one is correct
if type == 'py:module' and type in invdata and name in invdata[type]:
# due to a bug in 1.1 and below,
# two inventory entries are created
# for Python modules, and the first
# one is correct
continue
if location.endswith('$'):
location = location[:-1] + name

View File

@@ -9,13 +9,20 @@
"""
import json
import warnings
from collections import UserString
from sphinx.deprecation import RemovedInSphinx40Warning
if False:
# For type annotation
from typing import Any, IO # NOQA
warnings.warn('sphinx.util.jsonimpl is deprecated',
RemovedInSphinx40Warning, stacklevel=2)
class SphinxJSONEncoder(json.JSONEncoder):
"""JSONEncoder subclass that forces translation proxies."""
def default(self, obj):

View File

@@ -285,6 +285,14 @@ def find_source_node(node):
return None
def get_node_line(node):
# type: (nodes.Element) -> int
for pnode in traverse_parent(node):
if pnode.line:
return pnode.line
return None
def traverse_parent(node, cls=None):
# type: (nodes.Element, Any) -> Iterable[nodes.Element]
while node:
@@ -293,6 +301,15 @@ def traverse_parent(node, cls=None):
node = node.parent
def get_prev_node(node):
# type: (nodes.Node) -> nodes.Node
pos = node.parent.index(node)
if pos > 0:
return node.parent[pos - 1]
else:
return None
def traverse_translatable_index(doctree):
# type: (nodes.Element) -> Iterable[Tuple[nodes.Element, List[str]]]
"""Traverse translatable index node from a document tree."""

View File

@@ -17,6 +17,8 @@ import warnings
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.console import terminal_safe
from sphinx.util.typing import NoneType
if False:
# For type annotation
@@ -26,22 +28,9 @@ if False:
logger = logging.getLogger(__name__)
NoneType = type(None)
# ------------------------------------------------------------------------------
# Python 2/3 compatibility
# sys_encoding: some kind of default system encoding; should be used with
# a lenient error handler
sys_encoding = sys.getdefaultencoding()
# terminal_safe(): safely encode a string for printing to the terminal
def terminal_safe(s):
# type: (str) -> str
return s.encode('ascii', 'backslashreplace').decode('ascii')
# convert_with_2to3():
# support for running 2to3 over config files
def convert_with_2to3(filepath):
@@ -99,9 +88,12 @@ def execfile_(filepath, _globals, open=open):
deprecated_alias('sphinx.util.pycompat',
{
'NoneType': NoneType, # type: ignore
'TextIOWrapper': io.TextIOWrapper,
'htmlescape': html.escape,
'indent': textwrap.indent,
'terminal_safe': terminal_safe,
'sys_encoding': sys.getdefaultencoding(),
'u': '',
},
RemovedInSphinx40Warning)

View File

@@ -20,6 +20,9 @@ DirectiveOption = Callable[[str], Any]
# Text like nodes which are initialized with text and rawsource
TextlikeNode = Union[nodes.Text, nodes.TextElement]
# type of None
NoneType = type(None)
# common role functions
RoleFunction = Callable[[str, str, str, int, Inliner, Dict, List[str]],
Tuple[List[nodes.Node], List[nodes.system_message]]]

View File

@@ -509,9 +509,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator):
if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
self.add_permalink_ref(node.parent, _('Permalink to this code'))
elif isinstance(node.parent, nodes.figure):
image_nodes = node.parent.traverse(nodes.image)
target_node = image_nodes and image_nodes[0] or node.parent
self.add_permalink_ref(target_node, _('Permalink to this image'))
self.add_permalink_ref(node.parent, _('Permalink to this image'))
elif node.parent.get('toctree'):
self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree'))

View File

@@ -455,9 +455,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
self.add_permalink_ref(node.parent, _('Permalink to this code'))
elif isinstance(node.parent, nodes.figure):
image_nodes = node.parent.traverse(nodes.image)
target_node = image_nodes and image_nodes[0] or node.parent
self.add_permalink_ref(target_node, _('Permalink to this image'))
self.add_permalink_ref(node.parent, _('Permalink to this image'))
elif node.parent.get('toctree'):
self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree'))

View File

@@ -30,7 +30,7 @@ from sphinx.errors import SphinxError
from sphinx.locale import admonitionlabels, _, __
from sphinx.util import split_into, logging
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.nodes import clean_astext
from sphinx.util.nodes import clean_astext, get_prev_node
from sphinx.util.template import LaTeXRenderer
from sphinx.util.texescape import tex_escape_map, tex_replace_map
@@ -1533,7 +1533,11 @@ class LaTeXTranslator(SphinxTranslator):
# in reverse order
post = [] # type: List[str]
include_graphics_options = []
is_inline = self.is_inline(node)
has_hyperlink = isinstance(node.parent, nodes.reference)
if has_hyperlink:
is_inline = self.is_inline(node.parent)
else:
is_inline = self.is_inline(node)
if 'width' in attrs:
if 'scale' in attrs:
w = self.latex_image_length(attrs['width'], attrs['scale'])
@@ -1575,7 +1579,7 @@ class LaTeXTranslator(SphinxTranslator):
if self.in_parsed_literal:
pre.append('{\\sphinxunactivateextrasandspace ')
post.append('}')
if not is_inline:
if not is_inline and not has_hyperlink:
pre.append('\n\\noindent')
post.append('\n')
pre.reverse()
@@ -1755,7 +1759,12 @@ class LaTeXTranslator(SphinxTranslator):
if 'refuri' in node:
return
if node.get('refid'):
add_target(node['refid'])
prev_node = get_prev_node(node)
if isinstance(prev_node, nodes.reference) and node['refid'] == prev_node['refid']:
# a target for a hyperlink reference having alias
pass
else:
add_target(node['refid'])
for id in node['ids']:
add_target(id)
@@ -1858,6 +1867,8 @@ class LaTeXTranslator(SphinxTranslator):
for id in node.get('ids'):
anchor = not self.in_caption
self.body += self.hypertarget(id, anchor=anchor)
if not self.is_inline(node):
self.body.append('\n')
uri = node.get('refuri', '')
if not uri and node.get('refid'):
uri = '%' + self.curfilestack[-1] + '#' + node['refid']
@@ -1911,6 +1922,8 @@ class LaTeXTranslator(SphinxTranslator):
def depart_reference(self, node):
# type: (nodes.Element) -> None
self.body.append(self.context.pop())
if not self.is_inline(node):
self.body.append('\n')
def visit_number_reference(self, node):
# type: (nodes.Element) -> None

View File

@@ -247,7 +247,7 @@ class TexinfoTranslator(SphinxTranslator):
title = self.settings.title # type: str
if not title:
title_node = self.document.next_node(nodes.title)
title = (title and title_node.astext()) or '<untitled>'
title = (title_node and title_node.astext()) or '<untitled>'
elements['title'] = self.escape_id(title) or '<untitled>'
# filename
if not elements['filename']:
@@ -387,9 +387,12 @@ class TexinfoTranslator(SphinxTranslator):
def escape_id(self, s):
# type: (str) -> str
"""Return an escaped string suitable for node names and anchors."""
bad_chars = ',:.()'
bad_chars = ',:()'
for bc in bad_chars:
s = s.replace(bc, ' ')
if re.search('[^ .]', s):
# remove DOTs if name contains other characters
s = s.replace('.', ' ')
s = ' '.join(s.split()).strip()
return self.escape(s)

View File

@@ -7,6 +7,7 @@
"""
import os
import shutil
import docutils
import pytest
@@ -23,13 +24,32 @@ collect_ignore = ['roots']
@pytest.fixture(scope='session')
def rootdir():
return path(os.path.dirname(__file__)).abspath() / 'roots'
return path(__file__).parent.abspath() / 'roots'
def pytest_report_header(config):
return ("libraries: Sphinx-%s, docutils-%s" %
(sphinx.__display_version__, docutils.__version__))
header = ("libraries: Sphinx-%s, docutils-%s" %
(sphinx.__display_version__, docutils.__version__))
if hasattr(config, '_tmp_path_factory'):
header += "\nbase tempdir: %s" % config._tmp_path_factory.getbasetemp()
return header
def pytest_assertrepr_compare(op, left, right):
comparer.pytest_assertrepr_compare(op, left, right)
def _initialize_test_directory(session):
if 'SPHINX_TEST_TEMPDIR' in os.environ:
tempdir = os.path.abspath(os.getenv('SPHINX_TEST_TEMPDIR'))
print('Temporary files will be placed in %s.' % tempdir)
if os.path.exists(tempdir):
shutil.rmtree(tempdir)
os.makedirs(tempdir)
def pytest_sessionstart(session):
_initialize_test_directory(session)

View File

@@ -1 +0,0 @@
project = 'test'

Some files were not shown because too many files have changed in this diff Show More