summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakeshi KOMIYA <i.tkomiya@gmail.com>2019-05-13 22:03:26 +0900
committerTakeshi KOMIYA <i.tkomiya@gmail.com>2019-05-13 22:03:26 +0900
commitf63abac2cad2664a8af816017f0f997bae510d14 (patch)
tree27d641b3893424b1d3226c495ede57d3a6f85a6e
parent165897a74951fb03e497d6e05496ce02e897f820 (diff)
parent3e5adaa2252326681d33c09c1585e83e54072ffb (diff)
downloadsphinx-git-f63abac2cad2664a8af816017f0f997bae510d14.tar.gz
Merge branch '2.0'
-rw-r--r--CHANGES17
-rw-r--r--doc/develop.rst10
-rw-r--r--doc/extdev/index.rst3
-rw-r--r--doc/extdev/markupapi.rst5
-rw-r--r--doc/templating.rst9
-rw-r--r--doc/usage/restructuredtext/domains.rst54
-rw-r--r--setup.cfg5
-rw-r--r--sphinx/domains/python.py20
-rw-r--r--sphinx/domains/rst.py76
-rw-r--r--sphinx/ext/autodoc/__init__.py43
-rw-r--r--sphinx/ext/autodoc/directive.py22
-rw-r--r--sphinx/ext/autosummary/__init__.py4
-rw-r--r--sphinx/ext/autosummary/generate.py18
-rw-r--r--sphinx/highlighting.py42
-rw-r--r--sphinx/io.py11
-rw-r--r--sphinx/templates/latex/longtable.tex_t2
-rw-r--r--sphinx/templates/latex/tabular.tex_t2
-rw-r--r--sphinx/templates/latex/tabulary.tex_t2
-rw-r--r--sphinx/themes/basic/searchbox.html4
-rw-r--r--sphinx/themes/basic/static/basic.css_t15
-rw-r--r--sphinx/transforms/__init__.py2
-rw-r--r--sphinx/transforms/references.py19
-rw-r--r--sphinx/util/docstrings.py6
-rw-r--r--sphinx/util/inspect.py6
-rw-r--r--sphinx/writers/latex.py1
-rw-r--r--sphinx/writers/manpage.py2
-rw-r--r--tests/test_autodoc.py32
-rw-r--r--tests/test_build_html.py120
-rw-r--r--tests/test_build_manpage.py7
-rw-r--r--tests/test_domain_py.py15
-rw-r--r--tests/test_domain_rst.py63
-rw-r--r--tests/test_ext_graphviz.py4
-rw-r--r--tests/test_ext_inheritance_diagram.py6
-rw-r--r--tests/test_theming.py2
-rw-r--r--tests/test_util_inspect.py11
-rw-r--r--tox.ini9
-rw-r--r--utils/doclinter.py59
37 files changed, 576 insertions, 152 deletions
diff --git a/CHANGES b/CHANGES
index fa1b80ec9..d2a797046 100644
--- a/CHANGES
+++ b/CHANGES
@@ -34,6 +34,7 @@ Incompatible changes
API directly
* #6230: The anchor of term in glossary directive is changed if it is consisted
by non-ASCII characters
+* #4550: html: Centering tables by default using CSS
Deprecated
----------
@@ -109,8 +110,16 @@ Features added
imported members on autosummary
* #6271: ``make clean`` is catastrophically broken if building into '.'
* #4777: py domain: Add ``:async:`` option to :rst:dir:`py:function` directive
-* py domain: Add ``:async:``, ``:classmethod:`` and ``:staticmethod:`` options
- to :rst:dir:`py:method` directive
+* py domain: Add new options to :rst:dir:`py:method` directive
+
+ - ``:async:``
+ - ``:classmethod:``
+ - ``:property:``
+ - ``:staticmethod:``
+
+* rst domain: Add :rst:dir:`directive:option` directive to describe the option
+ for directive
+* #6306: html: Add a label to search form for accessability purposes
Bugs fixed
----------
@@ -120,6 +129,10 @@ Bugs fixed
* #6213: ifconfig: contents after headings are not shown
* commented term in glossary directive is wrongly recognized
* #6299: rst domain: rst:directive directive generates waste space
+* #6331: man: invalid output when doctest follows rubric
+* #6351: "Hyperlink target is not referenced" message is shown even if
+ referenced
+* #6165: autodoc: ``tab_width`` setting of docutils has been ignored
* Generated Makefiles lack a final EOL (refs: #6232)
Testing
diff --git a/doc/develop.rst b/doc/develop.rst
index d061aae61..3828b709d 100644
--- a/doc/develop.rst
+++ b/doc/develop.rst
@@ -31,7 +31,8 @@ This is the current list of contributed extensions in that repository:
- actdiag: embed activity diagrams by using actdiag_
- adadomain: an extension for Ada support (Sphinx 1.0 needed)
- ansi: parse ANSI color sequences inside documents
-- argdoc: automatically generate documentation for command-line arguments, descriptions, and help text
+- argdoc: automatically generate documentation for command-line arguments,
+ descriptions and help text
- astah: embed diagram by using astah
- autoanysrc: Gather reST documentation from any source files
- autorun: Execute code in a ``runblock`` directive
@@ -64,7 +65,8 @@ This is the current list of contributed extensions in that repository:
- imgur: embed Imgur images, albums, and metadata in documents
- inlinesyntaxhighlight_: inline syntax highlighting
- lassodomain: a domain for documenting Lasso_ source code
-- libreoffice: an extension to include any drawing supported by LibreOffice (e.g. odg, vsd, ...)
+- libreoffice: an extension to include any drawing supported by LibreOffice
+ (e.g. odg, vsd, ...)
- lilypond: an extension inserting music scripts from Lilypond_ in PNG format
- makedomain_: a domain for `GNU Make`_
- matlabdomain: document MATLAB_ code
@@ -100,8 +102,8 @@ This is the current list of contributed extensions in that repository:
- zopeext: provide an ``autointerface`` directive for using `Zope interfaces`_
-See the :doc:`extension tutorials <../development/tutorials/index>` on getting started with writing your
-own extensions.
+See the :doc:`extension tutorials <../development/tutorials/index>` on getting
+started with writing your own extensions.
.. _aafigure: https://launchpad.net/aafigure
diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst
index eac4ded40..c70ca37be 100644
--- a/doc/extdev/index.rst
+++ b/doc/extdev/index.rst
@@ -97,7 +97,8 @@ extension. These are:
The config is available as ``app.config`` or ``env.config``.
-To see an example of use of these objects, refer to :doc:`../development/tutorials/index`.
+To see an example of use of these objects, refer to
+:doc:`../development/tutorials/index`.
.. _build-phases:
diff --git a/doc/extdev/markupapi.rst b/doc/extdev/markupapi.rst
index ffa08cae7..fc25c2327 100644
--- a/doc/extdev/markupapi.rst
+++ b/doc/extdev/markupapi.rst
@@ -147,5 +147,6 @@ return ``node.children`` from the Directive.
.. seealso::
- `Creating directives <http://docutils.sourceforge.net/docs/howto/rst-directives.html>`_
- HOWTO of the Docutils documentation
+ `Creating directives`_ HOWTO of the Docutils documentation
+
+.. _Creating directives: http://docutils.sourceforge.net/docs/howto/rst-directives.html
diff --git a/doc/templating.rst b/doc/templating.rst
index b3a26c4b1..3790275f5 100644
--- a/doc/templating.rst
+++ b/doc/templating.rst
@@ -354,8 +354,8 @@ are in HTML form), these variables are also available:
.. data:: body
- A string containing the content of the page in HTML form as produced by the HTML builder,
- before the theme is applied.
+ A string containing the content of the page in HTML form as produced by the
+ HTML builder, before the theme is applied.
.. data:: display_toc
@@ -382,8 +382,9 @@ are in HTML form), these variables are also available:
.. data:: page_source_suffix
- The suffix of the file that was rendered. Since we support a list of :confval:`source_suffix`,
- this will allow you to properly link to the original source file.
+ The suffix of the file that was rendered. Since we support a list of
+ :confval:`source_suffix`, this will allow you to properly link to the
+ original source file.
.. data:: parents
diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst
index 10fbf6f6f..8b06bf0e0 100644
--- a/doc/usage/restructuredtext/domains.rst
+++ b/doc/usage/restructuredtext/domains.rst
@@ -229,9 +229,13 @@ The following directives are provided for module and class contents:
The ``classmethod`` option and ``staticmethod`` option can be given (with
no value) to indicate the method is a class method (or a static method).
+ The ``property`` option can be given (with no value) to indicate the method
+ is a property.
+
.. versionchanged:: 2.1
- ``:async:``, ``:classmethod:`` and ``:staticmethod:`` options added.
+ ``:async:``, ``:classmethod:``, ``:property:`` and ``:staticmethod:``
+ options added.
.. rst:directive:: .. py:staticmethod:: name(parameters)
@@ -1079,15 +1083,16 @@ These roles link to the given declaration types:
.. admonition:: Note on References with Templates Parameters/Arguments
- These roles follow the Sphinx :ref:`xref-syntax` rules. This means care must be
- taken when referencing a (partial) template specialization, e.g. if the link looks like
- this: ``:cpp:class:`MyClass<int>```.
+ These roles follow the Sphinx :ref:`xref-syntax` rules. This means care must
+ be taken when referencing a (partial) template specialization, e.g. if the
+ link looks like this: ``:cpp:class:`MyClass<int>```.
This is interpreted as a link to ``int`` with a title of ``MyClass``.
In this case, escape the opening angle bracket with a backslash,
like this: ``:cpp:class:`MyClass\<int>```.
- When a custom title is not needed it may be useful to use the roles for inline expressions,
- :rst:role:`cpp:expr` and :rst:role:`cpp:texpr`, where angle brackets do not need escaping.
+ When a custom title is not needed it may be useful to use the roles for
+ inline expressions, :rst:role:`cpp:expr` and :rst:role:`cpp:texpr`, where
+ angle brackets do not need escaping.
Declarations without template parameters and template arguments
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -1419,6 +1424,43 @@ The reStructuredText domain (name **rst**) provides the following directives:
Bar description.
+.. rst:directive:: .. rst:directive:option:: name
+
+ Describes an option for reST directive. The *name* can be a single option
+ name or option name with arguments which separated with colon (``:``).
+ For example::
+
+ .. rst:directive:: toctree
+
+ .. rst:directive:option:: caption: caption of ToC
+
+ .. rst:directive:option:: glob
+
+ will be rendered as:
+
+ .. rst:directive:: toctree
+ :noindex:
+
+ .. rst:directive:option:: caption: caption of ToC
+
+ .. rst:directive:option:: glob
+
+ .. rubric:: options
+
+ .. rst:directive:option:: type
+ :type: description for the option of directive
+
+ Describe the type of option value.
+
+ For example::
+
+ .. rst:directive:: toctree
+
+ .. rst:directive:option:: maxdepth
+ :type: integer or no value
+
+ .. versionadded:: 2.1
+
.. rst:directive:: .. rst:role:: name
Describes a reST role. For example::
diff --git a/setup.cfg b/setup.cfg
index 2db007339..c91a31879 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -55,6 +55,11 @@ strict_optional = False
filterwarnings =
all
ignore::DeprecationWarning:docutils.io
+markers =
+ sphinx
+ apidoc
+ setup_command
+ test_params
[coverage:run]
branch = True
diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py
index c0bef26a5..4bfaaf848 100644
--- a/sphinx/domains/python.py
+++ b/sphinx/domains/python.py
@@ -578,22 +578,28 @@ class PyMethod(PyObject):
option_spec.update({
'async': directives.flag,
'classmethod': directives.flag,
+ 'property': directives.flag,
'staticmethod': directives.flag,
})
def needs_arglist(self):
# type: () -> bool
- return True
+ if 'property' in self.options:
+ return False
+ else:
+ return True
def get_signature_prefix(self, sig):
# type: (str) -> str
prefix = []
if 'async' in self.options:
prefix.append('async')
- if 'staticmethod' in self.options:
- prefix.append('static')
if 'classmethod' in self.options:
prefix.append('classmethod')
+ if 'property' in self.options:
+ prefix.append('property')
+ if 'staticmethod' in self.options:
+ prefix.append('static')
if prefix:
return ' '.join(prefix) + ' '
@@ -613,10 +619,12 @@ class PyMethod(PyObject):
else:
return '%s()' % name
- if 'staticmethod' in self.options:
- return _('%s() (%s static method)') % (methname, clsname)
- elif 'classmethod' in self.options:
+ if 'classmethod' in self.options:
return _('%s() (%s class method)') % (methname, clsname)
+ elif 'property' in self.options:
+ return _('%s() (%s property)') % (methname, clsname)
+ elif 'staticmethod' in self.options:
+ return _('%s() (%s static method)') % (methname, clsname)
else:
return _('%s() (%s method)') % (methname, clsname)
diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py
index bccad0628..f054abf28 100644
--- a/sphinx/domains/rst.py
+++ b/sphinx/domains/rst.py
@@ -11,6 +11,8 @@
import re
from typing import cast
+from docutils.parsers.rst import directives
+
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType
@@ -98,6 +100,74 @@ class ReSTDirective(ReSTMarkup):
# type: (str, str) -> str
return _('%s (directive)') % name
+ def before_content(self):
+ # type: () -> None
+ if self.names:
+ directives = self.env.ref_context.setdefault('rst:directives', [])
+ directives.append(self.names[0])
+
+ def after_content(self):
+ # type: () -> None
+ directives = self.env.ref_context.setdefault('rst:directives', [])
+ if directives:
+ directives.pop()
+
+
+class ReSTDirectiveOption(ReSTMarkup):
+ """
+ Description of an option for reST directive.
+ """
+ option_spec = ReSTMarkup.option_spec.copy()
+ option_spec.update({
+ 'type': directives.unchanged,
+ })
+
+ def handle_signature(self, sig, signode):
+ # type: (str, addnodes.desc_signature) -> str
+ try:
+ name, argument = re.split(r'\s*:\s+', sig.strip(), 1)
+ except ValueError:
+ name, argument = sig, None
+
+ signode += addnodes.desc_name(':%s:' % name, ':%s:' % name)
+ if argument:
+ signode += addnodes.desc_annotation(' ' + argument, ' ' + argument)
+ if self.options.get('type'):
+ text = ' (%s)' % self.options['type']
+ signode += addnodes.desc_annotation(text, text)
+ return name
+
+ def add_target_and_index(self, name, sig, signode):
+ # type: (str, str, addnodes.desc_signature) -> None
+ targetname = '-'.join([self.objtype, self.current_directive, name])
+ if targetname not in self.state.document.ids:
+ signode['names'].append(targetname)
+ signode['ids'].append(targetname)
+ signode['first'] = (not self.names)
+ self.state.document.note_explicit_target(signode)
+
+ domain = cast(ReSTDomain, self.env.get_domain('rst'))
+ domain.note_object(self.objtype, name, location=(self.env.docname, self.lineno))
+
+ if self.current_directive:
+ key = name[0].upper()
+ pair = [_('%s (directive)') % self.current_directive,
+ _(':%s: (directive option)') % name]
+ self.indexnode['entries'].append(('pair', '; '.join(pair), targetname, '', key))
+ else:
+ key = name[0].upper()
+ text = _(':%s: (directive option)') % name
+ self.indexnode['entries'].append(('single', text, targetname, '', key))
+
+ @property
+ def current_directive(self):
+ # type: () -> str
+ directives = self.env.ref_context.get('rst:directives')
+ if directives:
+ return directives[-1]
+ else:
+ return ''
+
class ReSTRole(ReSTMarkup):
"""
@@ -119,11 +189,13 @@ class ReSTDomain(Domain):
label = 'reStructuredText'
object_types = {
- 'directive': ObjType(_('directive'), 'dir'),
- 'role': ObjType(_('role'), 'role'),
+ 'directive': ObjType(_('directive'), 'dir'),
+ 'directive:option': ObjType(_('directive-option'), 'dir'),
+ 'role': ObjType(_('role'), 'role'),
}
directives = {
'directive': ReSTDirective,
+ 'directive:option': ReSTDirectiveOption,
'role': ReSTRole,
}
roles = {
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 6343022c2..b3c04e464 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -440,7 +440,8 @@ class Documenter:
docstring = getdoc(self.object, self.get_attr,
self.env.config.autodoc_inherit_docstrings)
if docstring:
- return [prepare_docstring(docstring, ignore)]
+ tab_width = self.directive.state.document.settings.tab_width
+ return [prepare_docstring(docstring, ignore, tab_width)]
return []
def process_doc(self, docstrings):
@@ -934,7 +935,9 @@ class DocstringSignatureMixin:
if base not in valid_names:
continue
# re-prepare docstring to ignore more leading indentation
- self._new_docstrings[i] = prepare_docstring('\n'.join(doclines[1:]))
+ tab_width = self.directive.state.document.settings.tab_width # type: ignore
+ self._new_docstrings[i] = prepare_docstring('\n'.join(doclines[1:]),
+ tabsize=tab_width)
result = args, retann
# don't look any further
break
@@ -1177,7 +1180,9 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
docstrings = [initdocstring]
else:
docstrings.append(initdocstring)
- return [prepare_docstring(docstring, ignore) for docstring in docstrings]
+
+ tab_width = self.directive.state.document.settings.tab_width
+ return [prepare_docstring(docstring, ignore, tab_width) for docstring in docstrings]
def add_content(self, more_content, no_docstring=False):
# type: (Any, bool) -> None
@@ -1413,6 +1418,37 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
super().add_content(more_content, no_docstring)
+class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore
+ """
+ Specialized Documenter subclass for properties.
+ """
+ objtype = 'property'
+ directivetype = 'method'
+ member_order = 60
+
+ # before AttributeDocumenter
+ priority = AttributeDocumenter.priority + 1
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ # type: (Any, str, bool, Any) -> bool
+ return inspect.isproperty(member) and isinstance(parent, ClassDocumenter)
+
+ def document_members(self, all_members=False):
+ # type: (bool) -> None
+ pass
+
+ def get_real_modname(self):
+ # type: () -> str
+ return self.get_attr(self.parent or self.object, '__module__', None) \
+ or self.modname
+
+ def add_directive_header(self, sig):
+ # type: (str) -> None
+ super().add_directive_header(sig)
+ self.add_line(' :property:', self.get_sourcename())
+
+
class InstanceAttributeDocumenter(AttributeDocumenter):
"""
Specialized Documenter subclass for attributes that cannot be imported
@@ -1471,6 +1507,7 @@ def setup(app):
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)
app.add_autodocumenter(AttributeDocumenter)
+ app.add_autodocumenter(PropertyDocumenter)
app.add_autodocumenter(InstanceAttributeDocumenter)
app.add_config_value('autoclass_content', 'class', True)
diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py
index 42415433b..6b002b101 100644
--- a/sphinx/ext/autodoc/directive.py
+++ b/sphinx/ext/autodoc/directive.py
@@ -6,10 +6,14 @@
:license: BSD, see LICENSE for details.
"""
+import warnings
+
from docutils import nodes
+from docutils.parsers.rst.states import Struct
from docutils.statemachine import StringList
from docutils.utils import assemble_option_dict
+from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.ext.autodoc import Options, get_documenters
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective, switch_source_input
@@ -17,7 +21,7 @@ from sphinx.util.nodes import nested_parse_with_titles
if False:
# For type annotation
- from typing import Callable, Dict, List, Set, Type # NOQA
+ 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
@@ -50,8 +54,8 @@ class DummyOptionSpec(dict):
class DocumenterBridge:
"""A parameters container for Documenters."""
- def __init__(self, env, reporter, options, lineno):
- # type: (BuildEnvironment, Reporter, Options, int) -> None
+ def __init__(self, env, reporter, options, lineno, state=None):
+ # type: (BuildEnvironment, Reporter, Options, int, Any) -> None
self.env = env
self.reporter = reporter
self.genopt = options
@@ -59,6 +63,16 @@ class DocumenterBridge:
self.filename_set = set() # type: Set[str]
self.result = StringList()
+ if state:
+ self.state = state
+ else:
+ # create fake object for self.state.document.settings.tab_width
+ warnings.warn('DocumenterBridge requires a state object on instantiation.',
+ RemovedInSphinx40Warning)
+ settings = Struct(tab_width=8)
+ document = Struct(settings=settings)
+ self.state = Struct(document=document)
+
def warn(self, msg):
# type: (str) -> None
logger.warning(msg, location=(self.env.docname, self.lineno))
@@ -131,7 +145,7 @@ class AutodocDirective(SphinxDirective):
return []
# generate the output
- params = DocumenterBridge(self.env, reporter, documenter_options, lineno)
+ params = DocumenterBridge(self.env, reporter, documenter_options, lineno, self.state)
documenter = doccls(params, self.arguments[0])
documenter.generate(more_content=self.content)
if not params.result:
diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py
index 952bd9e2a..5840f0ccd 100644
--- a/sphinx/ext/autosummary/__init__.py
+++ b/sphinx/ext/autosummary/__init__.py
@@ -175,7 +175,7 @@ _app = None # type: Sphinx
class FakeDirective(DocumenterBridge):
def __init__(self):
# type: () -> None
- super().__init__({}, None, Options(), 0) # type: ignore
+ super().__init__({}, None, Options(), 0, None) # type: ignore
def get_documenter(app, obj, parent):
@@ -236,7 +236,7 @@ class Autosummary(SphinxDirective):
def run(self):
# type: () -> List[nodes.Node]
self.bridge = DocumenterBridge(self.env, self.state.document.reporter,
- Options(), self.lineno)
+ Options(), self.lineno, self.state)
names = [x.strip().split()[0] for x in self.content
if x.strip() and re.search(r'^[~a-zA-Z_]', x.strip()[0])]
diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py
index 1e9dbedc8..eac25697a 100644
--- a/sphinx/ext/autosummary/generate.py
+++ b/sphinx/ext/autosummary/generate.py
@@ -40,7 +40,7 @@ from sphinx.util.rst import escape as rst_escape
if False:
# For type annotation
- from typing import Any, Callable, Dict, List, Tuple, Type, Union # NOQA
+ 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
@@ -169,8 +169,8 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
except TemplateNotFound:
template = template_env.get_template('autosummary/base.rst')
- def get_members(obj, typ, include_public=[], imported=True):
- # type: (Any, str, List[str], bool) -> Tuple[List[str], List[str]]
+ def get_members(obj, types, include_public=[], imported=True):
+ # type: (Any, Set[str], List[str], bool) -> Tuple[List[str], List[str]] # NOQA
items = [] # type: List[str]
for name in dir(obj):
try:
@@ -178,7 +178,7 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
except AttributeError:
continue
documenter = get_documenter(app, value, obj)
- if documenter.objtype == typ:
+ if documenter.objtype in types:
if imported or getattr(value, '__module__', None) == obj.__name__:
# skip imported members if expected
items.append(name)
@@ -191,19 +191,19 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
if doc.objtype == 'module':
ns['members'] = dir(obj)
ns['functions'], ns['all_functions'] = \
- get_members(obj, 'function', imported=imported_members)
+ get_members(obj, {'function'}, imported=imported_members)
ns['classes'], ns['all_classes'] = \
- get_members(obj, 'class', imported=imported_members)
+ get_members(obj, {'class'}, imported=imported_members)
ns['exceptions'], ns['all_exceptions'] = \
- get_members(obj, 'exception', imported=imported_members)
+ get_members(obj, {'exception'}, imported=imported_members)
elif doc.objtype == 'class':
ns['members'] = dir(obj)
ns['inherited_members'] = \
set(dir(obj)) - set(obj.__dict__.keys())
ns['methods'], ns['all_methods'] = \
- get_members(obj, 'method', ['__init__'])
+ get_members(obj, {'method'}, ['__init__'])
ns['attributes'], ns['all_attributes'] = \
- get_members(obj, 'attribute')
+ get_members(obj, {'attribute', 'property'})
parts = name.split('.')
if doc.objtype in ('method', 'attribute'):
diff --git a/sphinx/highlighting.py b/sphinx/highlighting.py
index c194738f8..995ca65d3 100644
--- a/sphinx/highlighting.py
+++ b/sphinx/highlighting.py
@@ -27,6 +27,7 @@ if False:
# For type annotation
from typing import Any, Dict # NOQA
from pygments.formatter import Formatter # NOQA
+ from pygments.style import Style # NOQA
logger = logging.getLogger(__name__)
@@ -64,16 +65,8 @@ class PygmentsBridge:
def __init__(self, dest='html', stylename='sphinx'):
# type: (str, str) -> None
self.dest = dest
- if stylename is None or stylename == 'sphinx':
- style = SphinxStyle
- elif stylename == 'none':
- style = NoneStyle
- elif '.' in stylename:
- module, stylename = stylename.rsplit('.', 1)
- style = getattr(__import__(module, None, None, ['__name__']),
- stylename)
- else:
- style = get_style_by_name(stylename)
+
+ style = self.get_style(stylename)
self.formatter_args = {'style': style} # type: Dict[str, Any]
if dest == 'html':
self.formatter = self.html_formatter
@@ -81,16 +74,25 @@ class PygmentsBridge:
self.formatter = self.latex_formatter
self.formatter_args['commandprefix'] = 'PYG'
+ def get_style(self, stylename):
+ # type: (str) -> Style
+ if stylename is None or stylename == 'sphinx':
+ return SphinxStyle
+ elif stylename == 'none':
+ return NoneStyle
+ elif '.' in stylename:
+ module, stylename = stylename.rsplit('.', 1)
+ return getattr(__import__(module, None, None, ['__name__']), stylename)
+ else:
+ return get_style_by_name(stylename)
+
def get_formatter(self, **kwargs):
# type: (Any) -> Formatter
kwargs.update(self.formatter_args)
return self.formatter(**kwargs)
- def highlight_block(self, source, lang, opts=None, location=None, force=False, **kwargs):
- # type: (str, str, Any, Any, bool, Any) -> str
- if not isinstance(source, str):
- source = source.decode()
-
+ def get_lexer(self, source, lang, opts=None, location=None):
+ # type: (str, str, Any, Any) -> Lexer
# find out which lexer to use
if lang in ('py', 'python'):
if source.startswith('>>>'):
@@ -121,6 +123,15 @@ class PygmentsBridge:
else:
lexer.add_filter('raiseonerror')
+ return lexer
+
+ def highlight_block(self, source, lang, opts=None, location=None, force=False, **kwargs):
+ # type: (str, str, Any, Any, bool, Any) -> str
+ if not isinstance(source, str):
+ source = source.decode()
+
+ lexer = self.get_lexer(source, lang, opts, location)
+
# highlight via Pygments
formatter = self.get_formatter(**kwargs)
try:
@@ -136,6 +147,7 @@ class PygmentsBridge:
type='misc', subtype='highlighting_failure',
location=location)
hlsource = highlight(source, lexers['none'], formatter)
+
if self.dest == 'html':
return hlsource
else:
diff --git a/sphinx/io.py b/sphinx/io.py
index 105ed4397..354121c86 100644
--- a/sphinx/io.py
+++ b/sphinx/io.py
@@ -14,6 +14,7 @@ from docutils.core import Publisher
from docutils.io import FileInput, NullOutput
from docutils.parsers.rst import Parser as RSTParser
from docutils.readers import standalone
+from docutils.transforms.references import DanglingReferences
from docutils.writers import UnfilteredWriter
from sphinx.transforms import (
@@ -60,7 +61,15 @@ class SphinxBaseReader(standalone.Reader):
def get_transforms(self):
# type: () -> List[Type[Transform]]
- return super().get_transforms() + self.transforms
+ transforms = super().get_transforms() + self.transforms
+
+ # remove transforms which is not needed for Sphinx
+ unused = [DanglingReferences]
+ for transform in unused:
+ if transform in transforms:
+ transforms.remove(transform)
+
+ return transforms
def new_document(self):
# type: () -> nodes.document
diff --git a/sphinx/templates/latex/longtable.tex_t b/sphinx/templates/latex/longtable.tex_t
index ade1a54af..8fe5369df 100644
--- a/sphinx/templates/latex/longtable.tex_t
+++ b/sphinx/templates/latex/longtable.tex_t
@@ -1,5 +1,5 @@
\begin{savenotes}\sphinxatlongtablestart\begin{longtable}
-<%- if table.align == 'center' -%>
+<%- if table.align in ('center', 'default') -%>
[c]
<%- elif table.align == 'left' -%>
[l]
diff --git a/sphinx/templates/latex/tabular.tex_t b/sphinx/templates/latex/tabular.tex_t
index a4f56feb3..a0db7faff 100644
--- a/sphinx/templates/latex/tabular.tex_t
+++ b/sphinx/templates/latex/tabular.tex_t
@@ -1,6 +1,6 @@
\begin{savenotes}\sphinxattablestart
<% if table.align -%>
- <%- if table.align == 'center' -%>
+ <%- if table.align in ('center', 'default') -%>
\centering
<%- elif table.align == 'left' -%>
\raggedright
diff --git a/sphinx/templates/latex/tabulary.tex_t b/sphinx/templates/latex/tabulary.tex_t
index e3534725b..3236b798a 100644
--- a/sphinx/templates/latex/tabulary.tex_t
+++ b/sphinx/templates/latex/tabulary.tex_t
@@ -1,6 +1,6 @@
\begin{savenotes}\sphinxattablestart
<% if table.align -%>
- <%- if table.align == 'center' -%>
+ <%- if table.align in ('center', 'default') -%>
\centering
<%- elif table.align == 'left' -%>
\raggedright
diff --git a/sphinx/themes/basic/searchbox.html b/sphinx/themes/basic/searchbox.html
index 2ed7fa137..6679ca6b5 100644
--- a/sphinx/themes/basic/searchbox.html
+++ b/sphinx/themes/basic/searchbox.html
@@ -9,10 +9,10 @@
#}
{%- if pagename != "search" and builder != "singlehtml" %}
<div id="searchbox" style="display: none" role="search">
- <h3>{{ _('Quick search') }}</h3>
+ <h3 id="searchlabel">{{ _('Quick search') }}</h3>
<div class="searchformwrapper">
<form class="search" action="{{ pathto('search') }}" method="get">
- <input type="text" name="q" />
+ <input type="text" name="q" aria-labelledby="searchlabel" />
<input type="submit" value="{{ _('Go') }}" />
</form>
</div>
diff --git a/sphinx/themes/basic/static/basic.css_t b/sphinx/themes/basic/static/basic.css_t
index 90a14286f..91fd35755 100644
--- a/sphinx/themes/basic/static/basic.css_t
+++ b/sphinx/themes/basic/static/basic.css_t
@@ -289,6 +289,12 @@ img.align-center, .figure.align-center, object.align-center {
margin-right: auto;
}
+img.align-default, .figure.align-default {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
.align-left {
text-align: left;
}
@@ -297,6 +303,10 @@ img.align-center, .figure.align-center, object.align-center {
text-align: center;
}
+.align-default {
+ text-align: center;
+}
+
.align-right {
text-align: right;
}
@@ -368,6 +378,11 @@ table.align-center {
margin-right: auto;
}
+table.align-default {
+ margin-left: auto;
+ margin-right: auto;
+}
+
table caption span.caption-number {
font-style: italic;
}
diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py
index 16849c46c..a4e6e52bf 100644
--- a/sphinx/transforms/__init__.py
+++ b/sphinx/transforms/__init__.py
@@ -293,7 +293,7 @@ class FigureAligner(SphinxTransform):
# type: (Any) -> None
matcher = NodeMatcher(nodes.table, nodes.figure)
for node in self.document.traverse(matcher): # type: nodes.Element
- node.setdefault('align', 'center')
+ node.setdefault('align', 'default')
class FilterSystemMessages(SphinxTransform):
diff --git a/sphinx/transforms/references.py b/sphinx/transforms/references.py
index de512f437..9cdc28c78 100644
--- a/sphinx/transforms/references.py
+++ b/sphinx/transforms/references.py
@@ -9,7 +9,7 @@
"""
from docutils import nodes
-from docutils.transforms.references import Substitutions
+from docutils.transforms.references import DanglingReferences, Substitutions
from sphinx.transforms import SphinxTransform
@@ -31,6 +31,22 @@ class SubstitutionDefinitionsRemover(SphinxTransform):
node.parent.remove(node)
+class SphinxDanglingReferences(DanglingReferences):
+ """DanglingReferences transform which does not output info messages."""
+
+ def apply(self, **kwargs):
+ # type: (Any) -> None
+ try:
+ reporter = self.document.reporter
+ report_level = reporter.report_level
+
+ # suppress INFO level messages for a while
+ reporter.report_level = max(reporter.WARNING_LEVEL, reporter.report_level)
+ super().apply()
+ finally:
+ reporter.report_level = report_level
+
+
class SphinxDomains(SphinxTransform):
"""Collect objects to Sphinx domains for cross references."""
default_priority = 850
@@ -44,6 +60,7 @@ class SphinxDomains(SphinxTransform):
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.add_transform(SubstitutionDefinitionsRemover)
+ app.add_transform(SphinxDanglingReferences)
app.add_transform(SphinxDomains)
return {
diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py
index 97dd60294..31943b2cb 100644
--- a/sphinx/util/docstrings.py
+++ b/sphinx/util/docstrings.py
@@ -15,8 +15,8 @@ if False:
from typing import List # NOQA
-def prepare_docstring(s, ignore=1):
- # type: (str, int) -> List[str]
+def prepare_docstring(s, ignore=1, tabsize=8):
+ # type: (str, int, int) -> List[str]
"""Convert a docstring into lines of parseable reST. Remove common leading
indentation, where the indentation of a given number of lines (usually just
one) is ignored.
@@ -25,7 +25,7 @@ def prepare_docstring(s, ignore=1):
ViewList (used as argument of nested_parse().) An empty line is added to
act as a separator between this docstring and following content.
"""
- lines = s.expandtabs().splitlines()
+ lines = s.expandtabs(tabsize).splitlines()
# Find minimum indentation of any non-blank lines after ignored lines.
margin = sys.maxsize
for line in lines[ignore:]:
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index 5ba05f04b..caf333493 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -222,6 +222,12 @@ def iscoroutinefunction(obj):
return False
+def isproperty(obj):
+ # type: (Any) -> bool
+ """Check if the object is property."""
+ return isinstance(obj, property)
+
+
def safe_getattr(obj, name, *defargs):
# type: (Any, str, str) -> object
"""A getattr() that turns all exceptions into AttributeErrors."""
diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py
index b45a95eef..803dd1dbc 100644
--- a/sphinx/writers/latex.py
+++ b/sphinx/writers/latex.py
@@ -1527,6 +1527,7 @@ class LaTeXTranslator(SphinxTranslator):
(1, 'middle'): ('\\raisebox{-0.5\\height}{', '}'),
(1, 'bottom'): ('\\raisebox{-\\height}{', '}'),
(0, 'center'): ('{\\hspace*{\\fill}', '\\hspace*{\\fill}}'),
+ (0, 'default'): ('{\\hspace*{\\fill}', '\\hspace*{\\fill}}'),
# These 2 don't exactly do the right thing. The image should
# be floated alongside the paragraph. See
# https://www.w3.org/TR/html4/struct/objects.html#adef-align-IMG
diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py
index 0856ee5ee..7811ccc5b 100644
--- a/sphinx/writers/manpage.py
+++ b/sphinx/writers/manpage.py
@@ -282,7 +282,7 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator):
def depart_rubric(self, node):
# type: (nodes.Element) -> None
- pass
+ self.body.append('\n')
def visit_seealso(self, node):
# type: (nodes.Element) -> None
diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py
index 49ddbb9dc..0c3de1fae 100644
--- a/tests/test_autodoc.py
+++ b/tests/test_autodoc.py
@@ -11,6 +11,7 @@
import platform
import sys
+from unittest.mock import Mock
from warnings import catch_warnings
import pytest
@@ -33,7 +34,9 @@ def do_autodoc(app, objtype, name, options=None):
app.env.temp_data.setdefault('docname', 'index') # set dummy docname
doccls = app.registry.documenters[objtype]
docoptions = process_documenter_options(doccls, app.config, options)
- bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1)
+ state = Mock()
+ state.document.settings.tab_width = 8
+ bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1, state)
documenter = doccls(bridge, name)
documenter.generate()
@@ -92,7 +95,9 @@ def setup_test():
genopt = options,
result = ViewList(),
filename_set = set(),
+ state = Mock(),
)
+ directive.state.document.settings.tab_width = 8
processed_docstrings = []
processed_signatures = []
@@ -753,7 +758,7 @@ def test_autodoc_undoc_members(app):
' .. py:attribute:: Class.mdocattr',
' .. py:method:: Class.meth()',
' .. py:method:: Class.moore(a, e, f) -> happiness',
- ' .. py:attribute:: Class.prop',
+ ' .. py:method:: Class.prop',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)',
' .. py:attribute:: Class.skipattr',
' .. py:method:: Class.skipmeth()',
@@ -774,6 +779,7 @@ def test_autodoc_inherited_members(app):
' .. py:method:: Class.inheritedstaticmeth(cls)',
' .. py:method:: Class.meth()',
' .. py:method:: Class.moore(a, e, f) -> happiness',
+ ' .. py:method:: Class.prop',
' .. py:method:: Class.skipmeth()'
]
@@ -833,7 +839,7 @@ def test_autodoc_special_members(app):
' .. py:attribute:: Class.mdocattr',
' .. py:method:: Class.meth()',
' .. py:method:: Class.moore(a, e, f) -> happiness',
- ' .. py:attribute:: Class.prop',
+ ' .. py:method:: Class.prop',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)',
' .. py:attribute:: Class.skipattr',
' .. py:method:: Class.skipmeth()',
@@ -1025,7 +1031,7 @@ def test_autodoc_member_order(app):
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.skipattr',
' .. py:attribute:: Class.attr',
- ' .. py:attribute:: Class.prop',
+ ' .. py:method:: Class.prop',
' .. py:attribute:: Class.docattr',
' .. py:attribute:: Class.udocattr',
' .. py:attribute:: Class.mdocattr',
@@ -1059,7 +1065,7 @@ def test_autodoc_member_order(app):
' .. py:attribute:: Class.inst_attr_inline',
' .. py:attribute:: Class.inst_attr_string',
' .. py:attribute:: Class.mdocattr',
- ' .. py:attribute:: Class.prop',
+ ' .. py:method:: Class.prop',
' .. py:attribute:: Class.skipattr',
' .. py:attribute:: Class.udocattr'
]
@@ -1082,7 +1088,7 @@ def test_autodoc_member_order(app):
' .. py:attribute:: Class.mdocattr',
' .. py:method:: Class.meth()',
' .. py:method:: Class.moore(a, e, f) -> happiness',
- ' .. py:attribute:: Class.prop',
+ ' .. py:method:: Class.prop',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)',
' .. py:attribute:: Class.skipattr',
' .. py:method:: Class.skipmeth()',
@@ -1149,14 +1155,16 @@ def test_autodoc_docstring_signature(app):
' indented line',
' ',
' ',
- ' .. py:attribute:: DocstringSig.prop1',
+ ' .. py:method:: DocstringSig.prop1',
' :module: target',
+ ' :property:',
' ',
' First line of docstring',
' ',
' ',
- ' .. py:attribute:: DocstringSig.prop2',
+ ' .. py:method:: DocstringSig.prop2',
' :module: target',
+ ' :property:',
' ',
' First line of docstring',
' Second line of docstring',
@@ -1191,15 +1199,17 @@ def test_autodoc_docstring_signature(app):
' indented line',
' ',
' ',
- ' .. py:attribute:: DocstringSig.prop1',
+ ' .. py:method:: DocstringSig.prop1',
' :module: target',
+ ' :property:',
' ',
' DocstringSig.prop1(self)',
' First line of docstring',
' ',
' ',
- ' .. py:attribute:: DocstringSig.prop2',
+ ' .. py:method:: DocstringSig.prop2',
' :module: target',
+ ' :property:',
' ',
' First line of docstring',
' Second line of docstring',
@@ -1693,7 +1703,7 @@ def test_autodoc_default_options_with_values(app):
' .. py:method:: Class.skipmeth()',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.attr',
- ' .. py:attribute:: Class.prop',
+ ' .. py:method:: Class.prop',
' .. py:attribute:: Class.docattr',
' .. py:attribute:: Class.udocattr',
' .. py:attribute:: Class.mdocattr',
diff --git a/tests/test_build_html.py b/tests/test_build_html.py
index 1dbf05a4a..2df1f8412 100644
--- a/tests/test_build_html.py
+++ b/tests/test_build_html.py
@@ -565,7 +565,7 @@ def test_numfig_disabled_warn(app, warning):
@pytest.mark.parametrize("fname,expect", flat_dict({
'index.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", None, True),
(".//table/caption/span[@class='caption-number']", None, True),
(".//div[@class='code-block-caption']/"
@@ -582,21 +582,21 @@ def test_numfig_disabled_warn(app, warning):
(".//li/p/a/span", '^Sect.1 Foo$', True),
],
'foo.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", None, True),
(".//table/caption/span[@class='caption-number']", None, True),
(".//div[@class='code-block-caption']/"
"span[@class='caption-number']", None, True),
],
'bar.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", None, True),
(".//table/caption/span[@class='caption-number']", None, True),
(".//div[@class='code-block-caption']/"
"span[@class='caption-number']", None, True),
],
'baz.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", None, True),
(".//table/caption/span[@class='caption-number']", None, True),
(".//div[@class='code-block-caption']/"
@@ -633,9 +633,9 @@ def test_numfig_without_numbered_toctree_warn(app, warning):
@pytest.mark.parametrize("fname,expect", flat_dict({
'index.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 9 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 10 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 9 $', True),
@@ -657,13 +657,13 @@ def test_numfig_without_numbered_toctree_warn(app, warning):
(".//li/p/code/span", '^Sect.{number}$', True),
],
'foo.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 3 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 4 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 1 $', True),
@@ -683,11 +683,11 @@ def test_numfig_without_numbered_toctree_warn(app, warning):
"span[@class='caption-number']", '^Listing 4 $', True),
],
'bar.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 5 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 7 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 8 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 5 $', True),
@@ -703,7 +703,7 @@ def test_numfig_without_numbered_toctree_warn(app, warning):
"span[@class='caption-number']", '^Listing 8 $', True),
],
'baz.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 6 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 6 $', True),
@@ -741,9 +741,9 @@ def test_numfig_with_numbered_toctree_warn(app, warning):
@pytest.mark.parametrize("fname,expect", flat_dict({
'index.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 1 $', True),
@@ -765,13 +765,13 @@ def test_numfig_with_numbered_toctree_warn(app, warning):
(".//li/p/a/span", '^Sect.1 Foo$', True),
],
'foo.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.2 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.3 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.4 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 1.1 $', True),
@@ -791,11 +791,11 @@ def test_numfig_with_numbered_toctree_warn(app, warning):
"span[@class='caption-number']", '^Listing 1.4 $', True),
],
'bar.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.3 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.4 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 2.1 $', True),
@@ -811,7 +811,7 @@ def test_numfig_with_numbered_toctree_warn(app, warning):
"span[@class='caption-number']", '^Listing 2.4 $', True),
],
'baz.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.2 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 2.2 $', True),
@@ -846,9 +846,9 @@ def test_numfig_with_prefix_warn(app, warning):
@pytest.mark.parametrize("fname,expect", flat_dict({
'index.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:2 $', True),
(".//table/caption/span[@class='caption-number']",
'^Tab_1 $', True),
@@ -870,13 +870,13 @@ def test_numfig_with_prefix_warn(app, warning):
(".//li/p/a/span", '^Sect.1 Foo$', True),
],
'foo.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:1.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:1.2 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:1.3 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:1.4 $', True),
(".//table/caption/span[@class='caption-number']",
'^Tab_1.1 $', True),
@@ -896,11 +896,11 @@ def test_numfig_with_prefix_warn(app, warning):
"span[@class='caption-number']", '^Code-1.4 $', True),
],
'bar.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:2.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:2.3 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:2.4 $', True),
(".//table/caption/span[@class='caption-number']",
'^Tab_2.1 $', True),
@@ -916,7 +916,7 @@ def test_numfig_with_prefix_warn(app, warning):
"span[@class='caption-number']", '^Code-2.4 $', True),
],
'baz.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Figure:2.2 $', True),
(".//table/caption/span[@class='caption-number']",
'^Tab_2.2 $', True),
@@ -952,9 +952,9 @@ def test_numfig_with_secnum_depth_warn(app, warning):
@pytest.mark.parametrize("fname,expect", flat_dict({
'index.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 1 $', True),
@@ -976,13 +976,13 @@ def test_numfig_with_secnum_depth_warn(app, warning):
(".//li/p/a/span", '^Sect.1 Foo$', True),
],
'foo.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.1.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.1.2 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.2.1 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 1.1 $', True),
@@ -1002,11 +1002,11 @@ def test_numfig_with_secnum_depth_warn(app, warning):
"span[@class='caption-number']", '^Listing 1.2.1 $', True),
],
'bar.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.1.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.1.3 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.2.1 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 2.1.1 $', True),
@@ -1022,7 +1022,7 @@ def test_numfig_with_secnum_depth_warn(app, warning):
"span[@class='caption-number']", '^Listing 2.2.1 $', True),
],
'baz.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.1.2 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 2.1.2 $', True),
@@ -1043,9 +1043,9 @@ def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, expect):
@pytest.mark.parametrize("fname,expect", flat_dict({
'index.html': [
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 1 $', True),
@@ -1065,13 +1065,13 @@ def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, expect):
(".//li/p/a/span", '^Section.2.1$', True),
(".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
(".//li/p/a/span", '^Sect.1 Foo$', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.2 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.3 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 1.4 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 1.1 $', True),
@@ -1089,11 +1089,11 @@ def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, expect):
"span[@class='caption-number']", '^Listing 1.3 $', True),
(".//div[@class='code-block-caption']/"
"span[@class='caption-number']", '^Listing 1.4 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.1 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.3 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.4 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 2.1 $', True),
@@ -1107,7 +1107,7 @@ def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, expect):
"span[@class='caption-number']", '^Listing 2.3 $', True),
(".//div[@class='code-block-caption']/"
"span[@class='caption-number']", '^Listing 2.4 $', True),
- (".//div[@class='figure align-center']/p[@class='caption']/"
+ (".//div[@class='figure align-default']/p[@class='caption']/"
"span[@class='caption-number']", '^Fig. 2.2 $', True),
(".//table/caption/span[@class='caption-number']",
'^Table 2.2 $', True),
@@ -1126,11 +1126,11 @@ def test_numfig_with_singlehtml(app, cached_etree_parse, fname, expect):
@pytest.mark.parametrize("fname,expect", flat_dict({
'index.html': [
- (".//div[@class='figure align-center']/p[@class='caption']"
+ (".//div[@class='figure align-default']/p[@class='caption']"
"/span[@class='caption-number']", "Fig. 1", True),
- (".//div[@class='figure align-center']/p[@class='caption']"
+ (".//div[@class='figure align-default']/p[@class='caption']"
"/span[@class='caption-number']", "Fig. 2", True),
- (".//div[@class='figure align-center']/p[@class='caption']"
+ (".//div[@class='figure align-default']/p[@class='caption']"
"/span[@class='caption-number']", "Fig. 3", True),
(".//div//span[@class='caption-number']", "No.1 ", True),
(".//div//span[@class='caption-number']", "No.2 ", True),
@@ -1338,7 +1338,7 @@ def test_html_sidebar(app, status, warning):
assert '<h1 class="logo"><a href="#">Python</a></h1>' in result
assert '<h3>Navigation</h3>' in result
assert '<h3>Related Topics</h3>' in result
- assert '<h3>Quick search</h3>' in result
+ assert '<h3 id="searchlabel">Quick search</h3>' in result
app.builder.add_sidebars('index', ctx)
assert ctx['sidebars'] == ['about.html', 'navigation.html', 'relations.html',
@@ -1353,7 +1353,7 @@ def test_html_sidebar(app, status, warning):
assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
assert '<h3>Navigation</h3>' not in result
assert '<h3>Related Topics</h3>' in result
- assert '<h3>Quick search</h3>' not in result
+ assert '<h3 id="searchlabel">Quick search</h3>' not in result
app.builder.add_sidebars('index', ctx)
assert ctx['sidebars'] == ['relations.html']
@@ -1367,7 +1367,7 @@ def test_html_sidebar(app, status, warning):
assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
assert '<h3>Navigation</h3>' not in result
assert '<h3>Related Topics</h3>' not in result
- assert '<h3>Quick search</h3>' not in result
+ assert '<h3 id="searchlabel">Quick search</h3>' not in result
app.builder.add_sidebars('index', ctx)
assert ctx['sidebars'] == []
diff --git a/tests/test_build_manpage.py b/tests/test_build_manpage.py
index 17a2f7eb8..a0a3efb00 100644
--- a/tests/test_build_manpage.py
+++ b/tests/test_build_manpage.py
@@ -59,3 +59,10 @@ def test_default_man_pages():
expected = [('index', 'stasi', 'STASI™ Documentation 1.0',
["Wolfgang Schäuble & G'Beckstein"], 1)]
assert default_man_pages(config) == expected
+
+
+@pytest.mark.sphinx('man', testroot='markup-rubric')
+def test_rubric(app, status, warning):
+ app.build()
+ content = (app.outdir / 'python.1').text()
+ assert 'This is a rubric\n' in content
diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py
index d3c685388..fac8a838f 100644
--- a/tests/test_domain_py.py
+++ b/tests/test_domain_py.py
@@ -333,7 +333,9 @@ def test_pymethod_options(app):
" .. py:method:: meth3\n"
" :staticmethod:\n"
" .. py:method:: meth4\n"
- " :async:\n")
+ " :async:\n"
+ " .. py:method:: meth5\n"
+ " :property:\n")
domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index,
@@ -346,6 +348,8 @@ def test_pymethod_options(app):
addnodes.index,
desc,
addnodes.index,
+ desc,
+ addnodes.index,
desc)])]))
# method
@@ -387,6 +391,15 @@ def test_pymethod_options(app):
assert 'Class.meth4' in domain.objects
assert domain.objects['Class.meth4'] == ('index', 'method')
+ # :property:
+ assert_node(doctree[1][1][8], addnodes.index,
+ entries=[('single', 'meth5() (Class property)', 'Class.meth5', '', None)])
+ assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, "property "],
+ [desc_name, "meth5"])],
+ [desc_content, ()]))
+ assert 'Class.meth5' in domain.objects
+ assert domain.objects['Class.meth5'] == ('index', 'method')
+
def test_pyclassmethod(app):
text = (".. py:class:: Class\n"
diff --git a/tests/test_domain_rst.py b/tests/test_domain_rst.py
index 3310b5752..207ff1ff3 100644
--- a/tests/test_domain_rst.py
+++ b/tests/test_domain_rst.py
@@ -10,8 +10,7 @@
from sphinx import addnodes
from sphinx.addnodes import (
- desc, desc_addname, desc_content, desc_name, desc_optional, desc_parameter,
- desc_parameterlist, desc_returns, desc_signature
+ desc, desc_addname, desc_annotation, desc_content, desc_name, desc_signature
)
from sphinx.domains.rst import parse_directive
from sphinx.testing import restructuredtext
@@ -69,6 +68,66 @@ def test_rst_directive_with_argument(app):
domain="rst", objtype="directive", noindex=False)
+def test_rst_directive_option(app):
+ text = ".. rst:directive:option:: foo"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, desc_name, ":foo:"],
+ [desc_content, ()])]))
+ assert_node(doctree[0],
+ entries=[("single", ":foo: (directive option)",
+ "directive:option--foo", "", "F")])
+ assert_node(doctree[1], addnodes.desc, desctype="directive:option",
+ domain="rst", objtype="directive:option", noindex=False)
+
+
+def test_rst_directive_option_with_argument(app):
+ text = ".. rst:directive:option:: foo: bar baz"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, ":foo:"],
+ [desc_annotation, " bar baz"])],
+ [desc_content, ()])]))
+ assert_node(doctree[0],
+ entries=[("single", ":foo: (directive option)",
+ "directive:option--foo", "", "F")])
+ assert_node(doctree[1], addnodes.desc, desctype="directive:option",
+ domain="rst", objtype="directive:option", noindex=False)
+
+
+def test_rst_directive_option_type(app):
+ text = (".. rst:directive:option:: foo\n"
+ " :type: directives.flags\n")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, ":foo:"],
+ [desc_annotation, " (directives.flags)"])],
+ [desc_content, ()])]))
+ assert_node(doctree[0],
+ entries=[("single", ":foo: (directive option)",
+ "directive:option--foo", "", "F")])
+ assert_node(doctree[1], addnodes.desc, desctype="directive:option",
+ domain="rst", objtype="directive:option", noindex=False)
+
+
+def test_rst_directive_and_directive_option(app):
+ text = (".. rst:directive:: foo\n"
+ "\n"
+ " .. rst:directive:option:: bar\n")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, desc_name, ".. foo::"],
+ [desc_content, (addnodes.index,
+ desc)])]))
+ assert_node(doctree[1][1][0],
+ entries=[("pair", "foo (directive); :bar: (directive option)",
+ "directive:option-foo-bar", "", "B")])
+ assert_node(doctree[1][1][1], ([desc_signature, desc_name, ":bar:"],
+ [desc_content, ()]))
+ assert_node(doctree[1][1][1], addnodes.desc, desctype="directive:option",
+ domain="rst", objtype="directive:option", noindex=False)
+
+
def test_rst_role(app):
text = ".. rst:role:: ref"
doctree = restructuredtext.parse(app, text)
diff --git a/tests/test_ext_graphviz.py b/tests/test_ext_graphviz.py
index 6a3096c23..ec905aa5f 100644
--- a/tests/test_ext_graphviz.py
+++ b/tests/test_ext_graphviz.py
@@ -21,7 +21,7 @@ def test_graphviz_png_html(app, status, warning):
app.builder.build_all()
content = (app.outdir / 'index.html').text()
- html = (r'<div class="figure align-center" .*?>\s*'
+ html = (r'<div class="figure align-default" .*?>\s*'
r'<div class="graphviz"><img .*?/></div>\s*<p class="caption">'
r'<span class="caption-text">caption of graph</span>.*</p>\s*</div>')
assert re.search(html, content, re.S)
@@ -52,7 +52,7 @@ def test_graphviz_svg_html(app, status, warning):
content = (app.outdir / 'index.html').text()
- html = (r'<div class=\"figure align-center\" .*?>\n'
+ html = (r'<div class=\"figure align-default\" .*?>\n'
r'<div class="graphviz"><object data=\".*\.svg\".*>\n'
r'\s*<p class=\"warning\">digraph foo {\n'
r'bar -&gt; baz\n'
diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py
index efdace893..d9fcb3730 100644
--- a/tests/test_ext_inheritance_diagram.py
+++ b/tests/test_ext_inheritance_diagram.py
@@ -140,7 +140,7 @@ def test_inheritance_diagram_png_html(app, status, warning):
content = (app.outdir / 'index.html').text()
- pattern = ('<div class="figure align-center" id="id1">\n'
+ pattern = ('<div class="figure align-default" id="id1">\n'
'<div class="graphviz">'
'<img src="_images/inheritance-\\w+.png" alt="Inheritance diagram of test.Foo" '
'class="inheritance graphviz" /></div>\n<p class="caption">'
@@ -157,7 +157,7 @@ def test_inheritance_diagram_svg_html(app, status, warning):
content = (app.outdir / 'index.html').text()
- pattern = ('<div class="figure align-center" id="id1">\n'
+ pattern = ('<div class="figure align-default" id="id1">\n'
'<div class="graphviz">'
'<object data="_images/inheritance-\\w+.svg" '
'type="image/svg\\+xml" class="inheritance graphviz">\n'
@@ -197,7 +197,7 @@ def test_inheritance_diagram_latex_alias(app, status, warning):
content = (app.outdir / 'index.html').text()
- pattern = ('<div class="figure align-center" id="id1">\n'
+ pattern = ('<div class="figure align-default" id="id1">\n'
'<div class="graphviz">'
'<img src="_images/inheritance-\\w+.png" alt="Inheritance diagram of test.Foo" '
'class="inheritance graphviz" /></div>\n<p class="caption">'
diff --git a/tests/test_theming.py b/tests/test_theming.py
index cf4c23e75..a60a77129 100644
--- a/tests/test_theming.py
+++ b/tests/test_theming.py
@@ -126,4 +126,4 @@ def test_theme_sidebars(app, status, warning):
assert '<h3><a href="#">Table of Contents</a></h3>' in result
assert '<h3>Related Topics</h3>' not in result
assert '<h3>This Page</h3>' not in result
- assert '<h3>Quick search</h3>' in result
+ assert '<h3 id="searchlabel">Quick search</h3>' in result
diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py
index c298e2c64..c80b2b7c8 100644
--- a/tests/test_util_inspect.py
+++ b/tests/test_util_inspect.py
@@ -475,3 +475,14 @@ def test_isattributedescriptor(app):
assert inspect.isattributedescriptor(types.FrameType.f_locals) is True # GetSetDescriptorType # NOQA
assert inspect.isattributedescriptor(datetime.timedelta.days) is True # MemberDescriptorType # NOQA
assert inspect.isattributedescriptor(testinstancemethod) is False # instancemethod (C-API) # NOQA
+
+
+def test_isproperty(app):
+ from target.functions import func
+ from target.methods import Base
+
+ assert inspect.isproperty(Base.prop) is True # property of class
+ assert inspect.isproperty(Base().prop) is False # property of instance
+ assert inspect.isproperty(Base.meth) is False # method of class
+ assert inspect.isproperty(Base().meth) is False # method of instance
+ assert inspect.isproperty(func) is False # function
diff --git a/tox.ini b/tox.ini
index 64bebf2fb..0fd015b3e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -66,6 +66,15 @@ extras =
commands =
python setup.py build_sphinx {posargs}
+[testenv:docslint]
+basepython = python3
+description =
+ Lint documentation.
+extras =
+ docs
+commands =
+ python utils/doclinter.py CHANGES CONTRIBUTING.rst README.rst doc/
+
[testenv:bindep]
description =
Install binary dependencies.
diff --git a/utils/doclinter.py b/utils/doclinter.py
new file mode 100644
index 000000000..3f711bfa5
--- /dev/null
+++ b/utils/doclinter.py
@@ -0,0 +1,59 @@
+"""
+ utils.doclinter
+ ~~~~~~~~~~~~~~~
+
+ A linter for Sphinx docs
+
+ :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+import re
+import sys
+from typing import List
+
+
+MAX_LINE_LENGTH = 100
+
+
+def lint(path: str) -> int:
+ with open(path) as f:
+ document = f.readlines()
+
+ errors = 0
+ for i, line in enumerate(document):
+ if line.endswith(' '):
+ print('%s:%d: the line ends with whitespace.' %
+ (path, i + 1))
+ errors += 1
+
+ if len(line) > MAX_LINE_LENGTH:
+ if re.match(r'^\s*\.\. ', line):
+ # ignore directives and hyperlink targets
+ pass
+ else:
+ print('%s:%d: the line is too long (%d > %d).' %
+ (path, i + 1, len(line), MAX_LINE_LENGTH))
+ errors += 1
+
+ return errors
+
+
+def main(args: List[str]) -> int:
+ errors = 0
+ for directory in args:
+ for root, dirs, files in os.walk(directory):
+ for filename in files:
+ if filename.endswith('.rst'):
+ path = os.path.join(root, filename)
+ errors += lint(path)
+
+ if errors:
+ return 1
+ else:
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))