Merge branch '5.x'

# Conflicts:
#	.github/workflows/builddoc.yml
#	.github/workflows/lint.yml
#	sphinx/registry.py
This commit is contained in:
Adam Turner 2022-06-16 21:46:54 +01:00
commit 7e38d2dad8
61 changed files with 426 additions and 662 deletions

View File

@ -7,11 +7,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: 3
- name: Install dependencies
run: |
sudo apt update

View File

@ -9,8 +9,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python 3
uses: actions/setup-python@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')

View File

@ -11,9 +11,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3
- name: Check Python version
run: python --version
- name: Unpin docutils

View File

@ -11,11 +11,11 @@ jobs:
tool: [docslint, flake8, isort, mypy, twine]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: 3
- name: Install dependencies
run: pip install -U tox
- name: Run Tox

View File

@ -24,7 +24,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
if: "!endsWith(matrix.python, '-dev')"
with:
python-version: ${{ matrix.python }}
@ -47,7 +47,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: 3
- name: Install dependencies

View File

@ -9,9 +9,9 @@ jobs:
node-version: 16
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js ${{ env.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ env.node-version }}
cache: "npm"

View File

@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
ref: 5.x
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3.9 # https://github.com/transifex/transifex-client/pull/330
- name: Install dependencies
@ -34,11 +34,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
ref: 5.x
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3.9 # https://github.com/transifex/transifex-client/pull/330
- name: Install dependencies

View File

@ -15,10 +15,10 @@ Other co-maintainers:
* Jean-François Burnol <@jfbu>
* Yoshiki Shibukawa <@shibu_jp>
* Timotheus Kampik - <@TimKam>
* Adam Turner - <@AA-Turner>
Other contributors, listed alphabetically, are:
* Adam Turner -- JavaScript improvements
* Alastair Houghton -- Apple Help builder
* Alexander Todorov -- inheritance_diagram tests and improvements
* Andi Albrecht -- agogo theme
@ -96,5 +96,3 @@ authors and projects:
* sphinx.util.jsdump uses the basestring encoding from simplejson,
written by Bob Ippolito, released under the MIT license
* sphinx.util.stemmer was written by Vivake Gupta, placed in the
Public Domain

16
CHANGES
View File

@ -33,12 +33,26 @@ Incompatible changes
Deprecated
----------
* #10467: Deprecated ``sphinx.util.stemmer`` in favour of ``snowballstemmer``.
Patch by Adam Turner.
Features added
--------------
* #10366: std domain: Add support for emphasising placeholders in :rst:dir`option`
directives through a new ``option_emphasise_placeholders`` configuration option.
* #10439: std domain: Use the repr of some variables when displaying warnings,
making whitespace issues easier to identify.
Bugs fixed
----------
* #10031: py domain: Fix spurious whitespace in unparsing various operators (``+``,
``-``, ``~``, and ``**``). Patch by Adam Turner.
* #10460: logging: Always show node source locations as absolute paths.
* #10520: HTML Theme: Fix use of sidebar classes in ``agogo.css_t``.
* #6679: HTML Theme: Fix inclusion of hidden toctrees in the agogo theme.
Testing
--------
@ -159,6 +173,8 @@ Deprecated
<script src="{{ pathto('_static/underscore.js', resource=True) }}"></script>
{{ super() }}
{%- endblock %}
Patch by Adam Turner.
* setuptools integration. The ``build_sphinx`` sub-command for setup.py is
marked as deprecated to follow the policy of setuptools team.
* The ``locale`` argument of ``sphinx.util.i18n:babel_format_date()`` becomes

View File

@ -62,7 +62,7 @@ type-check:
.PHONY: doclinter
doclinter:
python utils/doclinter.py CHANGES *.rst doc/
sphinx-lint --enable line-too-long --max-line-length 85 CHANGES *.rst doc/
.PHONY: test
test:

View File

@ -2,10 +2,10 @@
Extending Sphinx
================
This guide is aimed at giving a quick introduction for those wishing to
develop their own extensions for Sphinx. Sphinx possesses significant
This guide is aimed at giving a quick introduction for those wishing to
develop their own extensions for Sphinx. Sphinx possesses significant
extensibility capabilities including the ability to hook into almost every
point of the build process. If you simply wish to use Sphinx with existing
point of the build process. If you simply wish to use Sphinx with existing
extensions, refer to :doc:`/usage/index`. For a more detailed discussion of
the extension interface see :doc:`/extdev/index`.

View File

@ -123,7 +123,7 @@ For example, you have the following ``IntEnum``:
.. code-block:: python
:caption: my_enums.py
class Colors(IntEnum):
"""Colors enumerator"""
NONE = 0
@ -138,5 +138,3 @@ This will be the documentation file with auto-documentation directive:
:caption: index.rst
.. autointenum:: my_enums.Colors

View File

@ -22,6 +22,11 @@ The following is a list of deprecated interfaces.
- (will be) Removed
- Alternatives
* - ``sphinx.util.stemmer``
- 5.1
- 7.0
- ``snowballstemmer``
* - ``sphinx.util.jsdump``
- 5.0
- 7.0
@ -824,7 +829,7 @@ The following is a list of deprecated interfaces.
- ``sphinx.domains.std.StandardDomain.process_doc()``
* - ``sphinx.domains.js.JSObject.display_prefix``
-
-
- 4.3
- ``sphinx.domains.js.JSObject.get_display_prefix()``

View File

@ -330,7 +330,7 @@ Keys that don't need to be overridden unless in special cases are:
.. attention::
- Do not use this key for a :confval:`latex_engine` other than
- Do not use this key for a :confval:`latex_engine` other than
``'pdflatex'``.
- If Greek is main language, do not use this key. Since Sphinx 2.2.1,
@ -528,7 +528,7 @@ Keys that don't need to be overridden unless in special cases are:
is adapted to the relative widths of the FreeFont family.
.. versionchanged:: 4.0.0
Changed default for ``'pdflatex'``. Previously it was using
Changed default for ``'pdflatex'``. Previously it was using
``'\\fvset{fontsize=\\small}'``.
.. versionchanged:: 4.1.0
@ -915,7 +915,7 @@ Do not use quotes to enclose values, whether numerical or strings.
``attentionBorderColor``, ``dangerBorderColor``,
``errorBorderColor``
.. |wgbdcolorslatex| replace:: ``warningBorderColor``, and
.. |wgbdcolorslatex| replace:: ``warningBorderColor``, and
``(caution|attention|danger|error)BorderColor``
.. else latex goes into right margin, as it does not hyphenate the names

View File

@ -377,7 +377,7 @@ in the future.
.. data:: sphinx_version_tuple
The version of Sphinx used to build represented as a tuple of five elements.
For Sphinx version 3.5.1 beta 3 this would be `(3, 5, 1, 'beta', 3)``.
For Sphinx version 3.5.1 beta 3 this would be ``(3, 5, 1, 'beta', 3)``.
The fourth element can be one of: ``alpha``, ``beta``, ``rc``, ``final``.
``final`` always has 0 as the last element.

View File

@ -190,11 +190,11 @@ contents:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build HTML
uses: ammaraskar/sphinx-action@0.4
- name: Upload artifacts
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: html-docs
path: docs/build/html/

View File

@ -85,10 +85,11 @@ you can use :rst:role:`py:func` for that, as follows:
or ``"veggies"``. Otherwise, :py:func:`lumache.get_random_ingredients`
will raise an exception.
When generating code documentation, Sphinx will generate a cross-reference automatically just
by using the name of the object, without you having to explicitly use a role
for that. For example, you can describe the custom exception raised by the
function using the :rst:dir:`py:exception` directive:
When generating code documentation, Sphinx will generate a
cross-reference automatically just by using the name of the object,
without you having to explicitly use a role for that. For example, you
can describe the custom exception raised by the function using the
:rst:dir:`py:exception` directive:
.. code-block:: rst
:caption: docs/source/usage.rst

View File

@ -706,6 +706,15 @@ General configuration
.. versionadded:: 3.0
.. confval:: option_emphasise_placeholders
Default is ``False``.
When enabled, emphasise placeholders in ``.. option:`` directives.
To display literal braces, escape with a backslash (``\{``). For example,
``option_emphasise_placeholders=True`` and ``.. option:: -foption={TYPE}`` would
render with ``TYPE`` emphasised.
.. versionadded:: 5.1
.. _intl-options:

View File

@ -209,7 +209,7 @@ The Intersphinx extension provides the following role.
If you would like to constrain the lookup to a specific external project,
then the key of the project, as specified in :confval:`intersphinx_mapping`,
is added as well to get the two forms
is added as well to get the two forms
- ``:external+invname:domain:reftype:`target```,
e.g., ``:external+python:py:class:`zipfile.ZipFile```, or

View File

@ -136,7 +136,7 @@ separate sections, whereas NumPy uses underlines.
Google style:
.. code-block:: python3
.. code-block:: python
def func(arg1, arg2):
"""Summary line.
@ -155,7 +155,7 @@ Google style:
NumPy style:
.. code-block:: python3
.. code-block:: python
def func(arg1, arg2):
"""Summary line.
@ -221,7 +221,7 @@ Google style with Python 3 type annotations::
"""
return True
class Class:
"""Summary line.
@ -251,7 +251,7 @@ Google style with types in docstrings::
"""
return True
class Class:
"""Summary line.

View File

@ -118,7 +118,7 @@ Chocolatey
::
$ choco install sphinx
You would need to `install Chocolatey
<https://chocolatey.org/install>`_
before running this.

View File

@ -1750,6 +1750,9 @@ There is a set of directives allowing documenting command-line programs:
referenceable by :rst:role:`option` (in the example case, you'd use something
like ``:option:`dest_dir```, ``:option:`-m```, or ``:option:`--module```).
Use :confval:`option_emphasise_placeholders` for parsing of
"variable part" of a literal text (similarly to the ``samp`` role).
``cmdoption`` directive is a deprecated alias for the ``option`` directive.
.. rst:directive:: .. envvar:: name

View File

@ -349,7 +349,7 @@ different style:
The name of a file or directory. Within the contents, you can use curly
braces to indicate a "variable" part, for example::
... is installed in :file:`/usr/lib/python2.{x}/site-packages` ...
... is installed in :file:`/usr/lib/python3.{x}/site-packages` ...
In the built documentation, the ``x`` will be displayed differently to
indicate that it is to be replaced by the Python minor version.

View File

@ -26,11 +26,13 @@ python_version = 3.6
disallow_incomplete_defs = True
show_column_numbers = True
show_error_context = True
show_error_codes = true
ignore_missing_imports = True
follow_imports = skip
check_untyped_defs = True
warn_unused_ignores = True
strict_optional = False
no_implicit_optional = True
[tool:pytest]
filterwarnings =

View File

@ -42,6 +42,7 @@ extras_require = {
'flake8>=3.5.0',
'isort',
'mypy>=0.950',
'sphinx-lint',
'docutils-stubs',
"types-typed-ast",
"types-requests",

View File

@ -133,9 +133,6 @@ class Sphinx:
self.phase = BuildPhase.INITIALIZATION
self.verbosity = verbosity
self.extensions: Dict[str, Extension] = {}
self.builder: Optional[Builder] = None
self.env: Optional[BuildEnvironment] = None
self.project: Optional[Project] = None
self.registry = SphinxComponentRegistry()
# validate provided directories
@ -246,10 +243,16 @@ class Sphinx:
# create the project
self.project = Project(self.srcdir, self.config.source_suffix)
# set up the build environment
self.env = self._init_env(freshenv)
# create the builder
self.builder = self.create_builder(buildername)
# set up the build environment
self._init_env(freshenv)
# build environment post-initialisation, after creating the builder
self._post_init_env()
# set up the builder
self._init_builder()
@ -281,20 +284,34 @@ class Sphinx:
else:
logger.info(__('not available for built-in messages'))
def _init_env(self, freshenv: bool) -> None:
def _init_env(self, freshenv: bool) -> BuildEnvironment:
filename = path.join(self.doctreedir, ENV_PICKLE_FILENAME)
if freshenv or not os.path.exists(filename):
self.env = BuildEnvironment(self)
self.env.find_files(self.config, self.builder)
return self._create_fresh_env()
else:
try:
with progress_message(__('loading pickled environment')):
with open(filename, 'rb') as f:
self.env = pickle.load(f)
self.env.setup(self)
except Exception as err:
logger.info(__('failed: %s'), err)
self._init_env(freshenv=True)
return self._load_existing_env(filename)
def _create_fresh_env(self) -> BuildEnvironment:
env = BuildEnvironment(self)
self._fresh_env_used = True
return env
def _load_existing_env(self, filename: str) -> BuildEnvironment:
try:
with progress_message(__('loading pickled environment')):
with open(filename, 'rb') as f:
env = pickle.load(f)
env.setup(self)
self._fresh_env_used = False
except Exception as err:
logger.info(__('failed: %s'), err)
env = self._create_fresh_env()
return env
def _post_init_env(self) -> None:
if self._fresh_env_used:
self.env.find_files(self.config, self.builder)
del self._fresh_env_used
def preload_builder(self, name: str) -> None:
self.registry.preload_builder(self, name)
@ -304,10 +321,11 @@ class Sphinx:
logger.info(__('No builder selected, using default: html'))
name = 'html'
return self.registry.create_builder(self, name)
return self.registry.create_builder(self, name, self.env)
def _init_builder(self) -> None:
self.builder.set_environment(self.env)
if not hasattr(self.builder, "env"):
self.builder.set_environment(self.env)
self.builder.init()
self.events.emit('builder-inited')
@ -984,8 +1002,9 @@ class Sphinx:
kwargs['defer'] = 'defer'
self.registry.add_js_file(filename, priority=priority, **kwargs)
if hasattr(self.builder, 'add_js_file'):
self.builder.add_js_file(filename, priority=priority, **kwargs) # type: ignore
if hasattr(self, 'builder') and hasattr(self.builder, 'add_js_file'):
self.builder.add_js_file(filename, # type: ignore[attr-defined]
priority=priority, **kwargs)
def add_css_file(self, filename: str, priority: int = 500, **kwargs: Any) -> None:
"""Register a stylesheet to include in the HTML output.
@ -1045,8 +1064,9 @@ class Sphinx:
"""
logger.debug('[app] adding stylesheet: %r', filename)
self.registry.add_css_files(filename, priority=priority, **kwargs)
if hasattr(self.builder, 'add_css_file'):
self.builder.add_css_file(filename, priority=priority, **kwargs) # type: ignore
if hasattr(self, 'builder') and hasattr(self.builder, 'add_css_file'):
self.builder.add_css_file(filename, # type: ignore[attr-defined]
priority=priority, **kwargs)
def add_latex_package(self, packagename: str, options: str = None,
after_hyperref: bool = False) -> None:

View File

@ -3,6 +3,7 @@
import codecs
import pickle
import time
import warnings
from os import path
from typing import (TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple,
Type, Union)
@ -11,6 +12,7 @@ from docutils import nodes
from docutils.nodes import Node
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx70Warning
from sphinx.environment import CONFIG_CHANGED_REASON, CONFIG_OK, BuildEnvironment
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import SphinxError
@ -75,7 +77,7 @@ class Builder:
#: The builder supports data URIs or not.
supported_data_uri_images = False
def __init__(self, app: "Sphinx") -> None:
def __init__(self, app: "Sphinx", env: BuildEnvironment = None) -> None:
self.srcdir = app.srcdir
self.confdir = app.confdir
self.outdir = app.outdir
@ -83,7 +85,14 @@ class Builder:
ensuredir(self.doctreedir)
self.app: Sphinx = app
self.env: Optional[BuildEnvironment] = None
if env is not None:
self.env: BuildEnvironment = env
self.env.set_versioning_method(self.versioning_method,
self.versioning_compare)
elif env is not Ellipsis:
# ... is passed by SphinxComponentRegistry.create_builder to not show two warnings.
warnings.warn("The 'env' argument to Builder will be required from Sphinx 7.",
RemovedInSphinx70Warning, stacklevel=2)
self.events: EventManager = app.events
self.config: Config = app.config
self.tags: Tags = app.tags
@ -105,6 +114,9 @@ class Builder:
def set_environment(self, env: BuildEnvironment) -> None:
"""Store BuildEnvironment object."""
warnings.warn("Builder.set_environment is deprecated, pass env to "
"'Builder.__init__()' instead.",
RemovedInSphinx70Warning, stacklevel=2)
self.env = env
self.env.set_versioning_method(self.versioning_method,
self.versioning_compare)

View File

@ -26,6 +26,7 @@ from sphinx.builders import Builder
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx70Warning, deprecated_alias
from sphinx.domains import Domain, Index, IndexEntry
from sphinx.environment import BuildEnvironment
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.environment.adapters.indexentries import IndexEntries
from sphinx.environment.adapters.toctree import TocTree
@ -51,6 +52,17 @@ INVENTORY_FILENAME = 'objects.inv'
logger = logging.getLogger(__name__)
return_codes_re = re.compile('[\r\n]+')
DOMAIN_INDEX_TYPE = Tuple[
# Index name (e.g. py-modindex)
str,
# Index class
Type[Index],
# list of (heading string, list of index entries) pairs.
List[Tuple[str, List[IndexEntry]]],
# whether sub-entries should start collapsed
bool
]
def get_stable_hash(obj: Any) -> str:
"""
@ -197,10 +209,10 @@ class StandaloneHTMLBuilder(Builder):
download_support = True # enable download role
imgpath: str = None
domain_indices: List[Tuple[str, Type[Index], List[Tuple[str, List[IndexEntry]]], bool]] = [] # NOQA
domain_indices: List[DOMAIN_INDEX_TYPE] = []
def __init__(self, app: Sphinx) -> None:
super().__init__(app)
def __init__(self, app: Sphinx, env: BuildEnvironment = None) -> None:
super().__init__(app, env)
# CSS files
self.css_files: List[Stylesheet] = []

View File

@ -7,11 +7,14 @@ import sys
import time
from collections import OrderedDict
from os import path
from typing import Any, Callable, Dict, List, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Union
# try to import readline, unix specific enhancement
try:
import readline
if TYPE_CHECKING and sys.platform == "win32": # always false, for type checking
raise ImportError
if readline.__doc__ and 'libedit' in readline.__doc__:
readline.parse_and_bind("bind ^I rl_complete")
USE_LIBEDIT = True

View File

@ -140,6 +140,7 @@ class Config:
'smartquotes_excludes': ({'languages': ['ja'],
'builders': ['man', 'text']},
'env', []),
'option_emphasise_placeholders': (False, 'env', []),
}
def __init__(self, config: Dict[str, Any] = {}, overrides: Dict[str, Any] = {}) -> None:

View File

@ -15,7 +15,7 @@ from sphinx.addnodes import desc_signature, pending_xref
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.roles import EmphasizedLiteral, XRefRole
from sphinx.util import docname_join, logging, ws_re
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import clean_astext, make_id, make_refnode
@ -34,6 +34,8 @@ option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)')
# RE for grammar tokens
token_re = re.compile(r'`((~?\w*:)?\w+)`', re.U)
samp_role = EmphasizedLiteral()
class GenericObject(ObjectDescription[str]):
"""
@ -170,15 +172,41 @@ class Cmdoption(ObjectDescription[str]):
location=signode)
continue
optname, args = m.groups()
if optname.endswith('[') and args.endswith(']'):
if optname[-1] == '[' and args[-1] == ']':
# optional value surrounded by brackets (ex. foo[=bar])
optname = optname[:-1]
args = '[' + args
if count:
signode += addnodes.desc_addname(', ', ', ')
if self.env.config.option_emphasise_placeholders:
signode += addnodes.desc_sig_punctuation(',', ',')
signode += addnodes.desc_sig_space()
else:
signode += addnodes.desc_addname(', ', ', ')
signode += addnodes.desc_name(optname, optname)
signode += addnodes.desc_addname(args, args)
if self.env.config.option_emphasise_placeholders:
add_end_bracket = False
if not args:
continue
if args[0] == '[' and args[-1] == ']':
add_end_bracket = True
signode += addnodes.desc_sig_punctuation('[', '[')
args = args[1:-1]
if args[0] == ' ':
signode += addnodes.desc_sig_space()
args = args.strip()
if args[0] == '=':
signode += addnodes.desc_sig_punctuation('=', '=')
args = args[1:]
for part in samp_role.parse(args):
if isinstance(part, nodes.Text):
signode += nodes.Text(part.astext())
else:
signode += part
if add_end_bracket:
signode += addnodes.desc_sig_punctuation(']', ']')
else:
signode += addnodes.desc_addname(args, args)
if not count:
firstname = optname
signode['allnames'] = [optname]
@ -573,11 +601,11 @@ class StandardDomain(Domain):
}
dangling_warnings = {
'term': 'term not in glossary: %(target)s',
'numref': 'undefined label: %(target)s',
'keyword': 'unknown keyword: %(target)s',
'doc': 'unknown document: %(target)s',
'option': 'unknown option: %(target)s',
'term': 'term not in glossary: %(target)r',
'numref': 'undefined label: %(target)r',
'keyword': 'unknown keyword: %(target)r',
'doc': 'unknown document: %(target)r',
'option': 'unknown option: %(target)r',
}
# node_class -> (figtype, title_getter)
@ -1072,9 +1100,9 @@ def warn_missing_reference(app: "Sphinx", domain: Domain, node: pending_xref
else:
target = node['reftarget']
if target not in domain.anonlabels: # type: ignore
msg = __('undefined label: %s')
msg = __('undefined label: %r')
else:
msg = __('Failed to create a cross reference. A title or caption not found: %s')
msg = __('Failed to create a cross reference. A title or caption not found: %r')
logger.warning(msg % target, location=node, type='ref', subtype=node['reftype'])
return True

View File

@ -3,7 +3,7 @@
import importlib
import traceback
import warnings
from typing import Any, Callable, Dict, List, NamedTuple, Optional
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional
from sphinx.ext.autodoc.mock import ismock, undecorate
from sphinx.pycode import ModuleAnalyzer, PycodeError
@ -11,10 +11,7 @@ from sphinx.util import logging
from sphinx.util.inspect import (getannotations, getmro, getslots, isclass, isenumclass,
safe_getattr)
if False:
# For type annotation
from typing import Type # NOQA
if TYPE_CHECKING:
from sphinx.ext.autodoc import ObjectMember
logger = logging.getLogger(__name__)

View File

@ -141,6 +141,9 @@ class _UnparseVisitor(ast.NodeVisitor):
return "%s.%s" % (self.visit(node.value), node.attr)
def visit_BinOp(self, node: ast.BinOp) -> str:
# Special case ``**`` to not have surrounding spaces.
if isinstance(node.op, ast.Pow):
return "".join(map(self.visit, (node.left, node.op, node.right)))
return " ".join(self.visit(e) for e in [node.left, node.op, node.right])
def visit_BoolOp(self, node: ast.BoolOp) -> str:
@ -202,7 +205,11 @@ class _UnparseVisitor(ast.NodeVisitor):
return "%s[%s]" % (self.visit(node.value), self.visit(node.slice))
def visit_UnaryOp(self, node: ast.UnaryOp) -> str:
return "%s %s" % (self.visit(node.op), self.visit(node.operand))
# UnaryOp is one of {UAdd, USub, Invert, Not}, which refer to ``+x``,
# ``-x``, ``~x``, and ``not x``. Only Not needs a space.
if isinstance(node.op, ast.Not):
return "%s %s" % (self.visit(node.op), self.visit(node.operand))
return "%s%s" % (self.visit(node.op), self.visit(node.operand))
def visit_Tuple(self, node: ast.Tuple) -> str:
if len(node.elts) == 0:

View File

@ -1,6 +1,7 @@
"""Sphinx component registry."""
import traceback
import warnings
from importlib import import_module
from types import MethodType
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Tuple, Type, Union
@ -19,6 +20,7 @@ except ImportError:
from sphinx.builders import Builder
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx70Warning
from sphinx.domains import Domain, Index, ObjType
from sphinx.domains.std import GenericObject, Target
from sphinx.environment import BuildEnvironment
@ -146,11 +148,23 @@ class SphinxComponentRegistry:
self.load_extension(app, entry_point.module)
def create_builder(self, app: "Sphinx", name: str) -> Builder:
def create_builder(self, app: "Sphinx", name: str,
env: BuildEnvironment = None) -> Builder:
if name not in self.builders:
raise SphinxError(__('Builder name %s not registered') % name)
return self.builders[name](app)
try:
return self.builders[name](app, env)
except TypeError:
warnings.warn(
f"The custom builder {name} defines a custom __init__ method without the "
f"'env'argument. Report this bug to the developers of your custom builder, "
f"this is likely not a issue with Sphinx. The 'env' argument will be required "
f"from Sphinx 7.", RemovedInSphinx70Warning, stacklevel=2)
builder = self.builders[name](app, env=...) # type: ignore[arg-type]
if env is not None:
builder.set_environment(env)
return builder
def add_domain(self, domain: Type[Domain], override: bool = False) -> None:
logger.debug('[app] adding domain: %r', domain)

View File

@ -2,8 +2,9 @@
from typing import Dict
import snowballstemmer
from sphinx.search import SearchLanguage
from sphinx.util.stemmer import get_stemmer
english_stopwords = set("""
a and are as at
@ -211,7 +212,7 @@ class SearchEnglish(SearchLanguage):
stopwords = english_stopwords
def init(self, options: Dict) -> None:
self.stemmer = get_stemmer()
self.stemmer = snowballstemmer.stemmer('porter')
def stem(self, word: str) -> str:
return self.stemmer.stem(word.lower())
return self.stemmer.stemWord(word.lower())

View File

@ -4,8 +4,9 @@ import os
import re
from typing import Dict, List
import snowballstemmer
from sphinx.search import SearchLanguage
from sphinx.util.stemmer import get_stemmer
try:
import jieba
@ -230,7 +231,7 @@ class SearchChinese(SearchLanguage):
if dict_path and os.path.isfile(dict_path):
jieba.load_userdict(dict_path)
self.stemmer = get_stemmer()
self.stemmer = snowballstemmer.stemmer('english')
def split(self, input: str) -> List[str]:
chinese: List[str] = []
@ -252,8 +253,8 @@ class SearchChinese(SearchLanguage):
should_not_be_stemmed = (
word in self.latin_terms and
len(word) >= 3 and
len(self.stemmer.stem(word.lower())) < 3
len(self.stemmer.stemWord(word.lower())) < 3
)
if should_not_be_stemmed:
return word.lower()
return self.stemmer.stem(word.lower())
return self.stemmer.stemWord(word.lower())

View File

@ -36,7 +36,7 @@
{%- macro agogo_sidebar() %}
{%- block sidebartoc %}
<h3>{{ _('Table of Contents') }}</h3>
{{ toctree() }}
{{ toctree(includehidden=True) }}
{%- endblock %}
{%- block sidebarsearch %}
<div role="search">

View File

@ -273,12 +273,6 @@ div.document ol {
div.sidebar,
aside.sidebar {
width: {{ theme_sidebarwidth|todim }};
{%- if theme_rightsidebar|tobool %}
float: right;
{%- else %}
float: left;
{%- endif %}
font-size: .9em;
}

View File

@ -23,6 +23,9 @@ def terminal_safe(s: str) -> str:
def get_terminal_width() -> int:
"""Borrowed from the py lib."""
if sys.platform == "win32":
# For static typing, as fcntl & termios never exist on Windows.
return int(os.environ.get('COLUMNS', 80)) - 1
try:
import fcntl
import struct
@ -32,7 +35,7 @@ def get_terminal_width() -> int:
terminal_width = width
except Exception:
# FALLBACK
terminal_width = int(os.environ.get('COLUMNS', "80")) - 1
terminal_width = int(os.environ.get('COLUMNS', 80)) - 1
return terminal_width

View File

@ -28,10 +28,6 @@ else:
MethodDescriptorType = type(str.join)
WrapperDescriptorType = type(dict.__dict__['fromkeys'])
if False:
# For type annotation
from typing import Type # NOQA
logger = logging.getLogger(__name__)
memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)

View File

@ -12,6 +12,7 @@ from docutils.utils import get_source_line
from sphinx.errors import SphinxWarning
from sphinx.util.console import colorize
from sphinx.util.osutil import abspath
if TYPE_CHECKING:
from sphinx.application import Sphinx
@ -381,8 +382,8 @@ class WarningSuppressor(logging.Filter):
super().__init__()
def filter(self, record: logging.LogRecord) -> bool:
type = getattr(record, 'type', None)
subtype = getattr(record, 'subtype', None)
type = getattr(record, 'type', '')
subtype = getattr(record, 'subtype', '')
try:
suppress_warnings = self.app.config.suppress_warnings
@ -514,6 +515,8 @@ class WarningLogRecordTranslator(SphinxLogRecordTranslator):
def get_node_location(node: Node) -> Optional[str]:
(source, line) = get_source_line(node)
if source:
source = abspath(source)
if source and line:
return "%s:%s" % (source, line)
elif source:

View File

@ -1,6 +1,7 @@
"""Parallel building utilities."""
import os
import sys
import time
import traceback
from math import sqrt
@ -16,6 +17,11 @@ from sphinx.util import logging
logger = logging.getLogger(__name__)
if sys.platform != "win32":
ForkProcess = multiprocessing.context.ForkProcess
else:
# For static typing, as ForkProcess doesn't exist on Windows
ForkProcess = multiprocessing.process.BaseProcess
# our parallel functionality only works for the forking Process
parallel_available = multiprocessing and os.name == 'posix'
@ -49,7 +55,7 @@ class ParallelTasks:
# task arguments
self._args: Dict[int, Optional[List[Any]]] = {}
# list of subprocesses (both started and waiting)
self._procs: Dict[int, multiprocessing.context.ForkProcess] = {}
self._procs: Dict[int, ForkProcess] = {}
# list of receiving pipe connections of running subprocesses
self._precvs: Dict[int, Any] = {}
# list of receiving pipe connections of waiting subprocesses

View File

@ -1,37 +1,62 @@
"""Word stemming utilities for Sphinx."""
from sphinx.util.stemmer.porter import PorterStemmer
import warnings
try:
from Stemmer import Stemmer as _PyStemmer
PYSTEMMER = True
except ImportError:
PYSTEMMER = False
import snowballstemmer
from sphinx.deprecation import RemovedInSphinx70Warning
class PorterStemmer:
def __init__(self):
warnings.warn(f"{self.__class__.__name__} is deprecated, use "
"snowballstemmer.stemmer('porter') instead.",
RemovedInSphinx70Warning, stacklevel=2)
self.stemmer = snowballstemmer.stemmer('porter')
def stem(self, p: str, i: int, j: int) -> str:
warnings.warn(f"{self.__class__.__name__}.stem() is deprecated, use "
"snowballstemmer.stemmer('porter').stemWord() instead.",
RemovedInSphinx70Warning, stacklevel=2)
return self.stemmer.stemWord(p)
class BaseStemmer:
def __init__(self):
warnings.warn(f"{self.__class__.__name__} is deprecated, use "
"snowballstemmer.stemmer('porter') instead.",
RemovedInSphinx70Warning, stacklevel=3)
def stem(self, word: str) -> str:
raise NotImplementedError()
raise NotImplementedError
class PyStemmer(BaseStemmer):
def __init__(self) -> None:
self.stemmer = _PyStemmer('porter')
def __init__(self): # NoQA
super().__init__()
self.stemmer = snowballstemmer.stemmer('porter')
def stem(self, word: str) -> str:
warnings.warn(f"{self.__class__.__name__}.stem() is deprecated, use "
"snowballstemmer.stemmer('porter').stemWord() instead.",
RemovedInSphinx70Warning, stacklevel=2)
return self.stemmer.stemWord(word)
class StandardStemmer(PorterStemmer, BaseStemmer):
"""All those porter stemmer implementations look hideous;
make at least the stem method nicer.
"""
def stem(self, word: str) -> str: # type: ignore
return super().stem(word, 0, len(word) - 1)
class StandardStemmer(BaseStemmer):
def __init__(self): # NoQA
super().__init__()
self.stemmer = snowballstemmer.stemmer('porter')
def stem(self, word: str) -> str:
warnings.warn(f"{self.__class__.__name__}.stem() is deprecated, use "
"snowballstemmer.stemmer('porter').stemWord() instead.",
RemovedInSphinx70Warning, stacklevel=2)
return self.stemmer.stemWord(word)
def get_stemmer() -> BaseStemmer:
if PYSTEMMER:
return PyStemmer()
else:
return StandardStemmer()
warnings.warn("get_stemmer() is deprecated, use "
"snowballstemmer.stemmer('porter') instead.",
RemovedInSphinx70Warning, stacklevel=2)
return PyStemmer()

View File

@ -1,406 +0,0 @@
"""Porter Stemming Algorithm
This is the Porter stemming algorithm, ported to Python from the
version coded up in ANSI C by the author. It may be be regarded
as canonical, in that it follows the algorithm presented in
Porter, 1980, An algorithm for suffix stripping, Program, Vol. 14,
no. 3, pp 130-137,
only differing from it at the points made --DEPARTURE-- below.
See also https://tartarus.org/martin/PorterStemmer/
The algorithm as described in the paper could be exactly replicated
by adjusting the points of DEPARTURE, but this is barely necessary,
because (a) the points of DEPARTURE are definitely improvements, and
(b) no encoding of the Porter stemmer I have seen is anything like
as exact as this version, even with the points of DEPARTURE!
Release 1: January 2001
:author: Vivake Gupta <v@nano.com>.
:license: Public Domain ("can be used free of charge for any purpose").
"""
class PorterStemmer:
def __init__(self) -> None:
"""The main part of the stemming algorithm starts here.
b is a buffer holding a word to be stemmed. The letters are in b[k0],
b[k0+1] ... ending at b[k]. In fact k0 = 0 in this demo program. k is
readjusted downwards as the stemming progresses. Zero termination is
not in fact used in the algorithm.
Note that only lower case sequences are stemmed. Forcing to lower case
should be done before stem(...) is called.
"""
self.b = "" # buffer for word to be stemmed
self.k = 0
self.k0 = 0
self.j = 0 # j is a general offset into the string
def cons(self, i: int) -> int:
"""cons(i) is TRUE <=> b[i] is a consonant."""
if self.b[i] == 'a' or self.b[i] == 'e' or self.b[i] == 'i' \
or self.b[i] == 'o' or self.b[i] == 'u':
return 0
if self.b[i] == 'y':
if i == self.k0:
return 1
else:
return (not self.cons(i - 1))
return 1
def m(self) -> int:
"""m() measures the number of consonant sequences between k0 and j.
if c is a consonant sequence and v a vowel sequence, and <..>
indicates arbitrary presence,
<c><v> gives 0
<c>vc<v> gives 1
<c>vcvc<v> gives 2
<c>vcvcvc<v> gives 3
....
"""
n = 0
i = self.k0
while 1:
if i > self.j:
return n
if not self.cons(i):
break
i = i + 1
i = i + 1
while 1:
while 1:
if i > self.j:
return n
if self.cons(i):
break
i = i + 1
i = i + 1
n = n + 1
while 1:
if i > self.j:
return n
if not self.cons(i):
break
i = i + 1
i = i + 1
def vowelinstem(self) -> int:
"""vowelinstem() is TRUE <=> k0,...j contains a vowel"""
for i in range(self.k0, self.j + 1):
if not self.cons(i):
return 1
return 0
def doublec(self, j: int) -> int:
"""doublec(j) is TRUE <=> j,(j-1) contain a double consonant."""
if j < (self.k0 + 1):
return 0
if (self.b[j] != self.b[j - 1]):
return 0
return self.cons(j)
def cvc(self, i: int) -> int:
"""cvc(i) is TRUE <=> i-2,i-1,i has the form
consonant - vowel - consonant
and also if the second c is not w,x or y. this is used when trying to
restore an e at the end of a short e.g.
cav(e), lov(e), hop(e), crim(e), but
snow, box, tray.
"""
if i < (self.k0 + 2) or not self.cons(i) or self.cons(i - 1) \
or not self.cons(i - 2):
return 0
ch = self.b[i]
if ch in ('w', 'x', 'y'):
return 0
return 1
def ends(self, s: str) -> int:
"""ends(s) is TRUE <=> k0,...k ends with the string s."""
length = len(s)
if s[length - 1] != self.b[self.k]: # tiny speed-up
return 0
if length > (self.k - self.k0 + 1):
return 0
if self.b[self.k - length + 1:self.k + 1] != s:
return 0
self.j = self.k - length
return 1
def setto(self, s: str) -> None:
"""setto(s) sets (j+1),...k to the characters in the string s,
readjusting k."""
length = len(s)
self.b = self.b[:self.j + 1] + s + self.b[self.j + length + 1:]
self.k = self.j + length
def r(self, s: str) -> None:
"""r(s) is used further down."""
if self.m() > 0:
self.setto(s)
def step1ab(self) -> None:
"""step1ab() gets rid of plurals and -ed or -ing. e.g.
caresses -> caress
ponies -> poni
ties -> ti
caress -> caress
cats -> cat
feed -> feed
agreed -> agree
disabled -> disable
matting -> mat
mating -> mate
meeting -> meet
milling -> mill
messing -> mess
meetings -> meet
"""
if self.b[self.k] == 's':
if self.ends("sses"):
self.k = self.k - 2
elif self.ends("ies"):
self.setto("i")
elif self.b[self.k - 1] != 's':
self.k = self.k - 1
if self.ends("eed"):
if self.m() > 0:
self.k = self.k - 1
elif (self.ends("ed") or self.ends("ing")) and self.vowelinstem():
self.k = self.j
if self.ends("at"):
self.setto("ate")
elif self.ends("bl"):
self.setto("ble")
elif self.ends("iz"):
self.setto("ize")
elif self.doublec(self.k):
self.k = self.k - 1
ch = self.b[self.k]
if ch in ('l', 's', 'z'):
self.k = self.k + 1
elif (self.m() == 1 and self.cvc(self.k)):
self.setto("e")
def step1c(self) -> None:
"""step1c() turns terminal y to i when there is another vowel in
the stem."""
if (self.ends("y") and self.vowelinstem()):
self.b = self.b[:self.k] + 'i' + self.b[self.k + 1:]
def step2(self) -> None:
"""step2() maps double suffices to single ones.
so -ization ( = -ize plus -ation) maps to -ize etc. note that the
string before the suffix must give m() > 0.
"""
if self.b[self.k - 1] == 'a':
if self.ends("ational"):
self.r("ate")
elif self.ends("tional"):
self.r("tion")
elif self.b[self.k - 1] == 'c':
if self.ends("enci"):
self.r("ence")
elif self.ends("anci"):
self.r("ance")
elif self.b[self.k - 1] == 'e':
if self.ends("izer"):
self.r("ize")
elif self.b[self.k - 1] == 'l':
if self.ends("bli"):
self.r("ble") # --DEPARTURE--
# To match the published algorithm, replace this phrase with
# if self.ends("abli"): self.r("able")
elif self.ends("alli"):
self.r("al")
elif self.ends("entli"):
self.r("ent")
elif self.ends("eli"):
self.r("e")
elif self.ends("ousli"):
self.r("ous")
elif self.b[self.k - 1] == 'o':
if self.ends("ization"):
self.r("ize")
elif self.ends("ation"):
self.r("ate")
elif self.ends("ator"):
self.r("ate")
elif self.b[self.k - 1] == 's':
if self.ends("alism"):
self.r("al")
elif self.ends("iveness"):
self.r("ive")
elif self.ends("fulness"):
self.r("ful")
elif self.ends("ousness"):
self.r("ous")
elif self.b[self.k - 1] == 't':
if self.ends("aliti"):
self.r("al")
elif self.ends("iviti"):
self.r("ive")
elif self.ends("biliti"):
self.r("ble")
elif self.b[self.k - 1] == 'g': # --DEPARTURE--
if self.ends("logi"):
self.r("log")
# To match the published algorithm, delete this phrase
def step3(self) -> None:
"""step3() dels with -ic-, -full, -ness etc. similar strategy
to step2."""
if self.b[self.k] == 'e':
if self.ends("icate"):
self.r("ic")
elif self.ends("ative"):
self.r("")
elif self.ends("alize"):
self.r("al")
elif self.b[self.k] == 'i':
if self.ends("iciti"):
self.r("ic")
elif self.b[self.k] == 'l':
if self.ends("ical"):
self.r("ic")
elif self.ends("ful"):
self.r("")
elif self.b[self.k] == 's':
if self.ends("ness"):
self.r("")
def step4(self) -> None:
"""step4() takes off -ant, -ence etc., in context <c>vcvc<v>."""
if self.b[self.k - 1] == 'a':
if self.ends("al"):
pass
else:
return
elif self.b[self.k - 1] == 'c':
if self.ends("ance"):
pass
elif self.ends("ence"):
pass
else:
return
elif self.b[self.k - 1] == 'e':
if self.ends("er"):
pass
else:
return
elif self.b[self.k - 1] == 'i':
if self.ends("ic"):
pass
else:
return
elif self.b[self.k - 1] == 'l':
if self.ends("able"):
pass
elif self.ends("ible"):
pass
else:
return
elif self.b[self.k - 1] == 'n':
if self.ends("ant"):
pass
elif self.ends("ement"):
pass
elif self.ends("ment"):
pass
elif self.ends("ent"):
pass
else:
return
elif self.b[self.k - 1] == 'o':
if self.ends("ion") and (self.b[self.j] == 's' or
self.b[self.j] == 't'):
pass
elif self.ends("ou"):
pass
# takes care of -ous
else:
return
elif self.b[self.k - 1] == 's':
if self.ends("ism"):
pass
else:
return
elif self.b[self.k - 1] == 't':
if self.ends("ate"):
pass
elif self.ends("iti"):
pass
else:
return
elif self.b[self.k - 1] == 'u':
if self.ends("ous"):
pass
else:
return
elif self.b[self.k - 1] == 'v':
if self.ends("ive"):
pass
else:
return
elif self.b[self.k - 1] == 'z':
if self.ends("ize"):
pass
else:
return
else:
return
if self.m() > 1:
self.k = self.j
def step5(self) -> None:
"""step5() removes a final -e if m() > 1, and changes -ll to -l if
m() > 1.
"""
self.j = self.k
if self.b[self.k] == 'e':
a = self.m()
if a > 1 or (a == 1 and not self.cvc(self.k - 1)):
self.k = self.k - 1
if self.b[self.k] == 'l' and self.doublec(self.k) and self.m() > 1:
self.k = self.k - 1
def stem(self, p: str, i: int, j: int) -> str:
"""In stem(p,i,j), p is a char pointer, and the string to be stemmed
is from p[i] to p[j] inclusive. Typically i is zero and j is the
offset to the last character of a string, (p[j+1] == '\0'). The
stemmer adjusts the characters p[i] ... p[j] and returns the new
end-point of the string, k. Stemming never increases word length, so
i <= k <= j. To turn the stemmer into a module, declare 'stem' as
extern, and delete the remainder of this file.
"""
# copy the parameters into statics
self.b = p
self.k = j
self.k0 = i
if self.k <= self.k0 + 1:
return self.b # --DEPARTURE--
# With this line, strings of length 1 or 2 don't go through the
# stemming process, although no mention is made of this in the
# published algorithm. Remove the line to match the published
# algorithm.
self.step1ab()
self.step1c()
self.step2()
self.step3()
self.step4()
self.step5()
return self.b[self.k0:self.k + 1]

View File

@ -31,11 +31,6 @@ try:
except ImportError:
UnionType = None
if False:
# For type annotation
from typing import Type # NOQA # for python3.5.1
# builtin classes that have incorrect __module__
INVALID_BUILTIN_CLASSES = {
Struct: 'struct.Struct', # Before Python 3.9

View File

@ -194,6 +194,15 @@ Link to :option:`perl +p`, :option:`--ObjC++`, :option:`--plugin.option`, :optio
Link to :option:`hg commit` and :option:`git commit -p`.
.. option:: --abi={TYPE}
.. option:: --test={WHERE}-{COUNT}
.. option:: --wrap=\{\{value\}\}
.. option:: -allowable_client {client_name}
Foo bar.
User markup
===========

View File

@ -1,15 +1,46 @@
"""Test the Sphinx class."""
import shutil
import sys
from io import StringIO
from pathlib import Path
from unittest.mock import Mock
import pytest
from docutils import nodes
import sphinx.application
from sphinx.errors import ExtensionError
from sphinx.testing.util import strip_escseq
from sphinx.testing.path import path
from sphinx.testing.util import SphinxTestApp, strip_escseq
from sphinx.util import logging
def test_instantiation(tmp_path_factory, rootdir: str, monkeypatch):
# Given
src_dir = tmp_path_factory.getbasetemp() / 'root'
# special support for sphinx/tests
if rootdir and not src_dir.exists():
shutil.copytree(Path(str(rootdir)) / 'test-root', src_dir)
monkeypatch.setattr('sphinx.application.abspath', lambda x: x)
syspath = sys.path[:]
# When
app_ = SphinxTestApp(
srcdir=path(src_dir),
status=StringIO(),
warning=StringIO()
)
sys.path[:] = syspath
app_.cleanup()
# Then
assert isinstance(app_, sphinx.application.Sphinx)
def test_events(app, status, warning):
def empty():
pass

View File

@ -39,7 +39,7 @@ with "\\?": b?'here: >>>(\\\\|/)xbb<<<((\\\\|/)r)?'
"""
HTML_WARNINGS = ENV_WARNINGS + """\
%(root)s/index.rst:\\d+: WARNING: unknown option: &option
%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
%(root)s/index.rst:\\d+: WARNING: citation not found: missing
%(root)s/index.rst:\\d+: WARNING: a suitable image for html builder not found: foo.\\*
%(root)s/index.rst:\\d+: WARNING: Could not lex literal_block as "c". Highlighting skipped.
@ -1719,3 +1719,25 @@ def test_html_code_role(app):
assert ('<div class="highlight-python notranslate">' +
'<div class="highlight"><pre><span></span>' +
common_content) in content
@pytest.mark.sphinx('html', testroot='root',
confoverrides={'option_emphasise_placeholders': True})
def test_option_emphasise_placeholders(app, status, warning):
app.build()
content = (app.outdir / 'objects.html').read_text()
assert '<em><span class="pre">TYPE</span></em>' in content
assert '{TYPE}' not in content
assert ('<em><span class="pre">WHERE</span></em>'
'<span class="pre">-</span>'
'<em><span class="pre">COUNT</span></em>' in content)
assert '<span class="pre">{{value}}</span>' in content
@pytest.mark.sphinx('html', testroot='root')
def test_option_emphasise_placeholders_default(app, status, warning):
app.build()
content = (app.outdir / 'objects.html').read_text()
assert '<span class="pre">={TYPE}</span>' in content
assert '<span class="pre">={WHERE}-{COUNT}</span></span>' in content
assert '<span class="pre">{client_name}</span>' in content

View File

@ -25,7 +25,7 @@ STYLEFILES = ['article.cls', 'fancyhdr.sty', 'titlesec.sty', 'amsmath.sty',
'fncychap.sty', 'geometry.sty', 'kvoptions.sty', 'hyperref.sty']
LATEX_WARNINGS = ENV_WARNINGS + """\
%(root)s/index.rst:\\d+: WARNING: unknown option: &option
%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
%(root)s/index.rst:\\d+: WARNING: citation not found: missing
%(root)s/index.rst:\\d+: WARNING: a suitable image for latex builder not found: foo.\\*
%(root)s/index.rst:\\d+: WARNING: Could not lex literal_block as "c". Highlighting skipped.

View File

@ -17,7 +17,7 @@ from sphinx.writers.texinfo import TexinfoTranslator
from .test_build_html import ENV_WARNINGS
TEXINFO_WARNINGS = ENV_WARNINGS + """\
%(root)s/index.rst:\\d+: WARNING: unknown option: &option
%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
%(root)s/index.rst:\\d+: WARNING: citation not found: missing
%(root)s/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: foo.\\*
%(root)s/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: \

View File

@ -452,6 +452,33 @@ def test_pyfunction_signature_full(app):
[desc_sig_name, pending_xref, "str"])])])
def test_pyfunction_with_unary_operators(app):
text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)"
doctree = restructuredtext.parse(app, text)
assert_node(doctree[1][0][1],
[desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"],
[desc_sig_operator, "="],
[nodes.inline, "+1"])],
[desc_parameter, ([desc_sig_name, "bacon"],
[desc_sig_operator, "="],
[nodes.inline, "-1"])],
[desc_parameter, ([desc_sig_name, "sausage"],
[desc_sig_operator, "="],
[nodes.inline, "~1"])],
[desc_parameter, ([desc_sig_name, "spam"],
[desc_sig_operator, "="],
[nodes.inline, "not spam"])])])
def test_pyfunction_with_binary_operators(app):
text = ".. py:function:: menu(spam=2**64)"
doctree = restructuredtext.parse(app, text)
assert_node(doctree[1][0][1],
[desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"],
[desc_sig_operator, "="],
[nodes.inline, "2**64"])])])
@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
def test_pyfunction_signature_full_py38(app):
# case: separator at head
@ -1342,6 +1369,6 @@ def test_python_python_use_unqualified_type_names_disabled(app, status, warning)
@pytest.mark.sphinx('dummy', testroot='domain-py-xref-warning')
def test_warn_missing_reference(app, status, warning):
app.build()
assert 'index.rst:6: WARNING: undefined label: no-label' in warning.getvalue()
assert ('index.rst:6: WARNING: Failed to create a cross reference. A title or caption not found: existing-label'
in warning.getvalue())
assert "index.rst:6: WARNING: undefined label: 'no-label'" in warning.getvalue()
assert ("index.rst:6: WARNING: Failed to create a cross reference. "
"A title or caption not found: 'existing-label'") in warning.getvalue()

View File

@ -49,8 +49,7 @@ def test_images(app):
app.build()
tree = app.env.get_doctree('images')
htmlbuilder = StandaloneHTMLBuilder(app)
htmlbuilder.set_environment(app.env)
htmlbuilder = StandaloneHTMLBuilder(app, app.env)
htmlbuilder.init()
htmlbuilder.imgpath = 'dummy'
htmlbuilder.post_process_images(tree)
@ -59,8 +58,7 @@ def test_images(app):
assert set(htmlbuilder.images.values()) == \
{'img.png', 'img1.png', 'simg.png', 'svgimg.svg', 'img.foo.png'}
latexbuilder = LaTeXBuilder(app)
latexbuilder.set_environment(app.env)
latexbuilder = LaTeXBuilder(app, app.env)
latexbuilder.init()
latexbuilder.post_process_images(tree)
assert set(latexbuilder.images.keys()) == \

View File

@ -156,7 +156,7 @@ def test_get_toc_for(app):
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
def test_get_toc_for_only(app):
app.build()
builder = StandaloneHTMLBuilder(app)
builder = StandaloneHTMLBuilder(app, app.env)
toctree = TocTree(app.env).get_toc_for('index', builder)
assert_node(toctree,

View File

@ -74,4 +74,4 @@ def test_autosectionlabel_maxdepth(app, status, warning):
html = '<li><p><span class="xref std std-ref">Linux</span></p></li>'
assert re.search(html, content, re.S)
assert 'WARNING: undefined label: linux' in warning.getvalue()
assert "WARNING: undefined label: 'linux'" in warning.getvalue()

View File

@ -106,8 +106,7 @@ def verify_re_html(app, parse):
def verify_re_latex(app, parse):
def verify(rst, latex_expected):
document = parse(rst)
app.builder = LaTeXBuilder(app)
app.builder.set_environment(app.env)
app.builder = LaTeXBuilder(app, app.env)
app.builder.init()
theme = app.builder.themes.get('manual')
latex_translator = ForgivingLaTeXTranslator(document, app.builder, theme)

View File

@ -25,7 +25,7 @@ from sphinx.pycode import ast
("...", "..."), # Ellipsis
("a // b", "a // b"), # FloorDiv
("Tuple[int, int]", "Tuple[int, int]"), # Index, Subscript
("~ 1", "~ 1"), # Invert
("~1", "~1"), # Invert
("lambda x, y: x + y",
"lambda x, y: ..."), # Lambda
("[1, 2, 3]", "[1, 2, 3]"), # List
@ -37,14 +37,14 @@ from sphinx.pycode import ast
("1234", "1234"), # Num
("not a", "not a"), # Not
("a or b", "a or b"), # Or
("a ** b", "a ** b"), # Pow
("a**b", "a**b"), # Pow
("a >> b", "a >> b"), # RShift
("{1, 2, 3}", "{1, 2, 3}"), # Set
("a - b", "a - b"), # Sub
("'str'", "'str'"), # Str
("+ a", "+ a"), # UAdd
("- 1", "- 1"), # UnaryOp
("- a", "- a"), # USub
("+a", "+a"), # UAdd
("-1", "-1"), # UnaryOp
("-a", "-a"), # USub
("(1, 2, 3)", "(1, 2, 3)"), # Tuple
("()", "()"), # Tuple (empty)
("(1,)", "(1,)"), # Tuple (single item)

View File

@ -111,6 +111,9 @@ def test_complex_assignment():
'f = g = None #: multiple assignment at once\n'
'(theta, phi) = (0, 0.5) #: unpack assignment via tuple\n'
'[x, y] = (5, 6) #: unpack assignment via list\n'
'h, *i, j = (1, 2, 3, 4) #: unpack assignment2\n'
'k, *self.attr = (5, 6, 7) #: unpack assignment3\n'
'l, *m[0] = (8, 9, 0) #: unpack assignment4\n'
)
parser = Parser(source)
parser.parse()
@ -124,22 +127,11 @@ def test_complex_assignment():
('', 'phi'): 'unpack assignment via tuple',
('', 'x'): 'unpack assignment via list',
('', 'y'): 'unpack assignment via list',
}
assert parser.definitions == {}
def test_complex_assignment_py3():
source = ('a, *b, c = (1, 2, 3, 4) #: unpack assignment\n'
'd, *self.attr = (5, 6, 7) #: unpack assignment2\n'
'e, *f[0] = (8, 9, 0) #: unpack assignment3\n'
)
parser = Parser(source)
parser.parse()
assert parser.comments == {('', 'a'): 'unpack assignment',
('', 'b'): 'unpack assignment',
('', 'c'): 'unpack assignment',
('', 'd'): 'unpack assignment2',
('', 'e'): 'unpack assignment3',
('', 'h'): 'unpack assignment2',
('', 'i'): 'unpack assignment2',
('', 'j'): 'unpack assignment2',
('', 'k'): 'unpack assignment3',
('', 'l'): 'unpack assignment4',
}
assert parser.definitions == {}

View File

@ -2,13 +2,14 @@
import codecs
import os
import os.path
import pytest
from docutils import nodes
from sphinx.errors import SphinxWarning
from sphinx.testing.util import strip_escseq
from sphinx.util import logging
from sphinx.util import logging, osutil
from sphinx.util.console import colorize
from sphinx.util.logging import is_suppressed_warning, prefixed_warnings
from sphinx.util.parallel import ParallelTasks
@ -379,3 +380,18 @@ def test_prefixed_warnings(app, status, warning):
assert 'WARNING: Another PREFIX: message3' in warning.getvalue()
assert 'WARNING: PREFIX: message4' in warning.getvalue()
assert 'WARNING: message5' in warning.getvalue()
def test_get_node_location_abspath():
# Ensure that node locations are reported as an absolute path,
# even if the source attribute is a relative path.
relative_filename = os.path.join('relative', 'path.txt')
absolute_filename = osutil.abspath(relative_filename)
n = nodes.Node()
n.source = relative_filename
location = logging.get_node_location(n)
assert location == absolute_filename + ':'

View File

@ -81,9 +81,9 @@ basepython = python3
description =
Lint documentation.
extras =
docs
lint
commands =
python utils/doclinter.py CHANGES CONTRIBUTING.rst README.rst doc/
sphinx-lint --disable missing-space-after-literal --enable line-too-long --max-line-length 85 CHANGES CONTRIBUTING.rst README.rst doc/
[testenv:twine]
basepython = python3

View File

@ -1,77 +0,0 @@
"""A linter for Sphinx docs"""
import os
import re
import sys
from typing import List
MAX_LINE_LENGTH = 85
LONG_INTERPRETED_TEXT = re.compile(r'^\s*\W*(:(\w+:)+)?`.*`\W*$')
CODE_BLOCK_DIRECTIVE = re.compile(r'^(\s*)\.\. code-block::')
LEADING_SPACES = re.compile(r'^(\s*)')
def lint(path: str) -> int:
with open(path, encoding='utf-8') as f:
document = f.readlines()
errors = 0
in_code_block = False
code_block_depth = 0
for i, line in enumerate(document):
if line.endswith(' '):
print('%s:%d: the line ends with whitespace.' %
(path, i + 1))
errors += 1
matched = CODE_BLOCK_DIRECTIVE.match(line)
if matched:
in_code_block = True
code_block_depth = len(matched.group(1))
elif in_code_block:
if line.strip() == '':
pass
else:
spaces = LEADING_SPACES.match(line).group(1)
if len(spaces) <= code_block_depth:
in_code_block = False
elif LONG_INTERPRETED_TEXT.match(line):
pass
elif len(line) > MAX_LINE_LENGTH:
if re.match(r'^\s*\.\. ', line):
# ignore directives and hyperlink targets
pass
elif re.match(r'^\s*__ ', line):
# ignore anonymous hyperlink targets
pass
elif re.match(r'^\s*``[^`]+``$', line):
# ignore a very long literal string
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 path in args:
if os.path.isfile(path):
errors += lint(path)
elif os.path.isdir(path):
for root, _dirs, files in os.walk(path):
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:]))