diff options
61 files changed, 426 insertions, 662 deletions
diff --git a/.github/workflows/builddoc.yml b/.github/workflows/builddoc.yml index 88082b31e..b045fcfc2 100644 --- a/.github/workflows/builddoc.yml +++ b/.github/workflows/builddoc.yml @@ -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 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 12945f665..091abd5c2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -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 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index cd86ec2fd..2da2ad3ea 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -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/') diff --git a/.github/workflows/docutils-latest.yml b/.github/workflows/docutils-latest.yml index b082ad193..7a730d657 100644 --- a/.github/workflows/docutils-latest.yml +++ b/.github/workflows/docutils-latest.yml @@ -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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fb85629e5..48c21510a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4dfae1781..6f14eac32 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index be8d7f027..ac92ac7f3 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -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" diff --git a/.github/workflows/transifex.yml b/.github/workflows/transifex.yml index ba8b6a734..d986293f8 100644 --- a/.github/workflows/transifex.yml +++ b/.github/workflows/transifex.yml @@ -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 @@ -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 @@ -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 @@ -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: diff --git a/doc/development/index.rst b/doc/development/index.rst index b4a7920ba..8ae71e76f 100644 --- a/doc/development/index.rst +++ b/doc/development/index.rst @@ -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`. diff --git a/doc/development/tutorials/autodoc_ext.rst b/doc/development/tutorials/autodoc_ext.rst index d8905710c..8de2e4d4a 100644 --- a/doc/development/tutorials/autodoc_ext.rst +++ b/doc/development/tutorials/autodoc_ext.rst @@ -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 - - diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 98bd463a9..d88eb27b0 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -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()`` diff --git a/doc/latex.rst b/doc/latex.rst index b49714e88..294de5599 100644 --- a/doc/latex.rst +++ b/doc/latex.rst @@ -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 diff --git a/doc/templating.rst b/doc/templating.rst index d9755a836..f2f4022ad 100644 --- a/doc/templating.rst +++ b/doc/templating.rst @@ -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. diff --git a/doc/tutorial/deploying.rst b/doc/tutorial/deploying.rst index 85fc6643a..cc62fd428 100644 --- a/doc/tutorial/deploying.rst +++ b/doc/tutorial/deploying.rst @@ -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/ diff --git a/doc/tutorial/describing-code.rst b/doc/tutorial/describing-code.rst index 57f1b2008..24fea38a6 100644 --- a/doc/tutorial/describing-code.rst +++ b/doc/tutorial/describing-code.rst @@ -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 diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index d4f482e4e..182f09e49 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -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: diff --git a/doc/usage/extensions/intersphinx.rst b/doc/usage/extensions/intersphinx.rst index 6b7b1e1bd..a70c7c531 100644 --- a/doc/usage/extensions/intersphinx.rst +++ b/doc/usage/extensions/intersphinx.rst @@ -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 diff --git a/doc/usage/extensions/napoleon.rst b/doc/usage/extensions/napoleon.rst index ad5af65c6..59ecad890 100644 --- a/doc/usage/extensions/napoleon.rst +++ b/doc/usage/extensions/napoleon.rst @@ -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. diff --git a/doc/usage/installation.rst b/doc/usage/installation.rst index 997bd40ca..8a597ce01 100644 --- a/doc/usage/installation.rst +++ b/doc/usage/installation.rst @@ -118,7 +118,7 @@ Chocolatey :: $ choco install sphinx - + You would need to `install Chocolatey <https://chocolatey.org/install>`_ before running this. diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 30bde8ea1..54e325400 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -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 diff --git a/doc/usage/restructuredtext/roles.rst b/doc/usage/restructuredtext/roles.rst index 9d790b30e..e2755dd4e 100644 --- a/doc/usage/restructuredtext/roles.rst +++ b/doc/usage/restructuredtext/roles.rst @@ -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. @@ -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 = @@ -42,6 +42,7 @@ extras_require = { 'flake8>=3.5.0', 'isort', 'mypy>=0.950', + 'sphinx-lint', 'docutils-stubs', "types-typed-ast", "types-requests", diff --git a/sphinx/application.py b/sphinx/application.py index 0aceff56b..218801322 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -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: diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index d8500e11b..9705ba894 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -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) diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index b76739523..7737a1d38 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -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] = [] diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index 47853c90d..610052ea9 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -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 diff --git a/sphinx/config.py b/sphinx/config.py index 8c6dcfe32..318173f27 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -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: diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index d5c962dc8..88a4d28cb 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -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 diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index d392ae75d..977cfbba4 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -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__) diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index 755116475..d4646f0b7 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -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: diff --git a/sphinx/registry.py b/sphinx/registry.py index 87864b311..d08ba71a7 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -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) diff --git a/sphinx/search/en.py b/sphinx/search/en.py index 53cd917dc..19bd9f019 100644 --- a/sphinx/search/en.py +++ b/sphinx/search/en.py @@ -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()) diff --git a/sphinx/search/zh.py b/sphinx/search/zh.py index 700c2683f..86f612d5d 100644 --- a/sphinx/search/zh.py +++ b/sphinx/search/zh.py @@ -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()) diff --git a/sphinx/themes/agogo/layout.html b/sphinx/themes/agogo/layout.html index e89657ba1..d76050c9b 100644 --- a/sphinx/themes/agogo/layout.html +++ b/sphinx/themes/agogo/layout.html @@ -36,7 +36,7 @@ {%- macro agogo_sidebar() %} {%- block sidebartoc %} <h3>{{ _('Table of Contents') }}</h3> - {{ toctree() }} + {{ toctree(includehidden=True) }} {%- endblock %} {%- block sidebarsearch %} <div role="search"> diff --git a/sphinx/themes/agogo/static/agogo.css_t b/sphinx/themes/agogo/static/agogo.css_t index 14c5e52ce..53c4c3848 100644 --- a/sphinx/themes/agogo/static/agogo.css_t +++ b/sphinx/themes/agogo/static/agogo.css_t @@ -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; } diff --git a/sphinx/util/console.py b/sphinx/util/console.py index abdbf4219..88b208470 100644 --- a/sphinx/util/console.py +++ b/sphinx/util/console.py @@ -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 diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index a807ceb83..3d89a4f6e 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -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) diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index 37fa672af..d43116f87 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -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: diff --git a/sphinx/util/parallel.py b/sphinx/util/parallel.py index e4bd852b0..193d2a80d 100644 --- a/sphinx/util/parallel.py +++ b/sphinx/util/parallel.py @@ -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 diff --git a/sphinx/util/stemmer/__init__.py b/sphinx/util/stemmer/__init__.py index ff6c365c7..6d27592d8 100644 --- a/sphinx/util/stemmer/__init__.py +++ b/sphinx/util/stemmer/__init__.py @@ -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() diff --git a/sphinx/util/stemmer/porter.py b/sphinx/util/stemmer/porter.py deleted file mode 100644 index c4f89eb95..000000000 --- a/sphinx/util/stemmer/porter.py +++ /dev/null @@ -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] diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 62bd462b8..430d22d16 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -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 diff --git a/tests/roots/test-root/objects.txt b/tests/roots/test-root/objects.txt index 7e2db1bb8..a4a5c667c 100644 --- a/tests/roots/test-root/objects.txt +++ b/tests/roots/test-root/objects.txt @@ -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 =========== diff --git a/tests/test_application.py b/tests/test_application.py index 365fff8ea..90758a939 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -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 diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 6a653664e..cb036a1af 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -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 diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 709dce05d..9a325a8d4 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -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. diff --git a/tests/test_build_texinfo.py b/tests/test_build_texinfo.py index 772644abe..5c72a3449 100644 --- a/tests/test_build_texinfo.py +++ b/tests/test_build_texinfo.py @@ -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: \ diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 014067e84..ce1636eb2 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -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() diff --git a/tests/test_environment.py b/tests/test_environment.py index 7ffca7898..c6f6b5aba 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -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()) == \ diff --git a/tests/test_environment_toctree.py b/tests/test_environment_toctree.py index 588bcac18..60a9826fd 100644 --- a/tests/test_environment_toctree.py +++ b/tests/test_environment_toctree.py @@ -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, diff --git a/tests/test_ext_autosectionlabel.py b/tests/test_ext_autosectionlabel.py index f950b8f1d..f99a6d3f6 100644 --- a/tests/test_ext_autosectionlabel.py +++ b/tests/test_ext_autosectionlabel.py @@ -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() diff --git a/tests/test_markup.py b/tests/test_markup.py index 9e6165a5f..f15761c5e 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -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) diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py index 6143105eb..31018baca 100644 --- a/tests/test_pycode_ast.py +++ b/tests/test_pycode_ast.py @@ -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) diff --git a/tests/test_pycode_parser.py b/tests/test_pycode_parser.py index 5d2496ba5..fde648d35 100644 --- a/tests/test_pycode_parser.py +++ b/tests/test_pycode_parser.py @@ -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 == {} diff --git a/tests/test_util_logging.py b/tests/test_util_logging.py index 49cd2c11e..b9756f947 100644 --- a/tests/test_util_logging.py +++ b/tests/test_util_logging.py @@ -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 + ':' @@ -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 diff --git a/utils/doclinter.py b/utils/doclinter.py deleted file mode 100644 index d67a49b05..000000000 --- a/utils/doclinter.py +++ /dev/null @@ -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:])) |