Merge branch '2.0'

This commit is contained in:
Takeshi KOMIYA 2020-02-09 16:21:32 +09:00
commit 6e0119526a
17 changed files with 200 additions and 62 deletions

62
CHANGES
View File

@ -62,7 +62,7 @@ Bugs fixed
Testing Testing
-------- --------
Release 2.4.0 (in development) Release 2.4.1 (in development)
============================== ==============================
Dependencies Dependencies
@ -74,11 +74,28 @@ Incompatible changes
Deprecated Deprecated
---------- ----------
Features added
--------------
Bugs fixed
----------
Testing
--------
Release 2.4.0 (released Feb 09, 2020)
=====================================
Deprecated
----------
* The ``decode`` argument of ``sphinx.pycode.ModuleAnalyzer()`` * The ``decode`` argument of ``sphinx.pycode.ModuleAnalyzer()``
* ``sphinx.directives.other.Index`` * ``sphinx.directives.other.Index``
* ``sphinx.environment.temp_data['gloss_entries']`` * ``sphinx.environment.temp_data['gloss_entries']``
* ``sphinx.environment.BuildEnvironment.indexentries`` * ``sphinx.environment.BuildEnvironment.indexentries``
* ``sphinx.environment.collectors.indexentries.IndexEntriesCollector`` * ``sphinx.environment.collectors.indexentries.IndexEntriesCollector``
* ``sphinx.ext.apidoc.INITPY``
* ``sphinx.ext.apidoc.shall_skip()``
* ``sphinx.io.FiletypeNotFoundError`` * ``sphinx.io.FiletypeNotFoundError``
* ``sphinx.io.get_filetype()`` * ``sphinx.io.get_filetype()``
* ``sphinx.pycode.ModuleAnalyzer.encoding`` * ``sphinx.pycode.ModuleAnalyzer.encoding``
@ -106,6 +123,8 @@ Features added
* #6446: duration: Add ``sphinx.ext.durations`` to inspect which documents slow * #6446: duration: Add ``sphinx.ext.durations`` to inspect which documents slow
down the build down the build
* #6837: LaTeX: Support a nested table * #6837: LaTeX: Support a nested table
* #7115: LaTeX: Allow to override LATEXOPTS and LATEXMKOPTS via environment
variable
* #6966: graphviz: Support ``:class:`` option * #6966: graphviz: Support ``:class:`` option
* #6696: html: ``:scale:`` option of image/figure directive not working for SVG * #6696: html: ``:scale:`` option of image/figure directive not working for SVG
images (imagesize-1.2.0 or above is required) images (imagesize-1.2.0 or above is required)
@ -133,6 +152,7 @@ Bugs fixed
---------- ----------
* #6925: html: Remove redundant type="text/javascript" from <script> elements * #6925: html: Remove redundant type="text/javascript" from <script> elements
* #7112: html: SVG image is not layouted as float even if aligned
* #6906, #6907: autodoc: failed to read the source codes encoeded in cp1251 * #6906, #6907: autodoc: failed to read the source codes encoeded in cp1251
* #6961: latex: warning for babel shown twice * #6961: latex: warning for babel shown twice
* #7059: latex: LaTeX compilation falls into infinite loop (wrapfig issue) * #7059: latex: LaTeX compilation falls into infinite loop (wrapfig issue)
@ -140,6 +160,7 @@ Bugs fixed
* #6559: Wrong node-ids are generated in glossary directive * #6559: Wrong node-ids are generated in glossary directive
* #6986: apidoc: misdetects module name for .so file inside module * #6986: apidoc: misdetects module name for .so file inside module
* #6899: apidoc: private members are not shown even if ``--private`` given * #6899: apidoc: private members are not shown even if ``--private`` given
* #6327: apidoc: Support a python package consisted of __init__.so file
* #6999: napoleon: fails to parse tilde in :exc: role * #6999: napoleon: fails to parse tilde in :exc: role
* #7019: gettext: Absolute path used in message catalogs * #7019: gettext: Absolute path used in message catalogs
* #7023: autodoc: nested partial functions are not listed * #7023: autodoc: nested partial functions are not listed
@ -153,34 +174,19 @@ Bugs fixed
modifier keys are ignored, which means the feature can interfere with browser modifier keys are ignored, which means the feature can interfere with browser
features features
* #7090: std domain: Can't assign numfig-numbers for custom container nodes * #7090: std domain: Can't assign numfig-numbers for custom container nodes
* #7106: std domain: enumerated nodes are marked as duplicated when extensions
call ``note_explicit_target()``
* #7095: dirhtml: Cross references are broken via intersphinx and ``:doc:`` role
* C++:
Testing - Don't crash when using the ``struct`` role in some cases.
-------- - Don't warn when using the ``var``/``member`` role for function
parameters.
Release 2.3.2 (in development) - Render call and braced-init expressions correctly.
============================== * #7097: Filenames of images generated by
``sphinx.transforms.post_transforms.images.ImageConverter``
Dependencies or its subclasses (used for latex build) are now sanitized,
------------ to prevent broken paths
Incompatible changes
--------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
* C++, don't crash when using the ``struct`` role in some cases.
* C++, don't warn when using the ``var``/``member`` role for function
parameters.
Testing
--------
Release 2.3.1 (released Dec 22, 2019) Release 2.3.1 (released Dec 22, 2019)
===================================== =====================================

View File

@ -71,6 +71,16 @@ The following is a list of deprecated interfaces.
- 4.0 - 4.0
- ``sphinx.errors.FiletypeNotFoundError`` - ``sphinx.errors.FiletypeNotFoundError``
* - ``sphinx.ext.apidoc.INITPY``
- 2.4
- 4.0
- N/A
* - ``sphinx.ext.apidoc.shall_skip()``
- 2.4
- 4.0
- ``sphinx.ext.apidoc.is_skipped_package``
* - ``sphinx.io.get_filetype()`` * - ``sphinx.io.get_filetype()``
- 2.4 - 2.4
- 4.0 - 4.0

View File

@ -3007,7 +3007,7 @@ class ASTParenExprList(ASTBase):
signode.append(nodes.Text(', ')) signode.append(nodes.Text(', '))
else: else:
first = False first = False
e.describe_signature(signode, mode, env, symbol) e.describe_signature(signode, mode, env, symbol)
signode.append(nodes.Text(')')) signode.append(nodes.Text(')'))
@ -3034,7 +3034,7 @@ class ASTBracedInitList(ASTBase):
signode.append(nodes.Text(', ')) signode.append(nodes.Text(', '))
else: else:
first = False first = False
e.describe_signature(signode, mode, env, symbol) e.describe_signature(signode, mode, env, symbol)
if self.trailingComma: if self.trailingComma:
signode.append(nodes.Text(',')) signode.append(nodes.Text(','))
signode.append(nodes.Text('}')) signode.append(nodes.Text('}'))

View File

@ -29,7 +29,7 @@ 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
from sphinx.cmd.quickstart import EXTENSIONS from sphinx.cmd.quickstart import EXTENSIONS
from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import rst from sphinx.util import rst
from sphinx.util.osutil import FileAvoidWrite, ensuredir from sphinx.util.osutil import FileAvoidWrite, ensuredir
@ -46,7 +46,6 @@ else:
'show-inheritance', 'show-inheritance',
] ]
INITPY = '__init__.py'
PY_SUFFIXES = ('.py', '.pyx') + tuple(EXTENSION_SUFFIXES) PY_SUFFIXES = ('.py', '.pyx') + tuple(EXTENSION_SUFFIXES)
template_dir = path.join(package_dir, 'templates', 'apidoc') template_dir = path.join(package_dir, 'templates', 'apidoc')
@ -66,11 +65,31 @@ def makename(package: str, module: str) -> str:
return name return name
def is_initpy(filename: str) -> bool:
"""Check *filename* is __init__ file or not."""
basename = path.basename(filename)
for suffix in sorted(PY_SUFFIXES, key=len, reverse=True):
if basename == '__init__' + suffix:
return True
else:
return False
def module_join(*modnames: str) -> str: def module_join(*modnames: str) -> str:
"""Join module names with dots.""" """Join module names with dots."""
return '.'.join(filter(None, modnames)) return '.'.join(filter(None, modnames))
def is_packagedir(dirname: str = None, files: List[str] = None) -> bool:
"""Check given *files* contains __init__ file."""
if files is None and dirname is None:
return False
if files is None:
files = os.listdir(dirname)
return any(f for f in files if is_initpy(f))
def write_file(name: str, text: str, opts: Any) -> None: def write_file(name: str, text: str, opts: Any) -> None:
"""Write the output file for module/package <name>.""" """Write the output file for module/package <name>."""
quiet = getattr(opts, 'quiet', None) quiet = getattr(opts, 'quiet', None)
@ -132,15 +151,14 @@ def create_package_file(root: str, master_package: str, subroot: str, py_files:
opts: Any, subs: List[str], is_namespace: bool, opts: Any, subs: List[str], is_namespace: bool,
excludes: List[str] = [], user_template_dir: str = None) -> None: 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 __init__ file)
subpackages = [sub for sub in subs if not
shall_skip(path.join(root, sub, INITPY), opts, excludes)]
subpackages = [module_join(master_package, subroot, pkgname) subpackages = [module_join(master_package, subroot, pkgname)
for pkgname in subpackages] for pkgname in subs
if not is_skipped_package(path.join(root, pkgname), opts, excludes)]
# build a list of sub modules # build a list of sub modules
submodules = [sub.split('.')[0] for sub in py_files submodules = [sub.split('.')[0] for sub in py_files
if not is_skipped_module(path.join(root, sub), opts, excludes) and if not is_skipped_module(path.join(root, sub), opts, excludes) and
sub != INITPY] not is_initpy(sub)]
submodules = [module_join(master_package, subroot, modname) submodules = [module_join(master_package, subroot, modname)
for modname in submodules] for modname in submodules]
options = copy(OPTIONS) options = copy(OPTIONS)
@ -189,12 +207,14 @@ def create_modules_toc_file(modules: List[str], opts: Any, name: str = 'modules'
def shall_skip(module: str, opts: Any, excludes: List[str] = []) -> bool: def shall_skip(module: str, opts: Any, excludes: List[str] = []) -> bool:
"""Check if we want to skip this module.""" """Check if we want to skip this module."""
warnings.warn('shall_skip() is deprecated.',
RemovedInSphinx40Warning)
# 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):
return True return True
# Are we a package (here defined as __init__.py, not the folder in itself) # Are we a package (here defined as __init__.py, not the folder in itself)
if os.path.basename(module) == INITPY: if is_initpy(module):
# Yes, check if we have any non-excluded modules at all here # Yes, check if we have any non-excluded modules at all here
all_skipped = True all_skipped = True
basemodule = path.dirname(module) basemodule = path.dirname(module)
@ -207,12 +227,30 @@ def shall_skip(module: str, opts: Any, excludes: List[str] = []) -> bool:
# skip if it has a "private" name and this is selected # skip if it has a "private" name and this is selected
filename = path.basename(module) filename = path.basename(module)
if filename != '__init__.py' and filename.startswith('_') and \ if is_initpy(filename) and filename.startswith('_') and not opts.includeprivate:
not opts.includeprivate:
return True return True
return False return False
def is_skipped_package(dirname: str, opts: Any, excludes: List[str] = []) -> bool:
"""Check if we want to skip this module."""
if not path.isdir(dirname):
return False
files = glob.glob(path.join(dirname, '*.py'))
regular_package = any(f for f in files if is_initpy(f))
if not regular_package and not opts.implicit_namespaces:
# *dirname* is not both a regular package and an implicit namespace pacage
return True
# Check there is some showable module inside package
if all(is_excluded(path.join(dirname, f), excludes) for f in files):
# all submodules are excluded
return True
else:
return False
def is_skipped_module(filename: str, opts: Any, excludes: List[str]) -> bool: def is_skipped_module(filename: str, opts: Any, excludes: 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):
@ -236,7 +274,7 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any,
implicit_namespaces = getattr(opts, 'implicit_namespaces', False) implicit_namespaces = getattr(opts, 'implicit_namespaces', False)
# check if the base directory is a package and get its name # check if the base directory is a package and get its name
if INITPY in os.listdir(rootpath) or implicit_namespaces: if is_packagedir(rootpath) or implicit_namespaces:
root_package = rootpath.split(path.sep)[-1] root_package = rootpath.split(path.sep)[-1]
else: else:
# otherwise, the base is a directory with packages # otherwise, the base is a directory with packages
@ -248,11 +286,13 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any,
py_files = sorted(f for f in files py_files = sorted(f for f in files
if f.endswith(PY_SUFFIXES) and if f.endswith(PY_SUFFIXES) and
not is_excluded(path.join(root, f), excludes)) not is_excluded(path.join(root, f), excludes))
is_pkg = INITPY in py_files is_pkg = is_packagedir(None, py_files)
is_namespace = INITPY not in py_files and implicit_namespaces is_namespace = not is_pkg and implicit_namespaces
if is_pkg: if is_pkg:
py_files.remove(INITPY) for f in py_files[:]:
py_files.insert(0, INITPY) if is_initpy(f):
py_files.remove(f)
py_files.insert(0, f)
elif root != rootpath: elif root != rootpath:
# only accept non-package at toplevel unless using implicit namespaces # only accept non-package at toplevel unless using implicit namespaces
if not implicit_namespaces: if not implicit_namespaces:
@ -269,7 +309,7 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any,
if is_pkg or is_namespace: if is_pkg or is_namespace:
# we are in a package with something to document # we are in a package with something to document
if subs or len(py_files) > 1 or not shall_skip(path.join(root, INITPY), opts): if subs or len(py_files) > 1 or not is_skipped_package(root, opts):
subpackage = root[len(rootpath):].lstrip(path.sep).\ subpackage = root[len(rootpath):].lstrip(path.sep).\
replace(path.sep, '.') replace(path.sep, '.')
# if this is not a namespace or # if this is not a namespace or
@ -475,6 +515,13 @@ def main(argv: List[str] = sys.argv[1:]) -> int:
return 0 return 0
deprecated_alias('sphinx.ext.apidoc',
{
'INITPY': '__init__.py',
},
RemovedInSphinx40Warning)
# So program can be started with "python -m sphinx.apidoc ..." # So program can be started with "python -m sphinx.apidoc ..."
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -14,13 +14,13 @@ ALLPS = $(addsuffix .ps,$(ALLDOCS))
# Prefix for archive names # Prefix for archive names
ARCHIVEPREFIX = ARCHIVEPREFIX =
# Additional LaTeX options (passed via variables in latexmkrc/latexmkjarc file) # Additional LaTeX options (passed via variables in latexmkrc/latexmkjarc file)
export LATEXOPTS = export LATEXOPTS ?=
# Additional latexmk options # Additional latexmk options
{% if latex_engine == 'xelatex' -%} {% if latex_engine == 'xelatex' -%}
# with latexmk version 4.52b or higher set LATEXMKOPTS to -xelatex either here # with latexmk version 4.52b or higher set LATEXMKOPTS to -xelatex either here
# or on command line for faster builds. # or on command line for faster builds.
{% endif -%} {% endif -%}
LATEXMKOPTS = LATEXMKOPTS ?=
{% if xindy_use -%} {% if xindy_use -%}
export XINDYOPTS = {{ xindy_lang_option }} -M sphinx.xdy export XINDYOPTS = {{ xindy_lang_option }} -M sphinx.xdy
{% if latex_engine == 'pdflatex' -%} {% if latex_engine == 'pdflatex' -%}

View File

@ -173,7 +173,9 @@ class AutoNumbering(SphinxTransform):
domain = self.env.get_domain('std') # type: StandardDomain domain = self.env.get_domain('std') # type: StandardDomain
for node in self.document.traverse(nodes.Element): for node in self.document.traverse(nodes.Element):
if domain.is_enumerable_node(node) and domain.get_numfig_title(node) is not None: if (domain.is_enumerable_node(node) and
domain.get_numfig_title(node) is not None and
node['ids'] == []):
self.document.note_implicit_target(node) self.document.note_implicit_target(node)

View File

@ -9,6 +9,7 @@
""" """
import os import os
import re
from hashlib import sha1 from hashlib import sha1
from math import ceil from math import ceil
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
@ -27,6 +28,7 @@ from sphinx.util.osutil import ensuredir, movefile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_FILENAME_LEN = 32 MAX_FILENAME_LEN = 32
CRITICAL_PATH_CHAR_RE = re.compile('[:;<>|*" ]')
class BaseImageConverter(SphinxTransform): class BaseImageConverter(SphinxTransform):
@ -65,6 +67,7 @@ class ImageDownloader(BaseImageConverter):
if basename == '' or len(basename) > MAX_FILENAME_LEN: if basename == '' or len(basename) > MAX_FILENAME_LEN:
filename, ext = os.path.splitext(node['uri']) filename, ext = os.path.splitext(node['uri'])
basename = sha1(filename.encode()).hexdigest() + ext basename = sha1(filename.encode()).hexdigest() + ext
basename = re.sub(CRITICAL_PATH_CHAR_RE, "_", basename)
dirname = node['uri'].replace('://', '/').translate({ord("?"): "/", dirname = node['uri'].replace('://', '/').translate({ord("?"): "/",
ord("&"): "/"}) ord("&"): "/"})
@ -146,6 +149,7 @@ class DataURIExtractor(BaseImageConverter):
def get_filename_for(filename: str, mimetype: str) -> str: def get_filename_for(filename: str, mimetype: str) -> str:
basename = os.path.basename(filename) basename = os.path.basename(filename)
basename = re.sub(CRITICAL_PATH_CHAR_RE, "_", basename)
return os.path.splitext(basename)[0] + get_image_extension(mimetype) return os.path.splitext(basename)[0] + get_image_extension(mimetype)

View File

@ -122,7 +122,7 @@ class InventoryFile:
for line in stream.read_compressed_lines(): for line in stream.read_compressed_lines():
# be careful to handle names with embedded spaces correctly # be careful to handle names with embedded spaces correctly
m = re.match(r'(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+(\S+)\s+(.*)', m = re.match(r'(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+?(\S*)\s+(.*)',
line.rstrip()) line.rstrip())
if not m: if not m:
continue continue

View File

@ -608,11 +608,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator):
atts['height'] = int(atts['height']) * scale atts['height'] = int(atts['height']) * scale
atts['alt'] = node.get('alt', uri) atts['alt'] = node.get('alt', uri)
if 'align' in node: if 'align' in node:
self.body.append('<div align="%s" class="align-%s">' % atts['class'] = 'align-%s' % node['align']
(node['align'], node['align']))
self.context.append('</div>\n')
else:
self.context.append('')
self.body.append(self.emptytag(node, 'img', '', **atts)) self.body.append(self.emptytag(node, 'img', '', **atts))
return return
@ -621,7 +617,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator):
# overwritten # overwritten
def depart_image(self, node: Element) -> None: def depart_image(self, node: Element) -> None:
if node['uri'].lower().endswith(('svg', 'svgz')): if node['uri'].lower().endswith(('svg', 'svgz')):
self.body.append(self.context.pop()) pass
else: else:
super().depart_image(node) super().depart_image(node)

View File

@ -549,11 +549,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
atts['height'] = int(atts['height']) * scale atts['height'] = int(atts['height']) * scale
atts['alt'] = node.get('alt', uri) atts['alt'] = node.get('alt', uri)
if 'align' in node: if 'align' in node:
self.body.append('<div align="%s" class="align-%s">' % atts['class'] = 'align-%s' % node['align']
(node['align'], node['align']))
self.context.append('</div>\n')
else:
self.context.append('')
self.body.append(self.emptytag(node, 'img', '', **atts)) self.body.append(self.emptytag(node, 'img', '', **atts))
return return
@ -562,7 +558,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
# overwritten # overwritten
def depart_image(self, node: Element) -> None: def depart_image(self, node: Element) -> None:
if node['uri'].lower().endswith(('svg', 'svgz')): if node['uri'].lower().endswith(('svg', 'svgz')):
self.body.append(self.context.pop()) pass
else: else:
super().depart_image(node) super().depart_image(node)

View File

@ -0,0 +1,4 @@
.. _bar:
bar
===

View File

View File

@ -0,0 +1,4 @@
.. _foo_1:
foo/foo_1
=========

View File

@ -0,0 +1,4 @@
.. _foo_2:
foo/foo_2
=========

View File

@ -0,0 +1,9 @@
.. _foo:
foo/index
=========
.. toctree::
foo_1
foo_2

View File

@ -0,0 +1,9 @@
.. _index:
index
=====
.. toctree::
foo/index
bar

View File

@ -0,0 +1,47 @@
"""
test_build_dirhtml
~~~~~~~~~~~~~~~~~~
Test dirhtml builder.
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import posixpath
import pytest
from sphinx.util.inventory import InventoryFile
@pytest.mark.sphinx(buildername='dirhtml', testroot='builder-dirhtml')
def test_dirhtml(app, status, warning):
app.build()
assert (app.outdir / 'index.html').exists()
assert (app.outdir / 'foo/index.html').exists()
assert (app.outdir / 'foo/foo_1/index.html').exists()
assert (app.outdir / 'foo/foo_2/index.html').exists()
assert (app.outdir / 'bar/index.html').exists()
content = (app.outdir / 'index.html').text()
assert 'href="foo/"' in content
assert 'href="foo/foo_1/"' in content
assert 'href="foo/foo_2/"' in content
assert 'href="bar/"' in content
# objects.inv (refs: #7095)
f = (app.outdir / 'objects.inv').open('rb')
invdata = InventoryFile.load(f, 'path/to', posixpath.join)
assert 'index' in invdata.get('std:doc')
assert ('Python', '', 'path/to/', '-') == invdata['std:doc']['index']
assert 'foo/index' in invdata.get('std:doc')
assert ('Python', '', 'path/to/foo/', '-') == invdata['std:doc']['foo/index']
assert 'index' in invdata.get('std:label')
assert ('Python', '', 'path/to/#index', '-') == invdata['std:label']['index']
assert 'foo' in invdata.get('std:label')
assert ('Python', '', 'path/to/foo/#foo', 'foo/index') == invdata['std:label']['foo']