Merge branch '2.0' into 5592_cmdoption_registers_multiple_indices

This commit is contained in:
Takeshi KOMIYA 2019-07-07 16:40:17 +09:00 committed by GitHub
commit fde3d2a1ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 1886 additions and 2447 deletions

32
CHANGES
View File

@ -7,6 +7,8 @@ Dependencies
Incompatible changes Incompatible changes
-------------------- --------------------
* apidoc: template files are renamed to ``.rst_t``
Deprecated Deprecated
---------- ----------
@ -25,22 +27,37 @@ Features added
-------------- --------------
* #5124: graphviz: ``:graphviz_dot:`` option is renamed to ``:layout:`` * #5124: graphviz: ``:graphviz_dot:`` option is renamed to ``:layout:``
* #1464: html: emit a warning if :confval:`html_static_path` and
:confval:`html_extra_path` directories are inside output directory
* #6514: html: Add a label to search input for accessability purposes
* #5602: apidoc: Add ``--templatedir`` option
* #6475: Add ``override`` argument to ``app.add_autodocumenter()``
* #6533: LaTeX: refactor visit_enumerated_list() to use ``\sphinxsetlistlabels``
Bugs fixed Bugs fixed
---------- ----------
* py domain: duplicated warning does not point the location of source code * py domain: duplicated warning does not point the location of source code
* #5592: std domain: :rst:dir:`option` directive registers an index entry for * #6499: html: Sphinx never updates a copy of :confval:`html_logo` even if
each comma separated option original file has changed
* #1125: html theme: scrollbar is hard to see on classic theme and macOS * #1125: html theme: scrollbar is hard to see on classic theme and macOS
* #5502: linkcheck: Consider HTTP 503 response as not an error * #5502: linkcheck: Consider HTTP 503 response as not an error
* #6439: Make generated download links reproducible * #6439: Make generated download links reproducible
* #6486: UnboundLocalError is raised if broken extension installed * #6486: UnboundLocalError is raised if broken extension installed
* #6498: autosummary: crashed with wrong autosummary_generate setting
* #6507: autosummary: crashes without no autosummary_generate setting
* #6511: LaTeX: autonumbered list can not be customized in LaTeX
since Sphinx 1.8.0 (refs: #6533)
* #6531: Failed to load last environment object when extension added
* #736: Invalid sort in pair index
* #6527: :confval:`last_updated` wrongly assumes timezone as UTC
* #5592: std domain: :rst:dir:`option` directive registers an index entry for
each comma separated option
Testing Testing
-------- --------
Release 2.1.2 (in development) Release 2.1.3 (in development)
============================== ==============================
Dependencies Dependencies
@ -61,6 +78,15 @@ Bugs fixed
Testing Testing
-------- --------
Release 2.1.2 (released Jun 19, 2019)
=====================================
Bugs fixed
----------
* #6497: custom lexers fails highlighting when syntax error
* #6478, #6488: info field lists are incorrectly recognized
Release 2.1.1 (released Jun 10, 2019) Release 2.1.1 (released Jun 10, 2019)
===================================== =====================================

View File

@ -126,6 +126,30 @@ These options are used when :option:`--full` is specified:
Sets the project release to put in generated files (see :confval:`release`). Sets the project release to put in generated files (see :confval:`release`).
.. rubric:: Project templating
.. versionadded:: 2.2
Project templating options for sphinx-apidoc
.. option:: -t, --templatedir=TEMPLATEDIR
Template directory for template files. You can modify the templates of
sphinx project files generated by apidoc. Following Jinja2 template
files are allowed:
* ``module.rst_t``
* ``package.rst_t``
* ``toc.rst_t``
* ``master_doc.rst_t``
* ``conf.py_t``
* ``Makefile_t``
* ``Makefile.new_t``
* ``make.bat_t``
* ``make.bat.new_t``
In detail, please refer the system template files Sphinx provides.
(``sphinx/templates/apidoc`` and ``sphinx/templates/quickstart``)
Environment Environment
----------- -----------

View File

@ -243,21 +243,23 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
These work exactly like :rst:dir:`autoclass` etc., These work exactly like :rst:dir:`autoclass` etc.,
but do not offer the options used for automatic member documentation. but do not offer the options used for automatic member documentation.
:rst:dir:`autodata` and :rst:dir:`autoattribute` support :rst:dir:`autodata` and :rst:dir:`autoattribute` support the ``annotation``
the ``annotation`` option. option. The option controls how the value of variable is shown. If specified
Without this option, the representation of the object without arguments, only the name of the variable will be printed, and its value
will be shown in the documentation. is not shown::
When the option is given without arguments,
only the name of the object will be printed::
.. autodata:: CD_DRIVE .. autodata:: CD_DRIVE
:annotation: :annotation:
You can tell sphinx what should be printed after the name:: If the option specified with arguments, it is printed after the name as a value
of the variable::
.. autodata:: CD_DRIVE .. autodata:: CD_DRIVE
:annotation: = your CD device name :annotation: = your CD device name
By default, without ``annotation`` option, Sphinx tries to obtain the value of
the variable and print it after the name.
For module data members and class attributes, documentation can either be put For module data members and class attributes, documentation can either be put
into a comment with special formatting (using a ``#:`` to start the comment into a comment with special formatting (using a ``#:`` to start the comment
instead of just ``#``), or in a docstring *after* the definition. Comments instead of just ``#``), or in a docstring *after* the definition. Comments

View File

@ -47,7 +47,7 @@ extras_require = {
'html5lib', 'html5lib',
'flake8>=3.5.0', 'flake8>=3.5.0',
'flake8-import-order', 'flake8-import-order',
'mypy>=0.590', 'mypy>=0.711',
'docutils-stubs', 'docutils-stubs',
], ],
} }

View File

@ -1054,8 +1054,8 @@ class Sphinx:
else: else:
lexer_classes[alias] = lexer lexer_classes[alias] = lexer
def add_autodocumenter(self, cls): def add_autodocumenter(self, cls, override=False):
# type: (Any) -> None # type: (Any, bool) -> None
"""Register a new documenter class for the autodoc extension. """Register a new documenter class for the autodoc extension.
Add *cls* as a new documenter class for the :mod:`sphinx.ext.autodoc` Add *cls* as a new documenter class for the :mod:`sphinx.ext.autodoc`
@ -1067,11 +1067,13 @@ class Sphinx:
.. todo:: Add real docs for Documenter and subclassing .. todo:: Add real docs for Documenter and subclassing
.. versionadded:: 0.6 .. versionadded:: 0.6
.. versionchanged:: 2.2
Add *override* keyword.
""" """
logger.debug('[app] adding autodocumenter: %r', cls) logger.debug('[app] adding autodocumenter: %r', cls)
from sphinx.ext.autodoc.directive import AutodocDirective from sphinx.ext.autodoc.directive import AutodocDirective
self.registry.add_documenter(cls.objtype, cls) self.registry.add_documenter(cls.objtype, cls)
self.add_directive('auto' + cls.objtype, AutodocDirective) self.add_directive('auto' + cls.objtype, AutodocDirective, override=override)
def add_autodoc_attrgetter(self, typ, getter): def add_autodoc_attrgetter(self, typ, getter):
# type: (Type, Callable[[Any, str, Any], Any]) -> None # type: (Type, Callable[[Any, str, Any], Any]) -> None

View File

@ -38,8 +38,7 @@ from sphinx.highlighting import PygmentsBridge
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.search import js_index from sphinx.search import js_index
from sphinx.theming import HTMLThemeFactory from sphinx.theming import HTMLThemeFactory
from sphinx.util import logging, status_iterator from sphinx.util import logging, progress_message, status_iterator
from sphinx.util.console import bold # type: ignore
from sphinx.util.docutils import is_html5_writer_available, new_document from sphinx.util.docutils import is_html5_writer_available, new_document
from sphinx.util.fileutil import copy_asset from sphinx.util.fileutil import copy_asset
from sphinx.util.i18n import format_date from sphinx.util.i18n import format_date
@ -626,6 +625,7 @@ class StandaloneHTMLBuilder(Builder):
def finish(self) -> None: def finish(self) -> None:
self.finish_tasks.add_task(self.gen_indices) self.finish_tasks.add_task(self.gen_indices)
self.finish_tasks.add_task(self.gen_pages_from_extensions)
self.finish_tasks.add_task(self.gen_additional_pages) self.finish_tasks.add_task(self.gen_additional_pages)
self.finish_tasks.add_task(self.copy_image_files) self.finish_tasks.add_task(self.copy_image_files)
self.finish_tasks.add_task(self.copy_download_files) self.finish_tasks.add_task(self.copy_download_files)
@ -636,9 +636,8 @@ class StandaloneHTMLBuilder(Builder):
# dump the search index # dump the search index
self.handle_finish() self.handle_finish()
@progress_message(__('generating indices'))
def gen_indices(self) -> None: def gen_indices(self) -> None:
logger.info(bold(__('generating indices...')), nonl=True)
# the global general index # the global general index
if self.use_index: if self.use_index:
self.write_genindex() self.write_genindex()
@ -646,16 +645,14 @@ class StandaloneHTMLBuilder(Builder):
# the global domain-specific indices # the global domain-specific indices
self.write_domain_indices() self.write_domain_indices()
logger.info('') def gen_pages_from_extensions(self) -> None:
def gen_additional_pages(self) -> None:
# pages from extensions # pages from extensions
for pagelist in self.events.emit('html-collect-pages'): for pagelist in self.events.emit('html-collect-pages'):
for pagename, context, template in pagelist: for pagename, context, template in pagelist:
self.handle_page(pagename, context, template) self.handle_page(pagename, context, template)
logger.info(bold(__('writing additional pages...')), nonl=True) @progress_message(__('writing additional pages'))
def gen_additional_pages(self) -> None:
# additional pages from conf.py # additional pages from conf.py
for pagename, template in self.config.html_additional_pages.items(): for pagename, template in self.config.html_additional_pages.items():
logger.info(' ' + pagename, nonl=True) logger.info(' ' + pagename, nonl=True)
@ -672,8 +669,6 @@ class StandaloneHTMLBuilder(Builder):
fn = path.join(self.outdir, '_static', 'opensearch.xml') fn = path.join(self.outdir, '_static', 'opensearch.xml')
self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn)
logger.info('')
def write_genindex(self) -> None: def write_genindex(self) -> None:
# the total count of lines for each index letter, used to distribute # the total count of lines for each index letter, used to distribute
# the entries into two columns # the entries into two columns
@ -746,84 +741,77 @@ class StandaloneHTMLBuilder(Builder):
logger.warning(__('cannot copy downloadable file %r: %s'), logger.warning(__('cannot copy downloadable file %r: %s'),
path.join(self.srcdir, src), err) path.join(self.srcdir, src), err)
def copy_static_files(self) -> None: def create_pygments_style_file(self) -> None:
try: """create a style file for pygments."""
# copy static files
logger.info(bold(__('copying static files... ')), nonl=True)
ensuredir(path.join(self.outdir, '_static'))
# first, create pygments style file
with open(path.join(self.outdir, '_static', 'pygments.css'), 'w') as f: with open(path.join(self.outdir, '_static', 'pygments.css'), 'w') as f:
f.write(self.highlighter.get_stylesheet()) f.write(self.highlighter.get_stylesheet())
# then, copy translations JavaScript file
def copy_translation_js(self) -> None:
"""Copy a JavaScript file for translations."""
if self.config.language is not None: if self.config.language is not None:
jsfile = self._get_translations_js() jsfile = self._get_translations_js()
if jsfile: if jsfile:
copyfile(jsfile, path.join(self.outdir, '_static', copyfile(jsfile, path.join(self.outdir, '_static', 'translations.js'))
'translations.js'))
# copy non-minified stemmer JavaScript file def copy_stemmer_js(self) -> None:
"""Copy a JavaScript file for stemmer."""
if self.indexer is not None: if self.indexer is not None:
jsfile = self.indexer.get_js_stemmer_rawcode() jsfile = self.indexer.get_js_stemmer_rawcode()
if jsfile: if jsfile:
copyfile(jsfile, path.join(self.outdir, '_static', '_stemmer.js')) copyfile(jsfile, path.join(self.outdir, '_static', '_stemmer.js'))
ctx = self.globalcontext.copy() def copy_theme_static_files(self, context: Dict) -> None:
# add context items for search function used in searchtools.js_t
if self.indexer is not None:
ctx.update(self.indexer.context_for_searchtool())
# then, copy over theme-supplied static files
if self.theme: if self.theme:
for theme_path in self.theme.get_theme_dirs()[::-1]: for entry in self.theme.get_theme_dirs()[::-1]:
entry = path.join(theme_path, 'static') copy_asset(path.join(entry, 'static'),
copy_asset(entry, path.join(self.outdir, '_static'), excluded=DOTFILES, path.join(self.outdir, '_static'),
context=ctx, renderer=self.templates) excluded=DOTFILES, context=context, renderer=self.templates)
# then, copy over all user-supplied static files
def copy_html_static_files(self, context: Dict) -> None:
excluded = Matcher(self.config.exclude_patterns + ["**/.*"]) excluded = Matcher(self.config.exclude_patterns + ["**/.*"])
for static_path in self.config.html_static_path: for entry in self.config.html_static_path:
entry = path.join(self.confdir, static_path) copy_asset(path.join(self.confdir, entry),
if not path.exists(entry): path.join(self.outdir, '_static'),
logger.warning(__('html_static_path entry %r does not exist'), entry) excluded, context=context, renderer=self.templates)
continue
copy_asset(entry, path.join(self.outdir, '_static'), excluded, def copy_html_logo(self) -> None:
context=ctx, renderer=self.templates)
# copy logo and favicon files if not already in static path
if self.config.html_logo: if self.config.html_logo:
logobase = path.basename(self.config.html_logo) copy_asset(path.join(self.confdir, self.config.html_logo),
logotarget = path.join(self.outdir, '_static', logobase) path.join(self.outdir, '_static'))
if not path.isfile(path.join(self.confdir, self.config.html_logo)):
logger.warning(__('logo file %r does not exist'), self.config.html_logo) def copy_html_favicon(self) -> None:
elif not path.isfile(logotarget):
copyfile(path.join(self.confdir, self.config.html_logo),
logotarget)
if self.config.html_favicon: if self.config.html_favicon:
iconbase = path.basename(self.config.html_favicon) copy_asset(path.join(self.confdir, self.config.html_favicon),
icontarget = path.join(self.outdir, '_static', iconbase) path.join(self.outdir, '_static'))
if not path.isfile(path.join(self.confdir, self.config.html_favicon)):
logger.warning(__('favicon file %r does not exist'), def copy_static_files(self) -> None:
self.config.html_favicon) try:
elif not path.isfile(icontarget): with progress_message(__('copying static files... ')):
copyfile(path.join(self.confdir, self.config.html_favicon), ensuredir(path.join(self.outdir, '_static'))
icontarget)
logger.info(__('done')) # prepare context for templates
context = self.globalcontext.copy()
if self.indexer is not None:
context.update(self.indexer.context_for_searchtool())
self.create_pygments_style_file()
self.copy_translation_js()
self.copy_stemmer_js()
self.copy_theme_static_files(context)
self.copy_html_static_files(context)
self.copy_html_logo()
self.copy_html_favicon()
except OSError as err: except OSError as err:
logger.warning(__('cannot copy static file %r'), err) logger.warning(__('cannot copy static file %r'), err)
def copy_extra_files(self) -> None: def copy_extra_files(self) -> None:
"""copy html_extra_path files."""
try: try:
# copy html_extra_path files with progress_message(__('copying extra files')):
logger.info(bold(__('copying extra files... ')), nonl=True)
excluded = Matcher(self.config.exclude_patterns) excluded = Matcher(self.config.exclude_patterns)
for extra_path in self.config.html_extra_path: for extra_path in self.config.html_extra_path:
entry = path.join(self.confdir, extra_path) entry = path.join(self.confdir, extra_path)
if not path.exists(entry):
logger.warning(__('html_extra_path entry %r does not exist'), entry)
continue
copy_asset(entry, self.outdir, excluded) copy_asset(entry, self.outdir, excluded)
logger.info(__('done'))
except OSError as err: except OSError as err:
logger.warning(__('cannot copy extra file %r'), err) logger.warning(__('cannot copy extra file %r'), err)
@ -1067,15 +1055,12 @@ class StandaloneHTMLBuilder(Builder):
self.finish_tasks.add_task(self.dump_search_index) self.finish_tasks.add_task(self.dump_search_index)
self.finish_tasks.add_task(self.dump_inventory) self.finish_tasks.add_task(self.dump_inventory)
@progress_message(__('dumping object inventory'))
def dump_inventory(self) -> None: def dump_inventory(self) -> None:
logger.info(bold(__('dumping object inventory... ')), nonl=True)
InventoryFile.dump(path.join(self.outdir, INVENTORY_FILENAME), self.env, self) InventoryFile.dump(path.join(self.outdir, INVENTORY_FILENAME), self.env, self)
logger.info(__('done'))
def dump_search_index(self) -> None: def dump_search_index(self) -> None:
logger.info( with progress_message(__('dumping search index in %s') % self.indexer.label()):
bold(__('dumping search index in %s ... ') % self.indexer.label()),
nonl=True)
self.indexer.prune(self.env.all_docs) self.indexer.prune(self.env.all_docs)
searchindexfn = path.join(self.outdir, self.searchindex_filename) searchindexfn = path.join(self.outdir, self.searchindex_filename)
# first write to a temporary file, so that if dumping fails, # first write to a temporary file, so that if dumping fails,
@ -1087,7 +1072,6 @@ class StandaloneHTMLBuilder(Builder):
with open(searchindexfn + '.tmp', 'wb') as fb: with open(searchindexfn + '.tmp', 'wb') as fb:
self.indexer.dump(fb, self.indexer_format) self.indexer.dump(fb, self.indexer_format)
movefile(searchindexfn + '.tmp', searchindexfn) movefile(searchindexfn + '.tmp', searchindexfn)
logger.info(__('done'))
def convert_html_css_files(app: Sphinx, config: Config) -> None: def convert_html_css_files(app: Sphinx, config: Config) -> None:
@ -1166,6 +1150,44 @@ def validate_math_renderer(app: Sphinx) -> None:
raise ConfigError(__('Unknown math_renderer %r is given.') % name) raise ConfigError(__('Unknown math_renderer %r is given.') % name)
def validate_html_extra_path(app: Sphinx, config: Config) -> None:
"""Check html_extra_paths setting."""
for entry in config.html_extra_path[:]:
extra_path = path.normpath(path.join(app.confdir, entry))
if not path.exists(extra_path):
logger.warning(__('html_extra_path entry %r does not exist'), entry)
config.html_extra_path.remove(entry)
elif path.commonpath([app.outdir, extra_path]) == app.outdir:
logger.warning(__('html_extra_path entry %r is placed inside outdir'), entry)
config.html_extra_path.remove(entry)
def validate_html_static_path(app: Sphinx, config: Config) -> None:
"""Check html_static_paths setting."""
for entry in config.html_static_path[:]:
static_path = path.normpath(path.join(app.confdir, entry))
if not path.exists(static_path):
logger.warning(__('html_static_path entry %r does not exist'), entry)
config.html_static_path.remove(entry)
elif path.commonpath([app.outdir, static_path]) == app.outdir:
logger.warning(__('html_static_path entry %r is placed inside outdir'), entry)
config.html_static_path.remove(entry)
def validate_html_logo(app: Sphinx, config: Config) -> None:
"""Check html_logo setting."""
if config.html_logo and not path.isfile(path.join(app.confdir, config.html_logo)):
logger.warning(__('logo file %r does not exist'), config.html_logo)
config.html_logo = None # type: ignore
def validate_html_favicon(app: Sphinx, config: Config) -> None:
"""Check html_favicon setting."""
if config.html_favicon and not path.isfile(path.join(app.confdir, config.html_favicon)):
logger.warning(__('favicon file %r does not exist'), config.html_favicon)
config.html_favicon = None # type: ignore
# for compatibility # for compatibility
import sphinx.builders.dirhtml # NOQA import sphinx.builders.dirhtml # NOQA
import sphinx.builders.singlehtml # NOQA import sphinx.builders.singlehtml # NOQA
@ -1221,6 +1243,10 @@ def setup(app: Sphinx) -> Dict[str, Any]:
# event handlers # event handlers
app.connect('config-inited', convert_html_css_files) app.connect('config-inited', convert_html_css_files)
app.connect('config-inited', convert_html_js_files) app.connect('config-inited', convert_html_js_files)
app.connect('config-inited', validate_html_extra_path)
app.connect('config-inited', validate_html_static_path)
app.connect('config-inited', validate_html_logo)
app.connect('config-inited', validate_html_favicon)
app.connect('builder-inited', validate_math_renderer) app.connect('builder-inited', validate_math_renderer)
app.connect('html-page-context', setup_js_tag_helper) app.connect('html-page-context', setup_js_tag_helper)

View File

@ -14,6 +14,7 @@ import multiprocessing
import os import os
import sys import sys
import traceback import traceback
from typing import Any, IO, List
from docutils.utils import SystemMessage from docutils.utils import SystemMessage
@ -26,13 +27,8 @@ from sphinx.util import Tee, format_exception_cut_frames, save_traceback
from sphinx.util.console import red, nocolor, color_terminal, terminal_safe # 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.docutils import docutils_namespace, patch_docutils
if False:
# For type annotation
from typing import Any, IO, List, Union # NOQA
def handle_exception(app: Sphinx, args: Any, exception: BaseException, stderr: IO = sys.stderr) -> None: # NOQA
def handle_exception(app, args, exception, stderr=sys.stderr):
# type: (Sphinx, Any, Union[Exception, KeyboardInterrupt], IO) -> None
if args.pdb: if args.pdb:
import pdb import pdb
print(red(__('Exception occurred while building, starting debugger:')), print(red(__('Exception occurred while building, starting debugger:')),
@ -82,8 +78,7 @@ def handle_exception(app, args, exception, stderr=sys.stderr):
file=stderr) file=stderr)
def jobs_argument(value): def jobs_argument(value: str) -> int:
# type: (str) -> int
""" """
Special type to handle 'auto' flags passed to 'sphinx-build' via -j flag. Can Special type to handle 'auto' flags passed to 'sphinx-build' via -j flag. Can
be expanded to handle other special scaling requests, such as setting job count be expanded to handle other special scaling requests, such as setting job count
@ -99,8 +94,7 @@ def jobs_argument(value):
return jobs return jobs
def get_parser(): def get_parser() -> argparse.ArgumentParser:
# type: () -> argparse.ArgumentParser
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
usage='%(prog)s [OPTIONS] SOURCEDIR OUTPUTDIR [FILENAMES...]', usage='%(prog)s [OPTIONS] SOURCEDIR OUTPUTDIR [FILENAMES...]',
epilog=__('For more information, visit <http://sphinx-doc.org/>.'), epilog=__('For more information, visit <http://sphinx-doc.org/>.'),
@ -195,15 +189,13 @@ files can be built by specifying individual filenames.
return parser return parser
def make_main(argv=sys.argv[1:]): def make_main(argv: List[str] = sys.argv[1:]) -> int:
# type: (List[str]) -> int
"""Sphinx build "make mode" entry.""" """Sphinx build "make mode" entry."""
from sphinx.cmd import make_mode from sphinx.cmd import make_mode
return make_mode.run_make_mode(argv[1:]) return make_mode.run_make_mode(argv[1:])
def build_main(argv=sys.argv[1:]): def build_main(argv: List[str] = sys.argv[1:]) -> int:
# type: (List[str]) -> int
"""Sphinx build "main" command-line entry.""" """Sphinx build "main" command-line entry."""
parser = get_parser() parser = get_parser()
@ -288,8 +280,7 @@ def build_main(argv=sys.argv[1:]):
return 2 return 2
def main(argv=sys.argv[1:]): def main(argv: List[str] = sys.argv[1:]) -> int:
# type: (List[str]) -> int
sphinx.locale.setlocale(locale.LC_ALL, '') sphinx.locale.setlocale(locale.LC_ALL, '')
sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx') sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx')

View File

@ -18,16 +18,13 @@ import os
import subprocess import subprocess
import sys import sys
from os import path from os import path
from typing import List
import sphinx import sphinx
from sphinx.cmd.build import build_main from sphinx.cmd.build import build_main
from sphinx.util.console import color_terminal, nocolor, bold, blue # type: ignore from sphinx.util.console import color_terminal, nocolor, bold, blue # type: ignore
from sphinx.util.osutil import cd, rmtree from sphinx.util.osutil import cd, rmtree
if False:
# For type annotation
from typing import List # NOQA
BUILDERS = [ BUILDERS = [
("", "html", "to make standalone HTML files"), ("", "html", "to make standalone HTML files"),
@ -58,20 +55,16 @@ BUILDERS = [
class Make: class Make:
def __init__(self, srcdir: str, builddir: str, opts: List[str]) -> None:
def __init__(self, srcdir, builddir, opts):
# type: (str, str, List[str]) -> None
self.srcdir = srcdir self.srcdir = srcdir
self.builddir = builddir self.builddir = builddir
self.opts = opts self.opts = opts
self.makecmd = os.environ.get('MAKE', 'make') # refer $MAKE to determine make command self.makecmd = os.environ.get('MAKE', 'make') # refer $MAKE to determine make command
def builddir_join(self, *comps): def builddir_join(self, *comps: str) -> str:
# type: (str) -> str
return path.join(self.builddir, *comps) return path.join(self.builddir, *comps)
def build_clean(self): def build_clean(self) -> int:
# type: () -> int
srcdir = path.abspath(self.srcdir) srcdir = path.abspath(self.srcdir)
builddir = path.abspath(self.builddir) builddir = path.abspath(self.builddir)
if not path.exists(self.builddir): if not path.exists(self.builddir):
@ -90,8 +83,7 @@ class Make:
rmtree(self.builddir_join(item)) rmtree(self.builddir_join(item))
return 0 return 0
def build_help(self): def build_help(self) -> None:
# type: () -> None
if not color_terminal(): if not color_terminal():
nocolor() nocolor()
@ -101,8 +93,7 @@ class Make:
if not osname or os.name == osname: if not osname or os.name == osname:
print(' %s %s' % (blue(bname.ljust(10)), description)) print(' %s %s' % (blue(bname.ljust(10)), description))
def build_latexpdf(self): def build_latexpdf(self) -> int:
# type: () -> int
if self.run_generic_build('latex') > 0: if self.run_generic_build('latex') > 0:
return 1 return 1
@ -117,8 +108,7 @@ class Make:
print('Error: Failed to run: %s' % makecmd) print('Error: Failed to run: %s' % makecmd)
return 1 return 1
def build_latexpdfja(self): def build_latexpdfja(self) -> int:
# type: () -> int
if self.run_generic_build('latex') > 0: if self.run_generic_build('latex') > 0:
return 1 return 1
@ -133,8 +123,7 @@ class Make:
print('Error: Failed to run: %s' % makecmd) print('Error: Failed to run: %s' % makecmd)
return 1 return 1
def build_info(self): def build_info(self) -> int:
# type: () -> int
if self.run_generic_build('texinfo') > 0: if self.run_generic_build('texinfo') > 0:
return 1 return 1
try: try:
@ -144,15 +133,13 @@ class Make:
print('Error: Failed to run: %s' % self.makecmd) print('Error: Failed to run: %s' % self.makecmd)
return 1 return 1
def build_gettext(self): def build_gettext(self) -> int:
# type: () -> int
dtdir = self.builddir_join('gettext', '.doctrees') dtdir = self.builddir_join('gettext', '.doctrees')
if self.run_generic_build('gettext', doctreedir=dtdir) > 0: if self.run_generic_build('gettext', doctreedir=dtdir) > 0:
return 1 return 1
return 0 return 0
def run_generic_build(self, builder, doctreedir=None): def run_generic_build(self, builder: str, doctreedir: str = None) -> int:
# type: (str, str) -> int
# compatibility with old Makefile # compatibility with old Makefile
papersize = os.getenv('PAPER', '') papersize = os.getenv('PAPER', '')
opts = self.opts opts = self.opts
@ -168,8 +155,7 @@ class Make:
return build_main(args + opts) return build_main(args + opts)
def run_make_mode(args): def run_make_mode(args: List[str]) -> int:
# type: (List[str]) -> int
if len(args) < 3: if len(args) < 3:
print('Error: at least 3 arguments (builder, source ' print('Error: at least 3 arguments (builder, source '
'dir, build dir) are required.', file=sys.stderr) 'dir, build dir) are required.', file=sys.stderr)

View File

@ -17,6 +17,7 @@ import time
import warnings import warnings
from collections import OrderedDict from collections import OrderedDict
from os import path from os import path
from typing import Any, Callable, Dict, List, Pattern, Union
# try to import readline, unix specific enhancement # try to import readline, unix specific enhancement
try: try:
@ -42,10 +43,6 @@ from sphinx.util.console import ( # type: ignore
from sphinx.util.osutil import ensuredir from sphinx.util.osutil import ensuredir
from sphinx.util.template import SphinxRenderer from sphinx.util.template import SphinxRenderer
if False:
# For type annotation
from typing import Any, Callable, Dict, List, Pattern, Union # NOQA
TERM_ENCODING = getattr(sys.stdin, 'encoding', None) # RemovedInSphinx40Warning TERM_ENCODING = getattr(sys.stdin, 'encoding', None) # RemovedInSphinx40Warning
EXTENSIONS = OrderedDict([ EXTENSIONS = OrderedDict([
@ -84,8 +81,7 @@ else:
# function to get input from terminal -- overridden by the test suite # function to get input from terminal -- overridden by the test suite
def term_input(prompt): def term_input(prompt: str) -> str:
# type: (str) -> str
if sys.platform == 'win32': if sys.platform == 'win32':
# Important: On windows, readline is not enabled by default. In these # Important: On windows, readline is not enabled by default. In these
# environment, escape sequences have been broken. To avoid the # environment, escape sequences have been broken. To avoid the
@ -100,58 +96,49 @@ class ValidationError(Exception):
"""Raised for validation errors.""" """Raised for validation errors."""
def is_path(x): def is_path(x: str) -> str:
# type: (str) -> str
x = path.expanduser(x) x = path.expanduser(x)
if not path.isdir(x): if not path.isdir(x):
raise ValidationError(__("Please enter a valid path name.")) raise ValidationError(__("Please enter a valid path name."))
return x return x
def allow_empty(x): def allow_empty(x: str) -> str:
# type: (str) -> str
return x return x
def nonempty(x): def nonempty(x: str) -> str:
# type: (str) -> str
if not x: if not x:
raise ValidationError(__("Please enter some text.")) raise ValidationError(__("Please enter some text."))
return x return x
def choice(*l): def choice(*l: str) -> Callable[[str], str]:
# type: (str) -> Callable[[str], str] def val(x: str) -> str:
def val(x):
# type: (str) -> str
if x not in l: if x not in l:
raise ValidationError(__('Please enter one of %s.') % ', '.join(l)) raise ValidationError(__('Please enter one of %s.') % ', '.join(l))
return x return x
return val return val
def boolean(x): def boolean(x: str) -> bool:
# type: (str) -> bool
if x.upper() not in ('Y', 'YES', 'N', 'NO'): if x.upper() not in ('Y', 'YES', 'N', 'NO'):
raise ValidationError(__("Please enter either 'y' or 'n'.")) raise ValidationError(__("Please enter either 'y' or 'n'."))
return x.upper() in ('Y', 'YES') return x.upper() in ('Y', 'YES')
def suffix(x): def suffix(x: str) -> str:
# type: (str) -> str
if not (x[0:1] == '.' and len(x) > 1): if not (x[0:1] == '.' and len(x) > 1):
raise ValidationError(__("Please enter a file suffix, " raise ValidationError(__("Please enter a file suffix, "
"e.g. '.rst' or '.txt'.")) "e.g. '.rst' or '.txt'."))
return x return x
def ok(x): def ok(x: str) -> str:
# type: (str) -> str
return x return x
def term_decode(text): def term_decode(text: Union[bytes, str]) -> str:
# type: (Union[bytes,str]) -> str
warnings.warn('term_decode() is deprecated.', warnings.warn('term_decode() is deprecated.',
RemovedInSphinx40Warning, stacklevel=2) RemovedInSphinx40Warning, stacklevel=2)
@ -175,8 +162,7 @@ def term_decode(text):
return text.decode('latin1') return text.decode('latin1')
def do_prompt(text, default=None, validator=nonempty): def do_prompt(text: str, default: str = None, validator: Callable[[str], Any] = nonempty) -> Union[str, bool]: # NOQA
# type: (str, str, Callable[[str], Any]) -> Union[str, bool]
while True: while True:
if default is not None: if default is not None:
prompt = PROMPT_PREFIX + '%s [%s]: ' % (text, default) prompt = PROMPT_PREFIX + '%s [%s]: ' % (text, default)
@ -201,8 +187,7 @@ def do_prompt(text, default=None, validator=nonempty):
return x return x
def convert_python_source(source, rex=re.compile(r"[uU]('.*?')")): def convert_python_source(source: str, rex: Pattern = re.compile(r"[uU]('.*?')")) -> str:
# type: (str, Pattern) -> str
# remove Unicode literal prefixes # remove Unicode literal prefixes
warnings.warn('convert_python_source() is deprecated.', warnings.warn('convert_python_source() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
@ -210,13 +195,11 @@ def convert_python_source(source, rex=re.compile(r"[uU]('.*?')")):
class QuickstartRenderer(SphinxRenderer): class QuickstartRenderer(SphinxRenderer):
def __init__(self, templatedir): def __init__(self, templatedir: str) -> None:
# type: (str) -> None
self.templatedir = templatedir or '' self.templatedir = templatedir or ''
super().__init__() super().__init__()
def render(self, template_name, context): def render(self, template_name: str, context: Dict) -> str:
# type: (str, Dict) -> str
user_template = path.join(self.templatedir, path.basename(template_name)) user_template = path.join(self.templatedir, path.basename(template_name))
if self.templatedir and path.exists(user_template): if self.templatedir and path.exists(user_template):
return self.render_from_file(user_template, context) return self.render_from_file(user_template, context)
@ -224,8 +207,7 @@ class QuickstartRenderer(SphinxRenderer):
return super().render(template_name, context) return super().render(template_name, context)
def ask_user(d): def ask_user(d: Dict) -> None:
# type: (Dict) -> None
"""Ask the user for quickstart values missing from *d*. """Ask the user for quickstart values missing from *d*.
Values are: Values are:
@ -367,8 +349,8 @@ directly.'''))
print() print()
def generate(d, overwrite=True, silent=False, templatedir=None): def generate(d: Dict, overwrite: bool = True, silent: bool = False, templatedir: str = None
# type: (Dict, bool, bool, str) -> None ) -> None:
"""Generate project based on values in *d*.""" """Generate project based on values in *d*."""
template = QuickstartRenderer(templatedir=templatedir) template = QuickstartRenderer(templatedir=templatedir)
@ -401,8 +383,7 @@ def generate(d, overwrite=True, silent=False, templatedir=None):
ensuredir(path.join(srcdir, d['dot'] + 'templates')) ensuredir(path.join(srcdir, d['dot'] + 'templates'))
ensuredir(path.join(srcdir, d['dot'] + 'static')) ensuredir(path.join(srcdir, d['dot'] + 'static'))
def write_file(fpath, content, newline=None): def write_file(fpath: str, content: str, newline: str = None) -> None:
# type: (str, str, str) -> None
if overwrite or not path.isfile(fpath): if overwrite or not path.isfile(fpath):
if 'quiet' not in d: if 'quiet' not in d:
print(__('Creating file %s.') % fpath) print(__('Creating file %s.') % fpath)
@ -460,8 +441,7 @@ where "builder" is one of the supported builders, e.g. html, latex or linkcheck.
''')) '''))
def valid_dir(d): def valid_dir(d: Dict) -> bool:
# type: (Dict) -> bool
dir = d['path'] dir = d['path']
if not path.exists(dir): if not path.exists(dir):
return True return True
@ -490,8 +470,7 @@ def valid_dir(d):
return True return True
def get_parser(): def get_parser() -> argparse.ArgumentParser:
# type: () -> argparse.ArgumentParser
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
usage='%(prog)s [OPTIONS] <PROJECT_DIR>', usage='%(prog)s [OPTIONS] <PROJECT_DIR>',
epilog=__("For more information, visit <http://sphinx-doc.org/>."), epilog=__("For more information, visit <http://sphinx-doc.org/>."),
@ -572,8 +551,7 @@ Makefile to be used with sphinx-build.
return parser return parser
def main(argv=sys.argv[1:]): def main(argv: List[str] = sys.argv[1:]) -> int:
# type: (List[str]) -> int
sphinx.locale.setlocale(locale.LC_ALL, '') sphinx.locale.setlocale(locale.LC_ALL, '')
sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx') sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx')

View File

@ -73,6 +73,7 @@ class ObjectDescription(SphinxDirective):
def get_field_type_map(self) -> Dict[str, Tuple[Field, bool]]: def get_field_type_map(self) -> Dict[str, Tuple[Field, bool]]:
if self._doc_field_type_map == {}: if self._doc_field_type_map == {}:
self._doc_field_type_map = {}
for field in self.doc_field_types: for field in self.doc_field_types:
for name in field.names: for name in field.names:
self._doc_field_type_map[name] = (field, False) self._doc_field_type_map[name] = (field, False)

View File

@ -10,21 +10,22 @@
""" """
import copy import copy
from typing import NamedTuple from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Tuple, Type, Union
from docutils import nodes
from docutils.nodes import Element, Node, system_message
from docutils.parsers.rst.states import Inliner
from sphinx.addnodes import pending_xref
from sphinx.errors import SphinxError from sphinx.errors import SphinxError
from sphinx.locale import _ from sphinx.locale import _
from sphinx.roles import XRefRole
from sphinx.util.typing import RoleFunction
if False: if False:
# For type annotation # For type annotation
from typing import Any, Callable, Dict, Iterable, List, Tuple, Type, Union # NOQA from sphinx.builders import Builder
from docutils import nodes # NOQA from sphinx.environment import BuildEnvironment
from docutils.parsers.rst.states import Inliner # NOQA
from sphinx import addnodes # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.roles import XRefRole # NOQA
from sphinx.util.typing import RoleFunction # NOQA
class ObjType: class ObjType:
@ -46,8 +47,7 @@ class ObjType:
'searchprio': 1, 'searchprio': 1,
} }
def __init__(self, lname, *roles, **attrs): def __init__(self, lname: str, *roles, **attrs) -> None:
# type: (str, Any, Any) -> None
self.lname = lname self.lname = lname
self.roles = roles # type: Tuple self.roles = roles # type: Tuple
self.attrs = self.known_attrs.copy() # type: Dict self.attrs = self.known_attrs.copy() # type: Dict
@ -82,15 +82,14 @@ class Index:
localname = None # type: str localname = None # type: str
shortname = None # type: str shortname = None # type: str
def __init__(self, domain): def __init__(self, domain: "Domain") -> None:
# type: (Domain) -> None
if self.name is None or self.localname is None: if self.name is None or self.localname is None:
raise SphinxError('Index subclass %s has no valid name or localname' raise SphinxError('Index subclass %s has no valid name or localname'
% self.__class__.__name__) % self.__class__.__name__)
self.domain = domain self.domain = domain
def generate(self, docnames=None): def generate(self, docnames: Iterable[str] = None
# type: (Iterable[str]) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool] ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
"""Get entries for the index. """Get entries for the index.
If ``docnames`` is given, restrict to entries referring to these If ``docnames`` is given, restrict to entries referring to these
@ -181,7 +180,7 @@ class Domain:
#: role name -> a warning message if reference is missing #: role name -> a warning message if reference is missing
dangling_warnings = {} # type: Dict[str, str] dangling_warnings = {} # type: Dict[str, str]
#: node_class -> (enum_node_type, title_getter) #: node_class -> (enum_node_type, title_getter)
enumerable_nodes = {} # type: Dict[Type[nodes.Node], Tuple[str, Callable]] enumerable_nodes = {} # type: Dict[Type[Node], Tuple[str, Callable]]
#: data value for a fresh environment #: data value for a fresh environment
initial_data = {} # type: Dict initial_data = {} # type: Dict
@ -190,8 +189,7 @@ class Domain:
#: data version, bump this when the format of `self.data` changes #: data version, bump this when the format of `self.data` changes
data_version = 0 data_version = 0
def __init__(self, env): def __init__(self, env: "BuildEnvironment") -> None:
# type: (BuildEnvironment) -> None
self.env = env # type: BuildEnvironment self.env = env # type: BuildEnvironment
self._role_cache = {} # type: Dict[str, Callable] self._role_cache = {} # type: Dict[str, Callable]
self._directive_cache = {} # type: Dict[str, Callable] self._directive_cache = {} # type: Dict[str, Callable]
@ -220,8 +218,7 @@ class Domain:
self.objtypes_for_role = self._role2type.get # type: Callable[[str], List[str]] self.objtypes_for_role = self._role2type.get # type: Callable[[str], List[str]]
self.role_for_objtype = self._type2role.get # type: Callable[[str], str] self.role_for_objtype = self._type2role.get # type: Callable[[str], str]
def add_object_type(self, name, objtype): def add_object_type(self, name: str, objtype: ObjType) -> None:
# type: (str, ObjType) -> None
"""Add an object type.""" """Add an object type."""
self.object_types[name] = objtype self.object_types[name] = objtype
if objtype.roles: if objtype.roles:
@ -232,8 +229,7 @@ class Domain:
for role in objtype.roles: for role in objtype.roles:
self._role2type.setdefault(role, []).append(name) self._role2type.setdefault(role, []).append(name)
def role(self, name): def role(self, name: str) -> RoleFunction:
# type: (str) -> RoleFunction
"""Return a role adapter function that always gives the registered """Return a role adapter function that always gives the registered
role its full name ('domain:name') as the first argument. role its full name ('domain:name') as the first argument.
""" """
@ -243,15 +239,15 @@ class Domain:
return None return None
fullname = '%s:%s' % (self.name, name) fullname = '%s:%s' % (self.name, name)
def role_adapter(typ, rawtext, text, lineno, inliner, options={}, content=[]): def role_adapter(typ: str, rawtext: str, text: str, lineno: int,
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA inliner: Inliner, options: Dict = {}, content: List[str] = []
) -> Tuple[List[Node], List[system_message]]:
return self.roles[name](fullname, rawtext, text, lineno, return self.roles[name](fullname, rawtext, text, lineno,
inliner, options, content) inliner, options, content)
self._role_cache[name] = role_adapter self._role_cache[name] = role_adapter
return role_adapter return role_adapter
def directive(self, name): def directive(self, name: str) -> Callable:
# type: (str) -> Callable
"""Return a directive adapter class that always gives the registered """Return a directive adapter class that always gives the registered
directive its full name ('domain:name') as ``self.name``. directive its full name ('domain:name') as ``self.name``.
""" """
@ -263,8 +259,7 @@ class Domain:
BaseDirective = self.directives[name] BaseDirective = self.directives[name]
class DirectiveAdapter(BaseDirective): # type: ignore class DirectiveAdapter(BaseDirective): # type: ignore
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
self.name = fullname self.name = fullname
return super().run() return super().run()
self._directive_cache[name] = DirectiveAdapter self._directive_cache[name] = DirectiveAdapter
@ -272,13 +267,11 @@ class Domain:
# methods that should be overwritten # methods that should be overwritten
def clear_doc(self, docname): def clear_doc(self, docname: str) -> None:
# type: (str) -> None
"""Remove traces of a document in the domain-specific inventories.""" """Remove traces of a document in the domain-specific inventories."""
pass pass
def merge_domaindata(self, docnames, otherdata): def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# type: (List[str], Dict) -> None
"""Merge in data regarding *docnames* from a different domaindata """Merge in data regarding *docnames* from a different domaindata
inventory (coming from a subprocess in parallel builds). inventory (coming from a subprocess in parallel builds).
""" """
@ -286,26 +279,24 @@ class Domain:
'to be able to do parallel builds!' % 'to be able to do parallel builds!' %
self.__class__) self.__class__)
def process_doc(self, env, docname, document): def process_doc(self, env: "BuildEnvironment", docname: str,
# type: (BuildEnvironment, str, nodes.document) -> None document: nodes.document) -> None:
"""Process a document after it is read by the environment.""" """Process a document after it is read by the environment."""
pass pass
def check_consistency(self): def check_consistency(self) -> None:
# type: () -> None
"""Do consistency checks (**experimental**).""" """Do consistency checks (**experimental**)."""
pass pass
def process_field_xref(self, pnode): def process_field_xref(self, pnode: pending_xref) -> None:
# type: (addnodes.pending_xref) -> None
"""Process a pending xref created in a doc field. """Process a pending xref created in a doc field.
For example, attach information about the current scope. For example, attach information about the current scope.
""" """
pass pass
def resolve_xref(self, env, fromdocname, builder, def resolve_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder",
typ, target, node, contnode): typ: str, target: str, node: pending_xref, contnode: Element
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA ) -> Element:
"""Resolve the pending_xref *node* with the given *typ* and *target*. """Resolve the pending_xref *node* with the given *typ* and *target*.
This method should return a new node, to replace the xref node, This method should return a new node, to replace the xref node,
@ -321,8 +312,9 @@ class Domain:
""" """
pass pass
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): def resolve_any_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder",
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA target: str, node: pending_xref, contnode: Element
) -> List[Tuple[str, Element]]:
"""Resolve the pending_xref *node* with the given *target*. """Resolve the pending_xref *node* with the given *target*.
The reference comes from an "any" or similar role, which means that we The reference comes from an "any" or similar role, which means that we
@ -338,8 +330,7 @@ class Domain:
""" """
raise NotImplementedError raise NotImplementedError
def get_objects(self): def get_objects(self) -> Iterable[Tuple[str, str, str, str, str, int]]:
# type: () -> Iterable[Tuple[str, str, str, str, str, int]]
"""Return an iterable of "object descriptions". """Return an iterable of "object descriptions".
Object descriptions are tuples with six items: Object descriptions are tuples with six items:
@ -374,20 +365,17 @@ class Domain:
""" """
return [] return []
def get_type_name(self, type, primary=False): def get_type_name(self, type: ObjType, primary: bool = False) -> str:
# type: (ObjType, bool) -> str
"""Return full name for given ObjType.""" """Return full name for given ObjType."""
if primary: if primary:
return type.lname return type.lname
return _('%s %s') % (self.label, type.lname) return _('%s %s') % (self.label, type.lname)
def get_enumerable_node_type(self, node): def get_enumerable_node_type(self, node: Node) -> str:
# type: (nodes.Node) -> str
"""Get type of enumerable nodes (experimental).""" """Get type of enumerable nodes (experimental)."""
enum_node_type, _ = self.enumerable_nodes.get(node.__class__, (None, None)) enum_node_type, _ = self.enumerable_nodes.get(node.__class__, (None, None))
return enum_node_type return enum_node_type
def get_full_qualified_name(self, node): def get_full_qualified_name(self, node: Element) -> str:
# type: (nodes.Element) -> str
"""Return full qualified name for given node.""" """Return full qualified name for given node."""
return None return None

View File

@ -10,24 +10,27 @@
import re import re
import string import string
from typing import Any, Dict, Iterator, List, Tuple
from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Element
from sphinx import addnodes from sphinx import addnodes
from sphinx.addnodes import pending_xref, desc_signature
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.directives import ObjectDescription from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType from sphinx.domains import Domain, ObjType
from sphinx.locale import _ from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.docfields import Field, TypedField from sphinx.util.docfields import Field, TypedField
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
if False:
# For type annotation
from typing import Any, Dict, Iterator, List, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__)
# RE to split at word boundaries # RE to split at word boundaries
wsplit_re = re.compile(r'(\W+)') wsplit_re = re.compile(r'(\W+)')
@ -79,23 +82,20 @@ class CObject(ObjectDescription):
'struct', '_Bool', 'struct', '_Bool',
} }
def _parse_type(self, node, ctype): def _parse_type(self, node: Element, ctype: str) -> None:
# type: (nodes.Element, str) -> None
# add cross-ref nodes for all words # add cross-ref nodes for all words
for part in [_f for _f in wsplit_re.split(ctype) if _f]: for part in [_f for _f in wsplit_re.split(ctype) if _f]:
tnode = nodes.Text(part, part) tnode = nodes.Text(part, part)
if part[0] in string.ascii_letters + '_' and \ if part[0] in string.ascii_letters + '_' and \
part not in self.stopwords: part not in self.stopwords:
pnode = addnodes.pending_xref( pnode = pending_xref('', refdomain='c', reftype='type', reftarget=part,
'', refdomain='c', reftype='type', reftarget=part,
modname=None, classname=None) modname=None, classname=None)
pnode += tnode pnode += tnode
node += pnode node += pnode
else: else:
node += tnode node += tnode
def _parse_arglist(self, arglist): def _parse_arglist(self, arglist: str) -> Iterator[str]:
# type: (str) -> Iterator[str]
while True: while True:
m = c_funcptr_arg_sig_re.match(arglist) m = c_funcptr_arg_sig_re.match(arglist)
if m: if m:
@ -113,8 +113,7 @@ class CObject(ObjectDescription):
yield arglist yield arglist
break break
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> str:
# type: (str, addnodes.desc_signature) -> str
"""Transform a C signature into RST nodes.""" """Transform a C signature into RST nodes."""
# first try the function pointer signature regex, it's more specific # first try the function pointer signature regex, it's more specific
m = c_funcptr_sig_re.match(sig) m = c_funcptr_sig_re.match(sig)
@ -183,8 +182,7 @@ class CObject(ObjectDescription):
signode += addnodes.desc_addname(const, const) signode += addnodes.desc_addname(const, const)
return fullname return fullname
def get_index_text(self, name): def get_index_text(self, name: str) -> str:
# type: (str) -> str
if self.objtype == 'function': if self.objtype == 'function':
return _('%s (C function)') % name return _('%s (C function)') % name
elif self.objtype == 'member': elif self.objtype == 'member':
@ -198,8 +196,7 @@ class CObject(ObjectDescription):
else: else:
return '' return ''
def add_target_and_index(self, name, sig, signode): def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:
# type: (str, str, addnodes.desc_signature) -> None
# for C API items we add a prefix since names are usually not qualified # for C API items we add a prefix since names are usually not qualified
# by a module name and so easily clash with e.g. section titles # by a module name and so easily clash with e.g. section titles
targetname = 'c.' + name targetname = 'c.' + name
@ -208,36 +205,30 @@ class CObject(ObjectDescription):
signode['ids'].append(targetname) signode['ids'].append(targetname)
signode['first'] = (not self.names) signode['first'] = (not self.names)
self.state.document.note_explicit_target(signode) self.state.document.note_explicit_target(signode)
inv = self.env.domaindata['c']['objects']
if name in inv: domain = cast(CDomain, self.env.get_domain('c'))
self.state_machine.reporter.warning( domain.note_object(name, self.objtype)
'duplicate C object description of %s, ' % name +
'other instance in ' + self.env.doc2path(inv[name][0]),
line=self.lineno)
inv[name] = (self.env.docname, self.objtype)
indextext = self.get_index_text(name) indextext = self.get_index_text(name)
if indextext: if indextext:
self.indexnode['entries'].append(('single', indextext, self.indexnode['entries'].append(('single', indextext,
targetname, '', None)) targetname, '', None))
def before_content(self): def before_content(self) -> None:
# type: () -> None
self.typename_set = False self.typename_set = False
if self.name == 'c:type': if self.name == 'c:type':
if self.names: if self.names:
self.env.ref_context['c:type'] = self.names[0] self.env.ref_context['c:type'] = self.names[0]
self.typename_set = True self.typename_set = True
def after_content(self): def after_content(self) -> None:
# type: () -> None
if self.typename_set: if self.typename_set:
self.env.ref_context.pop('c:type', None) self.env.ref_context.pop('c:type', None)
class CXRefRole(XRefRole): class CXRefRole(XRefRole):
def process_link(self, env, refnode, has_explicit_title, title, target): def process_link(self, env: BuildEnvironment, refnode: Element,
# type: (BuildEnvironment, nodes.Element, bool, str, str) -> Tuple[str, str] has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
if not has_explicit_title: if not has_explicit_title:
target = target.lstrip('~') # only has a meaning for the title target = target.lstrip('~') # only has a meaning for the title
# if the first character is a tilde, don't display the module/class # if the first character is a tilde, don't display the module/class
@ -280,53 +271,61 @@ class CDomain(Domain):
'objects': {}, # fullname -> docname, objtype 'objects': {}, # fullname -> docname, objtype
} # type: Dict[str, Dict[str, Tuple[str, Any]]] } # type: Dict[str, Dict[str, Tuple[str, Any]]]
def clear_doc(self, docname): @property
# type: (str) -> None def objects(self) -> Dict[str, Tuple[str, str]]:
for fullname, (fn, _l) in list(self.data['objects'].items()): return self.data.setdefault('objects', {}) # fullname -> docname, objtype
if fn == docname:
del self.data['objects'][fullname]
def merge_domaindata(self, docnames, otherdata): def note_object(self, name: str, objtype: str, location: Any = None) -> None:
# type: (List[str], Dict) -> None if name in self.objects:
docname = self.objects[name][0]
logger.warning(__('duplicate C object description of %s, '
'other instance in %s, use :noindex: for one of them'),
name, docname, location=location)
self.objects[name] = (self.env.docname, objtype)
def clear_doc(self, docname: str) -> None:
for fullname, (fn, _l) in list(self.objects.items()):
if fn == docname:
del self.objects[fullname]
def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# XXX check duplicates # XXX check duplicates
for fullname, (fn, objtype) in otherdata['objects'].items(): for fullname, (fn, objtype) in otherdata['objects'].items():
if fn in docnames: if fn in docnames:
self.data['objects'][fullname] = (fn, objtype) self.data['objects'][fullname] = (fn, objtype)
def resolve_xref(self, env, fromdocname, builder, def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
typ, target, node, contnode): typ: str, target: str, node: pending_xref, contnode: Element
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA ) -> Element:
# strip pointer asterisk # strip pointer asterisk
target = target.rstrip(' *') target = target.rstrip(' *')
# becase TypedField can generate xrefs # becase TypedField can generate xrefs
if target in CObject.stopwords: if target in CObject.stopwords:
return contnode return contnode
if target not in self.data['objects']: if target not in self.objects:
return None return None
obj = self.data['objects'][target] obj = self.objects[target]
return make_refnode(builder, fromdocname, obj[0], 'c.' + target, return make_refnode(builder, fromdocname, obj[0], 'c.' + target,
contnode, target) contnode, target)
def resolve_any_xref(self, env, fromdocname, builder, target, def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
node, contnode): target: str, node: pending_xref, contnode: Element
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA ) -> List[Tuple[str, Element]]:
# strip pointer asterisk # strip pointer asterisk
target = target.rstrip(' *') target = target.rstrip(' *')
if target not in self.data['objects']: if target not in self.objects:
return [] return []
obj = self.data['objects'][target] obj = self.objects[target]
return [('c:' + self.role_for_objtype(obj[1]), return [('c:' + self.role_for_objtype(obj[1]),
make_refnode(builder, fromdocname, obj[0], 'c.' + target, make_refnode(builder, fromdocname, obj[0], 'c.' + target,
contnode, target))] contnode, target))]
def get_objects(self): def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
# type: () -> Iterator[Tuple[str, str, str, str, str, int]] for refname, (docname, type) in list(self.objects.items()):
for refname, (docname, type) in list(self.data['objects'].items()):
yield (refname, refname, type, docname, 'c.' + refname, 1) yield (refname, refname, type, docname, 'c.' + refname, 1)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(CDomain) app.add_domain(CDomain)
return { return {

View File

@ -9,9 +9,11 @@
""" """
from collections import namedtuple from collections import namedtuple
from typing import Any, Dict, List
from typing import cast from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Node
from sphinx import addnodes from sphinx import addnodes
from sphinx import locale from sphinx import locale
@ -23,9 +25,8 @@ from sphinx.util.docutils import SphinxDirective
if False: if False:
# For type annotation # For type annotation
from typing import Any, Dict, List # NOQA from sphinx.application import Sphinx
from sphinx.application import Sphinx # NOQA from sphinx.environment import BuildEnvironment
from sphinx.environment import BuildEnvironment # NOQA
versionlabels = { versionlabels = {
@ -63,8 +64,7 @@ class VersionChange(SphinxDirective):
final_argument_whitespace = True final_argument_whitespace = True
option_spec = {} # type: Dict option_spec = {} # type: Dict
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
node = addnodes.versionmodified() node = addnodes.versionmodified()
node.document = self.state.document node.document = self.state.document
self.set_source_info(node) self.set_source_info(node)
@ -102,7 +102,7 @@ class VersionChange(SphinxDirective):
domain = cast(ChangeSetDomain, self.env.get_domain('changeset')) domain = cast(ChangeSetDomain, self.env.get_domain('changeset'))
domain.note_changeset(node) domain.note_changeset(node)
ret = [node] # type: List[nodes.Node] ret = [node] # type: List[Node]
ret += messages ret += messages
return ret return ret
@ -117,42 +117,40 @@ class ChangeSetDomain(Domain):
'changes': {}, # version -> list of ChangeSet 'changes': {}, # version -> list of ChangeSet
} # type: Dict } # type: Dict
def clear_doc(self, docname): @property
# type: (str) -> None def changesets(self) -> Dict[str, List[ChangeSet]]:
for version, changes in self.data['changes'].items(): return self.data.setdefault('changes', {}) # version -> list of ChangeSet
for changeset in changes[:]:
if changeset.docname == docname:
changes.remove(changeset)
def merge_domaindata(self, docnames, otherdata): def note_changeset(self, node: addnodes.versionmodified) -> None:
# type: (List[str], Dict) -> None
# XXX duplicates?
for version, otherchanges in otherdata['changes'].items():
changes = self.data['changes'].setdefault(version, [])
for changeset in otherchanges:
if changeset.docname in docnames:
changes.append(changeset)
def process_doc(self, env, docname, document):
# type: (BuildEnvironment, str, nodes.document) -> None
pass # nothing to do here. All changesets are registered on calling directive.
def note_changeset(self, node):
# type: (addnodes.versionmodified) -> None
version = node['version'] version = node['version']
module = self.env.ref_context.get('py:module') module = self.env.ref_context.get('py:module')
objname = self.env.temp_data.get('object') objname = self.env.temp_data.get('object')
changeset = ChangeSet(node['type'], self.env.docname, node.line, changeset = ChangeSet(node['type'], self.env.docname, node.line,
module, objname, node.astext()) module, objname, node.astext())
self.data['changes'].setdefault(version, []).append(changeset) self.changesets.setdefault(version, []).append(changeset)
def get_changesets_for(self, version): def clear_doc(self, docname: str) -> None:
# type: (str) -> List[ChangeSet] for version, changes in self.changesets.items():
return self.data['changes'].get(version, []) for changeset in changes[:]:
if changeset.docname == docname:
changes.remove(changeset)
def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# XXX duplicates?
for version, otherchanges in otherdata['changes'].items():
changes = self.changesets.setdefault(version, [])
for changeset in otherchanges:
if changeset.docname in docnames:
changes.append(changeset)
def process_doc(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA
pass # nothing to do here. All changesets are registered on calling directive.
def get_changesets_for(self, version: str) -> List[ChangeSet]:
return self.changesets.get(version, [])
def setup(app): def setup(app: "Sphinx") -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(ChangeSetDomain) app.add_domain(ChangeSetDomain)
app.add_directive('deprecated', VersionChange) app.add_directive('deprecated', VersionChange)
app.add_directive('versionadded', VersionChange) app.add_directive('versionadded', VersionChange)

View File

@ -8,11 +8,13 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, List, Set, Tuple
from typing import cast from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Element
from sphinx import addnodes from sphinx.addnodes import pending_xref
from sphinx.domains import Domain from sphinx.domains import Domain
from sphinx.locale import __ from sphinx.locale import __
from sphinx.transforms import SphinxTransform from sphinx.transforms import SphinxTransform
@ -21,10 +23,10 @@ from sphinx.util.nodes import copy_source_info, make_refnode
if False: if False:
# For type annotation # For type annotation
from typing import Any, Dict, List, Set, Tuple, Union # NOQA from sphinx.application import Sphinx
from sphinx.application import Sphinx # NOQA from sphinx.builders import Builder
from sphinx.builders import Builder # NOQA from sphinx.environment import BuildEnvironment
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -40,17 +42,14 @@ class CitationDomain(Domain):
} }
@property @property
def citations(self): def citations(self) -> Dict[str, Tuple[str, str, int]]:
# type: () -> Dict[str, Tuple[str, str, int]]
return self.data.setdefault('citations', {}) return self.data.setdefault('citations', {})
@property @property
def citation_refs(self): def citation_refs(self) -> Dict[str, Set[str]]:
# type: () -> Dict[str, Set[str]]
return self.data.setdefault('citation_refs', {}) return self.data.setdefault('citation_refs', {})
def clear_doc(self, docname): def clear_doc(self, docname: str) -> None:
# type: (str) -> None
for key, (fn, _l, lineno) in list(self.citations.items()): for key, (fn, _l, lineno) in list(self.citations.items()):
if fn == docname: if fn == docname:
del self.citations[key] del self.citations[key]
@ -60,8 +59,7 @@ class CitationDomain(Domain):
elif docname in docnames: elif docname in docnames:
docnames.remove(docname) docnames.remove(docname)
def merge_domaindata(self, docnames, otherdata): def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# type: (List[str], Dict) -> None
# XXX duplicates? # XXX duplicates?
for key, data in otherdata['citations'].items(): for key, data in otherdata['citations'].items():
if data[0] in docnames: if data[0] in docnames:
@ -72,8 +70,7 @@ class CitationDomain(Domain):
if docname in docnames: if docname in docnames:
citation_refs.add(docname) citation_refs.add(docname)
def note_citation(self, node): def note_citation(self, node: nodes.citation) -> None:
# type: (nodes.citation) -> None
label = node[0].astext() label = node[0].astext()
if label in self.citations: if label in self.citations:
path = self.env.doc2path(self.citations[label][0]) path = self.env.doc2path(self.citations[label][0])
@ -81,20 +78,19 @@ class CitationDomain(Domain):
location=node, type='ref', subtype='citation') location=node, type='ref', subtype='citation')
self.citations[label] = (node['docname'], node['ids'][0], node.line) self.citations[label] = (node['docname'], node['ids'][0], node.line)
def note_citation_reference(self, node): def note_citation_reference(self, node: pending_xref) -> None:
# type: (addnodes.pending_xref) -> None
docnames = self.citation_refs.setdefault(node['reftarget'], set()) docnames = self.citation_refs.setdefault(node['reftarget'], set())
docnames.add(self.env.docname) docnames.add(self.env.docname)
def check_consistency(self): def check_consistency(self) -> None:
# type: () -> None
for name, (docname, labelid, lineno) in self.citations.items(): for name, (docname, labelid, lineno) in self.citations.items():
if name not in self.citation_refs: if name not in self.citation_refs:
logger.warning(__('Citation [%s] is not referenced.'), name, logger.warning(__('Citation [%s] is not referenced.'), name,
type='ref', subtype='citation', location=(docname, lineno)) type='ref', subtype='citation', location=(docname, lineno))
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): def resolve_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder",
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA typ: str, target: str, node: pending_xref, contnode: Element
) -> Element:
docname, labelid, lineno = self.citations.get(target, ('', '', 0)) docname, labelid, lineno = self.citations.get(target, ('', '', 0))
if not docname: if not docname:
return None return None
@ -102,8 +98,9 @@ class CitationDomain(Domain):
return make_refnode(builder, fromdocname, docname, return make_refnode(builder, fromdocname, docname,
labelid, contnode) labelid, contnode)
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): def resolve_any_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder",
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA target: str, node: pending_xref, contnode: Element
) -> List[Tuple[str, Element]]:
refnode = self.resolve_xref(env, fromdocname, builder, 'ref', target, node, contnode) refnode = self.resolve_xref(env, fromdocname, builder, 'ref', target, node, contnode)
if refnode is None: if refnode is None:
return [] return []
@ -115,8 +112,7 @@ class CitationDefinitionTransform(SphinxTransform):
"""Mark citation definition labels as not smartquoted.""" """Mark citation definition labels as not smartquoted."""
default_priority = 619 default_priority = 619
def apply(self, **kwargs): def apply(self, **kwargs) -> None:
# type: (Any) -> None
domain = cast(CitationDomain, self.env.get_domain('citation')) domain = cast(CitationDomain, self.env.get_domain('citation'))
for node in self.document.traverse(nodes.citation): for node in self.document.traverse(nodes.citation):
# register citation node to domain # register citation node to domain
@ -135,12 +131,11 @@ class CitationReferenceTransform(SphinxTransform):
""" """
default_priority = 619 default_priority = 619
def apply(self, **kwargs): def apply(self, **kwargs) -> None:
# type: (Any) -> None
domain = cast(CitationDomain, self.env.get_domain('citation')) domain = cast(CitationDomain, self.env.get_domain('citation'))
for node in self.document.traverse(nodes.citation_reference): for node in self.document.traverse(nodes.citation_reference):
target = node.astext() target = node.astext()
ref = addnodes.pending_xref(target, refdomain='citation', reftype='ref', ref = pending_xref(target, refdomain='citation', reftype='ref',
reftarget=target, refwarn=True, reftarget=target, refwarn=True,
support_smartquotes=False, support_smartquotes=False,
ids=node["ids"], ids=node["ids"],
@ -153,8 +148,7 @@ class CitationReferenceTransform(SphinxTransform):
domain.note_citation_reference(ref) domain.note_citation_reference(ref)
def setup(app): def setup(app: "Sphinx") -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(CitationDomain) app.add_domain(CitationDomain)
app.add_transform(CitationDefinitionTransform) app.add_transform(CitationDefinitionTransform)
app.add_transform(CitationReferenceTransform) app.add_transform(CitationReferenceTransform)

View File

@ -8,25 +8,30 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, Iterator, List, Tuple
from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
from sphinx import addnodes from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.directives import ObjectDescription from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType from sphinx.domains import Domain, ObjType
from sphinx.domains.python import _pseudo_parse_arglist from sphinx.domains.python import _pseudo_parse_arglist
from sphinx.locale import _ from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.docfields import Field, GroupedField, TypedField from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
if False:
# For type annotation logger = logging.getLogger(__name__)
from typing import Any, Dict, Iterator, List, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
class JSObject(ObjectDescription): class JSObject(ObjectDescription):
@ -44,8 +49,7 @@ class JSObject(ObjectDescription):
#: based on directive nesting #: based on directive nesting
allow_nesting = False allow_nesting = False
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
# type: (str, addnodes.desc_signature) -> Tuple[str, str]
"""Breaks down construct signatures """Breaks down construct signatures
Parses out prefix and argument list from construct definition. The Parses out prefix and argument list from construct definition. The
@ -98,8 +102,8 @@ class JSObject(ObjectDescription):
_pseudo_parse_arglist(signode, arglist) _pseudo_parse_arglist(signode, arglist)
return fullname, prefix return fullname, prefix
def add_target_and_index(self, name_obj, sig, signode): def add_target_and_index(self, name_obj: Tuple[str, str], sig: str,
# type: (Tuple[str, str], str, addnodes.desc_signature) -> None signode: desc_signature) -> None:
mod_name = self.env.ref_context.get('js:module') mod_name = self.env.ref_context.get('js:module')
fullname = (mod_name and mod_name + '.' or '') + name_obj[0] fullname = (mod_name and mod_name + '.' or '') + name_obj[0]
if fullname not in self.state.document.ids: if fullname not in self.state.document.ids:
@ -107,14 +111,10 @@ class JSObject(ObjectDescription):
signode['ids'].append(fullname.replace('$', '_S_')) signode['ids'].append(fullname.replace('$', '_S_'))
signode['first'] = not self.names signode['first'] = not self.names
self.state.document.note_explicit_target(signode) self.state.document.note_explicit_target(signode)
objects = self.env.domaindata['js']['objects']
if fullname in objects: domain = cast(JavaScriptDomain, self.env.get_domain('js'))
self.state_machine.reporter.warning( domain.note_object(fullname, self.objtype,
'duplicate object description of %s, ' % fullname + location=(self.env.docname, self.lineno))
'other instance in ' +
self.env.doc2path(objects[fullname][0]),
line=self.lineno)
objects[fullname] = self.env.docname, self.objtype
indextext = self.get_index_text(mod_name, name_obj) indextext = self.get_index_text(mod_name, name_obj)
if indextext: if indextext:
@ -122,8 +122,7 @@ class JSObject(ObjectDescription):
fullname.replace('$', '_S_'), fullname.replace('$', '_S_'),
'', None)) '', None))
def get_index_text(self, objectname, name_obj): def get_index_text(self, objectname: str, name_obj: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
name, obj = name_obj name, obj = name_obj
if self.objtype == 'function': if self.objtype == 'function':
if not obj: if not obj:
@ -137,8 +136,7 @@ class JSObject(ObjectDescription):
return _('%s (%s attribute)') % (name, obj) return _('%s (%s attribute)') % (name, obj)
return '' return ''
def before_content(self): def before_content(self) -> None:
# type: () -> None
"""Handle object nesting before content """Handle object nesting before content
:py:class:`JSObject` represents JavaScript language constructs. For :py:class:`JSObject` represents JavaScript language constructs. For
@ -174,8 +172,7 @@ class JSObject(ObjectDescription):
objects = self.env.ref_context.setdefault('js:objects', []) objects = self.env.ref_context.setdefault('js:objects', [])
objects.append(prefix) objects.append(prefix)
def after_content(self): def after_content(self) -> None:
# type: () -> None
"""Handle object de-nesting after content """Handle object de-nesting after content
If this class is a nestable object, removing the last nested class prefix If this class is a nestable object, removing the last nested class prefix
@ -246,17 +243,19 @@ class JSModule(SphinxDirective):
'noindex': directives.flag 'noindex': directives.flag
} }
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
mod_name = self.arguments[0].strip() mod_name = self.arguments[0].strip()
self.env.ref_context['js:module'] = mod_name self.env.ref_context['js:module'] = mod_name
noindex = 'noindex' in self.options noindex = 'noindex' in self.options
ret = [] # type: List[nodes.Node] ret = [] # type: List[Node]
if not noindex: if not noindex:
self.env.domaindata['js']['modules'][mod_name] = self.env.docname domain = cast(JavaScriptDomain, self.env.get_domain('js'))
domain.note_module(mod_name)
# Make a duplicate entry in 'objects' to facilitate searching for # Make a duplicate entry in 'objects' to facilitate searching for
# the module in JavaScriptDomain.find_obj() # the module in JavaScriptDomain.find_obj()
self.env.domaindata['js']['objects'][mod_name] = (self.env.docname, 'module') domain.note_object(mod_name, 'module', location=(self.env.docname, self.lineno))
targetnode = nodes.target('', '', ids=['module-' + mod_name], targetnode = nodes.target('', '', ids=['module-' + mod_name],
ismod=True) ismod=True)
self.state.document.note_explicit_target(targetnode) self.state.document.note_explicit_target(targetnode)
@ -269,8 +268,8 @@ class JSModule(SphinxDirective):
class JSXRefRole(XRefRole): class JSXRefRole(XRefRole):
def process_link(self, env, refnode, has_explicit_title, title, target): def process_link(self, env: BuildEnvironment, refnode: Element,
# type: (BuildEnvironment, nodes.Element, bool, str, str) -> Tuple[str, str] has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
# basically what sphinx.domains.python.PyXRefRole does # basically what sphinx.domains.python.PyXRefRole does
refnode['js:object'] = env.ref_context.get('js:object') refnode['js:object'] = env.ref_context.get('js:object')
refnode['js:module'] = env.ref_context.get('js:module') refnode['js:module'] = env.ref_context.get('js:module')
@ -319,33 +318,48 @@ class JavaScriptDomain(Domain):
} }
initial_data = { initial_data = {
'objects': {}, # fullname -> docname, objtype 'objects': {}, # fullname -> docname, objtype
'modules': {}, # mod_name -> docname 'modules': {}, # modname -> docname
} # type: Dict[str, Dict[str, Tuple[str, str]]] } # type: Dict[str, Dict[str, Tuple[str, str]]]
def clear_doc(self, docname): @property
# type: (str) -> None def objects(self) -> Dict[str, Tuple[str, str]]:
for fullname, (pkg_docname, _l) in list(self.data['objects'].items()): return self.data.setdefault('objects', {}) # fullname -> docname, objtype
if pkg_docname == docname:
del self.data['objects'][fullname]
for mod_name, pkg_docname in list(self.data['modules'].items()):
if pkg_docname == docname:
del self.data['modules'][mod_name]
def merge_domaindata(self, docnames, otherdata): def note_object(self, fullname: str, objtype: str, location: Any = None) -> None:
# type: (List[str], Dict) -> None if fullname in self.objects:
docname = self.objects[fullname][0]
logger.warning(__('duplicate object description of %s, other instance in %s'),
fullname, docname, location=location)
self.objects[fullname] = (self.env.docname, objtype)
@property
def modules(self) -> Dict[str, str]:
return self.data.setdefault('modules', {}) # modname -> docname
def note_module(self, modname: str) -> None:
self.modules[modname] = self.env.docname
def clear_doc(self, docname: str) -> None:
for fullname, (pkg_docname, _l) in list(self.objects.items()):
if pkg_docname == docname:
del self.objects[fullname]
for modname, pkg_docname in list(self.modules.items()):
if pkg_docname == docname:
del self.modules[modname]
def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# XXX check duplicates # XXX check duplicates
for fullname, (fn, objtype) in otherdata['objects'].items(): for fullname, (fn, objtype) in otherdata['objects'].items():
if fn in docnames: if fn in docnames:
self.data['objects'][fullname] = (fn, objtype) self.objects[fullname] = (fn, objtype)
for mod_name, pkg_docname in otherdata['modules'].items(): for mod_name, pkg_docname in otherdata['modules'].items():
if pkg_docname in docnames: if pkg_docname in docnames:
self.data['modules'][mod_name] = pkg_docname self.modules[mod_name] = pkg_docname
def find_obj(self, env, mod_name, prefix, name, typ, searchorder=0): def find_obj(self, env: BuildEnvironment, mod_name: str, prefix: str, name: str,
# type: (BuildEnvironment, str, str, str, str, int) -> Tuple[str, Tuple[str, str]] typ: str, searchorder: int = 0) -> Tuple[str, Tuple[str, str]]:
if name[-2:] == '()': if name[-2:] == '()':
name = name[:-2] name = name[:-2]
objects = self.data['objects']
searches = [] searches = []
if mod_name and prefix: if mod_name and prefix:
@ -361,14 +375,14 @@ class JavaScriptDomain(Domain):
newname = None newname = None
for search_name in searches: for search_name in searches:
if search_name in objects: if search_name in self.objects:
newname = search_name newname = search_name
return newname, objects.get(newname) return newname, self.objects.get(newname)
def resolve_xref(self, env, fromdocname, builder, typ, target, node, def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
contnode): typ: str, target: str, node: pending_xref, contnode: Element
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA ) -> Element:
mod_name = node.get('js:module') mod_name = node.get('js:module')
prefix = node.get('js:object') prefix = node.get('js:object')
searchorder = node.hasattr('refspecific') and 1 or 0 searchorder = node.hasattr('refspecific') and 1 or 0
@ -378,9 +392,9 @@ class JavaScriptDomain(Domain):
return make_refnode(builder, fromdocname, obj[0], return make_refnode(builder, fromdocname, obj[0],
name.replace('$', '_S_'), contnode, name) name.replace('$', '_S_'), contnode, name)
def resolve_any_xref(self, env, fromdocname, builder, target, node, def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
contnode): target: str, node: pending_xref, contnode: Element
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA ) -> List[Tuple[str, Element]]:
mod_name = node.get('js:module') mod_name = node.get('js:module')
prefix = node.get('js:object') prefix = node.get('js:object')
name, obj = self.find_obj(env, mod_name, prefix, target, None, 1) name, obj = self.find_obj(env, mod_name, prefix, target, None, 1)
@ -390,14 +404,11 @@ class JavaScriptDomain(Domain):
make_refnode(builder, fromdocname, obj[0], make_refnode(builder, fromdocname, obj[0],
name.replace('$', '_S_'), contnode, name))] name.replace('$', '_S_'), contnode, name))]
def get_objects(self): def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
# type: () -> Iterator[Tuple[str, str, str, str, str, int]] for refname, (docname, type) in list(self.objects.items()):
for refname, (docname, type) in list(self.data['objects'].items()): yield refname, refname, type, docname, refname.replace('$', '_S_'), 1
yield refname, refname, type, docname, \
refname.replace('$', '_S_'), 1
def get_full_qualified_name(self, node): def get_full_qualified_name(self, node: Element) -> str:
# type: (nodes.Element) -> str
modname = node.get('js:module') modname = node.get('js:module')
prefix = node.get('js:object') prefix = node.get('js:object')
target = node.get('reftarget') target = node.get('reftarget')
@ -407,8 +418,7 @@ class JavaScriptDomain(Domain):
return '.'.join(filter(None, [modname, prefix, target])) return '.'.join(filter(None, [modname, prefix, target]))
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(JavaScriptDomain) app.add_domain(JavaScriptDomain)
return { return {

View File

@ -9,13 +9,17 @@
""" """
import warnings import warnings
from typing import Any, Dict, Iterable, List, Tuple
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node, system_message
from docutils.nodes import make_id from docutils.nodes import make_id
from sphinx.addnodes import math_block as displaymath from sphinx.addnodes import math_block as displaymath
from sphinx.addnodes import pending_xref
from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.domains import Domain from sphinx.domains import Domain
from sphinx.environment import BuildEnvironment
from sphinx.locale import __ from sphinx.locale import __
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
from sphinx.util import logging from sphinx.util import logging
@ -23,18 +27,16 @@ from sphinx.util.nodes import make_refnode
if False: if False:
# For type annotation # For type annotation
from typing import Any, Dict, Iterable, List, Tuple # NOQA from sphinx.application import Sphinx
from sphinx import addnodes # NOQA from sphinx.builders import Builder
from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class MathReferenceRole(XRefRole): class MathReferenceRole(XRefRole):
def result_nodes(self, document, env, node, is_ref): def result_nodes(self, document: nodes.document, env: BuildEnvironment, node: Element,
# type: (nodes.document, BuildEnvironment, nodes.Element, bool) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA is_ref: bool) -> Tuple[List[Node], List[system_message]]:
node['refdomain'] = 'math' node['refdomain'] = 'math'
return [node], [] return [node], []
@ -60,12 +62,10 @@ class MathDomain(Domain):
} }
@property @property
def equations(self): def equations(self) -> Dict[str, Tuple[str, int]]:
# type: () -> Dict[str, Tuple[str, int]]
return self.data.setdefault('objects', {}) # labelid -> (docname, eqno) return self.data.setdefault('objects', {}) # labelid -> (docname, eqno)
def note_equation(self, docname, labelid, location=None): def note_equation(self, docname: str, labelid: str, location: Any = None) -> None:
# type: (str, str, Any) -> None
if labelid in self.equations: if labelid in self.equations:
other = self.equations[labelid][0] other = self.equations[labelid][0]
logger.warning(__('duplicate label of equation %s, other instance in %s') % logger.warning(__('duplicate label of equation %s, other instance in %s') %
@ -73,31 +73,27 @@ class MathDomain(Domain):
self.equations[labelid] = (docname, self.env.new_serialno('eqno') + 1) self.equations[labelid] = (docname, self.env.new_serialno('eqno') + 1)
def get_equation_number_for(self, labelid): def get_equation_number_for(self, labelid: str) -> int:
# type: (str) -> int
if labelid in self.equations: if labelid in self.equations:
return self.equations[labelid][1] return self.equations[labelid][1]
else: else:
return None return None
def process_doc(self, env, docname, document): def process_doc(self, env: BuildEnvironment, docname: str,
# type: (BuildEnvironment, str, nodes.document) -> None document: nodes.document) -> None:
def math_node(node): def math_node(node: Node) -> bool:
# type: (nodes.Node) -> bool
return isinstance(node, (nodes.math, nodes.math_block)) return isinstance(node, (nodes.math, nodes.math_block))
self.data['has_equations'][docname] = any(document.traverse(math_node)) self.data['has_equations'][docname] = any(document.traverse(math_node))
def clear_doc(self, docname): def clear_doc(self, docname: str) -> None:
# type: (str) -> None
for equation_id, (doc, eqno) in list(self.equations.items()): for equation_id, (doc, eqno) in list(self.equations.items()):
if doc == docname: if doc == docname:
del self.equations[equation_id] del self.equations[equation_id]
self.data['has_equations'].pop(docname, None) self.data['has_equations'].pop(docname, None)
def merge_domaindata(self, docnames, otherdata): def merge_domaindata(self, docnames: Iterable[str], otherdata: Dict) -> None:
# type: (Iterable[str], Dict) -> None
for labelid, (doc, eqno) in otherdata['objects'].items(): for labelid, (doc, eqno) in otherdata['objects'].items():
if doc in docnames: if doc in docnames:
self.equations[labelid] = (doc, eqno) self.equations[labelid] = (doc, eqno)
@ -105,8 +101,9 @@ class MathDomain(Domain):
for docname in docnames: for docname in docnames:
self.data['has_equations'][docname] = otherdata['has_equations'][docname] self.data['has_equations'][docname] = otherdata['has_equations'][docname]
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: "Builder",
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA typ: str, target: str, node: pending_xref, contnode: Element
) -> Element:
assert typ in ('eq', 'numref') assert typ in ('eq', 'numref')
docname, number = self.equations.get(target, (None, None)) docname, number = self.equations.get(target, (None, None))
if docname: if docname:
@ -133,20 +130,19 @@ class MathDomain(Domain):
else: else:
return None return None
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: "Builder",
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA target: str, node: pending_xref, contnode: Element
) -> List[Tuple[str, Element]]:
refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode) refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode)
if refnode is None: if refnode is None:
return [] return []
else: else:
return [('eq', refnode)] return [('eq', refnode)]
def get_objects(self): def get_objects(self) -> List:
# type: () -> List
return [] return []
def add_equation(self, env, docname, labelid): def add_equation(self, env: BuildEnvironment, docname: str, labelid: str) -> int:
# type: (BuildEnvironment, str, str) -> int
warnings.warn('MathDomain.add_equation() is deprecated.', warnings.warn('MathDomain.add_equation() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
if labelid in self.equations: if labelid in self.equations:
@ -158,20 +154,17 @@ class MathDomain(Domain):
self.equations[labelid] = (docname, eqno) self.equations[labelid] = (docname, eqno)
return eqno return eqno
def get_next_equation_number(self, docname): def get_next_equation_number(self, docname: str) -> int:
# type: (str) -> int
warnings.warn('MathDomain.get_next_equation_number() is deprecated.', warnings.warn('MathDomain.get_next_equation_number() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
targets = [eq for eq in self.equations.values() if eq[0] == docname] targets = [eq for eq in self.equations.values() if eq[0] == docname]
return len(targets) + 1 return len(targets) + 1
def has_equations(self): def has_equations(self) -> bool:
# type: () -> bool
return any(self.data['has_equations'].values()) return any(self.data['has_equations'].values())
def setup(app): def setup(app: "Sphinx") -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(MathDomain) app.add_domain(MathDomain)
app.add_role('eq', MathReferenceRole(warn_dangling=True)) app.add_role('eq', MathReferenceRole(warn_dangling=True))

View File

@ -10,31 +10,31 @@
import re import re
import warnings import warnings
from typing import Any, Dict, Iterable, Iterator, List, Tuple, Type
from typing import cast from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
from sphinx import addnodes, locale from sphinx import addnodes, locale
from sphinx.addnodes import pending_xref, desc_signature
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.deprecation import ( from sphinx.deprecation import (
DeprecatedDict, RemovedInSphinx30Warning, RemovedInSphinx40Warning DeprecatedDict, RemovedInSphinx30Warning, RemovedInSphinx40Warning
) )
from sphinx.directives import ObjectDescription from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType, Index, IndexEntry from sphinx.domains import Domain, ObjType, Index, IndexEntry
from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.docfields import Field, GroupedField, TypedField from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
from sphinx.util.typing import TextlikeNode
if False:
# For type annotation
from typing import Any, Dict, Iterable, Iterator, List, Tuple, Type # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.util.typing import TextlikeNode # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -67,8 +67,7 @@ locale.pairindextypes = DeprecatedDict(
) )
def _pseudo_parse_arglist(signode, arglist): def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None:
# type: (addnodes.desc_signature, str) -> None
""""Parse" a list of arguments separated by commas. """"Parse" a list of arguments separated by commas.
Arguments can have "optional" annotations given by enclosing them in Arguments can have "optional" annotations given by enclosing them in
@ -76,7 +75,7 @@ def _pseudo_parse_arglist(signode, arglist):
string literal (e.g. default argument value). string literal (e.g. default argument value).
""" """
paramlist = addnodes.desc_parameterlist() paramlist = addnodes.desc_parameterlist()
stack = [paramlist] # type: List[nodes.Element] stack = [paramlist] # type: List[Element]
try: try:
for argument in arglist.split(','): for argument in arglist.split(','):
argument = argument.strip() argument = argument.strip()
@ -119,15 +118,9 @@ def _pseudo_parse_arglist(signode, arglist):
# This override allows our inline type specifiers to behave like :class: link # This override allows our inline type specifiers to behave like :class: link
# when it comes to handling "." and "~" prefixes. # when it comes to handling "." and "~" prefixes.
class PyXrefMixin: class PyXrefMixin:
def make_xref(self, def make_xref(self, rolename: str, domain: str, target: str,
rolename, # type: str innernode: Type[TextlikeNode] = nodes.emphasis,
domain, # type: str contnode: Node = None, env: BuildEnvironment = None) -> Node:
target, # type: str
innernode=nodes.emphasis, # type: Type[TextlikeNode]
contnode=None, # type: nodes.Node
env=None, # type: BuildEnvironment
):
# type: (...) -> nodes.Node
result = super().make_xref(rolename, domain, target, # type: ignore result = super().make_xref(rolename, domain, target, # type: ignore
innernode, contnode, env) innernode, contnode, env)
result['refspecific'] = True result['refspecific'] = True
@ -142,15 +135,9 @@ class PyXrefMixin:
break break
return result return result
def make_xrefs(self, def make_xrefs(self, rolename: str, domain: str, target: str,
rolename, # type: str innernode: Type[TextlikeNode] = nodes.emphasis,
domain, # type: str contnode: Node = None, env: BuildEnvironment = None) -> List[Node]:
target, # type: str
innernode=nodes.emphasis, # type: Type[TextlikeNode]
contnode=None, # type: nodes.Node
env=None, # type: BuildEnvironment
):
# type: (...) -> List[nodes.Node]
delims = r'(\s*[\[\]\(\),](?:\s*or\s)?\s*|\s+or\s+)' delims = r'(\s*[\[\]\(\),](?:\s*or\s)?\s*|\s+or\s+)'
delims_re = re.compile(delims) delims_re = re.compile(delims)
sub_targets = re.split(delims, target) sub_targets = re.split(delims, target)
@ -172,9 +159,9 @@ class PyXrefMixin:
class PyField(PyXrefMixin, Field): class PyField(PyXrefMixin, Field):
def make_xref(self, rolename, domain, target, def make_xref(self, rolename: str, domain: str, target: str,
innernode=nodes.emphasis, contnode=None, env=None): innernode: Type[TextlikeNode] = nodes.emphasis,
# type: (str, str, str, Type[TextlikeNode], nodes.Node, BuildEnvironment) -> nodes.Node # NOQA contnode: Node = None, env: BuildEnvironment = None) -> Node:
if rolename == 'class' and target == 'None': if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead. # None is not a type, so use obj role instead.
rolename = 'obj' rolename = 'obj'
@ -187,9 +174,9 @@ class PyGroupedField(PyXrefMixin, GroupedField):
class PyTypedField(PyXrefMixin, TypedField): class PyTypedField(PyXrefMixin, TypedField):
def make_xref(self, rolename, domain, target, def make_xref(self, rolename: str, domain: str, target: str,
innernode=nodes.emphasis, contnode=None, env=None): innernode: Type[TextlikeNode] = nodes.emphasis,
# type: (str, str, str, Type[TextlikeNode], nodes.Node, BuildEnvironment) -> nodes.Node # NOQA contnode: Node = None, env: BuildEnvironment = None) -> Node:
if rolename == 'class' and target == 'None': if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead. # None is not a type, so use obj role instead.
rolename = 'obj' rolename = 'obj'
@ -231,22 +218,19 @@ class PyObject(ObjectDescription):
allow_nesting = False allow_nesting = False
def get_signature_prefix(self, sig): def get_signature_prefix(self, sig: str) -> str:
# type: (str) -> str
"""May return a prefix to put before the object name in the """May return a prefix to put before the object name in the
signature. signature.
""" """
return '' return ''
def needs_arglist(self): def needs_arglist(self) -> bool:
# type: () -> bool
"""May return true if an empty argument list is to be generated even if """May return true if an empty argument list is to be generated even if
the document contains none. the document contains none.
""" """
return False return False
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
# type: (str, addnodes.desc_signature) -> Tuple[str, str]
"""Transform a Python signature into RST nodes. """Transform a Python signature into RST nodes.
Return (fully qualified name of the thing, classname if any). Return (fully qualified name of the thing, classname if any).
@ -320,13 +304,12 @@ class PyObject(ObjectDescription):
return fullname, prefix return fullname, prefix
def get_index_text(self, modname, name): def get_index_text(self, modname: str, name: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
"""Return the text for the index entry of the object.""" """Return the text for the index entry of the object."""
raise NotImplementedError('must be implemented in subclasses') raise NotImplementedError('must be implemented in subclasses')
def add_target_and_index(self, name_cls, sig, signode): def add_target_and_index(self, name_cls: Tuple[str, str], sig: str,
# type: (Tuple[str, str], str, addnodes.desc_signature) -> None signode: desc_signature) -> None:
modname = self.options.get('module', self.env.ref_context.get('py:module')) modname = self.options.get('module', self.env.ref_context.get('py:module'))
fullname = (modname and modname + '.' or '') + name_cls[0] fullname = (modname and modname + '.' or '') + name_cls[0]
# note target # note target
@ -345,8 +328,7 @@ class PyObject(ObjectDescription):
self.indexnode['entries'].append(('single', indextext, self.indexnode['entries'].append(('single', indextext,
fullname, '', None)) fullname, '', None))
def before_content(self): def before_content(self) -> None:
# type: () -> None
"""Handle object nesting before content """Handle object nesting before content
:py:class:`PyObject` represents Python language constructs. For :py:class:`PyObject` represents Python language constructs. For
@ -379,8 +361,7 @@ class PyObject(ObjectDescription):
modules.append(self.env.ref_context.get('py:module')) modules.append(self.env.ref_context.get('py:module'))
self.env.ref_context['py:module'] = self.options['module'] self.env.ref_context['py:module'] = self.options['module']
def after_content(self): def after_content(self) -> None:
# type: () -> None
"""Handle object de-nesting after content """Handle object de-nesting after content
If this class is a nestable object, removing the last nested class prefix If this class is a nestable object, removing the last nested class prefix
@ -411,19 +392,16 @@ class PyModulelevel(PyObject):
Description of an object on module level (functions, data). Description of an object on module level (functions, data).
""" """
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
warnings.warn('PyClassmember is deprecated.', warnings.warn('PyClassmember is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
return super().run() return super().run()
def needs_arglist(self): def needs_arglist(self) -> bool:
# type: () -> bool
return self.objtype == 'function' return self.objtype == 'function'
def get_index_text(self, modname, name_cls): def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
if self.objtype == 'function': if self.objtype == 'function':
if not modname: if not modname:
return _('%s() (built-in function)') % name_cls[0] return _('%s() (built-in function)') % name_cls[0]
@ -444,19 +422,16 @@ class PyFunction(PyObject):
'async': directives.flag, 'async': directives.flag,
}) })
def get_signature_prefix(self, sig): def get_signature_prefix(self, sig: str) -> str:
# type: (str) -> str
if 'async' in self.options: if 'async' in self.options:
return 'async ' return 'async '
else: else:
return '' return ''
def needs_arglist(self): def needs_arglist(self) -> bool:
# type: () -> bool
return True return True
def get_index_text(self, modname, name_cls): def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
name, cls = name_cls name, cls = name_cls
if modname: if modname:
return _('%s() (in module %s)') % (name, modname) return _('%s() (in module %s)') % (name, modname)
@ -467,8 +442,7 @@ class PyFunction(PyObject):
class PyVariable(PyObject): class PyVariable(PyObject):
"""Description of a variable.""" """Description of a variable."""
def get_index_text(self, modname, name_cls): def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
name, cls = name_cls name, cls = name_cls
if modname: if modname:
return _('%s (in module %s)') % (name, modname) return _('%s (in module %s)') % (name, modname)
@ -483,12 +457,10 @@ class PyClasslike(PyObject):
allow_nesting = True allow_nesting = True
def get_signature_prefix(self, sig): def get_signature_prefix(self, sig: str) -> str:
# type: (str) -> str
return self.objtype + ' ' return self.objtype + ' '
def get_index_text(self, modname, name_cls): def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
if self.objtype == 'class': if self.objtype == 'class':
if not modname: if not modname:
return _('%s (built-in class)') % name_cls[0] return _('%s (built-in class)') % name_cls[0]
@ -504,27 +476,23 @@ class PyClassmember(PyObject):
Description of a class member (methods, attributes). Description of a class member (methods, attributes).
""" """
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
warnings.warn('PyClassmember is deprecated.', warnings.warn('PyClassmember is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
return super().run() return super().run()
def needs_arglist(self): def needs_arglist(self) -> bool:
# type: () -> bool
return self.objtype.endswith('method') return self.objtype.endswith('method')
def get_signature_prefix(self, sig): def get_signature_prefix(self, sig: str) -> str:
# type: (str) -> str
if self.objtype == 'staticmethod': if self.objtype == 'staticmethod':
return 'static ' return 'static '
elif self.objtype == 'classmethod': elif self.objtype == 'classmethod':
return 'classmethod ' return 'classmethod '
return '' return ''
def get_index_text(self, modname, name_cls): def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
name, cls = name_cls name, cls = name_cls
add_modules = self.env.config.add_module_names add_modules = self.env.config.add_module_names
if self.objtype == 'method': if self.objtype == 'method':
@ -593,15 +561,13 @@ class PyMethod(PyObject):
'staticmethod': directives.flag, 'staticmethod': directives.flag,
}) })
def needs_arglist(self): def needs_arglist(self) -> bool:
# type: () -> bool
if 'property' in self.options: if 'property' in self.options:
return False return False
else: else:
return True return True
def get_signature_prefix(self, sig): def get_signature_prefix(self, sig: str) -> str:
# type: (str) -> str
prefix = [] prefix = []
if 'abstractmethod' in self.options: if 'abstractmethod' in self.options:
prefix.append('abstract') prefix.append('abstract')
@ -619,8 +585,7 @@ class PyMethod(PyObject):
else: else:
return '' return ''
def get_index_text(self, modname, name_cls): def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
name, cls = name_cls name, cls = name_cls
try: try:
clsname, methname = name.rsplit('.', 1) clsname, methname = name.rsplit('.', 1)
@ -647,8 +612,7 @@ class PyClassMethod(PyMethod):
option_spec = PyObject.option_spec.copy() option_spec = PyObject.option_spec.copy()
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
self.name = 'py:method' self.name = 'py:method'
self.options['classmethod'] = True self.options['classmethod'] = True
@ -660,8 +624,7 @@ class PyStaticMethod(PyMethod):
option_spec = PyObject.option_spec.copy() option_spec = PyObject.option_spec.copy()
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
self.name = 'py:method' self.name = 'py:method'
self.options['staticmethod'] = True self.options['staticmethod'] = True
@ -671,8 +634,7 @@ class PyStaticMethod(PyMethod):
class PyAttribute(PyObject): class PyAttribute(PyObject):
"""Description of an attribute.""" """Description of an attribute."""
def get_index_text(self, modname, name_cls): def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
# type: (str, Tuple[str, str]) -> str
name, cls = name_cls name, cls = name_cls
try: try:
clsname, attrname = name.rsplit('.', 1) clsname, attrname = name.rsplit('.', 1)
@ -691,14 +653,12 @@ class PyDecoratorMixin:
""" """
Mixin for decorator directives. Mixin for decorator directives.
""" """
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
# type: (str, addnodes.desc_signature) -> Tuple[str, str]
ret = super().handle_signature(sig, signode) # type: ignore ret = super().handle_signature(sig, signode) # type: ignore
signode.insert(0, addnodes.desc_addname('@', '@')) signode.insert(0, addnodes.desc_addname('@', '@'))
return ret return ret
def needs_arglist(self): def needs_arglist(self) -> bool:
# type: () -> bool
return False return False
@ -706,8 +666,7 @@ class PyDecoratorFunction(PyDecoratorMixin, PyModulelevel):
""" """
Directive to mark functions meant to be used as decorators. Directive to mark functions meant to be used as decorators.
""" """
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
# a decorator function is a function after all # a decorator function is a function after all
self.name = 'py:function' self.name = 'py:function'
return super().run() return super().run()
@ -717,8 +676,7 @@ class PyDecoratorMethod(PyDecoratorMixin, PyClassmember):
""" """
Directive to mark methods meant to be used as decorators. Directive to mark methods meant to be used as decorators.
""" """
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
self.name = 'py:method' self.name = 'py:method'
return super().run() return super().run()
@ -739,14 +697,13 @@ class PyModule(SphinxDirective):
'deprecated': directives.flag, 'deprecated': directives.flag,
} }
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
domain = cast(PythonDomain, self.env.get_domain('py')) domain = cast(PythonDomain, self.env.get_domain('py'))
modname = self.arguments[0].strip() modname = self.arguments[0].strip()
noindex = 'noindex' in self.options noindex = 'noindex' in self.options
self.env.ref_context['py:module'] = modname self.env.ref_context['py:module'] = modname
ret = [] # type: List[nodes.Node] ret = [] # type: List[Node]
if not noindex: if not noindex:
# note module to the domain # note module to the domain
domain.note_module(modname, domain.note_module(modname,
@ -780,8 +737,7 @@ class PyCurrentModule(SphinxDirective):
final_argument_whitespace = False final_argument_whitespace = False
option_spec = {} # type: Dict option_spec = {} # type: Dict
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
modname = self.arguments[0].strip() modname = self.arguments[0].strip()
if modname == 'None': if modname == 'None':
self.env.ref_context.pop('py:module', None) self.env.ref_context.pop('py:module', None)
@ -791,8 +747,8 @@ class PyCurrentModule(SphinxDirective):
class PyXRefRole(XRefRole): class PyXRefRole(XRefRole):
def process_link(self, env, refnode, has_explicit_title, title, target): def process_link(self, env: BuildEnvironment, refnode: Element,
# type: (BuildEnvironment, nodes.Element, bool, str, str) -> Tuple[str, str] has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
refnode['py:module'] = env.ref_context.get('py:module') refnode['py:module'] = env.ref_context.get('py:module')
refnode['py:class'] = env.ref_context.get('py:class') refnode['py:class'] = env.ref_context.get('py:class')
if not has_explicit_title: if not has_explicit_title:
@ -822,8 +778,8 @@ class PythonModuleIndex(Index):
localname = _('Python Module Index') localname = _('Python Module Index')
shortname = _('modules') shortname = _('modules')
def generate(self, docnames=None): def generate(self, docnames: Iterable[str] = None
# type: (Iterable[str]) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool] ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
content = {} # type: Dict[str, List[IndexEntry]] content = {} # type: Dict[str, List[IndexEntry]]
# list of prefixes to ignore # list of prefixes to ignore
ignores = None # type: List[str] ignores = None # type: List[str]
@ -937,12 +893,10 @@ class PythonDomain(Domain):
] ]
@property @property
def objects(self): def objects(self) -> Dict[str, Tuple[str, str]]:
# type: () -> Dict[str, Tuple[str, str]]
return self.data.setdefault('objects', {}) # fullname -> docname, objtype return self.data.setdefault('objects', {}) # fullname -> docname, objtype
def note_object(self, name, objtype, location=None): def note_object(self, name: str, objtype: str, location: Any = None) -> None:
# type: (str, str, Any) -> None
"""Note a python object for cross reference. """Note a python object for cross reference.
.. versionadded:: 2.1 .. versionadded:: 2.1
@ -955,20 +909,17 @@ class PythonDomain(Domain):
self.objects[name] = (self.env.docname, objtype) self.objects[name] = (self.env.docname, objtype)
@property @property
def modules(self): def modules(self) -> Dict[str, Tuple[str, str, str, bool]]:
# type: () -> Dict[str, Tuple[str, str, str, bool]]
return self.data.setdefault('modules', {}) # modname -> docname, synopsis, platform, deprecated # NOQA return self.data.setdefault('modules', {}) # modname -> docname, synopsis, platform, deprecated # NOQA
def note_module(self, name, synopsis, platform, deprecated): def note_module(self, name: str, synopsis: str, platform: str, deprecated: bool) -> None:
# type: (str, str, str, bool) -> None
"""Note a python module for cross reference. """Note a python module for cross reference.
.. versionadded:: 2.1 .. versionadded:: 2.1
""" """
self.modules[name] = (self.env.docname, synopsis, platform, deprecated) self.modules[name] = (self.env.docname, synopsis, platform, deprecated)
def clear_doc(self, docname): def clear_doc(self, docname: str) -> None:
# type: (str) -> None
for fullname, (fn, _l) in list(self.objects.items()): for fullname, (fn, _l) in list(self.objects.items()):
if fn == docname: if fn == docname:
del self.objects[fullname] del self.objects[fullname]
@ -976,8 +927,7 @@ class PythonDomain(Domain):
if fn == docname: if fn == docname:
del self.modules[modname] del self.modules[modname]
def merge_domaindata(self, docnames, otherdata): def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# type: (List[str], Dict) -> None
# XXX check duplicates? # XXX check duplicates?
for fullname, (fn, objtype) in otherdata['objects'].items(): for fullname, (fn, objtype) in otherdata['objects'].items():
if fn in docnames: if fn in docnames:
@ -986,8 +936,8 @@ class PythonDomain(Domain):
if data[0] in docnames: if data[0] in docnames:
self.modules[modname] = data self.modules[modname] = data
def find_obj(self, env, modname, classname, name, type, searchmode=0): def find_obj(self, env: BuildEnvironment, modname: str, classname: str,
# type: (BuildEnvironment, str, str, str, str, int) -> List[Tuple[str, Any]] name: str, type: str, searchmode: int = 0) -> List[Tuple[str, Any]]:
"""Find a Python object for "name", perhaps using the given module """Find a Python object for "name", perhaps using the given module
and/or classname. Returns a list of (name, object entry) tuples. and/or classname. Returns a list of (name, object entry) tuples.
""" """
@ -1049,9 +999,9 @@ class PythonDomain(Domain):
matches.append((newname, self.objects[newname])) matches.append((newname, self.objects[newname]))
return matches return matches
def resolve_xref(self, env, fromdocname, builder, def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
type, target, node, contnode): type: str, target: str, node: pending_xref, contnode: Element
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA ) -> Element:
modname = node.get('py:module') modname = node.get('py:module')
clsname = node.get('py:class') clsname = node.get('py:class')
searchmode = node.hasattr('refspecific') and 1 or 0 searchmode = node.hasattr('refspecific') and 1 or 0
@ -1070,12 +1020,12 @@ class PythonDomain(Domain):
else: else:
return make_refnode(builder, fromdocname, obj[0], name, contnode, name) return make_refnode(builder, fromdocname, obj[0], name, contnode, name)
def resolve_any_xref(self, env, fromdocname, builder, target, def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
node, contnode): target: str, node: pending_xref, contnode: Element
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA ) -> List[Tuple[str, Element]]:
modname = node.get('py:module') modname = node.get('py:module')
clsname = node.get('py:class') clsname = node.get('py:class')
results = [] # type: List[Tuple[str, nodes.Element]] results = [] # type: List[Tuple[str, Element]]
# always search in "refspecific" mode with the :any: role # always search in "refspecific" mode with the :any: role
matches = self.find_obj(env, modname, clsname, target, None, 1) matches = self.find_obj(env, modname, clsname, target, None, 1)
@ -1090,8 +1040,8 @@ class PythonDomain(Domain):
contnode, name))) contnode, name)))
return results return results
def _make_module_refnode(self, builder, fromdocname, name, contnode): def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str,
# type: (Builder, str, str, nodes.Node) -> nodes.Element contnode: Node) -> Element:
# get additional info for modules # get additional info for modules
docname, synopsis, platform, deprecated = self.modules[name] docname, synopsis, platform, deprecated = self.modules[name]
title = name title = name
@ -1104,16 +1054,14 @@ class PythonDomain(Domain):
return make_refnode(builder, fromdocname, docname, return make_refnode(builder, fromdocname, docname,
'module-' + name, contnode, title) 'module-' + name, contnode, title)
def get_objects(self): def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
# type: () -> Iterator[Tuple[str, str, str, str, str, int]]
for modname, info in self.modules.items(): for modname, info in self.modules.items():
yield (modname, modname, 'module', info[0], 'module-' + modname, 0) yield (modname, modname, 'module', info[0], 'module-' + modname, 0)
for refname, (docname, type) in self.objects.items(): for refname, (docname, type) in self.objects.items():
if type != 'module': # modules are already handled if type != 'module': # modules are already handled
yield (refname, refname, type, docname, refname, 1) yield (refname, refname, type, docname, refname, 1)
def get_full_qualified_name(self, node): def get_full_qualified_name(self, node: Element) -> str:
# type: (nodes.Element) -> str
modname = node.get('py:module') modname = node.get('py:module')
clsname = node.get('py:class') clsname = node.get('py:class')
target = node.get('reftarget') target = node.get('reftarget')
@ -1123,8 +1071,7 @@ class PythonDomain(Domain):
return '.'.join(filter(None, [modname, clsname, target])) return '.'.join(filter(None, [modname, clsname, target]))
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(PythonDomain) app.add_domain(PythonDomain)
return { return {

View File

@ -9,26 +9,24 @@
""" """
import re import re
from typing import Any, Dict, Iterator, List, Tuple
from typing import cast from typing import cast
from docutils.nodes import Element
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
from sphinx import addnodes from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.directives import ObjectDescription from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType from sphinx.domains import Domain, ObjType
from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
if False:
# For type annotation
from typing import Any, Dict, Iterator, List, Tuple # NOQA
from docutils import nodes # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -40,8 +38,7 @@ class ReSTMarkup(ObjectDescription):
Description of generic reST markup. Description of generic reST markup.
""" """
def add_target_and_index(self, name, sig, signode): def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:
# type: (str, str, addnodes.desc_signature) -> None
targetname = self.objtype + '-' + name targetname = self.objtype + '-' + name
if targetname not in self.state.document.ids: if targetname not in self.state.document.ids:
signode['names'].append(targetname) signode['names'].append(targetname)
@ -57,13 +54,11 @@ class ReSTMarkup(ObjectDescription):
self.indexnode['entries'].append(('single', indextext, self.indexnode['entries'].append(('single', indextext,
targetname, '', None)) targetname, '', None))
def get_index_text(self, objectname, name): def get_index_text(self, objectname: str, name: str) -> str:
# type: (str, str) -> str
return '' return ''
def parse_directive(d): def parse_directive(d: str) -> Tuple[str, str]:
# type: (str) -> Tuple[str, str]
"""Parse a directive signature. """Parse a directive signature.
Returns (directive, arguments) string tuple. If no arguments are given, Returns (directive, arguments) string tuple. If no arguments are given,
@ -87,8 +82,7 @@ class ReSTDirective(ReSTMarkup):
""" """
Description of a reST directive. Description of a reST directive.
""" """
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> str:
# type: (str, addnodes.desc_signature) -> str
name, args = parse_directive(sig) name, args = parse_directive(sig)
desc_name = '.. %s::' % name desc_name = '.. %s::' % name
signode += addnodes.desc_name(desc_name, desc_name) signode += addnodes.desc_name(desc_name, desc_name)
@ -96,18 +90,15 @@ class ReSTDirective(ReSTMarkup):
signode += addnodes.desc_addname(args, args) signode += addnodes.desc_addname(args, args)
return name return name
def get_index_text(self, objectname, name): def get_index_text(self, objectname: str, name: str) -> str:
# type: (str, str) -> str
return _('%s (directive)') % name return _('%s (directive)') % name
def before_content(self): def before_content(self) -> None:
# type: () -> None
if self.names: if self.names:
directives = self.env.ref_context.setdefault('rst:directives', []) directives = self.env.ref_context.setdefault('rst:directives', [])
directives.append(self.names[0]) directives.append(self.names[0])
def after_content(self): def after_content(self) -> None:
# type: () -> None
directives = self.env.ref_context.setdefault('rst:directives', []) directives = self.env.ref_context.setdefault('rst:directives', [])
if directives: if directives:
directives.pop() directives.pop()
@ -122,8 +113,7 @@ class ReSTDirectiveOption(ReSTMarkup):
'type': directives.unchanged, 'type': directives.unchanged,
}) })
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> str:
# type: (str, addnodes.desc_signature) -> str
try: try:
name, argument = re.split(r'\s*:\s+', sig.strip(), 1) name, argument = re.split(r'\s*:\s+', sig.strip(), 1)
except ValueError: except ValueError:
@ -137,8 +127,7 @@ class ReSTDirectiveOption(ReSTMarkup):
signode += addnodes.desc_annotation(text, text) signode += addnodes.desc_annotation(text, text)
return name return name
def add_target_and_index(self, name, sig, signode): def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:
# type: (str, str, addnodes.desc_signature) -> None
directive_name = self.current_directive directive_name = self.current_directive
targetname = '-'.join([self.objtype, self.current_directive, name]) targetname = '-'.join([self.objtype, self.current_directive, name])
if targetname not in self.state.document.ids: if targetname not in self.state.document.ids:
@ -162,8 +151,7 @@ class ReSTDirectiveOption(ReSTMarkup):
self.indexnode['entries'].append(('single', text, targetname, '', key)) self.indexnode['entries'].append(('single', text, targetname, '', key))
@property @property
def current_directive(self): def current_directive(self) -> str:
# type: () -> str
directives = self.env.ref_context.get('rst:directives') directives = self.env.ref_context.get('rst:directives')
if directives: if directives:
return directives[-1] return directives[-1]
@ -175,13 +163,11 @@ class ReSTRole(ReSTMarkup):
""" """
Description of a reST role. Description of a reST role.
""" """
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> str:
# type: (str, addnodes.desc_signature) -> str
signode += addnodes.desc_name(':%s:' % sig, ':%s:' % sig) signode += addnodes.desc_name(':%s:' % sig, ':%s:' % sig)
return sig return sig
def get_index_text(self, objectname, name): def get_index_text(self, objectname: str, name: str) -> str:
# type: (str, str) -> str
return _('%s (role)') % name return _('%s (role)') % name
@ -209,12 +195,10 @@ class ReSTDomain(Domain):
} # type: Dict[str, Dict[Tuple[str, str], str]] } # type: Dict[str, Dict[Tuple[str, str], str]]
@property @property
def objects(self): def objects(self) -> Dict[Tuple[str, str], str]:
# type: () -> Dict[Tuple[str, str], str]
return self.data.setdefault('objects', {}) # (objtype, fullname) -> docname return self.data.setdefault('objects', {}) # (objtype, fullname) -> docname
def note_object(self, objtype, name, location=None): def note_object(self, objtype: str, name: str, location: Any = None) -> None:
# type: (str, str, Any) -> None
if (objtype, name) in self.objects: if (objtype, name) in self.objects:
docname = self.objects[objtype, name] docname = self.objects[objtype, name]
logger.warning(__('duplicate description of %s %s, other instance in %s') % logger.warning(__('duplicate description of %s %s, other instance in %s') %
@ -222,21 +206,20 @@ class ReSTDomain(Domain):
self.objects[objtype, name] = self.env.docname self.objects[objtype, name] = self.env.docname
def clear_doc(self, docname): def clear_doc(self, docname: str) -> None:
# type: (str) -> None
for (typ, name), doc in list(self.objects.items()): for (typ, name), doc in list(self.objects.items()):
if doc == docname: if doc == docname:
del self.objects[typ, name] del self.objects[typ, name]
def merge_domaindata(self, docnames, otherdata): def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# type: (List[str], Dict) -> None
# XXX check duplicates # XXX check duplicates
for (typ, name), doc in otherdata['objects'].items(): for (typ, name), doc in otherdata['objects'].items():
if doc in docnames: if doc in docnames:
self.objects[typ, name] = doc self.objects[typ, name] = doc
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA typ: str, target: str, node: pending_xref, contnode: Element
) -> Element:
objtypes = self.objtypes_for_role(typ) objtypes = self.objtypes_for_role(typ)
for objtype in objtypes: for objtype in objtypes:
todocname = self.objects.get((objtype, target)) todocname = self.objects.get((objtype, target))
@ -246,9 +229,10 @@ class ReSTDomain(Domain):
contnode, target + ' ' + objtype) contnode, target + ' ' + objtype)
return None return None
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA target: str, node: pending_xref, contnode: Element
results = [] # type: List[Tuple[str, nodes.Element]] ) -> List[Tuple[str, Element]]:
results = [] # type: List[Tuple[str, Element]]
for objtype in self.object_types: for objtype in self.object_types:
todocname = self.objects.get((objtype, target)) todocname = self.objects.get((objtype, target))
if todocname: if todocname:
@ -258,14 +242,12 @@ class ReSTDomain(Domain):
contnode, target + ' ' + objtype))) contnode, target + ' ' + objtype)))
return results return results
def get_objects(self): def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
# type: () -> Iterator[Tuple[str, str, str, str, str, int]]
for (typ, name), docname in self.data['objects'].items(): for (typ, name), docname in self.data['objects'].items():
yield name, name, typ, docname, typ + '-' + name, 1 yield name, name, typ, docname, typ + '-' + name, 1
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(ReSTDomain) app.add_domain(ReSTDomain)
return { return {

View File

@ -12,13 +12,16 @@ import re
import unicodedata import unicodedata
import warnings import warnings
from copy import copy from copy import copy
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Type, Union
from typing import cast from typing import cast
from docutils import nodes from docutils import nodes
from docutils.parsers.rst import directives from docutils.nodes import Element, Node, system_message
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import StringList from docutils.statemachine import StringList
from sphinx import addnodes from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning
from sphinx.directives import ObjectDescription from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType from sphinx.domains import Domain, ObjType
@ -28,15 +31,13 @@ from sphinx.roles import XRefRole
from sphinx.util import ws_re, logging, docname_join from sphinx.util import ws_re, logging, docname_join
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import clean_astext, make_refnode from sphinx.util.nodes import clean_astext, make_refnode
from sphinx.util.typing import RoleFunction
if False: if False:
# For type annotation # For type annotation
from typing import Any, Callable, Dict, Iterable, Iterator, List, Tuple, Type, Union # NOQA from sphinx.application import Sphinx
from docutils.parsers.rst import Directive # NOQA from sphinx.builders import Builder
from sphinx.application import Sphinx # NOQA from sphinx.environment import BuildEnvironment
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.util.typing import RoleFunction # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,10 +53,9 @@ class GenericObject(ObjectDescription):
A generic x-ref directive registered with Sphinx.add_object_type(). A generic x-ref directive registered with Sphinx.add_object_type().
""" """
indextemplate = '' indextemplate = ''
parse_node = None # type: Callable[[GenericObject, BuildEnvironment, str, addnodes.desc_signature], str] # NOQA parse_node = None # type: Callable[[GenericObject, BuildEnvironment, str, desc_signature], str] # NOQA
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> str:
# type: (str, addnodes.desc_signature) -> str
if self.parse_node: if self.parse_node:
name = self.parse_node(self.env, sig, signode) name = self.parse_node(self.env, sig, signode)
else: else:
@ -65,8 +65,7 @@ class GenericObject(ObjectDescription):
name = ws_re.sub('', sig) name = ws_re.sub('', sig)
return name return name
def add_target_and_index(self, name, sig, signode): def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:
# type: (str, str, addnodes.desc_signature) -> None
targetname = '%s-%s' % (self.objtype, name) targetname = '%s-%s' % (self.objtype, name)
signode['ids'].append(targetname) signode['ids'].append(targetname)
self.state.document.note_explicit_target(signode) self.state.document.note_explicit_target(signode)
@ -94,8 +93,8 @@ class EnvVarXRefRole(XRefRole):
Cross-referencing role for environment variables (adds an index entry). Cross-referencing role for environment variables (adds an index entry).
""" """
def result_nodes(self, document, env, node, is_ref): def result_nodes(self, document: nodes.document, env: "BuildEnvironment", node: Element,
# type: (nodes.document, BuildEnvironment, nodes.Element, bool) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA is_ref: bool) -> Tuple[List[Node], List[system_message]]:
if not is_ref: if not is_ref:
return [node], [] return [node], []
varname = node['reftarget'] varname = node['reftarget']
@ -122,8 +121,7 @@ class Target(SphinxDirective):
final_argument_whitespace = True final_argument_whitespace = True
option_spec = {} # type: Dict option_spec = {} # type: Dict
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
# normalize whitespace in fullname like XRefRole does # normalize whitespace in fullname like XRefRole does
fullname = ws_re.sub(' ', self.arguments[0].strip()) fullname = ws_re.sub(' ', self.arguments[0].strip())
targetname = '%s-%s' % (self.name, fullname) targetname = '%s-%s' % (self.name, fullname)
@ -155,8 +153,7 @@ class Cmdoption(ObjectDescription):
Description of a command-line option (.. option). Description of a command-line option (.. option).
""" """
def handle_signature(self, sig, signode): def handle_signature(self, sig: str, signode: desc_signature) -> str:
# type: (str, addnodes.desc_signature) -> str
"""Transform an option description into RST nodes.""" """Transform an option description into RST nodes."""
count = 0 count = 0
firstname = '' firstname = ''
@ -184,8 +181,7 @@ class Cmdoption(ObjectDescription):
raise ValueError raise ValueError
return firstname return firstname
def add_target_and_index(self, firstname, sig, signode): def add_target_and_index(self, firstname: str, sig: str, signode: desc_signature) -> None:
# type: (str, str, addnodes.desc_signature) -> None
currprogram = self.env.ref_context.get('std:program') currprogram = self.env.ref_context.get('std:program')
for optname in signode.get('allnames', []): for optname in signode.get('allnames', []):
targetname = optname.replace('/', '-') targetname = optname.replace('/', '-')
@ -223,8 +219,7 @@ class Program(SphinxDirective):
final_argument_whitespace = True final_argument_whitespace = True
option_spec = {} # type: Dict option_spec = {} # type: Dict
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
program = ws_re.sub('-', self.arguments[0].strip()) program = ws_re.sub('-', self.arguments[0].strip())
if program == 'None': if program == 'None':
self.env.ref_context.pop('std:program', None) self.env.ref_context.pop('std:program', None)
@ -234,21 +229,20 @@ class Program(SphinxDirective):
class OptionXRefRole(XRefRole): class OptionXRefRole(XRefRole):
def process_link(self, env, refnode, has_explicit_title, title, target): def process_link(self, env: "BuildEnvironment", refnode: Element, has_explicit_title: bool,
# type: (BuildEnvironment, nodes.Element, bool, str, str) -> Tuple[str, str] title: str, target: str) -> Tuple[str, str]:
refnode['std:program'] = env.ref_context.get('std:program') refnode['std:program'] = env.ref_context.get('std:program')
return title, target return title, target
def split_term_classifiers(line): def split_term_classifiers(line: str) -> List[Optional[str]]:
# type: (str) -> List[Union[str, None]]
# split line into a term and classifiers. if no classifier, None is used.. # split line into a term and classifiers. if no classifier, None is used..
parts = re.split(' +: +', line) + [None] parts = re.split(' +: +', line) + [None]
return parts return parts
def make_glossary_term(env, textnodes, index_key, source, lineno, new_id=None): def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index_key: str,
# type: (BuildEnvironment, Iterable[nodes.Node], str, str, int, str) -> nodes.term source: str, lineno: int, new_id: str = None) -> nodes.term:
# get a text-only representation of the term and register it # get a text-only representation of the term and register it
# as a cross-reference target # as a cross-reference target
term = nodes.term('', '', *textnodes) term = nodes.term('', '', *textnodes)
@ -294,8 +288,7 @@ class Glossary(SphinxDirective):
'sorted': directives.flag, 'sorted': directives.flag,
} }
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
node = addnodes.glossary() node = addnodes.glossary()
node.document = self.state.document node.document = self.state.document
@ -401,16 +394,15 @@ class Glossary(SphinxDirective):
return messages + [node] return messages + [node]
def token_xrefs(text): def token_xrefs(text: str) -> List[Node]:
# type: (str) -> List[nodes.Node]
retnodes = [] # type: List[nodes.Node] retnodes = [] # type: List[nodes.Node]
pos = 0 pos = 0
for m in token_re.finditer(text): for m in token_re.finditer(text):
if m.start() > pos: if m.start() > pos:
txt = text[pos:m.start()] txt = text[pos:m.start()]
retnodes.append(nodes.Text(txt, txt)) retnodes.append(nodes.Text(txt, txt))
refnode = addnodes.pending_xref( refnode = pending_xref(m.group(1), reftype='token', refdomain='std',
m.group(1), reftype='token', refdomain='std', reftarget=m.group(1)) reftarget=m.group(1))
refnode += nodes.literal(m.group(1), m.group(1), classes=['xref']) refnode += nodes.literal(m.group(1), m.group(1), classes=['xref'])
retnodes.append(refnode) retnodes.append(refnode)
pos = m.end() pos = m.end()
@ -430,8 +422,7 @@ class ProductionList(SphinxDirective):
final_argument_whitespace = True final_argument_whitespace = True
option_spec = {} # type: Dict option_spec = {} # type: Dict
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
domain = cast(StandardDomain, self.env.get_domain('std')) domain = cast(StandardDomain, self.env.get_domain('std'))
node = addnodes.productionlist() # type: nodes.Element node = addnodes.productionlist() # type: nodes.Element
i = 0 i = 0
@ -536,8 +527,7 @@ class StandardDomain(Domain):
nodes.container: ('code-block', None), nodes.container: ('code-block', None),
} # type: Dict[Type[nodes.Node], Tuple[str, Callable]] } # type: Dict[Type[nodes.Node], Tuple[str, Callable]]
def __init__(self, env): def __init__(self, env: "BuildEnvironment") -> None:
# type: (BuildEnvironment) -> None
super().__init__(env) super().__init__(env)
# set up enumerable nodes # set up enumerable nodes
@ -546,27 +536,22 @@ class StandardDomain(Domain):
self.enumerable_nodes[node] = settings self.enumerable_nodes[node] = settings
@property @property
def objects(self): def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]:
# type: () -> Dict[Tuple[str, str], Tuple[str, str]]
return self.data.setdefault('objects', {}) # (objtype, name) -> docname, labelid return self.data.setdefault('objects', {}) # (objtype, name) -> docname, labelid
@property @property
def progoptions(self): def progoptions(self) -> Dict[Tuple[str, str], Tuple[str, str]]:
# type: () -> Dict[Tuple[str, str], Tuple[str, str]]
return self.data.setdefault('progoptions', {}) # (program, name) -> docname, labelid return self.data.setdefault('progoptions', {}) # (program, name) -> docname, labelid
@property @property
def labels(self): def labels(self) -> Dict[str, Tuple[str, str, str]]:
# type: () -> Dict[str, Tuple[str, str, str]]
return self.data.setdefault('labels', {}) # labelname -> docname, labelid, sectionname return self.data.setdefault('labels', {}) # labelname -> docname, labelid, sectionname
@property @property
def anonlabels(self): def anonlabels(self) -> Dict[str, Tuple[str, str]]:
# type: () -> Dict[str, Tuple[str, str]]
return self.data.setdefault('anonlabels', {}) # labelname -> docname, labelid return self.data.setdefault('anonlabels', {}) # labelname -> docname, labelid
def clear_doc(self, docname): def clear_doc(self, docname: str) -> None:
# type: (str) -> None
key = None # type: Any key = None # type: Any
for key, (fn, _l) in list(self.progoptions.items()): for key, (fn, _l) in list(self.progoptions.items()):
if fn == docname: if fn == docname:
@ -581,8 +566,7 @@ class StandardDomain(Domain):
if fn == docname: if fn == docname:
del self.anonlabels[key] del self.anonlabels[key]
def merge_domaindata(self, docnames, otherdata): def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# type: (List[str], Dict) -> None
# XXX duplicates? # XXX duplicates?
for key, data in otherdata['progoptions'].items(): for key, data in otherdata['progoptions'].items():
if data[0] in docnames: if data[0] in docnames:
@ -597,8 +581,7 @@ class StandardDomain(Domain):
if data[0] in docnames: if data[0] in docnames:
self.anonlabels[key] = data self.anonlabels[key] = data
def process_doc(self, env, docname, document): def process_doc(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA
# type: (BuildEnvironment, str, nodes.document) -> None
for name, explicit in document.nametypes.items(): for name, explicit in document.nametypes.items():
if not explicit: if not explicit:
continue continue
@ -639,17 +622,15 @@ class StandardDomain(Domain):
continue continue
self.labels[name] = docname, labelid, sectname self.labels[name] = docname, labelid, sectname
def add_object(self, objtype, name, docname, labelid): def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None:
# type: (str, str, str, str) -> None
self.objects[objtype, name] = (docname, labelid) self.objects[objtype, name] = (docname, labelid)
def add_program_option(self, program, name, docname, labelid): def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None:
# type: (str, str, str, str) -> None
self.progoptions[program, name] = (docname, labelid) self.progoptions[program, name] = (docname, labelid)
def build_reference_node(self, fromdocname, builder, docname, labelid, def build_reference_node(self, fromdocname: str, builder: "Builder", docname: str,
sectname, rolename, **options): labelid: str, sectname: str, rolename: str, **options
# type: (str, Builder, str, str, str, str, Any) -> nodes.Element ) -> Element:
nodeclass = options.pop('nodeclass', nodes.reference) nodeclass = options.pop('nodeclass', nodes.reference)
newnode = nodeclass('', '', internal=True, **options) newnode = nodeclass('', '', internal=True, **options)
innernode = nodes.inline(sectname, sectname) innernode = nodes.inline(sectname, sectname)
@ -662,7 +643,7 @@ class StandardDomain(Domain):
# set more info in contnode; in case the # set more info in contnode; in case the
# get_relative_uri call raises NoUri, # get_relative_uri call raises NoUri,
# the builder will then have to resolve these # the builder will then have to resolve these
contnode = addnodes.pending_xref('') contnode = pending_xref('')
contnode['refdocname'] = docname contnode['refdocname'] = docname
contnode['refsectname'] = sectname contnode['refsectname'] = sectname
newnode['refuri'] = builder.get_relative_uri( newnode['refuri'] = builder.get_relative_uri(
@ -672,8 +653,8 @@ class StandardDomain(Domain):
newnode.append(innernode) newnode.append(innernode)
return newnode return newnode
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): def resolve_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder",
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA typ: str, target: str, node: pending_xref, contnode: Element) -> Element:
if typ == 'ref': if typ == 'ref':
resolver = self._resolve_ref_xref resolver = self._resolve_ref_xref
elif typ == 'numref': elif typ == 'numref':
@ -694,8 +675,9 @@ class StandardDomain(Domain):
return resolver(env, fromdocname, builder, typ, target, node, contnode) return resolver(env, fromdocname, builder, typ, target, node, contnode)
def _resolve_ref_xref(self, env, fromdocname, builder, typ, target, node, contnode): def _resolve_ref_xref(self, env: "BuildEnvironment", fromdocname: str,
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA builder: "Builder", typ: str, target: str, node: pending_xref,
contnode: Element) -> Element:
if node['refexplicit']: if node['refexplicit']:
# reference to anonymous label; the reference uses # reference to anonymous label; the reference uses
# the supplied link caption # the supplied link caption
@ -711,8 +693,9 @@ class StandardDomain(Domain):
return self.build_reference_node(fromdocname, builder, return self.build_reference_node(fromdocname, builder,
docname, labelid, sectname, 'ref') docname, labelid, sectname, 'ref')
def _resolve_numref_xref(self, env, fromdocname, builder, typ, target, node, contnode): def _resolve_numref_xref(self, env: "BuildEnvironment", fromdocname: str,
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA builder: "Builder", typ: str, target: str,
node: pending_xref, contnode: Element) -> Element:
if target in self.labels: if target in self.labels:
docname, labelid, figname = self.labels.get(target, ('', '', '')) docname, labelid, figname = self.labels.get(target, ('', '', ''))
else: else:
@ -772,8 +755,9 @@ class StandardDomain(Domain):
nodeclass=addnodes.number_reference, nodeclass=addnodes.number_reference,
title=title) title=title)
def _resolve_keyword_xref(self, env, fromdocname, builder, typ, target, node, contnode): def _resolve_keyword_xref(self, env: "BuildEnvironment", fromdocname: str,
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA builder: "Builder", typ: str, target: str,
node: pending_xref, contnode: Element) -> Element:
# keywords are oddballs: they are referenced by named labels # keywords are oddballs: they are referenced by named labels
docname, labelid, _ = self.labels.get(target, ('', '', '')) docname, labelid, _ = self.labels.get(target, ('', '', ''))
if not docname: if not docname:
@ -781,8 +765,9 @@ class StandardDomain(Domain):
return make_refnode(builder, fromdocname, docname, return make_refnode(builder, fromdocname, docname,
labelid, contnode) labelid, contnode)
def _resolve_doc_xref(self, env, fromdocname, builder, typ, target, node, contnode): def _resolve_doc_xref(self, env: "BuildEnvironment", fromdocname: str,
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA builder: "Builder", typ: str, target: str,
node: pending_xref, contnode: Element) -> Element:
# directly reference to document by source name; can be absolute or relative # directly reference to document by source name; can be absolute or relative
refdoc = node.get('refdoc', fromdocname) refdoc = node.get('refdoc', fromdocname)
docname = docname_join(refdoc, node['reftarget']) docname = docname_join(refdoc, node['reftarget'])
@ -797,8 +782,9 @@ class StandardDomain(Domain):
innernode = nodes.inline(caption, caption, classes=['doc']) innernode = nodes.inline(caption, caption, classes=['doc'])
return make_refnode(builder, fromdocname, docname, None, innernode) return make_refnode(builder, fromdocname, docname, None, innernode)
def _resolve_option_xref(self, env, fromdocname, builder, typ, target, node, contnode): def _resolve_option_xref(self, env: "BuildEnvironment", fromdocname: str,
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA builder: "Builder", typ: str, target: str,
node: pending_xref, contnode: Element) -> Element:
progname = node.get('std:program') progname = node.get('std:program')
target = target.strip() target = target.strip()
docname, labelid = self.progoptions.get((progname, target), ('', '')) docname, labelid = self.progoptions.get((progname, target), ('', ''))
@ -818,8 +804,9 @@ class StandardDomain(Domain):
return make_refnode(builder, fromdocname, docname, return make_refnode(builder, fromdocname, docname,
labelid, contnode) labelid, contnode)
def _resolve_citation_xref(self, env, fromdocname, builder, typ, target, node, contnode): def _resolve_citation_xref(self, env: "BuildEnvironment", fromdocname: str,
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA builder: "Builder", typ: str, target: str,
node: pending_xref, contnode: Element) -> Element:
warnings.warn('StandardDomain._resolve_citation_xref() is deprecated.', warnings.warn('StandardDomain._resolve_citation_xref() is deprecated.',
RemovedInSphinx30Warning) RemovedInSphinx30Warning)
docname, labelid, lineno = self.data['citations'].get(target, ('', '', 0)) docname, labelid, lineno = self.data['citations'].get(target, ('', '', 0))
@ -841,8 +828,9 @@ class StandardDomain(Domain):
del node['ids'][:] del node['ids'][:]
raise raise
def _resolve_obj_xref(self, env, fromdocname, builder, typ, target, node, contnode): def _resolve_obj_xref(self, env: "BuildEnvironment", fromdocname: str,
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA builder: "Builder", typ: str, target: str,
node: pending_xref, contnode: Element) -> Element:
objtypes = self.objtypes_for_role(typ) or [] objtypes = self.objtypes_for_role(typ) or []
for objtype in objtypes: for objtype in objtypes:
if (objtype, target) in self.objects: if (objtype, target) in self.objects:
@ -855,8 +843,9 @@ class StandardDomain(Domain):
return make_refnode(builder, fromdocname, docname, return make_refnode(builder, fromdocname, docname,
labelid, contnode) labelid, contnode)
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): def resolve_any_xref(self, env: "BuildEnvironment", fromdocname: str,
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA builder: "Builder", target: str, node: pending_xref,
contnode: Element) -> List[Tuple[str, Element]]:
results = [] # type: List[Tuple[str, nodes.Element]] results = [] # type: List[Tuple[str, nodes.Element]]
ltarget = target.lower() # :ref: lowercases its target automatically ltarget = target.lower() # :ref: lowercases its target automatically
for role in ('ref', 'option'): # do not try "keyword" for role in ('ref', 'option'): # do not try "keyword"
@ -877,8 +866,7 @@ class StandardDomain(Domain):
labelid, contnode))) labelid, contnode)))
return results return results
def get_objects(self): def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
# type: () -> Iterator[Tuple[str, str, str, str, str, int]]
# handle the special 'doc' reference here # handle the special 'doc' reference here
for doc in self.env.all_docs: for doc in self.env.all_docs:
yield (doc, clean_astext(self.env.titles[doc]), 'doc', doc, '', -1) yield (doc, clean_astext(self.env.titles[doc]), 'doc', doc, '', -1)
@ -899,34 +887,30 @@ class StandardDomain(Domain):
if name not in non_anon_labels: if name not in non_anon_labels:
yield (name, name, 'label', docname, labelid, -1) yield (name, name, 'label', docname, labelid, -1)
def get_type_name(self, type, primary=False): def get_type_name(self, type: ObjType, primary: bool = False) -> str:
# type: (ObjType, bool) -> str
# never prepend "Default" # never prepend "Default"
return type.lname return type.lname
def is_enumerable_node(self, node): def is_enumerable_node(self, node: Node) -> bool:
# type: (nodes.Node) -> bool
return node.__class__ in self.enumerable_nodes return node.__class__ in self.enumerable_nodes
def get_numfig_title(self, node): def get_numfig_title(self, node: Node) -> str:
# type: (nodes.Node) -> str
"""Get the title of enumerable nodes to refer them using its title""" """Get the title of enumerable nodes to refer them using its title"""
if self.is_enumerable_node(node): if self.is_enumerable_node(node):
_, title_getter = self.enumerable_nodes.get(node.__class__, (None, None)) elem = cast(nodes.Element, node)
_, title_getter = self.enumerable_nodes.get(elem.__class__, (None, None))
if title_getter: if title_getter:
return title_getter(node) return title_getter(elem)
else: else:
for subnode in node: for subnode in elem:
if subnode.tagname in ('caption', 'title'): if isinstance(subnode, (nodes.caption, nodes.title)):
return clean_astext(subnode) return clean_astext(subnode)
return None return None
def get_enumerable_node_type(self, node): def get_enumerable_node_type(self, node: Node) -> str:
# type: (nodes.Node) -> str
"""Get type of enumerable nodes.""" """Get type of enumerable nodes."""
def has_child(node, cls): def has_child(node: Element, cls: Type) -> bool:
# type: (nodes.Element, Type) -> bool
return any(isinstance(child, cls) for child in node) return any(isinstance(child, cls) for child in node)
if isinstance(node, nodes.section): if isinstance(node, nodes.section):
@ -940,8 +924,7 @@ class StandardDomain(Domain):
figtype, _ = self.enumerable_nodes.get(node.__class__, (None, None)) figtype, _ = self.enumerable_nodes.get(node.__class__, (None, None))
return figtype return figtype
def get_figtype(self, node): def get_figtype(self, node: Node) -> str:
# type: (nodes.Node) -> str
"""Get figure type of nodes. """Get figure type of nodes.
.. deprecated:: 1.8 .. deprecated:: 1.8
@ -951,8 +934,8 @@ class StandardDomain(Domain):
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
return self.get_enumerable_node_type(node) return self.get_enumerable_node_type(node)
def get_fignumber(self, env, builder, figtype, docname, target_node): def get_fignumber(self, env: "BuildEnvironment", builder: "Builder",
# type: (BuildEnvironment, Builder, str, str, nodes.Element) -> Tuple[int, ...] figtype: str, docname: str, target_node: Element) -> Tuple[int, ...]:
if figtype == 'section': if figtype == 'section':
if builder.name == 'latex': if builder.name == 'latex':
return tuple() return tuple()
@ -974,8 +957,7 @@ class StandardDomain(Domain):
# Maybe it is defined in orphaned document. # Maybe it is defined in orphaned document.
raise ValueError raise ValueError
def get_full_qualified_name(self, node): def get_full_qualified_name(self, node: Element) -> str:
# type: (nodes.Element) -> str
if node.get('reftype') == 'option': if node.get('reftype') == 'option':
progname = node.get('std:program') progname = node.get('std:program')
command = ws_re.split(node.get('reftarget')) command = ws_re.split(node.get('reftarget'))
@ -989,24 +971,20 @@ class StandardDomain(Domain):
else: else:
return None return None
def note_citations(self, env, docname, document): def note_citations(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA
# type: (BuildEnvironment, str, nodes.document) -> None
warnings.warn('StandardDomain.note_citations() is deprecated.', warnings.warn('StandardDomain.note_citations() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
def note_citation_refs(self, env, docname, document): def note_citation_refs(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA
# type: (BuildEnvironment, str, nodes.document) -> None
warnings.warn('StandardDomain.note_citation_refs() is deprecated.', warnings.warn('StandardDomain.note_citation_refs() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
def note_labels(self, env, docname, document): def note_labels(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA
# type: (BuildEnvironment, str, nodes.document) -> None
warnings.warn('StandardDomain.note_labels() is deprecated.', warnings.warn('StandardDomain.note_labels() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
def setup(app): def setup(app: "Sphinx") -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(StandardDomain) app.add_domain(StandardDomain)
return { return {

View File

@ -15,14 +15,22 @@ from collections import defaultdict
from copy import copy from copy import copy
from io import BytesIO from io import BytesIO
from os import path from os import path
from typing import Any, Callable, Dict, Generator, IO, Iterator, List, Set, Tuple, Union
from docutils import nodes
from docutils.nodes import Node
from sphinx import addnodes from sphinx import addnodes
from sphinx.config import Config
from sphinx.deprecation import ( from sphinx.deprecation import (
RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias
) )
from sphinx.domains import Domain
from sphinx.environment.adapters.toctree import TocTree from sphinx.environment.adapters.toctree import TocTree
from sphinx.errors import SphinxError, BuildEnvironmentError, DocumentError, ExtensionError from sphinx.errors import SphinxError, BuildEnvironmentError, DocumentError, ExtensionError
from sphinx.events import EventManager
from sphinx.locale import __ from sphinx.locale import __
from sphinx.project import Project
from sphinx.transforms import SphinxTransformer from sphinx.transforms import SphinxTransformer
from sphinx.util import DownloadFiles, FilenameUniqDict from sphinx.util import DownloadFiles, FilenameUniqDict
from sphinx.util import logging from sphinx.util import logging
@ -32,14 +40,8 @@ from sphinx.util.nodes import is_translatable
if False: if False:
# For type annotation # For type annotation
from typing import Any, Callable, Dict, IO, Iterator, List, Optional, Set, Tuple, Union # NOQA from sphinx.application import Sphinx
from docutils import nodes # NOQA from sphinx.builders import Builder
from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.config import Config # NOQA
from sphinx.event import EventManager # NOQA
from sphinx.domains import Domain # NOQA
from sphinx.project import Project # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -92,8 +94,7 @@ class BuildEnvironment:
# --------- ENVIRONMENT INITIALIZATION ------------------------------------- # --------- ENVIRONMENT INITIALIZATION -------------------------------------
def __init__(self, app=None): def __init__(self, app: "Sphinx" = None):
# type: (Sphinx) -> None
self.app = None # type: Sphinx self.app = None # type: Sphinx
self.doctreedir = None # type: str self.doctreedir = None # type: str
self.srcdir = None # type: str self.srcdir = None # type: str
@ -191,19 +192,16 @@ class BuildEnvironment:
if app: if app:
self.setup(app) self.setup(app)
def __getstate__(self): def __getstate__(self) -> Dict:
# type: () -> Dict
"""Obtains serializable data for pickling.""" """Obtains serializable data for pickling."""
__dict__ = self.__dict__.copy() __dict__ = self.__dict__.copy()
__dict__.update(app=None, domains={}, events=None) # clear unpickable attributes __dict__.update(app=None, domains={}, events=None) # clear unpickable attributes
return __dict__ return __dict__
def __setstate__(self, state): def __setstate__(self, state: Dict) -> None:
# type: (Dict) -> None
self.__dict__.update(state) self.__dict__.update(state)
def setup(self, app): def setup(self, app: "Sphinx") -> None:
# type: (Sphinx) -> None
"""Set up BuildEnvironment object.""" """Set up BuildEnvironment object."""
if self.version and self.version != app.registry.get_envversion(app): if self.version and self.version != app.registry.get_envversion(app):
raise BuildEnvironmentError(__('build environment version not current')) raise BuildEnvironmentError(__('build environment version not current'))
@ -231,12 +229,13 @@ class BuildEnvironment:
# initialie settings # initialie settings
self._update_settings(app.config) self._update_settings(app.config)
def _update_config(self, config): def _update_config(self, config: Config) -> None:
# type: (Config) -> None
"""Update configurations by new one.""" """Update configurations by new one."""
self.config_status = CONFIG_OK self.config_status = CONFIG_OK
if self.config is None: if self.config is None:
self.config_status = CONFIG_NEW self.config_status = CONFIG_NEW
elif self.config.extensions != config.extensions:
self.config_status = CONFIG_EXTENSIONS_CHANGED
else: else:
# check if a config value was changed that affects how # check if a config value was changed that affects how
# doctrees are read # doctrees are read
@ -245,15 +244,9 @@ class BuildEnvironment:
self.config_status = CONFIG_CHANGED self.config_status = CONFIG_CHANGED
break break
# this value is not covered by the above loop because it is handled
# specially by the config class
if self.config.extensions != config.extensions:
self.config_status = CONFIG_EXTENSIONS_CHANGED
self.config = config self.config = config
def _update_settings(self, config): def _update_settings(self, config: Config) -> None:
# type: (Config) -> None
"""Update settings by new config.""" """Update settings by new config."""
self.settings['input_encoding'] = config.source_encoding self.settings['input_encoding'] = config.source_encoding
self.settings['trim_footnote_reference_space'] = config.trim_footnote_reference_space self.settings['trim_footnote_reference_space'] = config.trim_footnote_reference_space
@ -262,8 +255,7 @@ class BuildEnvironment:
# Allow to disable by 3rd party extension (workaround) # Allow to disable by 3rd party extension (workaround)
self.settings.setdefault('smart_quotes', True) self.settings.setdefault('smart_quotes', True)
def set_versioning_method(self, method, compare): def set_versioning_method(self, method: Union[str, Callable], compare: bool) -> None:
# type: (Union[str, Callable], bool) -> None
"""This sets the doctree versioning method for this environment. """This sets the doctree versioning method for this environment.
Versioning methods are a builder property; only builders with the same Versioning methods are a builder property; only builders with the same
@ -286,8 +278,7 @@ class BuildEnvironment:
self.versioning_condition = condition self.versioning_condition = condition
self.versioning_compare = compare self.versioning_compare = compare
def clear_doc(self, docname): def clear_doc(self, docname: str) -> None:
# type: (str) -> None
"""Remove all traces of a source file in the inventory.""" """Remove all traces of a source file in the inventory."""
if docname in self.all_docs: if docname in self.all_docs:
self.all_docs.pop(docname, None) self.all_docs.pop(docname, None)
@ -297,8 +288,8 @@ class BuildEnvironment:
for domain in self.domains.values(): for domain in self.domains.values():
domain.clear_doc(docname) domain.clear_doc(docname)
def merge_info_from(self, docnames, other, app): def merge_info_from(self, docnames: List[str], other: "BuildEnvironment",
# type: (List[str], BuildEnvironment, Sphinx) -> None app: "Sphinx") -> None:
"""Merge global information gathered about *docnames* while reading them """Merge global information gathered about *docnames* while reading them
from the *other* environment. from the *other* environment.
@ -315,16 +306,14 @@ class BuildEnvironment:
domain.merge_domaindata(docnames, other.domaindata[domainname]) domain.merge_domaindata(docnames, other.domaindata[domainname])
self.events.emit('env-merge-info', self, docnames, other) self.events.emit('env-merge-info', self, docnames, other)
def path2doc(self, filename): def path2doc(self, filename: str) -> str:
# type: (str) -> Optional[str]
"""Return the docname for the filename if the file is document. """Return the docname for the filename if the file is document.
*filename* should be absolute or relative to the source directory. *filename* should be absolute or relative to the source directory.
""" """
return self.project.path2doc(filename) return self.project.path2doc(filename)
def doc2path(self, docname, base=True, suffix=None): def doc2path(self, docname: str, base: Union[bool, str] = True, suffix: str = None) -> str:
# type: (str, Union[bool, str], str) -> str
"""Return the filename for the document name. """Return the filename for the document name.
If *base* is True, return absolute path under self.srcdir. If *base* is True, return absolute path under self.srcdir.
@ -347,8 +336,7 @@ class BuildEnvironment:
pathname = path.join(base, pathname) # type: ignore pathname = path.join(base, pathname) # type: ignore
return pathname return pathname
def relfn2path(self, filename, docname=None): def relfn2path(self, filename: str, docname: str = None) -> Tuple[str, str]:
# type: (str, str) -> Tuple[str, str]
"""Return paths to a file referenced from a document, relative to """Return paths to a file referenced from a document, relative to
documentation root and absolute. documentation root and absolute.
@ -367,13 +355,11 @@ class BuildEnvironment:
return rel_fn, path.abspath(path.join(self.srcdir, rel_fn)) return rel_fn, path.abspath(path.join(self.srcdir, rel_fn))
@property @property
def found_docs(self): def found_docs(self) -> Set[str]:
# type: () -> Set[str]
"""contains all existing docnames.""" """contains all existing docnames."""
return self.project.docnames return self.project.docnames
def find_files(self, config, builder): def find_files(self, config: Config, builder: "Builder") -> None:
# type: (Config, Builder) -> None
"""Find all source files in the source dir and put them in """Find all source files in the source dir and put them in
self.found_docs. self.found_docs.
""" """
@ -401,8 +387,7 @@ class BuildEnvironment:
except OSError as exc: except OSError as exc:
raise DocumentError(__('Failed to scan documents in %s: %r') % (self.srcdir, exc)) raise DocumentError(__('Failed to scan documents in %s: %r') % (self.srcdir, exc))
def get_outdated_files(self, config_changed): def get_outdated_files(self, config_changed: bool) -> Tuple[Set[str], Set[str], Set[str]]:
# type: (bool) -> Tuple[Set[str], Set[str], Set[str]]
"""Return (added, changed, removed) sets.""" """Return (added, changed, removed) sets."""
# clear all files no longer present # clear all files no longer present
removed = set(self.all_docs) - self.found_docs removed = set(self.all_docs) - self.found_docs
@ -452,8 +437,7 @@ class BuildEnvironment:
return added, changed, removed return added, changed, removed
def check_dependents(self, app, already): def check_dependents(self, app: "Sphinx", already: Set[str]) -> Generator[str, None, None]:
# type: (Sphinx, Set[str]) -> Iterator[str]
to_rewrite = [] # type: List[str] to_rewrite = [] # type: List[str]
for docnames in self.events.emit('env-get-updated', self): for docnames in self.events.emit('env-get-updated', self):
to_rewrite.extend(docnames) to_rewrite.extend(docnames)
@ -463,8 +447,7 @@ class BuildEnvironment:
# --------- SINGLE FILE READING -------------------------------------------- # --------- SINGLE FILE READING --------------------------------------------
def prepare_settings(self, docname): def prepare_settings(self, docname: str) -> None:
# type: (str) -> None
"""Prepare to set up environment for reading.""" """Prepare to set up environment for reading."""
self.temp_data['docname'] = docname self.temp_data['docname'] = docname
# defaults to the global default, but can be re-set in a document # defaults to the global default, but can be re-set in a document
@ -475,13 +458,11 @@ class BuildEnvironment:
# utilities to use while reading a document # utilities to use while reading a document
@property @property
def docname(self): def docname(self) -> str:
# type: () -> str
"""Returns the docname of the document currently being parsed.""" """Returns the docname of the document currently being parsed."""
return self.temp_data['docname'] return self.temp_data['docname']
def new_serialno(self, category=''): def new_serialno(self, category: str = '') -> int:
# type: (str) -> int
"""Return a serial number, e.g. for index entry targets. """Return a serial number, e.g. for index entry targets.
The number is guaranteed to be unique in the current document. The number is guaranteed to be unique in the current document.
@ -491,8 +472,7 @@ class BuildEnvironment:
self.temp_data[key] = cur + 1 self.temp_data[key] = cur + 1
return cur return cur
def note_dependency(self, filename): def note_dependency(self, filename: str) -> None:
# type: (str) -> None
"""Add *filename* as a dependency of the current document. """Add *filename* as a dependency of the current document.
This means that the document will be rebuilt if this file changes. This means that the document will be rebuilt if this file changes.
@ -501,8 +481,7 @@ class BuildEnvironment:
""" """
self.dependencies[self.docname].add(filename) self.dependencies[self.docname].add(filename)
def note_included(self, filename): def note_included(self, filename: str) -> None:
# type: (str) -> None
"""Add *filename* as a included from other document. """Add *filename* as a included from other document.
This means the document is not orphaned. This means the document is not orphaned.
@ -511,15 +490,13 @@ class BuildEnvironment:
""" """
self.included[self.docname].add(self.path2doc(filename)) self.included[self.docname].add(self.path2doc(filename))
def note_reread(self): def note_reread(self) -> None:
# type: () -> None
"""Add the current document to the list of documents that will """Add the current document to the list of documents that will
automatically be re-read at the next build. automatically be re-read at the next build.
""" """
self.reread_always.add(self.docname) self.reread_always.add(self.docname)
def get_domain(self, domainname): def get_domain(self, domainname: str) -> Domain:
# type: (str) -> Domain
"""Return the domain instance with the specified name. """Return the domain instance with the specified name.
Raises an ExtensionError if the domain is not registered. Raises an ExtensionError if the domain is not registered.
@ -531,8 +508,7 @@ class BuildEnvironment:
# --------- RESOLVING REFERENCES AND TOCTREES ------------------------------ # --------- RESOLVING REFERENCES AND TOCTREES ------------------------------
def get_doctree(self, docname): def get_doctree(self, docname: str) -> nodes.document:
# type: (str) -> nodes.document
"""Read the doctree for a file from the pickle and return it.""" """Read the doctree for a file from the pickle and return it."""
filename = path.join(self.doctreedir, docname + '.doctree') filename = path.join(self.doctreedir, docname + '.doctree')
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
@ -541,9 +517,9 @@ class BuildEnvironment:
doctree.reporter = LoggingReporter(self.doc2path(docname)) doctree.reporter = LoggingReporter(self.doc2path(docname))
return doctree return doctree
def get_and_resolve_doctree(self, docname, builder, doctree=None, def get_and_resolve_doctree(self, docname: str, builder: "Builder",
prune_toctrees=True, includehidden=False): doctree: nodes.document = None, prune_toctrees: bool = True,
# type: (str, Builder, nodes.document, bool, bool) -> nodes.document includehidden: bool = False) -> nodes.document:
"""Read the doctree from the pickle, resolve cross-references and """Read the doctree from the pickle, resolve cross-references and
toctrees and return it. toctrees and return it.
""" """
@ -565,9 +541,9 @@ class BuildEnvironment:
return doctree return doctree
def resolve_toctree(self, docname, builder, toctree, prune=True, maxdepth=0, def resolve_toctree(self, docname: str, builder: "Builder", toctree: addnodes.toctree,
titles_only=False, collapse=False, includehidden=False): prune: bool = True, maxdepth: int = 0, titles_only: bool = False,
# type: (str, Builder, addnodes.toctree, bool, int, bool, bool, bool) -> nodes.Node collapse: bool = False, includehidden: bool = False) -> Node:
"""Resolve a *toctree* node into individual bullet lists with titles """Resolve a *toctree* node into individual bullet lists with titles
as items, returning None (if no containing titles are found) or as items, returning None (if no containing titles are found) or
a new node. a new node.
@ -583,12 +559,11 @@ class BuildEnvironment:
maxdepth, titles_only, collapse, maxdepth, titles_only, collapse,
includehidden) includehidden)
def resolve_references(self, doctree, fromdocname, builder): def resolve_references(self, doctree: nodes.document, fromdocname: str,
# type: (nodes.document, str, Builder) -> None builder: "Builder") -> None:
self.apply_post_transforms(doctree, fromdocname) self.apply_post_transforms(doctree, fromdocname)
def apply_post_transforms(self, doctree, docname): def apply_post_transforms(self, doctree: nodes.document, docname: str) -> None:
# type: (nodes.document, str) -> None
"""Apply all post-transforms.""" """Apply all post-transforms."""
try: try:
# set env.docname during applying post-transforms # set env.docname during applying post-transforms
@ -605,12 +580,10 @@ class BuildEnvironment:
# allow custom references to be resolved # allow custom references to be resolved
self.events.emit('doctree-resolved', doctree, docname) self.events.emit('doctree-resolved', doctree, docname)
def collect_relations(self): def collect_relations(self) -> Dict[str, List[str]]:
# type: () -> Dict[str, List[str]]
traversed = set() traversed = set()
def traverse_toctree(parent, docname): def traverse_toctree(parent: str, docname: str) -> Iterator[Tuple[str, str]]:
# type: (str, str) -> Iterator[Tuple[str, str]]
if parent == docname: if parent == docname:
logger.warning(__('self referenced toctree found. Ignored.'), location=docname) logger.warning(__('self referenced toctree found. Ignored.'), location=docname)
return return
@ -639,8 +612,7 @@ class BuildEnvironment:
return relations return relations
def check_consistency(self): def check_consistency(self) -> None:
# type: () -> None
"""Do consistency checks.""" """Do consistency checks."""
included = set().union(*self.included.values()) # type: ignore included = set().union(*self.included.values()) # type: ignore
for docname in sorted(self.all_docs): for docname in sorted(self.all_docs):
@ -663,48 +635,41 @@ class BuildEnvironment:
# --------- METHODS FOR COMPATIBILITY -------------------------------------- # --------- METHODS FOR COMPATIBILITY --------------------------------------
def update(self, config, srcdir, doctreedir): def update(self, config: Config, srcdir: str, doctreedir: str) -> List[str]:
# type: (Config, str, str) -> List[str]
warnings.warn('env.update() is deprecated. Please use builder.read() instead.', warnings.warn('env.update() is deprecated. Please use builder.read() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
return self.app.builder.read() return self.app.builder.read()
def _read_serial(self, docnames, app): def _read_serial(self, docnames: List[str], app: "Sphinx") -> None:
# type: (List[str], Sphinx) -> None
warnings.warn('env._read_serial() is deprecated. Please use builder.read() instead.', warnings.warn('env._read_serial() is deprecated. Please use builder.read() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
return self.app.builder._read_serial(docnames) return self.app.builder._read_serial(docnames)
def _read_parallel(self, docnames, app, nproc): def _read_parallel(self, docnames: List[str], app: "Sphinx", nproc: int) -> None:
# type: (List[str], Sphinx, int) -> None
warnings.warn('env._read_parallel() is deprecated. Please use builder.read() instead.', warnings.warn('env._read_parallel() is deprecated. Please use builder.read() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
return self.app.builder._read_parallel(docnames, nproc) return self.app.builder._read_parallel(docnames, nproc)
def read_doc(self, docname, app=None): def read_doc(self, docname: str, app: "Sphinx" = None) -> None:
# type: (str, Sphinx) -> None
warnings.warn('env.read_doc() is deprecated. Please use builder.read_doc() instead.', warnings.warn('env.read_doc() is deprecated. Please use builder.read_doc() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
self.app.builder.read_doc(docname) self.app.builder.read_doc(docname)
def write_doctree(self, docname, doctree): def write_doctree(self, docname: str, doctree: nodes.document) -> None:
# type: (str, nodes.document) -> None
warnings.warn('env.write_doctree() is deprecated. ' warnings.warn('env.write_doctree() is deprecated. '
'Please use builder.write_doctree() instead.', 'Please use builder.write_doctree() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
self.app.builder.write_doctree(docname, doctree) self.app.builder.write_doctree(docname, doctree)
@property @property
def _nitpick_ignore(self): def _nitpick_ignore(self) -> List[str]:
# type: () -> List[str]
warnings.warn('env._nitpick_ignore is deprecated. ' warnings.warn('env._nitpick_ignore is deprecated. '
'Please use config.nitpick_ignore instead.', 'Please use config.nitpick_ignore instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
return self.config.nitpick_ignore return self.config.nitpick_ignore
@staticmethod @staticmethod
def load(f, app=None): def load(f: IO, app: "Sphinx" = None) -> "BuildEnvironment":
# type: (IO, Sphinx) -> BuildEnvironment
warnings.warn('BuildEnvironment.load() is deprecated. ' warnings.warn('BuildEnvironment.load() is deprecated. '
'Please use pickle.load() instead.', 'Please use pickle.load() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
@ -720,8 +685,7 @@ class BuildEnvironment:
return env return env
@classmethod @classmethod
def loads(cls, string, app=None): def loads(cls, string: bytes, app: "Sphinx" = None) -> "BuildEnvironment":
# type: (bytes, Sphinx) -> BuildEnvironment
warnings.warn('BuildEnvironment.loads() is deprecated. ' warnings.warn('BuildEnvironment.loads() is deprecated. '
'Please use pickle.loads() instead.', 'Please use pickle.loads() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
@ -729,8 +693,7 @@ class BuildEnvironment:
return cls.load(io, app) return cls.load(io, app)
@classmethod @classmethod
def frompickle(cls, filename, app): def frompickle(cls, filename: str, app: "Sphinx") -> "BuildEnvironment":
# type: (str, Sphinx) -> BuildEnvironment
warnings.warn('BuildEnvironment.frompickle() is deprecated. ' warnings.warn('BuildEnvironment.frompickle() is deprecated. '
'Please use pickle.load() instead.', 'Please use pickle.load() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
@ -738,16 +701,14 @@ class BuildEnvironment:
return cls.load(f, app) return cls.load(f, app)
@staticmethod @staticmethod
def dump(env, f): def dump(env: "BuildEnvironment", f: IO) -> None:
# type: (BuildEnvironment, IO) -> None
warnings.warn('BuildEnvironment.dump() is deprecated. ' warnings.warn('BuildEnvironment.dump() is deprecated. '
'Please use pickle.dump() instead.', 'Please use pickle.dump() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) pickle.dump(env, f, pickle.HIGHEST_PROTOCOL)
@classmethod @classmethod
def dumps(cls, env): def dumps(cls, env: "BuildEnvironment") -> bytes:
# type: (BuildEnvironment) -> bytes
warnings.warn('BuildEnvironment.dumps() is deprecated. ' warnings.warn('BuildEnvironment.dumps() is deprecated. '
'Please use pickle.dumps() instead.', 'Please use pickle.dumps() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
@ -755,8 +716,7 @@ class BuildEnvironment:
cls.dump(env, io) cls.dump(env, io)
return io.getvalue() return io.getvalue()
def topickle(self, filename): def topickle(self, filename: str) -> None:
# type: (str) -> None
warnings.warn('env.topickle() is deprecated. ' warnings.warn('env.topickle() is deprecated. '
'Please use pickle.dump() instead.', 'Please use pickle.dump() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
@ -764,15 +724,14 @@ class BuildEnvironment:
self.dump(self, f) self.dump(self, f)
@property @property
def versionchanges(self): def versionchanges(self) -> Dict[str, List[Tuple[str, str, int, str, str, str]]]:
# type: () -> Dict[str, List[Tuple[str, str, int, str, str, str]]]
warnings.warn('env.versionchanges() is deprecated. ' warnings.warn('env.versionchanges() is deprecated. '
'Please use ChangeSetDomain instead.', 'Please use ChangeSetDomain instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
return self.domaindata['changeset']['changes'] return self.domaindata['changeset']['changes']
def note_versionchange(self, type, version, node, lineno): def note_versionchange(self, type: str, version: str,
# type: (str, str, addnodes.versionmodified, int) -> None node: addnodes.versionmodified, lineno: int) -> None:
warnings.warn('env.note_versionchange() is deprecated. ' warnings.warn('env.note_versionchange() is deprecated. '
'Please use ChangeSetDomain.note_changeset() instead.', 'Please use ChangeSetDomain.note_changeset() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)

View File

@ -8,18 +8,14 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
if False: from sphinx.environment import BuildEnvironment
# For type annotation
from sphinx.environment import BuildEnvironment # NOQA
class ImageAdapter: class ImageAdapter:
def __init__(self, env): def __init__(self, env: BuildEnvironment) -> None:
# type: (BuildEnvironment) -> None
self.env = env self.env = env
def get_original_image_uri(self, name): def get_original_image_uri(self, name: str) -> str:
# type: (str) -> str
"""Get the original image URI.""" """Get the original image URI."""
while name in self.env.original_image_uri: while name in self.env.original_image_uri:
name = self.env.original_image_uri[name] name = self.env.original_image_uri[name]

View File

@ -11,33 +11,30 @@ import bisect
import re import re
import unicodedata import unicodedata
from itertools import groupby from itertools import groupby
from typing import Any, Dict, Pattern, List, Tuple
from sphinx.builders import Builder
from sphinx.environment import BuildEnvironment
from sphinx.errors import NoUri from sphinx.errors import NoUri
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.util import split_into, logging from sphinx.util import split_into, logging
if False:
# For type annotation
from typing import Any, Dict, Pattern, List, Tuple # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class IndexEntries: class IndexEntries:
def __init__(self, env): def __init__(self, env: BuildEnvironment) -> None:
# type: (BuildEnvironment) -> None
self.env = env self.env = env
def create_index(self, builder, group_entries=True, def create_index(self, builder: Builder, group_entries: bool = True,
_fixre=re.compile(r'(.*) ([(][^()]*[)])')): _fixre: Pattern = re.compile(r'(.*) ([(][^()]*[)])')
# type: (Builder, bool, Pattern) -> List[Tuple[str, List[Tuple[str, Any]]]] ) -> List[Tuple[str, List[Tuple[str, Any]]]]:
"""Create the real index from the collected index entries.""" """Create the real index from the collected index entries."""
new = {} # type: Dict[str, List] new = {} # type: Dict[str, List]
def add_entry(word, subword, main, link=True, dic=new, key=None): def add_entry(word: str, subword: str, main: str, link: bool = True,
# type: (str, str, str, bool, Dict, str) -> None dic: Dict = new, key: str = None) -> None:
# Force the word to be unicode if it's a ASCII bytestring. # Force the word to be unicode if it's a ASCII bytestring.
# This will solve problems with unicode normalization later. # This will solve problems with unicode normalization later.
# For instance the RFC role will add bytestrings at the moment # For instance the RFC role will add bytestrings at the moment
@ -91,8 +88,7 @@ class IndexEntries:
# sort the index entries; put all symbols at the front, even those # sort the index entries; put all symbols at the front, even those
# following the letters in ASCII, this is where the chr(127) comes from # following the letters in ASCII, this is where the chr(127) comes from
def keyfunc(entry): def keyfunc(entry: Tuple[str, List]) -> Tuple[str, str]:
# type: (Tuple[str, List]) -> Tuple[str, str]
key, (void, void, category_key) = entry key, (void, void, category_key) = entry
if category_key: if category_key:
# using specified category key to sort # using specified category key to sort
@ -137,12 +133,21 @@ class IndexEntries:
oldsubitems = subitems oldsubitems = subitems
i += 1 i += 1
# sort the sub-index entries
def keyfunc2(entry: Tuple[str, List]) -> str:
key = unicodedata.normalize('NFD', entry[0].lower())
if key.startswith('\N{RIGHT-TO-LEFT MARK}'):
key = key[1:]
if key[0:1].isalpha() or key.startswith('_'):
key = chr(127) + key
return key
# group the entries by letter # group the entries by letter
def keyfunc2(item): def keyfunc3(item: Tuple[str, List]) -> str:
# type: (Tuple[str, List]) -> str
# hack: mutating the subitems dicts to a list in the keyfunc # hack: mutating the subitems dicts to a list in the keyfunc
k, v = item k, v = item
v[1] = sorted((si, se) for (si, (se, void, void)) in v[1].items()) v[1] = sorted(((si, se) for (si, (se, void, void)) in v[1].items()),
key=keyfunc2)
if v[2] is None: if v[2] is None:
# now calculate the key # now calculate the key
if k.startswith('\N{RIGHT-TO-LEFT MARK}'): if k.startswith('\N{RIGHT-TO-LEFT MARK}'):
@ -156,4 +161,4 @@ class IndexEntries:
else: else:
return v[2] return v[2]
return [(key_, list(group)) return [(key_, list(group))
for (key_, group) in groupby(newlist, keyfunc2)] for (key_, group) in groupby(newlist, keyfunc3)]

View File

@ -8,9 +8,11 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Iterable, cast from typing import cast
from typing import Iterable, List
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node
from sphinx import addnodes from sphinx import addnodes
from sphinx.locale import __ from sphinx.locale import __
@ -20,20 +22,18 @@ from sphinx.util.nodes import clean_astext, process_only_nodes
if False: if False:
# For type annotation # For type annotation
from typing import Any, Dict, List # NOQA from sphinx.builders import Builder
from sphinx.builders import Builder # NOQA from sphinx.environment import BuildEnvironment
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TocTree: class TocTree:
def __init__(self, env): def __init__(self, env: "BuildEnvironment") -> None:
# type: (BuildEnvironment) -> None
self.env = env self.env = env
def note(self, docname, toctreenode): def note(self, docname: str, toctreenode: addnodes.toctree) -> None:
# type: (str, addnodes.toctree) -> None
"""Note a TOC tree directive in a document and gather information about """Note a TOC tree directive in a document and gather information about
file relations from it. file relations from it.
""" """
@ -48,9 +48,9 @@ class TocTree:
self.env.files_to_rebuild.setdefault(includefile, set()).add(docname) self.env.files_to_rebuild.setdefault(includefile, set()).add(docname)
self.env.toctree_includes.setdefault(docname, []).extend(includefiles) self.env.toctree_includes.setdefault(docname, []).extend(includefiles)
def resolve(self, docname, builder, toctree, prune=True, maxdepth=0, def resolve(self, docname: str, builder: "Builder", toctree: addnodes.toctree,
titles_only=False, collapse=False, includehidden=False): prune: bool = True, maxdepth: int = 0, titles_only: bool = False,
# type: (str, Builder, addnodes.toctree, bool, int, bool, bool, bool) -> nodes.Element collapse: bool = False, includehidden: bool = False) -> Element:
"""Resolve a *toctree* node into individual bullet lists with titles """Resolve a *toctree* node into individual bullet lists with titles
as items, returning None (if no containing titles are found) or as items, returning None (if no containing titles are found) or
a new node. a new node.
@ -86,8 +86,7 @@ class TocTree:
toctree_ancestors = self.get_toctree_ancestors(docname) toctree_ancestors = self.get_toctree_ancestors(docname)
excluded = Matcher(self.env.config.exclude_patterns) excluded = Matcher(self.env.config.exclude_patterns)
def _toctree_add_classes(node, depth): def _toctree_add_classes(node: Element, depth: int) -> None:
# type: (nodes.Element, int) -> None
"""Add 'toctree-l%d' and 'current' classes to the toctree.""" """Add 'toctree-l%d' and 'current' classes to the toctree."""
for subnode in node.children: for subnode in node.children:
if isinstance(subnode, (addnodes.compact_paragraph, if isinstance(subnode, (addnodes.compact_paragraph,
@ -105,7 +104,7 @@ class TocTree:
if not subnode['anchorname']: if not subnode['anchorname']:
# give the whole branch a 'current' class # give the whole branch a 'current' class
# (useful for styling it differently) # (useful for styling it differently)
branchnode = subnode # type: nodes.Element branchnode = subnode # type: Element
while branchnode: while branchnode:
branchnode['classes'].append('current') branchnode['classes'].append('current')
branchnode = branchnode.parent branchnode = branchnode.parent
@ -117,11 +116,12 @@ class TocTree:
subnode['iscurrent'] = True subnode['iscurrent'] = True
subnode = subnode.parent subnode = subnode.parent
def _entries_from_toctree(toctreenode, parents, separate=False, subtree=False): def _entries_from_toctree(toctreenode: addnodes.toctree, parents: List[str],
# type: (addnodes.toctree, List[str], bool, bool) -> List[nodes.Element] separate: bool = False, subtree: bool = False
) -> List[Element]:
"""Return TOC entries for a toctree node.""" """Return TOC entries for a toctree node."""
refs = [(e[0], e[1]) for e in toctreenode['entries']] refs = [(e[0], e[1]) for e in toctreenode['entries']]
entries = [] # type: List[nodes.Element] entries = [] # type: List[Element]
for (title, ref) in refs: for (title, ref) in refs:
try: try:
refdoc = None refdoc = None
@ -265,8 +265,7 @@ class TocTree:
docname, refnode['refuri']) + refnode['anchorname'] docname, refnode['refuri']) + refnode['anchorname']
return newnode return newnode
def get_toctree_ancestors(self, docname): def get_toctree_ancestors(self, docname: str) -> List[str]:
# type: (str) -> List[str]
parent = {} parent = {}
for p, children in self.env.toctree_includes.items(): for p, children in self.env.toctree_includes.items():
for child in children: for child in children:
@ -278,8 +277,8 @@ class TocTree:
d = parent[d] d = parent[d]
return ancestors return ancestors
def _toctree_prune(self, node, depth, maxdepth, collapse=False): def _toctree_prune(self, node: Element, depth: int, maxdepth: int, collapse: bool = False
# type: (nodes.Element, int, int, bool) -> None ) -> None:
"""Utility: Cut a TOC at a specified depth.""" """Utility: Cut a TOC at a specified depth."""
for subnode in node.children[:]: for subnode in node.children[:]:
if isinstance(subnode, (addnodes.compact_paragraph, if isinstance(subnode, (addnodes.compact_paragraph,
@ -300,8 +299,7 @@ class TocTree:
# recurse on visible children # recurse on visible children
self._toctree_prune(subnode, depth + 1, maxdepth, collapse) self._toctree_prune(subnode, depth + 1, maxdepth, collapse)
def get_toc_for(self, docname, builder): def get_toc_for(self, docname: str, builder: "Builder") -> Node:
# type: (str, Builder) -> nodes.Node
"""Return a TOC nodetree -- for use on the same page only!""" """Return a TOC nodetree -- for use on the same page only!"""
tocdepth = self.env.metadata[docname].get('tocdepth', 0) tocdepth = self.env.metadata[docname].get('tocdepth', 0)
try: try:
@ -316,11 +314,11 @@ class TocTree:
node['refuri'] = node['anchorname'] or '#' node['refuri'] = node['anchorname'] or '#'
return toc return toc
def get_toctree_for(self, docname, builder, collapse, **kwds): def get_toctree_for(self, docname: str, builder: "Builder", collapse: bool, **kwds
# type: (str, Builder, bool, Any) -> nodes.Element ) -> Element:
"""Return the global TOC nodetree.""" """Return the global TOC nodetree."""
doctree = self.env.get_doctree(self.env.config.master_doc) doctree = self.env.get_doctree(self.env.config.master_doc)
toctrees = [] # type: List[nodes.Element] toctrees = [] # type: List[Element]
if 'includehidden' not in kwds: if 'includehidden' not in kwds:
kwds['includehidden'] = True kwds['includehidden'] = True
if 'maxdepth' not in kwds: if 'maxdepth' not in kwds:

View File

@ -8,12 +8,12 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
if False: from typing import Dict, List, Set
# For type annotation
from typing import Dict, List, Set # NOQA from docutils import nodes
from docutils import nodes # NOQA
from sphinx.sphinx import Sphinx # NOQA from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment # NOQA from sphinx.environment import BuildEnvironment
class EnvironmentCollector: class EnvironmentCollector:
@ -27,8 +27,7 @@ class EnvironmentCollector:
listener_ids = None # type: Dict[str, int] listener_ids = None # type: Dict[str, int]
def enable(self, app): def enable(self, app: Sphinx) -> None:
# type: (Sphinx) -> None
assert self.listener_ids is None assert self.listener_ids is None
self.listener_ids = { self.listener_ids = {
'doctree-read': app.connect('doctree-read', self.process_doc), 'doctree-read': app.connect('doctree-read', self.process_doc),
@ -38,43 +37,39 @@ class EnvironmentCollector:
'env-get-outdated': app.connect('env-get-outdated', self.get_outdated_docs), 'env-get-outdated': app.connect('env-get-outdated', self.get_outdated_docs),
} }
def disable(self, app): def disable(self, app: Sphinx) -> None:
# type: (Sphinx) -> None
assert self.listener_ids is not None assert self.listener_ids is not None
for listener_id in self.listener_ids.values(): for listener_id in self.listener_ids.values():
app.disconnect(listener_id) app.disconnect(listener_id)
self.listener_ids = None self.listener_ids = None
def clear_doc(self, app, env, docname): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
"""Remove specified data of a document. """Remove specified data of a document.
This method is called on the removal of the document.""" This method is called on the removal of the document."""
raise NotImplementedError raise NotImplementedError
def merge_other(self, app, env, docnames, other): def merge_other(self, app: Sphinx, env: BuildEnvironment,
# type: (Sphinx, BuildEnvironment, Set[str], BuildEnvironment) -> None docnames: Set[str], other: BuildEnvironment) -> None:
"""Merge in specified data regarding docnames from a different `BuildEnvironment` """Merge in specified data regarding docnames from a different `BuildEnvironment`
object which coming from a subprocess in parallel builds.""" object which coming from a subprocess in parallel builds."""
raise NotImplementedError raise NotImplementedError
def process_doc(self, app, doctree): def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
"""Process a document and gather specific data from it. """Process a document and gather specific data from it.
This method is called after the document is read.""" This method is called after the document is read."""
raise NotImplementedError raise NotImplementedError
def get_updated_docs(self, app, env): def get_updated_docs(self, app: Sphinx, env: BuildEnvironment) -> List[str]:
# type: (Sphinx, BuildEnvironment) -> List[str]
"""Return a list of docnames to re-read. """Return a list of docnames to re-read.
This methods is called after reading the whole of documents (experimental). This methods is called after reading the whole of documents (experimental).
""" """
return [] return []
def get_outdated_docs(self, app, env, added, changed, removed): def get_outdated_docs(self, app: Sphinx, env: BuildEnvironment,
# type: (Sphinx, BuildEnvironment, str, Set[str], Set[str], Set[str]) -> List[str] added: Set[str], changed: Set[str], removed: Set[str]) -> List[str]:
"""Return a list of docnames to re-read. """Return a list of docnames to re-read.
This methods is called before reading the documents. This methods is called before reading the documents.

View File

@ -11,22 +11,21 @@
import os import os
from glob import glob from glob import glob
from os import path from os import path
from typing import Any, Dict, List, Set
from docutils import nodes from docutils import nodes
from docutils.nodes import Node
from docutils.utils import relative_path from docutils.utils import relative_path
from sphinx import addnodes from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.environment.collectors import EnvironmentCollector from sphinx.environment.collectors import EnvironmentCollector
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.i18n import get_image_filename_for_language, search_image_for_language from sphinx.util.i18n import get_image_filename_for_language, search_image_for_language
from sphinx.util.images import guess_mimetype from sphinx.util.images import guess_mimetype
if False:
# For type annotation
from typing import Dict, List, Set # NOQA
from sphinx.sphinx import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,16 +33,14 @@ logger = logging.getLogger(__name__)
class ImageCollector(EnvironmentCollector): class ImageCollector(EnvironmentCollector):
"""Image files collector for sphinx.environment.""" """Image files collector for sphinx.environment."""
def clear_doc(self, app, env, docname): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
env.images.purge_doc(docname) env.images.purge_doc(docname)
def merge_other(self, app, env, docnames, other): def merge_other(self, app: Sphinx, env: BuildEnvironment,
# type: (Sphinx, BuildEnvironment, Set[str], BuildEnvironment) -> None docnames: Set[str], other: BuildEnvironment) -> None:
env.images.merge_other(docnames, other.images) env.images.merge_other(docnames, other.images)
def process_doc(self, app, doctree): def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
"""Process and rewrite image URIs.""" """Process and rewrite image URIs."""
docname = app.env.docname docname = app.env.docname
@ -92,8 +89,8 @@ class ImageCollector(EnvironmentCollector):
continue continue
app.env.images.add_file(docname, imgpath) app.env.images.add_file(docname, imgpath)
def collect_candidates(self, env, imgpath, candidates, node): def collect_candidates(self, env: BuildEnvironment, imgpath: str,
# type: (BuildEnvironment, str, Dict[str, str], nodes.Node) -> None candidates: Dict[str, str], node: Node) -> None:
globbed = {} # type: Dict[str, List[str]] globbed = {} # type: Dict[str, List[str]]
for filename in glob(imgpath): for filename in glob(imgpath):
new_imgpath = relative_path(path.join(env.srcdir, 'dummy'), new_imgpath = relative_path(path.join(env.srcdir, 'dummy'),
@ -115,16 +112,14 @@ class ImageCollector(EnvironmentCollector):
class DownloadFileCollector(EnvironmentCollector): class DownloadFileCollector(EnvironmentCollector):
"""Download files collector for sphinx.environment.""" """Download files collector for sphinx.environment."""
def clear_doc(self, app, env, docname): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
env.dlfiles.purge_doc(docname) env.dlfiles.purge_doc(docname)
def merge_other(self, app, env, docnames, other): def merge_other(self, app: Sphinx, env: BuildEnvironment,
# type: (Sphinx, BuildEnvironment, Set[str], BuildEnvironment) -> None docnames: Set[str], other: BuildEnvironment) -> None:
env.dlfiles.merge_other(docnames, other.dlfiles) env.dlfiles.merge_other(docnames, other.dlfiles)
def process_doc(self, app, doctree): def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
"""Process downloadable file paths. """ """Process downloadable file paths. """
for node in doctree.traverse(addnodes.download_reference): for node in doctree.traverse(addnodes.download_reference):
targetname = node['reftarget'] targetname = node['reftarget']
@ -140,8 +135,7 @@ class DownloadFileCollector(EnvironmentCollector):
node['filename'] = app.env.dlfiles.add_file(app.env.docname, rel_filename) node['filename'] = app.env.dlfiles.add_file(app.env.docname, rel_filename)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict
app.add_env_collector(ImageCollector) app.add_env_collector(ImageCollector)
app.add_env_collector(DownloadFileCollector) app.add_env_collector(DownloadFileCollector)

View File

@ -10,35 +10,30 @@
import os import os
from os import path from os import path
from typing import Any, Dict, Set
from docutils import nodes
from docutils.utils import relative_path from docutils.utils import relative_path
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.environment.collectors import EnvironmentCollector from sphinx.environment.collectors import EnvironmentCollector
from sphinx.util.osutil import fs_encoding from sphinx.util.osutil import fs_encoding
if False:
# For type annotation
from typing import Dict, Set # NOQA
from docutils import nodes # NOQA
from sphinx.sphinx import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
class DependenciesCollector(EnvironmentCollector): class DependenciesCollector(EnvironmentCollector):
"""dependencies collector for sphinx.environment.""" """dependencies collector for sphinx.environment."""
def clear_doc(self, app, env, docname): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
env.dependencies.pop(docname, None) env.dependencies.pop(docname, None)
def merge_other(self, app, env, docnames, other): def merge_other(self, app: Sphinx, env: BuildEnvironment,
# type: (Sphinx, BuildEnvironment, Set[str], BuildEnvironment) -> None docnames: Set[str], other: BuildEnvironment) -> None:
for docname in docnames: for docname in docnames:
if docname in other.dependencies: if docname in other.dependencies:
env.dependencies[docname] = other.dependencies[docname] env.dependencies[docname] = other.dependencies[docname]
def process_doc(self, app, doctree): def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
"""Process docutils-generated dependency info.""" """Process docutils-generated dependency info."""
cwd = os.getcwd() cwd = os.getcwd()
frompath = path.join(path.normpath(app.srcdir), 'dummy') frompath = path.join(path.normpath(app.srcdir), 'dummy')
@ -55,8 +50,7 @@ class DependenciesCollector(EnvironmentCollector):
app.env.dependencies[app.env.docname].add(relpath) app.env.dependencies[app.env.docname].add(relpath)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict
app.add_env_collector(DependenciesCollector) app.add_env_collector(DependenciesCollector)
return { return {

View File

@ -8,34 +8,31 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, Set
from docutils import nodes
from sphinx import addnodes from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.environment.collectors import EnvironmentCollector from sphinx.environment.collectors import EnvironmentCollector
from sphinx.util import split_index_msg, logging from sphinx.util import split_index_msg, logging
if False:
# For type annotation
from typing import Dict, Set # NOQA
from docutils import nodes # NOQA
from sphinx.applicatin import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class IndexEntriesCollector(EnvironmentCollector): class IndexEntriesCollector(EnvironmentCollector):
name = 'indices' name = 'indices'
def clear_doc(self, app, env, docname): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
env.indexentries.pop(docname, None) env.indexentries.pop(docname, None)
def merge_other(self, app, env, docnames, other): def merge_other(self, app: Sphinx, env: BuildEnvironment,
# type: (Sphinx, BuildEnvironment, Set[str], BuildEnvironment) -> None docnames: Set[str], other: BuildEnvironment) -> None:
for docname in docnames: for docname in docnames:
env.indexentries[docname] = other.indexentries[docname] env.indexentries[docname] = other.indexentries[docname]
def process_doc(self, app, doctree): def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
docname = app.env.docname docname = app.env.docname
entries = app.env.indexentries[docname] = [] entries = app.env.indexentries[docname] = []
for node in doctree.traverse(addnodes.index): for node in doctree.traverse(addnodes.index):
@ -50,8 +47,7 @@ class IndexEntriesCollector(EnvironmentCollector):
entries.append(entry) entries.append(entry)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict
app.add_env_collector(IndexEntriesCollector) app.add_env_collector(IndexEntriesCollector)
return { return {

View File

@ -8,33 +8,28 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import List, cast from typing import Any, Dict, List, Set
from typing import cast
from docutils import nodes from docutils import nodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.environment.collectors import EnvironmentCollector from sphinx.environment.collectors import EnvironmentCollector
if False:
# For type annotation
from typing import Dict, Set # NOQA
from sphinx.sphinx import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
class MetadataCollector(EnvironmentCollector): class MetadataCollector(EnvironmentCollector):
"""metadata collector for sphinx.environment.""" """metadata collector for sphinx.environment."""
def clear_doc(self, app, env, docname): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
env.metadata.pop(docname, None) env.metadata.pop(docname, None)
def merge_other(self, app, env, docnames, other): def merge_other(self, app: Sphinx, env: BuildEnvironment,
# type: (Sphinx, BuildEnvironment, Set[str], BuildEnvironment) -> None docnames: Set[str], other: BuildEnvironment) -> None:
for docname in docnames: for docname in docnames:
env.metadata[docname] = other.metadata[docname] env.metadata[docname] = other.metadata[docname]
def process_doc(self, app, doctree): def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
"""Process the docinfo part of the doctree as metadata. """Process the docinfo part of the doctree as metadata.
Keep processing minimal -- just return what docutils says. Keep processing minimal -- just return what docutils says.
@ -67,8 +62,7 @@ class MetadataCollector(EnvironmentCollector):
doctree.pop(0) doctree.pop(0)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict
app.add_env_collector(MetadataCollector) app.add_env_collector(MetadataCollector)
return { return {

View File

@ -8,34 +8,30 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, Set
from docutils import nodes from docutils import nodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.environment.collectors import EnvironmentCollector from sphinx.environment.collectors import EnvironmentCollector
from sphinx.transforms import SphinxContentsFilter from sphinx.transforms import SphinxContentsFilter
if False:
# For type annotation
from typing import Dict, Set # NOQA
from sphinx.sphinx import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
class TitleCollector(EnvironmentCollector): class TitleCollector(EnvironmentCollector):
"""title collector for sphinx.environment.""" """title collector for sphinx.environment."""
def clear_doc(self, app, env, docname): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
env.titles.pop(docname, None) env.titles.pop(docname, None)
env.longtitles.pop(docname, None) env.longtitles.pop(docname, None)
def merge_other(self, app, env, docnames, other): def merge_other(self, app: Sphinx, env: BuildEnvironment,
# type: (Sphinx, BuildEnvironment, Set[str], BuildEnvironment) -> None docnames: Set[str], other: BuildEnvironment) -> None:
for docname in docnames: for docname in docnames:
env.titles[docname] = other.titles[docname] env.titles[docname] = other.titles[docname]
env.longtitles[docname] = other.longtitles[docname] env.longtitles[docname] = other.longtitles[docname]
def process_doc(self, app, doctree): def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
"""Add a title node to the document (just copy the first section title), """Add a title node to the document (just copy the first section title),
and store that title in the environment. and store that title in the environment.
""" """
@ -59,8 +55,7 @@ class TitleCollector(EnvironmentCollector):
app.env.longtitles[app.env.docname] = longtitlenode app.env.longtitles[app.env.docname] = longtitlenode
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict
app.add_env_collector(TitleCollector) app.add_env_collector(TitleCollector)
return { return {

View File

@ -8,22 +8,21 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, List, Set, Tuple, Type, TypeVar
from typing import cast from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node
from sphinx import addnodes from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.environment.adapters.toctree import TocTree from sphinx.environment.adapters.toctree import TocTree
from sphinx.environment.collectors import EnvironmentCollector from sphinx.environment.collectors import EnvironmentCollector
from sphinx.locale import __ from sphinx.locale import __
from sphinx.transforms import SphinxContentsFilter from sphinx.transforms import SphinxContentsFilter
from sphinx.util import url_re, logging from sphinx.util import url_re, logging
if False:
# For type annotation
from typing import Dict, List, Set, Tuple, Type, TypeVar # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
N = TypeVar('N') N = TypeVar('N')
@ -31,8 +30,7 @@ logger = logging.getLogger(__name__)
class TocTreeCollector(EnvironmentCollector): class TocTreeCollector(EnvironmentCollector):
def clear_doc(self, app, env, docname): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
env.tocs.pop(docname, None) env.tocs.pop(docname, None)
env.toc_secnumbers.pop(docname, None) env.toc_secnumbers.pop(docname, None)
env.toc_fignumbers.pop(docname, None) env.toc_fignumbers.pop(docname, None)
@ -46,8 +44,8 @@ class TocTreeCollector(EnvironmentCollector):
if not fnset: if not fnset:
del env.files_to_rebuild[subfn] del env.files_to_rebuild[subfn]
def merge_other(self, app, env, docnames, other): def merge_other(self, app: Sphinx, env: BuildEnvironment, docnames: Set[str],
# type: (Sphinx, BuildEnvironment, Set[str], BuildEnvironment) -> None other: BuildEnvironment) -> None:
for docname in docnames: for docname in docnames:
env.tocs[docname] = other.tocs[docname] env.tocs[docname] = other.tocs[docname]
env.toc_num_entries[docname] = other.toc_num_entries[docname] env.toc_num_entries[docname] = other.toc_num_entries[docname]
@ -61,14 +59,12 @@ class TocTreeCollector(EnvironmentCollector):
for subfn, fnset in other.files_to_rebuild.items(): for subfn, fnset in other.files_to_rebuild.items():
env.files_to_rebuild.setdefault(subfn, set()).update(fnset & set(docnames)) env.files_to_rebuild.setdefault(subfn, set()).update(fnset & set(docnames))
def process_doc(self, app, doctree): def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
"""Build a TOC from the doctree and store it in the inventory.""" """Build a TOC from the doctree and store it in the inventory."""
docname = app.env.docname docname = app.env.docname
numentries = [0] # nonlocal again... numentries = [0] # nonlocal again...
def traverse_in_section(node, cls): def traverse_in_section(node: Element, cls: Type[N]) -> List[N]:
# type: (nodes.Element, Type[N]) -> List[N]
"""Like traverse(), but stay within the same section.""" """Like traverse(), but stay within the same section."""
result = [] # type: List[N] result = [] # type: List[N]
if isinstance(node, cls): if isinstance(node, cls):
@ -80,9 +76,8 @@ class TocTreeCollector(EnvironmentCollector):
result.extend(traverse_in_section(child, cls)) result.extend(traverse_in_section(child, cls))
return result return result
def build_toc(node, depth=1): def build_toc(node: Element, depth: int = 1) -> nodes.bullet_list:
# type: (nodes.Element, int) -> nodes.bullet_list entries = [] # type: List[Element]
entries = [] # type: List[nodes.Element]
for sectionnode in node: for sectionnode in node:
# find all toctree nodes in this section and add them # find all toctree nodes in this section and add them
# to the toc (just copying the toctree node which is then # to the toc (just copying the toctree node which is then
@ -107,7 +102,7 @@ class TocTreeCollector(EnvironmentCollector):
'', '', internal=True, refuri=docname, '', '', internal=True, refuri=docname,
anchorname=anchorname, *nodetext) anchorname=anchorname, *nodetext)
para = addnodes.compact_paragraph('', '', reference) para = addnodes.compact_paragraph('', '', reference)
item = nodes.list_item('', para) # type: nodes.Element item = nodes.list_item('', para) # type: Element
sub_item = build_toc(sectionnode, depth + 1) sub_item = build_toc(sectionnode, depth + 1)
if sub_item: if sub_item:
item += sub_item item += sub_item
@ -135,12 +130,10 @@ class TocTreeCollector(EnvironmentCollector):
app.env.tocs[docname] = nodes.bullet_list('') app.env.tocs[docname] = nodes.bullet_list('')
app.env.toc_num_entries[docname] = numentries[0] app.env.toc_num_entries[docname] = numentries[0]
def get_updated_docs(self, app, env): def get_updated_docs(self, app: Sphinx, env: BuildEnvironment) -> List[str]:
# type: (Sphinx, BuildEnvironment) -> List[str]
return self.assign_section_numbers(env) + self.assign_figure_numbers(env) return self.assign_section_numbers(env) + self.assign_figure_numbers(env)
def assign_section_numbers(self, env): def assign_section_numbers(self, env: BuildEnvironment) -> List[str]:
# type: (BuildEnvironment) -> List[str]
"""Assign a section number to each heading under a numbered toctree.""" """Assign a section number to each heading under a numbered toctree."""
# a list of all docnames whose section numbers changed # a list of all docnames whose section numbers changed
rewrite_needed = [] rewrite_needed = []
@ -149,8 +142,7 @@ class TocTreeCollector(EnvironmentCollector):
old_secnumbers = env.toc_secnumbers old_secnumbers = env.toc_secnumbers
env.toc_secnumbers = {} env.toc_secnumbers = {}
def _walk_toc(node, secnums, depth, titlenode=None): def _walk_toc(node: Element, secnums: Dict, depth: int, titlenode: nodes.title = None) -> None: # NOQA
# type: (nodes.Element, Dict, int, nodes.title) -> None
# titlenode is the title of the document, it will get assigned a # titlenode is the title of the document, it will get assigned a
# secnumber too, so that it shows up in next/prev/parent rellinks # secnumber too, so that it shows up in next/prev/parent rellinks
for subnode in node.children: for subnode in node.children:
@ -184,8 +176,7 @@ class TocTreeCollector(EnvironmentCollector):
elif isinstance(subnode, addnodes.toctree): elif isinstance(subnode, addnodes.toctree):
_walk_toctree(subnode, depth) _walk_toctree(subnode, depth)
def _walk_toctree(toctreenode, depth): def _walk_toctree(toctreenode: addnodes.toctree, depth: int) -> None:
# type: (addnodes.toctree, int) -> None
if depth == 0: if depth == 0:
return return
for (title, ref) in toctreenode['entries']: for (title, ref) in toctreenode['entries']:
@ -216,8 +207,7 @@ class TocTreeCollector(EnvironmentCollector):
return rewrite_needed return rewrite_needed
def assign_figure_numbers(self, env): def assign_figure_numbers(self, env: BuildEnvironment) -> List[str]:
# type: (BuildEnvironment) -> List[str]
"""Assign a figure number to each figure under a numbered toctree.""" """Assign a figure number to each figure under a numbered toctree."""
rewrite_needed = [] rewrite_needed = []
@ -227,8 +217,7 @@ class TocTreeCollector(EnvironmentCollector):
env.toc_fignumbers = {} env.toc_fignumbers = {}
fignum_counter = {} # type: Dict[str, Dict[Tuple[int, ...], int]] fignum_counter = {} # type: Dict[str, Dict[Tuple[int, ...], int]]
def get_figtype(node): def get_figtype(node: Node) -> str:
# type: (nodes.Node) -> str
for domain in env.domains.values(): for domain in env.domains.values():
figtype = domain.get_enumerable_node_type(node) figtype = domain.get_enumerable_node_type(node)
if figtype: if figtype:
@ -236,8 +225,7 @@ class TocTreeCollector(EnvironmentCollector):
return None return None
def get_section_number(docname, section): def get_section_number(docname: str, section: nodes.section) -> Tuple[int, ...]:
# type: (str, nodes.section) -> Tuple[int, ...]
anchorname = '#' + section['ids'][0] anchorname = '#' + section['ids'][0]
secnumbers = env.toc_secnumbers.get(docname, {}) secnumbers = env.toc_secnumbers.get(docname, {})
if anchorname in secnumbers: if anchorname in secnumbers:
@ -247,24 +235,22 @@ class TocTreeCollector(EnvironmentCollector):
return secnum or tuple() return secnum or tuple()
def get_next_fignumber(figtype, secnum): def get_next_fignumber(figtype: str, secnum: Tuple[int, ...]) -> Tuple[int, ...]:
# type: (str, Tuple[int, ...]) -> Tuple[int, ...]
counter = fignum_counter.setdefault(figtype, {}) counter = fignum_counter.setdefault(figtype, {})
secnum = secnum[:env.config.numfig_secnum_depth] secnum = secnum[:env.config.numfig_secnum_depth]
counter[secnum] = counter.get(secnum, 0) + 1 counter[secnum] = counter.get(secnum, 0) + 1
return secnum + (counter[secnum],) return secnum + (counter[secnum],)
def register_fignumber(docname, secnum, figtype, fignode): def register_fignumber(docname: str, secnum: Tuple[int, ...],
# type: (str, Tuple[int, ...], str, nodes.Element) -> None figtype: str, fignode: Element) -> None:
env.toc_fignumbers.setdefault(docname, {}) env.toc_fignumbers.setdefault(docname, {})
fignumbers = env.toc_fignumbers[docname].setdefault(figtype, {}) fignumbers = env.toc_fignumbers[docname].setdefault(figtype, {})
figure_id = fignode['ids'][0] figure_id = fignode['ids'][0]
fignumbers[figure_id] = get_next_fignumber(figtype, secnum) fignumbers[figure_id] = get_next_fignumber(figtype, secnum)
def _walk_doctree(docname, doctree, secnum): def _walk_doctree(docname: str, doctree: Element, secnum: Tuple[int, ...]) -> None:
# type: (str, nodes.Element, Tuple[int, ...]) -> None
for subnode in doctree.children: for subnode in doctree.children:
if isinstance(subnode, nodes.section): if isinstance(subnode, nodes.section):
next_secnum = get_section_number(docname, subnode) next_secnum = get_section_number(docname, subnode)
@ -286,8 +272,7 @@ class TocTreeCollector(EnvironmentCollector):
_walk_doctree(docname, subnode, secnum) _walk_doctree(docname, subnode, secnum)
def _walk_doc(docname, secnum): def _walk_doc(docname: str, secnum: Tuple[int, ...]) -> None:
# type: (str, Tuple[int, ...]) -> None
if docname not in assigned: if docname not in assigned:
assigned.add(docname) assigned.add(docname)
doctree = env.get_doctree(docname) doctree = env.get_doctree(docname)
@ -302,8 +287,7 @@ class TocTreeCollector(EnvironmentCollector):
return rewrite_needed return rewrite_needed
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict
app.add_env_collector(TocTreeCollector) app.add_env_collector(TocTreeCollector)
return { return {

View File

@ -22,6 +22,7 @@ import sys
import warnings import warnings
from fnmatch import fnmatch from fnmatch import fnmatch
from os import path from os import path
from typing import Any, List, Tuple
import sphinx.locale import sphinx.locale
from sphinx import __display_version__, package_dir from sphinx import __display_version__, package_dir
@ -32,10 +33,6 @@ from sphinx.util import rst
from sphinx.util.osutil import FileAvoidWrite, ensuredir from sphinx.util.osutil import FileAvoidWrite, ensuredir
from sphinx.util.template import ReSTRenderer from sphinx.util.template import ReSTRenderer
if False:
# For type annotation
from typing import Any, List, Tuple # NOQA
# automodule options # automodule options
if 'SPHINX_APIDOC_OPTIONS' in os.environ: if 'SPHINX_APIDOC_OPTIONS' in os.environ:
OPTIONS = os.environ['SPHINX_APIDOC_OPTIONS'].split(',') OPTIONS = os.environ['SPHINX_APIDOC_OPTIONS'].split(',')
@ -53,8 +50,7 @@ PY_SUFFIXES = {'.py', '.pyx'}
template_dir = path.join(package_dir, 'templates', 'apidoc') template_dir = path.join(package_dir, 'templates', 'apidoc')
def makename(package, module): def makename(package: str, module: str) -> str:
# type: (str, str) -> str
"""Join package and module with a dot.""" """Join package and module with a dot."""
warnings.warn('makename() is deprecated.', warnings.warn('makename() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
@ -68,14 +64,12 @@ def makename(package, module):
return name return name
def module_join(*modnames): def module_join(*modnames: str) -> str:
# type: (*str) -> str
"""Join module names with dots.""" """Join module names with dots."""
return '.'.join(filter(None, modnames)) return '.'.join(filter(None, modnames))
def write_file(name, text, opts): def write_file(name: str, text: str, opts: Any) -> None:
# type: (str, str, Any) -> None
"""Write the output file for module/package <name>.""" """Write the output file for module/package <name>."""
fname = path.join(opts.destdir, '%s.%s' % (name, opts.suffix)) fname = path.join(opts.destdir, '%s.%s' % (name, opts.suffix))
if opts.dryrun: if opts.dryrun:
@ -89,8 +83,7 @@ def write_file(name, text, opts):
f.write(text) f.write(text)
def format_heading(level, text, escape=True): def format_heading(level: int, text: str, escape: bool = True) -> str:
# type: (int, str, bool) -> str
"""Create a heading of <level> [1, 2 or 3 supported].""" """Create a heading of <level> [1, 2 or 3 supported]."""
warnings.warn('format_warning() is deprecated.', warnings.warn('format_warning() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
@ -100,8 +93,7 @@ def format_heading(level, text, escape=True):
return '%s\n%s\n\n' % (text, underlining) return '%s\n%s\n\n' % (text, underlining)
def format_directive(module, package=None): def format_directive(module: str, package: str = None) -> str:
# type: (str, str) -> str
"""Create the automodule directive and add the options.""" """Create the automodule directive and add the options."""
warnings.warn('format_directive() is deprecated.', warnings.warn('format_directive() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
@ -111,8 +103,8 @@ def format_directive(module, package=None):
return directive return directive
def create_module_file(package, basename, opts): def create_module_file(package: str, basename: str, opts: Any,
# type: (str, str, Any) -> None user_template_dir: str = None) -> None:
"""Build the text of the file and write the file.""" """Build the text of the file and write the file."""
qualname = module_join(package, basename) qualname = module_join(package, basename)
context = { context = {
@ -121,12 +113,13 @@ def create_module_file(package, basename, opts):
'qualname': qualname, 'qualname': qualname,
'automodule_options': OPTIONS, 'automodule_options': OPTIONS,
} }
text = ReSTRenderer(template_dir).render('module.rst', context) text = ReSTRenderer([user_template_dir, template_dir]).render('module.rst_t', context)
write_file(qualname, text, opts) write_file(qualname, text, opts)
def create_package_file(root, master_package, subroot, py_files, opts, subs, is_namespace, excludes=[]): # NOQA def create_package_file(root: str, master_package: str, subroot: str, py_files: List[str],
# type: (str, str, str, List[str], Any, List[str], bool, List[str]) -> None opts: Any, subs: List[str], is_namespace: bool,
excludes: List[str] = [], user_template_dir: str = None) -> None:
"""Build the text of the file and write the file.""" """Build the text of the file and write the file."""
# build a list of sub packages (directories containing an INITPY file) # build a list of sub packages (directories containing an INITPY file)
subpackages = [sub for sub in subs if not subpackages = [sub for sub in subs if not
@ -151,16 +144,16 @@ def create_package_file(root, master_package, subroot, py_files, opts, subs, is_
'automodule_options': OPTIONS, 'automodule_options': OPTIONS,
'show_headings': not opts.noheadings, 'show_headings': not opts.noheadings,
} }
text = ReSTRenderer(template_dir).render('package.rst', context) text = ReSTRenderer([user_template_dir, template_dir]).render('package.rst_t', context)
write_file(pkgname, text, opts) write_file(pkgname, text, opts)
if submodules and opts.separatemodules: if submodules and opts.separatemodules:
for submodule in submodules: for submodule in submodules:
create_module_file(None, submodule, opts) create_module_file(None, submodule, opts, user_template_dir)
def create_modules_toc_file(modules, opts, name='modules'): def create_modules_toc_file(modules: List[str], opts: Any, name: str = 'modules',
# type: (List[str], Any, str) -> None user_template_dir: str = None) -> None:
"""Create the module's index.""" """Create the module's index."""
modules.sort() modules.sort()
prev_module = '' prev_module = ''
@ -176,12 +169,11 @@ def create_modules_toc_file(modules, opts, name='modules'):
'maxdepth': opts.maxdepth, 'maxdepth': opts.maxdepth,
'docnames': modules, 'docnames': modules,
} }
text = ReSTRenderer(template_dir).render('toc.rst', context) text = ReSTRenderer([user_template_dir, template_dir]).render('toc.rst_t', context)
write_file(name, text, opts) write_file(name, text, opts)
def shall_skip(module, opts, excludes=[]): def shall_skip(module: str, opts: Any, excludes: List[str] = []) -> bool:
# type: (str, Any, List[str]) -> bool
"""Check if we want to skip this module.""" """Check if we want to skip this module."""
# skip if the file doesn't exist and not using implicit namespaces # skip if the file doesn't exist and not using implicit namespaces
if not opts.implicit_namespaces and not path.exists(module): if not opts.implicit_namespaces and not path.exists(module):
@ -207,8 +199,7 @@ def shall_skip(module, opts, excludes=[]):
return False return False
def is_skipped_module(filename, opts, excludes): def is_skipped_module(filename: str, opts: Any, excludes: List[str]) -> bool:
# type: (str, Any, List[str]) -> bool
"""Check if we want to skip this module.""" """Check if we want to skip this module."""
if not path.exists(filename): if not path.exists(filename):
# skip if the file doesn't exist # skip if the file doesn't exist
@ -220,8 +211,8 @@ def is_skipped_module(filename, opts, excludes):
return False return False
def recurse_tree(rootpath, excludes, opts): def recurse_tree(rootpath: str, excludes: List[str], opts: Any,
# type: (str, List[str], Any) -> List[str] user_template_dir: str = None) -> List[str]:
""" """
Look for every file in the directory tree and create the corresponding Look for every file in the directory tree and create the corresponding
ReST files. ReST files.
@ -271,7 +262,8 @@ def recurse_tree(rootpath, excludes, opts):
# a namespace and there is something there to document # a namespace and there is something there to document
if not is_namespace or len(py_files) > 0: if not is_namespace or len(py_files) > 0:
create_package_file(root, root_package, subpackage, create_package_file(root, root_package, subpackage,
py_files, opts, subs, is_namespace, excludes) py_files, opts, subs, is_namespace, excludes,
user_template_dir)
toplevels.append(module_join(root_package, subpackage)) toplevels.append(module_join(root_package, subpackage))
else: else:
# if we are at the root level, we don't require it to be a package # if we are at the root level, we don't require it to be a package
@ -279,14 +271,13 @@ def recurse_tree(rootpath, excludes, opts):
for py_file in py_files: for py_file in py_files:
if not is_skipped_module(path.join(rootpath, py_file), opts, excludes): if not is_skipped_module(path.join(rootpath, py_file), opts, excludes):
module = path.splitext(py_file)[0] module = path.splitext(py_file)[0]
create_module_file(root_package, module, opts) create_module_file(root_package, module, opts, user_template_dir)
toplevels.append(module) toplevels.append(module)
return toplevels return toplevels
def is_excluded(root, excludes): def is_excluded(root: str, excludes: List[str]) -> bool:
# type: (str, List[str]) -> bool
"""Check if the directory is in the exclude list. """Check if the directory is in the exclude list.
Note: by having trailing slashes, we avoid common prefix issues, like Note: by having trailing slashes, we avoid common prefix issues, like
@ -298,8 +289,7 @@ def is_excluded(root, excludes):
return False return False
def get_parser(): def get_parser() -> argparse.ArgumentParser:
# type: () -> argparse.ArgumentParser
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
usage='%(prog)s [OPTIONS] -o <OUTPUT_PATH> <MODULE_PATH> ' usage='%(prog)s [OPTIONS] -o <OUTPUT_PATH> <MODULE_PATH> '
'[EXCLUDE_PATTERN, ...]', '[EXCLUDE_PATTERN, ...]',
@ -386,11 +376,15 @@ Note: By default this script will not overwrite already created files."""))
const='sphinx.ext.%s' % ext, dest='extensions', const='sphinx.ext.%s' % ext, dest='extensions',
help=__('enable %s extension') % ext) help=__('enable %s extension') % ext)
group = parser.add_argument_group(__('Project templating'))
group.add_argument('-t', '--templatedir', metavar='TEMPLATEDIR',
dest='templatedir',
help=__('template directory for template files'))
return parser return parser
def main(argv=sys.argv[1:]): def main(argv: List[str] = sys.argv[1:]) -> int:
# type: (List[str]) -> int
"""Parse and check the command line arguments.""" """Parse and check the command line arguments."""
sphinx.locale.setlocale(locale.LC_ALL, '') sphinx.locale.setlocale(locale.LC_ALL, '')
sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx') sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx')
@ -412,7 +406,7 @@ def main(argv=sys.argv[1:]):
if not args.dryrun: if not args.dryrun:
ensuredir(args.destdir) ensuredir(args.destdir)
excludes = [path.abspath(exclude) for exclude in args.exclude_pattern] excludes = [path.abspath(exclude) for exclude in args.exclude_pattern]
modules = recurse_tree(rootpath, excludes, args) modules = recurse_tree(rootpath, excludes, args, args.templatedir)
if args.full: if args.full:
from sphinx.cmd import quickstart as qs from sphinx.cmd import quickstart as qs
@ -455,9 +449,10 @@ def main(argv=sys.argv[1:]):
d['extensions'].extend(ext.split(',')) d['extensions'].extend(ext.split(','))
if not args.dryrun: if not args.dryrun:
qs.generate(d, silent=True, overwrite=args.force) qs.generate(d, silent=True, overwrite=args.force,
templatedir=args.templatedir)
elif args.tocfile: elif args.tocfile:
create_modules_toc_file(modules, args, args.tocfile) create_modules_toc_file(modules, args, args.tocfile, args.templatedir)
return 0 return 0

View File

@ -12,15 +12,18 @@
import re import re
import warnings import warnings
from typing import Any from types import ModuleType
from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union
from docutils.statemachine import StringList from docutils.statemachine import StringList
import sphinx import sphinx
from sphinx.config import ENUM from sphinx.application import Sphinx
from sphinx.config import Config, ENUM
from sphinx.deprecation import ( from sphinx.deprecation import (
RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias
) )
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc.importer import import_object, get_object_members from sphinx.ext.autodoc.importer import import_object, get_object_members
from sphinx.ext.autodoc.mock import mock from sphinx.ext.autodoc.mock import mock
from sphinx.locale import _, __ from sphinx.locale import _, __
@ -35,12 +38,8 @@ from sphinx.util.inspect import (
if False: if False:
# For type annotation # For type annotation
from types import ModuleType # NOQA from sphinx.ext.autodoc.directive import DocumenterBridge
from typing import Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.config import Config # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.ext.autodoc.directive import DocumenterBridge # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -61,8 +60,7 @@ py_ext_sig_re = re.compile(
''', re.VERBOSE) ''', re.VERBOSE)
def identity(x): def identity(x: Any) -> Any:
# type: (Any) -> Any
return x return x
@ -71,16 +69,14 @@ INSTANCEATTR = object()
SLOTSATTR = object() SLOTSATTR = object()
def members_option(arg): def members_option(arg: Any) -> Union[object, List[str]]:
# type: (Any) -> Union[object, List[str]]
"""Used to convert the :members: option to auto directives.""" """Used to convert the :members: option to auto directives."""
if arg is None or arg is True: if arg is None or arg is True:
return ALL return ALL
return [x.strip() for x in arg.split(',')] return [x.strip() for x in arg.split(',')]
def members_set_option(arg): def members_set_option(arg: Any) -> Union[object, Set[str]]:
# type: (Any) -> Union[object, Set[str]]
"""Used to convert the :members: option to auto directives.""" """Used to convert the :members: option to auto directives."""
if arg is None: if arg is None:
return ALL return ALL
@ -90,8 +86,7 @@ def members_set_option(arg):
SUPPRESS = object() SUPPRESS = object()
def annotation_option(arg): def annotation_option(arg: Any) -> Any:
# type: (Any) -> Any
if arg is None: if arg is None:
# suppress showing the representation of the object # suppress showing the representation of the object
return SUPPRESS return SUPPRESS
@ -99,16 +94,14 @@ def annotation_option(arg):
return arg return arg
def bool_option(arg): def bool_option(arg: Any) -> bool:
# type: (Any) -> bool
"""Used to convert flag options to auto directives. (Instead of """Used to convert flag options to auto directives. (Instead of
directives.flag(), which returns None). directives.flag(), which returns None).
""" """
return True return True
def merge_special_members_option(options): def merge_special_members_option(options: Dict) -> None:
# type: (Dict) -> None
"""Merge :special-members: option to :members: option.""" """Merge :special-members: option to :members: option."""
if 'special-members' in options and options['special-members'] is not ALL: if 'special-members' in options and options['special-members'] is not ALL:
if options.get('members') is ALL: if options.get('members') is ALL:
@ -123,8 +116,7 @@ def merge_special_members_option(options):
# Some useful event listener factories for autodoc-process-docstring. # Some useful event listener factories for autodoc-process-docstring.
def cut_lines(pre, post=0, what=None): def cut_lines(pre: int, post: int = 0, what: str = None) -> Callable:
# type: (int, int, str) -> Callable
"""Return a listener that removes the first *pre* and last *post* """Return a listener that removes the first *pre* and last *post*
lines of every docstring. If *what* is a sequence of strings, lines of every docstring. If *what* is a sequence of strings,
only docstrings of a type in *what* will be processed. only docstrings of a type in *what* will be processed.
@ -136,8 +128,8 @@ def cut_lines(pre, post=0, what=None):
This can (and should) be used in place of :confval:`automodule_skip_lines`. This can (and should) be used in place of :confval:`automodule_skip_lines`.
""" """
def process(app, what_, name, obj, options, lines): def process(app: Sphinx, what_: str, name: str, obj: Any, options: Any, lines: List[str]
# type: (Sphinx, str, str, Any, Any, List[str]) -> None ) -> None:
if what and what_ not in what: if what and what_ not in what:
return return
del lines[:pre] del lines[:pre]
@ -152,8 +144,8 @@ def cut_lines(pre, post=0, what=None):
return process return process
def between(marker, what=None, keepempty=False, exclude=False): def between(marker: str, what: Sequence[str] = None, keepempty: bool = False,
# type: (str, Sequence[str], bool, bool) -> Callable exclude: bool = False) -> Callable:
"""Return a listener that either keeps, or if *exclude* is True excludes, """Return a listener that either keeps, or if *exclude* is True excludes,
lines between lines that match the *marker* regular expression. If no line lines between lines that match the *marker* regular expression. If no line
matches, the resulting docstring would be empty, so no change will be made matches, the resulting docstring would be empty, so no change will be made
@ -164,8 +156,8 @@ def between(marker, what=None, keepempty=False, exclude=False):
""" """
marker_re = re.compile(marker) marker_re = re.compile(marker)
def process(app, what_, name, obj, options, lines): def process(app: Sphinx, what_: str, name: str, obj: Any, options: Any, lines: List[str]
# type: (Sphinx, str, str, Any, Any, List[str]) -> None ) -> None:
if what and what_ not in what: if what and what_ not in what:
return return
deleted = 0 deleted = 0
@ -192,8 +184,7 @@ def between(marker, what=None, keepempty=False, exclude=False):
# But we define this class here to keep compatibility (see #4538) # But we define this class here to keep compatibility (see #4538)
class Options(dict): class Options(dict):
"""A dict/attribute hybrid that returns None on nonexisting keys.""" """A dict/attribute hybrid that returns None on nonexisting keys."""
def __getattr__(self, name): def __getattr__(self, name: str) -> Any:
# type: (str) -> Any
try: try:
return self[name.replace('_', '-')] return self[name.replace('_', '-')]
except KeyError: except KeyError:
@ -229,19 +220,17 @@ class Documenter:
option_spec = {'noindex': bool_option} # type: Dict[str, Callable] option_spec = {'noindex': bool_option} # type: Dict[str, Callable]
def get_attr(self, obj, name, *defargs): def get_attr(self, obj: Any, name: str, *defargs) -> Any:
# type: (Any, str, Any) -> Any
"""getattr() override for types such as Zope interfaces.""" """getattr() override for types such as Zope interfaces."""
return autodoc_attrgetter(self.env.app, obj, name, *defargs) return autodoc_attrgetter(self.env.app, obj, name, *defargs)
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
"""Called to see if a member can be documented by this documenter.""" """Called to see if a member can be documented by this documenter."""
raise NotImplementedError('must be implemented in subclasses') raise NotImplementedError('must be implemented in subclasses')
def __init__(self, directive, name, indent=''): def __init__(self, directive: "DocumenterBridge", name: str, indent: str = '') -> None:
# type: (DocumenterBridge, str, str) -> None
self.directive = directive self.directive = directive
self.env = directive.env # type: BuildEnvironment self.env = directive.env # type: BuildEnvironment
self.options = directive.genopt self.options = directive.genopt
@ -266,18 +255,16 @@ class Documenter:
self.analyzer = None # type: ModuleAnalyzer self.analyzer = None # type: ModuleAnalyzer
@property @property
def documenters(self): def documenters(self) -> Dict[str, Type["Documenter"]]:
# type: () -> Dict[str, Type[Documenter]]
"""Returns registered Documenter classes""" """Returns registered Documenter classes"""
return get_documenters(self.env.app) return get_documenters(self.env.app)
def add_line(self, line, source, *lineno): def add_line(self, line: str, source: str, *lineno: int) -> None:
# type: (str, str, int) -> None
"""Append one line of generated reST to the output.""" """Append one line of generated reST to the output."""
self.directive.result.append(self.indent + line, source, *lineno) self.directive.result.append(self.indent + line, source, *lineno)
def resolve_name(self, modname, parents, path, base): def resolve_name(self, modname: str, parents: Any, path: str, base: Any
# type: (str, Any, str, Any) -> Tuple[str, List[str]] ) -> Tuple[str, List[str]]:
"""Resolve the module and name of the object to document given by the """Resolve the module and name of the object to document given by the
arguments and the current module/class. arguments and the current module/class.
@ -287,8 +274,7 @@ class Documenter:
""" """
raise NotImplementedError('must be implemented in subclasses') raise NotImplementedError('must be implemented in subclasses')
def parse_name(self): def parse_name(self) -> bool:
# type: () -> bool
"""Determine what module to import and what attribute to document. """Determine what module to import and what attribute to document.
Returns True and sets *self.modname*, *self.objpath*, *self.fullname*, Returns True and sets *self.modname*, *self.objpath*, *self.fullname*,
@ -324,8 +310,7 @@ class Documenter:
(self.objpath and '.' + '.'.join(self.objpath) or '') (self.objpath and '.' + '.'.join(self.objpath) or '')
return True return True
def import_object(self): def import_object(self) -> bool:
# type: () -> bool
"""Import the object given by *self.modname* and *self.objpath* and set """Import the object given by *self.modname* and *self.objpath* and set
it as *self.object*. it as *self.object*.
@ -343,8 +328,7 @@ class Documenter:
self.env.note_reread() self.env.note_reread()
return False return False
def get_real_modname(self): def get_real_modname(self) -> str:
# type: () -> str
"""Get the real module name of an object to document. """Get the real module name of an object to document.
It can differ from the name of the module through which the object was It can differ from the name of the module through which the object was
@ -352,8 +336,7 @@ class Documenter:
""" """
return self.get_attr(self.object, '__module__', None) or self.modname return self.get_attr(self.object, '__module__', None) or self.modname
def check_module(self): def check_module(self) -> bool:
# type: () -> bool
"""Check if *self.object* is really defined in the module given by """Check if *self.object* is really defined in the module given by
*self.modname*. *self.modname*.
""" """
@ -367,16 +350,14 @@ class Documenter:
return False return False
return True return True
def format_args(self, **kwargs): def format_args(self, **kwargs) -> str:
# type: (Any) -> str
"""Format the argument signature of *self.object*. """Format the argument signature of *self.object*.
Should return None if the object does not have a signature. Should return None if the object does not have a signature.
""" """
return None return None
def format_name(self): def format_name(self) -> str:
# type: () -> str
"""Format the name of *self.object*. """Format the name of *self.object*.
This normally should be something that can be parsed by the generated This normally should be something that can be parsed by the generated
@ -387,8 +368,7 @@ class Documenter:
# directives of course) # directives of course)
return '.'.join(self.objpath) or self.modname return '.'.join(self.objpath) or self.modname
def format_signature(self, **kwargs): def format_signature(self, **kwargs) -> str:
# type: (Any) -> str
"""Format the signature (arguments and return annotation) of the object. """Format the signature (arguments and return annotation) of the object.
Let the user process it via the ``autodoc-process-signature`` event. Let the user process it via the ``autodoc-process-signature`` event.
@ -422,8 +402,7 @@ class Documenter:
else: else:
return '' return ''
def add_directive_header(self, sig): def add_directive_header(self, sig: str) -> None:
# type: (str) -> None
"""Add the directive header and options to the generated content.""" """Add the directive header and options to the generated content."""
domain = getattr(self, 'domain', 'py') domain = getattr(self, 'domain', 'py')
directive = getattr(self, 'directivetype', self.objtype) directive = getattr(self, 'directivetype', self.objtype)
@ -438,8 +417,7 @@ class Documenter:
# etc. don't support a prepended module name # etc. don't support a prepended module name
self.add_line(' :module: %s' % self.modname, sourcename) self.add_line(' :module: %s' % self.modname, sourcename)
def get_doc(self, encoding=None, ignore=1): def get_doc(self, encoding: str = None, ignore: int = 1) -> List[List[str]]:
# type: (str, int) -> List[List[str]]
"""Decode and return lines of the docstring(s) for the object.""" """Decode and return lines of the docstring(s) for the object."""
if encoding is not None: if encoding is not None:
warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated."
@ -452,8 +430,7 @@ class Documenter:
return [prepare_docstring(docstring, ignore, tab_width)] return [prepare_docstring(docstring, ignore, tab_width)]
return [] return []
def process_doc(self, docstrings): def process_doc(self, docstrings: List[List[str]]) -> Iterator[str]:
# type: (List[List[str]]) -> Iterator[str]
"""Let the user process the docstrings before adding them.""" """Let the user process the docstrings before adding them."""
for docstringlines in docstrings: for docstringlines in docstrings:
if self.env.app: if self.env.app:
@ -463,14 +440,12 @@ class Documenter:
self.options, docstringlines) self.options, docstringlines)
yield from docstringlines yield from docstringlines
def get_sourcename(self): def get_sourcename(self) -> str:
# type: () -> str
if self.analyzer: if self.analyzer:
return '%s:docstring of %s' % (self.analyzer.srcname, self.fullname) return '%s:docstring of %s' % (self.analyzer.srcname, self.fullname)
return 'docstring of %s' % self.fullname return 'docstring of %s' % self.fullname
def add_content(self, more_content, no_docstring=False): def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
# type: (Any, bool) -> None
"""Add content from docstrings, attribute documentation and user.""" """Add content from docstrings, attribute documentation and user."""
# set sourcename and add content from attribute documentation # set sourcename and add content from attribute documentation
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
@ -500,8 +475,7 @@ class Documenter:
for line, src in zip(more_content.data, more_content.items): for line, src in zip(more_content.data, more_content.items):
self.add_line(line, src[0], src[1]) self.add_line(line, src[0], src[1])
def get_object_members(self, want_all): def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]:
# type: (bool) -> Tuple[bool, List[Tuple[str, Any]]]
"""Return `(members_check_module, members)` where `members` is a """Return `(members_check_module, members)` where `members` is a
list of `(membername, member)` pairs of the members of *self.object*. list of `(membername, member)` pairs of the members of *self.object*.
@ -527,8 +501,8 @@ class Documenter:
return False, sorted((m.name, m.value) for m in members.values() return False, sorted((m.name, m.value) for m in members.values()
if m.directly_defined) if m.directly_defined)
def filter_members(self, members, want_all): def filter_members(self, members: List[Tuple[str, Any]], want_all: bool
# type: (List[Tuple[str, Any]], bool) -> List[Tuple[str, Any, bool]] ) -> List[Tuple[str, Any, bool]]:
"""Filter the given member list. """Filter the given member list.
Members are skipped if Members are skipped if
@ -616,8 +590,7 @@ class Documenter:
return ret return ret
def document_members(self, all_members=False): def document_members(self, all_members: bool = False) -> None:
# type: (bool) -> None
"""Generate reST for member documentation. """Generate reST for member documentation.
If *all_members* is True, do all members, else those given by If *all_members* is True, do all members, else those given by
@ -669,8 +642,7 @@ class Documenter:
# sort by source order, by virtue of the module analyzer # sort by source order, by virtue of the module analyzer
tagorder = self.analyzer.tagorder tagorder = self.analyzer.tagorder
def keyfunc(entry): def keyfunc(entry: Tuple[Documenter, bool]) -> int:
# type: (Tuple[Documenter, bool]) -> int
fullname = entry[0].name.split('::')[1] fullname = entry[0].name.split('::')[1]
return tagorder.get(fullname, len(tagorder)) return tagorder.get(fullname, len(tagorder))
memberdocumenters.sort(key=keyfunc) memberdocumenters.sort(key=keyfunc)
@ -684,9 +656,8 @@ class Documenter:
self.env.temp_data['autodoc:module'] = None self.env.temp_data['autodoc:module'] = None
self.env.temp_data['autodoc:class'] = None self.env.temp_data['autodoc:class'] = None
def generate(self, more_content=None, real_modname=None, def generate(self, more_content: Any = None, real_modname: str = None,
check_module=False, all_members=False): check_module: bool = False, all_members: bool = False) -> None:
# type: (Any, str, bool, bool) -> None
"""Generate reST for the object given by *self.name*, and possibly for """Generate reST for the object given by *self.name*, and possibly for
its members. its members.
@ -778,26 +749,24 @@ class ModuleDocumenter(Documenter):
'imported-members': bool_option, 'ignore-module-all': bool_option 'imported-members': bool_option, 'ignore-module-all': bool_option
} # type: Dict[str, Callable] } # type: Dict[str, Callable]
def __init__(self, *args): def __init__(self, *args) -> None:
# type: (Any) -> None
super().__init__(*args) super().__init__(*args)
merge_special_members_option(self.options) merge_special_members_option(self.options)
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
# don't document submodules automatically # don't document submodules automatically
return False return False
def resolve_name(self, modname, parents, path, base): def resolve_name(self, modname: str, parents: Any, path: str, base: Any
# type: (str, Any, str, Any) -> Tuple[str, List[str]] ) -> Tuple[str, List[str]]:
if modname is not None: if modname is not None:
logger.warning(__('"::" in automodule name doesn\'t make sense'), logger.warning(__('"::" in automodule name doesn\'t make sense'),
type='autodoc') type='autodoc')
return (path or '') + base, [] return (path or '') + base, []
def parse_name(self): def parse_name(self) -> bool:
# type: () -> bool
ret = super().parse_name() ret = super().parse_name()
if self.args or self.retann: if self.args or self.retann:
logger.warning(__('signature arguments or return annotation ' logger.warning(__('signature arguments or return annotation '
@ -805,8 +774,7 @@ class ModuleDocumenter(Documenter):
type='autodoc') type='autodoc')
return ret return ret
def add_directive_header(self, sig): def add_directive_header(self, sig: str) -> None:
# type: (str) -> None
Documenter.add_directive_header(self, sig) Documenter.add_directive_header(self, sig)
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
@ -819,8 +787,7 @@ class ModuleDocumenter(Documenter):
if self.options.deprecated: if self.options.deprecated:
self.add_line(' :deprecated:', sourcename) self.add_line(' :deprecated:', sourcename)
def get_object_members(self, want_all): def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, object]]]:
# type: (bool) -> Tuple[bool, List[Tuple[str, object]]]
if want_all: if want_all:
if (self.options.ignore_module_all or not if (self.options.ignore_module_all or not
hasattr(self.object, '__all__')): hasattr(self.object, '__all__')):
@ -861,8 +828,8 @@ class ModuleLevelDocumenter(Documenter):
Specialized Documenter subclass for objects on module level (functions, Specialized Documenter subclass for objects on module level (functions,
classes, data/constants). classes, data/constants).
""" """
def resolve_name(self, modname, parents, path, base): def resolve_name(self, modname: str, parents: Any, path: str, base: Any
# type: (str, Any, str, Any) -> Tuple[str, List[str]] ) -> Tuple[str, List[str]]:
if modname is None: if modname is None:
if path: if path:
modname = path.rstrip('.') modname = path.rstrip('.')
@ -882,8 +849,8 @@ class ClassLevelDocumenter(Documenter):
Specialized Documenter subclass for objects on class level (methods, Specialized Documenter subclass for objects on class level (methods,
attributes). attributes).
""" """
def resolve_name(self, modname, parents, path, base): def resolve_name(self, modname: str, parents: Any, path: str, base: Any
# type: (str, Any, str, Any) -> Tuple[str, List[str]] ) -> Tuple[str, List[str]]:
if modname is None: if modname is None:
if path: if path:
mod_cls = path.rstrip('.') mod_cls = path.rstrip('.')
@ -916,8 +883,7 @@ class DocstringSignatureMixin:
feature of reading the signature from the docstring. feature of reading the signature from the docstring.
""" """
def _find_signature(self, encoding=None): def _find_signature(self, encoding: str = None) -> Tuple[str, str]:
# type: (str) -> Tuple[str, str]
if encoding is not None: if encoding is not None:
warnings.warn("The 'encoding' argument to autodoc.%s._find_signature() is " warnings.warn("The 'encoding' argument to autodoc.%s._find_signature() is "
"deprecated." % self.__class__.__name__, "deprecated." % self.__class__.__name__,
@ -951,8 +917,7 @@ class DocstringSignatureMixin:
break break
return result return result
def get_doc(self, encoding=None, ignore=1): def get_doc(self, encoding: str = None, ignore: int = 1) -> List[List[str]]:
# type: (str, int) -> List[List[str]]
if encoding is not None: if encoding is not None:
warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__, % self.__class__.__name__,
@ -962,8 +927,7 @@ class DocstringSignatureMixin:
return lines return lines
return super().get_doc(None, ignore) # type: ignore return super().get_doc(None, ignore) # type: ignore
def format_signature(self, **kwargs): def format_signature(self, **kwargs) -> str:
# type: (Any) -> str
if self.args is None and self.env.config.autodoc_docstring_signature: # type: ignore if self.args is None and self.env.config.autodoc_docstring_signature: # type: ignore
# only act if a signature is not explicitly given already, and if # only act if a signature is not explicitly given already, and if
# the feature is enabled # the feature is enabled
@ -978,8 +942,7 @@ class DocstringStripSignatureMixin(DocstringSignatureMixin):
Mixin for AttributeDocumenter to provide the Mixin for AttributeDocumenter to provide the
feature of stripping any function signature from the docstring. feature of stripping any function signature from the docstring.
""" """
def format_signature(self, **kwargs): def format_signature(self, **kwargs) -> str:
# type: (Any) -> str
if self.args is None and self.env.config.autodoc_docstring_signature: # type: ignore if self.args is None and self.env.config.autodoc_docstring_signature: # type: ignore
# only act if a signature is not explicitly given already, and if # only act if a signature is not explicitly given already, and if
# the feature is enabled # the feature is enabled
@ -1000,14 +963,13 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
member_order = 30 member_order = 30
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
# supports functions, builtins and bound methods exported at the module level # supports functions, builtins and bound methods exported at the module level
return (inspect.isfunction(member) or inspect.isbuiltin(member) or return (inspect.isfunction(member) or inspect.isbuiltin(member) or
(inspect.isroutine(member) and isinstance(parent, ModuleDocumenter))) (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter)))
def format_args(self, **kwargs): def format_args(self, **kwargs) -> str:
# type: (Any) -> str
if self.env.config.autodoc_typehints == 'none': if self.env.config.autodoc_typehints == 'none':
kwargs.setdefault('show_annotation', False) kwargs.setdefault('show_annotation', False)
@ -1042,12 +1004,10 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
args = args.replace('\\', '\\\\') args = args.replace('\\', '\\\\')
return args return args
def document_members(self, all_members=False): def document_members(self, all_members: bool = False) -> None:
# type: (bool) -> None
pass pass
def add_directive_header(self, sig): def add_directive_header(self, sig: str) -> None:
# type: (str) -> None
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
super().add_directive_header(sig) super().add_directive_header(sig)
@ -1086,18 +1046,16 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
'private-members': bool_option, 'special-members': members_option, 'private-members': bool_option, 'special-members': members_option,
} # type: Dict[str, Callable] } # type: Dict[str, Callable]
def __init__(self, *args): def __init__(self, *args) -> None:
# type: (Any) -> None
super().__init__(*args) super().__init__(*args)
merge_special_members_option(self.options) merge_special_members_option(self.options)
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
return isinstance(member, type) return isinstance(member, type)
def import_object(self): def import_object(self) -> Any:
# type: () -> Any
ret = super().import_object() ret = super().import_object()
# if the class is documented under another name, document it # if the class is documented under another name, document it
# as data/attribute # as data/attribute
@ -1108,8 +1066,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
self.doc_as_attr = True self.doc_as_attr = True
return ret return ret
def format_args(self, **kwargs): def format_args(self, **kwargs) -> str:
# type: (Any) -> str
if self.env.config.autodoc_typehints == 'none': if self.env.config.autodoc_typehints == 'none':
kwargs.setdefault('show_annotation', False) kwargs.setdefault('show_annotation', False)
@ -1129,15 +1086,13 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
# with __init__ in C # with __init__ in C
return None return None
def format_signature(self, **kwargs): def format_signature(self, **kwargs) -> str:
# type: (Any) -> str
if self.doc_as_attr: if self.doc_as_attr:
return '' return ''
return super().format_signature(**kwargs) return super().format_signature(**kwargs)
def add_directive_header(self, sig): def add_directive_header(self, sig: str) -> None:
# type: (str) -> None
if self.doc_as_attr: if self.doc_as_attr:
self.directivetype = 'attribute' self.directivetype = 'attribute'
super().add_directive_header(sig) super().add_directive_header(sig)
@ -1154,8 +1109,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), self.add_line(' ' + _('Bases: %s') % ', '.join(bases),
sourcename) sourcename)
def get_doc(self, encoding=None, ignore=1): def get_doc(self, encoding: str = None, ignore: int = 1) -> List[List[str]]:
# type: (str, int) -> List[List[str]]
if encoding is not None: if encoding is not None:
warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__, % self.__class__.__name__,
@ -1199,8 +1153,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
tab_width = self.directive.state.document.settings.tab_width tab_width = self.directive.state.document.settings.tab_width
return [prepare_docstring(docstring, ignore, tab_width) for docstring in docstrings] return [prepare_docstring(docstring, ignore, tab_width) for docstring in docstrings]
def add_content(self, more_content, no_docstring=False): def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
# type: (Any, bool) -> None
if self.doc_as_attr: if self.doc_as_attr:
classname = safe_getattr(self.object, '__qualname__', None) classname = safe_getattr(self.object, '__qualname__', None)
if not classname: if not classname:
@ -1215,15 +1168,13 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
else: else:
super().add_content(more_content) super().add_content(more_content)
def document_members(self, all_members=False): def document_members(self, all_members: bool = False) -> None:
# type: (bool) -> None
if self.doc_as_attr: if self.doc_as_attr:
return return
super().document_members(all_members) super().document_members(all_members)
def generate(self, more_content=None, real_modname=None, def generate(self, more_content: Any = None, real_modname: str = None,
check_module=False, all_members=False): check_module: bool = False, all_members: bool = False) -> None:
# type: (Any, str, bool, bool) -> None
# Do not pass real_modname and use the name from the __module__ # Do not pass real_modname and use the name from the __module__
# attribute of the class. # attribute of the class.
# If a class gets imported into the module real_modname # If a class gets imported into the module real_modname
@ -1245,8 +1196,8 @@ class ExceptionDocumenter(ClassDocumenter):
priority = 10 priority = 10
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
return isinstance(member, type) and issubclass(member, BaseException) return isinstance(member, type) and issubclass(member, BaseException)
@ -1261,12 +1212,11 @@ class DataDocumenter(ModuleLevelDocumenter):
option_spec["annotation"] = annotation_option option_spec["annotation"] = annotation_option
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
return isinstance(parent, ModuleDocumenter) and isattr return isinstance(parent, ModuleDocumenter) and isattr
def add_directive_header(self, sig): def add_directive_header(self, sig: str) -> None:
# type: (str) -> None
super().add_directive_header(sig) super().add_directive_header(sig)
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
if not self.options.annotation: if not self.options.annotation:
@ -1282,12 +1232,10 @@ class DataDocumenter(ModuleLevelDocumenter):
self.add_line(' :annotation: %s' % self.options.annotation, self.add_line(' :annotation: %s' % self.options.annotation,
sourcename) sourcename)
def document_members(self, all_members=False): def document_members(self, all_members: bool = False) -> None:
# type: (bool) -> None
pass pass
def get_real_modname(self): def get_real_modname(self) -> str:
# type: () -> str
return self.get_attr(self.parent or self.object, '__module__', None) \ return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname or self.modname
@ -1302,13 +1250,12 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
priority = 1 # must be more than FunctionDocumenter priority = 1 # must be more than FunctionDocumenter
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
return inspect.isroutine(member) and \ return inspect.isroutine(member) and \
not isinstance(parent, ModuleDocumenter) not isinstance(parent, ModuleDocumenter)
def import_object(self): def import_object(self) -> Any:
# type: () -> Any
ret = super().import_object() ret = super().import_object()
if not ret: if not ret:
return ret return ret
@ -1325,8 +1272,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return ret return ret
def format_args(self, **kwargs): def format_args(self, **kwargs) -> str:
# type: (Any) -> str
if self.env.config.autodoc_typehints == 'none': if self.env.config.autodoc_typehints == 'none':
kwargs.setdefault('show_annotation', False) kwargs.setdefault('show_annotation', False)
@ -1341,8 +1287,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
args = args.replace('\\', '\\\\') args = args.replace('\\', '\\\\')
return args return args
def add_directive_header(self, sig): def add_directive_header(self, sig) -> None:
# type: (str) -> None
super().add_directive_header(sig) super().add_directive_header(sig)
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
@ -1356,8 +1301,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name): if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
self.add_line(' :staticmethod:', sourcename) self.add_line(' :staticmethod:', sourcename)
def document_members(self, all_members=False): def document_members(self, all_members: bool = False) -> None:
# type: (bool) -> None
pass pass
@ -1375,13 +1319,12 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
priority = 10 priority = 10
@staticmethod @staticmethod
def is_function_or_method(obj): def is_function_or_method(obj: Any) -> bool:
# type: (Any) -> bool
return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj) return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj)
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
if inspect.isattributedescriptor(member): if inspect.isattributedescriptor(member):
return True return True
elif (not isinstance(parent, ModuleDocumenter) and elif (not isinstance(parent, ModuleDocumenter) and
@ -1391,12 +1334,10 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
else: else:
return False return False
def document_members(self, all_members=False): def document_members(self, all_members: bool = False) -> None:
# type: (bool) -> None
pass pass
def import_object(self): def import_object(self) -> Any:
# type: () -> Any
ret = super().import_object() ret = super().import_object()
if inspect.isenumattribute(self.object): if inspect.isenumattribute(self.object):
self.object = self.object.value self.object = self.object.value
@ -1407,13 +1348,11 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
self._datadescriptor = False self._datadescriptor = False
return ret return ret
def get_real_modname(self): def get_real_modname(self) -> str:
# type: () -> str
return self.get_attr(self.parent or self.object, '__module__', None) \ return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname or self.modname
def add_directive_header(self, sig): def add_directive_header(self, sig: str) -> None:
# type: (str) -> None
super().add_directive_header(sig) super().add_directive_header(sig)
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
if not self.options.annotation: if not self.options.annotation:
@ -1429,8 +1368,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
else: else:
self.add_line(' :annotation: %s' % self.options.annotation, sourcename) self.add_line(' :annotation: %s' % self.options.annotation, sourcename)
def add_content(self, more_content, no_docstring=False): def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
# type: (Any, bool) -> None
if not self._datadescriptor: if not self._datadescriptor:
# if it's not a data descriptor, its docstring is very probably the # if it's not a data descriptor, its docstring is very probably the
# wrong thing to display # wrong thing to display
@ -1450,21 +1388,18 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
priority = AttributeDocumenter.priority + 1 priority = AttributeDocumenter.priority + 1
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
return inspect.isproperty(member) and isinstance(parent, ClassDocumenter) return inspect.isproperty(member) and isinstance(parent, ClassDocumenter)
def document_members(self, all_members=False): def document_members(self, all_members: bool = False) -> None:
# type: (bool) -> None
pass pass
def get_real_modname(self): def get_real_modname(self) -> str:
# type: () -> str
return self.get_attr(self.parent or self.object, '__module__', None) \ return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname or self.modname
def add_directive_header(self, sig): def add_directive_header(self, sig: str) -> None:
# type: (str) -> None
super().add_directive_header(sig) super().add_directive_header(sig)
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
if inspect.isabstractmethod(self.object): if inspect.isabstractmethod(self.object):
@ -1485,21 +1420,19 @@ class InstanceAttributeDocumenter(AttributeDocumenter):
priority = 11 priority = 11
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
"""This documents only INSTANCEATTR members.""" """This documents only INSTANCEATTR members."""
return isattr and (member is INSTANCEATTR) return isattr and (member is INSTANCEATTR)
def import_object(self): def import_object(self) -> bool:
# type: () -> bool
"""Never import anything.""" """Never import anything."""
# disguise as an attribute # disguise as an attribute
self.objtype = 'attribute' self.objtype = 'attribute'
self._datadescriptor = False self._datadescriptor = False
return True return True
def add_content(self, more_content, no_docstring=False): def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
# type: (Any, bool) -> None
"""Never try to get a docstring from the object.""" """Never try to get a docstring from the object."""
super().add_content(more_content, no_docstring=True) super().add_content(more_content, no_docstring=True)
@ -1517,13 +1450,12 @@ class SlotsAttributeDocumenter(AttributeDocumenter):
priority = 11 priority = 11
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
# type: (Any, str, bool, Any) -> bool ) -> bool:
"""This documents only SLOTSATTR members.""" """This documents only SLOTSATTR members."""
return member is SLOTSATTR return member is SLOTSATTR
def import_object(self): def import_object(self) -> Any:
# type: () -> bool
"""Never import anything.""" """Never import anything."""
# disguise as an attribute # disguise as an attribute
self.objtype = 'attribute' self.objtype = 'attribute'
@ -1541,8 +1473,7 @@ class SlotsAttributeDocumenter(AttributeDocumenter):
self.env.note_reread() self.env.note_reread()
return False return False
def get_doc(self, encoding=None, ignore=1): def get_doc(self, encoding: str = None, ignore: int = 1) -> List[List[str]]:
# type: (str, int) -> List[List[str]]
"""Decode and return lines of the docstring(s) for the object.""" """Decode and return lines of the docstring(s) for the object."""
name = self.objpath[-1] name = self.objpath[-1]
__slots__ = safe_getattr(self.parent, '__slots__', []) __slots__ = safe_getattr(self.parent, '__slots__', [])
@ -1553,14 +1484,12 @@ class SlotsAttributeDocumenter(AttributeDocumenter):
return [] return []
def get_documenters(app): def get_documenters(app: Sphinx) -> Dict[str, Type[Documenter]]:
# type: (Sphinx) -> Dict[str, Type[Documenter]]
"""Returns registered Documenter classes""" """Returns registered Documenter classes"""
return app.registry.documenters return app.registry.documenters
def autodoc_attrgetter(app, obj, name, *defargs): def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs) -> Any:
# type: (Sphinx, Any, str, Any) -> Any
"""Alternative getattr() for types""" """Alternative getattr() for types"""
for typ, func in app.registry.autodoc_attrgettrs.items(): for typ, func in app.registry.autodoc_attrgettrs.items():
if isinstance(obj, typ): if isinstance(obj, typ):
@ -1569,8 +1498,7 @@ def autodoc_attrgetter(app, obj, name, *defargs):
return safe_getattr(obj, name, *defargs) return safe_getattr(obj, name, *defargs)
def merge_autodoc_default_flags(app, config): def merge_autodoc_default_flags(app: Sphinx, config: Config) -> None:
# type: (Sphinx, Config) -> None
"""This merges the autodoc_default_flags to autodoc_default_options.""" """This merges the autodoc_default_flags to autodoc_default_options."""
if not config.autodoc_default_flags: if not config.autodoc_default_flags:
return return
@ -1602,8 +1530,7 @@ deprecated_alias('sphinx.ext.autodoc',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_autodocumenter(ModuleDocumenter) app.add_autodocumenter(ModuleDocumenter)
app.add_autodocumenter(ClassDocumenter) app.add_autodocumenter(ClassDocumenter)
app.add_autodocumenter(ExceptionDocumenter) app.add_autodocumenter(ExceptionDocumenter)

View File

@ -7,27 +7,22 @@
""" """
import warnings import warnings
from typing import Any, Callable, Dict, List, Set, Type
from docutils import nodes from docutils import nodes
from docutils.parsers.rst.states import Struct from docutils.nodes import Element, Node
from docutils.parsers.rst.states import RSTState, Struct
from docutils.statemachine import StringList from docutils.statemachine import StringList
from docutils.utils import assemble_option_dict from docutils.utils import Reporter, assemble_option_dict
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.ext.autodoc import Options, get_documenters from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc import Documenter, Options, get_documenters
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective, switch_source_input from sphinx.util.docutils import SphinxDirective, switch_source_input
from sphinx.util.nodes import nested_parse_with_titles from sphinx.util.nodes import nested_parse_with_titles
if False:
# For type annotation
from typing import Any, Callable, Dict, List, Set, Type # NOQA
from docutils.parsers.rst.state import RSTState # NOQA
from docutils.utils import Reporter # NOQA
from sphinx.config import Config # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.ext.autodoc import Documenter # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,21 +36,19 @@ AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members',
class DummyOptionSpec(dict): class DummyOptionSpec(dict):
"""An option_spec allows any options.""" """An option_spec allows any options."""
def __bool__(self): def __bool__(self) -> bool:
# type: () -> bool
"""Behaves like some options are defined.""" """Behaves like some options are defined."""
return True return True
def __getitem__(self, key): def __getitem__(self, key: str) -> Callable[[str], str]:
# type: (str) -> Callable[[str], str]
return lambda x: x return lambda x: x
class DocumenterBridge: class DocumenterBridge:
"""A parameters container for Documenters.""" """A parameters container for Documenters."""
def __init__(self, env, reporter, options, lineno, state=None): def __init__(self, env: BuildEnvironment, reporter: Reporter, options: Options,
# type: (BuildEnvironment, Reporter, Options, int, Any) -> None lineno: int, state: Any = None) -> None:
self.env = env self.env = env
self.reporter = reporter self.reporter = reporter
self.genopt = options self.genopt = options
@ -73,13 +66,12 @@ class DocumenterBridge:
document = Struct(settings=settings) document = Struct(settings=settings)
self.state = Struct(document=document) self.state = Struct(document=document)
def warn(self, msg): def warn(self, msg: str) -> None:
# type: (str) -> None
logger.warning(msg, location=(self.env.docname, self.lineno)) logger.warning(msg, location=(self.env.docname, self.lineno))
def process_documenter_options(documenter, config, options): def process_documenter_options(documenter: Type[Documenter], config: Config, options: Dict
# type: (Type[Documenter], Config, Dict) -> Options ) -> Options:
"""Recognize options of Documenter from user input.""" """Recognize options of Documenter from user input."""
for name in AUTODOC_DEFAULT_OPTIONS: for name in AUTODOC_DEFAULT_OPTIONS:
if name not in documenter.option_spec: if name not in documenter.option_spec:
@ -92,12 +84,12 @@ def process_documenter_options(documenter, config, options):
return Options(assemble_option_dict(options.items(), documenter.option_spec)) return Options(assemble_option_dict(options.items(), documenter.option_spec))
def parse_generated_content(state, content, documenter): def parse_generated_content(state: RSTState, content: StringList, documenter: Documenter
# type: (RSTState, StringList, Documenter) -> List[nodes.Node] ) -> List[Node]:
"""Parse a generated content by Documenter.""" """Parse a generated content by Documenter."""
with switch_source_input(state, content): with switch_source_input(state, content):
if documenter.titles_allowed: if documenter.titles_allowed:
node = nodes.section() # type: nodes.Element node = nodes.section() # type: Element
# necessary so that the child nodes get the right source/line set # necessary so that the child nodes get the right source/line set
node.document = state.document node.document = state.document
nested_parse_with_titles(state, content, node) nested_parse_with_titles(state, content, node)
@ -121,8 +113,7 @@ class AutodocDirective(SphinxDirective):
optional_arguments = 0 optional_arguments = 0
final_argument_whitespace = True final_argument_whitespace = True
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
reporter = self.state.document.reporter reporter = self.state.document.reporter
try: try:

View File

@ -12,20 +12,16 @@ import sys
import traceback import traceback
import warnings import warnings
from collections import namedtuple from collections import namedtuple
from typing import Any, Callable, Dict, List
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.inspect import isclass, isenumclass, safe_getattr from sphinx.util.inspect import isclass, isenumclass, safe_getattr
if False:
# For type annotation
from typing import Any, Callable, Dict, List # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def import_module(modname, warningiserror=False): def import_module(modname: str, warningiserror: bool = False) -> Any:
# type: (str, bool) -> Any
""" """
Call __import__(modname), convert exceptions to ImportError Call __import__(modname), convert exceptions to ImportError
""" """
@ -41,8 +37,9 @@ def import_module(modname, warningiserror=False):
raise ImportError(exc, traceback.format_exc()) raise ImportError(exc, traceback.format_exc())
def import_object(modname, objpath, objtype='', attrgetter=safe_getattr, warningiserror=False): def import_object(modname: str, objpath: List[str], objtype: str = '',
# type: (str, List[str], str, Callable[[Any, str], Any], bool) -> Any attrgetter: Callable[[Any, str], Any] = safe_getattr,
warningiserror: bool = False) -> Any:
if objpath: if objpath:
logger.debug('[autodoc] from %s import %s', modname, '.'.join(objpath)) logger.debug('[autodoc] from %s import %s', modname, '.'.join(objpath))
else: else:
@ -108,8 +105,8 @@ def import_object(modname, objpath, objtype='', attrgetter=safe_getattr, warning
Attribute = namedtuple('Attribute', ['name', 'directly_defined', 'value']) Attribute = namedtuple('Attribute', ['name', 'directly_defined', 'value'])
def get_object_members(subject, objpath, attrgetter, analyzer=None): def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
# type: (Any, List[str], Callable, Any) -> Dict[str, Attribute] # NOQA analyzer: Any = None) -> Dict[str, Attribute]:
"""Get members and attributes of target object.""" """Get members and attributes of target object."""
# the members directly defined in the class # the members directly defined in the class
obj_dict = attrgetter(subject, '__dict__', {}) obj_dict = attrgetter(subject, '__dict__', {})

View File

@ -15,14 +15,11 @@ import warnings
from importlib.abc import Loader, MetaPathFinder from importlib.abc import Loader, MetaPathFinder
from importlib.machinery import ModuleSpec from importlib.machinery import ModuleSpec
from types import FunctionType, MethodType, ModuleType from types import FunctionType, MethodType, ModuleType
from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union
from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.util import logging 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__) logger = logging.getLogger(__name__)
@ -31,8 +28,7 @@ class _MockObject:
__display_name__ = '_MockObject' __display_name__ = '_MockObject'
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs) -> Any:
# type: (Any, Any) -> Any
if len(args) == 3 and isinstance(args[1], tuple): if len(args) == 3 and isinstance(args[1], tuple):
superclass = args[1][-1].__class__ superclass = args[1][-1].__class__
if superclass is cls: if superclass is cls:
@ -42,48 +38,39 @@ class _MockObject:
return super().__new__(cls) return super().__new__(cls)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
# type: (Any, Any) -> None
self.__qualname__ = '' self.__qualname__ = ''
def __len__(self): def __len__(self) -> int:
# type: () -> int
return 0 return 0
def __contains__(self, key): def __contains__(self, key: str) -> bool:
# type: (str) -> bool
return False return False
def __iter__(self): def __iter__(self) -> Iterator:
# type: () -> Iterator
return iter([]) return iter([])
def __mro_entries__(self, bases): def __mro_entries__(self, bases: Tuple) -> Tuple:
# type: (Tuple) -> Tuple
return (self.__class__,) return (self.__class__,)
def __getitem__(self, key): def __getitem__(self, key: str) -> "_MockObject":
# type: (str) -> _MockObject
return _make_subclass(key, self.__display_name__, self.__class__)() return _make_subclass(key, self.__display_name__, self.__class__)()
def __getattr__(self, key): def __getattr__(self, key: str) -> "_MockObject":
# type: (str) -> _MockObject
return _make_subclass(key, self.__display_name__, self.__class__)() return _make_subclass(key, self.__display_name__, self.__class__)()
def __call__(self, *args, **kw): def __call__(self, *args, **kw) -> Any:
# type: (Any, Any) -> Any
if args and type(args[0]) in [FunctionType, MethodType]: if args and type(args[0]) in [FunctionType, MethodType]:
# Appears to be a decorator, pass through unchanged # Appears to be a decorator, pass through unchanged
return args[0] return args[0]
return self return self
def __repr__(self): def __repr__(self) -> str:
# type: () -> str
return self.__display_name__ return self.__display_name__
def _make_subclass(name, module, superclass=_MockObject, attributes=None): def _make_subclass(name: str, module: str, superclass: Any = _MockObject,
# type: (str, str, Any, dict) -> Any attributes: Any = None) -> Any:
attrs = {'__module__': module, '__display_name__': module + '.' + name} attrs = {'__module__': module, '__display_name__': module + '.' + name}
attrs.update(attributes or {}) attrs.update(attributes or {})
@ -94,8 +81,7 @@ class _MockModule(ModuleType):
"""Used by autodoc_mock_imports.""" """Used by autodoc_mock_imports."""
__file__ = os.devnull __file__ = os.devnull
def __init__(self, name, loader=None): def __init__(self, name: str, loader: "_MockImporter" = None) -> None:
# type: (str, _MockImporter) -> None
super().__init__(name) super().__init__(name)
self.__all__ = [] # type: List[str] self.__all__ = [] # type: List[str]
self.__path__ = [] # type: List[str] self.__path__ = [] # type: List[str]
@ -104,18 +90,15 @@ class _MockModule(ModuleType):
warnings.warn('The loader argument for _MockModule is deprecated.', warnings.warn('The loader argument for _MockModule is deprecated.',
RemovedInSphinx30Warning) RemovedInSphinx30Warning)
def __getattr__(self, name): def __getattr__(self, name: str) -> _MockObject:
# type: (str) -> _MockObject
return _make_subclass(name, self.__name__)() return _make_subclass(name, self.__name__)()
def __repr__(self): def __repr__(self) -> str:
# type: () -> str
return self.__name__ return self.__name__
class _MockImporter(MetaPathFinder): class _MockImporter(MetaPathFinder):
def __init__(self, names): def __init__(self, names: List[str]) -> None:
# type: (List[str]) -> None
self.names = names self.names = names
self.mocked_modules = [] # type: List[str] self.mocked_modules = [] # type: List[str]
# enable hook by adding itself to meta_path # enable hook by adding itself to meta_path
@ -124,8 +107,7 @@ class _MockImporter(MetaPathFinder):
warnings.warn('_MockImporter is now deprecated.', warnings.warn('_MockImporter is now deprecated.',
RemovedInSphinx30Warning) RemovedInSphinx30Warning)
def disable(self): def disable(self) -> None:
# type: () -> None
# remove `self` from `sys.meta_path` to disable import hook # 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] 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 # remove mocked modules from sys.modules to avoid side effects after
@ -134,16 +116,14 @@ class _MockImporter(MetaPathFinder):
if m in sys.modules: if m in sys.modules:
del sys.modules[m] del sys.modules[m]
def find_module(self, name, path=None): def find_module(self, name: str, path: Sequence[Union[bytes, str]] = None) -> Any:
# type: (str, Sequence[Union[bytes, str]]) -> Any
# check if name is (or is a descendant of) one of our base_packages # check if name is (or is a descendant of) one of our base_packages
for n in self.names: for n in self.names:
if n == name or name.startswith(n + '.'): if n == name or name.startswith(n + '.'):
return self return self
return None return None
def load_module(self, name): def load_module(self, name: str) -> ModuleType:
# type: (str) -> ModuleType
if name in sys.modules: if name in sys.modules:
# module has already been imported, return it # module has already been imported, return it
return sys.modules[name] return sys.modules[name]
@ -157,34 +137,30 @@ class _MockImporter(MetaPathFinder):
class MockLoader(Loader): class MockLoader(Loader):
"""A loader for mocking.""" """A loader for mocking."""
def __init__(self, finder): def __init__(self, finder: "MockFinder") -> None:
# type: (MockFinder) -> None
super().__init__() super().__init__()
self.finder = finder self.finder = finder
def create_module(self, spec): def create_module(self, spec: ModuleSpec) -> ModuleType:
# type: (ModuleSpec) -> ModuleType
logger.debug('[autodoc] adding a mock module as %s!', spec.name) logger.debug('[autodoc] adding a mock module as %s!', spec.name)
self.finder.mocked_modules.append(spec.name) self.finder.mocked_modules.append(spec.name)
return _MockModule(spec.name) return _MockModule(spec.name)
def exec_module(self, module): def exec_module(self, module: ModuleType) -> None:
# type: (ModuleType) -> None
pass # nothing to do pass # nothing to do
class MockFinder(MetaPathFinder): class MockFinder(MetaPathFinder):
"""A finder for mocking.""" """A finder for mocking."""
def __init__(self, modnames): def __init__(self, modnames: List[str]) -> None:
# type: (List[str]) -> None
super().__init__() super().__init__()
self.modnames = modnames self.modnames = modnames
self.loader = MockLoader(self) self.loader = MockLoader(self)
self.mocked_modules = [] # type: List[str] self.mocked_modules = [] # type: List[str]
def find_spec(self, fullname, path, target=None): def find_spec(self, fullname: str, path: Sequence[Union[bytes, str]],
# type: (str, Sequence[Union[bytes, str]], ModuleType) -> ModuleSpec target: ModuleType = None) -> ModuleSpec:
for modname in self.modnames: for modname in self.modnames:
# check if fullname is (or is a descendant of) one of our targets # check if fullname is (or is a descendant of) one of our targets
if modname == fullname or fullname.startswith(modname + '.'): if modname == fullname or fullname.startswith(modname + '.'):
@ -192,16 +168,14 @@ class MockFinder(MetaPathFinder):
return None return None
def invalidate_caches(self): def invalidate_caches(self) -> None:
# type: () -> None
"""Invalidate mocked modules on sys.modules.""" """Invalidate mocked modules on sys.modules."""
for modname in self.mocked_modules: for modname in self.mocked_modules:
sys.modules.pop(modname, None) sys.modules.pop(modname, None)
@contextlib.contextmanager @contextlib.contextmanager
def mock(modnames): def mock(modnames: List[str]) -> Generator[None, None, None]:
# type: (List[str]) -> Generator[None, None, None]
"""Insert mock modules during context:: """Insert mock modules during context::
with mock(['target.module.name']): with mock(['target.module.name']):

View File

@ -8,24 +8,22 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict
from typing import cast from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Node
from sphinx.application import Sphinx
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.nodes import clean_astext from sphinx.util.nodes import clean_astext
if False:
# For type annotation
from typing import Any, Dict # NOQA
from sphinx.application import Sphinx # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_node_depth(node): def get_node_depth(node: Node) -> int:
i = 0 i = 0
cur_node = node cur_node = node
while cur_node.parent != node.document: while cur_node.parent != node.document:
@ -34,8 +32,7 @@ def get_node_depth(node):
return i return i
def register_sections_as_label(app, document): def register_sections_as_label(app: Sphinx, document: Node) -> None:
# type: (Sphinx, nodes.Node) -> None
labels = app.env.domaindata['std']['labels'] labels = app.env.domaindata['std']['labels']
anonlabels = app.env.domaindata['std']['anonlabels'] anonlabels = app.env.domaindata['std']['anonlabels']
for node in document.traverse(nodes.section): for node in document.traverse(nodes.section):
@ -61,8 +58,7 @@ def register_sections_as_label(app, document):
labels[name] = docname, labelid, sectname labels[name] = docname, labelid, sectname
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_config_value('autosectionlabel_prefix_document', False, 'env') app.add_config_value('autosectionlabel_prefix_document', False, 'env')
app.add_config_value('autosectionlabel_maxdepth', None, 'env') app.add_config_value('autosectionlabel_maxdepth', None, 'env')
app.connect('doctree-read', register_sections_as_label) app.connect('doctree-read', register_sections_as_label)

View File

@ -58,19 +58,24 @@ import posixpath
import re import re
import sys import sys
import warnings import warnings
from os import path
from types import ModuleType from types import ModuleType
from typing import List, cast from typing import Any, Dict, List, Tuple, Type
from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node, system_message
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
from docutils.parsers.rst.states import RSTStateMachine, Struct, state_classes from docutils.parsers.rst.states import Inliner, RSTStateMachine, Struct, state_classes
from docutils.statemachine import StringList from docutils.statemachine import StringList
import sphinx import sphinx
from sphinx import addnodes from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.environment import BuildEnvironment
from sphinx.environment.adapters.toctree import TocTree from sphinx.environment.adapters.toctree import TocTree
from sphinx.ext.autodoc import get_documenters from sphinx.ext.autodoc import Documenter, get_documenters
from sphinx.ext.autodoc.directive import DocumenterBridge, Options from sphinx.ext.autodoc.directive import DocumenterBridge, Options
from sphinx.ext.autodoc.importer import import_module from sphinx.ext.autodoc.importer import import_module
from sphinx.ext.autodoc.mock import mock from sphinx.ext.autodoc.mock import mock
@ -81,15 +86,8 @@ from sphinx.util.docutils import (
NullReporter, SphinxDirective, SphinxRole, new_document, switch_source_input NullReporter, SphinxDirective, SphinxRole, new_document, switch_source_input
) )
from sphinx.util.matching import Matcher from sphinx.util.matching import Matcher
from sphinx.writers.html import HTMLTranslator
if False:
# For type annotation
from typing import Any, Dict, Tuple, Type # NOQA
from docutils.parsers.rst.states import Inliner # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.ext.autodoc import Documenter # NOQA
from sphinx.writers.html import HTMLTranslator # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -104,16 +102,14 @@ class autosummary_toc(nodes.comment):
pass pass
def process_autosummary_toc(app, doctree): def process_autosummary_toc(app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
"""Insert items described in autosummary:: to the TOC tree, but do """Insert items described in autosummary:: to the TOC tree, but do
not generate the toctree:: list. not generate the toctree:: list.
""" """
env = app.builder.env env = app.builder.env
crawled = {} crawled = {}
def crawl_toc(node, depth=1): def crawl_toc(node: Element, depth: int = 1) -> None:
# type: (nodes.Element, int) -> None
crawled[node] = True crawled[node] = True
for j, subnode in enumerate(node): for j, subnode in enumerate(node):
try: try:
@ -130,14 +126,12 @@ def process_autosummary_toc(app, doctree):
crawl_toc(doctree) crawl_toc(doctree)
def autosummary_toc_visit_html(self, node): def autosummary_toc_visit_html(self: nodes.NodeVisitor, node: autosummary_toc) -> None:
# type: (nodes.NodeVisitor, autosummary_toc) -> None
"""Hide autosummary toctree list in HTML output.""" """Hide autosummary toctree list in HTML output."""
raise nodes.SkipNode raise nodes.SkipNode
def autosummary_noop(self, node): def autosummary_noop(self: nodes.NodeVisitor, node: Node) -> None:
# type: (nodes.NodeVisitor, nodes.Node) -> None
pass pass
@ -147,8 +141,7 @@ class autosummary_table(nodes.comment):
pass pass
def autosummary_table_visit_html(self, node): def autosummary_table_visit_html(self: HTMLTranslator, node: autosummary_table) -> None:
# type: (HTMLTranslator, autosummary_table) -> None
"""Make the first column of the table non-breaking.""" """Make the first column of the table non-breaking."""
try: try:
table = cast(nodes.table, node[0]) table = cast(nodes.table, node[0])
@ -173,16 +166,14 @@ _app = None # type: Sphinx
class FakeDirective(DocumenterBridge): class FakeDirective(DocumenterBridge):
def __init__(self): def __init__(self) -> None:
# type: () -> None
settings = Struct(tab_width=8) settings = Struct(tab_width=8)
document = Struct(settings=settings) document = Struct(settings=settings)
state = Struct(document=document) state = Struct(document=document)
super().__init__({}, None, Options(), 0, state) # type: ignore super().__init__({}, None, Options(), 0, state) # type: ignore
def get_documenter(app, obj, parent): def get_documenter(app: Sphinx, obj: Any, parent: Any) -> Type[Documenter]:
# type: (Sphinx, Any, Any) -> Type[Documenter]
"""Get an autodoc.Documenter class suitable for documenting the given """Get an autodoc.Documenter class suitable for documenting the given
object. object.
@ -236,8 +227,7 @@ class Autosummary(SphinxDirective):
'template': directives.unchanged, 'template': directives.unchanged,
} }
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
self.bridge = DocumenterBridge(self.env, self.state.document.reporter, self.bridge = DocumenterBridge(self.env, self.state.document.reporter,
Options(), self.lineno, self.state) Options(), self.lineno, self.state)
@ -256,12 +246,19 @@ class Autosummary(SphinxDirective):
docname = posixpath.join(tree_prefix, real_name) docname = posixpath.join(tree_prefix, real_name)
docname = posixpath.normpath(posixpath.join(dirname, docname)) docname = posixpath.normpath(posixpath.join(dirname, docname))
if docname not in self.env.found_docs: if docname not in self.env.found_docs:
location = self.state_machine.get_source_and_line(self.lineno)
if excluded(self.env.doc2path(docname, None)): if excluded(self.env.doc2path(docname, None)):
logger.warning(__('toctree references excluded document %r'), docname) msg = __('autosummary references excluded document %r. Ignored.')
else: else:
logger.warning(__('toctree references unknown document %r'), docname) msg = __('autosummary: stub file not found %r. '
'Check your autosummary_generate setting.')
logger.warning(msg, real_name, location=location)
continue
docnames.append(docname) docnames.append(docname)
if docnames:
tocnode = addnodes.toctree() tocnode = addnodes.toctree()
tocnode['includefiles'] = docnames tocnode['includefiles'] = docnames
tocnode['entries'] = [(None, docn) for docn in docnames] tocnode['entries'] = [(None, docn) for docn in docnames]
@ -272,8 +269,7 @@ class Autosummary(SphinxDirective):
return nodes return nodes
def get_items(self, names): def get_items(self, names: List[str]) -> List[Tuple[str, str, str, str]]:
# type: (List[str]) -> List[Tuple[str, str, str, str]]
"""Try to import the given names, and return a list of """Try to import the given names, and return a list of
``[(name, signature, summary_string, real_name), ...]``. ``[(name, signature, summary_string, real_name), ...]``.
""" """
@ -353,8 +349,7 @@ class Autosummary(SphinxDirective):
return items return items
def get_table(self, items): def get_table(self, items: List[Tuple[str, str, str, str]]) -> List[Node]:
# type: (List[Tuple[str, str, str, str]]) -> List[nodes.Node]
"""Generate a proper list of table nodes for autosummary:: directive. """Generate a proper list of table nodes for autosummary:: directive.
*items* is a list produced by :meth:`get_items`. *items* is a list produced by :meth:`get_items`.
@ -372,8 +367,7 @@ class Autosummary(SphinxDirective):
body = nodes.tbody('') body = nodes.tbody('')
group.append(body) group.append(body)
def append_row(*column_texts): def append_row(*column_texts: str) -> None:
# type: (str) -> None
row = nodes.row('') row = nodes.row('')
source, line = self.state_machine.get_source_and_line() source, line = self.state_machine.get_source_and_line()
for text in column_texts: for text in column_texts:
@ -401,42 +395,36 @@ class Autosummary(SphinxDirective):
return [table_spec, table] return [table_spec, table]
def warn(self, msg): def warn(self, msg: str) -> None:
# type: (str) -> None
warnings.warn('Autosummary.warn() is deprecated', warnings.warn('Autosummary.warn() is deprecated',
RemovedInSphinx40Warning, stacklevel=2) RemovedInSphinx40Warning, stacklevel=2)
logger.warning(msg) logger.warning(msg)
@property @property
def genopt(self): def genopt(self) -> Options:
# type: () -> Options
warnings.warn('Autosummary.genopt is deprecated', warnings.warn('Autosummary.genopt is deprecated',
RemovedInSphinx40Warning, stacklevel=2) RemovedInSphinx40Warning, stacklevel=2)
return self.bridge.genopt return self.bridge.genopt
@property @property
def warnings(self): def warnings(self) -> List[Node]:
# type: () -> List[nodes.Node]
warnings.warn('Autosummary.warnings is deprecated', warnings.warn('Autosummary.warnings is deprecated',
RemovedInSphinx40Warning, stacklevel=2) RemovedInSphinx40Warning, stacklevel=2)
return [] return []
@property @property
def result(self): def result(self) -> StringList:
# type: () -> StringList
warnings.warn('Autosummary.result is deprecated', warnings.warn('Autosummary.result is deprecated',
RemovedInSphinx40Warning, stacklevel=2) RemovedInSphinx40Warning, stacklevel=2)
return self.bridge.result return self.bridge.result
def strip_arg_typehint(s): def strip_arg_typehint(s: str) -> str:
# type: (str) -> str
"""Strip a type hint from argument definition.""" """Strip a type hint from argument definition."""
return s.split(':')[0].strip() return s.split(':')[0].strip()
def mangle_signature(sig, max_chars=30): def mangle_signature(sig: str, max_chars: int = 30) -> str:
# type: (str, int) -> str
"""Reformat a function signature to a more compact form.""" """Reformat a function signature to a more compact form."""
# Strip return type annotation # Strip return type annotation
s = re.sub(r"\)\s*->\s.*$", ")", sig) s = re.sub(r"\)\s*->\s.*$", ")", sig)
@ -493,8 +481,7 @@ def mangle_signature(sig, max_chars=30):
return "(%s)" % sig return "(%s)" % sig
def extract_summary(doc, document): def extract_summary(doc: List[str], document: Any) -> str:
# type: (List[str], Any) -> str
"""Extract summary from docstring.""" """Extract summary from docstring."""
# Skip a blank lines at the top # Skip a blank lines at the top
@ -542,8 +529,8 @@ def extract_summary(doc, document):
return summary return summary
def limited_join(sep, items, max_chars=30, overflow_marker="..."): def limited_join(sep: str, items: List[str], max_chars: int = 30,
# type: (str, List[str], int, str) -> str overflow_marker: str = "...") -> str:
"""Join a number of strings to one, limiting the length to *max_chars*. """Join a number of strings to one, limiting the length to *max_chars*.
If the string overflows this limit, replace the last fitting item by If the string overflows this limit, replace the last fitting item by
@ -569,8 +556,7 @@ def limited_join(sep, items, max_chars=30, overflow_marker="..."):
# -- Importing items ----------------------------------------------------------- # -- Importing items -----------------------------------------------------------
def get_import_prefixes_from_env(env): def get_import_prefixes_from_env(env: BuildEnvironment) -> List[str]:
# type: (BuildEnvironment) -> List[str]
""" """
Obtain current Python import prefixes (for `import_by_name`) Obtain current Python import prefixes (for `import_by_name`)
from ``document.env`` from ``document.env``
@ -591,8 +577,7 @@ def get_import_prefixes_from_env(env):
return prefixes return prefixes
def import_by_name(name, prefixes=[None]): def import_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, Any, Any, str]:
# type: (str, List[str]) -> Tuple[str, Any, Any, str]
"""Import a Python object that has the given *name*, under one of the """Import a Python object that has the given *name*, under one of the
*prefixes*. The first name that succeeds is used. *prefixes*. The first name that succeeds is used.
""" """
@ -610,8 +595,7 @@ def import_by_name(name, prefixes=[None]):
raise ImportError('no module named %s' % ' or '.join(tried)) raise ImportError('no module named %s' % ' or '.join(tried))
def _import_by_name(name): def _import_by_name(name: str) -> Tuple[Any, Any, str]:
# type: (str) -> Tuple[Any, Any, str]
"""Import a Python object given its full name.""" """Import a Python object given its full name."""
try: try:
name_parts = name.split('.') name_parts = name.split('.')
@ -654,8 +638,9 @@ def _import_by_name(name):
# -- :autolink: (smart default role) ------------------------------------------- # -- :autolink: (smart default role) -------------------------------------------
def autolink_role(typ, rawtext, etext, lineno, inliner, options={}, content=[]): def autolink_role(typ: str, rawtext: str, etext: str, lineno: int, inliner: Inliner,
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA options: Dict = {}, content: List[str] = []
) -> Tuple[List[Node], List[system_message]]:
"""Smart linking role. """Smart linking role.
Expands to ':obj:`text`' if `text` is an object that can be imported; Expands to ':obj:`text`' if `text` is an object that can be imported;
@ -686,8 +671,7 @@ class AutoLink(SphinxRole):
Expands to ':obj:`text`' if `text` is an object that can be imported; Expands to ':obj:`text`' if `text` is an object that can be imported;
otherwise expands to '*text*'. otherwise expands to '*text*'.
""" """
def run(self): def run(self) -> Tuple[List[Node], List[system_message]]:
# type: () -> Tuple[List[nodes.Node], List[nodes.system_message]]
pyobj_role = self.env.get_domain('py').role('obj') pyobj_role = self.env.get_domain('py').role('obj')
objects, errors = pyobj_role('obj', self.rawtext, self.text, self.lineno, objects, errors = pyobj_role('obj', self.rawtext, self.text, self.lineno,
self.inliner, self.options, self.content) self.inliner, self.options, self.content)
@ -708,10 +692,8 @@ class AutoLink(SphinxRole):
return objects, errors return objects, errors
def get_rst_suffix(app): def get_rst_suffix(app: Sphinx) -> str:
# type: (Sphinx) -> str def get_supported_format(suffix: str) -> Tuple[str, ...]:
def get_supported_format(suffix):
# type: (str) -> Tuple[str, ...]
parser_class = app.registry.get_source_parsers().get(suffix) parser_class = app.registry.get_source_parsers().get(suffix)
if parser_class is None: if parser_class is None:
return ('restructuredtext',) return ('restructuredtext',)
@ -727,30 +709,34 @@ def get_rst_suffix(app):
return None return None
def process_generate_options(app): def process_generate_options(app: Sphinx) -> None:
# type: (Sphinx) -> None
genfiles = app.config.autosummary_generate genfiles = app.config.autosummary_generate
if genfiles and not hasattr(genfiles, '__len__'): if genfiles is True:
env = app.builder.env env = app.builder.env
genfiles = [env.doc2path(x, base=None) for x in env.found_docs genfiles = [env.doc2path(x, base=None) for x in env.found_docs
if os.path.isfile(env.doc2path(x))] if os.path.isfile(env.doc2path(x))]
else:
if not genfiles:
return
from sphinx.ext.autosummary.generate import generate_autosummary_docs
ext = list(app.config.source_suffix) ext = list(app.config.source_suffix)
genfiles = [genfile + (not genfile.endswith(tuple(ext)) and ext[0] or '') genfiles = [genfile + (not genfile.endswith(tuple(ext)) and ext[0] or '')
for genfile in genfiles] for genfile in genfiles]
for entry in genfiles[:]:
if not path.isfile(path.join(app.srcdir, entry)):
logger.warning(__('autosummary_generate: file not found: %s'), entry)
genfiles.remove(entry)
if not genfiles:
return
suffix = get_rst_suffix(app) suffix = get_rst_suffix(app)
if suffix is None: if suffix is None:
logger.warning(__('autosummary generats .rst files internally. ' logger.warning(__('autosummary generats .rst files internally. '
'But your source_suffix does not contain .rst. Skipped.')) 'But your source_suffix does not contain .rst. Skipped.'))
return return
from sphinx.ext.autosummary.generate import generate_autosummary_docs
imported_members = app.config.autosummary_imported_members imported_members = app.config.autosummary_imported_members
with mock(app.config.autosummary_mock_imports): with mock(app.config.autosummary_mock_imports):
generate_autosummary_docs(genfiles, builder=app.builder, generate_autosummary_docs(genfiles, builder=app.builder,
@ -758,8 +744,7 @@ def process_generate_options(app):
app=app, imported_members=imported_members) app=app, imported_members=imported_members)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
# I need autodoc # I need autodoc
app.setup_extension('sphinx.ext.autodoc') app.setup_extension('sphinx.ext.autodoc')
app.add_node(autosummary_toc, app.add_node(autosummary_toc,

View File

@ -24,6 +24,7 @@ import pydoc
import re import re
import sys import sys
import warnings import warnings
from typing import Any, Callable, Dict, List, Set, Tuple, Type
from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound
from jinja2.sandbox import SandboxedEnvironment from jinja2.sandbox import SandboxedEnvironment
@ -31,7 +32,9 @@ from jinja2.sandbox import SandboxedEnvironment
import sphinx.locale import sphinx.locale
from sphinx import __display_version__ from sphinx import __display_version__
from sphinx import package_dir from sphinx import package_dir
from sphinx.builders import Builder
from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.ext.autodoc import Documenter
from sphinx.ext.autosummary import import_by_name, get_documenter from sphinx.ext.autosummary import import_by_name, get_documenter
from sphinx.jinja2glue import BuiltinTemplateLoader from sphinx.jinja2glue import BuiltinTemplateLoader
from sphinx.locale import __ from sphinx.locale import __
@ -41,12 +44,6 @@ from sphinx.util import rst
from sphinx.util.inspect import safe_getattr from sphinx.util.inspect import safe_getattr
from sphinx.util.osutil import ensuredir from sphinx.util.osutil import ensuredir
if False:
# For type annotation
from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.ext.autodoc import Documenter # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -54,15 +51,13 @@ logger = logging.getLogger(__name__)
class DummyApplication: class DummyApplication:
"""Dummy Application class for sphinx-autogen command.""" """Dummy Application class for sphinx-autogen command."""
def __init__(self): def __init__(self) -> None:
# type: () -> None
self.registry = SphinxComponentRegistry() self.registry = SphinxComponentRegistry()
self.messagelog = [] # type: List[str] self.messagelog = [] # type: List[str]
self.verbosity = 0 self.verbosity = 0
def setup_documenters(app): def setup_documenters(app: Any) -> None:
# type: (Any) -> None
from sphinx.ext.autodoc import ( from sphinx.ext.autodoc import (
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, AttributeDocumenter, FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
@ -79,18 +74,15 @@ def setup_documenters(app):
app.registry.add_documenter(documenter.objtype, documenter) app.registry.add_documenter(documenter.objtype, documenter)
def _simple_info(msg): def _simple_info(msg: str) -> None:
# type: (str) -> None
print(msg) print(msg)
def _simple_warn(msg): def _simple_warn(msg: str) -> None:
# type: (str) -> None
print('WARNING: ' + msg, file=sys.stderr) print('WARNING: ' + msg, file=sys.stderr)
def _underline(title, line='='): def _underline(title: str, line: str = '=') -> str:
# type: (str, str) -> str
if '\n' in title: if '\n' in title:
raise ValueError('Can only underline single lines') raise ValueError('Can only underline single lines')
return title + '\n' + line * len(title) return title + '\n' + line * len(title)
@ -99,8 +91,7 @@ def _underline(title, line='='):
class AutosummaryRenderer: class AutosummaryRenderer:
"""A helper class for rendering.""" """A helper class for rendering."""
def __init__(self, builder, template_dir): def __init__(self, builder: Builder, template_dir: str) -> None:
# type: (Builder, str) -> None
loader = None # type: BaseLoader loader = None # type: BaseLoader
template_dirs = [os.path.join(package_dir, 'ext', 'autosummary', 'templates')] template_dirs = [os.path.join(package_dir, 'ext', 'autosummary', 'templates')]
if builder is None: if builder is None:
@ -117,8 +108,7 @@ class AutosummaryRenderer:
self.env.filters['e'] = rst.escape self.env.filters['e'] = rst.escape
self.env.filters['underline'] = _underline self.env.filters['underline'] = _underline
def exists(self, template_name): def exists(self, template_name: str) -> bool:
# type: (str) -> bool
"""Check if template file exists.""" """Check if template file exists."""
try: try:
self.env.get_template(template_name) self.env.get_template(template_name)
@ -126,75 +116,17 @@ class AutosummaryRenderer:
except TemplateNotFound: except TemplateNotFound:
return False return False
def render(self, template_name, context): def render(self, template_name: str, context: Dict) -> str:
# type: (str, Dict) -> str
"""Render a template file.""" """Render a template file."""
return self.env.get_template(template_name).render(context) return self.env.get_template(template_name).render(context)
# -- Generating output --------------------------------------------------------- # -- Generating output ---------------------------------------------------------
def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
warn=None, info=None, base_path=None, builder=None,
template_dir=None, imported_members=False, app=None):
# type: (List[str], str, str, Callable, Callable, str, Builder, str, bool, Any) -> None
if info:
warnings.warn('info argument for generate_autosummary_docs() is deprecated.',
RemovedInSphinx40Warning)
else:
info = logger.info
if warn: def generate_autosummary_content(name: str, obj: Any, parent: Any,
warnings.warn('warn argument for generate_autosummary_docs() is deprecated.', template: AutosummaryRenderer, template_name: str,
RemovedInSphinx40Warning) imported_members: bool, app: Any) -> str:
else:
warn = logger.warning
showed_sources = list(sorted(sources))
if len(showed_sources) > 20:
showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:]
info(__('[autosummary] generating autosummary for: %s') %
', '.join(showed_sources))
if output_dir:
info(__('[autosummary] writing to %s') % output_dir)
if base_path is not None:
sources = [os.path.join(base_path, filename) for filename in sources]
template = AutosummaryRenderer(builder, template_dir)
# read
items = find_autosummary_in_files(sources)
# keep track of new files
new_files = []
# write
for name, path, template_name in sorted(set(items), key=str):
if path is None:
# The corresponding autosummary:: directive did not have
# a :toctree: option
continue
path = output_dir or os.path.abspath(path)
ensuredir(path)
try:
name, obj, parent, mod_name = import_by_name(name)
except ImportError as e:
warn('[autosummary] failed to import %r: %s' % (name, e))
continue
fn = os.path.join(path, name + suffix)
# skip it if it exists
if os.path.isfile(fn):
continue
new_files.append(fn)
with open(fn, 'w') as f:
doc = get_documenter(app, obj, parent) doc = get_documenter(app, obj, parent)
if template_name is None: if template_name is None:
@ -202,8 +134,8 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
if not template.exists(template_name): if not template.exists(template_name):
template_name = 'autosummary/base.rst' template_name = 'autosummary/base.rst'
def get_members(obj, types, include_public=[], imported=True): def get_members(obj: Any, types: Set[str], include_public: List[str] = [],
# type: (Any, Set[str], List[str], bool) -> Tuple[List[str], List[str]] # NOQA imported: bool = True) -> Tuple[List[str], List[str]]:
items = [] # type: List[str] items = [] # type: List[str]
for name in dir(obj): for name in dir(obj):
try: try:
@ -255,7 +187,76 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
ns['objtype'] = doc.objtype ns['objtype'] = doc.objtype
ns['underline'] = len(name) * '=' ns['underline'] = len(name) * '='
rendered = template.render(template_name, ns) return template.render(template_name, ns)
def generate_autosummary_docs(sources: List[str], output_dir: str = None,
suffix: str = '.rst', warn: Callable = None,
info: Callable = None, base_path: str = None,
builder: Builder = None, template_dir: str = None,
imported_members: bool = False, app: Any = None) -> None:
if info:
warnings.warn('info argument for generate_autosummary_docs() is deprecated.',
RemovedInSphinx40Warning)
_info = info
else:
_info = logger.info
if warn:
warnings.warn('warn argument for generate_autosummary_docs() is deprecated.',
RemovedInSphinx40Warning)
_warn = warn
else:
_warn = logger.warning
showed_sources = list(sorted(sources))
if len(showed_sources) > 20:
showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:]
_info(__('[autosummary] generating autosummary for: %s') %
', '.join(showed_sources))
if output_dir:
_info(__('[autosummary] writing to %s') % output_dir)
if base_path is not None:
sources = [os.path.join(base_path, filename) for filename in sources]
template = AutosummaryRenderer(builder, template_dir)
# read
items = find_autosummary_in_files(sources)
# keep track of new files
new_files = []
# write
for name, path, template_name in sorted(set(items), key=str):
if path is None:
# The corresponding autosummary:: directive did not have
# a :toctree: option
continue
path = output_dir or os.path.abspath(path)
ensuredir(path)
try:
name, obj, parent, mod_name = import_by_name(name)
except ImportError as e:
_warn('[autosummary] failed to import %r: %s' % (name, e))
continue
fn = os.path.join(path, name + suffix)
# skip it if it exists
if os.path.isfile(fn):
continue
new_files.append(fn)
with open(fn, 'w') as f:
rendered = generate_autosummary_content(name, obj, parent,
template, template_name,
imported_members, app)
f.write(rendered) f.write(rendered)
# descend recursively to new files # descend recursively to new files
@ -268,8 +269,7 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
# -- Finding documented entries in files --------------------------------------- # -- Finding documented entries in files ---------------------------------------
def find_autosummary_in_files(filenames): def find_autosummary_in_files(filenames: List[str]) -> List[Tuple[str, str, str]]:
# type: (List[str]) -> List[Tuple[str, str, str]]
"""Find out what items are documented in source/*.rst. """Find out what items are documented in source/*.rst.
See `find_autosummary_in_lines`. See `find_autosummary_in_lines`.
@ -282,8 +282,8 @@ def find_autosummary_in_files(filenames):
return documented return documented
def find_autosummary_in_docstring(name, module=None, filename=None): def find_autosummary_in_docstring(name: str, module: Any = None, filename: str = None
# type: (str, Any, str) -> List[Tuple[str, str, str]] ) -> List[Tuple[str, str, str]]:
"""Find out what items are documented in the given object's docstring. """Find out what items are documented in the given object's docstring.
See `find_autosummary_in_lines`. See `find_autosummary_in_lines`.
@ -302,8 +302,8 @@ def find_autosummary_in_docstring(name, module=None, filename=None):
return [] return []
def find_autosummary_in_lines(lines, module=None, filename=None): def find_autosummary_in_lines(lines: List[str], module: Any = None, filename: str = None
# type: (List[str], Any, str) -> List[Tuple[str, str, str]] ) -> List[Tuple[str, str, str]]:
"""Find out what items appear in autosummary:: directives in the """Find out what items appear in autosummary:: directives in the
given lines. given lines.
@ -389,8 +389,7 @@ def find_autosummary_in_lines(lines, module=None, filename=None):
return documented return documented
def get_parser(): def get_parser() -> argparse.ArgumentParser:
# type: () -> argparse.ArgumentParser
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
usage='%(prog)s [OPTIONS] <SOURCE_FILE>...', usage='%(prog)s [OPTIONS] <SOURCE_FILE>...',
epilog=__('For more information, visit <http://sphinx-doc.org/>.'), epilog=__('For more information, visit <http://sphinx-doc.org/>.'),
@ -432,8 +431,7 @@ The format of the autosummary directive is documented in the
return parser return parser
def main(argv=sys.argv[1:]): def main(argv: List[str] = sys.argv[1:]) -> None:
# type: (List[str]) -> None
sphinx.locale.setlocale(locale.LC_ALL, '') sphinx.locale.setlocale(locale.LC_ALL, '')
sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx') sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx')

View File

@ -14,30 +14,25 @@ import inspect
import pickle import pickle
import re import re
from os import path from os import path
from typing import Any, Dict, IO, List, Pattern, Set, Tuple
import sphinx import sphinx
from sphinx.application import Sphinx
from sphinx.builders import Builder from sphinx.builders import Builder
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.inspect import safe_getattr from sphinx.util.inspect import safe_getattr
if False:
# For type annotation
from typing import Any, Dict, IO, List, Pattern, Set, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# utility # utility
def write_header(f, text, char='-'): def write_header(f: IO, text: str, char: str = '-') -> None:
# type:(IO, str, str) -> None
f.write(text + '\n') f.write(text + '\n')
f.write(char * len(text) + '\n') f.write(char * len(text) + '\n')
def compile_regex_list(name, exps): def compile_regex_list(name: str, exps: str) -> List[Pattern]:
# type: (str, str) -> List[Pattern]
lst = [] lst = []
for exp in exps: for exp in exps:
try: try:
@ -55,8 +50,7 @@ class CoverageBuilder(Builder):
epilog = __('Testing of coverage in the sources finished, look at the ' epilog = __('Testing of coverage in the sources finished, look at the '
'results in %(outdir)s' + path.sep + 'python.txt.') 'results in %(outdir)s' + path.sep + 'python.txt.')
def init(self): def init(self) -> None:
# type: () -> None
self.c_sourcefiles = [] # type: List[str] self.c_sourcefiles = [] # type: List[str]
for pattern in self.config.coverage_c_path: for pattern in self.config.coverage_c_path:
pattern = path.join(self.srcdir, pattern) pattern = path.join(self.srcdir, pattern)
@ -82,12 +76,10 @@ class CoverageBuilder(Builder):
self.py_ignorexps = compile_regex_list('coverage_ignore_pyobjects', self.py_ignorexps = compile_regex_list('coverage_ignore_pyobjects',
self.config.coverage_ignore_pyobjects) self.config.coverage_ignore_pyobjects)
def get_outdated_docs(self): def get_outdated_docs(self) -> str:
# type: () -> str
return 'coverage overview' return 'coverage overview'
def write(self, *ignored): def write(self, *ignored) -> None:
# type: (Any) -> None
self.py_undoc = {} # type: Dict[str, Dict[str, Any]] self.py_undoc = {} # type: Dict[str, Dict[str, Any]]
self.build_py_coverage() self.build_py_coverage()
self.write_py_coverage() self.write_py_coverage()
@ -96,8 +88,7 @@ class CoverageBuilder(Builder):
self.build_c_coverage() self.build_c_coverage()
self.write_c_coverage() self.write_c_coverage()
def build_c_coverage(self): def build_c_coverage(self) -> None:
# type: () -> None
# Fetch all the info from the header files # Fetch all the info from the header files
c_objects = self.env.domaindata['c']['objects'] c_objects = self.env.domaindata['c']['objects']
for filename in self.c_sourcefiles: for filename in self.c_sourcefiles:
@ -118,8 +109,7 @@ class CoverageBuilder(Builder):
if undoc: if undoc:
self.c_undoc[filename] = undoc self.c_undoc[filename] = undoc
def write_c_coverage(self): def write_c_coverage(self) -> None:
# type: () -> None
output_file = path.join(self.outdir, 'c.txt') output_file = path.join(self.outdir, 'c.txt')
with open(output_file, 'w') as op: with open(output_file, 'w') as op:
if self.config.coverage_write_headline: if self.config.coverage_write_headline:
@ -138,8 +128,7 @@ class CoverageBuilder(Builder):
return True return True
return False return False
def build_py_coverage(self): def build_py_coverage(self) -> None:
# type: () -> None
objects = self.env.domaindata['py']['objects'] objects = self.env.domaindata['py']['objects']
modules = self.env.domaindata['py']['modules'] modules = self.env.domaindata['py']['modules']
@ -230,8 +219,7 @@ class CoverageBuilder(Builder):
self.py_undoc[mod_name] = {'funcs': funcs, 'classes': classes} self.py_undoc[mod_name] = {'funcs': funcs, 'classes': classes}
def write_py_coverage(self): def write_py_coverage(self) -> None:
# type: () -> None
output_file = path.join(self.outdir, 'python.txt') output_file = path.join(self.outdir, 'python.txt')
failed = [] failed = []
with open(output_file, 'w') as op: with open(output_file, 'w') as op:
@ -266,16 +254,14 @@ class CoverageBuilder(Builder):
write_header(op, 'Modules that failed to import') write_header(op, 'Modules that failed to import')
op.writelines(' * %s -- %s\n' % x for x in failed) op.writelines(' * %s -- %s\n' % x for x in failed)
def finish(self): def finish(self) -> None:
# type: () -> None
# dump the coverage data to a pickle file too # dump the coverage data to a pickle file too
picklepath = path.join(self.outdir, 'undoc.pickle') picklepath = path.join(self.outdir, 'undoc.pickle')
with open(picklepath, 'wb') as dumpfile: with open(picklepath, 'wb') as dumpfile:
pickle.dump((self.py_undoc, self.c_undoc), dumpfile) pickle.dump((self.py_undoc, self.c_undoc), dumpfile)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_builder(CoverageBuilder) app.add_builder(CoverageBuilder)
app.add_config_value('coverage_ignore_modules', [], False) app.add_config_value('coverage_ignore_modules', [], False)
app.add_config_value('coverage_ignore_functions', [], False) app.add_config_value('coverage_ignore_functions', [], False)

View File

@ -16,8 +16,10 @@ import time
import warnings import warnings
from io import StringIO from io import StringIO
from os import path from os import path
from typing import Any, Callable, Dict, Iterable, List, Sequence, Set, Tuple, Type
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node, TextElement
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
from packaging.specifiers import SpecifierSet, InvalidSpecifier from packaging.specifiers import SpecifierSet, InvalidSpecifier
from packaging.version import Version from packaging.version import Version
@ -33,8 +35,8 @@ from sphinx.util.osutil import relpath
if False: if False:
# For type annotation # For type annotation
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type # NOQA from sphinx.application import Sphinx
from sphinx.application import Sphinx # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -42,15 +44,13 @@ blankline_re = re.compile(r'^\s*<BLANKLINE>', re.MULTILINE)
doctestopt_re = re.compile(r'#\s*doctest:.+$', re.MULTILINE) doctestopt_re = re.compile(r'#\s*doctest:.+$', re.MULTILINE)
def doctest_encode(text, encoding): def doctest_encode(text: str, encoding: str) -> str:
# type: (str, str) -> str
warnings.warn('doctest_encode() is deprecated.', warnings.warn('doctest_encode() is deprecated.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
return text return text
def is_allowed_version(spec, version): def is_allowed_version(spec: str, version: str) -> bool:
# type: (str, str) -> bool
"""Check `spec` satisfies `version` or not. """Check `spec` satisfies `version` or not.
This obeys PEP-440 specifiers: This obeys PEP-440 specifiers:
@ -80,8 +80,7 @@ class TestDirective(SphinxDirective):
optional_arguments = 1 optional_arguments = 1
final_argument_whitespace = True final_argument_whitespace = True
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
# use ordinary docutils nodes for test code: they get special attributes # use ordinary docutils nodes for test code: they get special attributes
# so that our builder recognizes them, and the other builders are happy. # so that our builder recognizes them, and the other builders are happy.
code = '\n'.join(self.content) code = '\n'.join(self.content)
@ -95,7 +94,7 @@ class TestDirective(SphinxDirective):
if not test: if not test:
test = code test = code
code = doctestopt_re.sub('', code) code = doctestopt_re.sub('', code)
nodetype = nodes.literal_block # type: Type[nodes.TextElement] nodetype = nodes.literal_block # type: Type[TextElement]
if self.name in ('testsetup', 'testcleanup') or 'hide' in self.options: if self.name in ('testsetup', 'testcleanup') or 'hide' in self.options:
nodetype = nodes.comment nodetype = nodes.comment
if self.arguments: if self.arguments:
@ -194,15 +193,13 @@ parser = doctest.DocTestParser()
# helper classes # helper classes
class TestGroup: class TestGroup:
def __init__(self, name): def __init__(self, name: str) -> None:
# type: (str) -> None
self.name = name self.name = name
self.setup = [] # type: List[TestCode] self.setup = [] # type: List[TestCode]
self.tests = [] # type: List[List[TestCode]] self.tests = [] # type: List[List[TestCode]]
self.cleanup = [] # type: List[TestCode] self.cleanup = [] # type: List[TestCode]
def add_code(self, code, prepend=False): def add_code(self, code: "TestCode", prepend: bool = False) -> None:
# type: (TestCode, bool) -> None
if code.type == 'testsetup': if code.type == 'testsetup':
if prepend: if prepend:
self.setup.insert(0, code) self.setup.insert(0, code)
@ -220,30 +217,28 @@ class TestGroup:
else: else:
raise RuntimeError(__('invalid TestCode type')) raise RuntimeError(__('invalid TestCode type'))
def __repr__(self): def __repr__(self) -> str:
# type: () -> str
return 'TestGroup(name=%r, setup=%r, cleanup=%r, tests=%r)' % ( return 'TestGroup(name=%r, setup=%r, cleanup=%r, tests=%r)' % (
self.name, self.setup, self.cleanup, self.tests) self.name, self.setup, self.cleanup, self.tests)
class TestCode: class TestCode:
def __init__(self, code, type, filename, lineno, options=None): def __init__(self, code: str, type: str, filename: str,
# type: (str, str, Optional[str], int, Optional[Dict]) -> None lineno: int, options: Dict = None) -> None:
self.code = code self.code = code
self.type = type self.type = type
self.filename = filename self.filename = filename
self.lineno = lineno self.lineno = lineno
self.options = options or {} self.options = options or {}
def __repr__(self): def __repr__(self) -> str:
# type: () -> str
return 'TestCode(%r, %r, filename=%r, lineno=%r, options=%r)' % ( return 'TestCode(%r, %r, filename=%r, lineno=%r, options=%r)' % (
self.code, self.type, self.filename, self.lineno, self.options) self.code, self.type, self.filename, self.lineno, self.options)
class SphinxDocTestRunner(doctest.DocTestRunner): class SphinxDocTestRunner(doctest.DocTestRunner):
def summarize(self, out, verbose=None): # type: ignore def summarize(self, out: Callable, verbose: bool = None # type: ignore
# type: (Callable, bool) -> Tuple[int, int] ) -> Tuple[int, int]:
string_io = StringIO() string_io = StringIO()
old_stdout = sys.stdout old_stdout = sys.stdout
sys.stdout = string_io sys.stdout = string_io
@ -254,9 +249,8 @@ class SphinxDocTestRunner(doctest.DocTestRunner):
out(string_io.getvalue()) out(string_io.getvalue())
return res return res
def _DocTestRunner__patched_linecache_getlines(self, filename, def _DocTestRunner__patched_linecache_getlines(self, filename: str,
module_globals=None): module_globals: Any = None) -> Any:
# type: (str, Any) -> Any
# this is overridden from DocTestRunner adding the try-except below # this is overridden from DocTestRunner adding the try-except below
m = self._DocTestRunner__LINECACHE_FILENAME_RE.match(filename) # type: ignore m = self._DocTestRunner__LINECACHE_FILENAME_RE.match(filename) # type: ignore
if m and m.group('name') == self.test.name: if m and m.group('name') == self.test.name:
@ -282,8 +276,7 @@ class DocTestBuilder(Builder):
epilog = __('Testing of doctests in the sources finished, look at the ' epilog = __('Testing of doctests in the sources finished, look at the '
'results in %(outdir)s/output.txt.') 'results in %(outdir)s/output.txt.')
def init(self): def init(self) -> None:
# type: () -> None
# default options # default options
self.opt = self.config.doctest_default_flags self.opt = self.config.doctest_default_flags
@ -312,32 +305,26 @@ class DocTestBuilder(Builder):
'==================================%s\n') % '==================================%s\n') %
(date, '=' * len(date))) (date, '=' * len(date)))
def _out(self, text): def _out(self, text: str) -> None:
# type: (str) -> None
logger.info(text, nonl=True) logger.info(text, nonl=True)
self.outfile.write(text) self.outfile.write(text)
def _warn_out(self, text): def _warn_out(self, text: str) -> None:
# type: (str) -> None
if self.app.quiet or self.app.warningiserror: if self.app.quiet or self.app.warningiserror:
logger.warning(text) logger.warning(text)
else: else:
logger.info(text, nonl=True) logger.info(text, nonl=True)
self.outfile.write(text) self.outfile.write(text)
def get_target_uri(self, docname, typ=None): def get_target_uri(self, docname: str, typ: str = None) -> str:
# type: (str, str) -> str
return '' return ''
def get_outdated_docs(self): def get_outdated_docs(self) -> Set[str]:
# type: () -> Set[str]
return self.env.found_docs return self.env.found_docs
def finish(self): def finish(self) -> None:
# type: () -> None
# write executive summary # write executive summary
def s(v): def s(v: int) -> str:
# type: (int) -> str
return v != 1 and 's' or '' return v != 1 and 's' or ''
repl = (self.total_tries, s(self.total_tries), repl = (self.total_tries, s(self.total_tries),
self.total_failures, s(self.total_failures), self.total_failures, s(self.total_failures),
@ -356,8 +343,8 @@ Doctest summary
if self.total_failures or self.setup_failures or self.cleanup_failures: if self.total_failures or self.setup_failures or self.cleanup_failures:
self.app.statuscode = 1 self.app.statuscode = 1
def write(self, build_docnames, updated_docnames, method='update'): def write(self, build_docnames: Iterable[str], updated_docnames: Sequence[str],
# type: (Iterable[str], Sequence[str], str) -> None method: str = 'update') -> None:
if build_docnames is None: if build_docnames is None:
build_docnames = sorted(self.env.all_docs) build_docnames = sorted(self.env.all_docs)
@ -367,8 +354,7 @@ Doctest summary
doctree = self.env.get_doctree(docname) doctree = self.env.get_doctree(docname)
self.test_doc(docname, doctree) self.test_doc(docname, doctree)
def get_filename_for_node(self, node, docname): def get_filename_for_node(self, node: Node, docname: str) -> str:
# type: (nodes.Node, str) -> str
"""Try to get the file which actually contains the doctest, not the """Try to get the file which actually contains the doctest, not the
filename of the document it's included in.""" filename of the document it's included in."""
try: try:
@ -379,8 +365,7 @@ Doctest summary
return filename return filename
@staticmethod @staticmethod
def get_line_number(node): def get_line_number(node: Node) -> int:
# type: (nodes.Node) -> Optional[int]
"""Get the real line number or admit we don't know.""" """Get the real line number or admit we don't know."""
# TODO: Work out how to store or calculate real (file-relative) # TODO: Work out how to store or calculate real (file-relative)
# line numbers for doctest blocks in docstrings. # line numbers for doctest blocks in docstrings.
@ -395,8 +380,7 @@ Doctest summary
return node.line - 1 return node.line - 1
return None return None
def skipped(self, node): def skipped(self, node: Element) -> bool:
# type: (nodes.Element) -> bool
if 'skipif' not in node: if 'skipif' not in node:
return False return False
else: else:
@ -409,8 +393,7 @@ Doctest summary
exec(self.config.doctest_global_cleanup, context) exec(self.config.doctest_global_cleanup, context)
return should_skip return should_skip
def test_doc(self, docname, doctree): def test_doc(self, docname: str, doctree: Node) -> None:
# type: (str, nodes.Node) -> None
groups = {} # type: Dict[str, TestGroup] groups = {} # type: Dict[str, TestGroup]
add_to_all_groups = [] add_to_all_groups = []
self.setup_runner = SphinxDocTestRunner(verbose=False, self.setup_runner = SphinxDocTestRunner(verbose=False,
@ -424,17 +407,15 @@ Doctest summary
self.cleanup_runner._fakeout = self.setup_runner._fakeout # type: ignore self.cleanup_runner._fakeout = self.setup_runner._fakeout # type: ignore
if self.config.doctest_test_doctest_blocks: if self.config.doctest_test_doctest_blocks:
def condition(node): def condition(node: Node) -> bool:
# type: (nodes.Node) -> bool
return (isinstance(node, (nodes.literal_block, nodes.comment)) and return (isinstance(node, (nodes.literal_block, nodes.comment)) and
'testnodetype' in node) or \ 'testnodetype' in node) or \
isinstance(node, nodes.doctest_block) isinstance(node, nodes.doctest_block)
else: else:
def condition(node): def condition(node: Node) -> bool:
# type: (nodes.Node) -> bool
return isinstance(node, (nodes.literal_block, nodes.comment)) \ return isinstance(node, (nodes.literal_block, nodes.comment)) \
and 'testnodetype' in node and 'testnodetype' in node
for node in doctree.traverse(condition): # type: nodes.Element for node in doctree.traverse(condition): # type: Element
if self.skipped(node): if self.skipped(node):
continue continue
@ -490,16 +471,13 @@ Doctest summary
self.cleanup_failures += res_f self.cleanup_failures += res_f
self.cleanup_tries += res_t self.cleanup_tries += res_t
def compile(self, code, name, type, flags, dont_inherit): def compile(self, code: str, name: str, type: str, flags: Any, dont_inherit: bool) -> Any:
# type: (str, str, str, Any, bool) -> Any
return compile(code, name, self.type, flags, dont_inherit) return compile(code, name, self.type, flags, dont_inherit)
def test_group(self, group): def test_group(self, group: TestGroup) -> None:
# type: (TestGroup) -> None
ns = {} # type: Dict ns = {} # type: Dict
def run_setup_cleanup(runner, testcodes, what): def run_setup_cleanup(runner: Any, testcodes: List[TestCode], what: Any) -> bool:
# type: (Any, List[TestCode], Any) -> bool
examples = [] examples = []
for testcode in testcodes: for testcode in testcodes:
example = doctest.Example(testcode.code, '', lineno=testcode.lineno) example = doctest.Example(testcode.code, '', lineno=testcode.lineno)
@ -568,8 +546,7 @@ Doctest summary
run_setup_cleanup(self.cleanup_runner, group.cleanup, 'cleanup') run_setup_cleanup(self.cleanup_runner, group.cleanup, 'cleanup')
def setup(app): def setup(app: "Sphinx") -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_directive('testsetup', TestsetupDirective) app.add_directive('testsetup', TestsetupDirective)
app.add_directive('testcleanup', TestcleanupDirective) app.add_directive('testcleanup', TestcleanupDirective)
app.add_directive('doctest', DoctestDirective) app.add_directive('doctest', DoctestDirective)

View File

@ -23,23 +23,22 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, List, Tuple
from docutils import nodes, utils from docutils import nodes, utils
from docutils.nodes import Node, system_message
from docutils.parsers.rst.states import Inliner
import sphinx import sphinx
from sphinx.application import Sphinx
from sphinx.util.nodes import split_explicit_title from sphinx.util.nodes import split_explicit_title
from sphinx.util.typing import RoleFunction
if False:
# For type annotation
from typing import Any, Dict, List, Tuple # NOQA
from docutils.parsers.rst.states import Inliner # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.util.typing import RoleFunction # NOQA
def make_link_role(base_url, prefix): def make_link_role(base_url: str, prefix: str) -> RoleFunction:
# type: (str, str) -> RoleFunction def role(typ: str, rawtext: str, text: str, lineno: int,
def role(typ, rawtext, text, lineno, inliner, options={}, content=[]): inliner: Inliner, options: Dict = {}, content: List[str] = []
# type: (str, str, str, int, Inliner, Dict, List[str]) -> Tuple[List[nodes.Node], List[nodes.system_message]] # NOQA ) -> Tuple[List[Node], List[system_message]]:
text = utils.unescape(text) text = utils.unescape(text)
has_explicit_title, title, part = split_explicit_title(text) has_explicit_title, title, part = split_explicit_title(text)
try: try:
@ -60,14 +59,12 @@ def make_link_role(base_url, prefix):
return role return role
def setup_link_roles(app): def setup_link_roles(app: Sphinx) -> None:
# type: (Sphinx) -> None
for name, (base_url, prefix) in app.config.extlinks.items(): for name, (base_url, prefix) in app.config.extlinks.items():
app.add_role(name, make_link_role(base_url, prefix)) app.add_role(name, make_link_role(base_url, prefix))
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_config_value('extlinks', {}, 'env') app.add_config_value('extlinks', {}, 'env')
app.connect('builder-inited', setup_link_roles) app.connect('builder-inited', setup_link_roles)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True} return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@ -10,18 +10,14 @@
import os import os
import urllib import urllib
from typing import Any, Dict
import sphinx import sphinx
from sphinx.application import Sphinx
if False: from sphinx.environment import BuildEnvironment
# For type annotation
from typing import Any, Dict # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
def create_nojekyll_and_cname(app, env): def create_nojekyll_and_cname(app: Sphinx, env: BuildEnvironment) -> None:
# type: (Sphinx, BuildEnvironment) -> None
if app.builder.format == 'html': if app.builder.format == 'html':
open(os.path.join(app.builder.outdir, '.nojekyll'), 'wt').close() open(os.path.join(app.builder.outdir, '.nojekyll'), 'wt').close()
@ -35,7 +31,6 @@ def create_nojekyll_and_cname(app, env):
f.write(domain) f.write(domain)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.connect('env-updated', create_nojekyll_and_cname) app.connect('env-updated', create_nojekyll_and_cname)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True} return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@ -15,31 +15,27 @@ import subprocess
from hashlib import sha1 from hashlib import sha1
from os import path from os import path
from subprocess import CalledProcessError, PIPE from subprocess import CalledProcessError, PIPE
from typing import Any, Dict, List, Tuple
from docutils import nodes from docutils import nodes
from docutils.parsers.rst import directives from docutils.nodes import Node
from docutils.parsers.rst import Directive, directives
import sphinx import sphinx
from sphinx.application import Sphinx
from sphinx.errors import SphinxError from sphinx.errors import SphinxError
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective, SphinxTranslator
from sphinx.util.fileutil import copy_asset from sphinx.util.fileutil import copy_asset
from sphinx.util.i18n import search_image_for_language from sphinx.util.i18n import search_image_for_language
from sphinx.util.nodes import set_source_info from sphinx.util.nodes import set_source_info
from sphinx.util.osutil import ensuredir from sphinx.util.osutil import ensuredir
from sphinx.writers.html import HTMLTranslator
if False: from sphinx.writers.latex import LaTeXTranslator
# For type annotation from sphinx.writers.manpage import ManualPageTranslator
from docutils.parsers.rst import Directive # NOQA from sphinx.writers.texinfo import TexinfoTranslator
from typing import Any, Dict, List, Tuple # NOQA from sphinx.writers.text import TextTranslator
from sphinx.application import Sphinx # NOQA
from sphinx.util.docutils import SphinxTranslator # NOQA
from sphinx.writers.html import HTMLTranslator # NOQA
from sphinx.writers.latex import LaTeXTranslator # NOQA
from sphinx.writers.manpage import ManualPageTranslator # NOQA
from sphinx.writers.texinfo import TexinfoTranslator # NOQA
from sphinx.writers.text import TextTranslator # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -53,8 +49,7 @@ class ClickableMapDefinition:
maptag_re = re.compile('<map id="(.*?)"') maptag_re = re.compile('<map id="(.*?)"')
href_re = re.compile('href=".*?"') href_re = re.compile('href=".*?"')
def __init__(self, filename, content, dot=''): def __init__(self, filename: str, content: str, dot: str = '') -> None:
# type: (str, str, str) -> None
self.id = None # type: str self.id = None # type: str
self.filename = filename self.filename = filename
self.content = content.splitlines() self.content = content.splitlines()
@ -62,8 +57,7 @@ class ClickableMapDefinition:
self.parse(dot=dot) self.parse(dot=dot)
def parse(self, dot=None): def parse(self, dot: str = None) -> None:
# type: (str) -> None
matched = self.maptag_re.match(self.content[0]) matched = self.maptag_re.match(self.content[0])
if not matched: if not matched:
raise GraphvizError('Invalid clickable map file found: %s' % self.filename) raise GraphvizError('Invalid clickable map file found: %s' % self.filename)
@ -80,8 +74,7 @@ class ClickableMapDefinition:
if self.href_re.search(line): if self.href_re.search(line):
self.clickable.append(line) self.clickable.append(line)
def generate_clickable_map(self): def generate_clickable_map(self) -> str:
# type: () -> str
"""Generate clickable map tags if clickable item exists. """Generate clickable map tags if clickable item exists.
If not exists, this only returns empty string. If not exists, this only returns empty string.
@ -96,8 +89,7 @@ class graphviz(nodes.General, nodes.Inline, nodes.Element):
pass pass
def figure_wrapper(directive, node, caption): def figure_wrapper(directive: Directive, node: graphviz, caption: str) -> nodes.figure:
# type: (Directive, graphviz, str) -> nodes.figure
figure_node = nodes.figure('', node) figure_node = nodes.figure('', node)
if 'align' in node: if 'align' in node:
figure_node['align'] = node.attributes.pop('align') figure_node['align'] = node.attributes.pop('align')
@ -110,8 +102,7 @@ def figure_wrapper(directive, node, caption):
return figure_node return figure_node
def align_spec(argument): def align_spec(argument: Any) -> str:
# type: (Any) -> str
return directives.choice(argument, ('left', 'center', 'right')) return directives.choice(argument, ('left', 'center', 'right'))
@ -132,8 +123,7 @@ class Graphviz(SphinxDirective):
'name': directives.unchanged, 'name': directives.unchanged,
} }
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
if self.arguments: if self.arguments:
document = self.state.document document = self.state.document
if self.content: if self.content:
@ -194,8 +184,7 @@ class GraphvizSimple(SphinxDirective):
'name': directives.unchanged, 'name': directives.unchanged,
} }
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
node = graphviz() node = graphviz()
node['code'] = '%s %s {\n%s\n}\n' % \ node['code'] = '%s %s {\n%s\n}\n' % \
(self.name, self.arguments[0], '\n'.join(self.content)) (self.name, self.arguments[0], '\n'.join(self.content))
@ -216,8 +205,8 @@ class GraphvizSimple(SphinxDirective):
return [figure] return [figure]
def render_dot(self, code, options, format, prefix='graphviz'): def render_dot(self: SphinxTranslator, code: str, options: Dict,
# type: (SphinxTranslator, str, Dict, str, str) -> Tuple[str, str] format: str, prefix: str = 'graphviz') -> Tuple[str, str]:
"""Render graphviz code into a PNG or PDF output file.""" """Render graphviz code into a PNG or PDF output file."""
graphviz_dot = options.get('graphviz_dot', self.builder.config.graphviz_dot) graphviz_dot = options.get('graphviz_dot', self.builder.config.graphviz_dot)
hashkey = (code + str(options) + str(graphviz_dot) + hashkey = (code + str(options) + str(graphviz_dot) +
@ -265,9 +254,9 @@ def render_dot(self, code, options, format, prefix='graphviz'):
'[stdout]\n%r') % (exc.stderr, exc.stdout)) '[stdout]\n%r') % (exc.stderr, exc.stdout))
def render_dot_html(self, node, code, options, prefix='graphviz', def render_dot_html(self: HTMLTranslator, node: graphviz, code: str, options: Dict,
imgcls=None, alt=None): prefix: str = 'graphviz', imgcls: str = None, alt: str = None
# type: (HTMLTranslator, graphviz, str, Dict, str, str, str) -> Tuple[str, str] ) -> Tuple[str, str]:
format = self.builder.config.graphviz_output_format format = self.builder.config.graphviz_output_format
try: try:
if format not in ('png', 'svg'): if format not in ('png', 'svg'):
@ -319,13 +308,12 @@ def render_dot_html(self, node, code, options, prefix='graphviz',
raise nodes.SkipNode raise nodes.SkipNode
def html_visit_graphviz(self, node): def html_visit_graphviz(self: HTMLTranslator, node: graphviz) -> None:
# type: (HTMLTranslator, graphviz) -> None
render_dot_html(self, node, node['code'], node['options']) render_dot_html(self, node, node['code'], node['options'])
def render_dot_latex(self, node, code, options, prefix='graphviz'): def render_dot_latex(self: LaTeXTranslator, node: graphviz, code: str,
# type: (LaTeXTranslator, graphviz, str, Dict, str) -> None options: Dict, prefix: str = 'graphviz') -> None:
try: try:
fname, outfn = render_dot(self, code, options, 'pdf', prefix) fname, outfn = render_dot(self, code, options, 'pdf', prefix)
except GraphvizError as exc: except GraphvizError as exc:
@ -357,13 +345,12 @@ def render_dot_latex(self, node, code, options, prefix='graphviz'):
raise nodes.SkipNode raise nodes.SkipNode
def latex_visit_graphviz(self, node): def latex_visit_graphviz(self: LaTeXTranslator, node: graphviz) -> None:
# type: (LaTeXTranslator, graphviz) -> None
render_dot_latex(self, node, node['code'], node['options']) render_dot_latex(self, node, node['code'], node['options'])
def render_dot_texinfo(self, node, code, options, prefix='graphviz'): def render_dot_texinfo(self: TexinfoTranslator, node: graphviz, code: str,
# type: (TexinfoTranslator, graphviz, str, Dict, str) -> None options: Dict, prefix: str = 'graphviz') -> None:
try: try:
fname, outfn = render_dot(self, code, options, 'png', prefix) fname, outfn = render_dot(self, code, options, 'png', prefix)
except GraphvizError as exc: except GraphvizError as exc:
@ -374,13 +361,11 @@ def render_dot_texinfo(self, node, code, options, prefix='graphviz'):
raise nodes.SkipNode raise nodes.SkipNode
def texinfo_visit_graphviz(self, node): def texinfo_visit_graphviz(self: TexinfoTranslator, node: graphviz) -> None:
# type: (TexinfoTranslator, graphviz) -> None
render_dot_texinfo(self, node, node['code'], node['options']) render_dot_texinfo(self, node, node['code'], node['options'])
def text_visit_graphviz(self, node): def text_visit_graphviz(self: TextTranslator, node: graphviz) -> None:
# type: (TextTranslator, graphviz) -> None
if 'alt' in node.attributes: if 'alt' in node.attributes:
self.add_text(_('[graph: %s]') % node['alt']) self.add_text(_('[graph: %s]') % node['alt'])
else: else:
@ -388,8 +373,7 @@ def text_visit_graphviz(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def man_visit_graphviz(self, node): def man_visit_graphviz(self: ManualPageTranslator, node: graphviz) -> None:
# type: (ManualPageTranslator, graphviz) -> None
if 'alt' in node.attributes: if 'alt' in node.attributes:
self.body.append(_('[graph: %s]') % node['alt']) self.body.append(_('[graph: %s]') % node['alt'])
else: else:
@ -397,16 +381,14 @@ def man_visit_graphviz(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def on_build_finished(app, exc): def on_build_finished(app: Sphinx, exc: Exception) -> None:
# type: (Sphinx, Exception) -> None
if exc is None: if exc is None:
src = path.join(sphinx.package_dir, 'templates', 'graphviz', 'graphviz.css') src = path.join(sphinx.package_dir, 'templates', 'graphviz', 'graphviz.css')
dst = path.join(app.outdir, '_static') dst = path.join(app.outdir, '_static')
copy_asset(src, dst) copy_asset(src, dst)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_node(graphviz, app.add_node(graphviz,
html=(html_visit_graphviz, None), html=(html_visit_graphviz, None),
latex=(latex_visit_graphviz, None), latex=(latex_visit_graphviz, None),

View File

@ -19,17 +19,16 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, List
from docutils import nodes from docutils import nodes
from docutils.nodes import Node
import sphinx import sphinx
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import nested_parse_with_titles from sphinx.util.nodes import nested_parse_with_titles
if False:
# For type annotation
from typing import Any, Dict, List # NOQA
from sphinx.application import Sphinx # NOQA
class ifconfig(nodes.Element): class ifconfig(nodes.Element):
pass pass
@ -43,8 +42,7 @@ class IfConfig(SphinxDirective):
final_argument_whitespace = True final_argument_whitespace = True
option_spec = {} # type: Dict option_spec = {} # type: Dict
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
node = ifconfig() node = ifconfig()
node.document = self.state.document node.document = self.state.document
self.set_source_info(node) self.set_source_info(node)
@ -53,8 +51,7 @@ class IfConfig(SphinxDirective):
return [node] return [node]
def process_ifconfig_nodes(app, doctree, docname): def process_ifconfig_nodes(app: Sphinx, doctree: nodes.document, docname: str) -> None:
# type: (Sphinx, nodes.document, str) -> None
ns = {confval.name: confval.value for confval in app.config} ns = {confval.name: confval.value for confval in app.config}
ns.update(app.config.__dict__.copy()) ns.update(app.config.__dict__.copy())
ns['builder'] = app.builder.name ns['builder'] = app.builder.name
@ -76,8 +73,7 @@ def process_ifconfig_nodes(app, doctree, docname):
node.replace_self(node.children) node.replace_self(node.children)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_node(ifconfig) app.add_node(ifconfig)
app.add_directive('ifconfig', IfConfig) app.add_directive('ifconfig', IfConfig)
app.connect('doctree-resolved', process_ifconfig_nodes) app.connect('doctree-resolved', process_ifconfig_nodes)

View File

@ -7,19 +7,17 @@
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import subprocess import subprocess
from subprocess import CalledProcessError, PIPE from subprocess import CalledProcessError, PIPE
from typing import Any, Dict
from sphinx.application import Sphinx
from sphinx.errors import ExtensionError from sphinx.errors import ExtensionError
from sphinx.locale import __ from sphinx.locale import __
from sphinx.transforms.post_transforms.images import ImageConverter from sphinx.transforms.post_transforms.images import ImageConverter
from sphinx.util import logging from sphinx.util import logging
if False:
# For type annotation
from typing import Any, Dict # NOQA
from sphinx.application import Sphinx # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,8 +29,7 @@ class ImagemagickConverter(ImageConverter):
('application/pdf', 'image/png'), ('application/pdf', 'image/png'),
] ]
def is_available(self): def is_available(self) -> bool:
# type: () -> bool
"""Confirms the converter is available or not.""" """Confirms the converter is available or not."""
try: try:
args = [self.config.image_converter, '-version'] args = [self.config.image_converter, '-version']
@ -50,8 +47,7 @@ class ImagemagickConverter(ImageConverter):
exc.stderr, exc.stdout) exc.stderr, exc.stdout)
return False return False
def convert(self, _from, _to): def convert(self, _from: str, _to: str) -> bool:
# type: (str, str) -> bool
"""Converts the image to expected one.""" """Converts the image to expected one."""
try: try:
# append an index 0 to source filename to pick up the first frame # append an index 0 to source filename to pick up the first frame
@ -75,8 +71,7 @@ class ImagemagickConverter(ImageConverter):
(exc.stderr, exc.stdout)) (exc.stderr, exc.stdout))
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_post_transform(ImagemagickConverter) app.add_post_transform(ImagemagickConverter)
app.add_config_value('image_converter', 'convert', 'env') app.add_config_value('image_converter', 'convert', 'env')
app.add_config_value('image_converter_args', [], 'env') app.add_config_value('image_converter_args', [], 'env')

View File

@ -17,11 +17,16 @@ import tempfile
from hashlib import sha1 from hashlib import sha1
from os import path from os import path
from subprocess import CalledProcessError, PIPE from subprocess import CalledProcessError, PIPE
from typing import Any, Dict, List, Tuple
from docutils import nodes from docutils import nodes
from docutils.nodes import Element
import sphinx import sphinx
from sphinx import package_dir from sphinx import package_dir
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.errors import SphinxError from sphinx.errors import SphinxError
from sphinx.locale import _, __ from sphinx.locale import _, __
@ -30,14 +35,7 @@ from sphinx.util.math import get_node_equation_number, wrap_displaymath
from sphinx.util.osutil import ensuredir from sphinx.util.osutil import ensuredir
from sphinx.util.png import read_png_depth, write_png_depth from sphinx.util.png import read_png_depth, write_png_depth
from sphinx.util.template import LaTeXRenderer from sphinx.util.template import LaTeXRenderer
from sphinx.writers.html import HTMLTranslator
if False:
# For type annotation
from typing import Any, Dict, List, Tuple, Union # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.config import Config # NOQA
from sphinx.writers.html import HTMLTranslator # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -47,8 +45,7 @@ templates_path = path.join(package_dir, 'templates', 'imgmath')
class MathExtError(SphinxError): class MathExtError(SphinxError):
category = 'Math extension error' category = 'Math extension error'
def __init__(self, msg, stderr=None, stdout=None): def __init__(self, msg: str, stderr: bytes = None, stdout: bytes = None) -> None:
# type: (str, bytes, bytes) -> None
if stderr: if stderr:
msg += '\n[stderr]\n' + stderr.decode(sys.getdefaultencoding(), 'replace') msg += '\n[stderr]\n' + stderr.decode(sys.getdefaultencoding(), 'replace')
if stdout: if stdout:
@ -92,8 +89,7 @@ DOC_BODY_PREVIEW = r'''
depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]') depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]')
def generate_latex_macro(math, config, confdir=''): def generate_latex_macro(math: str, config: Config, confdir: str = '') -> str:
# type: (str, Config, str) -> str
"""Generate LaTeX macro.""" """Generate LaTeX macro."""
variables = { variables = {
'fontsize': config.imgmath_font_size, 'fontsize': config.imgmath_font_size,
@ -115,8 +111,7 @@ def generate_latex_macro(math, config, confdir=''):
return LaTeXRenderer(templates_path).render(template_name, variables) return LaTeXRenderer(templates_path).render(template_name, variables)
def ensure_tempdir(builder): def ensure_tempdir(builder: Builder) -> str:
# type: (Builder) -> str
"""Create temporary directory. """Create temporary directory.
use only one tempdir per build -- the use of a directory is cleaner use only one tempdir per build -- the use of a directory is cleaner
@ -129,8 +124,7 @@ def ensure_tempdir(builder):
return builder._imgmath_tempdir # type: ignore return builder._imgmath_tempdir # type: ignore
def compile_math(latex, builder): def compile_math(latex: str, builder: Builder) -> str:
# type: (str, Builder) -> str
"""Compile LaTeX macros for math to DVI.""" """Compile LaTeX macros for math to DVI."""
tempdir = ensure_tempdir(builder) tempdir = ensure_tempdir(builder)
filename = path.join(tempdir, 'math.tex') filename = path.join(tempdir, 'math.tex')
@ -157,8 +151,7 @@ def compile_math(latex, builder):
raise MathExtError('latex exited with error', exc.stderr, exc.stdout) raise MathExtError('latex exited with error', exc.stderr, exc.stdout)
def convert_dvi_to_image(command, name): def convert_dvi_to_image(command: List[str], name: str) -> Tuple[bytes, bytes]:
# type: (List[str], str) -> Tuple[bytes, bytes]
"""Convert DVI file to specific image format.""" """Convert DVI file to specific image format."""
try: try:
ret = subprocess.run(command, stdout=PIPE, stderr=PIPE, check=True) ret = subprocess.run(command, stdout=PIPE, stderr=PIPE, check=True)
@ -172,8 +165,7 @@ def convert_dvi_to_image(command, name):
raise MathExtError('%s exited with error' % name, exc.stderr, exc.stdout) raise MathExtError('%s exited with error' % name, exc.stderr, exc.stdout)
def convert_dvi_to_png(dvipath, builder): def convert_dvi_to_png(dvipath: str, builder: Builder) -> Tuple[str, int]:
# type: (str, Builder) -> Tuple[str, int]
"""Convert DVI file to PNG image.""" """Convert DVI file to PNG image."""
tempdir = ensure_tempdir(builder) tempdir = ensure_tempdir(builder)
filename = path.join(tempdir, 'math.png') filename = path.join(tempdir, 'math.png')
@ -199,8 +191,7 @@ def convert_dvi_to_png(dvipath, builder):
return filename, depth return filename, depth
def convert_dvi_to_svg(dvipath, builder): def convert_dvi_to_svg(dvipath: str, builder: Builder) -> Tuple[str, int]:
# type: (str, Builder) -> Tuple[str, int]
"""Convert DVI file to SVG image.""" """Convert DVI file to SVG image."""
tempdir = ensure_tempdir(builder) tempdir = ensure_tempdir(builder)
filename = path.join(tempdir, 'math.svg') filename = path.join(tempdir, 'math.svg')
@ -214,8 +205,7 @@ def convert_dvi_to_svg(dvipath, builder):
return filename, None return filename, None
def render_math(self, math): def render_math(self: HTMLTranslator, math: str) -> Tuple[str, int]:
# type: (HTMLTranslator, str) -> Tuple[str, int]
"""Render the LaTeX math expression *math* using latex and dvipng or """Render the LaTeX math expression *math* using latex and dvipng or
dvisvgm. dvisvgm.
@ -271,8 +261,7 @@ def render_math(self, math):
return relfn, depth return relfn, depth
def cleanup_tempdir(app, exc): def cleanup_tempdir(app: Sphinx, exc: Exception) -> None:
# type: (Sphinx, Exception) -> None
if exc: if exc:
return return
if not hasattr(app.builder, '_imgmath_tempdir'): if not hasattr(app.builder, '_imgmath_tempdir'):
@ -283,15 +272,13 @@ def cleanup_tempdir(app, exc):
pass pass
def get_tooltip(self, node): def get_tooltip(self: HTMLTranslator, node: Element) -> str:
# type: (HTMLTranslator, Union[nodes.math, nodes.math_block]) -> str
if self.builder.config.imgmath_add_tooltips: if self.builder.config.imgmath_add_tooltips:
return ' alt="%s"' % self.encode(node.astext()).strip() return ' alt="%s"' % self.encode(node.astext()).strip()
return '' return ''
def html_visit_math(self, node): def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
# type: (HTMLTranslator, nodes.math) -> None
try: try:
fname, depth = render_math(self, '$' + node.astext() + '$') fname, depth = render_math(self, '$' + node.astext() + '$')
except MathExtError as exc: except MathExtError as exc:
@ -313,8 +300,7 @@ def html_visit_math(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def html_visit_displaymath(self, node): def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None:
# type: (HTMLTranslator, nodes.math_block) -> None
if node['nowrap']: if node['nowrap']:
latex = node.astext() latex = node.astext()
else: else:
@ -354,8 +340,7 @@ deprecated_alias('sphinx.ext.imgmath',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_html_math_renderer('imgmath', app.add_html_math_renderer('imgmath',
(html_visit_math, None), (html_visit_math, None),
(html_visit_displaymath, None)) (html_visit_displaymath, None))

View File

@ -40,27 +40,25 @@ import inspect
import re import re
import sys import sys
from hashlib import md5 from hashlib import md5
from typing import Iterable, cast from typing import Any, Dict, Iterable, List, Tuple
from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Node
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
import sphinx import sphinx
from sphinx import addnodes from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.ext.graphviz import ( from sphinx.ext.graphviz import (
graphviz, figure_wrapper, graphviz, figure_wrapper,
render_dot_html, render_dot_latex, render_dot_texinfo render_dot_html, render_dot_latex, render_dot_texinfo
) )
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective
from sphinx.writers.html import HTMLTranslator
if False: from sphinx.writers.latex import LaTeXTranslator
# For type annotation from sphinx.writers.texinfo import TexinfoTranslator
from typing import Any, Dict, List, Optional, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.writers.html import HTMLTranslator # NOQA
from sphinx.writers.latex import LaTeXTranslator # NOQA
from sphinx.writers.texinfo import TexinfoTranslator # NOQA
module_sig_re = re.compile(r'''^(?:([\w.]*)\.)? # module names module_sig_re = re.compile(r'''^(?:([\w.]*)\.)? # module names
@ -68,8 +66,7 @@ module_sig_re = re.compile(r'''^(?:([\w.]*)\.)? # module names
''', re.VERBOSE) ''', re.VERBOSE)
def try_import(objname): def try_import(objname: str) -> Any:
# type: (str) -> Any
"""Import a object or module using *name* and *currentmodule*. """Import a object or module using *name* and *currentmodule*.
*name* should be a relative name from *currentmodule* or *name* should be a relative name from *currentmodule* or
a fully-qualified name. a fully-qualified name.
@ -96,8 +93,7 @@ def try_import(objname):
return None return None
def import_classes(name, currmodule): def import_classes(name: str, currmodule: str) -> Any:
# type: (str, str) -> Any
"""Import a class using its fully-qualified *name*.""" """Import a class using its fully-qualified *name*."""
target = None target = None
@ -138,9 +134,9 @@ class InheritanceGraph:
from all the way to the root "object", and then is able to generate a from all the way to the root "object", and then is able to generate a
graphviz dot graph from them. graphviz dot graph from them.
""" """
def __init__(self, class_names, currmodule, show_builtins=False, def __init__(self, class_names: List[str], currmodule: str, show_builtins: bool = False,
private_bases=False, parts=0, aliases=None, top_classes=[]): private_bases: bool = False, parts: int = 0, aliases: Dict[str, str] = None,
# type: (List[str], str, bool, bool, int, Optional[Dict[str, str]], List[Any]) -> None top_classes: List[Any] = []) -> None:
"""*class_names* is a list of child classes to show bases from. """*class_names* is a list of child classes to show bases from.
If *show_builtins* is True, then Python builtins will be shown If *show_builtins* is True, then Python builtins will be shown
@ -154,16 +150,16 @@ class InheritanceGraph:
raise InheritanceException('No classes found for ' raise InheritanceException('No classes found for '
'inheritance diagram') 'inheritance diagram')
def _import_classes(self, class_names, currmodule): def _import_classes(self, class_names: List[str], currmodule: str) -> List[Any]:
# type: (List[str], str) -> List[Any]
"""Import a list of classes.""" """Import a list of classes."""
classes = [] # type: List[Any] classes = [] # type: List[Any]
for name in class_names: for name in class_names:
classes.extend(import_classes(name, currmodule)) classes.extend(import_classes(name, currmodule))
return classes return classes
def _class_info(self, classes, show_builtins, private_bases, parts, aliases, top_classes): def _class_info(self, classes: List[Any], show_builtins: bool, private_bases: bool,
# type: (List[Any], bool, bool, int, Optional[Dict[str, str]], List[Any]) -> List[Tuple[str, str, List[str], str]] # NOQA parts: int, aliases: Dict[str, str], top_classes: List[Any]
) -> List[Tuple[str, str, List[str], str]]:
"""Return name and bases for all classes that are ancestors of """Return name and bases for all classes that are ancestors of
*classes*. *classes*.
@ -182,8 +178,7 @@ class InheritanceGraph:
all_classes = {} all_classes = {}
py_builtins = vars(builtins).values() py_builtins = vars(builtins).values()
def recurse(cls): def recurse(cls: Any) -> None:
# type: (Any) -> None
if not show_builtins and cls in py_builtins: if not show_builtins and cls in py_builtins:
return return
if not private_bases and cls.__name__.startswith('_'): if not private_bases and cls.__name__.startswith('_'):
@ -222,8 +217,7 @@ class InheritanceGraph:
return list(all_classes.values()) return list(all_classes.values())
def class_name(self, cls, parts=0, aliases=None): def class_name(self, cls: Any, parts: int = 0, aliases: Dict[str, str] = None) -> str:
# type: (Any, int, Optional[Dict[str, str]]) -> str
"""Given a class object, return a fully-qualified name. """Given a class object, return a fully-qualified name.
This works for things I've tested in matplotlib so far, but may not be This works for things I've tested in matplotlib so far, but may not be
@ -243,8 +237,7 @@ class InheritanceGraph:
return aliases[result] return aliases[result]
return result return result
def get_all_class_names(self): def get_all_class_names(self) -> List[str]:
# type: () -> List[str]
"""Get all of the class names involved in the graph.""" """Get all of the class names involved in the graph."""
return [fullname for (_, fullname, _, _) in self.class_info] return [fullname for (_, fullname, _, _) in self.class_info]
@ -266,17 +259,15 @@ class InheritanceGraph:
'style': '"setlinewidth(0.5)"', 'style': '"setlinewidth(0.5)"',
} }
def _format_node_attrs(self, attrs): def _format_node_attrs(self, attrs: Dict) -> str:
# type: (Dict) -> str
return ','.join(['%s=%s' % x for x in sorted(attrs.items())]) return ','.join(['%s=%s' % x for x in sorted(attrs.items())])
def _format_graph_attrs(self, attrs): def _format_graph_attrs(self, attrs: Dict) -> str:
# type: (Dict) -> str
return ''.join(['%s=%s;\n' % x for x in sorted(attrs.items())]) return ''.join(['%s=%s;\n' % x for x in sorted(attrs.items())])
def generate_dot(self, name, urls={}, env=None, def generate_dot(self, name: str, urls: Dict = {}, env: BuildEnvironment = None,
graph_attrs={}, node_attrs={}, edge_attrs={}): graph_attrs: Dict = {}, node_attrs: Dict = {}, edge_attrs: Dict = {}
# type: (str, Dict, BuildEnvironment, Dict, Dict, Dict) -> str ) -> str:
"""Generate a graphviz dot graph from the classes that were passed in """Generate a graphviz dot graph from the classes that were passed in
to __init__. to __init__.
@ -344,8 +335,7 @@ class InheritanceDiagram(SphinxDirective):
'top-classes': directives.unchanged_required, 'top-classes': directives.unchanged_required,
} }
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
node = inheritance_diagram() node = inheritance_diagram()
node.document = self.state.document node.document = self.state.document
class_names = self.arguments[0].split() class_names = self.arguments[0].split()
@ -391,14 +381,12 @@ class InheritanceDiagram(SphinxDirective):
return [figure] return [figure]
def get_graph_hash(node): def get_graph_hash(node: inheritance_diagram) -> str:
# type: (inheritance_diagram) -> str
encoded = (node['content'] + str(node['parts'])).encode() encoded = (node['content'] + str(node['parts'])).encode()
return md5(encoded).hexdigest()[-10:] return md5(encoded).hexdigest()[-10:]
def html_visit_inheritance_diagram(self, node): def html_visit_inheritance_diagram(self: HTMLTranslator, node: inheritance_diagram) -> None:
# type: (HTMLTranslator, inheritance_diagram) -> None
""" """
Output the graph for HTML. This will insert a PNG with clickable Output the graph for HTML. This will insert a PNG with clickable
image map. image map.
@ -431,8 +419,7 @@ def html_visit_inheritance_diagram(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def latex_visit_inheritance_diagram(self, node): def latex_visit_inheritance_diagram(self: LaTeXTranslator, node: inheritance_diagram) -> None:
# type: (LaTeXTranslator, inheritance_diagram) -> None
""" """
Output the graph for LaTeX. This will insert a PDF. Output the graph for LaTeX. This will insert a PDF.
""" """
@ -447,8 +434,8 @@ def latex_visit_inheritance_diagram(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def texinfo_visit_inheritance_diagram(self, node): def texinfo_visit_inheritance_diagram(self: TexinfoTranslator, node: inheritance_diagram
# type: (TexinfoTranslator, inheritance_diagram) -> None ) -> None:
""" """
Output the graph for Texinfo. This will insert a PNG. Output the graph for Texinfo. This will insert a PNG.
""" """
@ -463,13 +450,11 @@ def texinfo_visit_inheritance_diagram(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def skip(self, node): def skip(self: nodes.NodeVisitor, node: inheritance_diagram) -> None:
# type: (nodes.NodeVisitor, inheritance_diagram) -> None
raise nodes.SkipNode raise nodes.SkipNode
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.setup_extension('sphinx.ext.graphviz') app.setup_extension('sphinx.ext.graphviz')
app.add_node( app.add_node(
inheritance_diagram, inheritance_diagram,

View File

@ -28,24 +28,23 @@ import posixpath
import sys import sys
import time import time
from os import path from os import path
from typing import Any, Dict, IO, List, Tuple
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, TextElement
from docutils.utils import relative_path from docutils.utils import relative_path
import sphinx import sphinx
from sphinx.application import Sphinx
from sphinx.builders.html import INVENTORY_FILENAME from sphinx.builders.html import INVENTORY_FILENAME
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.util import requests, logging from sphinx.util import requests, logging
from sphinx.util.inventory import InventoryFile from sphinx.util.inventory import InventoryFile
from sphinx.util.typing import Inventory
if False:
# For type annotation
from typing import Any, Dict, IO, List, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.config import Config # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.util.typing import Inventory # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -53,8 +52,7 @@ logger = logging.getLogger(__name__)
class InventoryAdapter: class InventoryAdapter:
"""Inventory adapter for environment""" """Inventory adapter for environment"""
def __init__(self, env): def __init__(self, env: BuildEnvironment) -> None:
# type: (BuildEnvironment) -> None
self.env = env self.env = env
if not hasattr(env, 'intersphinx_cache'): if not hasattr(env, 'intersphinx_cache'):
@ -63,28 +61,23 @@ class InventoryAdapter:
self.env.intersphinx_named_inventory = {} # type: ignore self.env.intersphinx_named_inventory = {} # type: ignore
@property @property
def cache(self): def cache(self) -> Dict[str, Tuple[str, int, Inventory]]:
# type: () -> Dict[str, Tuple[str, int, Inventory]]
return self.env.intersphinx_cache # type: ignore return self.env.intersphinx_cache # type: ignore
@property @property
def main_inventory(self): def main_inventory(self) -> Inventory:
# type: () -> Inventory
return self.env.intersphinx_inventory # type: ignore return self.env.intersphinx_inventory # type: ignore
@property @property
def named_inventory(self): def named_inventory(self) -> Dict[str, Inventory]:
# type: () -> Dict[str, Inventory]
return self.env.intersphinx_named_inventory # type: ignore return self.env.intersphinx_named_inventory # type: ignore
def clear(self): def clear(self) -> None:
# type: () -> None
self.env.intersphinx_inventory.clear() # type: ignore self.env.intersphinx_inventory.clear() # type: ignore
self.env.intersphinx_named_inventory.clear() # type: ignore self.env.intersphinx_named_inventory.clear() # type: ignore
def _strip_basic_auth(url): def _strip_basic_auth(url: str) -> str:
# type: (str) -> str
"""Returns *url* with basic auth credentials removed. Also returns the """Returns *url* with basic auth credentials removed. Also returns the
basic auth username and password if they're present in *url*. basic auth username and password if they're present in *url*.
@ -105,8 +98,7 @@ def _strip_basic_auth(url):
return urlunsplit(frags) return urlunsplit(frags)
def _read_from_url(url, config=None): def _read_from_url(url: str, config: Config = None) -> IO:
# type: (str, Config) -> IO
"""Reads data from *url* with an HTTP *GET*. """Reads data from *url* with an HTTP *GET*.
This function supports fetching from resources which use basic HTTP auth as This function supports fetching from resources which use basic HTTP auth as
@ -131,8 +123,7 @@ def _read_from_url(url, config=None):
return r.raw return r.raw
def _get_safe_url(url): def _get_safe_url(url: str) -> str:
# type: (str) -> str
"""Gets version of *url* with basic auth passwords obscured. This function """Gets version of *url* with basic auth passwords obscured. This function
returns results suitable for printing and logging. returns results suitable for printing and logging.
@ -157,8 +148,7 @@ def _get_safe_url(url):
return urlunsplit(frags) return urlunsplit(frags)
def fetch_inventory(app, uri, inv): def fetch_inventory(app: Sphinx, uri: str, inv: Any) -> Any:
# type: (Sphinx, str, Any) -> Any
"""Fetch, parse and return an intersphinx inventory file.""" """Fetch, parse and return an intersphinx inventory file."""
# both *uri* (base URI of the links to generate) and *inv* (actual # both *uri* (base URI of the links to generate) and *inv* (actual
# location of the inventory file) can be local or remote URIs # location of the inventory file) can be local or remote URIs
@ -197,8 +187,7 @@ def fetch_inventory(app, uri, inv):
return invdata return invdata
def load_mappings(app): def load_mappings(app: Sphinx) -> None:
# type: (Sphinx) -> None
"""Load all intersphinx mappings into the environment.""" """Load all intersphinx mappings into the environment."""
now = int(time.time()) now = int(time.time())
cache_time = now - app.config.intersphinx_cache_limit * 86400 cache_time = now - app.config.intersphinx_cache_limit * 86400
@ -258,8 +247,8 @@ def load_mappings(app):
inventories.main_inventory.setdefault(type, {}).update(objects) inventories.main_inventory.setdefault(type, {}).update(objects)
def missing_reference(app, env, node, contnode): def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: TextElement
# type: (Sphinx, BuildEnvironment, nodes.Element, nodes.TextElement) -> nodes.reference ) -> nodes.reference:
"""Attempt to resolve a missing reference via intersphinx references.""" """Attempt to resolve a missing reference via intersphinx references."""
target = node['reftarget'] target = node['reftarget']
inventories = InventoryAdapter(env) inventories = InventoryAdapter(env)
@ -336,8 +325,7 @@ def missing_reference(app, env, node, contnode):
return None return None
def normalize_intersphinx_mapping(app, config): def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
# type: (Sphinx, Config) -> None
for key, value in config.intersphinx_mapping.copy().items(): for key, value in config.intersphinx_mapping.copy().items():
try: try:
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
@ -361,8 +349,7 @@ def normalize_intersphinx_mapping(app, config):
config.intersphinx_mapping.pop(key) config.intersphinx_mapping.pop(key)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_config_value('intersphinx_mapping', {}, True) app.add_config_value('intersphinx_mapping', {}, True)
app.add_config_value('intersphinx_cache_limit', 5, False) app.add_config_value('intersphinx_cache_limit', 5, False)
app.add_config_value('intersphinx_timeout', None, False) app.add_config_value('intersphinx_timeout', None, False)
@ -376,8 +363,7 @@ def setup(app):
} }
def inspect_main(argv): def inspect_main(argv: List[str]) -> None:
# type: (List[str]) -> None
"""Debug functionality to print out an inventory""" """Debug functionality to print out an inventory"""
if len(argv) < 1: if len(argv) < 1:
print("Print out an inventory file.\n" print("Print out an inventory file.\n"
@ -393,8 +379,7 @@ def inspect_main(argv):
srcdir = '' srcdir = ''
config = MockConfig() config = MockConfig()
def warn(self, msg): def warn(self, msg: str) -> None:
# type: (str) -> None
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
try: try:

View File

@ -10,6 +10,7 @@
""" """
import warnings import warnings
from typing import Any, Dict
from sphinxcontrib.jsmath import ( # NOQA from sphinxcontrib.jsmath import ( # NOQA
html_visit_math, html_visit_math,
@ -18,16 +19,11 @@ from sphinxcontrib.jsmath import ( # NOQA
) )
import sphinx import sphinx
from sphinx.application import Sphinx
from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx40Warning
if False:
# For type annotation
from typing import Any, Dict # NOQA
from sphinx.application import Sphinx # NOQA
def setup(app: Sphinx) -> Dict[str, Any]:
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
warnings.warn('sphinx.ext.jsmath has been moved to sphinxcontrib-jsmath.', warnings.warn('sphinx.ext.jsmath has been moved to sphinxcontrib-jsmath.',
RemovedInSphinx40Warning) RemovedInSphinx40Warning)

View File

@ -8,25 +8,23 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, Set
from docutils import nodes from docutils import nodes
from docutils.nodes import Node
import sphinx import sphinx
from sphinx import addnodes from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.errors import SphinxError from sphinx.errors import SphinxError
from sphinx.locale import _ from sphinx.locale import _
if False:
# For type annotation
from typing import Any, Dict, Set # NOQA
from sphinx.application import Sphinx # NOQA
class LinkcodeError(SphinxError): class LinkcodeError(SphinxError):
category = "linkcode error" category = "linkcode error"
def doctree_read(app, doctree): def doctree_read(app: Sphinx, doctree: Node) -> None:
# type: (Sphinx, nodes.Node) -> None
env = app.builder.env env = app.builder.env
resolve_target = getattr(env.config, 'linkcode_resolve', None) resolve_target = getattr(env.config, 'linkcode_resolve', None)
@ -75,8 +73,7 @@ def doctree_read(app, doctree):
signode += onlynode signode += onlynode
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.connect('doctree-read', doctree_read) app.connect('doctree-read', doctree_read)
app.add_config_value('linkcode_resolve', None, '') app.add_config_value('linkcode_resolve', None, '')
return {'version': sphinx.__display_version__, 'parallel_read_safe': True} return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@ -9,26 +9,24 @@
""" """
import warnings import warnings
from typing import Callable, List, Tuple
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node
from docutils.parsers.rst.roles import math_role as math_role_base from docutils.parsers.rst.roles import math_role as math_role_base
from sphinx.addnodes import math, math_block as displaymath # NOQA # to keep compatibility from sphinx.addnodes import math, math_block as displaymath # NOQA # to keep compatibility
from sphinx.application import Sphinx
from sphinx.builders.latex.nodes import math_reference as eqref # NOQA # to keep compatibility from sphinx.builders.latex.nodes import math_reference as eqref # NOQA # to keep compatibility
from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.directives.patches import MathDirective as MathDirectiveBase from sphinx.directives.patches import MathDirective as MathDirectiveBase
from sphinx.domains.math import MathDomain # NOQA # to keep compatibility from sphinx.domains.math import MathDomain # NOQA # to keep compatibility
from sphinx.domains.math import MathReferenceRole as EqXRefRole # NOQA # to keep compatibility from sphinx.domains.math import MathReferenceRole as EqXRefRole # NOQA # to keep compatibility
from sphinx.writers.html import HTMLTranslator
if False:
# For type annotation
from typing import Callable, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.writers.html import HTMLTranslator # NOQA
class MathDirective(MathDirectiveBase): class MathDirective(MathDirectiveBase):
def run(self): def run(self) -> List[Node]:
warnings.warn('sphinx.ext.mathbase.MathDirective is moved to ' warnings.warn('sphinx.ext.mathbase.MathDirective is moved to '
'sphinx.directives.patches package.', 'sphinx.directives.patches package.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
@ -42,8 +40,7 @@ def math_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
return math_role_base(role, rawtext, text, lineno, inliner, options, content) return math_role_base(role, rawtext, text, lineno, inliner, options, content)
def get_node_equation_number(writer, node): def get_node_equation_number(writer: HTMLTranslator, node: nodes.math_block) -> str:
# type: (HTMLTranslator, nodes.math_block) -> str
warnings.warn('sphinx.ext.mathbase.get_node_equation_number() is moved to ' warnings.warn('sphinx.ext.mathbase.get_node_equation_number() is moved to '
'sphinx.util.math package.', 'sphinx.util.math package.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
@ -51,8 +48,7 @@ def get_node_equation_number(writer, node):
return get_node_equation_number(writer, node) return get_node_equation_number(writer, node)
def wrap_displaymath(text, label, numbering): def wrap_displaymath(text: str, label: str, numbering: bool) -> str:
# type: (str, str, bool) -> str
warnings.warn('sphinx.ext.mathbase.wrap_displaymath() is moved to ' warnings.warn('sphinx.ext.mathbase.wrap_displaymath() is moved to '
'sphinx.util.math package.', 'sphinx.util.math package.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
@ -60,8 +56,7 @@ def wrap_displaymath(text, label, numbering):
return wrap_displaymath(text, label, numbering) return wrap_displaymath(text, label, numbering)
def is_in_section_title(node): def is_in_section_title(node: Element) -> bool:
# type: (nodes.Element) -> bool
"""Determine whether the node is in a section title""" """Determine whether the node is in a section title"""
from sphinx.util.nodes import traverse_parent from sphinx.util.nodes import traverse_parent
@ -75,8 +70,9 @@ def is_in_section_title(node):
return False return False
def setup_math(app, htmlinlinevisitors, htmldisplayvisitors): def setup_math(app: Sphinx,
# type: (Sphinx, Tuple[Callable, Callable], Tuple[Callable, Callable]) -> None htmlinlinevisitors: Tuple[Callable, Callable],
htmldisplayvisitors: Tuple[Callable, Callable]) -> None:
warnings.warn('setup_math() is deprecated. ' warnings.warn('setup_math() is deprecated. '
'Please use app.add_html_math_renderer() instead.', 'Please use app.add_html_math_renderer() instead.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)

View File

@ -11,27 +11,23 @@
""" """
import json import json
from typing import Any, Dict
from typing import cast from typing import cast
from docutils import nodes from docutils import nodes
import sphinx import sphinx
from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.domains.math import MathDomain from sphinx.domains.math import MathDomain
from sphinx.environment import BuildEnvironment
from sphinx.errors import ExtensionError from sphinx.errors import ExtensionError
from sphinx.locale import _ from sphinx.locale import _
from sphinx.util.math import get_node_equation_number from sphinx.util.math import get_node_equation_number
from sphinx.writers.html import HTMLTranslator
if False:
# For type annotation
from typing import Any, Dict # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.writers.html import HTMLTranslator # NOQA
def html_visit_math(self, node): def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
# type: (HTMLTranslator, nodes.math) -> None
self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight')) self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight'))
self.body.append(self.builder.config.mathjax_inline[0] + self.body.append(self.builder.config.mathjax_inline[0] +
self.encode(node.astext()) + self.encode(node.astext()) +
@ -39,8 +35,7 @@ def html_visit_math(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def html_visit_displaymath(self, node): def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None:
# type: (HTMLTranslator, nodes.math_block) -> None
self.body.append(self.starttag(node, 'div', CLASS='math notranslate nohighlight')) self.body.append(self.starttag(node, 'div', CLASS='math notranslate nohighlight'))
if node['nowrap']: if node['nowrap']:
self.body.append(self.encode(node.astext())) self.body.append(self.encode(node.astext()))
@ -72,8 +67,7 @@ def html_visit_displaymath(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def install_mathjax(app, env): def install_mathjax(app: Sphinx, env: BuildEnvironment) -> None:
# type: (Sphinx, BuildEnvironment) -> None
if app.builder.format != 'html' or app.builder.math_renderer_name != 'mathjax': # type: ignore # NOQA if app.builder.format != 'html' or app.builder.math_renderer_name != 'mathjax': # type: ignore # NOQA
return return
if not app.config.mathjax_path: if not app.config.mathjax_path:
@ -94,8 +88,7 @@ def install_mathjax(app, env):
builder.add_js_file(None, type="text/x-mathjax-config", body=body) builder.add_js_file(None, type="text/x-mathjax-config", body=body)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_html_math_renderer('mathjax', app.add_html_math_renderer('mathjax',
(html_visit_math, None), (html_visit_math, None),
(html_visit_displaymath, None)) (html_visit_displaymath, None))

View File

@ -8,14 +8,12 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from typing import Any, Dict, List
from sphinx import __display_version__ as __version__ from sphinx import __display_version__ as __version__
from sphinx.application import Sphinx from sphinx.application import Sphinx
from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring
if False:
# For type annotation
from typing import Any, Dict, List # NOQA
class Config: class Config:
"""Sphinx napoleon extension settings in `conf.py`. """Sphinx napoleon extension settings in `conf.py`.
@ -267,16 +265,14 @@ class Config:
'napoleon_custom_sections': (None, 'env') 'napoleon_custom_sections': (None, 'env')
} }
def __init__(self, **settings): def __init__(self, **settings) -> None:
# type: (Any) -> None
for name, (default, rebuild) in self._config_values.items(): for name, (default, rebuild) in self._config_values.items():
setattr(self, name, default) setattr(self, name, default)
for name, value in settings.items(): for name, value in settings.items():
setattr(self, name, value) setattr(self, name, value)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
"""Sphinx extension setup function. """Sphinx extension setup function.
When the extension is loaded, Sphinx imports this module and executes When the extension is loaded, Sphinx imports this module and executes
@ -313,8 +309,7 @@ def setup(app):
return {'version': __version__, 'parallel_read_safe': True} return {'version': __version__, 'parallel_read_safe': True}
def _patch_python_domain(): def _patch_python_domain() -> None:
# type: () -> None
try: try:
from sphinx.domains.python import PyTypedField from sphinx.domains.python import PyTypedField
except ImportError: except ImportError:
@ -333,8 +328,8 @@ def _patch_python_domain():
can_collapse=True)) can_collapse=True))
def _process_docstring(app, what, name, obj, options, lines): def _process_docstring(app: Sphinx, what: str, name: str, obj: Any,
# type: (Sphinx, str, str, Any, Any, List[str]) -> None options: Any, lines: List[str]) -> None:
"""Process the docstring for a given python object. """Process the docstring for a given python object.
Called when autodoc has read and processed a docstring. `lines` is a list Called when autodoc has read and processed a docstring. `lines` is a list
@ -383,8 +378,8 @@ def _process_docstring(app, what, name, obj, options, lines):
lines[:] = result_lines[:] lines[:] = result_lines[:]
def _skip_member(app, what, name, obj, skip, options): def _skip_member(app: Sphinx, what: str, name: str, obj: Any,
# type: (Sphinx, str, str, Any, bool, Any) -> bool skip: bool, options: Any) -> bool:
"""Determine if private and special class members are included in docs. """Determine if private and special class members are included in docs.
The following settings in conf.py determine if private and special class The following settings in conf.py determine if private and special class

View File

@ -13,16 +13,13 @@
import inspect import inspect
import re import re
from functools import partial from functools import partial
from typing import Any, Callable, Dict, List, Tuple, Type, Union
from sphinx.application import Sphinx
from sphinx.config import Config as SphinxConfig
from sphinx.ext.napoleon.iterators import modify_iter from sphinx.ext.napoleon.iterators import modify_iter
from sphinx.locale import _ from sphinx.locale import _
if False:
# For type annotation
from typing import Any, Callable, Dict, List, Tuple, Type, Union # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.config import Config as SphinxConfig # NOQA
_directive_regex = re.compile(r'\.\. \S+::') _directive_regex = re.compile(r'\.\. \S+::')
_google_section_regex = re.compile(r'^(\s|\w)+:\s*$') _google_section_regex = re.compile(r'^(\s|\w)+:\s*$')
@ -103,9 +100,9 @@ class GoogleDocstring:
_name_rgx = re.compile(r"^\s*((?::(?P<role>\S+):)?`(?P<name>[a-zA-Z0-9_.-]+)`|" _name_rgx = re.compile(r"^\s*((?::(?P<role>\S+):)?`(?P<name>[a-zA-Z0-9_.-]+)`|"
r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X) r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X)
def __init__(self, docstring, config=None, app=None, what='', name='', def __init__(self, docstring: Union[str, List[str]], config: SphinxConfig = None,
obj=None, options=None): app: Sphinx = None, what: str = '', name: str = '',
# type: (Union[str, List[str]], SphinxConfig, Sphinx, str, str, Any, Any) -> None obj: Any = None, options: Any = None) -> None:
self._config = config self._config = config
self._app = app self._app = app
@ -175,8 +172,7 @@ class GoogleDocstring:
self._parse() self._parse()
def __str__(self): def __str__(self) -> str:
# type: () -> str
"""Return the parsed docstring in reStructuredText format. """Return the parsed docstring in reStructuredText format.
Returns Returns
@ -187,8 +183,7 @@ class GoogleDocstring:
""" """
return '\n'.join(self.lines()) return '\n'.join(self.lines())
def lines(self): def lines(self) -> List[str]:
# type: () -> List[str]
"""Return the parsed lines of the docstring in reStructuredText format. """Return the parsed lines of the docstring in reStructuredText format.
Returns Returns
@ -199,8 +194,7 @@ class GoogleDocstring:
""" """
return self._parsed_lines return self._parsed_lines
def _consume_indented_block(self, indent=1): def _consume_indented_block(self, indent: int = 1) -> List[str]:
# type: (int) -> List[str]
lines = [] lines = []
line = self._line_iter.peek() line = self._line_iter.peek()
while(not self._is_section_break() and while(not self._is_section_break() and
@ -209,8 +203,7 @@ class GoogleDocstring:
line = self._line_iter.peek() line = self._line_iter.peek()
return lines return lines
def _consume_contiguous(self): def _consume_contiguous(self) -> List[str]:
# type: () -> List[str]
lines = [] lines = []
while (self._line_iter.has_next() and while (self._line_iter.has_next() and
self._line_iter.peek() and self._line_iter.peek() and
@ -218,8 +211,7 @@ class GoogleDocstring:
lines.append(next(self._line_iter)) lines.append(next(self._line_iter))
return lines return lines
def _consume_empty(self): def _consume_empty(self) -> List[str]:
# type: () -> List[str]
lines = [] lines = []
line = self._line_iter.peek() line = self._line_iter.peek()
while self._line_iter.has_next() and not line: while self._line_iter.has_next() and not line:
@ -227,8 +219,8 @@ class GoogleDocstring:
line = self._line_iter.peek() line = self._line_iter.peek()
return lines return lines
def _consume_field(self, parse_type=True, prefer_type=False): def _consume_field(self, parse_type: bool = True, prefer_type: bool = False
# type: (bool, bool) -> Tuple[str, str, List[str]] ) -> Tuple[str, str, List[str]]:
line = next(self._line_iter) line = next(self._line_iter)
before, colon, after = self._partition_field_on_colon(line) before, colon, after = self._partition_field_on_colon(line)
@ -249,8 +241,8 @@ class GoogleDocstring:
_descs = self.__class__(_descs, self._config).lines() _descs = self.__class__(_descs, self._config).lines()
return _name, _type, _descs return _name, _type, _descs
def _consume_fields(self, parse_type=True, prefer_type=False): def _consume_fields(self, parse_type: bool = True, prefer_type: bool = False
# type: (bool, bool) -> List[Tuple[str, str, List[str]]] ) -> List[Tuple[str, str, List[str]]]:
self._consume_empty() self._consume_empty()
fields = [] fields = []
while not self._is_section_break(): while not self._is_section_break():
@ -259,8 +251,7 @@ class GoogleDocstring:
fields.append((_name, _type, _desc,)) fields.append((_name, _type, _desc,))
return fields return fields
def _consume_inline_attribute(self): def _consume_inline_attribute(self) -> Tuple[str, List[str]]:
# type: () -> Tuple[str, List[str]]
line = next(self._line_iter) line = next(self._line_iter)
_type, colon, _desc = self._partition_field_on_colon(line) _type, colon, _desc = self._partition_field_on_colon(line)
if not colon or not _desc: if not colon or not _desc:
@ -270,8 +261,7 @@ class GoogleDocstring:
_descs = self.__class__(_descs, self._config).lines() _descs = self.__class__(_descs, self._config).lines()
return _type, _descs return _type, _descs
def _consume_returns_section(self): def _consume_returns_section(self) -> List[Tuple[str, str, List[str]]]:
# type: () -> List[Tuple[str, str, List[str]]]
lines = self._dedent(self._consume_to_next_section()) lines = self._dedent(self._consume_to_next_section())
if lines: if lines:
before, colon, after = self._partition_field_on_colon(lines[0]) before, colon, after = self._partition_field_on_colon(lines[0])
@ -290,44 +280,38 @@ class GoogleDocstring:
else: else:
return [] return []
def _consume_usage_section(self): def _consume_usage_section(self) -> List[str]:
# type: () -> List[str]
lines = self._dedent(self._consume_to_next_section()) lines = self._dedent(self._consume_to_next_section())
return lines return lines
def _consume_section_header(self): def _consume_section_header(self) -> str:
# type: () -> str
section = next(self._line_iter) section = next(self._line_iter)
stripped_section = section.strip(':') stripped_section = section.strip(':')
if stripped_section.lower() in self._sections: if stripped_section.lower() in self._sections:
section = stripped_section section = stripped_section
return section return section
def _consume_to_end(self): def _consume_to_end(self) -> List[str]:
# type: () -> List[str]
lines = [] lines = []
while self._line_iter.has_next(): while self._line_iter.has_next():
lines.append(next(self._line_iter)) lines.append(next(self._line_iter))
return lines return lines
def _consume_to_next_section(self): def _consume_to_next_section(self) -> List[str]:
# type: () -> List[str]
self._consume_empty() self._consume_empty()
lines = [] lines = []
while not self._is_section_break(): while not self._is_section_break():
lines.append(next(self._line_iter)) lines.append(next(self._line_iter))
return lines + self._consume_empty() return lines + self._consume_empty()
def _dedent(self, lines, full=False): def _dedent(self, lines: List[str], full: bool = False) -> List[str]:
# type: (List[str], bool) -> List[str]
if full: if full:
return [line.lstrip() for line in lines] return [line.lstrip() for line in lines]
else: else:
min_indent = self._get_min_indent(lines) min_indent = self._get_min_indent(lines)
return [line[min_indent:] for line in lines] return [line[min_indent:] for line in lines]
def _escape_args_and_kwargs(self, name): def _escape_args_and_kwargs(self, name: str) -> str:
# type: (str) -> str
if name.endswith('_'): if name.endswith('_'):
name = name[:-1] + r'\_' name = name[:-1] + r'\_'
@ -338,8 +322,7 @@ class GoogleDocstring:
else: else:
return name return name
def _fix_field_desc(self, desc): def _fix_field_desc(self, desc: List[str]) -> List[str]:
# type: (List[str]) -> List[str]
if self._is_list(desc): if self._is_list(desc):
desc = [''] + desc desc = [''] + desc
elif desc[0].endswith('::'): elif desc[0].endswith('::'):
@ -352,8 +335,7 @@ class GoogleDocstring:
desc = ['', desc[0]] + self._indent(desc_block, 4) desc = ['', desc[0]] + self._indent(desc_block, 4)
return desc return desc
def _format_admonition(self, admonition, lines): def _format_admonition(self, admonition: str, lines: List[str]) -> List[str]:
# type: (str, List[str]) -> List[str]
lines = self._strip_empty(lines) lines = self._strip_empty(lines)
if len(lines) == 1: if len(lines) == 1:
return ['.. %s:: %s' % (admonition, lines[0].strip()), ''] return ['.. %s:: %s' % (admonition, lines[0].strip()), '']
@ -363,8 +345,7 @@ class GoogleDocstring:
else: else:
return ['.. %s::' % admonition, ''] return ['.. %s::' % admonition, '']
def _format_block(self, prefix, lines, padding=None): def _format_block(self, prefix: str, lines: List[str], padding: str = None) -> List[str]:
# type: (str, List[str], str) -> List[str]
if lines: if lines:
if padding is None: if padding is None:
padding = ' ' * len(prefix) padding = ' ' * len(prefix)
@ -380,9 +361,9 @@ class GoogleDocstring:
else: else:
return [prefix] return [prefix]
def _format_docutils_params(self, fields, field_role='param', def _format_docutils_params(self, fields: List[Tuple[str, str, List[str]]],
type_role='type'): field_role: str = 'param', type_role: str = 'type'
# type: (List[Tuple[str, str, List[str]]], str, str) -> List[str] ) -> List[str]:
lines = [] lines = []
for _name, _type, _desc in fields: for _name, _type, _desc in fields:
_desc = self._strip_empty(_desc) _desc = self._strip_empty(_desc)
@ -397,8 +378,7 @@ class GoogleDocstring:
lines.append(':%s %s: %s' % (type_role, _name, _type)) lines.append(':%s %s: %s' % (type_role, _name, _type))
return lines + [''] return lines + ['']
def _format_field(self, _name, _type, _desc): def _format_field(self, _name: str, _type: str, _desc: List[str]) -> List[str]:
# type: (str, str, List[str]) -> List[str]
_desc = self._strip_empty(_desc) _desc = self._strip_empty(_desc)
has_desc = any(_desc) has_desc = any(_desc)
separator = has_desc and ' -- ' or '' separator = has_desc and ' -- ' or ''
@ -427,8 +407,8 @@ class GoogleDocstring:
else: else:
return [field] return [field]
def _format_fields(self, field_type, fields): def _format_fields(self, field_type: str, fields: List[Tuple[str, str, List[str]]]
# type: (str, List[Tuple[str, str, List[str]]]) -> List[str] ) -> List[str]:
field_type = ':%s:' % field_type.strip() field_type = ':%s:' % field_type.strip()
padding = ' ' * len(field_type) padding = ' ' * len(field_type)
multi = len(fields) > 1 multi = len(fields) > 1
@ -446,8 +426,7 @@ class GoogleDocstring:
lines.append('') lines.append('')
return lines return lines
def _get_current_indent(self, peek_ahead=0): def _get_current_indent(self, peek_ahead: int = 0) -> int:
# type: (int) -> int
line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] line = self._line_iter.peek(peek_ahead + 1)[peek_ahead]
while line != self._line_iter.sentinel: while line != self._line_iter.sentinel:
if line: if line:
@ -456,22 +435,19 @@ class GoogleDocstring:
line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] line = self._line_iter.peek(peek_ahead + 1)[peek_ahead]
return 0 return 0
def _get_indent(self, line): def _get_indent(self, line: str) -> int:
# type: (str) -> int
for i, s in enumerate(line): for i, s in enumerate(line):
if not s.isspace(): if not s.isspace():
return i return i
return len(line) return len(line)
def _get_initial_indent(self, lines): def _get_initial_indent(self, lines: List[str]) -> int:
# type: (List[str]) -> int
for line in lines: for line in lines:
if line: if line:
return self._get_indent(line) return self._get_indent(line)
return 0 return 0
def _get_min_indent(self, lines): def _get_min_indent(self, lines: List[str]) -> int:
# type: (List[str]) -> int
min_indent = None min_indent = None
for line in lines: for line in lines:
if line: if line:
@ -482,12 +458,10 @@ class GoogleDocstring:
min_indent = indent min_indent = indent
return min_indent or 0 return min_indent or 0
def _indent(self, lines, n=4): def _indent(self, lines: List[str], n: int = 4) -> List[str]:
# type: (List[str], int) -> List[str]
return [(' ' * n) + line for line in lines] return [(' ' * n) + line for line in lines]
def _is_indented(self, line, indent=1): def _is_indented(self, line: str, indent: int = 1) -> bool:
# type: (str, int) -> bool
for i, s in enumerate(line): for i, s in enumerate(line):
if i >= indent: if i >= indent:
return True return True
@ -495,8 +469,7 @@ class GoogleDocstring:
return False return False
return False return False
def _is_list(self, lines): def _is_list(self, lines: List[str]) -> bool:
# type: (List[str]) -> bool
if not lines: if not lines:
return False return False
if _bullet_list_regex.match(lines[0]): if _bullet_list_regex.match(lines[0]):
@ -513,8 +486,7 @@ class GoogleDocstring:
break break
return next_indent > indent return next_indent > indent
def _is_section_header(self): def _is_section_header(self) -> bool:
# type: () -> bool
section = self._line_iter.peek().lower() section = self._line_iter.peek().lower()
match = _google_section_regex.match(section) match = _google_section_regex.match(section)
if match and section.strip(':') in self._sections: if match and section.strip(':') in self._sections:
@ -528,8 +500,7 @@ class GoogleDocstring:
return True return True
return False return False
def _is_section_break(self): def _is_section_break(self) -> bool:
# type: () -> bool
line = self._line_iter.peek() line = self._line_iter.peek()
return (not self._line_iter.has_next() or return (not self._line_iter.has_next() or
self._is_section_header() or self._is_section_header() or
@ -537,9 +508,7 @@ class GoogleDocstring:
line and line and
not self._is_indented(line, self._section_indent))) not self._is_indented(line, self._section_indent)))
def _load_custom_sections(self): def _load_custom_sections(self) -> None:
# type: () -> None
if self._config.napoleon_custom_sections is not None: if self._config.napoleon_custom_sections is not None:
for entry in self._config.napoleon_custom_sections: for entry in self._config.napoleon_custom_sections:
if isinstance(entry, str): if isinstance(entry, str):
@ -554,8 +523,7 @@ class GoogleDocstring:
self._sections.get(entry[1].lower(), self._sections.get(entry[1].lower(),
self._parse_custom_generic_section) self._parse_custom_generic_section)
def _parse(self): def _parse(self) -> None:
# type: () -> None
self._parsed_lines = self._consume_empty() self._parsed_lines = self._consume_empty()
if self._name and self._what in ('attribute', 'data', 'property'): if self._name and self._what in ('attribute', 'data', 'property'):
@ -594,16 +562,14 @@ class GoogleDocstring:
lines = self._consume_to_next_section() lines = self._consume_to_next_section()
return self._format_admonition(admonition, lines) return self._format_admonition(admonition, lines)
def _parse_attribute_docstring(self): def _parse_attribute_docstring(self) -> List[str]:
# type: () -> List[str]
_type, _desc = self._consume_inline_attribute() _type, _desc = self._consume_inline_attribute()
lines = self._format_field('', '', _desc) lines = self._format_field('', '', _desc)
if _type: if _type:
lines.extend(['', ':type: %s' % _type]) lines.extend(['', ':type: %s' % _type])
return lines return lines
def _parse_attributes_section(self, section): def _parse_attributes_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
lines = [] lines = []
for _name, _type, _desc in self._consume_fields(): for _name, _type, _desc in self._consume_fields():
if self._config.napoleon_use_ivar: if self._config.napoleon_use_ivar:
@ -624,8 +590,7 @@ class GoogleDocstring:
lines.append('') lines.append('')
return lines return lines
def _parse_examples_section(self, section): def _parse_examples_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
labels = { labels = {
'example': _('Example'), 'example': _('Example'),
'examples': _('Examples'), 'examples': _('Examples'),
@ -638,16 +603,14 @@ class GoogleDocstring:
# for now, no admonition for simple custom sections # for now, no admonition for simple custom sections
return self._parse_generic_section(section, False) return self._parse_generic_section(section, False)
def _parse_usage_section(self, section): def _parse_usage_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
header = ['.. rubric:: Usage:', ''] header = ['.. rubric:: Usage:', '']
block = ['.. code-block:: python', ''] block = ['.. code-block:: python', '']
lines = self._consume_usage_section() lines = self._consume_usage_section()
lines = self._indent(lines, 3) lines = self._indent(lines, 3)
return header + block + lines + [''] return header + block + lines + ['']
def _parse_generic_section(self, section, use_admonition): def _parse_generic_section(self, section: str, use_admonition: bool) -> List[str]:
# type: (str, bool) -> List[str]
lines = self._strip_empty(self._consume_to_next_section()) lines = self._strip_empty(self._consume_to_next_section())
lines = self._dedent(lines) lines = self._dedent(lines)
if use_admonition: if use_admonition:
@ -660,8 +623,7 @@ class GoogleDocstring:
else: else:
return [header, ''] return [header, '']
def _parse_keyword_arguments_section(self, section): def _parse_keyword_arguments_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
fields = self._consume_fields() fields = self._consume_fields()
if self._config.napoleon_use_keyword: if self._config.napoleon_use_keyword:
return self._format_docutils_params( return self._format_docutils_params(
@ -671,8 +633,7 @@ class GoogleDocstring:
else: else:
return self._format_fields(_('Keyword Arguments'), fields) return self._format_fields(_('Keyword Arguments'), fields)
def _parse_methods_section(self, section): def _parse_methods_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
lines = [] # type: List[str] lines = [] # type: List[str]
for _name, _type, _desc in self._consume_fields(parse_type=False): for _name, _type, _desc in self._consume_fields(parse_type=False):
lines.append('.. method:: %s' % _name) lines.append('.. method:: %s' % _name)
@ -681,25 +642,21 @@ class GoogleDocstring:
lines.append('') lines.append('')
return lines return lines
def _parse_notes_section(self, section): def _parse_notes_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
use_admonition = self._config.napoleon_use_admonition_for_notes use_admonition = self._config.napoleon_use_admonition_for_notes
return self._parse_generic_section(_('Notes'), use_admonition) return self._parse_generic_section(_('Notes'), use_admonition)
def _parse_other_parameters_section(self, section): def _parse_other_parameters_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
return self._format_fields(_('Other Parameters'), self._consume_fields()) return self._format_fields(_('Other Parameters'), self._consume_fields())
def _parse_parameters_section(self, section): def _parse_parameters_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
fields = self._consume_fields() fields = self._consume_fields()
if self._config.napoleon_use_param: if self._config.napoleon_use_param:
return self._format_docutils_params(fields) return self._format_docutils_params(fields)
else: else:
return self._format_fields(_('Parameters'), fields) return self._format_fields(_('Parameters'), fields)
def _parse_raises_section(self, section): def _parse_raises_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
fields = self._consume_fields(parse_type=False, prefer_type=True) fields = self._consume_fields(parse_type=False, prefer_type=True)
lines = [] # type: List[str] lines = [] # type: List[str]
for _name, _type, _desc in fields: for _name, _type, _desc in fields:
@ -714,13 +671,11 @@ class GoogleDocstring:
lines.append('') lines.append('')
return lines return lines
def _parse_references_section(self, section): def _parse_references_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
use_admonition = self._config.napoleon_use_admonition_for_references use_admonition = self._config.napoleon_use_admonition_for_references
return self._parse_generic_section(_('References'), use_admonition) return self._parse_generic_section(_('References'), use_admonition)
def _parse_returns_section(self, section): def _parse_returns_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
fields = self._consume_returns_section() fields = self._consume_returns_section()
multi = len(fields) > 1 multi = len(fields) > 1
if multi: if multi:
@ -748,21 +703,17 @@ class GoogleDocstring:
lines.append('') lines.append('')
return lines return lines
def _parse_see_also_section(self, section): def _parse_see_also_section(self, section: str) -> List[str]:
# type (str) -> List[str]
return self._parse_admonition('seealso', section) return self._parse_admonition('seealso', section)
def _parse_warns_section(self, section): def _parse_warns_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
return self._format_fields(_('Warns'), self._consume_fields()) return self._format_fields(_('Warns'), self._consume_fields())
def _parse_yields_section(self, section): def _parse_yields_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
fields = self._consume_returns_section() fields = self._consume_returns_section()
return self._format_fields(_('Yields'), fields) return self._format_fields(_('Yields'), fields)
def _partition_field_on_colon(self, line): def _partition_field_on_colon(self, line: str) -> Tuple[str, str, str]:
# type: (str) -> Tuple[str, str, str]
before_colon = [] before_colon = []
after_colon = [] after_colon = []
colon = '' colon = ''
@ -784,8 +735,7 @@ class GoogleDocstring:
colon, colon,
"".join(after_colon).strip()) "".join(after_colon).strip())
def _qualify_name(self, attr_name, klass): def _qualify_name(self, attr_name: str, klass: Type) -> str:
# type: (str, Type) -> str
if klass and '.' not in attr_name: if klass and '.' not in attr_name:
if attr_name.startswith('~'): if attr_name.startswith('~'):
attr_name = attr_name[1:] attr_name = attr_name[1:]
@ -796,8 +746,7 @@ class GoogleDocstring:
return '~%s.%s' % (q, attr_name) return '~%s.%s' % (q, attr_name)
return attr_name return attr_name
def _strip_empty(self, lines): def _strip_empty(self, lines: List[str]) -> List[str]:
# type: (List[str]) -> List[str]
if lines: if lines:
start = -1 start = -1
for i, line in enumerate(lines): for i, line in enumerate(lines):
@ -910,14 +859,14 @@ class NumpyDocstring(GoogleDocstring):
The lines of the docstring in a list. The lines of the docstring in a list.
""" """
def __init__(self, docstring, config=None, app=None, what='', name='', def __init__(self, docstring: Union[str, List[str]], config: SphinxConfig = None,
obj=None, options=None): app: Sphinx = None, what: str = '', name: str = '',
# type: (Union[str, List[str]], SphinxConfig, Sphinx, str, str, Any, Any) -> None obj: Any = None, options: Any = None) -> None:
self._directive_sections = ['.. index::'] self._directive_sections = ['.. index::']
super().__init__(docstring, config, app, what, name, obj, options) super().__init__(docstring, config, app, what, name, obj, options)
def _consume_field(self, parse_type=True, prefer_type=False): def _consume_field(self, parse_type: bool = True, prefer_type: bool = False
# type: (bool, bool) -> Tuple[str, str, List[str]] ) -> Tuple[str, str, List[str]]:
line = next(self._line_iter) line = next(self._line_iter)
if parse_type: if parse_type:
_name, _, _type = self._partition_field_on_colon(line) _name, _, _type = self._partition_field_on_colon(line)
@ -933,20 +882,17 @@ class NumpyDocstring(GoogleDocstring):
_desc = self.__class__(_desc, self._config).lines() _desc = self.__class__(_desc, self._config).lines()
return _name, _type, _desc return _name, _type, _desc
def _consume_returns_section(self): def _consume_returns_section(self) -> List[Tuple[str, str, List[str]]]:
# type: () -> List[Tuple[str, str, List[str]]]
return self._consume_fields(prefer_type=True) return self._consume_fields(prefer_type=True)
def _consume_section_header(self): def _consume_section_header(self) -> str:
# type: () -> str
section = next(self._line_iter) section = next(self._line_iter)
if not _directive_regex.match(section): if not _directive_regex.match(section):
# Consume the header underline # Consume the header underline
next(self._line_iter) next(self._line_iter)
return section return section
def _is_section_break(self): def _is_section_break(self) -> bool:
# type: () -> bool
line1, line2 = self._line_iter.peek(2) line1, line2 = self._line_iter.peek(2)
return (not self._line_iter.has_next() or return (not self._line_iter.has_next() or
self._is_section_header() or self._is_section_header() or
@ -955,8 +901,7 @@ class NumpyDocstring(GoogleDocstring):
line1 and line1 and
not self._is_indented(line1, self._section_indent))) not self._is_indented(line1, self._section_indent)))
def _is_section_header(self): def _is_section_header(self) -> bool:
# type: () -> bool
section, underline = self._line_iter.peek(2) section, underline = self._line_iter.peek(2)
section = section.lower() section = section.lower()
if section in self._sections and isinstance(underline, str): if section in self._sections and isinstance(underline, str):
@ -968,16 +913,14 @@ class NumpyDocstring(GoogleDocstring):
return True return True
return False return False
def _parse_see_also_section(self, section): def _parse_see_also_section(self, section: str) -> List[str]:
# type: (str) -> List[str]
lines = self._consume_to_next_section() lines = self._consume_to_next_section()
try: try:
return self._parse_numpydoc_see_also_section(lines) return self._parse_numpydoc_see_also_section(lines)
except ValueError: except ValueError:
return self._format_admonition('seealso', lines) return self._format_admonition('seealso', lines)
def _parse_numpydoc_see_also_section(self, content): def _parse_numpydoc_see_also_section(self, content: List[str]) -> List[str]:
# type: (List[str]) -> List[str]
""" """
Derived from the NumpyDoc implementation of _parse_see_also. Derived from the NumpyDoc implementation of _parse_see_also.
@ -991,8 +934,7 @@ class NumpyDocstring(GoogleDocstring):
""" """
items = [] items = []
def parse_item_name(text): def parse_item_name(text: str) -> Tuple[str, str]:
# type: (str) -> Tuple[str, str]
"""Match ':role:`name`' or 'name'""" """Match ':role:`name`' or 'name'"""
m = self._name_rgx.match(text) m = self._name_rgx.match(text)
if m: if m:
@ -1003,8 +945,7 @@ class NumpyDocstring(GoogleDocstring):
return g[2], g[1] return g[2], g[1]
raise ValueError("%s is not a item name" % text) raise ValueError("%s is not a item name" % text)
def push_item(name, rest): def push_item(name: str, rest: List[str]) -> None:
# type: (str, List[str]) -> None
if not name: if not name:
return return
name, role = parse_item_name(name) name, role = parse_item_name(name)

View File

@ -11,10 +11,7 @@
""" """
import collections import collections
from typing import Any, Iterable
if False:
# For type annotation
from typing import Any, Iterable # NOQA
class peek_iter: class peek_iter:
@ -50,8 +47,7 @@ class peek_iter:
be set to a new object instance: ``object()``. be set to a new object instance: ``object()``.
""" """
def __init__(self, *args): def __init__(self, *args) -> None:
# type: (Any) -> None
"""__init__(o, sentinel=None)""" """__init__(o, sentinel=None)"""
self._iterable = iter(*args) # type: Iterable self._iterable = iter(*args) # type: Iterable
self._cache = collections.deque() # type: collections.deque self._cache = collections.deque() # type: collections.deque
@ -60,18 +56,15 @@ class peek_iter:
else: else:
self.sentinel = object() self.sentinel = object()
def __iter__(self): def __iter__(self) -> "peek_iter":
# type: () -> peek_iter
return self return self
def __next__(self, n=None): def __next__(self, n: int = None) -> Any:
# type: (int) -> Any
# note: prevent 2to3 to transform self.next() in next(self) which # note: prevent 2to3 to transform self.next() in next(self) which
# causes an infinite loop ! # causes an infinite loop !
return getattr(self, 'next')(n) return getattr(self, 'next')(n)
def _fillcache(self, n): def _fillcache(self, n: int) -> None:
# type: (int) -> None
"""Cache `n` items. If `n` is 0 or None, then 1 item is cached.""" """Cache `n` items. If `n` is 0 or None, then 1 item is cached."""
if not n: if not n:
n = 1 n = 1
@ -82,8 +75,7 @@ class peek_iter:
while len(self._cache) < n: while len(self._cache) < n:
self._cache.append(self.sentinel) self._cache.append(self.sentinel)
def has_next(self): def has_next(self) -> bool:
# type: () -> bool
"""Determine if iterator is exhausted. """Determine if iterator is exhausted.
Returns Returns
@ -98,8 +90,7 @@ class peek_iter:
""" """
return self.peek() != self.sentinel return self.peek() != self.sentinel
def next(self, n=None): def next(self, n: int = None) -> Any:
# type: (int) -> Any
"""Get the next item or `n` items of the iterator. """Get the next item or `n` items of the iterator.
Parameters Parameters
@ -134,8 +125,7 @@ class peek_iter:
result = [self._cache.popleft() for i in range(n)] result = [self._cache.popleft() for i in range(n)]
return result return result
def peek(self, n=None): def peek(self, n: int = None) -> Any:
# type: (int) -> Any
"""Preview the next item or `n` items of the iterator. """Preview the next item or `n` items of the iterator.
The iterator is not advanced when peek is called. The iterator is not advanced when peek is called.
@ -218,8 +208,7 @@ class modify_iter(peek_iter):
"whitespace." "whitespace."
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
# type: (Any, Any) -> None
"""__init__(o, sentinel=None, modifier=lambda x: x)""" """__init__(o, sentinel=None, modifier=lambda x: x)"""
if 'modifier' in kwargs: if 'modifier' in kwargs:
self.modifier = kwargs['modifier'] self.modifier = kwargs['modifier']
@ -233,8 +222,7 @@ class modify_iter(peek_iter):
'modifier must be callable') 'modifier must be callable')
super().__init__(*args) super().__init__(*args)
def _fillcache(self, n): def _fillcache(self, n: int) -> None:
# type: (int) -> None
"""Cache `n` modified items. If `n` is 0 or None, 1 item is cached. """Cache `n` modified items. If `n` is 0 or None, 1 item is cached.
Each item returned by the iterator is passed through the Each item returned by the iterator is passed through the

View File

@ -12,29 +12,27 @@
""" """
import warnings import warnings
from typing import Any, Dict, Iterable, List, Tuple
from typing import cast from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
from docutils.parsers.rst.directives.admonitions import BaseAdmonition from docutils.parsers.rst.directives.admonitions import BaseAdmonition
import sphinx import sphinx
from sphinx.application import Sphinx
from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.domains import Domain from sphinx.domains import Domain
from sphinx.environment import BuildEnvironment
from sphinx.errors import NoUri from sphinx.errors import NoUri
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
from sphinx.util.texescape import tex_escape_map from sphinx.util.texescape import tex_escape_map
from sphinx.writers.html import HTMLTranslator
if False: from sphinx.writers.latex import LaTeXTranslator
# For type annotation
from typing import Any, Dict, Iterable, List, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.writers.html import HTMLTranslator # NOQA
from sphinx.writers.latex import LaTeXTranslator # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -62,12 +60,11 @@ class Todo(BaseAdmonition, SphinxDirective):
'name': directives.unchanged, 'name': directives.unchanged,
} }
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
if not self.options.get('class'): if not self.options.get('class'):
self.options['class'] = ['admonition-todo'] self.options['class'] = ['admonition-todo']
(todo,) = super().run() # type: Tuple[nodes.Node] (todo,) = super().run() # type: Tuple[Node]
if isinstance(todo, nodes.system_message): if isinstance(todo, nodes.system_message):
return [todo] return [todo]
elif isinstance(todo, todo_node): elif isinstance(todo, todo_node):
@ -86,21 +83,18 @@ class TodoDomain(Domain):
label = 'todo' label = 'todo'
@property @property
def todos(self): def todos(self) -> Dict[str, List[todo_node]]:
# type: () -> Dict[str, List[todo_node]]
return self.data.setdefault('todos', {}) return self.data.setdefault('todos', {})
def clear_doc(self, docname): def clear_doc(self, docname: str) -> None:
# type: (str) -> None
self.todos.pop(docname, None) self.todos.pop(docname, None)
def merge_domaindata(self, docnames, otherdata): def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# type: (List[str], Dict) -> None
for docname in docnames: for docname in docnames:
self.todos[docname] = otherdata['todos'][docname] self.todos[docname] = otherdata['todos'][docname]
def process_doc(self, env, docname, document): def process_doc(self, env: BuildEnvironment, docname: str,
# type: (BuildEnvironment, str, nodes.document) -> None document: nodes.document) -> None:
todos = self.todos.setdefault(docname, []) todos = self.todos.setdefault(docname, [])
for todo in document.traverse(todo_node): for todo in document.traverse(todo_node):
env.app.emit('todo-defined', todo) env.app.emit('todo-defined', todo)
@ -111,8 +105,7 @@ class TodoDomain(Domain):
location=todo) location=todo)
def process_todos(app, doctree): def process_todos(app: Sphinx, doctree: nodes.document) -> None:
# type: (Sphinx, nodes.document) -> None
warnings.warn('process_todos() is deprecated.', RemovedInSphinx40Warning) warnings.warn('process_todos() is deprecated.', RemovedInSphinx40Warning)
# collect all todos in the environment # collect all todos in the environment
# this is not done in the directive itself because it some transformations # this is not done in the directive itself because it some transformations
@ -150,16 +143,14 @@ class TodoList(SphinxDirective):
final_argument_whitespace = False final_argument_whitespace = False
option_spec = {} # type: Dict option_spec = {} # type: Dict
def run(self): def run(self) -> List[Node]:
# type: () -> List[nodes.Node]
# Simply insert an empty todolist node which will be replaced later # Simply insert an empty todolist node which will be replaced later
# when process_todo_nodes is called # when process_todo_nodes is called
return [todolist('')] return [todolist('')]
class TodoListProcessor: class TodoListProcessor:
def __init__(self, app, doctree, docname): def __init__(self, app: Sphinx, doctree: nodes.document, docname: str) -> None:
# type: (Sphinx, nodes.document, str) -> None
self.builder = app.builder self.builder = app.builder
self.config = app.config self.config = app.config
self.env = app.env self.env = app.env
@ -167,8 +158,7 @@ class TodoListProcessor:
self.process(doctree, docname) self.process(doctree, docname)
def process(self, doctree, docname): def process(self, doctree: nodes.document, docname: str) -> None:
# type: (nodes.document, str) -> None
todos = sum(self.domain.todos.values(), []) todos = sum(self.domain.todos.values(), [])
for node in doctree.traverse(todolist): for node in doctree.traverse(todolist):
if not self.config.todo_include_todos: if not self.config.todo_include_todos:
@ -176,7 +166,7 @@ class TodoListProcessor:
continue continue
if node.get('ids'): if node.get('ids'):
content = [nodes.target()] # type: List[nodes.Element] content = [nodes.target()] # type: List[Element]
else: else:
content = [] content = []
@ -194,8 +184,7 @@ class TodoListProcessor:
node.replace_self(content) node.replace_self(content)
def create_todo_reference(self, todo, docname): def create_todo_reference(self, todo: todo_node, docname: str) -> nodes.paragraph:
# type: (todo_node, str) -> nodes.paragraph
if self.config.todo_link_only: if self.config.todo_link_only:
description = _('<<original entry>>') description = _('<<original entry>>')
else: else:
@ -224,8 +213,7 @@ class TodoListProcessor:
return para return para
def process_todo_nodes(app, doctree, fromdocname): def process_todo_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str) -> None:
# type: (Sphinx, nodes.document, str) -> None
"""Replace all todolist nodes with a list of the collected todos. """Replace all todolist nodes with a list of the collected todos.
Augment each todo with a backlink to the original location. Augment each todo with a backlink to the original location.
""" """
@ -236,7 +224,7 @@ def process_todo_nodes(app, doctree, fromdocname):
for node in doctree.traverse(todolist): for node in doctree.traverse(todolist):
if node.get('ids'): if node.get('ids'):
content = [nodes.target()] # type: List[nodes.Element] content = [nodes.target()] # type: List[Element]
else: else:
content = [] content = []
@ -280,8 +268,7 @@ def process_todo_nodes(app, doctree, fromdocname):
node.replace_self(content) node.replace_self(content)
def purge_todos(app, env, docname): def purge_todos(app: Sphinx, env: BuildEnvironment, docname: str) -> None:
# type: (Sphinx, BuildEnvironment, str) -> None
warnings.warn('purge_todos() is deprecated.', RemovedInSphinx40Warning) warnings.warn('purge_todos() is deprecated.', RemovedInSphinx40Warning)
if not hasattr(env, 'todo_all_todos'): if not hasattr(env, 'todo_all_todos'):
return return
@ -289,8 +276,8 @@ def purge_todos(app, env, docname):
if todo['docname'] != docname] if todo['docname'] != docname]
def merge_info(app, env, docnames, other): def merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],
# type: (Sphinx, BuildEnvironment, Iterable[str], BuildEnvironment) -> None other: BuildEnvironment) -> None:
warnings.warn('merge_info() is deprecated.', RemovedInSphinx40Warning) warnings.warn('merge_info() is deprecated.', RemovedInSphinx40Warning)
if not hasattr(other, 'todo_all_todos'): if not hasattr(other, 'todo_all_todos'):
return return
@ -299,21 +286,18 @@ def merge_info(app, env, docnames, other):
env.todo_all_todos.extend(other.todo_all_todos) # type: ignore env.todo_all_todos.extend(other.todo_all_todos) # type: ignore
def visit_todo_node(self, node): def visit_todo_node(self: HTMLTranslator, node: todo_node) -> None:
# type: (HTMLTranslator, todo_node) -> None
if self.config.todo_include_todos: if self.config.todo_include_todos:
self.visit_admonition(node) self.visit_admonition(node)
else: else:
raise nodes.SkipNode raise nodes.SkipNode
def depart_todo_node(self, node): def depart_todo_node(self: HTMLTranslator, node: todo_node) -> None:
# type: (HTMLTranslator, todo_node) -> None
self.depart_admonition(node) self.depart_admonition(node)
def latex_visit_todo_node(self, node): def latex_visit_todo_node(self: LaTeXTranslator, node: todo_node) -> None:
# type: (LaTeXTranslator, todo_node) -> None
if self.config.todo_include_todos: if self.config.todo_include_todos:
self.body.append('\n\\begin{sphinxadmonition}{note}{') self.body.append('\n\\begin{sphinxadmonition}{note}{')
self.body.append(self.hypertarget_to(node)) self.body.append(self.hypertarget_to(node))
@ -324,13 +308,11 @@ def latex_visit_todo_node(self, node):
raise nodes.SkipNode raise nodes.SkipNode
def latex_depart_todo_node(self, node): def latex_depart_todo_node(self: LaTeXTranslator, node: todo_node) -> None:
# type: (LaTeXTranslator, todo_node) -> None
self.body.append('\\end{sphinxadmonition}\n') self.body.append('\\end{sphinxadmonition}\n')
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_event('todo-defined') app.add_event('todo-defined')
app.add_config_value('todo_include_todos', False, 'html') app.add_config_value('todo_include_todos', False, 'html')
app.add_config_value('todo_link_only', False, 'html') app.add_config_value('todo_link_only', False, 'html')

View File

@ -10,29 +10,27 @@
import traceback import traceback
import warnings import warnings
from typing import Any, Dict, Iterable, Iterator, Set, Tuple
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node
import sphinx import sphinx
from sphinx import addnodes from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer from sphinx.pycode import ModuleAnalyzer
from sphinx.util import get_full_modname, logging, status_iterator from sphinx.util import get_full_modname, logging, status_iterator
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
if False:
# For type annotation
from typing import Any, Dict, Iterable, Iterator, Set, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.config import Config # NOQA
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _get_full_modname(app, modname, attribute): def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> str:
# type: (Sphinx, str, str) -> str
try: try:
return get_full_modname(modname, attribute) return get_full_modname(modname, attribute)
except AttributeError: except AttributeError:
@ -50,8 +48,7 @@ def _get_full_modname(app, modname, attribute):
return None return None
def doctree_read(app, doctree): def doctree_read(app: Sphinx, doctree: Node) -> None:
# type: (Sphinx, nodes.Node) -> None
env = app.builder.env env = app.builder.env
if not hasattr(env, '_viewcode_modules'): if not hasattr(env, '_viewcode_modules'):
env._viewcode_modules = {} # type: ignore env._viewcode_modules = {} # type: ignore
@ -122,8 +119,8 @@ def doctree_read(app, doctree):
signode += onlynode signode += onlynode
def env_merge_info(app, env, docnames, other): def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],
# type: (Sphinx, BuildEnvironment, Iterable[str], BuildEnvironment) -> None other: BuildEnvironment) -> None:
if not hasattr(other, '_viewcode_modules'): if not hasattr(other, '_viewcode_modules'):
return return
# create a _viewcode_modules dict on the main environment # create a _viewcode_modules dict on the main environment
@ -133,8 +130,8 @@ def env_merge_info(app, env, docnames, other):
env._viewcode_modules.update(other._viewcode_modules) # type: ignore env._viewcode_modules.update(other._viewcode_modules) # type: ignore
def missing_reference(app, env, node, contnode): def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node
# type: (Sphinx, BuildEnvironment, nodes.Element, nodes.Node) -> nodes.Node ) -> Node:
# resolve our "viewcode" reference nodes -- they need special treatment # resolve our "viewcode" reference nodes -- they need special treatment
if node['reftype'] == 'viewcode': if node['reftype'] == 'viewcode':
return make_refnode(app.builder, node['refdoc'], node['reftarget'], return make_refnode(app.builder, node['refdoc'], node['reftarget'],
@ -143,8 +140,7 @@ def missing_reference(app, env, node, contnode):
return None return None
def collect_pages(app): def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]:
# type: (Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]
env = app.builder.env env = app.builder.env
if not hasattr(env, '_viewcode_modules'): if not hasattr(env, '_viewcode_modules'):
return return
@ -238,16 +234,14 @@ def collect_pages(app):
yield ('_modules/index', context, 'page.html') yield ('_modules/index', context, 'page.html')
def migrate_viewcode_import(app, config): def migrate_viewcode_import(app: Sphinx, config: Config) -> None:
# type: (Sphinx, Config) -> None
if config.viewcode_import is not None: if config.viewcode_import is not None:
warnings.warn('viewcode_import was renamed to viewcode_follow_imported_members. ' warnings.warn('viewcode_import was renamed to viewcode_follow_imported_members. '
'Please update your configuration.', 'Please update your configuration.',
RemovedInSphinx30Warning, stacklevel=2) RemovedInSphinx30Warning, stacklevel=2)
def setup(app): def setup(app: Sphinx) -> Dict[str, Any]:
# type: (Sphinx) -> Dict[str, Any]
app.add_config_value('viewcode_import', None, False) app.add_config_value('viewcode_import', None, False)
app.add_config_value('viewcode_enable_epub', False, False) app.add_config_value('viewcode_enable_epub', False, False)
app.add_config_value('viewcode_follow_imported_members', True, False) app.add_config_value('viewcode_follow_imported_members', True, False)

View File

@ -139,7 +139,8 @@ class PygmentsBridge:
lexer = lexers['none'] lexer = lexers['none']
if lang in lexers: if lang in lexers:
lexer = lexers[lang] # just return custom lexers here (without installing raiseonerror filter)
return lexers[lang]
elif lang in lexer_classes: elif lang in lexer_classes:
lexer = lexer_classes[lang](**opts) lexer = lexer_classes[lang](**opts)
else: else:

View File

@ -11,29 +11,25 @@
import re import re
from io import StringIO from io import StringIO
from os import path from os import path
from typing import Any, Dict, IO, List, Tuple
from zipfile import ZipFile from zipfile import ZipFile
from sphinx.errors import PycodeError from sphinx.errors import PycodeError
from sphinx.pycode.parser import Parser from sphinx.pycode.parser import Parser
from sphinx.util import get_module_source, detect_encoding from sphinx.util import get_module_source, detect_encoding
if False:
# For type annotation
from typing import Any, Dict, IO, List, Tuple # NOQA
class ModuleAnalyzer: class ModuleAnalyzer:
# cache for analyzer objects -- caches both by module and file name # cache for analyzer objects -- caches both by module and file name
cache = {} # type: Dict[Tuple[str, str], Any] cache = {} # type: Dict[Tuple[str, str], Any]
@classmethod @classmethod
def for_string(cls, string, modname, srcname='<string>'): def for_string(cls, string: str, modname: str, srcname: str = '<string>'
# type: (str, str, str) -> ModuleAnalyzer ) -> "ModuleAnalyzer":
return cls(StringIO(string), modname, srcname, decoded=True) return cls(StringIO(string), modname, srcname, decoded=True)
@classmethod @classmethod
def for_file(cls, filename, modname): def for_file(cls, filename: str, modname: str) -> "ModuleAnalyzer":
# type: (str, str) -> ModuleAnalyzer
if ('file', filename) in cls.cache: if ('file', filename) in cls.cache:
return cls.cache['file', filename] return cls.cache['file', filename]
try: try:
@ -48,8 +44,7 @@ class ModuleAnalyzer:
return obj return obj
@classmethod @classmethod
def for_egg(cls, filename, modname): def for_egg(cls, filename: str, modname: str) -> "ModuleAnalyzer":
# type: (str, str) -> ModuleAnalyzer
SEP = re.escape(path.sep) SEP = re.escape(path.sep)
eggpath, relpath = re.split('(?<=\\.egg)' + SEP, filename) eggpath, relpath = re.split('(?<=\\.egg)' + SEP, filename)
try: try:
@ -60,8 +55,7 @@ class ModuleAnalyzer:
raise PycodeError('error opening %r' % filename, exc) raise PycodeError('error opening %r' % filename, exc)
@classmethod @classmethod
def for_module(cls, modname): def for_module(cls, modname: str) -> "ModuleAnalyzer":
# type: (str) -> ModuleAnalyzer
if ('module', modname) in cls.cache: if ('module', modname) in cls.cache:
entry = cls.cache['module', modname] entry = cls.cache['module', modname]
if isinstance(entry, PycodeError): if isinstance(entry, PycodeError):
@ -80,8 +74,7 @@ class ModuleAnalyzer:
cls.cache['module', modname] = obj cls.cache['module', modname] = obj
return obj return obj
def __init__(self, source, modname, srcname, decoded=False): def __init__(self, source: IO, modname: str, srcname: str, decoded: bool = False) -> None:
# type: (IO, str, str, bool) -> None
self.modname = modname # name of the module self.modname = modname # name of the module
self.srcname = srcname # name of the source file self.srcname = srcname # name of the source file
@ -100,8 +93,7 @@ class ModuleAnalyzer:
self.tagorder = None # type: Dict[str, int] self.tagorder = None # type: Dict[str, int]
self.tags = None # type: Dict[str, Tuple[str, int, int]] self.tags = None # type: Dict[str, Tuple[str, int, int]]
def parse(self): def parse(self) -> None:
# type: () -> None
"""Parse the source code.""" """Parse the source code."""
try: try:
parser = Parser(self.code, self.encoding) parser = Parser(self.code, self.encoding)
@ -119,16 +111,14 @@ class ModuleAnalyzer:
except Exception as exc: except Exception as exc:
raise PycodeError('parsing %r failed: %r' % (self.srcname, exc)) raise PycodeError('parsing %r failed: %r' % (self.srcname, exc))
def find_attr_docs(self): def find_attr_docs(self) -> Dict[Tuple[str, str], List[str]]:
# type: () -> Dict[Tuple[str, str], List[str]]
"""Find class and module-level attributes and their documentation.""" """Find class and module-level attributes and their documentation."""
if self.attr_docs is None: if self.attr_docs is None:
self.parse() self.parse()
return self.attr_docs return self.attr_docs
def find_tags(self): def find_tags(self) -> Dict[str, Tuple[str, int, int]]:
# type: () -> Dict[str, Tuple[str, int, int]]
"""Find class, function and method definitions and their location.""" """Find class, function and method definitions and their location."""
if self.tags is None: if self.tags is None:
self.parse() self.parse()

View File

@ -15,10 +15,8 @@ import sys
import tokenize import tokenize
from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING
from tokenize import COMMENT, NL from tokenize import COMMENT, NL
from typing import Any, Dict, List, Tuple
if False:
# For type annotation
from typing import Any, Dict, List, Tuple # NOQA
comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$') comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$')
indent_re = re.compile('^\\s*$') indent_re = re.compile('^\\s*$')
@ -31,13 +29,11 @@ else:
ASSIGN_NODES = (ast.Assign) ASSIGN_NODES = (ast.Assign)
def filter_whitespace(code): def filter_whitespace(code: str) -> str:
# type: (str) -> str
return code.replace('\f', ' ') # replace FF (form feed) with whitespace return code.replace('\f', ' ') # replace FF (form feed) with whitespace
def get_assign_targets(node): def get_assign_targets(node: ast.AST) -> List[ast.expr]:
# type: (ast.AST) -> List[ast.expr]
"""Get list of targets from Assign and AnnAssign node.""" """Get list of targets from Assign and AnnAssign node."""
if isinstance(node, ast.Assign): if isinstance(node, ast.Assign):
return node.targets return node.targets
@ -45,8 +41,7 @@ def get_assign_targets(node):
return [node.target] # type: ignore return [node.target] # type: ignore
def get_lvar_names(node, self=None): def get_lvar_names(node: ast.AST, self: ast.arg = None) -> List[str]:
# type: (ast.AST, ast.arg) -> List[str]
"""Convert assignment-AST to variable names. """Convert assignment-AST to variable names.
This raises `TypeError` if the assignment does not create new variable:: This raises `TypeError` if the assignment does not create new variable::
@ -88,11 +83,9 @@ def get_lvar_names(node, self=None):
raise NotImplementedError('Unexpected node name %r' % node_name) raise NotImplementedError('Unexpected node name %r' % node_name)
def dedent_docstring(s): def dedent_docstring(s: str) -> str:
# type: (str) -> str
"""Remove common leading indentation from docstring.""" """Remove common leading indentation from docstring."""
def dummy(): def dummy() -> None:
# type: () -> None
# dummy function to mock `inspect.getdoc`. # dummy function to mock `inspect.getdoc`.
pass pass
@ -104,16 +97,15 @@ def dedent_docstring(s):
class Token: class Token:
"""Better token wrapper for tokenize module.""" """Better token wrapper for tokenize module."""
def __init__(self, kind, value, start, end, source): def __init__(self, kind: int, value: Any, start: Tuple[int, int], end: Tuple[int, int],
# type: (int, Any, Tuple[int, int], Tuple[int, int], str) -> None source: str) -> None:
self.kind = kind self.kind = kind
self.value = value self.value = value
self.start = start self.start = start
self.end = end self.end = end
self.source = source self.source = source
def __eq__(self, other): def __eq__(self, other: Any) -> bool:
# type: (Any) -> bool
if isinstance(other, int): if isinstance(other, int):
return self.kind == other return self.kind == other
elif isinstance(other, str): elif isinstance(other, str):
@ -125,32 +117,27 @@ class Token:
else: else:
raise ValueError('Unknown value: %r' % other) raise ValueError('Unknown value: %r' % other)
def match(self, *conditions): def match(self, *conditions) -> bool:
# type: (Any) -> bool
return any(self == candidate for candidate in conditions) return any(self == candidate for candidate in conditions)
def __repr__(self): def __repr__(self) -> str:
# type: () -> str
return '<Token kind=%r value=%r>' % (tokenize.tok_name[self.kind], return '<Token kind=%r value=%r>' % (tokenize.tok_name[self.kind],
self.value.strip()) self.value.strip())
class TokenProcessor: class TokenProcessor:
def __init__(self, buffers): def __init__(self, buffers: List[str]) -> None:
# type: (List[str]) -> None
lines = iter(buffers) lines = iter(buffers)
self.buffers = buffers self.buffers = buffers
self.tokens = tokenize.generate_tokens(lambda: next(lines)) self.tokens = tokenize.generate_tokens(lambda: next(lines))
self.current = None # type: Token self.current = None # type: Token
self.previous = None # type: Token self.previous = None # type: Token
def get_line(self, lineno): def get_line(self, lineno: int) -> str:
# type: (int) -> str
"""Returns specified line.""" """Returns specified line."""
return self.buffers[lineno - 1] return self.buffers[lineno - 1]
def fetch_token(self): def fetch_token(self) -> Token:
# type: () -> Token
"""Fetch a next token from source code. """Fetch a next token from source code.
Returns ``False`` if sequence finished. Returns ``False`` if sequence finished.
@ -163,8 +150,7 @@ class TokenProcessor:
return self.current return self.current
def fetch_until(self, condition): def fetch_until(self, condition: Any) -> List[Token]:
# type: (Any) -> List[Token]
"""Fetch tokens until specified token appeared. """Fetch tokens until specified token appeared.
.. note:: This also handles parenthesis well. .. note:: This also handles parenthesis well.
@ -191,13 +177,11 @@ class AfterCommentParser(TokenProcessor):
and returns the comments for variable if exists. and returns the comments for variable if exists.
""" """
def __init__(self, lines): def __init__(self, lines: List[str]) -> None:
# type: (List[str]) -> None
super().__init__(lines) super().__init__(lines)
self.comment = None # type: str self.comment = None # type: str
def fetch_rvalue(self): def fetch_rvalue(self) -> List[Token]:
# type: () -> List[Token]
"""Fetch right-hand value of assignment.""" """Fetch right-hand value of assignment."""
tokens = [] tokens = []
while self.fetch_token(): while self.fetch_token():
@ -217,8 +201,7 @@ class AfterCommentParser(TokenProcessor):
return tokens return tokens
def parse(self): def parse(self) -> None:
# type: () -> None
"""Parse the code and obtain comment after assignment.""" """Parse the code and obtain comment after assignment."""
# skip lvalue (or whole of AnnAssign) # skip lvalue (or whole of AnnAssign)
while not self.fetch_token().match([OP, '='], NEWLINE, COMMENT): while not self.fetch_token().match([OP, '='], NEWLINE, COMMENT):
@ -235,8 +218,7 @@ class AfterCommentParser(TokenProcessor):
class VariableCommentPicker(ast.NodeVisitor): class VariableCommentPicker(ast.NodeVisitor):
"""Python source code parser to pick up variable comments.""" """Python source code parser to pick up variable comments."""
def __init__(self, buffers, encoding): def __init__(self, buffers: List[str], encoding: str) -> None:
# type: (List[str], str) -> None
self.counter = itertools.count() self.counter = itertools.count()
self.buffers = buffers self.buffers = buffers
self.encoding = encoding self.encoding = encoding
@ -248,8 +230,7 @@ class VariableCommentPicker(ast.NodeVisitor):
self.deforders = {} # type: Dict[str, int] self.deforders = {} # type: Dict[str, int]
super().__init__() super().__init__()
def add_entry(self, name): def add_entry(self, name: str) -> None:
# type: (str) -> None
if self.current_function: if self.current_function:
if self.current_classes and self.context[-1] == "__init__": if self.current_classes and self.context[-1] == "__init__":
# store variable comments inside __init__ method of classes # store variable comments inside __init__ method of classes
@ -261,8 +242,7 @@ class VariableCommentPicker(ast.NodeVisitor):
self.deforders[".".join(definition)] = next(self.counter) self.deforders[".".join(definition)] = next(self.counter)
def add_variable_comment(self, name, comment): def add_variable_comment(self, name: str, comment: str) -> None:
# type: (str, str) -> None
if self.current_function: if self.current_function:
if self.current_classes and self.context[-1] == "__init__": if self.current_classes and self.context[-1] == "__init__":
# store variable comments inside __init__ method of classes # store variable comments inside __init__ method of classes
@ -274,27 +254,23 @@ class VariableCommentPicker(ast.NodeVisitor):
self.comments[(context, name)] = comment self.comments[(context, name)] = comment
def get_self(self): def get_self(self) -> ast.arg:
# type: () -> ast.arg
"""Returns the name of first argument if in function.""" """Returns the name of first argument if in function."""
if self.current_function and self.current_function.args.args: if self.current_function and self.current_function.args.args:
return self.current_function.args.args[0] return self.current_function.args.args[0]
else: else:
return None return None
def get_line(self, lineno): def get_line(self, lineno: int) -> str:
# type: (int) -> str
"""Returns specified line.""" """Returns specified line."""
return self.buffers[lineno - 1] return self.buffers[lineno - 1]
def visit(self, node): def visit(self, node: ast.AST) -> None:
# type: (ast.AST) -> None
"""Updates self.previous to .""" """Updates self.previous to ."""
super().visit(node) super().visit(node)
self.previous = node self.previous = node
def visit_Assign(self, node): def visit_Assign(self, node: ast.Assign) -> None:
# type: (ast.Assign) -> None
"""Handles Assign node and pick up a variable comment.""" """Handles Assign node and pick up a variable comment."""
try: try:
targets = get_assign_targets(node) targets = get_assign_targets(node)
@ -334,13 +310,11 @@ class VariableCommentPicker(ast.NodeVisitor):
for varname in varnames: for varname in varnames:
self.add_entry(varname) self.add_entry(varname)
def visit_AnnAssign(self, node): def visit_AnnAssign(self, node: ast.AST) -> None: # Note: ast.AnnAssign not found in py35
# type: (ast.AST) -> None
"""Handles AnnAssign node and pick up a variable comment.""" """Handles AnnAssign node and pick up a variable comment."""
self.visit_Assign(node) # type: ignore self.visit_Assign(node) # type: ignore
def visit_Expr(self, node): def visit_Expr(self, node: ast.Expr) -> None:
# type: (ast.Expr) -> None
"""Handles Expr node and pick up a comment if string.""" """Handles Expr node and pick up a comment if string."""
if (isinstance(self.previous, ASSIGN_NODES) and isinstance(node.value, ast.Str)): if (isinstance(self.previous, ASSIGN_NODES) and isinstance(node.value, ast.Str)):
try: try:
@ -357,8 +331,7 @@ class VariableCommentPicker(ast.NodeVisitor):
except TypeError: except TypeError:
pass # this assignment is not new definition! pass # this assignment is not new definition!
def visit_Try(self, node): def visit_Try(self, node: ast.Try) -> None:
# type: (ast.Try) -> None
"""Handles Try node and processes body and else-clause. """Handles Try node and processes body and else-clause.
.. note:: pycode parser ignores objects definition in except-clause. .. note:: pycode parser ignores objects definition in except-clause.
@ -368,8 +341,7 @@ class VariableCommentPicker(ast.NodeVisitor):
for subnode in node.orelse: for subnode in node.orelse:
self.visit(subnode) self.visit(subnode)
def visit_ClassDef(self, node): def visit_ClassDef(self, node: ast.ClassDef) -> None:
# type: (ast.ClassDef) -> None
"""Handles ClassDef node and set context.""" """Handles ClassDef node and set context."""
self.current_classes.append(node.name) self.current_classes.append(node.name)
self.add_entry(node.name) self.add_entry(node.name)
@ -380,8 +352,7 @@ class VariableCommentPicker(ast.NodeVisitor):
self.context.pop() self.context.pop()
self.current_classes.pop() self.current_classes.pop()
def visit_FunctionDef(self, node): def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
# type: (ast.FunctionDef) -> None
"""Handles FunctionDef node and set context.""" """Handles FunctionDef node and set context."""
if self.current_function is None: if self.current_function is None:
self.add_entry(node.name) # should be called before setting self.current_function self.add_entry(node.name) # should be called before setting self.current_function
@ -392,8 +363,7 @@ class VariableCommentPicker(ast.NodeVisitor):
self.context.pop() self.context.pop()
self.current_function = None self.current_function = None
def visit_AsyncFunctionDef(self, node): def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
# type: (ast.AsyncFunctionDef) -> None
"""Handles AsyncFunctionDef node and set context.""" """Handles AsyncFunctionDef node and set context."""
self.visit_FunctionDef(node) # type: ignore self.visit_FunctionDef(node) # type: ignore
@ -403,16 +373,14 @@ class DefinitionFinder(TokenProcessor):
classes and methods. classes and methods.
""" """
def __init__(self, lines): def __init__(self, lines: List[str]) -> None:
# type: (List[str]) -> None
super().__init__(lines) super().__init__(lines)
self.decorator = None # type: Token self.decorator = None # type: Token
self.context = [] # type: List[str] self.context = [] # type: List[str]
self.indents = [] # type: List self.indents = [] # type: List
self.definitions = {} # type: Dict[str, Tuple[str, int, int]] self.definitions = {} # type: Dict[str, Tuple[str, int, int]]
def add_definition(self, name, entry): def add_definition(self, name: str, entry: Tuple[str, int, int]) -> None:
# type: (str, Tuple[str, int, int]) -> None
"""Add a location of definition.""" """Add a location of definition."""
if self.indents and self.indents[-1][0] == 'def' and entry[0] == 'def': if self.indents and self.indents[-1][0] == 'def' and entry[0] == 'def':
# ignore definition of inner function # ignore definition of inner function
@ -420,8 +388,7 @@ class DefinitionFinder(TokenProcessor):
else: else:
self.definitions[name] = entry self.definitions[name] = entry
def parse(self): def parse(self) -> None:
# type: () -> None
"""Parse the code to obtain location of definitions.""" """Parse the code to obtain location of definitions."""
while True: while True:
token = self.fetch_token() token = self.fetch_token()
@ -442,8 +409,7 @@ class DefinitionFinder(TokenProcessor):
elif token == DEDENT: elif token == DEDENT:
self.finalize_block() self.finalize_block()
def parse_definition(self, typ): def parse_definition(self, typ: str) -> None:
# type: (str) -> None
"""Parse AST of definition.""" """Parse AST of definition."""
name = self.fetch_token() name = self.fetch_token()
self.context.append(name.value) self.context.append(name.value)
@ -464,8 +430,7 @@ class DefinitionFinder(TokenProcessor):
self.add_definition(funcname, (typ, start_pos, name.end[0])) self.add_definition(funcname, (typ, start_pos, name.end[0]))
self.context.pop() self.context.pop()
def finalize_block(self): def finalize_block(self) -> None:
# type: () -> None
"""Finalize definition block.""" """Finalize definition block."""
definition = self.indents.pop() definition = self.indents.pop()
if definition[0] != 'other': if definition[0] != 'other':
@ -484,22 +449,19 @@ class Parser:
This is a better wrapper for ``VariableCommentPicker``. This is a better wrapper for ``VariableCommentPicker``.
""" """
def __init__(self, code, encoding='utf-8'): def __init__(self, code: str, encoding: str = 'utf-8') -> None:
# type: (str, str) -> None
self.code = filter_whitespace(code) self.code = filter_whitespace(code)
self.encoding = encoding self.encoding = encoding
self.comments = {} # type: Dict[Tuple[str, str], str] self.comments = {} # type: Dict[Tuple[str, str], str]
self.deforders = {} # type: Dict[str, int] self.deforders = {} # type: Dict[str, int]
self.definitions = {} # type: Dict[str, Tuple[str, int, int]] self.definitions = {} # type: Dict[str, Tuple[str, int, int]]
def parse(self): def parse(self) -> None:
# type: () -> None
"""Parse the source code.""" """Parse the source code."""
self.parse_comments() self.parse_comments()
self.parse_definition() self.parse_definition()
def parse_comments(self): def parse_comments(self) -> None:
# type: () -> None
"""Parse the code and pick up comments.""" """Parse the code and pick up comments."""
tree = ast.parse(self.code.encode()) tree = ast.parse(self.code.encode())
picker = VariableCommentPicker(self.code.splitlines(True), self.encoding) picker = VariableCommentPicker(self.code.splitlines(True), self.encoding)
@ -507,8 +469,7 @@ class Parser:
self.comments = picker.comments self.comments = picker.comments
self.deforders = picker.deforders self.deforders = picker.deforders
def parse_definition(self): def parse_definition(self) -> None:
# type: () -> None
"""Parse the location of definitions from the code.""" """Parse the location of definitions from the code."""
parser = DefinitionFinder(self.code.splitlines(True)) parser = DefinitionFinder(self.code.splitlines(True))
parser.parse() parser.parse()

View File

@ -14,25 +14,20 @@ import sys
from collections import namedtuple from collections import namedtuple
from io import StringIO from io import StringIO
from subprocess import PIPE from subprocess import PIPE
from typing import Any, Dict
import pytest import pytest
from . import util from . import util
if False:
# For type annotation
from typing import Any, Dict, Union # NOQA
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def rootdir(): def rootdir() -> None:
# type: () -> None
return None return None
@pytest.fixture @pytest.fixture
def app_params(request, test_params, shared_result, sphinx_test_tempdir, rootdir): def app_params(request, test_params, shared_result, sphinx_test_tempdir, rootdir):
# type: (Any, Any, Any, Any, Any) -> None
""" """
parameters that is specified by 'pytest.mark.sphinx' for parameters that is specified by 'pytest.mark.sphinx' for
sphinx.application.Sphinx initialization sphinx.application.Sphinx initialization
@ -158,10 +153,10 @@ def make_app(test_params, monkeypatch):
status, warning = StringIO(), StringIO() status, warning = StringIO(), StringIO()
kwargs.setdefault('status', status) kwargs.setdefault('status', status)
kwargs.setdefault('warning', warning) kwargs.setdefault('warning', warning)
app_ = util.SphinxTestApp(*args, **kwargs) # type: Union[util.SphinxTestApp, util.SphinxTestAppWrapperForSkipBuilding] # NOQA app_ = util.SphinxTestApp(*args, **kwargs) # type: Any
apps.append(app_) apps.append(app_)
if test_params['shared_result']: if test_params['shared_result']:
app_ = util.SphinxTestAppWrapperForSkipBuilding(app_) # type: ignore app_ = util.SphinxTestAppWrapperForSkipBuilding(app_)
return app_ return app_
yield make yield make

View File

@ -5,14 +5,12 @@
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import builtins
import os import os
import shutil import shutil
import sys import sys
from typing import Any, Callable, IO, List
if False:
# For type annotation
import builtins # NOQA
from typing import Any, Callable, IO, List # NOQA
FILESYSTEMENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding() FILESYSTEMENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding()
@ -24,61 +22,52 @@ class path(str):
""" """
@property @property
def parent(self): def parent(self) -> "path":
# type: () -> path
""" """
The name of the directory the file or directory is in. The name of the directory the file or directory is in.
""" """
return self.__class__(os.path.dirname(self)) return self.__class__(os.path.dirname(self))
def basename(self): def basename(self) -> str:
# type: () -> str
return os.path.basename(self) return os.path.basename(self)
def abspath(self): def abspath(self) -> "path":
# type: () -> path
""" """
Returns the absolute path. Returns the absolute path.
""" """
return self.__class__(os.path.abspath(self)) return self.__class__(os.path.abspath(self))
def isabs(self): def isabs(self) -> bool:
# type: () -> bool
""" """
Returns ``True`` if the path is absolute. Returns ``True`` if the path is absolute.
""" """
return os.path.isabs(self) return os.path.isabs(self)
def isdir(self): def isdir(self) -> bool:
# type: () -> bool
""" """
Returns ``True`` if the path is a directory. Returns ``True`` if the path is a directory.
""" """
return os.path.isdir(self) return os.path.isdir(self)
def isfile(self): def isfile(self) -> bool:
# type: () -> bool
""" """
Returns ``True`` if the path is a file. Returns ``True`` if the path is a file.
""" """
return os.path.isfile(self) return os.path.isfile(self)
def islink(self): def islink(self) -> bool:
# type: () -> bool
""" """
Returns ``True`` if the path is a symbolic link. Returns ``True`` if the path is a symbolic link.
""" """
return os.path.islink(self) return os.path.islink(self)
def ismount(self): def ismount(self) -> bool:
# type: () -> bool
""" """
Returns ``True`` if the path is a mount point. Returns ``True`` if the path is a mount point.
""" """
return os.path.ismount(self) return os.path.ismount(self)
def rmtree(self, ignore_errors=False, onerror=None): def rmtree(self, ignore_errors: bool = False, onerror: Callable = None) -> None:
# type: (bool, Callable) -> None
""" """
Removes the file or directory and any files or directories it may Removes the file or directory and any files or directories it may
contain. contain.
@ -96,8 +85,7 @@ class path(str):
""" """
shutil.rmtree(self, ignore_errors=ignore_errors, onerror=onerror) shutil.rmtree(self, ignore_errors=ignore_errors, onerror=onerror)
def copytree(self, destination, symlinks=False): def copytree(self, destination: str, symlinks: bool = False) -> None:
# type: (str, bool) -> None
""" """
Recursively copy a directory to the given `destination`. If the given Recursively copy a directory to the given `destination`. If the given
`destination` does not exist it will be created. `destination` does not exist it will be created.
@ -109,8 +97,7 @@ class path(str):
""" """
shutil.copytree(self, destination, symlinks=symlinks) shutil.copytree(self, destination, symlinks=symlinks)
def movetree(self, destination): def movetree(self, destination: str) -> None:
# type: (str) -> None
""" """
Recursively move the file or directory to the given `destination` Recursively move the file or directory to the given `destination`
similar to the Unix "mv" command. similar to the Unix "mv" command.
@ -122,54 +109,46 @@ class path(str):
move = movetree move = movetree
def unlink(self): def unlink(self) -> None:
# type: () -> None
""" """
Removes a file. Removes a file.
""" """
os.unlink(self) os.unlink(self)
def stat(self): def stat(self) -> Any:
# type: () -> Any
""" """
Returns a stat of the file. Returns a stat of the file.
""" """
return os.stat(self) return os.stat(self)
def utime(self, arg): def utime(self, arg: Any) -> None:
# type: (Any) -> None
os.utime(self, arg) os.utime(self, arg)
def open(self, mode='r', **kwargs): def open(self, mode: str = 'r', **kwargs) -> IO:
# type: (str, Any) -> IO
return open(self, mode, **kwargs) return open(self, mode, **kwargs)
def write_text(self, text, encoding='utf-8', **kwargs): def write_text(self, text: str, encoding: str = 'utf-8', **kwargs) -> None:
# type: (str, str, Any) -> None
""" """
Writes the given `text` to the file. Writes the given `text` to the file.
""" """
with open(self, 'w', encoding=encoding, **kwargs) as f: with open(self, 'w', encoding=encoding, **kwargs) as f:
f.write(text) f.write(text)
def text(self, encoding='utf-8', **kwargs): def text(self, encoding: str = 'utf-8', **kwargs) -> str:
# type: (str, Any) -> str
""" """
Returns the text in the file. Returns the text in the file.
""" """
with open(self, encoding=encoding, **kwargs) as f: with open(self, encoding=encoding, **kwargs) as f:
return f.read() return f.read()
def bytes(self): def bytes(self) -> builtins.bytes:
# type: () -> builtins.bytes
""" """
Returns the bytes in the file. Returns the bytes in the file.
""" """
with open(self, mode='rb') as f: with open(self, mode='rb') as f:
return f.read() return f.read()
def write_bytes(self, bytes, append=False): def write_bytes(self, bytes: str, append: bool = False) -> None:
# type: (str, bool) -> None
""" """
Writes the given `bytes` to the file. Writes the given `bytes` to the file.
@ -183,41 +162,35 @@ class path(str):
with open(self, mode=mode) as f: with open(self, mode=mode) as f:
f.write(bytes) f.write(bytes)
def exists(self): def exists(self) -> bool:
# type: () -> bool
""" """
Returns ``True`` if the path exist. Returns ``True`` if the path exist.
""" """
return os.path.exists(self) return os.path.exists(self)
def lexists(self): def lexists(self) -> bool:
# type: () -> bool
""" """
Returns ``True`` if the path exists unless it is a broken symbolic Returns ``True`` if the path exists unless it is a broken symbolic
link. link.
""" """
return os.path.lexists(self) return os.path.lexists(self)
def makedirs(self, mode=0o777, exist_ok=False): def makedirs(self, mode: int = 0o777, exist_ok: bool = False) -> None:
# type: (int, bool) -> None
""" """
Recursively create directories. Recursively create directories.
""" """
os.makedirs(self, mode, exist_ok=exist_ok) os.makedirs(self, mode, exist_ok=exist_ok)
def joinpath(self, *args): def joinpath(self, *args) -> "path":
# type: (Any) -> path
""" """
Joins the path with the argument given and returns the result. Joins the path with the argument given and returns the result.
""" """
return self.__class__(os.path.join(self, *map(self.__class__, args))) return self.__class__(os.path.join(self, *map(self.__class__, args)))
def listdir(self): def listdir(self) -> List[str]:
# type: () -> List[str]
return os.listdir(self) return os.listdir(self)
__div__ = __truediv__ = joinpath __div__ = __truediv__ = joinpath
def __repr__(self): def __repr__(self) -> str:
# type: () -> str
return '%s(%s)' % (self.__class__.__name__, super().__repr__()) return '%s(%s)' % (self.__class__.__name__, super().__repr__())

View File

@ -8,21 +8,16 @@
from os import path from os import path
from docutils import nodes
from docutils.core import publish_doctree from docutils.core import publish_doctree
from sphinx.application import Sphinx
from sphinx.io import SphinxStandaloneReader from sphinx.io import SphinxStandaloneReader
from sphinx.parsers import RSTParser from sphinx.parsers import RSTParser
from sphinx.util.docutils import sphinx_domains from sphinx.util.docutils import sphinx_domains
if False: def parse(app: Sphinx, text: str, docname: str = 'index') -> nodes.document:
# For type annotation
from docutils import nodes # NOQA
from sphinx.application import Sphinx # NOQA
def parse(app, text, docname='index'):
# type: (Sphinx, str, str) -> nodes.document
"""Parse a string as reStructuredText with Sphinx application.""" """Parse a string as reStructuredText with Sphinx application."""
try: try:
app.env.temp_data['docname'] = docname app.env.temp_data['docname'] = docname

View File

@ -11,6 +11,7 @@ import os
import re import re
import sys import sys
import warnings import warnings
from typing import Any, Dict, Generator, IO, List, Pattern
from xml.etree import ElementTree from xml.etree import ElementTree
from docutils import nodes from docutils import nodes
@ -23,10 +24,6 @@ from sphinx.pycode import ModuleAnalyzer
from sphinx.testing.path import path from sphinx.testing.path import path
from sphinx.util.osutil import relpath from sphinx.util.osutil import relpath
if False:
# For type annotation
from typing import Any, Dict, Generator, IO, List, Pattern # NOQA
__all__ = [ __all__ = [
'Struct', 'Struct',
@ -35,26 +32,22 @@ __all__ = [
] ]
def assert_re_search(regex, text, flags=0): def assert_re_search(regex: Pattern, text: str, flags: int = 0) -> None:
# type: (Pattern, str, int) -> None
if not re.search(regex, text, flags): if not re.search(regex, text, flags):
assert False, '%r did not match %r' % (regex, text) assert False, '%r did not match %r' % (regex, text)
def assert_not_re_search(regex, text, flags=0): def assert_not_re_search(regex: Pattern, text: str, flags: int = 0) -> None:
# type: (Pattern, str, int) -> None
if re.search(regex, text, flags): if re.search(regex, text, flags):
assert False, '%r did match %r' % (regex, text) assert False, '%r did match %r' % (regex, text)
def assert_startswith(thing, prefix): def assert_startswith(thing: str, prefix: str) -> None:
# type: (str, str) -> None
if not thing.startswith(prefix): if not thing.startswith(prefix):
assert False, '%r does not start with %r' % (thing, prefix) assert False, '%r does not start with %r' % (thing, prefix)
def assert_node(node, cls=None, xpath="", **kwargs): def assert_node(node: nodes.Node, cls: Any = None, xpath: str = "", **kwargs) -> None:
# type: (nodes.Node, Any, str, Any) -> None
if cls: if cls:
if isinstance(cls, list): if isinstance(cls, list):
assert_node(node, cls[0], xpath=xpath, **kwargs) assert_node(node, cls[0], xpath=xpath, **kwargs)
@ -92,16 +85,14 @@ def assert_node(node, cls=None, xpath="", **kwargs):
'The node%s[%s] is not %r: %r' % (xpath, key, value, node[key]) 'The node%s[%s] is not %r: %r' % (xpath, key, value, node[key])
def etree_parse(path): def etree_parse(path: str) -> Any:
# type: (str) -> Any
with warnings.catch_warnings(record=False): with warnings.catch_warnings(record=False):
warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=DeprecationWarning)
return ElementTree.parse(path) return ElementTree.parse(path)
class Struct: class Struct:
def __init__(self, **kwds): def __init__(self, **kwds) -> None:
# type: (Any) -> None
self.__dict__.update(kwds) self.__dict__.update(kwds)
@ -111,10 +102,9 @@ class SphinxTestApp(application.Sphinx):
better default values for the initialization parameters. better default values for the initialization parameters.
""" """
def __init__(self, buildername='html', srcdir=None, def __init__(self, buildername: str = 'html', srcdir: path = None, freshenv: bool = False,
freshenv=False, confoverrides=None, status=None, warning=None, confoverrides: Dict = None, status: IO = None, warning: IO = None,
tags=None, docutilsconf=None): tags: List[str] = None, docutilsconf: str = None) -> None:
# type: (str, path, bool, Dict, IO, IO, List[str], str) -> None
if docutilsconf is not None: if docutilsconf is not None:
(srcdir / 'docutils.conf').write_text(docutilsconf) (srcdir / 'docutils.conf').write_text(docutilsconf)
@ -144,8 +134,7 @@ class SphinxTestApp(application.Sphinx):
self.cleanup() self.cleanup()
raise raise
def cleanup(self, doctrees=False): def cleanup(self, doctrees: bool = False) -> None:
# type: (bool) -> None
ModuleAnalyzer.cache.clear() ModuleAnalyzer.cache.clear()
LaTeXBuilder.usepackages = [] LaTeXBuilder.usepackages = []
locale.translators.clear() locale.translators.clear()
@ -159,8 +148,7 @@ class SphinxTestApp(application.Sphinx):
delattr(nodes.GenericNodeVisitor, 'visit_' + method[6:]) delattr(nodes.GenericNodeVisitor, 'visit_' + method[6:])
delattr(nodes.GenericNodeVisitor, 'depart_' + method[6:]) delattr(nodes.GenericNodeVisitor, 'depart_' + method[6:])
def __repr__(self): def __repr__(self) -> str:
# type: () -> str
return '<%s buildername=%r>' % (self.__class__.__name__, self.builder.name) return '<%s buildername=%r>' % (self.__class__.__name__, self.builder.name)
@ -171,16 +159,13 @@ class SphinxTestAppWrapperForSkipBuilding:
file. file.
""" """
def __init__(self, app_): def __init__(self, app_: SphinxTestApp) -> None:
# type: (SphinxTestApp) -> None
self.app = app_ self.app = app_
def __getattr__(self, name): def __getattr__(self, name: str) -> Any:
# type: (str) -> Any
return getattr(self.app, name) return getattr(self.app, name)
def build(self, *args, **kw): def build(self, *args, **kw) -> None:
# type: (Any, Any) -> None
if not self.app.outdir.listdir(): # type: ignore if not self.app.outdir.listdir(): # type: ignore
# if listdir is empty, do build. # if listdir is empty, do build.
self.app.build(*args, **kw) self.app.build(*args, **kw)
@ -190,15 +175,13 @@ class SphinxTestAppWrapperForSkipBuilding:
_unicode_literals_re = re.compile(r'u(".*?")|u(\'.*?\')') _unicode_literals_re = re.compile(r'u(".*?")|u(\'.*?\')')
def remove_unicode_literals(s): def remove_unicode_literals(s: str) -> str:
# type: (str) -> str
warnings.warn('remove_unicode_literals() is deprecated.', warnings.warn('remove_unicode_literals() is deprecated.',
RemovedInSphinx40Warning, stacklevel=2) RemovedInSphinx40Warning, stacklevel=2)
return _unicode_literals_re.sub(lambda x: x.group(1) or x.group(2), s) return _unicode_literals_re.sub(lambda x: x.group(1) or x.group(2), s)
def find_files(root, suffix=None): def find_files(root: str, suffix: bool = None) -> Generator[str, None, None]:
# type: (str, bool) -> Generator
for dirpath, dirs, files in os.walk(root, followlinks=True): for dirpath, dirs, files in os.walk(root, followlinks=True):
dirpath = path(dirpath) dirpath = path(dirpath)
for f in [f for f in files if not suffix or f.endswith(suffix)]: # type: ignore for f in [f for f in files if not suffix or f.endswith(suffix)]: # type: ignore
@ -206,6 +189,5 @@ def find_files(root, suffix=None):
yield relpath(fpath, root) yield relpath(fpath, root)
def strip_escseq(text): def strip_escseq(text: str) -> str:
# type: (str) -> str
return re.sub('\x1b.*?m', '', text) return re.sub('\x1b.*?m', '', text)

View File

@ -413,6 +413,18 @@
\newcommand\sphinxsetup[1]{\setkeys{sphinx}{#1}} \newcommand\sphinxsetup[1]{\setkeys{sphinx}{#1}}
%% ALPHANUMERIC LIST ITEMS
\newcommand\sphinxsetlistlabels[5]
{% #1 = style, #2 = enum, #3 = enumnext, #4 = prefix, #5 = suffix
% #2 and #3 are counters used by enumerate environement e.g. enumi, enumii.
% #1 is a macro such as \arabic or \alph
% prefix and suffix are strings (by default empty and a dot).
\@namedef{the#2}{#1{#2}}%
\@namedef{label#2}{#4\@nameuse{the#2}#5}%
\@namedef{p@#3}{\@nameuse{p@#2}#4\@nameuse{the#2}#5}%
}%
%% MAXLISTDEPTH %% MAXLISTDEPTH
% %
% remove LaTeX's cap on nesting depth if 'maxlistdepth' key used. % remove LaTeX's cap on nesting depth if 'maxlistdepth' key used.

View File

@ -33,7 +33,7 @@
containing fewer words won't appear in the result list.{% endtrans %} containing fewer words won't appear in the result list.{% endtrans %}
</p> </p>
<form action="" method="get"> <form action="" method="get">
<input type="text" name="q" value="" /> <input type="text" name="q" aria-labelledby="search-documentation" value="" />
<input type="submit" value="{{ _('search') }}" /> <input type="submit" value="{{ _('search') }}" />
<span id="search-progress" style="padding-left: 10px"></span> <span id="search-progress" style="padding-left: 10px"></span>
</form> </form>

View File

@ -169,7 +169,7 @@ class DownloadFiles(dict):
Hence don't hack this directly. Hence don't hack this directly.
""" """
def add_file(self, docname: str, filename: str) -> None: def add_file(self, docname: str, filename: str) -> str:
if filename not in self: if filename not in self:
digest = md5(filename.encode()).hexdigest() digest = md5(filename.encode()).hexdigest()
dest = '%s/%s' % (digest, os.path.basename(filename)) dest = '%s/%s' % (digest, os.path.basename(filename))

View File

@ -12,7 +12,7 @@ import os
import re import re
import warnings import warnings
from collections import namedtuple from collections import namedtuple
from datetime import datetime from datetime import datetime, timezone
from os import path from os import path
from typing import Callable, Generator, List, Set, Tuple from typing import Callable, Generator, List, Set, Tuple
@ -274,7 +274,7 @@ def format_date(format: str, date: datetime = None, language: str = None) -> str
if source_date_epoch is not None: if source_date_epoch is not None:
date = datetime.utcfromtimestamp(float(source_date_epoch)) date = datetime.utcfromtimestamp(float(source_date_epoch))
else: else:
date = datetime.now() date = datetime.now(timezone.utc).astimezone()
result = [] result = []
tokens = date_format_re.split(format) tokens = date_format_re.split(format)

View File

@ -30,7 +30,7 @@ class PyStemmer(BaseStemmer):
return self.stemmer.stemWord(word) return self.stemmer.stemWord(word)
class StandardStemmer(PorterStemmer, BaseStemmer): # type: ignore class StandardStemmer(PorterStemmer, BaseStemmer):
"""All those porter stemmer implementations look hideous; """All those porter stemmer implementations look hideous;
make at least the stem method nicer. make at least the stem method nicer.
""" """

View File

@ -9,7 +9,7 @@
""" """
import os import os
from typing import Dict from typing import Dict, List, Union
from jinja2.loaders import BaseLoader from jinja2.loaders import BaseLoader
from jinja2.sandbox import SandboxedEnvironment from jinja2.sandbox import SandboxedEnvironment
@ -34,7 +34,13 @@ class BaseRenderer:
class FileRenderer(BaseRenderer): class FileRenderer(BaseRenderer):
def __init__(self, search_path: str) -> None: def __init__(self, search_path: Union[str, List[str]]) -> None:
if isinstance(search_path, str):
search_path = [search_path]
else:
# filter "None" paths
search_path = list(filter(None, search_path))
loader = SphinxFileSystemLoader(search_path) loader = SphinxFileSystemLoader(search_path)
super().__init__(loader) super().__init__(loader)
@ -46,7 +52,7 @@ class FileRenderer(BaseRenderer):
class SphinxRenderer(FileRenderer): class SphinxRenderer(FileRenderer):
def __init__(self, template_path: str = None) -> None: def __init__(self, template_path: Union[str, List[str]] = None) -> None:
if template_path is None: if template_path is None:
template_path = os.path.join(package_dir, 'templates') template_path = os.path.join(package_dir, 'templates')
super().__init__(template_path) super().__init__(template_path)
@ -76,7 +82,7 @@ class LaTeXRenderer(SphinxRenderer):
class ReSTRenderer(SphinxRenderer): class ReSTRenderer(SphinxRenderer):
def __init__(self, template_path: str = None, language: str = None) -> None: def __init__(self, template_path: Union[str, List[str]] = None, language: str = None) -> None: # NOQA
super().__init__(template_path) super().__init__(template_path)
# add language to environment # add language to environment

View File

@ -1373,11 +1373,8 @@ class LaTeXTranslator(SphinxTranslator):
suffix = node.get('suffix', '.') suffix = node.get('suffix', '.')
self.body.append('\\begin{enumerate}\n') self.body.append('\\begin{enumerate}\n')
self.body.append('\\def\\the%s{%s{%s}}\n' % (enum, style, enum)) self.body.append('\\sphinxsetlistlabels{%s}{%s}{%s}{%s}{%s}%%\n' %
self.body.append('\\def\\label%s{%s\\the%s %s}\n' % (style, enum, enumnext, prefix, suffix))
(enum, prefix, enum, suffix))
self.body.append('\\makeatletter\\def\\p@%s{\\p@%s %s\\the%s %s}\\makeatother\n' %
(enumnext, enum, prefix, enum, suffix))
if 'start' in node: if 'start' in node:
self.body.append('\\setcounter{%s}{%d}\n' % (enum, node['start'] - 1)) self.body.append('\\setcounter{%s}{%d}\n' % (enum, node['start'] - 1))
if self.table: if self.table:
@ -2598,7 +2595,7 @@ class LaTeXTranslator(SphinxTranslator):
RemovedInSphinx30Warning) RemovedInSphinx30Warning)
def visit_admonition(self, node): def visit_admonition(self, node):
# type: (nodes.Element) -> None # type: (LaTeXTranslator, nodes.Element) -> None
self.body.append('\n\\begin{sphinxadmonition}{%s}{%s:}' % self.body.append('\n\\begin{sphinxadmonition}{%s}{%s:}' %
(name, admonitionlabels[name])) (name, admonitionlabels[name]))
return visit_admonition return visit_admonition

View File

@ -1752,6 +1752,6 @@ class TexinfoTranslator(SphinxTranslator):
RemovedInSphinx30Warning) RemovedInSphinx30Warning)
def visit(self, node): def visit(self, node):
# type: (nodes.Element) -> None # type: (TexinfoTranslator, nodes.Element) -> None
self.visit_admonition(node, admonitionlabels[name]) self.visit_admonition(node, admonitionlabels[name])
return visit return visit

View File

@ -1375,6 +1375,6 @@ class TextTranslator(SphinxTranslator):
RemovedInSphinx30Warning) RemovedInSphinx30Warning)
def depart_admonition(self, node): def depart_admonition(self, node):
# type: (nodes.Element) -> None # type: (TextTranslator, nodes.Element) -> None
self.end_state(first=admonitionlabels[name] + ': ') self.end_state(first=admonitionlabels[name] + ': ')
return depart_admonition return depart_admonition

View File

@ -15,5 +15,3 @@ class Derived(Base):
def inheritedmeth(self): def inheritedmeth(self):
# no docstring here # no docstring here
pass pass

View File

@ -26,4 +26,4 @@ test-image
.. image:: https://www.python.org/static/img/python-logo.png .. image:: https://www.python.org/static/img/python-logo.png
.. non-exist remote image .. non-exist remote image
.. image:: http://example.com/NOT_EXIST.PNG .. image:: https://www.google.com/NOT_EXIST.PNG

View File

@ -23,8 +23,8 @@ link to external1_ and external2_.
link to `Sphinx Site <http://sphinx-doc.org>`_ and `Python Site <http://python.org>`_. link to `Sphinx Site <http://sphinx-doc.org>`_ and `Python Site <http://python.org>`_.
.. _external1: http://example.com/external1 .. _external1: https://www.google.com/external1
.. _external2: http://example.com/external2 .. _external2: https://www.google.com/external2
Multiple references in the same line Multiple references in the same line

View File

@ -6,11 +6,11 @@ This is from CPython documentation.
Some additional anchors to exercise ignore code Some additional anchors to exercise ignore code
* `Example Bar invalid <http://example.com/#!bar>`_ * `Example Bar invalid <https://www.google.com/#!bar>`_
* `Example Bar invalid <http://example.com#!bar>`_ tests that default ignore anchor of #! does not need to be prefixed with / * `Example Bar invalid <https://www.google.com#!bar>`_ tests that default ignore anchor of #! does not need to be prefixed with /
* `Example Bar invalid <http://example.com/#top>`_ * `Example Bar invalid <https://www.google.com/#top>`_
* `Example anchor invalid <http://www.sphinx-doc.org/en/1.7/intro.html#does-not-exist>`_ * `Example anchor invalid <http://www.sphinx-doc.org/en/1.7/intro.html#does-not-exist>`_
* `Complete nonsense <https://localhost:7777/doesnotexist>`_ * `Complete nonsense <https://localhost:7777/doesnotexist>`_
.. image:: http://example.com/image.png .. image:: https://www.google.com/image.png
.. figure:: http://example.com/image2.png .. figure:: https://www.google.com/image2.png

View File

@ -52,7 +52,8 @@ def test_msgfmt(app):
assert (app.outdir / 'en_US.po').isfile(), 'msginit failed' assert (app.outdir / 'en_US.po').isfile(), 'msginit failed'
try: try:
args = ['msgfmt', 'en_US.po', '-o', os.path.join('en', 'LC_MESSAGES', 'test_root.mo')] args = ['msgfmt', 'en_US.po',
'-o', os.path.join('en', 'LC_MESSAGES', 'test_root.mo')]
subprocess.run(args, stdout=PIPE, stderr=PIPE, check=True) subprocess.run(args, stdout=PIPE, stderr=PIPE, check=True)
except OSError: except OSError:
pytest.skip() # most likely msgfmt was not found pytest.skip() # most likely msgfmt was not found

View File

@ -16,6 +16,7 @@ from itertools import cycle, chain
import pytest import pytest
from html5lib import HTMLParser from html5lib import HTMLParser
from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
from sphinx.errors import ConfigError from sphinx.errors import ConfigError
from sphinx.testing.util import strip_escseq from sphinx.testing.util import strip_escseq
from sphinx.util import docutils from sphinx.util import docutils
@ -1239,25 +1240,25 @@ def test_html_entity(app):
def test_html_inventory(app): def test_html_inventory(app):
app.builder.build_all() app.builder.build_all()
with open(app.outdir / 'objects.inv', 'rb') as f: with open(app.outdir / 'objects.inv', 'rb') as f:
invdata = InventoryFile.load(f, 'http://example.com', os.path.join) invdata = InventoryFile.load(f, 'https://www.google.com', os.path.join)
assert set(invdata.keys()) == {'std:label', 'std:doc'} assert set(invdata.keys()) == {'std:label', 'std:doc'}
assert set(invdata['std:label'].keys()) == {'modindex', 'genindex', 'search'} assert set(invdata['std:label'].keys()) == {'modindex', 'genindex', 'search'}
assert invdata['std:label']['modindex'] == ('Python', assert invdata['std:label']['modindex'] == ('Python',
'', '',
'http://example.com/py-modindex.html', 'https://www.google.com/py-modindex.html',
'Module Index') 'Module Index')
assert invdata['std:label']['genindex'] == ('Python', assert invdata['std:label']['genindex'] == ('Python',
'', '',
'http://example.com/genindex.html', 'https://www.google.com/genindex.html',
'Index') 'Index')
assert invdata['std:label']['search'] == ('Python', assert invdata['std:label']['search'] == ('Python',
'', '',
'http://example.com/search.html', 'https://www.google.com/search.html',
'Search Page') 'Search Page')
assert set(invdata['std:doc'].keys()) == {'index'} assert set(invdata['std:doc'].keys()) == {'index'}
assert invdata['std:doc']['index'] == ('Python', assert invdata['std:doc']['index'] == ('Python',
'', '',
'http://example.com/index.html', 'https://www.google.com/index.html',
'The basic Sphinx documentation for testing') 'The basic Sphinx documentation for testing')
@ -1496,3 +1497,29 @@ def test_html_pygments_style_manually(app):
def test_html_pygments_for_classic_theme(app): def test_html_pygments_for_classic_theme(app):
style = app.builder.highlighter.formatter_args.get('style') style = app.builder.highlighter.formatter_args.get('style')
assert style.__name__ == 'SphinxStyle' assert style.__name__ == 'SphinxStyle'
@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path')
def test_validate_html_extra_path(app):
(app.confdir / '_static').makedirs()
app.config.html_extra_path = [
'/path/to/not_found', # not found
'_static',
app.outdir, # outdir
app.outdir / '_static', # inside outdir
]
validate_html_extra_path(app, app.config)
assert app.config.html_extra_path == ['_static']
@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_static_path')
def test_validate_html_static_path(app):
(app.confdir / '_static').makedirs()
app.config.html_static_path = [
'/path/to/not_found', # not found
'_static',
app.outdir, # outdir
app.outdir / '_static', # inside outdir
]
validate_html_static_path(app, app.config)
assert app.config.html_static_path == ['_static']

View File

@ -1242,7 +1242,7 @@ def test_latex_images(app, status, warning):
# not found images # not found images
assert '\\sphinxincludegraphics{{NOT_EXIST}.PNG}' not in result assert '\\sphinxincludegraphics{{NOT_EXIST}.PNG}' not in result
assert ('WARNING: Could not fetch remote image: ' assert ('WARNING: Could not fetch remote image: '
'http://example.com/NOT_EXIST.PNG [404]' in warning.getvalue()) 'https://www.google.com/NOT_EXIST.PNG [404]' in warning.getvalue())
# an image having target # an image having target
assert ('\\sphinxhref{https://www.sphinx-doc.org/}' assert ('\\sphinxhref{https://www.sphinx-doc.org/}'
@ -1292,25 +1292,15 @@ def test_latex_nested_enumerated_list(app, status, warning):
app.builder.build_all() app.builder.build_all()
result = (app.outdir / 'python.tex').text(encoding='utf8') result = (app.outdir / 'python.tex').text(encoding='utf8')
assert ('\\def\\theenumi{\\arabic{enumi}}\n' assert ('\\sphinxsetlistlabels{\\arabic}{enumi}{enumii}{}{.}%\n'
'\\def\\labelenumi{\\theenumi .}\n'
'\\makeatletter\\def\\p@enumii{\\p@enumi \\theenumi .}\\makeatother\n'
'\\setcounter{enumi}{4}\n' in result) '\\setcounter{enumi}{4}\n' in result)
assert ('\\def\\theenumii{\\alph{enumii}}\n' assert ('\\sphinxsetlistlabels{\\alph}{enumii}{enumiii}{}{.}%\n'
'\\def\\labelenumii{\\theenumii .}\n'
'\\makeatletter\\def\\p@enumiii{\\p@enumii \\theenumii .}\\makeatother\n'
'\\setcounter{enumii}{3}\n' in result) '\\setcounter{enumii}{3}\n' in result)
assert ('\\def\\theenumiii{\\arabic{enumiii}}\n' assert ('\\sphinxsetlistlabels{\\arabic}{enumiii}{enumiv}{}{)}%\n'
'\\def\\labelenumiii{\\theenumiii )}\n'
'\\makeatletter\\def\\p@enumiv{\\p@enumiii \\theenumiii )}\\makeatother\n'
'\\setcounter{enumiii}{9}\n' in result) '\\setcounter{enumiii}{9}\n' in result)
assert ('\\def\\theenumiv{\\arabic{enumiv}}\n' assert ('\\sphinxsetlistlabels{\\arabic}{enumiv}{enumv}{(}{)}%\n'
'\\def\\labelenumiv{(\\theenumiv )}\n'
'\\makeatletter\\def\\p@enumv{\\p@enumiv (\\theenumiv )}\\makeatother\n'
'\\setcounter{enumiv}{23}\n' in result) '\\setcounter{enumiv}{23}\n' in result)
assert ('\\def\\theenumii{\\roman{enumii}}\n' assert ('\\sphinxsetlistlabels{\\roman}{enumii}{enumiii}{}{.}%\n'
'\\def\\labelenumii{\\theenumii .}\n'
'\\makeatletter\\def\\p@enumiii{\\p@enumii \\theenumii .}\\makeatother\n'
'\\setcounter{enumii}{2}\n' in result) '\\setcounter{enumii}{2}\n' in result)
@ -1405,6 +1395,7 @@ def test_latex_figure_in_admonition(app, status, warning):
result = (app.outdir / 'python.tex').text(encoding='utf8') result = (app.outdir / 'python.tex').text(encoding='utf8')
assert(r'\begin{figure}[H]' in result) assert(r'\begin{figure}[H]' in result)
def test_default_latex_documents(): def test_default_latex_documents():
from sphinx.util import texescape from sphinx.util import texescape
texescape.init() texescape.init()

View File

@ -25,8 +25,8 @@ def test_defaults(app, status, warning):
# looking for non-existent URL should fail # looking for non-existent URL should fail
assert " Max retries exceeded with url: /doesnotexist" in content assert " Max retries exceeded with url: /doesnotexist" in content
# images should fail # images should fail
assert "Not Found for url: http://example.com/image.png" in content assert "Not Found for url: https://www.google.com/image.png" in content
assert "Not Found for url: http://example.com/image2.png" in content assert "Not Found for url: https://www.google.com/image2.png" in content
assert len(content.splitlines()) == 5 assert len(content.splitlines()) == 5
@ -36,8 +36,8 @@ def test_defaults(app, status, warning):
'linkcheck_ignore': [ 'linkcheck_ignore': [
'https://localhost:7777/doesnotexist', 'https://localhost:7777/doesnotexist',
'http://www.sphinx-doc.org/en/1.7/intro.html#', 'http://www.sphinx-doc.org/en/1.7/intro.html#',
'http://example.com/image.png', 'https://www.google.com/image.png',
'http://example.com/image2.png'] 'https://www.google.com/image2.png']
}) })
def test_anchors_ignored(app, status, warning): def test_anchors_ignored(app, status, warning):
app.builder.build_all() app.builder.build_all()

View File

@ -11,9 +11,32 @@ import pytest
from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.builders.latex import LaTeXBuilder from sphinx.builders.latex import LaTeXBuilder
from sphinx.environment import CONFIG_OK, CONFIG_CHANGED, CONFIG_EXTENSIONS_CHANGED, CONFIG_NEW
from sphinx.testing.comparer import PathComparer from sphinx.testing.comparer import PathComparer
@pytest.mark.sphinx('dummy', testroot='basic')
def test_config_status(make_app, app_params):
args, kwargs = app_params
# clean build
app1 = make_app(*args, freshenv=True, **kwargs)
assert app1.env.config_status == CONFIG_NEW
app1.build()
# incremental build (no config changed)
app2 = make_app(*args, **kwargs)
assert app2.env.config_status == CONFIG_OK
# incremental build (config entry changed)
app3 = make_app(*args, confoverrides={'master_doc': 'content'}, **kwargs)
assert app3.env.config_status == CONFIG_CHANGED
# incremental build (extension changed)
app4 = make_app(*args, confoverrides={'extensions': ['sphinx.ext.autodoc']}, **kwargs)
assert app4.env.config_status == CONFIG_EXTENSIONS_CHANGED
@pytest.mark.sphinx('dummy') @pytest.mark.sphinx('dummy')
def test_images(app): def test_images(app):
app.build() app.build()

View File

@ -47,19 +47,30 @@ def test_create_pair_index(app):
app.env.indexentries.clear() app.env.indexentries.clear()
text = (".. index:: pair: docutils; reStructuredText\n" text = (".. index:: pair: docutils; reStructuredText\n"
".. index:: pair: Python; interpreter\n" ".. index:: pair: Python; interpreter\n"
".. index:: pair: Sphinx; documentation tool\n") ".. index:: pair: Sphinx; documentation tool\n"
".. index:: pair: Sphinx; :+1:\n"
".. index:: pair: Sphinx; Ель\n"
".. index:: pair: Sphinx; ёлка\n")
restructuredtext.parse(app, text) restructuredtext.parse(app, text)
index = IndexEntries(app.env).create_index(app.builder) index = IndexEntries(app.env).create_index(app.builder)
assert len(index) == 5 assert len(index) == 7
assert index[0] == ('D', assert index[0] == ('Symbols', [(':+1:', [[], [('Sphinx', [('', '#index-3')])], None])])
assert index[1] == ('D',
[('documentation tool', [[], [('Sphinx', [('', '#index-2')])], None]), [('documentation tool', [[], [('Sphinx', [('', '#index-2')])], None]),
('docutils', [[], [('reStructuredText', [('', '#index-0')])], None])]) ('docutils', [[], [('reStructuredText', [('', '#index-0')])], None])])
assert index[1] == ('I', [('interpreter', [[], [('Python', [('', '#index-1')])], None])]) assert index[2] == ('I', [('interpreter', [[], [('Python', [('', '#index-1')])], None])])
assert index[2] == ('P', [('Python', [[], [('interpreter', [('', '#index-1')])], None])]) assert index[3] == ('P', [('Python', [[], [('interpreter', [('', '#index-1')])], None])])
assert index[3] == ('R', assert index[4] == ('R',
[('reStructuredText', [[], [('docutils', [('', '#index-0')])], None])]) [('reStructuredText', [[], [('docutils', [('', '#index-0')])], None])])
assert index[4] == ('S', assert index[5] == ('S',
[('Sphinx', [[], [('documentation tool', [('', '#index-2')])], None])]) [('Sphinx', [[],
[(':+1:', [('', '#index-3')]),
('documentation tool', [('', '#index-2')]),
('ёлка', [('', '#index-5')]),
('Ель', [('', '#index-4')])],
None])])
assert index[6] == ('Е', [('ёлка', [[], [('Sphinx', [('', '#index-5')])], None]),
('Ель', [[], [('Sphinx', [('', '#index-4')])], None])])
@pytest.mark.sphinx('dummy') @pytest.mark.sphinx('dummy')

View File

@ -302,3 +302,17 @@ def test_generate_autosummary_docs_property(app):
".. currentmodule:: target.methods\n" ".. currentmodule:: target.methods\n"
"\n" "\n"
".. autoproperty:: Base.prop") ".. autoproperty:: Base.prop")
@pytest.mark.sphinx('dummy', testroot='ext-autosummary',
confoverrides={'autosummary_generate': []})
def test_empty_autosummary_generate(app, status, warning):
app.build()
assert ("WARNING: autosummary: stub file not found 'autosummary_importfail'"
in warning.getvalue())
@pytest.mark.sphinx('dummy', testroot='ext-autosummary',
confoverrides={'autosummary_generate': ['unknown']})
def test_invalid_autosummary_generate(app, status, warning):
assert 'WARNING: autosummary_generate: file not found: unknown.rst' in warning.getvalue()

View File

@ -18,14 +18,16 @@ def test_githubpages(app, status, warning):
assert not (app.outdir / 'CNAME').exists() assert not (app.outdir / 'CNAME').exists()
@pytest.mark.sphinx('html', testroot='ext-githubpages', confoverrides={'html_baseurl': 'https://sphinx-doc.github.io'}) @pytest.mark.sphinx('html', testroot='ext-githubpages',
confoverrides={'html_baseurl': 'https://sphinx-doc.github.io'})
def test_no_cname_for_github_io_domain(app, status, warning): def test_no_cname_for_github_io_domain(app, status, warning):
app.builder.build_all() app.builder.build_all()
assert (app.outdir / '.nojekyll').exists() assert (app.outdir / '.nojekyll').exists()
assert not (app.outdir / 'CNAME').exists() assert not (app.outdir / 'CNAME').exists()
@pytest.mark.sphinx('html', testroot='ext-githubpages', confoverrides={'html_baseurl': 'https://sphinx-doc.org'}) @pytest.mark.sphinx('html', testroot='ext-githubpages',
confoverrides={'html_baseurl': 'https://sphinx-doc.org'})
def test_cname_for_custom_domain(app, status, warning): def test_cname_for_custom_domain(app, status, warning):
app.builder.build_all() app.builder.build_all()
assert (app.outdir / '.nojekyll').exists() assert (app.outdir / '.nojekyll').exists()

View File

@ -885,8 +885,8 @@ def test_xml_keep_external_links(app):
assert_elem( assert_elem(
para1[0], para1[0],
['LINK TO', 'external2', 'AND', 'external1', '.'], ['LINK TO', 'external2', 'AND', 'external1', '.'],
['http://example.com/external2', ['https://www.google.com/external2',
'http://example.com/external1']) 'https://www.google.com/external1'])
assert_elem( assert_elem(
para1[1], para1[1],
['LINK TO', 'THE PYTHON SITE', 'AND', 'THE SPHINX SITE', '.'], ['LINK TO', 'THE PYTHON SITE', 'AND', 'THE SPHINX SITE', '.'],

View File

@ -279,9 +279,9 @@ def get_verifier(verify, verify_re):
( (
# in URIs # in URIs
'verify_re', 'verify_re',
'`test <http://example.com/~me/>`_', '`test <https://www.google.com/~me/>`_',
None, None,
r'\\sphinxhref{http://example.com/~me/}{test}.*', r'\\sphinxhref{https://www.google.com/~me/}{test}.*',
), ),
( (
# description list: simple # description list: simple

View File

@ -13,7 +13,6 @@ import datetime
import functools import functools
import sys import sys
import types import types
from textwrap import dedent
import pytest import pytest

View File

@ -8,8 +8,6 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import tempfile
from sphinx.testing.util import strip_escseq from sphinx.testing.util import strip_escseq
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.pycompat import execfile_ from sphinx.util.pycompat import execfile_