diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6f4fe672f..c8f8969a1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ Subject: - Critical or severe bugs: X.Y.Z - Others: X.Y - For more details, see https://www.sphinx-doc.org/en/master/devguide.html#branch-model + For more details, see https://www.sphinx-doc.org/en/master/internals/release-process.html#branch-model --> ### Feature or Bugfix diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fea1f17a2..913abcedd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - tool: [docslint, flake8, mypy, twine] + tool: [docslint, flake8, isort, mypy, twine] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7acfef6d2..89dc8fde9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,64 @@ -name: CI on Windows +name: CI on: [push, pull_request] jobs: - build: + ubuntu: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + name: [py35, py36, py37, py38, py39] + os: [ubuntu-16.04] + include: + - name: py35 + python: 3.5 + docutils: du12 + - name: py36 + python: 3.6 + docutils: du13 + - name: py37 + python: 3.7 + docutils: du14 + - name: py38 + python: 3.8 + docutils: du15 + - name: py39 + python: 3.9 + docutils: du16 + coverage: "--cov ./ --cov-append --cov-config setup.cfg" + - name: py310-dev + python: 3.10-dev + docutils: du16 + os: ubuntu-latest # required + env: + PYTEST_ADDOPTS: ${{ matrix.coverage }} + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v2 + if: "!endsWith(matrix.python, '-dev')" + with: + python-version: ${{ matrix.python }} + - name: Set up Python ${{ matrix.python }} (deadsnakes) + uses: deadsnakes/action@v2.0.1 + if: endsWith(matrix.python, '-dev') + with: + python-version: ${{ matrix.python }} + - name: Check Python version + run: python --version + - name: Install graphviz + run: sudo apt-get install graphviz + - name: Install dependencies + run: pip install -U tox codecov + - name: Run Tox + run: tox -e ${{ matrix.docutils }} -- -vv + - name: codecov + uses: codecov/codecov-action@v1 + if: matrix.coverage + + windows: runs-on: windows-latest strategy: matrix: diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 000000000..d7a7c95f1 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,21 @@ +name: CI (node.js) + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + env: + node-version: 10.7 + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ env.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ env.node-version }} + - run: npm install + - name: Run headless test + uses: GabrielBB/xvfb-action@v1 + with: + run: npm test diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..680a0e3b5 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,8 @@ +version: 2 +python: + version: 3 + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d73be03ec..000000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -os: linux -dist: xenial -language: python -cache: pip - -env: - global: - - PYTHONFAULTHANDLER=x - - SKIP_LATEX_BUILD=1 - - IS_PYTHON=true - -jobs: - include: - - python: '3.5' - env: - - TOXENV=du12 - - python: '3.6' - env: - - TOXENV=du13 - - python: '3.7' - env: - - TOXENV=du14 - - python: '3.8' - env: - - TOXENV=du15 - - PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg" - - python: 'nightly' - env: - - TOXENV=du16 - - - language: node_js - node_js: '10.7' - env: IS_PYTHON=false - services: xvfb - -install: - - "sudo apt-get install graphviz" - - if [ $IS_PYTHON = true ]; then pip install -U tox codecov; fi - - if [ $IS_PYTHON = false ]; then npm install; fi - -script: - - if [ $IS_PYTHON = true ]; then tox -- -vv; fi - - if [ $IS_PYTHON = false ]; then npm test; fi - -after_success: - - if [[ -e .coverage ]]; then codecov -e $TOXENV; fi diff --git a/CHANGES b/CHANGES index 37cf54f9a..ebdd331ed 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,127 @@ -Release 3.3.0 (in development) +Release 3.5.0 (in development) +============================== + +Dependencies +------------ + +* LaTeX: ``multicol`` (it is anyhow a required part of the official latex2e + base distribution) + +Incompatible changes +-------------------- + +* Update Underscore.js to 1.12.0 +* #6550: html: The config variable ``html_add_permalinks`` is replaced by + :confval:`html_permalinks` and :confval:`html_permalinks_icon` + +Deprecated +---------- + +* pending_xref node for viewcode extension +* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken`` +* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good`` +* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected`` +* ``sphinx.builders.linkcheck.node_line_or_0()`` +* ``sphinx.ext.autodoc.AttributeDocumenter.isinstanceattribute()`` +* ``sphinx.ext.autodoc.directive.DocumenterBridge.reporter`` +* ``sphinx.ext.autodoc.importer.get_module_members()`` +* ``sphinx.ext.autosummary.generate._simple_info()`` +* ``sphinx.ext.autosummary.generate._simple_warn()`` +* ``sphinx.writers.html.HTMLTranslator.permalink_text`` +* ``sphinx.writers.html5.HTML5Translator.permalink_text`` + +Features added +-------------- + +* #8022: autodoc: autodata and autoattribute directives does not show right-hand + value of the variable if docstring contains ``:meta hide-value:`` in + info-field-list +* #8514: autodoc: Default values of overloaded functions are taken from actual + implementation if they're ellipsis +* #8619: html: kbd role generates customizable HTML tags for compound keys +* #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter + for :meth:`Sphinx.add_js_file()` and :meth:`Sphinx.add_css_file()` +* #6241: html: Allow to add JS/CSS files to the specific page when an extension + calls ``app.add_js_file()`` or ``app.add_css_file()`` on + :event:`html-page-context` event +* #6550: html: Allow to use HTML permalink texts via + :confval:`html_permalinks_icon` +* #1638: html: Add permalink icons to glossary terms +* #8649: imgconverter: Skip availability check if builder supports the image + type +* #8573: napoleon: Allow to change the style of custom sections using + :confval:`napoleon_custom_styles` +* #8004: napoleon: Type definitions in Google style docstrings are rendered as + references when :confval:`napoleon_preprocess_types` enabled +* #6241: mathjax: Include mathjax.js only on the document using equations +* #8651: std domain: cross-reference for a rubric having inline item is broken +* #7642: std domain: Optimize case-insensitive match of term +* #8681: viewcode: Support incremental build +* #8132: Add :confval:`project_copyright` as an alias of :confval:`copyright` +* #207: Now :confval:`highlight_language` supports multiple languages +* #2030: :rst:dir:`code-block` and :rst:dir:`literalinclude` supports automatic + dedent via no-argument ``:dedent:`` option +* C++, also hyperlink operator overloads in expressions and alias declarations. +* #8247: Allow production lists to refer to tokens from other production groups + +Bugs fixed +---------- + +* #8727: apidoc: namespace module file is not generated if no submodules there +* #741: autodoc: inherited-members doesn't work for instance attributes on super + class +* #8592: autodoc: ``:meta public:`` does not effect to variables +* #8594: autodoc: empty __all__ attribute is ignored +* #8315: autodoc: Failed to resolve struct.Struct type annotation +* #8652: autodoc: All variable comments in the module are ignored if the module + contains invalid type comments +* #8693: autodoc: Default values for overloaded functions are rendered as string +* #8134: autodoc: crashes when mocked decorator takes arguments +* #8306: autosummary: mocked modules are documented as empty page when using + :recursive: option +* #8232: graphviz: Image node is not rendered if graph file is in subdirectory +* #8618: html: kbd role produces incorrect HTML when compound-key separators (-, + + or ^) are used as keystrokes +* #8629: html: A type warning for html_use_opensearch is shown twice +* #8714: html: kbd role with "Caps Lock" rendered incorrectly +* #8123: html search: fix searching for terms containing + (Requires a custom + search language that does not split on +) +* #8665: html theme: Could not override globaltoc_maxdepth in theme.conf +* #8446: html: consecutive spaces are displayed as single space +* #8745: i18n: crashes with KeyError when translation message adds a new auto + footnote reference +* #4304: linkcheck: Fix race condition that could lead to checking the + availability of the same URL twice +* #7118: sphinx-quickstart: questionare got Mojibake if libreadline unavailable +* #8094: texinfo: image files on the different directory with document are not + copied +* #8720: viewcode: module pages are generated for epub on incremental build +* #8704: viewcode: anchors are generated in incremental build after singlehtml +* #8756: viewcode: highlighted code is generated even if not referenced +* #8671: :confval:`highlight_options` is not working +* #8341: C, fix intersphinx lookup types for names in declarations. +* C, C++: in general fix intersphinx and role lookup types. +* #8683: :confval:`html_last_updated_fmt` does not support UTC offset (%z) +* #8683: :confval:`html_last_updated_fmt` generates wrong time zone for %Z +* #1112: ``download`` role creates duplicated copies when relative path is + specified +* #7576: LaTeX with French babel and memoir crash: "Illegal parameter number + in definition of ``\FNH@prefntext``" +* #8072: LaTeX: Directive :rst:dir:`hlist` not implemented in LaTeX +* #8214: LaTeX: The :rst:role:`index` role and the glossary generate duplicate + entries in the LaTeX index (if both used for same term) +* #8735: LaTeX: wrong internal links in pdf to captioned code-blocks when + :confval:`numfig` is not True +* #8442: LaTeX: some indexed terms are ignored when using xelatex engine + (or pdflatex and :confval:`latex_use_xindy` set to True) with memoir class +* #8780: LaTeX: long words in narrow columns may not be hyphenated +* #8788: LaTeX: ``\titleformat`` last argument in sphinx.sty should be + bracketed, not braced (and is anyhow not needed) + +Testing +-------- + +Release 3.4.4 (in development) ============================== Dependencies @@ -10,6 +133,175 @@ Incompatible changes Deprecated ---------- +Features added +-------------- + +Bugs fixed +---------- + +* #8655: autodoc: Failed to generate document if target module contains an + object that raises an exception on ``hasattr()`` +* C, ``expr`` role should start symbol lookup in the current scope. +* #8796: LaTeX: potentially critical low level TeX coding mistake has gone + unnoticed so far + +Testing +-------- + +Release 3.4.3 (released Jan 08, 2021) +===================================== + +Bugs fixed +---------- + +* #8655: autodoc: Failed to generate document if target module contains an + object that raises an exception on ``hasattr()`` + +Release 3.4.2 (released Jan 04, 2021) +===================================== + +Bugs fixed +---------- + +* #8164: autodoc: Classes that inherit mocked class are not documented +* #8602: autodoc: The ``autodoc-process-docstring`` event is emitted to the + non-datadescriptors unexpectedly +* #8616: autodoc: AttributeError is raised on non-class object is passed to + autoclass directive + +Release 3.4.1 (released Dec 25, 2020) +===================================== + +Bugs fixed +---------- + +* #8559: autodoc: AttributeError is raised when using forward-reference type + annotations +* #8568: autodoc: TypeError is raised on checking slots attribute +* #8567: autodoc: Instance attributes are incorrectly added to Parent class +* #8566: autodoc: The ``autodoc-process-docstring`` event is emitted to the + alias classes unexpectedly +* #8583: autodoc: Unnecessary object comparision via ``__eq__`` method +* #8565: linkcheck: Fix PriorityQueue crash when link tuples are not + comparable + +Release 3.4.0 (released Dec 20, 2020) +===================================== + +Incompatible changes +-------------------- + +* #8105: autodoc: the signature of class constructor will be shown for decorated + classes, not a signature of decorator + +Deprecated +---------- + +* The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()`` +* The ``no_docstring`` argument of + ``sphinx.ext.autodoc.Documenter.add_content()`` +* ``sphinx.ext.autodoc.Documenter.get_object_members()`` +* ``sphinx.ext.autodoc.DataDeclarationDocumenter`` +* ``sphinx.ext.autodoc.GenericAliasDocumenter`` +* ``sphinx.ext.autodoc.InstanceAttributeDocumenter`` +* ``sphinx.ext.autodoc.SlotsAttributeDocumenter`` +* ``sphinx.ext.autodoc.TypeVarDocumenter`` +* ``sphinx.ext.autodoc.importer._getannotations()`` +* ``sphinx.ext.autodoc.importer._getmro()`` +* ``sphinx.pycode.ModuleAnalyzer.parse()`` +* ``sphinx.util.osutil.movefile()`` +* ``sphinx.util.requests.is_ssl_error()`` + +Features added +-------------- + +* #8119: autodoc: Allow to determine whether a member not included in + ``__all__`` attribute of the module should be documented or not via + :event:`autodoc-skip-member` event +* #8219: autodoc: Parameters for generic class are not shown when super class is + a generic class and show-inheritance option is given (in Python 3.7 or above) +* autodoc: Add ``Documenter.config`` as a shortcut to access the config object +* autodoc: Add Optional[t] to annotation of function and method if a default + value equal to None is set. +* #8209: autodoc: Add ``:no-value:`` option to :rst:dir:`autoattribute` and + :rst:dir:`autodata` directive to suppress the default value of the variable +* #8460: autodoc: Support custom types defined by typing.NewType +* #8285: napoleon: Add :confval:`napoleon_attr_annotations` to merge type hints + on source code automatically if any type is specified in docstring +* #8236: napoleon: Support numpydoc's "Receives" section +* #6914: Add a new event :event:`warn-missing-reference` to custom warning + messages when failed to resolve a cross-reference +* #6914: Emit a detailed warning when failed to resolve a ``:ref:`` reference +* #6629: linkcheck: The builder now handles rate limits. See + :confval:`linkcheck_retry_on_rate_limit` for details. + +Bugs fixed +---------- + +* #7613: autodoc: autodoc does not respect __signature__ of the class +* #4606: autodoc: the location of the warning is incorrect for inherited method +* #8105: autodoc: the signature of class constructor is incorrect if the class + is decorated +* #8434: autodoc: :confval:`autodoc_type_aliases` does not effect to variables + and attributes +* #8443: autodoc: autodata directive can't create document for PEP-526 based + type annotated variables +* #8443: autodoc: autoattribute directive can't create document for PEP-526 + based uninitalized variables +* #8480: autodoc: autoattribute could not create document for __slots__ + attributes +* #8503: autodoc: autoattribute could not create document for a GenericAlias as + class attributes correctly +* #8534: autodoc: autoattribute could not create document for a commented + attribute in alias class +* #8452: autodoc: autodoc_type_aliases doesn't work when autodoc_typehints is + set to "description" +* #8541: autodoc: autodoc_type_aliases doesn't work for the type annotation to + instance attributes +* #8460: autodoc: autodata and autoattribute directives do not display type + information of TypeVars +* #8493: autodoc: references to builtins not working in class aliases +* #8522: autodoc: ``__bool__`` method could be called +* #8067: autodoc: A typehint for the instance variable having type_comment on + super class is not displayed +* #8545: autodoc: a __slots__ attribute is not documented even having docstring +* #741: autodoc: inherited-members doesn't work for instance attributes on super + class +* #8477: autosummary: non utf-8 reST files are generated when template contains + multibyte characters +* #8501: autosummary: summary extraction splits text after "el at." unexpectedly +* #8524: html: Wrong url_root has been generated on a document named "index" +* #8419: html search: Do not load ``language_data.js`` in non-search pages +* #8549: i18n: ``-D gettext_compact=0`` is no longer working +* #8454: graphviz: The layout option for graph and digraph directives don't work +* #8131: linkcheck: Use GET when HEAD requests cause Too Many Redirects, to + accommodate infinite redirect loops on HEAD +* #8437: Makefile: ``make clean`` with empty BUILDDIR is dangerous +* #8365: py domain: ``:type:`` and ``:rtype:`` gives false ambiguous class + lookup warnings +* #8352: std domain: Failed to parse an option that starts with bracket +* #8519: LaTeX: Prevent page brake in the middle of a seealso +* #8520: C, fix copying of AliasNode. + +Release 3.3.1 (released Nov 12, 2020) +===================================== + +Bugs fixed +---------- + +* #8372: autodoc: autoclass directive became slower than Sphinx-3.2 +* #7727: autosummary: raise PycodeError when documenting python package + without __init__.py +* #8350: autosummary: autosummary_mock_imports causes slow down builds +* #8364: C, properly initialize attributes in empty symbols. +* #8399: i18n: Put system locale path after the paths specified by configuration + +Release 3.3.0 (released Nov 02, 2020) +===================================== + +Deprecated +---------- + * ``sphinx.builders.latex.LaTeXBuilder.usepackages`` * ``sphinx.builders.latex.LaTeXBuilder.usepackages_afger_hyperref`` * ``sphinx.ext.autodoc.SingledispatchFunctionDocumenter`` @@ -26,6 +318,15 @@ Features added just before writing .tex file * #7996: manpage: Add :confval:`man_make_section_directory` to make a section directory on build man page +* #8289: epub: Allow to suppress "duplicated ToC entry found" warnings from epub + builder using :confval:`suppress_warnings`. +* #8298: sphinx-quickstart: Add :option:`sphinx-quickstart --no-sep` option +* #8304: sphinx.testing: Register public markers in sphinx.testing.fixtures +* #8051: napoleon: use the obj role for all See Also items +* #8050: napoleon: Apply :confval:`napoleon_preprocess_types` to every field +* C and C++, show line numbers for previous declarations when duplicates are + detected. +* #8183: Remove substitution_reference nodes from doctree only on LaTeX builds Bugs fixed ---------- @@ -43,52 +344,45 @@ Bugs fixed * #7964: autodoc: Tuple in default value is wrongly rendered * #8200: autodoc: type aliases break type formatting of autoattribute * #7786: autodoc: can't detect overloaded methods defined in other file +* #8294: autodoc: single-string __slots__ is not handled correctly +* #7785: autodoc: autodoc_typehints='none' does not effect to overloaded functions * #8192: napoleon: description is disappeared when it contains inline literals * #8142: napoleon: Potential of regex denial of service in google style docs * #8169: LaTeX: pxjahyper loaded even when latex_engine is not platex +* #8215: LaTeX: 'oneside' classoption causes build warning * #8175: intersphinx: Potential of regex denial of service by broken inventory * #8277: sphinx-build: missing and redundant spacing (and etc) for console output on building * #7973: imgconverter: Check availability of imagemagick many times * #8255: py domain: number in default argument value is changed from hexadecimal to decimal +* #8316: html: Prevent arrow keys changing page when button elements are focused +* #8343: html search: Fix unnecessary load of images when parsing the document +* #8254: html theme: Line numbers misalign with code lines * #8093: The highlight warning has wrong location in some builders (LaTeX, singlehtml and so on) +* #8215: Eliminate Fancyhdr build warnings for oneside documents * #8239: Failed to refer a token in productionlist if it is indented * #8268: linkcheck: Report HTTP errors when ``linkcheck_anchors`` is ``True`` * #8245: linkcheck: take source directory into account for local files +* #8321: linkcheck: ``tel:`` schema hyperlinks are detected as errors +* #8323: linkcheck: An exit status is incorrect when links having unsupported + schema found +* #8188: C, add missing items to internal object types dictionary, + e.g., preventing intersphinx from resolving them. +* C, fix anon objects in intersphinx. +* #8270, C++, properly reject functions as duplicate declarations if a + non-function declaration of the same name already exists. +* C, fix references to function parameters. + Link to the function instead of a non-existing anchor. * #6914: figure numbers are unexpectedly assigned to uncaptioned items +* #8320: make "inline" line numbers un-selectable Testing -------- * #8257: Support parallel build in sphinx.testing -Release 3.2.2 (in development) -============================== - -Dependencies ------------- - -Incompatible changes --------------------- - -Deprecated ----------- - -Features added --------------- - -Bugs fixed ----------- - -* #8188: C, add missing items to internal object types dictionary, - e.g., preventing intersphinx from resolving them. - - -Testing --------- - Release 3.2.1 (released Aug 14, 2020) ===================================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a20192679..bd164694d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -14,4 +14,5 @@ You can also browse it from this repository from ``doc/internals/contributing.rst`` Sphinx uses GitHub to host source code, track patches and bugs, and more. -Please make an effort to provide as much possible when filing bugs. +Please make an effort to provide as much detail as possible when filing +bugs. diff --git a/EXAMPLES b/EXAMPLES index 19f23172f..348e76c8c 100644 --- a/EXAMPLES +++ b/EXAMPLES @@ -230,6 +230,7 @@ Documentation using sphinx_rtd_theme * `MyHDL `__ * `Nextflow `__ * `NICOS `__ (customized) +* `OpenFAST `__ * `Pelican `__ * `picamera `__ * `Pillow `__ @@ -317,6 +318,7 @@ Documentation using a custom theme or integrated in a website * `Django `__ * `Doctrine `__ * `Enterprise Toolkit for Acrobat products `__ +* `FreeFEM `__ * `Gameduino `__ * `gensim `__ * `GeoServer `__ diff --git a/LICENSE b/LICENSE index f709c9ad7..2249594da 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ License for Sphinx ================== -Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). +Copyright (c) 2007-2021 by the Sphinx team (see AUTHORS file). All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Makefile b/Makefile index 9cff012d4..5ba113dc3 100644 --- a/Makefile +++ b/Makefile @@ -66,11 +66,11 @@ doclinter: .PHONY: test test: - @$(PYTHON) -m pytest -v $(TEST) + @$(PYTHON) -X dev -m pytest -v $(TEST) .PHONY: covertest covertest: - @$(PYTHON) -m pytest -v --cov=sphinx --junitxml=.junit.xml $(TEST) + @$(PYTHON) -X dev -m pytest -v --cov=sphinx --junitxml=.junit.xml $(TEST) .PHONY: build build: diff --git a/doc/_static/favicon.svg b/doc/_static/favicon.svg new file mode 100644 index 000000000..c3e1acd35 --- /dev/null +++ b/doc/_static/favicon.svg @@ -0,0 +1,8 @@ + + + + diff --git a/doc/_templates/index.html b/doc/_templates/index.html index 5e588acc4..9e9f7af56 100644 --- a/doc/_templates/index.html +++ b/doc/_templates/index.html @@ -33,9 +33,9 @@
  • {%trans path=pathto('ext/builtins')%}Extensions: automatic testing of code snippets, inclusion of docstrings from Python modules (API docs), and more{%endtrans%}
  • -
  • {%trans path=pathto('develop')%}Contributed extensions: more than - 50 extensions contributed by users - in a second repository; most of them installable from PyPI{%endtrans%}
  • +
  • {%trans path=pathto("usage/extensions")%}Contributed extensions: dozens of + extensions contributed by users; + most of them installable from PyPI{%endtrans%}
  • {%trans%} Sphinx uses reStructuredText diff --git a/doc/_themes/sphinx13/layout.html b/doc/_themes/sphinx13/layout.html index e3bb37dce..238fb52b7 100644 --- a/doc/_themes/sphinx13/layout.html +++ b/doc/_themes/sphinx13/layout.html @@ -67,7 +67,7 @@

  • Home
  • Get it
  • Docs
  • -
  • Extend/Develop
  • +
  • Extend
  • diff --git a/doc/changes.rst b/doc/changes.rst index b4872784a..829c7f7ed 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -8,4 +8,9 @@ Changelog ========= +.. raw:: latex + + \hypersetup{bookmarksdepth=1}% pdf bookmarks + \addtocontents{toc}{\protect\setcounter{tocdepth}{1}}% + .. include:: ../CHANGES diff --git a/doc/conf.py b/doc/conf.py index 74e5a8b80..53f036d3e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -4,7 +4,6 @@ import re import sphinx - extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', @@ -15,7 +14,7 @@ templates_path = ['_templates'] exclude_patterns = ['_build'] project = 'Sphinx' -copyright = '2007-2020, Georg Brandl and the Sphinx team' +copyright = '2007-2021, Georg Brandl and the Sphinx team' version = sphinx.__display_version__ release = version show_authors = True @@ -28,6 +27,7 @@ html_sidebars = {'index': ['indexsidebar.html', 'searchbox.html']} html_additional_pages = {'index': 'index.html'} html_use_opensearch = 'https://www.sphinx-doc.org/en/master' html_baseurl = 'https://www.sphinx-doc.org/en/master/' +html_favicon = '_static/favicon.svg' htmlhelp_basename = 'Sphinxdoc' @@ -69,8 +69,15 @@ latex_elements = { \substitutefont{X2}{\sfdefault}{cmss} \substitutefont{X2}{\ttdefault}{cmtt} ''', - 'passoptionstopackages': '\\PassOptionsToPackage{svgnames}{xcolor}', - 'preamble': '\\DeclareUnicodeCharacter{229E}{\\ensuremath{\\boxplus}}', + 'passoptionstopackages': r''' +\PassOptionsToPackage{svgnames}{xcolor} +\PassOptionsToPackage{bookmarksdepth=3}{hyperref}% depth of pdf bookmarks +''', + 'preamble': r''' +\DeclareUnicodeCharacter{229E}{\ensuremath{\boxplus}} +\setcounter{tocdepth}{3}% depth of what is kept from toc file +\setcounter{secnumdepth}{1}% depth of section numbering +''', 'fvset': '\\fvset{fontsize=auto}', # fix missing index entry due to RTD doing only once pdflatex after makeindex 'printindex': r''' @@ -110,7 +117,10 @@ texinfo_documents = [ 1), ] -intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} +intersphinx_mapping = { + 'python': ('https://docs.python.org/3/', None), + 'requests': ('https://requests.readthedocs.io/en/master', None), +} # Sphinx document translation with sphinx gettext feature uses these settings: locale_dirs = ['locale/'] diff --git a/doc/develop.rst b/doc/develop.rst deleted file mode 100644 index 1287a6539..000000000 --- a/doc/develop.rst +++ /dev/null @@ -1,153 +0,0 @@ -:orphan: - -Sphinx development -================== - -Sphinx is a maintained by a group of volunteers. We value every contribution! - -* The code can be found in a Git repository, at - https://github.com/sphinx-doc/sphinx/. -* Issues and feature requests should be raised in the `tracker - `_. -* The mailing list for development is at `Google Groups - `_. -* There is also the #sphinx-doc IRC channel on `freenode - `_. - -For more about our development process and methods, refer to -:doc:`/internals/index`. - -Extensions -========== - -To learn how to write your own extension, see :ref:`dev-extensions`. - -The `sphinx-contrib `_ -repository contains many contributed extensions. Some of them have their own -releases on PyPI, others you can install from a checkout. - -This is the current list of contributed extensions in that repository: - -- aafig: render embedded ASCII art as nice images using aafigure_ -- actdiag: embed activity diagrams by using actdiag_ -- adadomain: an extension for Ada support (Sphinx 1.0 needed) -- ansi: parse ANSI color sequences inside documents -- argdoc: automatically generate documentation for command-line arguments, - descriptions and help text -- astah: embed diagram by using astah -- autoanysrc: Gather reST documentation from any source files -- autorun: Execute code in a ``runblock`` directive -- beamer_: A builder for Beamer (LaTeX) output. -- blockdiag: embed block diagrams by using blockdiag_ -- cacoo: embed diagram from Cacoo -- cf3domain: a domain for CFEngine 3 policies -- cheader: The missing c:header directive for Sphinx's built-in C domain -- cheeseshop: easily link to PyPI packages -- clearquest: create tables from ClearQuest_ queries -- cmakedomain_: a domain for CMake_ -- coffeedomain: a domain for (auto)documenting CoffeeScript source code -- context: a builder for ConTeXt -- disqus: embed Disqus comments in documents -- documentedlist: converts a Python list to a table in the generated - documentation -- doxylink: Link to external Doxygen-generated HTML documentation -- domaintools_: A tool for easy domain creation -- email: obfuscate email addresses -- erlangdomain: an extension for Erlang support (Sphinx 1.0 needed) -- exceltable: embed Excel spreadsheets into documents using exceltable_ -- feed: an extension for creating syndication feeds and time-based overviews - from your site content -- findanything_: an extension to add Sublime Text 2-like findanything panels - to your documentation to find pages, sections and index entries while typing -- gnuplot: produces images using gnuplot_ language -- googleanalytics: track web visitor statistics by using `Google Analytics`_ -- googlechart: embed charts by using `Google Chart`_ -- googlemaps: embed maps by using `Google Maps`_ -- httpdomain: a domain for documenting RESTful HTTP APIs -- hyphenator: client-side hyphenation of HTML using hyphenator_ -- imgur: embed Imgur images, albums, and metadata in documents -- inlinesyntaxhighlight_: inline syntax highlighting -- lassodomain: a domain for documenting Lasso_ source code -- libreoffice: an extension to include any drawing supported by LibreOffice - (e.g. odg, vsd, ...) -- lilypond: an extension inserting music scripts from Lilypond_ in PNG format -- makedomain_: a domain for `GNU Make`_ -- matlabdomain: document MATLAB_ code -- mockautodoc: mock imports -- mscgen: embed mscgen-formatted MSC (Message Sequence Chart)s -- napoleon: supports `Google style`_ and `NumPy style`_ docstrings -- nicovideo: embed videos from nicovideo -- nwdiag: embed network diagrams by using nwdiag_ -- omegat: support tools to collaborate with OmegaT_ (Sphinx 1.1 needed) -- osaka: convert standard Japanese doc to Osaka dialect (this is a joke - extension) -- paverutils: an alternate integration of Sphinx with Paver_ -- phpdomain: an extension for PHP support -- plantuml: embed UML diagram by using PlantUML_ -- py_directive: Execute python code in a ``py`` directive and return a math - node -- rawfiles: copy raw files, like a CNAME -- requirements: declare requirements wherever you need (e.g. in test - docstrings), mark statuses and collect them in a single list -- restbuilder: a builder for reST (reStructuredText) files -- rubydomain: an extension for Ruby support (Sphinx 1.0 needed) -- sadisplay: display SqlAlchemy model sadisplay_ -- sdedit: an extension inserting sequence diagram by using Quick Sequence - Diagram Editor (sdedit_) -- seqdiag: embed sequence diagrams by using seqdiag_ -- slide: embed presentation slides on slideshare_ and other sites -- swf_: embed flash files -- sword: an extension inserting Bible verses from Sword_ -- tikz: draw pictures with the `TikZ/PGF LaTeX package`_ -- traclinks: create TracLinks_ to a Trac_ instance from within Sphinx -- versioning: Sphinx extension that allows building versioned docs for - self-hosting -- whooshindex: whoosh indexer extension -- youtube: embed videos from YouTube_ -- zopeext: provide an ``autointerface`` directive for using `Zope interfaces`_ - - -See the :doc:`extension tutorials <../development/tutorials/index>` on getting -started with writing your own extensions. - - -.. _aafigure: https://launchpad.net/aafigure -.. _gnuplot: http://www.gnuplot.info/ -.. _paver: https://paver.readthedocs.io/en/latest/ -.. _Sword: https://www.crosswire.org/sword/ -.. _Lilypond: http://lilypond.org/ -.. _sdedit: http://sdedit.sourceforge.net/ -.. _Trac: https://trac.edgewall.org/ -.. _TracLinks: https://trac.edgewall.org/wiki/TracLinks -.. _OmegaT: https://omegat.org/ -.. _PlantUML: http://plantuml.com/ -.. _PyEnchant: https://pythonhosted.org/pyenchant/ -.. _sadisplay: https://bitbucket.org/estin/sadisplay/wiki/Home -.. _blockdiag: http://blockdiag.com/en/ -.. _seqdiag: http://blockdiag.com/en/ -.. _actdiag: http://blockdiag.com/en/ -.. _nwdiag: http://blockdiag.com/en/ -.. _Google Analytics: https://www.google.com/analytics/ -.. _Google Chart: https://developers.google.com/chart/ -.. _Google Maps: https://www.google.com/maps -.. _Google style: https://google.github.io/styleguide/pyguide.html -.. _NumPy style: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt -.. _hyphenator: https://github.com/mnater/hyphenator -.. _exceltable: https://pythonhosted.org/sphinxcontrib-exceltable/ -.. _YouTube: https://www.youtube.com/ -.. _ClearQuest: https://www.ibm.com/us-en/marketplace/rational-clearquest -.. _Zope interfaces: https://zopeinterface.readthedocs.io/en/latest/README.html -.. _slideshare: https://www.slideshare.net/ -.. _TikZ/PGF LaTeX package: https://sourceforge.net/projects/pgf/ -.. _MATLAB: https://www.mathworks.com/products/matlab.html -.. _swf: https://github.com/sphinx-contrib/swf -.. _findanything: https://github.com/sphinx-contrib/findanything -.. _cmakedomain: https://github.com/sphinx-contrib/cmakedomain -.. _GNU Make: https://www.gnu.org/software/make/ -.. _makedomain: https://github.com/sphinx-contrib/makedomain -.. _inlinesyntaxhighlight: https://sphinxcontrib-inlinesyntaxhighlight.readthedocs.io/ -.. _CMake: https://cmake.org -.. _domaintools: https://github.com/sphinx-contrib/domaintools -.. _restbuilder: https://pypi.org/project/sphinxcontrib-restbuilder/ -.. _Lasso: http://www.lassosoft.com/ -.. _beamer: https://pypi.org/project/sphinxcontrib-beamer/ diff --git a/doc/development/index.rst b/doc/development/index.rst index 04918acd6..b4a7920ba 100644 --- a/doc/development/index.rst +++ b/doc/development/index.rst @@ -2,10 +2,12 @@ Extending Sphinx ================ -This guide is aimed at 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 extensions, refer to :doc:`/usage/index`. +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 +extensions, refer to :doc:`/usage/index`. For a more detailed discussion of +the extension interface see :doc:`/extdev/index`. .. toctree:: :maxdepth: 2 diff --git a/doc/development/tutorials/examples/recipe.py b/doc/development/tutorials/examples/recipe.py index 2464302da..c7317578b 100644 --- a/doc/development/tutorials/examples/recipe.py +++ b/doc/development/tutorials/examples/recipe.py @@ -4,8 +4,7 @@ from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.directives import ObjectDescription -from sphinx.domains import Domain -from sphinx.domains import Index +from sphinx.domains import Domain, Index from sphinx.roles import XRefRole from sphinx.util.nodes import make_refnode diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index df3eb3d67..4585df949 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -25,75 +25,75 @@ package. .. currentmodule:: sphinx.application -.. automethod:: Sphinx.setup_extension(name) +.. automethod:: Sphinx.setup_extension -.. automethod:: Sphinx.require_sphinx(version) +.. automethod:: Sphinx.require_sphinx -.. automethod:: Sphinx.connect(event, callback) +.. automethod:: Sphinx.connect -.. automethod:: Sphinx.disconnect(listener_id) +.. automethod:: Sphinx.disconnect -.. automethod:: Sphinx.add_builder(builder) +.. automethod:: Sphinx.add_builder -.. automethod:: Sphinx.add_config_value(name, default, rebuild) +.. automethod:: Sphinx.add_config_value -.. automethod:: Sphinx.add_event(name) +.. automethod:: Sphinx.add_event -.. automethod:: Sphinx.set_translator(name, translator_class) +.. automethod:: Sphinx.set_translator -.. automethod:: Sphinx.add_node(node, \*\*kwds) +.. automethod:: Sphinx.add_node -.. automethod:: Sphinx.add_enumerable_node(node, figtype, title_getter=None, \*\*kwds) +.. automethod:: Sphinx.add_enumerable_node -.. automethod:: Sphinx.add_directive(name, directiveclass) +.. automethod:: Sphinx.add_directive -.. automethod:: Sphinx.add_role(name, role) +.. automethod:: Sphinx.add_role -.. automethod:: Sphinx.add_generic_role(name, nodeclass) +.. automethod:: Sphinx.add_generic_role -.. automethod:: Sphinx.add_domain(domain) +.. automethod:: Sphinx.add_domain -.. automethod:: Sphinx.add_directive_to_domain(domain, name, directiveclass) +.. automethod:: Sphinx.add_directive_to_domain -.. automethod:: Sphinx.add_role_to_domain(domain, name, role) +.. automethod:: Sphinx.add_role_to_domain -.. automethod:: Sphinx.add_index_to_domain(domain, index) +.. automethod:: Sphinx.add_index_to_domain -.. automethod:: Sphinx.add_object_type(directivename, rolename, indextemplate='', parse_node=None, ref_nodeclass=None, objname='', doc_field_types=[]) +.. automethod:: Sphinx.add_object_type -.. automethod:: Sphinx.add_crossref_type(directivename, rolename, indextemplate='', ref_nodeclass=None, objname='') +.. automethod:: Sphinx.add_crossref_type -.. automethod:: Sphinx.add_transform(transform) +.. automethod:: Sphinx.add_transform -.. automethod:: Sphinx.add_post_transform(transform) +.. automethod:: Sphinx.add_post_transform -.. automethod:: Sphinx.add_js_file(filename, **kwargs) +.. automethod:: Sphinx.add_js_file -.. automethod:: Sphinx.add_css_file(filename, **kwargs) +.. automethod:: Sphinx.add_css_file -.. automethod:: Sphinx.add_latex_package(packagename, options=None) +.. automethod:: Sphinx.add_latex_package -.. automethod:: Sphinx.add_lexer(alias, lexer) +.. automethod:: Sphinx.add_lexer -.. automethod:: Sphinx.add_autodocumenter(cls) +.. automethod:: Sphinx.add_autodocumenter -.. automethod:: Sphinx.add_autodoc_attrgetter(type, getter) +.. automethod:: Sphinx.add_autodoc_attrgetter -.. automethod:: Sphinx.add_search_language(cls) +.. automethod:: Sphinx.add_search_language -.. automethod:: Sphinx.add_source_suffix(suffix, filetype) +.. automethod:: Sphinx.add_source_suffix -.. automethod:: Sphinx.add_source_parser(parser) +.. automethod:: Sphinx.add_source_parser -.. automethod:: Sphinx.add_env_collector(collector) +.. automethod:: Sphinx.add_env_collector -.. automethod:: Sphinx.add_html_theme(name, theme_path) +.. automethod:: Sphinx.add_html_theme -.. automethod:: Sphinx.add_html_math_renderer(name, inline_renderers, block_renderers) +.. automethod:: Sphinx.add_html_math_renderer -.. automethod:: Sphinx.add_message_catalog(catalog, locale_dir) +.. automethod:: Sphinx.add_message_catalog -.. automethod:: Sphinx.is_parallel_allowed(typ) +.. automethod:: Sphinx.is_parallel_allowed .. exception:: ExtensionError @@ -107,9 +107,9 @@ Emitting events .. class:: Sphinx :noindex: - .. automethod:: emit(event, \*arguments) + .. automethod:: emit - .. automethod:: emit_firstresult(event, \*arguments) + .. automethod:: emit_firstresult Sphinx runtime information @@ -186,6 +186,7 @@ type for that event:: 13. apply post-transforms (by priority): docutils.document -> docutils.document 14. event.doctree-resolved(app, doctree, docname) - (for any reference node that fails to resolve) event.missing-reference(env, node, contnode) + - (for any reference node that fails to resolve) event.warn-missing-reference(domain, node) 15. Generate output files 16. event.build-finished(app, exception) @@ -284,6 +285,14 @@ Here is a more detailed list of these events. .. versionadded:: 0.5 +.. event:: warn-missing-reference (app, domain, node) + + Emitted when a cross-reference to an object cannot be resolved even after + :event:`missing-reference`. If the event handler can emit warnings for + the missing reference, it should return ``True``. + + .. versionadded:: 3.4 + .. event:: doctree-resolved (app, doctree, docname) Emitted when a doctree has been "resolved" by the environment, that is, all @@ -360,6 +369,9 @@ Here is a more detailed list of these events. You can return a string from the handler, it will then replace ``'page.html'`` as the HTML template for this page. + .. note:: You can install JS/CSS files for the specific page via + :meth:`Sphinx.add_js_file` and :meth:`Sphinx.add_css_file` since v3.5.0. + .. versionadded:: 0.4 .. versionchanged:: 1.3 diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 2bb8aebfd..1350085ef 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -12,20 +12,142 @@ The following is a list of deprecated interfaces. .. tabularcolumns:: |>{\raggedright}\Y{.4}|>{\centering}\Y{.1}|>{\centering}\Y{.12}|>{\raggedright\arraybackslash}\Y{.38}| -.. |LaTeXHyphenate| raw:: latex - - \hspace{0pt} - .. list-table:: deprecated APIs :header-rows: 1 :class: deprecated :widths: 40, 10, 10, 40 * - Target - - |LaTeXHyphenate|\ Deprecated + - Deprecated - (will be) Removed - Alternatives + * - pending_xref node for viewcode extension + - 3.5 + - 5.0 + - ``sphinx.ext.viewcode.viewcode_anchor`` + + * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken`` + - 3.5 + - 5.0 + - N/A + + * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good`` + - 3.5 + - 5.0 + - N/A + + * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected`` + - 3.5 + - 5.0 + - N/A + + * - ``sphinx.builders.linkcheck.node_line_or_0()`` + - 3.5 + - 5.0 + - ``sphinx.util.nodes.get_node_line()`` + + * - ``sphinx.ext.autodoc.AttributeDocumenter.isinstanceattribute()`` + - 3.5 + - 5.0 + - N/A + + * - ``sphinx.ext.autodoc.importer.get_module_members()`` + - 3.5 + - 5.0 + - ``sphinx.ext.autodoc.ModuleDocumenter.get_module_members()`` + + * - ``sphinx.ext.autosummary.generate._simple_info()`` + - 3.5 + - 5.0 + - :ref:`logging-api` + + * - ``sphinx.ext.autosummary.generate._simple_warn()`` + - 3.5 + - 5.0 + - :ref:`logging-api` + + * - ``sphinx.writers.html.HTMLTranslator.permalink_text`` + - 3.5 + - 5.0 + - :confval:`html_permalinks_icon` + + * - ``sphinx.writers.html5.HTML5Translator.permalink_text`` + - 3.5 + - 5.0 + - :confval:`html_permalinks_icon` + + * - The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()`` + - 3.4 + - 5.0 + - N/A + + * - The ``no_docstring`` argument of + ``sphinx.ext.autodoc.Documenter.add_content()`` + - 3.4 + - 5.0 + - ``sphinx.ext.autodoc.Documenter.get_doc()`` + + * - ``sphinx.ext.autodoc.Documenter.get_object_members()`` + - 3.4 + - 6.0 + - ``sphinx.ext.autodoc.ClassDocumenter.get_object_members()`` + + * - ``sphinx.ext.autodoc.DataDeclarationDocumenter`` + - 3.4 + - 5.0 + - ``sphinx.ext.autodoc.DataDocumenter`` + + * - ``sphinx.ext.autodoc.GenericAliasDocumenter`` + - 3.4 + - 5.0 + - ``sphinx.ext.autodoc.DataDocumenter`` + + * - ``sphinx.ext.autodoc.InstanceAttributeDocumenter`` + - 3.4 + - 5.0 + - ``sphinx.ext.autodoc.AttributeDocumenter`` + + * - ``sphinx.ext.autodoc.SlotsAttributeDocumenter`` + - 3.4 + - 5.0 + - ``sphinx.ext.autodoc.AttributeDocumenter`` + + * - ``sphinx.ext.autodoc.TypeVarDocumenter`` + - 3.4 + - 5.0 + - ``sphinx.ext.autodoc.DataDocumenter`` + + * - ``sphinx.ext.autodoc.directive.DocumenterBridge.reporter`` + - 3.5 + - 5.0 + - ``sphinx.util.logging`` + + * - ``sphinx.ext.autodoc.importer._getannotations()`` + - 3.4 + - 4.0 + - ``sphinx.util.inspect.getannotations()`` + + * - ``sphinx.ext.autodoc.importer._getmro()`` + - 3.4 + - 4.0 + - ``sphinx.util.inspect.getmro()`` + + * - ``sphinx.pycode.ModuleAnalyzer.parse()`` + - 3.4 + - 5.0 + - ``sphinx.pycode.ModuleAnalyzer.analyze()`` + + * - ``sphinx.util.osutil.movefile()`` + - 3.4 + - 5.0 + - ``os.replace()`` + + * - ``sphinx.util.requests.is_ssl_error()`` + - 3.4 + - 5.0 + - N/A + * - ``sphinx.builders.latex.LaTeXBuilder.usepackages`` - 3.3 - 5.0 diff --git a/doc/extdev/domainapi.rst b/doc/extdev/domainapi.rst index d6ecf0633..674a3aa9a 100644 --- a/doc/extdev/domainapi.rst +++ b/doc/extdev/domainapi.rst @@ -1,7 +1,7 @@ .. _domain-api: Domain API ----------- +========== .. module:: sphinx.domains @@ -12,3 +12,16 @@ Domain API .. autoclass:: Index :members: + + +Python Domain +------------- + +.. module:: sphinx.domains.python + +.. autoclass:: PythonDomain + + .. autoattribute:: objects + .. autoattribute:: modules + .. automethod:: note_object + .. automethod:: note_module diff --git a/doc/glossary.rst b/doc/glossary.rst index d3367e5df..87b7014b6 100644 --- a/doc/glossary.rst +++ b/doc/glossary.rst @@ -9,8 +9,8 @@ Glossary A class (inheriting from :class:`~sphinx.builders.Builder`) that takes parsed documents and performs an action on them. Normally, builders translate the documents to an output format, but it is also possible to - use the builder builders that e.g. check for broken links in the - documentation, or build coverage information. + use builders that e.g. check for broken links in the documentation, or + build coverage information. See :doc:`/usage/builders/index` for an overview over Sphinx's built-in builders. diff --git a/doc/internals/contributing.rst b/doc/internals/contributing.rst index 1f4a31013..798736b03 100644 --- a/doc/internals/contributing.rst +++ b/doc/internals/contributing.rst @@ -138,10 +138,7 @@ Coding style Please follow these guidelines when writing code for Sphinx: -* Try to use the same code style as used in the rest of the project. See the - `Pocoo Styleguide`__ for more information. - - __ http://flask.pocoo.org/docs/styleguide/ +* Try to use the same code style as used in the rest of the project. * For non-trivial changes, please update the :file:`CHANGES` file. If your changes alter existing behavior, please document this. @@ -272,9 +269,9 @@ identifier and put ``sphinx.po`` in there. Don't forget to update the possible values for :confval:`language` in ``doc/usage/configuration.rst``. The Sphinx core messages can also be translated on `Transifex -`_. There ``tx`` client tool, which is -provided by the ``transifex_client`` Python package, can be used to pull -translations in ``.po`` format from Transifex. To do this, go to +`_. There ``tx`` client tool, +which is provided by the ``transifex_client`` Python package, can be used to +pull translations in ``.po`` format from Transifex. To do this, go to ``sphinx/locale`` and then run ``tx pull -f -l LANG`` where ``LANG`` is an existing language identifier. It is good practice to run ``python setup.py update_catalog`` afterwards to make sure the ``.po`` file has the canonical diff --git a/doc/latex.rst b/doc/latex.rst index 53fe9301a..39d4b8147 100644 --- a/doc/latex.rst +++ b/doc/latex.rst @@ -95,6 +95,12 @@ Keys that you may want to override include: A string which will be positioned early in the preamble, designed to contain ``\\PassOptionsToPackage{options}{foo}`` commands. + .. hint:: + + It may be also used for loading LaTeX packages very early in the + preamble. For example package ``fancybox`` is incompatible with + being loaded via the ``'preamble'`` key, it must be loaded earlier. + Default: ``''`` .. versionadded:: 1.4 @@ -195,8 +201,8 @@ Keys that you may want to override include: "Bjornstrup". You can also set this to ``''`` to disable fncychap. Default: ``'\\usepackage[Bjarne]{fncychap}'`` for English documents, - ``'\\usepackage[Sonny]{fncychap}'`` for internationalized documents, and - ``''`` for Japanese documents. + ``'\\usepackage[Sonny]{fncychap}'`` for internationalized documents, and + ``''`` for Japanese documents. ``'preamble'`` Additional preamble content. One may move all needed macros into some file @@ -300,7 +306,7 @@ Keys that don't need to be overridden unless in special cases are: "inputenc" package inclusion. Default: ``'\\usepackage[utf8]{inputenc}'`` when using pdflatex, else - ``''`` + ``''`` .. versionchanged:: 1.4.3 Previously ``'\\usepackage[utf8]{inputenc}'`` was used for all @@ -389,7 +395,7 @@ Keys that don't need to be overridden unless in special cases are: key is ignored. Default: ``'\\usepackage{textalpha}'`` or ``''`` if ``fontenc`` does not - include the ``LGR`` option. + include the ``LGR`` option. .. versionadded:: 2.0 @@ -407,7 +413,7 @@ Keys that don't need to be overridden unless in special cases are: `. Default: ``'\\usepackage{geometry}'`` (or - ``'\\usepackage[dvipdfm]{geometry}'`` for Japanese documents) + ``'\\usepackage[dvipdfm]{geometry}'`` for Japanese documents) .. versionadded:: 1.5 @@ -784,14 +790,14 @@ macros may be significant. |warningbdcolors| The colour for the admonition frame. - Default: ``{rgb}{0,0,0}`` (black) + Default: ``{rgb}{0,0,0}`` (black) .. only:: latex |wgbdcolorslatex| The colour for the admonition frame. - Default: ``{rgb}{0,0,0}`` (black) + Default: ``{rgb}{0,0,0}`` (black) |warningbgcolors| The background colours for the respective admonitions. @@ -1018,6 +1024,14 @@ Environments Miscellany ~~~~~~~~~~ +- Every text paragraph in document body starts with ``\sphinxAtStartPar``. + Currently, this is used to insert a zero width horizontal skip which + is a trick to allow TeX hyphenation of the first word of a paragraph + in a narrow context (like a table cell). For ``'lualatex'`` which + does not need the trick, the ``\sphinxAtStartPar`` does nothing. + + .. versionadded:: 3.5.0 + - The section, subsection, ... headings are set using *titlesec*'s ``\titleformat`` command. diff --git a/doc/man/sphinx-quickstart.rst b/doc/man/sphinx-quickstart.rst index 2407e3be7..520a420ce 100644 --- a/doc/man/sphinx-quickstart.rst +++ b/doc/man/sphinx-quickstart.rst @@ -33,6 +33,10 @@ Options If specified, separate source and build directories. +.. option:: --no-sep + + If specified, create build directroy under source directroy. + .. option:: --dot=DOT Inside the root directory, two more directories will be created; diff --git a/doc/usage/builders/index.rst b/doc/usage/builders/index.rst index db6706944..c45a8062f 100644 --- a/doc/usage/builders/index.rst +++ b/doc/usage/builders/index.rst @@ -442,6 +442,10 @@ name is ``rinoh``. Refer to the `rinohtype manual`_ for details. Since Sphinx-1.5, the linkcheck builder comes to use requests module. + .. versionchanged:: 3.4 + + The linkcheck builder retries links when servers apply rate limits. + .. module:: sphinx.builders.xml .. class:: XMLBuilder diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 270fcbf33..c2250eb7d 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -73,6 +73,12 @@ Project information A copyright statement in the style ``'2008, Author Name'``. +.. confval:: project_copyright + + An alias of :confval:`copyright`. + + .. versionadded:: 3.5 + .. confval:: version The major project version, used as the replacement for ``|version|``. For @@ -316,6 +322,7 @@ General configuration * ``toc.circular`` * ``toc.secnum`` * ``epub.unknown_project_files`` + * ``epub.duplicated_toc_entry`` * ``autosectionlabel.*`` You can choose from these types. @@ -340,6 +347,10 @@ General configuration Added ``autosectionlabel.*`` + .. versionchanged:: 3.3.0 + + Added ``epub.duplicated_toc_entry`` + .. confval:: needs_sphinx If set to a ``major.minor`` version string like ``'1.1'``, Sphinx will @@ -551,7 +562,7 @@ General configuration * Otherwise, the current time is formatted using :func:`time.strftime` and the format given in :confval:`today_fmt`. - The default is now :confval:`today` and a :confval:`today_fmt` of ``'%B %d, + The default is now :confval:`today` and a :confval:`today_fmt` of ``'%b %d, %Y'`` (or, if translation is enabled with :confval:`language`, an equivalent format for the selected locale). @@ -572,12 +583,27 @@ General configuration .. confval:: highlight_options - A dictionary of options that modify how the lexer specified by - :confval:`highlight_language` generates highlighted source code. These are - lexer-specific; for the options understood by each, see the - `Pygments documentation `_. + A dictionary that maps language names to options for the lexer modules of + Pygments. These are lexer-specific; for the options understood by each, + see the `Pygments documentation `_. + + Example:: + + highlight_options = { + 'default': {'stripall': True}, + 'php': {'startinline': True}, + } + + A single dictionary of options are also allowed. Then it is recognized + as options to the lexer specified by :confval:`highlight_language`:: + + # configuration for the ``highlight_language`` + highlight_options = {'stripall': True} .. versionadded:: 1.3 + .. versionchanged:: 3.5 + + Allow to configure highlight options for multiple languages .. confval:: pygments_style @@ -932,8 +958,11 @@ that use Sphinx's HTMLWriter class. .. confval:: html_baseurl - The URL which points to the root of the HTML documentation. It is used to - indicate the location of document like ``canonical_url``. + The base URL which points to the root of the HTML documentation. It is used + to indicate the location of document using `The Canonical Link Relation`_. + Default: ``''``. + + .. _The Canonical Link Relation: https://tools.ietf.org/html/rfc6596 .. versionadded:: 1.8 @@ -991,7 +1020,14 @@ that use Sphinx's HTMLWriter class. 'https://example.com/css/custom.css', ('print.css', {'media': 'print'})] + As a special attribute, *priority* can be set as an integer to load the CSS + file earlier or lazier step. For more information, refer + :meth:`Sphinx.add_css_files()`. + .. versionadded:: 1.8 + .. versionchanged:: 3.5 + + Support priority attribute .. confval:: html_js_files @@ -1007,7 +1043,14 @@ that use Sphinx's HTMLWriter class. 'https://example.com/scripts/custom.js', ('custom.js', {'async': 'async'})] + As a special attribute, *priority* can be set as an integer to load the CSS + file earlier or lazier step. For more information, refer + :meth:`Sphinx.add_css_files()`. + .. versionadded:: 1.8 + .. versionchanged:: 3.5 + + Support priority attribute .. confval:: html_static_path @@ -1091,6 +1134,23 @@ that use Sphinx's HTMLWriter class. This can now be a string to select the actual text of the link. Previously, only boolean values were accepted. + .. deprecated:: 3.5 + This has been replaced by :confval:`html_permalinks` + +.. confval:: html_permalinks + + If true, Sphinx will add "permalinks" for each heading and description + environment. Default: ``True``. + + .. versionadded:: 3.5 + +.. confval:: html_permalinks_icon + + A text for permalinks for each heading and description environment. HTML + tags are allowed. Default: a paragraph sign; ``¶`` + + .. versionadded:: 3.5 + .. confval:: html_sidebars Custom sidebar templates, must be a dictionary that maps document names to @@ -2520,6 +2580,23 @@ Options for the linkcheck builder .. versionadded:: 2.3 +.. confval:: linkcheck_rate_limit_timeout + + The ``linkcheck`` builder may issue a large number of requests to the same + site over a short period of time. This setting controls the builder behavior + when servers indicate that requests are rate-limited. + + If a server indicates when to retry (using the `Retry-After`_ header), + ``linkcheck`` always follows the server indication. + + Otherwise, ``linkcheck`` waits for a minute before to retry and keeps + doubling the wait time between attempts until it succeeds or exceeds the + ``linkcheck_rate_limit_timeout``. By default, the timeout is 5 minutes. + + .. _Retry-After: https://tools.ietf.org/html/rfc7231#section-7.1.3 + + .. versionadded:: 3.4 + Options for the XML builder --------------------------- diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 3e5f0b1be..a07042781 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -168,7 +168,7 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, ``:meta private:`` in its :ref:`info-field-lists`. For example: - .. code-block:: rst + .. code-block:: python def my_function(my_arg, my_other_arg): """blah blah blah @@ -183,7 +183,7 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, an underscore. For example: - .. code-block:: rst + .. code-block:: python def _my_function(my_arg, my_other_arg): """blah blah blah @@ -193,6 +193,16 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, .. versionadded:: 3.1 + * autodoc considers a variable member does not have any default value if its + docstring contains ``:meta hide-value:`` in its :ref:`info-field-lists`. + Example: + + .. code-block:: python + + var1 = None #: :meta hide-value: + + .. versionadded:: 3.5 + * Python "special" members (that is, those named like ``__special__``) will be included if the ``special-members`` flag option is given:: @@ -337,6 +347,15 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, By default, without ``annotation`` option, Sphinx tries to obtain the value of the variable and print it after the name. + The ``no-value`` option can be used instead of a blank ``annotation`` to show the + type hint but not the value:: + + .. autodata:: CD_DRIVE + :no-value: + + If both the ``annotation`` and ``no-value`` options are used, ``no-value`` has no + effect. + For module data members and class attributes, documentation can either be put into a comment with special formatting (using a ``#:`` to start the comment instead of just ``#``), or in a docstring *after* the definition. Comments @@ -376,6 +395,9 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, option. .. versionchanged:: 2.0 :rst:dir:`autodecorator` added. + .. versionchanged:: 3.4 + :rst:dir:`autodata` and :rst:dir:`autoattribute` now have a ``no-value`` + option. .. note:: @@ -553,7 +575,7 @@ There are also config values that you can set: ... If you set ``autodoc_type_aliases`` as - ``{'AliasType': 'your.module.TypeAlias'}``, it generates a following document + ``{'AliasType': 'your.module.AliasType'}``, it generates the following document internally:: .. py:function:: f() -> your.module.AliasType: diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst index d50abd89e..03ea7548e 100644 --- a/doc/usage/extensions/autosummary.rst +++ b/doc/usage/extensions/autosummary.rst @@ -304,7 +304,7 @@ The following variables available in the templates: .. data:: modules List containing names of "public" modules in the package. Only available for - modules that are packages. + modules that are packages and the ``recursive`` option is on. .. versionadded:: 3.1 diff --git a/doc/usage/extensions/example_google.py b/doc/usage/extensions/example_google.py index 97ffe8a05..5fde6e226 100644 --- a/doc/usage/extensions/example_google.py +++ b/doc/usage/extensions/example_google.py @@ -294,3 +294,21 @@ class ExampleClass: def _private_without_docstring(self): pass + +class ExamplePEP526Class: + """The summary line for a class docstring should fit on one line. + + If the class has public attributes, they may be documented here + in an ``Attributes`` section and follow the same formatting as a + function's ``Args`` section. If ``napoleon_attr_annotations`` + is True, types can be specified in the class body using ``PEP 526`` + annotations. + + Attributes: + attr1: Description of `attr1`. + attr2: Description of `attr2`. + + """ + + attr1: str + attr2: int \ No newline at end of file diff --git a/doc/usage/extensions/index.rst b/doc/usage/extensions/index.rst index 6aab8adb2..0d446cb61 100644 --- a/doc/usage/extensions/index.rst +++ b/doc/usage/extensions/index.rst @@ -41,22 +41,20 @@ These extensions are built in and can be activated by respective entries in the Third-party extensions ---------------------- -.. todo:: This should reference the GitHub organization now +You can find several extensions contributed by users in the `sphinx-contrib`__ +organization. If you wish to include your extension in this organization, +simply follow the instructions provided in the `github-administration`__ +project. This is optional and there are several extensions hosted elsewhere. +The `awesome-sphinxdoc`__ project contains a curated list of Sphinx packages, +and many packages use the `Framework :: Sphinx :: Extension`__ and +`Framework :: Sphinx :: Theme`__ trove classifiers for Sphinx extensions and +themes, respectively. -You can find several extensions contributed by users in the `Sphinx Contrib`_ -repository. It is open for anyone who wants to maintain an extension publicly; -just send a short message asking for write permissions. - -There are also several extensions hosted elsewhere. The `Sphinx extension -survey `__ and `awesome-sphinxdoc -`__ contains a comprehensive -list. - -If you write an extension that you think others will find useful or you think -should be included as a part of Sphinx, please write to the project mailing -list (`join here `_). - -.. _Sphinx Contrib: https://bitbucket.org/birkenfeld/sphinx-contrib +.. __: https://github.com/sphinx-contrib/ +.. __: https://github.com/sphinx-contrib/github-administration +.. __: https://github.com/yoloseem/awesome-sphinxdoc +.. __: https://pypi.org/search/?c=Framework+%3A%3A+Sphinx+%3A%3A+Extension +.. __: https://pypi.org/search/?c=Framework+%3A%3A+Sphinx+%3A%3A+Theme Where to put your own extensions? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/usage/extensions/intersphinx.rst b/doc/usage/extensions/intersphinx.rst index 619ec8c20..178655cae 100644 --- a/doc/usage/extensions/intersphinx.rst +++ b/doc/usage/extensions/intersphinx.rst @@ -75,8 +75,9 @@ linking: A dictionary mapping unique identifiers to a tuple ``(target, inventory)``. Each ``target`` is the base URI of a foreign Sphinx documentation set and can be a local path or an HTTP URI. The ``inventory`` indicates where the - inventory file can be found: it can be ``None`` (at the same location as - the base URI) or another local or HTTP URI. + inventory file can be found: it can be ``None`` (an :file:`objects.inv` file + at the same location as the base URI) or another local file path or a full + HTTP URI to an inventory file. The unique identifier can be used to prefix cross-reference targets, so that it is clear which intersphinx set the target belongs to. A link like @@ -106,7 +107,7 @@ linking: ``https://docs.python.org/3``. It is up to you to update the inventory file as new objects are added to the Python documentation. - **Multiple target for the inventory** + **Multiple targets for the inventory** .. versionadded:: 1.3 @@ -120,6 +121,16 @@ linking: intersphinx_mapping = {'python': ('https://docs.python.org/3', (None, 'python-inv.txt'))} + For a set of books edited and tested locally and then published + together, it could be helpful to try a local inventory file first, + to check references before publication:: + + intersphinx_mapping = { + 'otherbook': + ('https://myproj.readthedocs.io/projects/otherbook/en/latest', + ('../../otherbook/build/html/objects.inv', None)), + } + .. confval:: intersphinx_cache_limit The maximum number of days to cache remote inventories. The default is diff --git a/doc/usage/extensions/napoleon.rst b/doc/usage/extensions/napoleon.rst index b16577e2d..066c56e2d 100644 --- a/doc/usage/extensions/napoleon.rst +++ b/doc/usage/extensions/napoleon.rst @@ -203,7 +203,8 @@ Type Annotations This is an alternative to expressing types directly in docstrings. One benefit of expressing types according to `PEP 484`_ is that type checkers and IDEs can take advantage of them for static code -analysis. +analysis. `PEP 484`_ was then extended by `PEP 526`_ which introduced +a similar way to annotate variables (and attributes). Google style with Python 3 type annotations:: @@ -221,6 +222,19 @@ Google style with Python 3 type annotations:: """ return True + + class Class: + """Summary line. + + Extended description of class + + Attributes: + attr1: Description of attr1 + attr2: Description of attr2 + """ + + attr1: int + attr2: str Google style with types in docstrings:: @@ -238,6 +252,16 @@ Google style with types in docstrings:: """ return True + + class Class: + """Summary line. + + Extended description of class + + Attributes: + attr1 (int): Description of attr1 + attr2 (str): Description of attr2 + """ .. Note:: `Python 2/3 compatible annotations`_ aren't currently @@ -246,6 +270,9 @@ Google style with types in docstrings:: .. _PEP 484: https://www.python.org/dev/peps/pep-0484/ +.. _PEP 526: + https://www.python.org/dev/peps/pep-0526/ + .. _Python 2/3 compatible annotations: https://www.python.org/dev/peps/pep-0484/#suggested-syntax-for-python-2-7-and-straddling-code @@ -275,6 +302,7 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`:: napoleon_use_param = True napoleon_use_rtype = True napoleon_type_aliases = None + napoleon_attr_annotations = True .. _Google style: https://google.github.io/styleguide/pyguide.html @@ -511,3 +539,35 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`:: :type arg2: :term:`dict-like ` .. versionadded:: 3.2 + +.. confval:: napoleon_attr_annotations + + True to allow using `PEP 526`_ attributes annotations in classes. + If an attribute is documented in the docstring without a type and + has an annotation in the class body, that type is used. + + .. versionadded:: 3.4 + +.. confval:: napoleon_custom_sections + + Add a list of custom sections to include, expanding the list of parsed sections. + *Defaults to None.* + + The entries can either be strings or tuples, depending on the intention: + + * To create a custom "generic" section, just pass a string. + * To create an alias for an existing section, pass a tuple containing the + alias name and the original, in that order. + * To create a custom section that displays like the parameters or returns + section, pass a tuple containing the custom section name and a string + value, "params_style" or "returns_style". + + If an entry is just a string, it is interpreted as a header for a generic + section. If the entry is a tuple/list/indexed container, the first entry + is the name of the section, the second is the section key to emulate. If the + second entry value is "params_style" or "returns_style", the custom section + will be displayed like the parameters section or returns section. + + .. versionadded:: 1.8 + .. versionchanged:: 3.5 + Support ``params_style`` and ``returns_style`` \ No newline at end of file diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index 92bf78489..e8b88c21b 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -569,12 +569,28 @@ __ http://pygments.org/docs/lexers print 'Explicit is better than implicit.' + In order to cross-reference a code-block using either the + :rst:role:`ref` or the :rst:role:`numref` role, it is necessary + that both :strong:`name` and :strong:`caption` be defined. The + argument of :strong:`name` can then be given to :rst:role:`numref` + to generate the cross-reference. Example:: + + See :numref:`this-py` for an example. + + When using :rst:role:`ref`, it is possible to generate a cross-reference + with only :strong:`name` defined, provided an explicit title is + given. Example:: + + See :ref:`this code snippet ` for an example. + .. versionadded:: 1.3 .. rst:directive:option:: dedent: number - :type: number + :type: number or no value - Strip indentation characters from the code block. For example:: + Strip indentation characters from the code block. When number given, + leading N characters are removed. When no argument given, leading spaces + are removed via :func:`textwrap.dedent()`. For example:: .. code-block:: ruby :dedent: 4 @@ -582,6 +598,8 @@ __ http://pygments.org/docs/lexers some ruby code .. versionadded:: 1.3 + .. versionchanged:: 3.5 + Support automatic dedent. .. rst:directive:option:: force :type: no value @@ -742,6 +760,9 @@ __ http://pygments.org/docs/lexers .. versionchanged:: 2.1 Added the ``force`` option. + .. versionchanged:: 3.5 + Support automatic dedent. + .. _glossary-directive: Glossary @@ -1199,20 +1220,29 @@ the definition of the symbol. There is this directive: the following definition. If the definition spans multiple lines, each continuation line must begin with a colon placed at the same column as in the first line. + Blank lines are not allowed within ``productionlist`` directive arguments. + + The definition can contain token names which are marked as interpreted text + (e.g., "``sum ::= `integer` "+" `integer```") -- this generates + cross-references to the productions of these tokens. Outside of the + production list, you can reference to token productions using + :rst:role:`token`. The *productionGroup* argument to :rst:dir:`productionlist` serves to distinguish different sets of production lists that belong to different grammars. Multiple production lists with the same *productionGroup* thus define rules in the same scope. - Blank lines are not allowed within ``productionlist`` directive arguments. + Inside of the production list, tokens implicitly refer to productions + from the current group. You can refer to the production of another + grammar by prefixing the token with its group name and a colon, e.g, + "``otherGroup:sum``". If the group of the token should not be shown in + the production, it can be prefixed by a tilde, e.g., + "``~otherGroup:sum``". To refer to a production from an unnamed + grammar, the token should be prefixed by a colon, e.g., "``:sum``". - The definition can contain token names which are marked as interpreted text - (e.g. "``sum ::= `integer` "+" `integer```") -- this generates - cross-references to the productions of these tokens. Outside of the - production list, you can reference to token productions using - :rst:role:`token`. - However, if you have given a *productionGroup* argument you must prefix the + Outside of the production list, + if you have given a *productionGroup* argument you must prefix the token name in the cross-reference with the group name and a colon, e.g., "``myGroup:sum``" instead of just "``sum``". If the group should not be shown in the title of the link either diff --git a/karma.conf.js b/karma.conf.js index d0c11aa21..82be18a71 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -18,6 +18,7 @@ module.exports = function(config) { 'sphinx/themes/basic/static/underscore.js', 'sphinx/themes/basic/static/jquery.js', 'sphinx/themes/basic/static/doctools.js', + 'sphinx/themes/basic/static/searchtools.js', 'tests/js/*.js' ], diff --git a/setup.cfg b/setup.cfg index 37e42df84..7d629a9c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,9 +29,11 @@ directory = sphinx/locale/ [flake8] max-line-length = 95 ignore = E116,E241,E251,E741,W504,I101 -exclude = .git,.tox,.venv,tests/*,build/*,doc/_build/*,sphinx/search/*,doc/usage/extensions/example*.py +exclude = .git,.tox,.venv,tests/roots/*,build/*,doc/_build/*,sphinx/search/*,doc/usage/extensions/example*.py application-import-names = sphinx import-order-style = smarkets +per-file-ignores = + tests/*: E501 [flake8:local-plugins] extension = @@ -39,6 +41,9 @@ extension = paths = . +[isort] +line_length = 95 + [mypy] python_version = 3.5 disallow_incomplete_defs = True @@ -55,12 +60,11 @@ filterwarnings = all ignore::DeprecationWarning:docutils.io ignore::DeprecationWarning:pyximport.pyximport + ignore::ImportWarning:importlib._bootstrap ignore::PendingDeprecationWarning:sphinx.util.pycompat markers = - sphinx apidoc setup_command - test_params testpaths = tests [coverage:run] diff --git a/setup.py b/setup.py index a404f1fa5..dfc80578f 100644 --- a/setup.py +++ b/setup.py @@ -43,15 +43,15 @@ extras_require = { ], 'lint': [ 'flake8>=3.5.0', - 'flake8-import-order', - 'mypy>=0.780', + 'isort', + 'mypy>=0.800', 'docutils-stubs', ], 'test': [ 'pytest', 'pytest-cov', 'html5lib', - 'typed_ast', # for py35-37 + "typed_ast; python_version < '3.8'", 'cython', ], } @@ -76,9 +76,10 @@ class Tee: try: - from babel.messages.pofile import read_po - from babel.messages.frontend import compile_catalog from json import dump + + from babel.messages.frontend import compile_catalog + from babel.messages.pofile import read_po except ImportError: pass else: @@ -203,6 +204,7 @@ setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Framework :: Setuptools Plugin', diff --git a/sphinx/__init__.py b/sphinx/__init__.py index fde2345a8..23a867fa0 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -4,7 +4,7 @@ The Sphinx documentation toolchain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -32,8 +32,8 @@ if 'PYTHONWARNINGS' not in os.environ: warnings.filterwarnings('ignore', "'U' mode is deprecated", DeprecationWarning, module='docutils.io') -__version__ = '3.3.0+' -__released__ = '3.3.0' # used when Sphinx builds its own docs +__version__ = '3.5.0+' +__released__ = '3.5.0' # used when Sphinx builds its own docs #: Version info for better programmatic use. #: @@ -43,7 +43,7 @@ __released__ = '3.3.0' # used when Sphinx builds its own docs #: #: .. versionadded:: 1.2 #: Before version 1.2, check the string ``sphinx.__version__``. -version_info = (3, 3, 0, 'beta', 0) +version_info = (3, 5, 0, 'beta', 0) package_dir = path.abspath(path.dirname(__file__)) diff --git a/sphinx/__main__.py b/sphinx/__main__.py index 5da409015..6192f52ae 100644 --- a/sphinx/__main__.py +++ b/sphinx/__main__.py @@ -4,7 +4,7 @@ The Sphinx documentation toolchain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index 33503bb08..5f371e46b 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -4,7 +4,7 @@ Additional docutils nodes. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/application.py b/sphinx/application.py index 59ac04b20..f61a6a549 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -6,7 +6,7 @@ Gracefully adapted from the TextPress system by Armin. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,7 +18,7 @@ import warnings from collections import deque from io import StringIO from os import path -from typing import Any, Callable, Dict, IO, List, Optional, Tuple, Union +from typing import IO, Any, Callable, Dict, List, Optional, Tuple, Union from docutils import nodes from docutils.nodes import Element, TextElement @@ -28,7 +28,7 @@ from docutils.transforms import Transform from pygments.lexer import Lexer import sphinx -from sphinx import package_dir, locale +from sphinx import locale, package_dir from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.domains import Domain, Index @@ -43,9 +43,7 @@ from sphinx.project import Project from sphinx.registry import SphinxComponentRegistry from sphinx.roles import XRefRole from sphinx.theming import Theme -from sphinx.util import docutils -from sphinx.util import logging -from sphinx.util import progress_message +from sphinx.util import docutils, logging, progress_message from sphinx.util.build_phase import BuildPhase from sphinx.util.console import bold # type: ignore from sphinx.util.i18n import CatalogRepository @@ -56,8 +54,10 @@ from sphinx.util.typing import RoleFunction, TitleGetter if False: # For type annotation - from docutils.nodes import Node # NOQA from typing import Type # for python3.5.1 + + from docutils.nodes import Node # NOQA + from sphinx.builders import Builder @@ -135,7 +135,7 @@ class Sphinx: :ivar outdir: Directory for storing build documents. """ - def __init__(self, srcdir: str, confdir: str, outdir: str, doctreedir: str, + def __init__(self, srcdir: str, confdir: Optional[str], outdir: str, doctreedir: str, buildername: str, confoverrides: Dict = None, status: IO = sys.stdout, warning: IO = sys.stderr, freshenv: bool = False, warningiserror: bool = False, tags: List[str] = None, @@ -294,8 +294,8 @@ class Sphinx: if catalog.domain == 'sphinx' and catalog.is_outdated(): catalog.write_mo(self.config.language) - locale_dirs = [None] # type: List[Optional[str]] - locale_dirs += list(repo.locale_dirs) + locale_dirs = list(repo.locale_dirs) # type: List[Optional[str]] + locale_dirs += [None] locale_dirs += [path.join(package_dir, 'locale')] self.translator, has_translation = locale.init(locale_dirs, self.config.language) @@ -404,9 +404,10 @@ class Sphinx: def require_sphinx(self, version: str) -> None: """Check the Sphinx version if requested. - Compare *version* (which must be a ``major.minor`` version string, e.g. - ``'1.1'``) with the version of the running Sphinx, and abort the build - when it is too old. + Compare *version* with the version of the running Sphinx, and abort the + build when it is too old. + + :param version: The required version in the form of ``major.minor``. .. versionadded:: 1.0 """ @@ -420,11 +421,11 @@ class Sphinx: For details on available core events and the arguments of callback functions, please see :ref:`events`. - Registered callbacks will be invoked on event in the order of *priority* and - registration. The priority is ascending order. - - The method returns a "listener ID" that can be used as an argument to - :meth:`disconnect`. + :param event: The name of target event + :param callback: Callback function for the event + :param priority: The priority of the callback. The callbacks will be invoked + in the order of *priority* in asending. + :return: A listener ID. It can be used for :meth:`disconnect`. .. versionchanged:: 3.0 @@ -436,7 +437,10 @@ class Sphinx: return listener_id def disconnect(self, listener_id: int) -> None: - """Unregister callback by *listener_id*.""" + """Unregister callback by *listener_id*. + + :param listener_id: A listener_id that :meth:`connect` returns + """ logger.debug('[app] disconnecting event: [id=%s]', listener_id) self.events.disconnect(listener_id) @@ -447,6 +451,10 @@ class Sphinx: Return the return values of all callbacks as a list. Do not emit core Sphinx events in extensions! + :param event: The name of event that will be emitted + :param args: The arguments for the event + :param allowed_exceptions: The list of exceptions that are allowed in the callbacks + .. versionchanged:: 3.1 Added *allowed_exceptions* to specify path-through exceptions @@ -459,6 +467,10 @@ class Sphinx: Return the result of the first callback that doesn't return ``None``. + :param event: The name of event that will be emitted + :param args: The arguments for the event + :param allowed_exceptions: The list of exceptions that are allowed in the callbacks + .. versionadded:: 0.5 .. versionchanged:: 3.1 @@ -472,10 +484,9 @@ class Sphinx: def add_builder(self, builder: "Type[Builder]", override: bool = False) -> None: """Register a new builder. - *builder* must be a class that inherits from :class:`~sphinx.builders.Builder`. - - If *override* is True, the given *builder* is forcedly installed even if - a builder having the same name is already installed. + :param builder: A builder class + :param override: If true, install the builder forcedly even if another builder + is already installed as the same name .. versionchanged:: 1.8 Add *override* keyword. @@ -488,27 +499,34 @@ class Sphinx: """Register a configuration value. This is necessary for Sphinx to recognize new values and set default - values accordingly. The *name* should be prefixed with the extension - name, to avoid clashes. The *default* value can be any Python object. - The string value *rebuild* must be one of those values: + values accordingly. - * ``'env'`` if a change in the setting only takes effect when a - document is parsed -- this means that the whole environment must be - rebuilt. - * ``'html'`` if a change in the setting needs a full rebuild of HTML - documents. - * ``''`` if a change in the setting will not need any special rebuild. - .. versionchanged:: 0.6 - Changed *rebuild* from a simple boolean (equivalent to ``''`` or - ``'env'``) to a string. However, booleans are still accepted and - converted internally. + :param name: The name of configuration value. It is recommended to be prefixed + with the extension name (ex. ``html_logo``, ``epub_title``) + :param default: The default value of the configuration. + :param rebuild: The condition of rebuild. It must be one of those values: + + * ``'env'`` if a change in the setting only takes effect when a + document is parsed -- this means that the whole environment must be + rebuilt. + * ``'html'`` if a change in the setting needs a full rebuild of HTML + documents. + * ``''`` if a change in the setting will not need any special rebuild. + :param types: The type of configuration value. A list of types can be specified. For + example, ``[str]`` is used to describe a configuration that takes string + value. .. versionchanged:: 0.4 If the *default* value is a callable, it will be called with the config object as its argument in order to get the default value. This can be used to implement config values whose default depends on other values. + + .. versionchanged:: 0.6 + Changed *rebuild* from a simple boolean (equivalent to ``''`` or + ``'env'``) to a string. However, booleans are still accepted and + converted internally. """ logger.debug('[app] adding config value: %r', (name, default, rebuild) + ((types,) if types else ())) @@ -520,6 +538,8 @@ class Sphinx: """Register an event called *name*. This is needed to be able to emit it. + + :param name: The name of the event """ logger.debug('[app] adding event: %r', name) self.events.add(name) @@ -532,8 +552,10 @@ class Sphinx: builtin translator. This allows extensions to use custom translator and define custom nodes for the translator (see :meth:`add_node`). - If *override* is True, the given *translator_class* is forcedly installed even if - a translator for *name* is already installed. + :param name: The name of builder for the translator + :param translator_class: A translator class + :param override: If true, install the translator forcedly even if another translator + is already installed as the same name .. versionadded:: 1.3 .. versionchanged:: 1.8 @@ -548,6 +570,11 @@ class Sphinx: This is necessary for Docutils internals. It may also be used in the future to validate nodes in the parsed documents. + :param node: A node class + :param kwargs: Visitor functions for each builder (see below) + :param override: If true, install the node forcedly even if another node is already + installed as the same name + Node visitor functions for the Sphinx HTML, LaTeX, text and manpage writers can be given as keyword arguments: the keyword should be one or more of ``'html'``, ``'latex'``, ``'text'``, ``'man'``, ``'texinfo'`` @@ -569,9 +596,6 @@ class Sphinx: Obviously, translators for which you don't specify visitor methods will choke on the node when encountered in a document to translate. - If *override* is True, the given *node* is forcedly installed even if - a node having the same name is already installed. - .. versionchanged:: 0.5 Added the support for keyword arguments giving visit functions. """ @@ -591,24 +615,21 @@ class Sphinx: Sphinx numbers the node automatically. And then the users can refer it using :rst:role:`numref`. - *figtype* is a type of enumerable nodes. Each figtypes have individual - numbering sequences. As a system figtypes, ``figure``, ``table`` and - ``code-block`` are defined. It is able to add custom nodes to these - default figtypes. It is also able to define new custom figtype if new - figtype is given. - - *title_getter* is a getter function to obtain the title of node. It - takes an instance of the enumerable node, and it must return its title - as string. The title is used to the default title of references for - :rst:role:`ref`. By default, Sphinx searches - ``docutils.nodes.caption`` or ``docutils.nodes.title`` from the node as - a title. - - Other keyword arguments are used for node visitor functions. See the - :meth:`.Sphinx.add_node` for details. - - If *override* is True, the given *node* is forcedly installed even if - a node having the same name is already installed. + :param node: A node class + :param figtype: The type of enumerable nodes. Each figtypes have individual numbering + sequences. As a system figtypes, ``figure``, ``table`` and + ``code-block`` are defined. It is able to add custom nodes to these + default figtypes. It is also able to define new custom figtype if new + figtype is given. + :param title_getter: A getter function to obtain the title of node. It takes an + instance of the enumerable node, and it must return its title as + string. The title is used to the default title of references for + :rst:role:`ref`. By default, Sphinx searches + ``docutils.nodes.caption`` or ``docutils.nodes.title`` from the + node as a title. + :param kwargs: Visitor functions for each builder (same as :meth:`add_node`) + :param override: If true, install the node forcedly even if another node is already + installed as the same name .. versionadded:: 1.4 """ @@ -618,10 +639,10 @@ class Sphinx: def add_directive(self, name: str, cls: "Type[Directive]", override: bool = False) -> None: """Register a Docutils directive. - *name* must be the prospective directive name. *cls* is a directive - class which inherits ``docutils.parsers.rst.Directive``. For more - details, see `the Docutils docs - `_ . + :param name: The name of directive + :param cls: A directive class + :param override: If true, install the directive forcedly even if another directive + is already installed as the same name For example, a custom directive named ``my-directive`` would be added like this: @@ -646,8 +667,8 @@ class Sphinx: def setup(app): add_directive('my-directive', MyDirective) - If *override* is True, the given *cls* is forcedly installed even if - a directive named as *name* is already installed. + For more details, see `the Docutils docs + `__ . .. versionchanged:: 0.6 Docutils 0.5-style directive classes are now supported. @@ -666,13 +687,13 @@ class Sphinx: def add_role(self, name: str, role: Any, override: bool = False) -> None: """Register a Docutils role. - *name* must be the role name that occurs in the source, *role* the role - function. Refer to the `Docutils documentation - `_ for - more information. + :param name: The name of role + :param role: A role function + :param override: If true, install the role forcedly even if another role is already + installed as the same name - If *override* is True, the given *role* is forcedly installed even if - a role named as *name* is already installed. + For more details about role functions, see `the Docutils docs + `__ . .. versionchanged:: 1.8 Add *override* keyword. @@ -708,11 +729,9 @@ class Sphinx: def add_domain(self, domain: "Type[Domain]", override: bool = False) -> None: """Register a domain. - Make the given *domain* (which must be a class; more precisely, a - subclass of :class:`~sphinx.domains.Domain`) known to Sphinx. - - If *override* is True, the given *domain* is forcedly installed even if - a domain having the same name is already installed. + :param domain: A domain class + :param override: If true, install the domain forcedly even if another domain + is already installed as the same name .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -727,8 +746,11 @@ class Sphinx: Like :meth:`add_directive`, but the directive is added to the domain named *domain*. - If *override* is True, the given *directive* is forcedly installed even if - a directive named as *name* is already installed. + :param domain: The name of target domain + :param name: A name of directive + :param cls: A directive class + :param override: If true, install the directive forcedly even if another directive + is already installed as the same name .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -743,8 +765,11 @@ class Sphinx: Like :meth:`add_role`, but the role is added to the domain named *domain*. - If *override* is True, the given *role* is forcedly installed even if - a role named as *name* is already installed. + :param domain: The name of target domain + :param name: A name of role + :param role: A role function + :param override: If true, install the role forcedly even if another role is already + installed as the same name .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -756,11 +781,12 @@ class Sphinx: ) -> None: """Register a custom index for a domain. - Add a custom *index* class to the domain named *domain*. *index* must - be a subclass of :class:`~sphinx.domains.Index`. + Add a custom *index* class to the domain named *domain*. - If *override* is True, the given *index* is forcedly installed even if - an index having the same name is already installed. + :param domain: The name of target domain + :param index: A index class + :param override: If true, install the index forcedly even if another index is + already installed as the same name .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -881,6 +907,8 @@ class Sphinx: the list of transforms that are applied after Sphinx parses a reST document. + :param transform: A transform class + .. list-table:: priority range categories for Sphinx transforms :widths: 20,80 @@ -913,25 +941,29 @@ class Sphinx: Add the standard docutils :class:`Transform` subclass *transform* to the list of transforms that are applied before Sphinx writes a document. + + :param transform: A transform class """ self.registry.add_post_transform(transform) - def add_javascript(self, filename: str, **kwargs: str) -> None: + def add_javascript(self, filename: str, **kwargs: Any) -> None: """An alias of :meth:`add_js_file`.""" warnings.warn('The app.add_javascript() is deprecated. ' 'Please use app.add_js_file() instead.', RemovedInSphinx40Warning, stacklevel=2) self.add_js_file(filename, **kwargs) - def add_js_file(self, filename: str, **kwargs: str) -> None: + def add_js_file(self, filename: str, priority: int = 500, **kwargs: Any) -> None: """Register a JavaScript file to include in the HTML output. Add *filename* to the list of JavaScript files that the default HTML - template will include. The filename must be relative to the HTML - static path , or a full URI with scheme. If the keyword argument - ``body`` is given, its value will be added between the - `` + .. list-table:: priority range for JavaScript files + :widths: 20,80 + + * - Priority + - Main purpose in Sphinx + * - 200 + - default priority for built-in JavaScript files + * - 500 + - default priority for extensions + * - 800 + - default priority for :confval:`html_js_files` + + A JavaScript file can be added to the specific HTML page when on extension + calls this method on :event:`html-page-context` event. + .. versionadded:: 0.5 .. versionchanged:: 1.8 Renamed from ``app.add_javascript()``. And it allows keyword arguments as attributes of script tag. - """ - self.registry.add_js_file(filename, **kwargs) - if hasattr(self.builder, 'add_js_file'): - self.builder.add_js_file(filename, **kwargs) # type: ignore - def add_css_file(self, filename: str, **kwargs: str) -> None: + .. versionchanged:: 3.5 + Take priority argument. Allow to add a JavaScript file to the specific page. + """ + 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 + + def add_css_file(self, filename: str, priority: int = 500, **kwargs: Any) -> None: """Register a stylesheet to include in the HTML output. Add *filename* to the list of CSS files that the default HTML template - will include. The filename must be relative to the HTML static path, - or a full URI with scheme. The keyword arguments are also accepted for - attributes of ```` tag. + will include in order of *priority* (ascending). The filename must be + relative to the HTML static path, or a full URI with scheme. If the + priority of CSS file is the same as others, the CSS files will be + included in order of the registration. The keyword arguments are also + accepted for attributes of ```` tag. Example:: @@ -975,6 +1027,19 @@ class Sphinx: # => + .. list-table:: priority range for CSS files + :widths: 20,80 + + * - Priority + - Main purpose in Sphinx + * - 500 + - default priority for extensions + * - 800 + - default priority for :confval:`html_css_files` + + A CSS file can be added to the specific HTML page when on extension calls + this method on :event:`html-page-context` event. + .. versionadded:: 1.0 .. versionchanged:: 1.6 @@ -987,11 +1052,14 @@ class Sphinx: .. versionchanged:: 1.8 Renamed from ``app.add_stylesheet()``. And it allows keyword arguments as attributes of link tag. + + .. versionchanged:: 3.5 + Take priority argument. Allow to add a CSS file to the specific page. """ logger.debug('[app] adding stylesheet: %r', filename) - self.registry.add_css_files(filename, **kwargs) + self.registry.add_css_files(filename, priority=priority, **kwargs) if hasattr(self.builder, 'add_css_file'): - self.builder.add_css_file(filename, **kwargs) # type: ignore + self.builder.add_css_file(filename, priority=priority, **kwargs) # type: ignore def add_stylesheet(self, filename: str, alternate: bool = False, title: str = None ) -> None: @@ -1000,7 +1068,7 @@ class Sphinx: 'Please use app.add_css_file() instead.', RemovedInSphinx40Warning, stacklevel=2) - attributes = {} # type: Dict[str, str] + attributes = {} # type: Dict[str, Any] if alternate: attributes['rel'] = 'alternate stylesheet' else: @@ -1103,7 +1171,7 @@ class Sphinx: .. versionadded:: 1.1 """ logger.debug('[app] adding search language: %r', cls) - from sphinx.search import languages, SearchLanguage + from sphinx.search import SearchLanguage, languages assert issubclass(cls, SearchLanguage) languages[cls.lang] = cls @@ -1175,9 +1243,10 @@ class Sphinx: def add_message_catalog(self, catalog: str, locale_dir: str) -> None: """Register a message catalog. - The *catalog* is a name of catalog, and *locale_dir* is a base path - of message catalog. For more details, see - :func:`sphinx.locale.get_translation()`. + :param catalog: A name of catalog + :param locale_dir: The base path of message catalog + + For more details, see :func:`sphinx.locale.get_translation()`. .. versionadded:: 1.8 """ @@ -1188,7 +1257,7 @@ class Sphinx: def is_parallel_allowed(self, typ: str) -> bool: """Check parallel processing is allowed or not. - ``typ`` is a type of processing; ``'read'`` or ``'write'``. + :param typ: A type of processing; ``'read'`` or ``'write'``. """ if typ == 'read': attrname = 'parallel_read_safe' diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 93c246c69..58030bb6c 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -4,7 +4,7 @@ Builder superclass for all builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -17,26 +17,24 @@ from docutils import nodes from docutils.nodes import Node from sphinx.config import Config -from sphinx.environment import BuildEnvironment, CONFIG_OK, CONFIG_CHANGED_REASON +from sphinx.environment import CONFIG_CHANGED_REASON, CONFIG_OK, BuildEnvironment from sphinx.environment.adapters.asset import ImageAdapter from sphinx.errors import SphinxError from sphinx.events import EventManager from sphinx.io import read_doc from sphinx.locale import __ -from sphinx.util import import_object, logging, rst, progress_message, status_iterator +from sphinx.util import import_object, logging, progress_message, rst, status_iterator from sphinx.util.build_phase import BuildPhase from sphinx.util.console import bold # type: ignore from sphinx.util.docutils import sphinx_domains from sphinx.util.i18n import CatalogInfo, CatalogRepository, docname_to_domain from sphinx.util.osutil import SEP, ensuredir, relative_uri, relpath -from sphinx.util.parallel import ParallelTasks, SerialTasks, make_chunks, \ - parallel_available +from sphinx.util.parallel import ParallelTasks, SerialTasks, make_chunks, parallel_available from sphinx.util.tags import Tags # side effect: registers roles and directives -from sphinx import roles # noqa -from sphinx import directives # noqa - +from sphinx import directives # NOQA isort:skip +from sphinx import roles # NOQA isort:skip try: import multiprocessing except ImportError: @@ -45,6 +43,7 @@ except ImportError: if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx diff --git a/sphinx/builders/_epub_base.py b/sphinx/builders/_epub_base.py index 95f9ab8ed..7df3f8df5 100644 --- a/sphinx/builders/_epub_base.py +++ b/sphinx/builders/_epub_base.py @@ -4,7 +4,7 @@ Base class of epub2/epub3 builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -25,11 +25,10 @@ from sphinx import addnodes from sphinx.builders.html import BuildInfo, StandaloneHTMLBuilder from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import __ -from sphinx.util import logging -from sphinx.util import status_iterator +from sphinx.util import logging, status_iterator from sphinx.util.fileutil import copy_asset_file from sphinx.util.i18n import format_date -from sphinx.util.osutil import ensuredir, copyfile +from sphinx.util.osutil import copyfile, ensuredir try: from PIL import Image @@ -208,7 +207,12 @@ class EpubBuilder(StandaloneHTMLBuilder): appeared = set() # type: Set[str] for node in nodes: if node['refuri'] in appeared: - logger.warning(__('duplicated ToC entry found: %s'), node['refuri']) + logger.warning( + __('duplicated ToC entry found: %s'), + node['refuri'], + type="epub", + subtype="duplicated_toc_entry", + ) else: appeared.add(node['refuri']) diff --git a/sphinx/builders/applehelp.py b/sphinx/builders/applehelp.py index 82a74f4b6..bfc8c8dc8 100644 --- a/sphinx/builders/applehelp.py +++ b/sphinx/builders/applehelp.py @@ -4,23 +4,19 @@ Build Apple help books. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import warnings from typing import Any, Dict -from sphinxcontrib.applehelp import ( - AppleHelpCodeSigningFailed, - AppleHelpIndexerFailed, - AppleHelpBuilder, -) +from sphinxcontrib.applehelp import (AppleHelpBuilder, AppleHelpCodeSigningFailed, + AppleHelpIndexerFailed) from sphinx.application import Sphinx from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias - deprecated_alias('sphinx.builders.applehelp', { 'AppleHelpCodeSigningFailed': AppleHelpCodeSigningFailed, diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py index 7cf9d098b..87dd03fb8 100644 --- a/sphinx/builders/changes.py +++ b/sphinx/builders/changes.py @@ -4,14 +4,13 @@ Changelog builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import html from os import path -from typing import Any, Dict, List, Tuple -from typing import cast +from typing import Any, Dict, List, Tuple, cast from sphinx import package_dir from sphinx.application import Sphinx @@ -24,7 +23,6 @@ from sphinx.util.console import bold # type: ignore from sphinx.util.fileutil import copy_asset_file from sphinx.util.osutil import ensuredir, os_path - logger = logging.getLogger(__name__) diff --git a/sphinx/builders/devhelp.py b/sphinx/builders/devhelp.py index 3e402690a..9625b62f4 100644 --- a/sphinx/builders/devhelp.py +++ b/sphinx/builders/devhelp.py @@ -6,7 +6,7 @@ .. _Devhelp: https://wiki.gnome.org/Apps/Devhelp - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,7 +18,6 @@ from sphinxcontrib.devhelp import DevhelpBuilder from sphinx.application import Sphinx from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias - deprecated_alias('sphinx.builders.devhelp', { 'DevhelpBuilder': DevhelpBuilder, diff --git a/sphinx/builders/dirhtml.py b/sphinx/builders/dirhtml.py index 6fab8cf82..5e6b17259 100644 --- a/sphinx/builders/dirhtml.py +++ b/sphinx/builders/dirhtml.py @@ -4,7 +4,7 @@ Directory HTML builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/builders/dummy.py b/sphinx/builders/dummy.py index 33d2506ac..722e70d1c 100644 --- a/sphinx/builders/dummy.py +++ b/sphinx/builders/dummy.py @@ -4,7 +4,7 @@ Do syntax checks, but no writing. - :copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index cf795f3ba..d1cf64eb3 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -5,7 +5,7 @@ Build epub3 files. Originally derived from epub.py. - :copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,7 +18,7 @@ from typing import Any, Dict, List, Set, Tuple from sphinx import package_dir from sphinx.application import Sphinx from sphinx.builders import _epub_base -from sphinx.config import Config, ENUM +from sphinx.config import ENUM, Config from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import __ from sphinx.util import logging, xmlname_checker diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index f8a19c57a..75c95c0bc 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -4,33 +4,32 @@ The MessageCatalogBuilder class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from codecs import open -from collections import defaultdict, OrderedDict -from datetime import datetime, tzinfo, timedelta -from os import path, walk, getenv +from collections import OrderedDict, defaultdict +from datetime import datetime, timedelta, tzinfo +from os import getenv, path, walk from time import time -from typing import Any, Dict, Iterable, Generator, List, Set, Tuple, Union +from typing import Any, Dict, Generator, Iterable, List, Set, Tuple, Union from uuid import uuid4 from docutils import nodes from docutils.nodes import Element -from sphinx import addnodes -from sphinx import package_dir +from sphinx import addnodes, package_dir from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.domains.python import pairindextypes from sphinx.errors import ThemeError from sphinx.locale import __ -from sphinx.util import split_index_msg, logging, status_iterator +from sphinx.util import logging, split_index_msg, status_iterator from sphinx.util.console import bold # type: ignore from sphinx.util.i18n import CatalogInfo, docname_to_domain from sphinx.util.nodes import extract_messages, traverse_translatable_index -from sphinx.util.osutil import ensuredir, canon_path, relpath +from sphinx.util.osutil import canon_path, ensuredir, relpath from sphinx.util.tags import Tags from sphinx.util.template import SphinxRenderer @@ -316,7 +315,7 @@ class MessageCatalogBuilder(I18nBuilder): def setup(app: Sphinx) -> Dict[str, Any]: app.add_builder(MessageCatalogBuilder) - app.add_config_value('gettext_compact', True, 'gettext', Any) + app.add_config_value('gettext_compact', True, 'gettext', {bool, str}) app.add_config_value('gettext_location', True, 'gettext') app.add_config_value('gettext_uuid', False, 'gettext') app.add_config_value('gettext_auto_build', True, 'env') diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index beb650991..f5152f6da 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -4,17 +4,18 @@ Several HTML builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import html +import os import posixpath import re import sys import warnings from os import path -from typing import Any, Dict, IO, Iterable, Iterator, List, Set, Tuple +from typing import IO, Any, Dict, Iterable, Iterator, List, Set, Tuple from urllib.parse import quote from docutils import nodes @@ -24,10 +25,10 @@ from docutils.io import DocTreeInput, StringOutput from docutils.nodes import Node from docutils.utils import relative_path -from sphinx import package_dir, __display_version__ +from sphinx import __display_version__, package_dir from sphinx.application import Sphinx from sphinx.builders import Builder -from sphinx.config import Config, ENUM +from sphinx.config import ENUM, Config from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.domains import Domain, Index, IndexEntry from sphinx.environment.adapters.asset import ImageAdapter @@ -38,15 +39,15 @@ from sphinx.highlighting import PygmentsBridge from sphinx.locale import _, __ from sphinx.search import js_index from sphinx.theming import HTMLThemeFactory -from sphinx.util import logging, progress_message, status_iterator, md5 +from sphinx.util import logging, md5, progress_message, status_iterator from sphinx.util.docutils import is_html5_writer_available, new_document from sphinx.util.fileutil import copy_asset from sphinx.util.i18n import format_date from sphinx.util.inventory import InventoryFile -from sphinx.util.matching import patmatch, Matcher, DOTFILES -from sphinx.util.osutil import os_path, relative_uri, ensuredir, movefile, copyfile +from sphinx.util.matching import DOTFILES, Matcher, patmatch +from sphinx.util.osutil import copyfile, ensuredir, os_path, relative_uri from sphinx.util.tags import Tags -from sphinx.writers.html import HTMLWriter, HTMLTranslator +from sphinx.writers.html import HTMLTranslator, HTMLWriter if False: # For type annotation @@ -89,10 +90,13 @@ class Stylesheet(str): attributes = None # type: Dict[str, str] filename = None # type: str + priority = None # type: int - def __new__(cls, filename: str, *args: str, **attributes: str) -> "Stylesheet": - self = str.__new__(cls, filename) # type: ignore + def __new__(cls, filename: str, *args: str, priority: int = 500, **attributes: Any + ) -> "Stylesheet": + self = str.__new__(cls, filename) self.filename = filename + self.priority = priority self.attributes = attributes self.attributes.setdefault('rel', 'stylesheet') self.attributes.setdefault('type', 'text/css') @@ -112,10 +116,12 @@ class JavaScript(str): attributes = None # type: Dict[str, str] filename = None # type: str + priority = None # type: int - def __new__(cls, filename: str, **attributes: str) -> "JavaScript": - self = str.__new__(cls, filename) # type: ignore + def __new__(cls, filename: str, priority: int = 500, **attributes: str) -> "JavaScript": + self = str.__new__(cls, filename) self.filename = filename + self.priority = priority self.attributes = attributes return self @@ -289,30 +295,31 @@ class StandaloneHTMLBuilder(Builder): self.add_css_file(filename, **attrs) for filename, attrs in self.get_builder_config('css_files', 'html'): + attrs.setdefault('priority', 800) # User's CSSs are loaded after extensions' self.add_css_file(filename, **attrs) - def add_css_file(self, filename: str, **kwargs: str) -> None: + def add_css_file(self, filename: str, **kwargs: Any) -> None: if '://' not in filename: filename = posixpath.join('_static', filename) self.css_files.append(Stylesheet(filename, **kwargs)) # type: ignore def init_js_files(self) -> None: - self.add_js_file('jquery.js') - self.add_js_file('underscore.js') - self.add_js_file('doctools.js') - self.add_js_file('language_data.js') + self.add_js_file('jquery.js', priority=200) + self.add_js_file('underscore.js', priority=200) + self.add_js_file('doctools.js', priority=200) for filename, attrs in self.app.registry.js_files: self.add_js_file(filename, **attrs) for filename, attrs in self.get_builder_config('js_files', 'html'): + attrs.setdefault('priority', 800) # User's JSs are loaded after extensions' self.add_js_file(filename, **attrs) if self.config.language and self._get_translations_js(): self.add_js_file('translations.js') - def add_js_file(self, filename: str, **kwargs: str) -> None: + def add_js_file(self, filename: str, **kwargs: Any) -> None: if filename and '://' not in filename: filename = posixpath.join('_static', filename) @@ -448,9 +455,6 @@ class StandaloneHTMLBuilder(Builder): logo = path.basename(self.config.html_logo) if self.config.html_logo else '' favicon = path.basename(self.config.html_favicon) if self.config.html_favicon else '' - if not isinstance(self.config.html_use_opensearch, str): - logger.warning(__('html_use_opensearch config value must now be a string')) - self.relations = self.env.collect_relations() rellinks = [] # type: List[Tuple[str, str, str, str]] @@ -462,6 +466,10 @@ class StandaloneHTMLBuilder(Builder): rellinks.append((indexname, indexcls.localname, '', indexcls.shortname)) + # back up script_files and css_files to allow adding JS/CSS files to a specific page. + self._script_files = list(self.script_files) + self._css_files = list(self.css_files) + if self.config.html_style is not None: stylename = self.config.html_style elif self.theme: @@ -1012,12 +1020,20 @@ class StandaloneHTMLBuilder(Builder): self.add_sidebars(pagename, ctx) ctx.update(addctx) + # revert script_files and css_files + self.script_files[:] = self._script_files + self.css_files[:] = self.css_files + self.update_page_context(pagename, templatename, ctx, event_arg) newtmpl = self.app.emit_firstresult('html-page-context', pagename, templatename, ctx, event_arg) if newtmpl: templatename = newtmpl + # sort JS/CSS before rendering HTML + ctx['script_files'].sort(key=lambda js: js.priority) + ctx['css_files'].sort(key=lambda js: js.priority) + try: output = self.templates.render(templatename, ctx) except UnicodeError: @@ -1071,7 +1087,7 @@ class StandaloneHTMLBuilder(Builder): else: with open(searchindexfn + '.tmp', 'wb') as fb: self.indexer.dump(fb, self.indexer_format) - movefile(searchindexfn + '.tmp', searchindexfn) + os.replace(searchindexfn + '.tmp', searchindexfn) def convert_html_css_files(app: Sphinx, config: Config) -> None: @@ -1189,10 +1205,21 @@ def validate_html_favicon(app: Sphinx, config: Config) -> None: config.html_favicon = None # type: ignore +def migrate_html_add_permalinks(app: Sphinx, config: Config) -> None: + """Migrate html_add_permalinks to html_permalinks*.""" + if config.html_add_permalinks: + if (isinstance(config.html_add_permalinks, bool) and + config.html_add_permalinks is False): + config.html_permalinks = False # type: ignore + else: + config.html_permalinks_icon = html.escape(config.html_add_permalinks) # type: ignore # NOQA + + # for compatibility +import sphinxcontrib.serializinghtml # NOQA + import sphinx.builders.dirhtml # NOQA import sphinx.builders.singlehtml # NOQA -import sphinxcontrib.serializinghtml # NOQA def setup(app: Sphinx) -> Dict[str, Any]: @@ -1218,7 +1245,9 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('html_sidebars', {}, 'html') app.add_config_value('html_additional_pages', {}, 'html') app.add_config_value('html_domain_indices', True, 'html', [list]) - app.add_config_value('html_add_permalinks', '¶', 'html') + app.add_config_value('html_add_permalinks', None, 'html') + app.add_config_value('html_permalinks', True, 'html') + app.add_config_value('html_permalinks_icon', '¶', 'html') app.add_config_value('html_use_index', True, 'html') app.add_config_value('html_split_index', False, 'html') app.add_config_value('html_copy_source', True, 'html') @@ -1243,9 +1272,14 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('html_math_renderer', None, 'env') app.add_config_value('html4_writer', False, 'html') + # events + app.add_event('html-collect-pages') + app.add_event('html-page-context') + # event handlers app.connect('config-inited', convert_html_css_files, priority=800) app.connect('config-inited', convert_html_js_files, priority=800) + app.connect('config-inited', migrate_html_add_permalinks, priority=800) app.connect('config-inited', validate_html_extra_path, priority=800) app.connect('config-inited', validate_html_static_path, priority=800) app.connect('config-inited', validate_html_logo, priority=800) diff --git a/sphinx/builders/html/transforms.py b/sphinx/builders/html/transforms.py index c91da57e9..cb9af5f28 100644 --- a/sphinx/builders/html/transforms.py +++ b/sphinx/builders/html/transforms.py @@ -4,12 +4,12 @@ Transforms for HTML builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re -from typing import Any, Dict +from typing import Any, Dict, List from docutils import nodes @@ -28,7 +28,7 @@ class KeyboardTransform(SphinxPostTransform): After:: - + Control - @@ -37,18 +37,30 @@ class KeyboardTransform(SphinxPostTransform): """ default_priority = 400 builders = ('html',) - pattern = re.compile(r'(-|\+|\^|\s+)') + pattern = re.compile(r'(?<=.)(-|\+|\^|\s+)(?=.)') + multiwords_keys = (('caps', 'lock'), + ('page' 'down'), + ('page', 'up'), + ('scroll' 'lock'), + ('num', 'lock'), + ('sys' 'rq'), + ('back' 'space')) def run(self, **kwargs: Any) -> None: matcher = NodeMatcher(nodes.literal, classes=["kbd"]) for node in self.document.traverse(matcher): # type: nodes.literal parts = self.pattern.split(node[-1].astext()) - if len(parts) == 1: + if len(parts) == 1 or self.is_multiwords_key(parts): continue + node['classes'].append('compound') node.pop() while parts: - key = parts.pop(0) + if self.is_multiwords_key(parts): + key = ''.join(parts[:3]) + parts[:3] = [] + else: + key = parts.pop(0) node += nodes.literal('', key, classes=["kbd"]) try: @@ -58,6 +70,16 @@ class KeyboardTransform(SphinxPostTransform): except IndexError: pass + def is_multiwords_key(self, parts: List[str]) -> bool: + if len(parts) >= 3 and parts[1].strip() == '': + name = parts[0].lower(), parts[2].lower() + if name in self.multiwords_keys: + return True + else: + return False + else: + return False + def setup(app: Sphinx) -> Dict[str, Any]: app.add_post_transform(KeyboardTransform) diff --git a/sphinx/builders/htmlhelp.py b/sphinx/builders/htmlhelp.py index d30f9805a..08719712c 100644 --- a/sphinx/builders/htmlhelp.py +++ b/sphinx/builders/htmlhelp.py @@ -5,21 +5,19 @@ Build HTML help support files. Parts adapted from Python's Doc/tools/prechm.py. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import warnings from typing import Any, Dict -from sphinxcontrib.htmlhelp import ( - chm_locales, chm_htmlescape, HTMLHelpBuilder, default_htmlhelp_basename -) +from sphinxcontrib.htmlhelp import (HTMLHelpBuilder, chm_htmlescape, chm_locales, + default_htmlhelp_basename) from sphinx.application import Sphinx from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias - deprecated_alias('sphinx.builders.htmlhelp', { 'chm_locales': chm_locales, diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index 8fd1c077d..5c7a7412a 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -4,7 +4,7 @@ LaTeX builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -17,18 +17,18 @@ from docutils.frontend import OptionParser from docutils.nodes import Node import sphinx.builders.latex.nodes # NOQA # Workaround: import this before writer to avoid ImportError -from sphinx import package_dir, addnodes, highlighting +from sphinx import addnodes, highlighting, package_dir from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.builders.latex.constants import ADDITIONAL_SETTINGS, DEFAULT_SETTINGS, SHORTHANDOFF from sphinx.builders.latex.theming import Theme, ThemeFactory from sphinx.builders.latex.util import ExtBabel -from sphinx.config import Config, ENUM +from sphinx.config import ENUM, Config from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.environment.adapters.asset import ImageAdapter from sphinx.errors import NoUri, SphinxError from sphinx.locale import _, __ -from sphinx.util import texescape, logging, progress_message, status_iterator +from sphinx.util import logging, progress_message, status_iterator, texescape from sphinx.util.console import bold, darkgreen # type: ignore from sphinx.util.docutils import SphinxFileOutput, new_document from sphinx.util.fileutil import copy_asset_file @@ -36,11 +36,10 @@ from sphinx.util.i18n import format_date from sphinx.util.nodes import inline_all_toctrees from sphinx.util.osutil import SEP, make_filename_from_project from sphinx.util.template import LaTeXRenderer -from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator +from sphinx.writers.latex import LaTeXTranslator, LaTeXWriter # load docutils.nodes after loading sphinx.builders.latex.nodes -from docutils import nodes # NOQA - +from docutils import nodes # isort:skip XINDY_LANG_OPTIONS = { # language codes from docutils.writers.latex2e.Babel diff --git a/sphinx/builders/latex/constants.py b/sphinx/builders/latex/constants.py index 7146079ff..0b20c7cef 100644 --- a/sphinx/builders/latex/constants.py +++ b/sphinx/builders/latex/constants.py @@ -4,13 +4,12 @@ consntants for LaTeX builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from typing import Any, Dict - PDFLATEX_DEFAULT_FONTPKG = r''' \usepackage{times} \expandafter\ifx\csname T@LGR\endcsname\relax diff --git a/sphinx/builders/latex/nodes.py b/sphinx/builders/latex/nodes.py index e6b1e5aee..eaed6b862 100644 --- a/sphinx/builders/latex/nodes.py +++ b/sphinx/builders/latex/nodes.py @@ -4,7 +4,7 @@ Additional nodes for LaTeX writer. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/builders/latex/theming.py b/sphinx/builders/latex/theming.py index 130bded4a..5af79e8a2 100644 --- a/sphinx/builders/latex/theming.py +++ b/sphinx/builders/latex/theming.py @@ -4,7 +4,7 @@ Theming support for LaTeX builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py index 28841ad77..402cc3fe5 100644 --- a/sphinx/builders/latex/transforms.py +++ b/sphinx/builders/latex/transforms.py @@ -4,21 +4,20 @@ Transforms for LaTeX builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List, Set, Tuple -from typing import cast +from typing import Any, Dict, List, Set, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node +from docutils.transforms.references import Substitutions from sphinx import addnodes from sphinx.application import Sphinx -from sphinx.builders.latex.nodes import ( - captioned_literal_block, footnotemark, footnotetext, math_reference, thebibliography -) +from sphinx.builders.latex.nodes import (captioned_literal_block, footnotemark, footnotetext, + math_reference, thebibliography) from sphinx.domains.citation import CitationDomain from sphinx.transforms import SphinxTransform from sphinx.transforms.post_transforms import SphinxPostTransform @@ -38,6 +37,18 @@ class FootnoteDocnameUpdater(SphinxTransform): node['docname'] = self.env.docname +class SubstitutionDefinitionsRemover(SphinxPostTransform): + """Remove ``substitution_definition node from doctrees.""" + + # should be invoked after Substitutions process + default_priority = Substitutions.default_priority + 1 + builders = ('latex',) + + def apply(self, **kwargs: Any) -> None: + for node in self.document.traverse(nodes.substitution_definition): + node.parent.remove(node) + + class ShowUrlsTransform(SphinxPostTransform): """Expand references to inline text or footnotes. @@ -602,6 +613,7 @@ class IndexInSectionTitleTransform(SphinxTransform): def setup(app: Sphinx) -> Dict[str, Any]: app.add_transform(FootnoteDocnameUpdater) + app.add_post_transform(SubstitutionDefinitionsRemover) app.add_post_transform(BibliographyTransform) app.add_post_transform(CitationReferenceTransform) app.add_post_transform(DocumentTargetTransform) diff --git a/sphinx/builders/latex/util.py b/sphinx/builders/latex/util.py index 0e3eb739d..4f2391c4e 100644 --- a/sphinx/builders/latex/util.py +++ b/sphinx/builders/latex/util.py @@ -4,7 +4,7 @@ Utilities for LaTeX builder. - :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index a9e6b05b0..f3b6e8c4a 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -4,7 +4,7 @@ The CheckExternalLinksBuilder class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,34 +13,55 @@ import queue import re import socket import threading +import time +import warnings +from datetime import datetime, timezone +from email.utils import parsedate_to_datetime from html.parser import HTMLParser from os import path -from typing import Any, Dict, List, Set, Tuple +from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, cast from urllib.parse import unquote, urlparse from docutils import nodes -from docutils.nodes import Node -from requests.exceptions import HTTPError +from docutils.nodes import Element +from requests import Response +from requests.exceptions import HTTPError, TooManyRedirects from sphinx.application import Sphinx -from sphinx.builders import Builder +from sphinx.builders.dummy import DummyBuilder +from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.locale import __ -from sphinx.util import encode_uri, requests, logging -from sphinx.util.console import ( # type: ignore - purple, red, darkgreen, darkgray, turquoise -) +from sphinx.transforms.post_transforms import SphinxPostTransform +from sphinx.util import encode_uri, logging, requests +from sphinx.util.console import darkgray, darkgreen, purple, red, turquoise # type: ignore from sphinx.util.nodes import get_node_line -from sphinx.util.requests import is_ssl_error - logger = logging.getLogger(__name__) uri_re = re.compile('([a-z]+:)?//') # matches to foo:// and // (a protocol relative URL) +Hyperlink = NamedTuple('Hyperlink', (('next_check', float), + ('uri', Optional[str]), + ('docname', Optional[str]), + ('lineno', Optional[int]))) +RateLimit = NamedTuple('RateLimit', (('delay', float), ('next_check', float))) DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8', } +CHECK_IMMEDIATELY = 0 +QUEUE_POLL_SECS = 1 +DEFAULT_DELAY = 60.0 + + +def node_line_or_0(node: Element) -> int: + """ + PriorityQueue items must be comparable. The line number is part of the + tuple used by the PriorityQueue, keep an homogeneous type for comparison. + """ + warnings.warn('node_line_or_0() is deprecated.', + RemovedInSphinx50Warning, stacklevel=2) + return get_node_line(node) or 0 class AnchorCheckParser(HTMLParser): @@ -77,7 +98,7 @@ def check_anchor(response: requests.requests.Response, anchor: str) -> bool: return parser.found -class CheckExternalLinksBuilder(Builder): +class CheckExternalLinksBuilder(DummyBuilder): """ Checks for broken external links. """ @@ -86,37 +107,59 @@ class CheckExternalLinksBuilder(Builder): '%(outdir)s/output.txt') def init(self) -> None: - self.to_ignore = [re.compile(x) for x in self.app.config.linkcheck_ignore] + self.hyperlinks = {} # type: Dict[str, Hyperlink] + self.to_ignore = [re.compile(x) for x in self.config.linkcheck_ignore] self.anchors_ignore = [re.compile(x) - for x in self.app.config.linkcheck_anchors_ignore] + for x in self.config.linkcheck_anchors_ignore] self.auth = [(re.compile(pattern), auth_info) for pattern, auth_info - in self.app.config.linkcheck_auth] - self.good = set() # type: Set[str] - self.broken = {} # type: Dict[str, str] - self.redirected = {} # type: Dict[str, Tuple[str, int]] + in self.config.linkcheck_auth] + self._good = set() # type: Set[str] + self._broken = {} # type: Dict[str, str] + self._redirected = {} # type: Dict[str, Tuple[str, int]] # set a timeout for non-responding servers socket.setdefaulttimeout(5.0) - # create output file - open(path.join(self.outdir, 'output.txt'), 'w').close() - # create JSON output file - open(path.join(self.outdir, 'output.json'), 'w').close() # create queues and worker threads - self.wqueue = queue.Queue() # type: queue.Queue + self.rate_limits = {} # type: Dict[str, RateLimit] + self.wqueue = queue.PriorityQueue() # type: queue.PriorityQueue self.rqueue = queue.Queue() # type: queue.Queue self.workers = [] # type: List[threading.Thread] - for i in range(self.app.config.linkcheck_workers): - thread = threading.Thread(target=self.check_thread) - thread.setDaemon(True) + for i in range(self.config.linkcheck_workers): + thread = threading.Thread(target=self.check_thread, daemon=True) thread.start() self.workers.append(thread) + @property + def good(self) -> Set[str]: + warnings.warn( + "%s.%s is deprecated." % (self.__class__.__name__, "good"), + RemovedInSphinx50Warning, + stacklevel=2, + ) + return self._good + + @property + def broken(self) -> Dict[str, str]: + warnings.warn( + "%s.%s is deprecated." % (self.__class__.__name__, "broken"), + RemovedInSphinx50Warning, + stacklevel=2, + ) + return self._broken + + @property + def redirected(self) -> Dict[str, Tuple[str, int]]: + warnings.warn( + "%s.%s is deprecated." % (self.__class__.__name__, "redirected"), + RemovedInSphinx50Warning, + stacklevel=2, + ) + return self._redirected + def check_thread(self) -> None: - kwargs = { - 'allow_redirects': True, - } # type: Dict - if self.app.config.linkcheck_timeout: - kwargs['timeout'] = self.app.config.linkcheck_timeout + kwargs = {} + if self.config.linkcheck_timeout: + kwargs['timeout'] = self.config.linkcheck_timeout def get_request_headers() -> Dict: url = urlparse(uri) @@ -162,9 +205,9 @@ class CheckExternalLinksBuilder(Builder): kwargs['headers'] = get_request_headers() try: - if anchor and self.app.config.linkcheck_anchors: + if anchor and self.config.linkcheck_anchors: # Read the whole document and see if #anchor exists - response = requests.get(req_url, stream=True, config=self.app.config, + response = requests.get(req_url, stream=True, config=self.config, auth=auth_info, **kwargs) response.raise_for_status() found = check_anchor(response, unquote(anchor)) @@ -175,29 +218,42 @@ class CheckExternalLinksBuilder(Builder): try: # try a HEAD request first, which should be easier on # the server and the network - response = requests.head(req_url, config=self.app.config, - auth=auth_info, **kwargs) + response = requests.head(req_url, allow_redirects=True, + config=self.config, auth=auth_info, + **kwargs) response.raise_for_status() - except HTTPError: + except (HTTPError, TooManyRedirects) as err: + if isinstance(err, HTTPError) and err.response.status_code == 429: + raise # retry with GET request if that fails, some servers # don't like HEAD requests. - response = requests.get(req_url, stream=True, config=self.app.config, + response = requests.get(req_url, stream=True, + config=self.config, auth=auth_info, **kwargs) response.raise_for_status() except HTTPError as err: if err.response.status_code == 401: # We'll take "Unauthorized" as working. return 'working', ' - unauthorized', 0 + elif err.response.status_code == 429: + next_check = self.limit_rate(err.response) + if next_check is not None: + self.wqueue.put((next_check, uri, docname, lineno), False) + return 'rate-limited', '', 0 + return 'broken', str(err), 0 elif err.response.status_code == 503: # We'll take "Service Unavailable" as ignored. return 'ignored', str(err), 0 else: return 'broken', str(err), 0 except Exception as err: - if is_ssl_error(err): - return 'ignored', str(err), 0 - else: - return 'broken', str(err), 0 + return 'broken', str(err), 0 + else: + netloc = urlparse(req_url).netloc + try: + del self.rate_limits[netloc] + except KeyError: + pass if response.url.rstrip('/') == req_url.rstrip('/'): return 'working', '', 0 else: @@ -213,7 +269,7 @@ class CheckExternalLinksBuilder(Builder): def check(docname: str) -> Tuple[str, str, int]: # check for various conditions without bothering the network - if len(uri) == 0 or uri.startswith(('#', 'mailto:')): + if len(uri) == 0 or uri.startswith(('#', 'mailto:', 'tel:')): return 'unchecked', '', 0 elif not uri.startswith(('http:', 'https:')): if uri_re.match(uri): @@ -228,38 +284,97 @@ class CheckExternalLinksBuilder(Builder): if rex.match(uri): return 'ignored', '', 0 else: + self._broken[uri] = '' return 'broken', '', 0 - elif uri in self.good: + elif uri in self._good: return 'working', 'old', 0 - elif uri in self.broken: - return 'broken', self.broken[uri], 0 - elif uri in self.redirected: - return 'redirected', self.redirected[uri][0], self.redirected[uri][1] + elif uri in self._broken: + return 'broken', self._broken[uri], 0 + elif uri in self._redirected: + return 'redirected', self._redirected[uri][0], self._redirected[uri][1] for rex in self.to_ignore: if rex.match(uri): return 'ignored', '', 0 # need to actually check the URI - for _ in range(self.app.config.linkcheck_retries): + for _ in range(self.config.linkcheck_retries): status, info, code = check_uri() if status != "broken": break if status == "working": - self.good.add(uri) + self._good.add(uri) elif status == "broken": - self.broken[uri] = info + self._broken[uri] = info elif status == "redirected": - self.redirected[uri] = (info, code) + self._redirected[uri] = (info, code) return (status, info, code) while True: - uri, docname, lineno = self.wqueue.get() + next_check, uri, docname, lineno = self.wqueue.get() if uri is None: break + netloc = urlparse(uri).netloc + try: + # Refresh rate limit. + # When there are many links in the queue, workers are all stuck waiting + # for responses, but the builder keeps queuing. Links in the queue may + # have been queued before rate limits were discovered. + next_check = self.rate_limits[netloc].next_check + except KeyError: + pass + if next_check > time.time(): + # Sleep before putting message back in the queue to avoid + # waking up other threads. + time.sleep(QUEUE_POLL_SECS) + self.wqueue.put((next_check, uri, docname, lineno), False) + self.wqueue.task_done() + continue status, info, code = check(docname) - self.rqueue.put((uri, docname, lineno, status, info, code)) + if status == 'rate-limited': + logger.info(darkgray('-rate limited- ') + uri + darkgray(' | sleeping...')) + else: + self.rqueue.put((uri, docname, lineno, status, info, code)) + self.wqueue.task_done() + + def limit_rate(self, response: Response) -> Optional[float]: + next_check = None + retry_after = response.headers.get("Retry-After") + if retry_after: + try: + # Integer: time to wait before next attempt. + delay = float(retry_after) + except ValueError: + try: + # An HTTP-date: time of next attempt. + until = parsedate_to_datetime(retry_after) + except (TypeError, ValueError): + # TypeError: Invalid date format. + # ValueError: Invalid date, e.g. Oct 52th. + pass + else: + next_check = datetime.timestamp(until) + delay = (until - datetime.now(timezone.utc)).total_seconds() + else: + next_check = time.time() + delay + netloc = urlparse(response.url).netloc + if next_check is None: + max_delay = self.config.linkcheck_rate_limit_timeout + try: + rate_limit = self.rate_limits[netloc] + except KeyError: + delay = DEFAULT_DELAY + else: + last_wait_time = rate_limit.delay + delay = 2.0 * last_wait_time + if delay > max_delay and last_wait_time < max_delay: + delay = max_delay + if delay > max_delay: + return None + next_check = time.time() + delay + self.rate_limits[netloc] = RateLimit(delay, next_check) + return next_check def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: uri, docname, lineno, status, info, code = result @@ -313,62 +428,71 @@ class CheckExternalLinksBuilder(Builder): self.write_entry('redirected ' + text, docname, filename, lineno, uri + ' to ' + info) self.write_linkstat(linkstat) + else: + raise ValueError("Unknown status %s." % status) - def get_target_uri(self, docname: str, typ: str = None) -> str: - return '' + def write_entry(self, what: str, docname: str, filename: str, line: int, + uri: str) -> None: + self.txt_outfile.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) - def get_outdated_docs(self) -> Set[str]: - return self.env.found_docs + def write_linkstat(self, data: dict) -> None: + self.json_outfile.write(json.dumps(data)) + self.json_outfile.write('\n') - def prepare_writing(self, docnames: Set[str]) -> None: - return - - def write_doc(self, docname: str, doctree: Node) -> None: + def finish(self) -> None: logger.info('') - n = 0 + + for hyperlink in self.hyperlinks.values(): + self.wqueue.put(hyperlink, False) + + total_links = len(self.hyperlinks) + done = 0 + with open(path.join(self.outdir, 'output.txt'), 'w') as self.txt_outfile,\ + open(path.join(self.outdir, 'output.json'), 'w') as self.json_outfile: + while done < total_links: + self.process_result(self.rqueue.get()) + done += 1 + + if self._broken: + self.app.statuscode = 1 + + self.wqueue.join() + # Shutdown threads. + for worker in self.workers: + self.wqueue.put((CHECK_IMMEDIATELY, None, None, None), False) + + +class HyperlinkCollector(SphinxPostTransform): + builders = ('linkcheck',) + default_priority = 800 + + def run(self, **kwargs: Any) -> None: + builder = cast(CheckExternalLinksBuilder, self.app.builder) + hyperlinks = builder.hyperlinks # reference nodes - for refnode in doctree.traverse(nodes.reference): + for refnode in self.document.traverse(nodes.reference): if 'refuri' not in refnode: continue uri = refnode['refuri'] lineno = get_node_line(refnode) - self.wqueue.put((uri, docname, lineno), False) - n += 1 + uri_info = Hyperlink(CHECK_IMMEDIATELY, uri, self.env.docname, lineno) + if uri not in hyperlinks: + hyperlinks[uri] = uri_info # image nodes - for imgnode in doctree.traverse(nodes.image): + for imgnode in self.document.traverse(nodes.image): uri = imgnode['candidates'].get('?') if uri and '://' in uri: lineno = get_node_line(imgnode) - self.wqueue.put((uri, docname, lineno), False) - n += 1 - - done = 0 - while done < n: - self.process_result(self.rqueue.get()) - done += 1 - - if self.broken: - self.app.statuscode = 1 - - def write_entry(self, what: str, docname: str, filename: str, line: int, - uri: str) -> None: - with open(path.join(self.outdir, 'output.txt'), 'a') as output: - output.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) - - def write_linkstat(self, data: dict) -> None: - with open(path.join(self.outdir, 'output.json'), 'a') as output: - output.write(json.dumps(data)) - output.write('\n') - - def finish(self) -> None: - for worker in self.workers: - self.wqueue.put((None, None, None), False) + uri_info = Hyperlink(CHECK_IMMEDIATELY, uri, self.env.docname, lineno) + if uri not in hyperlinks: + hyperlinks[uri] = uri_info def setup(app: Sphinx) -> Dict[str, Any]: app.add_builder(CheckExternalLinksBuilder) + app.add_post_transform(HyperlinkCollector) app.add_config_value('linkcheck_ignore', [], None) app.add_config_value('linkcheck_auth', [], None) @@ -380,6 +504,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: # Anchors starting with ! are ignored since they are # commonly used for dynamic pages app.add_config_value('linkcheck_anchors_ignore', ["^!"], None) + app.add_config_value('linkcheck_rate_limit_timeout', 300.0, None) return { 'version': 'builtin', diff --git a/sphinx/builders/manpage.py b/sphinx/builders/manpage.py index 2a10ba62f..292f495de 100644 --- a/sphinx/builders/manpage.py +++ b/sphinx/builders/manpage.py @@ -4,7 +4,7 @@ Manual pages builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -20,13 +20,11 @@ from sphinx.builders import Builder from sphinx.config import Config from sphinx.errors import NoUri from sphinx.locale import __ -from sphinx.util import logging -from sphinx.util import progress_message +from sphinx.util import logging, progress_message from sphinx.util.console import darkgreen # type: ignore from sphinx.util.nodes import inline_all_toctrees from sphinx.util.osutil import ensuredir, make_filename_from_project -from sphinx.writers.manpage import ManualPageWriter, ManualPageTranslator - +from sphinx.writers.manpage import ManualPageTranslator, ManualPageWriter logger = logging.getLogger(__name__) diff --git a/sphinx/builders/qthelp.py b/sphinx/builders/qthelp.py index cec4d338e..c5219dd75 100644 --- a/sphinx/builders/qthelp.py +++ b/sphinx/builders/qthelp.py @@ -4,7 +4,7 @@ Build input files for the Qt collection generator. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -17,7 +17,6 @@ import sphinx from sphinx.application import Sphinx from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias - deprecated_alias('sphinx.builders.qthelp', { 'render_file': render_file, diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py index df90b4c73..2e72887e3 100644 --- a/sphinx/builders/singlehtml.py +++ b/sphinx/builders/singlehtml.py @@ -4,7 +4,7 @@ Single HTML builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -19,8 +19,7 @@ from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.environment.adapters.toctree import TocTree from sphinx.locale import __ -from sphinx.util import logging -from sphinx.util import progress_message +from sphinx.util import logging, progress_message from sphinx.util.console import darkgreen # type: ignore from sphinx.util.nodes import inline_all_toctrees diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index 5e2e6e240..1a56be0f9 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -4,7 +4,7 @@ Texinfo builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -16,23 +16,20 @@ from docutils import nodes from docutils.frontend import OptionParser from docutils.io import FileOutput -from sphinx import addnodes -from sphinx import package_dir +from sphinx import addnodes, package_dir from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.config import Config from sphinx.environment.adapters.asset import ImageAdapter from sphinx.errors import NoUri from sphinx.locale import _, __ -from sphinx.util import logging -from sphinx.util import progress_message, status_iterator +from sphinx.util import logging, progress_message, status_iterator from sphinx.util.console import darkgreen # type: ignore from sphinx.util.docutils import new_document from sphinx.util.fileutil import copy_asset_file from sphinx.util.nodes import inline_all_toctrees from sphinx.util.osutil import SEP, ensuredir, make_filename_from_project -from sphinx.writers.texinfo import TexinfoWriter, TexinfoTranslator - +from sphinx.writers.texinfo import TexinfoTranslator, TexinfoWriter logger = logging.getLogger(__name__) template_dir = os.path.join(package_dir, 'templates', 'texinfo') @@ -182,7 +179,8 @@ class TexinfoBuilder(Builder): try: imagedir = path.join(self.outdir, targetname + '-figures') ensuredir(imagedir) - copy_asset_file(path.join(self.srcdir, dest), imagedir) + copy_asset_file(path.join(self.srcdir, src), + path.join(imagedir, dest)) except Exception as err: logger.warning(__('cannot copy image file %r: %s'), path.join(self.srcdir, src), err) diff --git a/sphinx/builders/text.py b/sphinx/builders/text.py index 89b041abd..ae770818c 100644 --- a/sphinx/builders/text.py +++ b/sphinx/builders/text.py @@ -4,7 +4,7 @@ Plain-text Sphinx builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -19,7 +19,7 @@ from sphinx.builders import Builder from sphinx.locale import __ from sphinx.util import logging from sphinx.util.osutil import ensuredir, os_path -from sphinx.writers.text import TextWriter, TextTranslator +from sphinx.writers.text import TextTranslator, TextWriter logger = logging.getLogger(__name__) diff --git a/sphinx/builders/xml.py b/sphinx/builders/xml.py index 81d729def..1b051119b 100644 --- a/sphinx/builders/xml.py +++ b/sphinx/builders/xml.py @@ -4,7 +4,7 @@ Docutils-native XML and pseudo-XML builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -21,7 +21,7 @@ from sphinx.builders import Builder from sphinx.locale import __ from sphinx.util import logging from sphinx.util.osutil import ensuredir, os_path -from sphinx.writers.xml import XMLWriter, PseudoXMLWriter +from sphinx.writers.xml import PseudoXMLWriter, XMLWriter if False: # For type annotation diff --git a/sphinx/cmd/__init__.py b/sphinx/cmd/__init__.py index 33fbf747b..583e50ed7 100644 --- a/sphinx/cmd/__init__.py +++ b/sphinx/cmd/__init__.py @@ -4,6 +4,6 @@ Modules for command line executables. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py index c4cf11cc4..32a89eb29 100644 --- a/sphinx/cmd/build.py +++ b/sphinx/cmd/build.py @@ -4,7 +4,7 @@ Build documentation from a provided source. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -16,7 +16,7 @@ import os import pdb import sys import traceback -from typing import Any, IO, List +from typing import IO, Any, List from docutils.utils import SystemMessage @@ -26,7 +26,7 @@ from sphinx.application import Sphinx from sphinx.errors import SphinxError from sphinx.locale import __ from sphinx.util import Tee, format_exception_cut_frames, save_traceback -from sphinx.util.console import red, nocolor, color_terminal, terminal_safe # type: ignore +from sphinx.util.console import color_terminal, nocolor, red, terminal_safe # type: ignore from sphinx.util.docutils import docutils_namespace, patch_docutils diff --git a/sphinx/cmd/make_mode.py b/sphinx/cmd/make_mode.py index aff2ea7f5..aaa40fba0 100644 --- a/sphinx/cmd/make_mode.py +++ b/sphinx/cmd/make_mode.py @@ -10,7 +10,7 @@ This is in its own module so that importing it is fast. It should not import the main Sphinx modules (like sphinx.applications, sphinx.builders). - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -22,10 +22,9 @@ from typing import List import sphinx from sphinx.cmd.build import build_main -from sphinx.util.console import color_terminal, nocolor, bold, blue # type: ignore +from sphinx.util.console import blue, bold, color_terminal, nocolor # type: ignore from sphinx.util.osutil import cd, rmtree - BUILDERS = [ ("", "html", "to make standalone HTML files"), ("", "dirhtml", "to make HTML files named index.html in directories"), diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index cad3c65e5..4234c039c 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -4,7 +4,7 @@ Quickly setup documentation source to work with Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -29,6 +29,7 @@ try: readline.parse_and_bind("tab: complete") USE_LIBEDIT = False except ImportError: + readline = None USE_LIBEDIT = False from docutils.utils import column_width @@ -37,9 +38,8 @@ import sphinx.locale from sphinx import __display_version__, package_dir from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import __ -from sphinx.util.console import ( # type: ignore - colorize, bold, red, turquoise, nocolor, color_terminal -) +from sphinx.util.console import (bold, color_terminal, colorize, nocolor, red, # type: ignore + turquoise) from sphinx.util.osutil import ensuredir from sphinx.util.template import SphinxRenderer @@ -170,8 +170,11 @@ def do_prompt(text: str, default: str = None, validator: Callable[[str], Any] = # sequence (see #5335). To avoid the problem, all prompts are not colored # on libedit. pass - else: + elif readline: + # pass input_mode=True if readline available prompt = colorize(COLOR_QUESTION, prompt, input_mode=True) + else: + prompt = colorize(COLOR_QUESTION, prompt, input_mode=False) x = term_input(prompt).strip() if default and not x: x = default @@ -489,8 +492,10 @@ def get_parser() -> argparse.ArgumentParser: help=__('project root')) group = parser.add_argument_group(__('Structure options')) - group.add_argument('--sep', action='store_true', default=None, + group.add_argument('--sep', action='store_true', dest='sep', default=None, help=__('if specified, separate source and build dirs')) + group.add_argument('--no-sep', action='store_false', dest='sep', + help=__('if specified, create build dir under source dir')) group.add_argument('--dot', metavar='DOT', default='_', help=__('replacement for dot in _templates etc.')) diff --git a/sphinx/config.py b/sphinx/config.py index 353268e38..4c038b061 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -4,7 +4,7 @@ Build configuration file handling. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,10 +13,9 @@ import traceback import types import warnings from collections import OrderedDict -from os import path, getenv -from typing import ( - Any, Callable, Dict, Generator, Iterator, List, NamedTuple, Set, Tuple, Union -) +from os import getenv, path +from typing import (Any, Callable, Dict, Generator, Iterator, List, NamedTuple, Set, Tuple, + Union) from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import ConfigError, ExtensionError @@ -99,7 +98,8 @@ class Config: # general options 'project': ('Python', 'env', []), 'author': ('unknown', 'env', []), - 'copyright': ('', 'html', []), + 'project_copyright': ('', 'html', [str]), + 'copyright': (lambda c: c.project_copyright, 'html', [str]), 'version': ('', 'env', []), 'release': ('', 'env', []), 'today': ('', 'env', []), @@ -181,6 +181,14 @@ class Config: defvalue = self.values[name][0] if self.values[name][2] == Any: return value + elif self.values[name][2] == {bool, str}: + if value == '0': + # given falsy string from command line option + return False + elif value == '1': + return True + else: + return value elif type(defvalue) is bool or self.values[name][2] == [bool]: if value == '0': # given falsy string from command line option @@ -359,6 +367,18 @@ def convert_source_suffix(app: "Sphinx", config: Config) -> None: "But `%r' is given." % source_suffix)) +def convert_highlight_options(app: "Sphinx", config: Config) -> None: + """Convert old styled highlight_options to new styled one. + + * old style: options + * new style: dict that maps language names to options + """ + options = config.highlight_options + if options and not all(isinstance(v, dict) for v in options.values()): + # old styled option detected because all values are not dictionary. + config.highlight_options = {config.highlight_language: options} # type: ignore + + def init_numfig_format(app: "Sphinx", config: Config) -> None: """Initialize :confval:`numfig_format`.""" numfig_format = {'section': _('Section %s'), @@ -479,6 +499,7 @@ def check_master_doc(app: "Sphinx", env: "BuildEnvironment", added: Set[str], def setup(app: "Sphinx") -> Dict[str, Any]: app.connect('config-inited', convert_source_suffix, priority=800) + app.connect('config-inited', convert_highlight_options, priority=800) app.connect('config-inited', init_numfig_format, priority=800) app.connect('config-inited', correct_copyright_year, priority=800) app.connect('config-inited', check_confval_types, priority=800) diff --git a/sphinx/deprecation.py b/sphinx/deprecation.py index 48db97760..161d7bcac 100644 --- a/sphinx/deprecation.py +++ b/sphinx/deprecation.py @@ -4,7 +4,7 @@ Sphinx deprecation classes and utilities. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -26,6 +26,10 @@ class RemovedInSphinx50Warning(PendingDeprecationWarning): pass +class RemovedInSphinx60Warning(PendingDeprecationWarning): + pass + + RemovedInNextVersionWarning = RemovedInSphinx40Warning diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index e6313ddab..e386b3eaa 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -4,13 +4,12 @@ Handlers for additional ReST directives. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re -from typing import Any, Dict, List, Tuple -from typing import cast +from typing import Any, Dict, Generic, List, Tuple, TypeVar, cast from docutils import nodes from docutils.nodes import Node @@ -18,9 +17,8 @@ from docutils.parsers.rst import directives, roles from sphinx import addnodes from sphinx.addnodes import desc_signature -from sphinx.deprecation import ( - RemovedInSphinx40Warning, RemovedInSphinx50Warning, deprecated_alias -) +from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning, + deprecated_alias) from sphinx.util import docutils from sphinx.util.docfields import DocFieldTransformer, Field, TypedField from sphinx.util.docutils import SphinxDirective @@ -35,6 +33,8 @@ if False: nl_escape_re = re.compile(r'\\\n') strip_backslash_re = re.compile(r'\\(.)') +T = TypeVar('T') + def optional_int(argument: str) -> int: """ @@ -49,7 +49,7 @@ def optional_int(argument: str) -> int: return value -class ObjectDescription(SphinxDirective): +class ObjectDescription(SphinxDirective, Generic[T]): """ Directive to describe a class, function or similar object. Not used directly, but subclassed (in domain-specific directives) to add custom @@ -99,7 +99,7 @@ class ObjectDescription(SphinxDirective): else: return [line.strip() for line in lines] - def handle_signature(self, sig: str, signode: desc_signature) -> Any: + def handle_signature(self, sig: str, signode: desc_signature) -> T: """ Parse the signature *sig* into individual nodes and append them to *signode*. If ValueError is raised, parsing is aborted and the whole @@ -111,7 +111,7 @@ class ObjectDescription(SphinxDirective): """ raise ValueError - def add_target_and_index(self, name: Any, sig: str, signode: desc_signature) -> None: + def add_target_and_index(self, name: T, sig: str, signode: desc_signature) -> None: """ Add cross-reference IDs and entries to self.indexnode, if applicable. @@ -175,7 +175,7 @@ class ObjectDescription(SphinxDirective): if self.domain: node['classes'].append(self.domain) - self.names = [] # type: List[Any] + self.names = [] # type: List[T] signatures = self.get_signatures() for i, sig in enumerate(signatures): # add a signature node for each signature in the current unit @@ -266,16 +266,10 @@ class DefaultDomain(SphinxDirective): self.env.temp_data['default_domain'] = self.env.domains.get(domain_name) return [] -from sphinx.directives.code import ( # noqa - Highlight, CodeBlock, LiteralInclude -) -from sphinx.directives.other import ( # noqa - TocTree, Author, VersionChange, SeeAlso, - TabularColumns, Centered, Acks, HList, Only, Include, Class -) -from sphinx.directives.patches import ( # noqa - Figure, Meta -) +from sphinx.directives.code import CodeBlock, Highlight, LiteralInclude # noqa +from sphinx.directives.other import (Acks, Author, Centered, Class, HList, Include, # noqa + Only, SeeAlso, TabularColumns, TocTree, VersionChange) +from sphinx.directives.patches import Figure, Meta # noqa from sphinx.domains.index import IndexDirective # noqa deprecated_alias('sphinx.directives', diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index 4ca849cf0..e01b8f9ec 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -2,11 +2,12 @@ sphinx.directives.code ~~~~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import sys +import textwrap import warnings from difflib import unified_diff from typing import Any, Dict, List, Tuple @@ -19,9 +20,9 @@ from docutils.statemachine import StringList from sphinx import addnodes from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.directives import optional_int from sphinx.locale import __ -from sphinx.util import logging -from sphinx.util import parselinenos +from sphinx.util import logging, parselinenos from sphinx.util.docutils import SphinxDirective if False: @@ -69,7 +70,7 @@ class HighlightLang(Highlight): def dedent_lines(lines: List[str], dedent: int, location: Tuple[str, int] = None) -> List[str]: if not dedent: - return lines + return textwrap.dedent(''.join(lines)).splitlines(True) if any(s[:dedent].strip() for s in lines): logger.warning(__('non-whitespace stripped by dedent'), location=location) @@ -118,7 +119,7 @@ class CodeBlock(SphinxDirective): option_spec = { 'force': directives.flag, 'linenos': directives.flag, - 'dedent': int, + 'dedent': optional_int, 'lineno-start': int, 'emphasize-lines': directives.unchanged_required, 'caption': directives.unchanged_required, @@ -392,7 +393,7 @@ class LiteralInclude(SphinxDirective): optional_arguments = 0 final_argument_whitespace = True option_spec = { - 'dedent': int, + 'dedent': optional_int, 'linenos': directives.flag, 'lineno-start': int, 'lineno-match': directives.flag, diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index f55f95219..2ace3738f 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -2,13 +2,12 @@ sphinx.directives.other ~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re -from typing import Any, Dict, List -from typing import cast +from typing import Any, Dict, List, cast from docutils import nodes from docutils.nodes import Element, Node @@ -21,7 +20,7 @@ from sphinx import addnodes from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.domains.changeset import VersionChange # NOQA # for compatibility from sphinx.locale import _ -from sphinx.util import url_re, docname_join +from sphinx.util import docname_join, url_re from sphinx.util.docutils import SphinxDirective from sphinx.util.matching import Matcher, patfilter from sphinx.util.nodes import explicit_title_re @@ -277,6 +276,7 @@ class HList(SphinxDirective): npercol, nmore = divmod(len(fulllist), ncolumns) index = 0 newnode = addnodes.hlist() + newnode['ncolumns'] = str(ncolumns) for column in range(ncolumns): endindex = index + ((npercol + 1) if column < nmore else npercol) bullet_list = nodes.bullet_list() diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index 4b73a7955..1eae6d0c8 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -2,17 +2,16 @@ sphinx.directives.patches ~~~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List, Tuple -from typing import cast +from typing import Any, Dict, List, Tuple, cast from docutils import nodes from docutils.nodes import Node, make_id, system_message from docutils.parsers.rst import directives -from docutils.parsers.rst.directives import images, html, tables +from docutils.parsers.rst.directives import html, images, tables from sphinx import addnodes from sphinx.directives import optional_int diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index 11b3a4604..60142f584 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -5,13 +5,12 @@ Support for domains, which are groupings of description directives and roles describing e.g. constructs of one programming language. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import copy -from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Tuple, Union -from typing import cast +from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Tuple, Union, cast from docutils import nodes from docutils.nodes import Element, Node, system_message @@ -26,6 +25,7 @@ from sphinx.util.typing import RoleFunction if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.builders import Builder from sphinx.environment import BuildEnvironment diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 304c871ea..5336b003c 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -4,15 +4,13 @@ The C language domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re -from typing import ( - Any, Callable, Dict, Generator, Iterator, List, Type, TypeVar, Tuple, Union -) -from typing import cast +from typing import (Any, Callable, Dict, Generator, Iterator, List, Tuple, Type, TypeVar, + Union, cast) from docutils import nodes from docutils.nodes import Element, Node, TextElement, system_message @@ -31,15 +29,13 @@ from sphinx.roles import SphinxRole, XRefRole from sphinx.transforms import SphinxTransform from sphinx.transforms.post_transforms import ReferencesResolver from sphinx.util import logging -from sphinx.util.cfamily import ( - NoOldIdError, ASTBaseBase, ASTAttribute, ASTBaseParenExprList, - verify_description_mode, StringifyTransform, - BaseParser, DefinitionError, UnsupportedMultiCharacterCharLiteral, - identifier_re, anon_identifier_re, integer_literal_re, octal_literal_re, - hex_literal_re, binary_literal_re, integers_literal_suffix_re, - float_literal_re, float_literal_suffix_re, - char_literal_re -) +from sphinx.util.cfamily import (ASTAttribute, ASTBaseBase, ASTBaseParenExprList, BaseParser, + DefinitionError, NoOldIdError, StringifyTransform, + UnsupportedMultiCharacterCharLiteral, anon_identifier_re, + binary_literal_re, char_literal_re, float_literal_re, + float_literal_suffix_re, hex_literal_re, identifier_re, + integer_literal_re, integers_literal_suffix_re, + octal_literal_re, verify_description_mode) from sphinx.util.docfields import Field, TypedField from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_refnode @@ -47,6 +43,11 @@ from sphinx.util.nodes import make_refnode logger = logging.getLogger(__name__) T = TypeVar('T') +DeclarationType = Union[ + "ASTStruct", "ASTUnion", "ASTEnum", "ASTEnumerator", + "ASTType", "ASTTypeWithInit", "ASTMacro", +] + # https://en.cppreference.com/w/c/keyword _keywords = [ 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', @@ -136,8 +137,7 @@ class ASTIdentifier(ASTBaseBase): reftype='identifier', reftarget=targetText, modname=None, classname=None) - key = symbol.get_lookup_key() - pnode['c:parent_key'] = key + pnode['c:parent_key'] = symbol.get_lookup_key() if self.is_anon(): pnode += nodes.strong(text="[anonymous]") else: @@ -636,6 +636,10 @@ class ASTFunctionParameter(ASTBase): self.arg = arg self.ellipsis = ellipsis + def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=False) + def _stringify(self, transform: StringifyTransform) -> str: if self.ellipsis: return '...' @@ -1149,6 +1153,9 @@ class ASTType(ASTBase): def name(self) -> ASTNestedName: return self.decl.name + def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: + return symbol.get_full_nested_name().get_id(version) + @property def function_params(self) -> List[ASTFunctionParameter]: return self.decl.function_params @@ -1191,6 +1198,9 @@ class ASTTypeWithInit(ASTBase): def name(self) -> ASTNestedName: return self.type.name + def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: + return self.type.get_id(version, objectType, symbol) + def _stringify(self, transform: StringifyTransform) -> str: res = [] res.append(transform(self.type)) @@ -1242,6 +1252,9 @@ class ASTMacro(ASTBase): def name(self) -> ASTNestedName: return self.ident + def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: + return symbol.get_full_nested_name().get_id(version) + def _stringify(self, transform: StringifyTransform) -> str: res = [] res.append(transform(self.ident)) @@ -1342,7 +1355,8 @@ class ASTEnumerator(ASTBase): class ASTDeclaration(ASTBaseBase): - def __init__(self, objectType: str, directiveType: str, declaration: Any, + def __init__(self, objectType: str, directiveType: str, + declaration: Union[DeclarationType, ASTFunctionParameter], semicolon: bool = False) -> None: self.objectType = objectType self.directiveType = directiveType @@ -1359,18 +1373,20 @@ class ASTDeclaration(ASTBaseBase): @property def name(self) -> ASTNestedName: - return self.declaration.name + decl = cast(DeclarationType, self.declaration) + return decl.name @property def function_params(self) -> List[ASTFunctionParameter]: if self.objectType != 'function': return None - return self.declaration.function_params + decl = cast(ASTType, self.declaration) + return decl.function_params def get_id(self, version: int, prefixed: bool = True) -> str: if self.objectType == 'enumerator' and self.enumeratorScopedSymbol: return self.enumeratorScopedSymbol.declaration.get_id(version, prefixed) - id_ = self.symbol.get_full_nested_name().get_id(version) + id_ = self.declaration.get_id(version, self.objectType, self.symbol) if prefixed: return _id_prefix[version] + id_ else: @@ -1413,7 +1429,8 @@ class ASTDeclaration(ASTBaseBase): elif self.objectType == 'enumerator': mainDeclNode += addnodes.desc_annotation('enumerator ', 'enumerator ') elif self.objectType == 'type': - prefix = self.declaration.get_type_declaration_prefix() + decl = cast(ASTType, self.declaration) + prefix = decl.get_type_declaration_prefix() prefix += ' ' mainDeclNode += addnodes.desc_annotation(prefix, prefix) else: @@ -1454,7 +1471,7 @@ class Symbol: assert False # shouldn't happen else: # the domain base class makes a copy of the initial data, which is fine - return Symbol(None, None, None, None) + return Symbol(None, None, None, None, None) @staticmethod def debug_print(*args: Any) -> None: @@ -1477,7 +1494,7 @@ class Symbol: return super().__setattr__(key, value) def __init__(self, parent: "Symbol", ident: ASTIdentifier, - declaration: ASTDeclaration, docname: str) -> None: + declaration: ASTDeclaration, docname: str, line: int) -> None: self.parent = parent # declarations in a single directive are linked together self.siblingAbove = None # type: Symbol @@ -1485,6 +1502,7 @@ class Symbol: self.ident = ident self.declaration = declaration self.docname = docname + self.line = line self.isRedeclaration = False self._assert_invariants() @@ -1500,15 +1518,18 @@ class Symbol: # Do symbol addition after self._children has been initialised. self._add_function_params() - def _fill_empty(self, declaration: ASTDeclaration, docname: str) -> None: + def _fill_empty(self, declaration: ASTDeclaration, docname: str, line: int) -> None: self._assert_invariants() - assert not self.declaration - assert not self.docname - assert declaration - assert docname + assert self.declaration is None + assert self.docname is None + assert self.line is None + assert declaration is not None + assert docname is not None + assert line is not None self.declaration = declaration self.declaration.symbol = self self.docname = docname + self.line = line self._assert_invariants() # and symbol addition should be done as well self._add_function_params() @@ -1532,7 +1553,7 @@ class Symbol: decl = ASTDeclaration('functionParam', None, p) assert not nn.rooted assert len(nn.names) == 1 - self._add_symbols(nn, decl, self.docname) + self._add_symbols(nn, decl, self.docname, self.line) if Symbol.debug_lookup: Symbol.debug_indent -= 1 @@ -1549,6 +1570,7 @@ class Symbol: if sChild.declaration and sChild.docname == docname: sChild.declaration = None sChild.docname = None + sChild.line = None if sChild.siblingAbove is not None: sChild.siblingAbove.siblingBelow = sChild.siblingBelow if sChild.siblingBelow is not None: @@ -1559,13 +1581,11 @@ class Symbol: def get_all_symbols(self) -> Iterator["Symbol"]: yield self for sChild in self._children: - for s in sChild.get_all_symbols(): - yield s + yield from sChild.get_all_symbols() @property def children(self) -> Iterator["Symbol"]: - for c in self._children: - yield c + yield from self._children @property def children_recurse_anon(self) -> Iterator["Symbol"]: @@ -1742,7 +1762,7 @@ class Symbol: return SymbolLookupResult(symbols, parentSymbol, ident) def _add_symbols(self, nestedName: ASTNestedName, - declaration: ASTDeclaration, docname: str) -> "Symbol": + declaration: ASTDeclaration, docname: str, line: int) -> "Symbol": # TODO: further simplification from C++ to C # Used for adding a whole path of symbols, where the last may or may not # be an actual declaration. @@ -1751,9 +1771,9 @@ class Symbol: Symbol.debug_indent += 1 Symbol.debug_print("_add_symbols:") Symbol.debug_indent += 1 - Symbol.debug_print("nn: ", nestedName) - Symbol.debug_print("decl: ", declaration) - Symbol.debug_print("doc: ", docname) + Symbol.debug_print("nn: ", nestedName) + Symbol.debug_print("decl: ", declaration) + Symbol.debug_print("location: {}:{}".format(docname, line)) def onMissingQualifiedSymbol(parentSymbol: "Symbol", ident: ASTIdentifier) -> "Symbol": if Symbol.debug_lookup: @@ -1763,7 +1783,7 @@ class Symbol: Symbol.debug_print("ident: ", ident) Symbol.debug_indent -= 2 return Symbol(parent=parentSymbol, ident=ident, - declaration=None, docname=None) + declaration=None, docname=None, line=None) lookupResult = self._symbol_lookup(nestedName, onMissingQualifiedSymbol, @@ -1779,12 +1799,12 @@ class Symbol: Symbol.debug_indent += 1 Symbol.debug_print("ident: ", lookupResult.ident) Symbol.debug_print("declaration: ", declaration) - Symbol.debug_print("docname: ", docname) + Symbol.debug_print("location: {}:{}".format(docname, line)) Symbol.debug_indent -= 1 symbol = Symbol(parent=lookupResult.parentSymbol, ident=lookupResult.ident, declaration=declaration, - docname=docname) + docname=docname, line=line) if Symbol.debug_lookup: Symbol.debug_indent -= 2 return symbol @@ -1832,7 +1852,7 @@ class Symbol: symbol = Symbol(parent=lookupResult.parentSymbol, ident=lookupResult.ident, declaration=declaration, - docname=docname) + docname=docname, line=line) if Symbol.debug_lookup: Symbol.debug_print("end: creating candidate symbol") return symbol @@ -1898,7 +1918,7 @@ class Symbol: # .. namespace:: Test # .. namespace:: nullptr # .. class:: Test - symbol._fill_empty(declaration, docname) + symbol._fill_empty(declaration, docname, line) return symbol def merge_with(self, other: "Symbol", docnames: List[str], @@ -1919,13 +1939,15 @@ class Symbol: continue if otherChild.declaration and otherChild.docname in docnames: if not ourChild.declaration: - ourChild._fill_empty(otherChild.declaration, otherChild.docname) + ourChild._fill_empty(otherChild.declaration, + otherChild.docname, otherChild.line) elif ourChild.docname != otherChild.docname: name = str(ourChild.declaration) - msg = __("Duplicate C declaration, also defined in '%s'.\n" - "Declaration is '%s'.") - msg = msg % (ourChild.docname, name) - logger.warning(msg, location=otherChild.docname) + msg = __("Duplicate C declaration, also defined at %s:%s.\n" + "Declaration is '.. c:%s:: %s'.") + msg = msg % (ourChild.docname, ourChild.line, + ourChild.declaration.directiveType, name) + logger.warning(msg, location=(otherChild.docname, otherChild.line)) else: # Both have declarations, and in the same docname. # This can apparently happen, it should be safe to @@ -1939,19 +1961,21 @@ class Symbol: if Symbol.debug_lookup: Symbol.debug_indent += 1 Symbol.debug_print("add_name:") - res = self._add_symbols(nestedName, declaration=None, docname=None) + res = self._add_symbols(nestedName, declaration=None, docname=None, line=None) if Symbol.debug_lookup: Symbol.debug_indent -= 1 return res - def add_declaration(self, declaration: ASTDeclaration, docname: str) -> "Symbol": + def add_declaration(self, declaration: ASTDeclaration, + docname: str, line: int) -> "Symbol": if Symbol.debug_lookup: Symbol.debug_indent += 1 Symbol.debug_print("add_declaration:") - assert declaration - assert docname + assert declaration is not None + assert docname is not None + assert line is not None nestedName = declaration.name - res = self._add_symbols(nestedName, declaration, docname) + res = self._add_symbols(nestedName, declaration, docname, line) if Symbol.debug_lookup: Symbol.debug_indent -= 1 return res @@ -2988,7 +3012,7 @@ class DefinitionParser(BaseParser): def parse_pre_v3_type_definition(self) -> ASTDeclaration: self.skip_ws() - declaration = None # type: Any + declaration = None # type: DeclarationType if self.skip_word('struct'): typ = 'struct' declaration = self._parse_struct() @@ -3011,7 +3035,7 @@ class DefinitionParser(BaseParser): 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): raise Exception('Internal error, unknown directiveType "%s".' % directiveType) - declaration = None # type: Any + declaration = None # type: DeclarationType if objectType == 'member': declaration = self._parse_type_with_init(named=True, outer='member') elif objectType == 'function': @@ -3074,7 +3098,7 @@ def _make_phony_error_name() -> ASTNestedName: return ASTNestedName([ASTIdentifier("PhonyNameDueToError")], rooted=False) -class CObject(ObjectDescription): +class CObject(ObjectDescription[ASTDeclaration]): """ Description of a C language object. """ @@ -3128,7 +3152,7 @@ class CObject(ObjectDescription): declClone.enumeratorScopedSymbol = symbol Symbol(parent=targetSymbol, ident=symbol.ident, declaration=declClone, - docname=self.env.docname) + docname=self.env.docname, line=self.get_source_info()[1]) def add_target_and_index(self, ast: ASTDeclaration, sig: str, signode: TextElement) -> None: @@ -3158,10 +3182,6 @@ class CObject(ObjectDescription): self.state.document.note_explicit_target(signode) - domain = cast(CDomain, self.env.get_domain('c')) - if name not in domain.objects: - domain.objects[name] = (domain.env.docname, newestId, self.objtype) - if 'noindexentry' not in self.options: indexText = self.get_index_text(name) self.indexnode['entries'].append(('single', indexText, newestId, '', None)) @@ -3183,7 +3203,8 @@ class CObject(ObjectDescription): def parse_pre_v3_type_definition(self, parser: DefinitionParser) -> ASTDeclaration: return parser.parse_pre_v3_type_definition() - def describe_signature(self, signode: TextElement, ast: Any, options: Dict) -> None: + def describe_signature(self, signode: TextElement, ast: ASTDeclaration, + options: Dict) -> None: ast.describe_signature(signode, 'lastIsName', self.env, options) def run(self) -> List[Node]: @@ -3235,7 +3256,8 @@ class CObject(ObjectDescription): raise ValueError from e try: - symbol = parentSymbol.add_declaration(ast, docname=self.env.docname) + symbol = parentSymbol.add_declaration( + ast, docname=self.env.docname, line=self.get_source_info()[1]) # append the new declaration to the sibling list assert symbol.siblingAbove is None assert symbol.siblingBelow is None @@ -3248,9 +3270,9 @@ class CObject(ObjectDescription): # Assume we are actually in the old symbol, # instead of the newly created duplicate. self.env.temp_data['c:last_symbol'] = e.symbol - msg = __("Duplicate C declaration, also defined in '%s'.\n" - "Declaration is '%s'.") - msg = msg % (e.symbol.docname, sig) + msg = __("Duplicate C declaration, also defined at %s:%s.\n" + "Declaration is '.. c:%s:: %s'.") + msg = msg % (e.symbol.docname, e.symbol.line, self.display_object_type, sig) logger.warning(msg, location=signode) if ast.objectType == 'enumerator': @@ -3429,8 +3451,9 @@ class AliasNode(nodes.Element): assert parentKey is not None self.parentKey = parentKey - def copy(self: T) -> T: - return self.__class__(self.sig, env=None, parentKey=self.parentKey) # type: ignore + def copy(self) -> 'AliasNode': + return self.__class__(self.sig, self.maxdepth, self.document, + env=None, parentKey=self.parentKey) class AliasTransform(SphinxTransform): @@ -3619,7 +3642,7 @@ class CExprRole(SphinxRole): location=self.get_source_info()) # see below return [self.node_type(text, text, classes=classes)], [] - parentSymbol = self.env.temp_data.get('cpp:parent_symbol', None) + parentSymbol = self.env.temp_data.get('c:parent_symbol', None) if parentSymbol is None: parentSymbol = self.env.domaindata['c']['root_symbol'] # ...most if not all of these classes should really apply to the individual references, @@ -3634,15 +3657,18 @@ class CDomain(Domain): name = 'c' label = 'C' object_types = { - 'function': ObjType(_('function'), 'func'), - 'member': ObjType(_('member'), 'member'), - 'macro': ObjType(_('macro'), 'macro'), - 'type': ObjType(_('type'), 'type'), - 'var': ObjType(_('variable'), 'data'), - 'enum': ObjType(_('enum'), 'enum'), - 'enumerator': ObjType(_('enumerator'), 'enumerator'), - 'struct': ObjType(_('struct'), 'struct'), - 'union': ObjType(_('union'), 'union'), + # 'identifier' is the one used for xrefs generated in signatures, not in roles + 'member': ObjType(_('member'), 'var', 'member', 'data', 'identifier'), + 'var': ObjType(_('variable'), 'var', 'member', 'data', 'identifier'), + 'function': ObjType(_('function'), 'func', 'identifier', 'type'), + 'macro': ObjType(_('macro'), 'macro', 'identifier'), + 'struct': ObjType(_('struct'), 'struct', 'identifier', 'type'), + 'union': ObjType(_('union'), 'union', 'identifier', 'type'), + 'enum': ObjType(_('enum'), 'enum', 'identifier', 'type'), + 'enumerator': ObjType(_('enumerator'), 'enumerator', 'identifier'), + 'type': ObjType(_('type'), 'identifier', 'type'), + # generated object types + 'functionParam': ObjType(_('function parameter'), 'identifier', 'var', 'member', 'data'), # noqa } directives = { @@ -3677,14 +3703,10 @@ class CDomain(Domain): 'texpr': CExprRole(asCode=False) } initial_data = { - 'root_symbol': Symbol(None, None, None, None), + 'root_symbol': Symbol(None, None, None, None, None), 'objects': {}, # fullname -> docname, node_id, objtype } # type: Dict[str, Union[Symbol, Dict[str, Tuple[str, str, str]]]] - @property - def objects(self) -> Dict[str, Tuple[str, str, str]]: - return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype - def clear_doc(self, docname: str) -> None: if Symbol.debug_show_tree: print("clear_doc:", docname) @@ -3700,9 +3722,6 @@ class CDomain(Domain): print(self.data['root_symbol'].dump(1)) print("\tafter end") print("clear_doc end:", docname) - for fullname, (fn, _id, _l) in list(self.objects.items()): - if fn == docname: - del self.objects[fullname] def process_doc(self, env: BuildEnvironment, docname: str, document: nodes.document) -> None: @@ -3788,8 +3807,18 @@ class CDomain(Domain): return [] def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: - for refname, (docname, node_id, objtype) in list(self.objects.items()): - yield (refname, refname, objtype, docname, node_id, 1) + rootSymbol = self.data['root_symbol'] + for symbol in rootSymbol.get_all_symbols(): + if symbol.declaration is None: + continue + assert symbol.docname + fullNestedName = symbol.get_full_nested_name() + name = str(fullNestedName).lstrip('.') + dispname = fullNestedName.get_display_string().lstrip('.') + objectType = symbol.declaration.objectType + docname = symbol.docname + newestId = symbol.declaration.get_newest_id() + yield (name, dispname, objectType, docname, newestId, 1) def setup(app: Sphinx) -> Dict[str, Any]: diff --git a/sphinx/domains/changeset.py b/sphinx/domains/changeset.py index a07944db8..ba323b729 100644 --- a/sphinx/domains/changeset.py +++ b/sphinx/domains/changeset.py @@ -4,13 +4,12 @@ The changeset domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from collections import namedtuple -from typing import Any, Dict, List -from typing import cast +from typing import Any, Dict, List, cast from docutils import nodes from docutils.nodes import Node @@ -20,7 +19,6 @@ from sphinx.domains import Domain from sphinx.locale import _ from sphinx.util.docutils import SphinxDirective - if False: # For type annotation from sphinx.application import Sphinx diff --git a/sphinx/domains/citation.py b/sphinx/domains/citation.py index 38901867a..1ceb53037 100644 --- a/sphinx/domains/citation.py +++ b/sphinx/domains/citation.py @@ -4,12 +4,11 @@ The citation domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List, Set, Tuple -from typing import cast +from typing import Any, Dict, List, Set, Tuple, cast from docutils import nodes from docutils.nodes import Element diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 7b10f8166..25e6f1421 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -4,14 +4,13 @@ The C++ language domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re -from typing import ( - Any, Callable, Dict, Generator, Iterator, List, Tuple, Type, TypeVar, Union, Optional -) +from typing import (Any, Callable, Dict, Generator, Iterator, List, Optional, Tuple, Type, + TypeVar, Union) from docutils import nodes from docutils.nodes import Element, Node, TextElement, system_message @@ -30,20 +29,17 @@ from sphinx.roles import SphinxRole, XRefRole from sphinx.transforms import SphinxTransform from sphinx.transforms.post_transforms import ReferencesResolver from sphinx.util import logging -from sphinx.util.cfamily import ( - NoOldIdError, ASTBaseBase, ASTAttribute, ASTBaseParenExprList, - verify_description_mode, StringifyTransform, - BaseParser, DefinitionError, UnsupportedMultiCharacterCharLiteral, - identifier_re, anon_identifier_re, integer_literal_re, octal_literal_re, - hex_literal_re, binary_literal_re, integers_literal_suffix_re, - float_literal_re, float_literal_suffix_re, - char_literal_re -) +from sphinx.util.cfamily import (ASTAttribute, ASTBaseBase, ASTBaseParenExprList, BaseParser, + DefinitionError, NoOldIdError, StringifyTransform, + UnsupportedMultiCharacterCharLiteral, anon_identifier_re, + binary_literal_re, char_literal_re, float_literal_re, + float_literal_suffix_re, hex_literal_re, identifier_re, + integer_literal_re, integers_literal_suffix_re, + octal_literal_re, verify_description_mode) from sphinx.util.docfields import Field, GroupedField from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_refnode - logger = logging.getLogger(__name__) T = TypeVar('T') @@ -1596,6 +1592,15 @@ class ASTOperator(ASTBase): identifier = str(self) if mode == 'lastIsName': signode += addnodes.desc_name(identifier, identifier) + elif mode == 'markType': + targetText = prefix + identifier + templateArgs + pnode = addnodes.pending_xref('', refdomain='cpp', + reftype='identifier', + reftarget=targetText, modname=None, + classname=None) + pnode['cpp:parent_key'] = symbol.get_lookup_key() + pnode += nodes.Text(identifier) + signode += pnode else: signode += addnodes.desc_addname(identifier, identifier) @@ -1836,7 +1841,7 @@ class ASTFunctionParameter(ASTBase): # this is not part of the normal name mangling in C++ if symbol: # the anchor will be our parent - return symbol.parent.declaration.get_id(version, prefixed=None) + return symbol.parent.declaration.get_id(version, prefixed=False) # else, do the usual if self.ellipsis: return 'z' @@ -3798,7 +3803,7 @@ class Symbol: assert False # shouldn't happen else: # the domain base class makes a copy of the initial data, which is fine - return Symbol(None, None, None, None, None, None) + return Symbol(None, None, None, None, None, None, None) @staticmethod def debug_print(*args: Any) -> None: @@ -3825,7 +3830,8 @@ class Symbol: def __init__(self, parent: "Symbol", identOrOp: Union[ASTIdentifier, ASTOperator], templateParams: Union[ASTTemplateParams, ASTTemplateIntroduction], - templateArgs: Any, declaration: ASTDeclaration, docname: str) -> None: + templateArgs: Any, declaration: ASTDeclaration, + docname: str, line: int) -> None: self.parent = parent # declarations in a single directive are linked together self.siblingAbove = None # type: Symbol @@ -3835,6 +3841,7 @@ class Symbol: self.templateArgs = templateArgs # identifier self.declaration = declaration self.docname = docname + self.line = line self.isRedeclaration = False self._assert_invariants() @@ -3850,15 +3857,18 @@ class Symbol: # Do symbol addition after self._children has been initialised. self._add_template_and_function_params() - def _fill_empty(self, declaration: ASTDeclaration, docname: str) -> None: + def _fill_empty(self, declaration: ASTDeclaration, docname: str, line: int) -> None: self._assert_invariants() - assert not self.declaration - assert not self.docname - assert declaration - assert docname + assert self.declaration is None + assert self.docname is None + assert self.line is None + assert declaration is not None + assert docname is not None + assert line is not None self.declaration = declaration self.declaration.symbol = self self.docname = docname + self.line = line self._assert_invariants() # and symbol addition should be done as well self._add_template_and_function_params() @@ -3882,7 +3892,7 @@ class Symbol: decl = None nne = ASTNestedNameElement(tp.get_identifier(), None) nn = ASTNestedName([nne], [False], rooted=False) - self._add_symbols(nn, [], decl, self.docname) + self._add_symbols(nn, [], decl, self.docname, self.line) # add symbols for function parameters, if any if self.declaration is not None and self.declaration.function_params is not None: for fp in self.declaration.function_params: @@ -3895,7 +3905,7 @@ class Symbol: decl = ASTDeclaration('functionParam', None, None, None, None, fp, None) assert not nn.rooted assert len(nn.names) == 1 - self._add_symbols(nn, [], decl, self.docname) + self._add_symbols(nn, [], decl, self.docname, self.line) if Symbol.debug_lookup: Symbol.debug_indent -= 1 @@ -3913,6 +3923,7 @@ class Symbol: if sChild.declaration and sChild.docname == docname: sChild.declaration = None sChild.docname = None + sChild.line = None if sChild.siblingAbove is not None: sChild.siblingAbove.siblingBelow = sChild.siblingBelow if sChild.siblingBelow is not None: @@ -3925,8 +3936,7 @@ class Symbol: def get_all_symbols(self) -> Iterator[Any]: yield self for sChild in self._children: - for s in sChild.get_all_symbols(): - yield s + yield from sChild.get_all_symbols() @property def children_recurse_anon(self) -> Generator["Symbol", None, None]: @@ -4107,7 +4117,7 @@ class Symbol: Symbol.debug_print("self:") print(self.to_string(Symbol.debug_indent + 1), end="") Symbol.debug_print("nestedName: ", nestedName) - Symbol.debug_print("templateDecls: ", templateDecls) + Symbol.debug_print("templateDecls: ", ",".join(str(t) for t in templateDecls)) Symbol.debug_print("strictTemplateParamArgLists:", strictTemplateParamArgLists) Symbol.debug_print("ancestorLookupType:", ancestorLookupType) Symbol.debug_print("templateShorthand: ", templateShorthand) @@ -4223,7 +4233,7 @@ class Symbol: identOrOp, templateParams, templateArgs) def _add_symbols(self, nestedName: ASTNestedName, templateDecls: List[Any], - declaration: ASTDeclaration, docname: str) -> "Symbol": + declaration: ASTDeclaration, docname: str, line: int) -> "Symbol": # Used for adding a whole path of symbols, where the last may or may not # be an actual declaration. @@ -4231,10 +4241,10 @@ class Symbol: Symbol.debug_indent += 1 Symbol.debug_print("_add_symbols:") Symbol.debug_indent += 1 - Symbol.debug_print("tdecls:", templateDecls) - Symbol.debug_print("nn: ", nestedName) - Symbol.debug_print("decl: ", declaration) - Symbol.debug_print("doc: ", docname) + Symbol.debug_print("tdecls:", ",".join(str(t) for t in templateDecls)) + Symbol.debug_print("nn: ", nestedName) + Symbol.debug_print("decl: ", declaration) + Symbol.debug_print("location: {}:{}".format(docname, line)) def onMissingQualifiedSymbol(parentSymbol: "Symbol", identOrOp: Union[ASTIdentifier, ASTOperator], @@ -4251,7 +4261,7 @@ class Symbol: return Symbol(parent=parentSymbol, identOrOp=identOrOp, templateParams=templateParams, templateArgs=templateArgs, declaration=None, - docname=None) + docname=None, line=None) lookupResult = self._symbol_lookup(nestedName, templateDecls, onMissingQualifiedSymbol, @@ -4272,14 +4282,14 @@ class Symbol: Symbol.debug_print("identOrOp: ", lookupResult.identOrOp) Symbol.debug_print("templateArgs: ", lookupResult.templateArgs) Symbol.debug_print("declaration: ", declaration) - Symbol.debug_print("docname: ", docname) + Symbol.debug_print("location: {}:{}".format(docname, line)) Symbol.debug_indent -= 1 symbol = Symbol(parent=lookupResult.parentSymbol, identOrOp=lookupResult.identOrOp, templateParams=lookupResult.templateParams, templateArgs=lookupResult.templateArgs, declaration=declaration, - docname=docname) + docname=docname, line=line) if Symbol.debug_lookup: Symbol.debug_indent -= 2 return symbol @@ -4328,7 +4338,7 @@ class Symbol: templateParams=lookupResult.templateParams, templateArgs=lookupResult.templateArgs, declaration=declaration, - docname=docname) + docname=docname, line=line) if Symbol.debug_lookup: Symbol.debug_print("end: creating candidate symbol") return symbol @@ -4360,6 +4370,11 @@ class Symbol: if Symbol.debug_lookup: Symbol.debug_print("candId:", candId) for symbol in withDecl: + # but all existing must be functions as well, + # otherwise we declare it to be a duplicate + if symbol.declaration.objectType != 'function': + handleDuplicateDeclaration(symbol, candSymbol) + # (not reachable) oldId = symbol.declaration.get_newest_id() if Symbol.debug_lookup: Symbol.debug_print("oldId: ", oldId) @@ -4370,7 +4385,11 @@ class Symbol: # if there is an empty symbol, fill that one if len(noDecl) == 0: if Symbol.debug_lookup: - Symbol.debug_print("no match, no empty, candSybmol is not None?:", candSymbol is not None) # NOQA + Symbol.debug_print("no match, no empty") + if candSymbol is not None: + Symbol.debug_print("result is already created candSymbol") + else: + Symbol.debug_print("result is makeCandSymbol()") Symbol.debug_indent -= 2 if candSymbol is not None: return candSymbol @@ -4391,7 +4410,7 @@ class Symbol: # .. namespace:: Test # .. namespace:: nullptr # .. class:: Test - symbol._fill_empty(declaration, docname) + symbol._fill_empty(declaration, docname, line) return symbol def merge_with(self, other: "Symbol", docnames: List[str], @@ -4470,13 +4489,15 @@ class Symbol: continue if otherChild.declaration and otherChild.docname in docnames: if not ourChild.declaration: - ourChild._fill_empty(otherChild.declaration, otherChild.docname) + ourChild._fill_empty(otherChild.declaration, + otherChild.docname, otherChild.line) elif ourChild.docname != otherChild.docname: name = str(ourChild.declaration) - msg = __("Duplicate C++ declaration, also defined in '%s'.\n" - "Declaration is '%s'.") - msg = msg % (ourChild.docname, name) - logger.warning(msg, location=otherChild.docname) + msg = __("Duplicate C++ declaration, also defined at %s:%s.\n" + "Declaration is '.. cpp:%s:: %s'.") + msg = msg % (ourChild.docname, ourChild.line, + ourChild.declaration.directiveType, name) + logger.warning(msg, location=(otherChild.docname, otherChild.line)) else: # Both have declarations, and in the same docname. # This can apparently happen, it should be safe to @@ -4500,23 +4521,25 @@ class Symbol: else: templateDecls = [] res = self._add_symbols(nestedName, templateDecls, - declaration=None, docname=None) + declaration=None, docname=None, line=None) if Symbol.debug_lookup: Symbol.debug_indent -= 1 return res - def add_declaration(self, declaration: ASTDeclaration, docname: str) -> "Symbol": + def add_declaration(self, declaration: ASTDeclaration, + docname: str, line: int) -> "Symbol": if Symbol.debug_lookup: Symbol.debug_indent += 1 Symbol.debug_print("add_declaration:") - assert declaration - assert docname + assert declaration is not None + assert docname is not None + assert line is not None nestedName = declaration.name if declaration.templatePrefix: templateDecls = declaration.templatePrefix.templates else: templateDecls = [] - res = self._add_symbols(nestedName, templateDecls, declaration, docname) + res = self._add_symbols(nestedName, templateDecls, declaration, docname, line) if Symbol.debug_lookup: Symbol.debug_indent -= 1 return res @@ -4711,7 +4734,8 @@ class Symbol: templateParams=lookupResult.templateParams, templateArgs=lookupResult.templateArgs, declaration=declaration, - docname='fakeDocnameForQuery') + docname='fakeDocnameForQuery', + line=42) queryId = declaration.get_newest_id() for symbol in symbols: if symbol.declaration is None: @@ -6655,7 +6679,7 @@ def _make_phony_error_name() -> ASTNestedName: return ASTNestedName([nne], [False], rooted=False) -class CPPObject(ObjectDescription): +class CPPObject(ObjectDescription[ASTDeclaration]): """Description of a C++ language object.""" doc_field_types = [ @@ -6717,7 +6741,7 @@ class CPPObject(ObjectDescription): Symbol(parent=targetSymbol, identOrOp=symbol.identOrOp, templateParams=None, templateArgs=None, declaration=declClone, - docname=self.env.docname) + docname=self.env.docname, line=self.get_source_info()[1]) def add_target_and_index(self, ast: ASTDeclaration, sig: str, signode: TextElement) -> None: @@ -6814,10 +6838,12 @@ class CPPObject(ObjectDescription): parentSymbol = env.temp_data['cpp:parent_symbol'] parentDecl = parentSymbol.declaration if parentDecl is not None and parentDecl.objectType == 'function': - logger.warning("C++ declarations inside functions are not supported." + - " Parent function is " + - str(parentSymbol.get_full_nested_name()), - location=self.get_source_info()) + msg = "C++ declarations inside functions are not supported." \ + " Parent function: {}\nDirective name: {}\nDirective arg: {}" + logger.warning(msg.format( + str(parentSymbol.get_full_nested_name()), + self.name, self.arguments[0] + ), location=self.get_source_info()) name = _make_phony_error_name() symbol = parentSymbol.add_name(name) env.temp_data['cpp:last_symbol'] = symbol @@ -6829,7 +6855,7 @@ class CPPObject(ObjectDescription): return super().run() def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: - parentSymbol = self.env.temp_data['cpp:parent_symbol'] + parentSymbol = self.env.temp_data['cpp:parent_symbol'] # type: Symbol parser = DefinitionParser(sig, location=signode, config=self.env.config) try: @@ -6845,7 +6871,8 @@ class CPPObject(ObjectDescription): raise ValueError from e try: - symbol = parentSymbol.add_declaration(ast, docname=self.env.docname) + symbol = parentSymbol.add_declaration( + ast, docname=self.env.docname, line=self.get_source_info()[1]) # append the new declaration to the sibling list assert symbol.siblingAbove is None assert symbol.siblingBelow is None @@ -6858,9 +6885,10 @@ class CPPObject(ObjectDescription): # Assume we are actually in the old symbol, # instead of the newly created duplicate. self.env.temp_data['cpp:last_symbol'] = e.symbol - msg = __("Duplicate C++ declaration, also defined in '%s'.\n" - "Declaration is '%s'.") - msg = msg % (e.symbol.docname, sig) + msg = __("Duplicate C++ declaration, also defined at %s:%s.\n" + "Declaration is '.. cpp:%s:: %s'.") + msg = msg % (e.symbol.docname, e.symbol.line, + self.display_object_type, sig) logger.warning(msg, location=signode) if ast.objectType == 'enumerator': @@ -7031,8 +7059,8 @@ class AliasNode(nodes.Element): assert parentKey is not None self.parentKey = parentKey - def copy(self: T) -> T: - return self.__class__(self.sig, env=None, parentKey=self.parentKey) # type: ignore + def copy(self) -> 'AliasNode': + return self.__class__(self.sig, env=None, parentKey=self.parentKey) class AliasTransform(SphinxTransform): @@ -7232,14 +7260,18 @@ class CPPDomain(Domain): name = 'cpp' label = 'C++' object_types = { - 'class': ObjType(_('class'), 'class', 'type', 'identifier'), - 'union': ObjType(_('union'), 'union', 'type', 'identifier'), - 'function': ObjType(_('function'), 'function', 'func', 'type', 'identifier'), - 'member': ObjType(_('member'), 'member', 'var'), - 'type': ObjType(_('type'), 'type', 'identifier'), - 'concept': ObjType(_('concept'), 'concept', 'identifier'), - 'enum': ObjType(_('enum'), 'enum', 'type', 'identifier'), - 'enumerator': ObjType(_('enumerator'), 'enumerator') + 'class': ObjType(_('class'), 'class', 'struct', 'identifier', 'type'), + 'union': ObjType(_('union'), 'union', 'identifier', 'type'), + 'function': ObjType(_('function'), 'func', 'identifier', 'type'), + 'member': ObjType(_('member'), 'member', 'var', 'identifier'), + 'type': ObjType(_('type'), 'identifier', 'type'), + 'concept': ObjType(_('concept'), 'concept', 'identifier'), + 'enum': ObjType(_('enum'), 'enum', 'identifier', 'type'), + 'enumerator': ObjType(_('enumerator'), 'enumerator', 'identifier'), + # generated object types + 'functionParam': ObjType(_('function parameter'), 'identifier', 'member', 'var'), # noqa + 'templateParam': ObjType(_('template parameter'), + 'identifier', 'class', 'struct', 'union', 'member', 'var', 'type'), # noqa } directives = { @@ -7279,7 +7311,7 @@ class CPPDomain(Domain): 'texpr': CPPExprRole(asCode=False) } initial_data = { - 'root_symbol': Symbol(None, None, None, None, None, None), + 'root_symbol': Symbol(None, None, None, None, None, None, None), 'names': {} # full name for indexing -> docname } @@ -7416,30 +7448,19 @@ class CPPDomain(Domain): if typ.startswith('cpp:'): typ = typ[4:] - origTyp = typ - if typ == 'func': - typ = 'function' - if typ == 'struct': - typ = 'class' declTyp = s.declaration.objectType def checkType() -> bool: - if typ == 'any' or typ == 'identifier': + if typ == 'any': return True - if declTyp == 'templateParam': - # TODO: perhaps this should be strengthened one day - return True - if declTyp == 'functionParam': - if typ == 'var' or typ == 'member': - return True objtypes = self.objtypes_for_role(typ) if objtypes: return declTyp in objtypes - print("Type is %s (originally: %s), declType is %s" % (typ, origTyp, declTyp)) + print("Type is %s, declaration type is %s" % (typ, declTyp)) assert False if not checkType(): logger.warning("cpp:%s targets a %s (%s).", - origTyp, s.declaration.objectType, + typ, s.declaration.objectType, s.get_full_nested_name(), location=node) @@ -7469,10 +7490,10 @@ class CPPDomain(Domain): if env.config.add_function_parentheses and typ == 'any': addParen += 1 # and now this stuff for operator() - if (env.config.add_function_parentheses and typ == 'function' and + if (env.config.add_function_parentheses and typ == 'func' and title.endswith('operator()')): addParen += 1 - if ((typ == 'any' or typ == 'function') and + if ((typ == 'any' or typ == 'func') and title.endswith('operator') and displayName.endswith('operator()')): addParen += 1 @@ -7481,7 +7502,7 @@ class CPPDomain(Domain): if env.config.add_function_parentheses: if typ == 'any' and displayName.endswith('()'): addParen += 1 - elif typ == 'function': + elif typ == 'func': if title.endswith('()') and not displayName.endswith('()'): title = title[:-2] else: diff --git a/sphinx/domains/index.py b/sphinx/domains/index.py index 18a256bac..4a91d6ad1 100644 --- a/sphinx/domains/index.py +++ b/sphinx/domains/index.py @@ -4,7 +4,7 @@ The index domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -17,8 +17,7 @@ from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.domains import Domain from sphinx.environment import BuildEnvironment -from sphinx.util import logging -from sphinx.util import split_index_msg +from sphinx.util import logging, split_index_msg from sphinx.util.docutils import ReferenceRole, SphinxDirective from sphinx.util.nodes import process_index_entry diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index dc7d610c2..f612fb914 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -4,12 +4,11 @@ The JavaScript domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, Iterator, List, Tuple -from typing import cast +from typing import Any, Dict, Iterator, List, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node @@ -30,11 +29,10 @@ from sphinx.util.docfields import Field, GroupedField, TypedField from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_id, make_refnode - logger = logging.getLogger(__name__) -class JSObject(ObjectDescription): +class JSObject(ObjectDescription[Tuple[str, str]]): """ Description of a JavaScript object. """ diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py index e77bd0ed7..248a7a2a6 100644 --- a/sphinx/domains/math.py +++ b/sphinx/domains/math.py @@ -4,7 +4,7 @@ The math domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,8 +12,7 @@ import warnings from typing import Any, Dict, Iterable, List, Tuple from docutils import nodes -from docutils.nodes import Element, Node, system_message -from docutils.nodes import make_id +from docutils.nodes import Element, Node, make_id, system_message from sphinx.addnodes import pending_xref from sphinx.deprecation import RemovedInSphinx40Warning @@ -158,8 +157,11 @@ class MathDomain(Domain): targets = [eq for eq in self.equations.values() if eq[0] == docname] return len(targets) + 1 - def has_equations(self) -> bool: - return any(self.data['has_equations'].values()) + def has_equations(self, docname: str = None) -> bool: + if docname: + return self.data['has_equations'].get(docname, False) + else: + return any(self.data['has_equations'].values()) def setup(app: "Sphinx") -> Dict[str, Any]: diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index f4bc58b69..c2af9886f 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -4,7 +4,7 @@ The Python domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,23 +15,23 @@ import sys import typing import warnings from inspect import Parameter -from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Tuple -from typing import cast +from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node from docutils.parsers.rst import directives from sphinx import addnodes -from sphinx.addnodes import pending_xref, desc_signature +from sphinx.addnodes import desc_signature, pending_xref from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.directives import ObjectDescription -from sphinx.domains import Domain, ObjType, Index, IndexEntry +from sphinx.domains import Domain, Index, IndexEntry, ObjType from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ -from sphinx.pycode.ast import ast, parse as ast_parse +from sphinx.pycode.ast import ast +from sphinx.pycode.ast import parse as ast_parse from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.docfields import Field, GroupedField, TypedField @@ -272,6 +272,8 @@ class PyXrefMixin: result = super().make_xref(rolename, domain, target, # type: ignore innernode, contnode, env) result['refspecific'] = True + result['py:module'] = env.ref_context.get('py:module') + result['py:class'] = env.ref_context.get('py:class') if target.startswith(('.', '~')): prefix, result['reftarget'] = target[0], target[1:] if prefix == '.': @@ -332,7 +334,7 @@ class PyTypedField(PyXrefMixin, TypedField): return super().make_xref(rolename, domain, target, innernode, contnode, env) -class PyObject(ObjectDescription): +class PyObject(ObjectDescription[Tuple[str, str]]): """ Description of a general Python object. diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index e25b31936..07bf46b75 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -4,13 +4,12 @@ The reStructuredText domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re -from typing import Any, Dict, Iterator, List, Tuple -from typing import cast +from typing import Any, Dict, Iterator, List, Tuple, cast from docutils.nodes import Element from docutils.parsers.rst import directives @@ -27,13 +26,12 @@ from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.nodes import make_id, make_refnode - logger = logging.getLogger(__name__) dir_sig_re = re.compile(r'\.\. (.+?)::(.*)$') -class ReSTMarkup(ObjectDescription): +class ReSTMarkup(ObjectDescription[str]): """ Description of generic reST markup. """ diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 39f67b54e..352e5c7f3 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -4,7 +4,7 @@ The standard domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,8 +12,7 @@ import re import unicodedata import warnings from copy import copy -from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union -from typing import cast +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast from docutils import nodes from docutils.nodes import Element, Node, system_message @@ -27,7 +26,7 @@ from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.locale import _, __ from sphinx.roles import XRefRole -from sphinx.util import ws_re, logging, docname_join +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 from sphinx.util.typing import RoleFunction @@ -35,6 +34,7 @@ from sphinx.util.typing import RoleFunction if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.environment import BuildEnvironment @@ -43,12 +43,12 @@ logger = logging.getLogger(__name__) # RE for option descriptions -option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=[]+)(=?\s*.*)') +option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)') # RE for grammar tokens -token_re = re.compile(r'`(\w+)`', re.U) +token_re = re.compile(r'`((~?\w*:)?\w+)`', re.U) -class GenericObject(ObjectDescription): +class GenericObject(ObjectDescription[str]): """ A generic x-ref directive registered with Sphinx.add_object_type(). """ @@ -178,7 +178,7 @@ class Target(SphinxDirective): return self.name + '-' + name -class Cmdoption(ObjectDescription): +class Cmdoption(ObjectDescription[str]): """ Description of a command-line option (.. option). """ @@ -197,6 +197,11 @@ class Cmdoption(ObjectDescription): location=signode) continue optname, args = m.groups() + if optname.endswith('[') and args.endswith(']'): + # optional value surrounded by brackets (ex. foo[=bar]) + optname = optname[:-1] + args = '[' + args + if count: signode += addnodes.desc_addname(', ', ', ') signode += addnodes.desc_name(optname, optname) @@ -318,7 +323,7 @@ def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index term['ids'].append(node_id) std = cast(StandardDomain, env.get_domain('std')) - std.note_object('term', termtext, node_id, location=term) + std._note_term(termtext, node_id, location=term) # add an index entry too indexnode = addnodes.index() @@ -459,9 +464,23 @@ def token_xrefs(text: str, productionGroup: str = '') -> List[Node]: if m.start() > pos: txt = text[pos:m.start()] retnodes.append(nodes.Text(txt, txt)) - refnode = pending_xref(m.group(1), reftype='token', refdomain='std', - reftarget=productionGroup + m.group(1)) - refnode += nodes.literal(m.group(1), m.group(1), classes=['xref']) + token = m.group(1) + if ':' in token: + if token[0] == '~': + _, title = token.split(':') + target = token[1:] + elif token[0] == ':': + title = token[1:] + target = title + else: + title = token + target = token + else: + title = token + target = productionGroup + token + refnode = pending_xref(title, reftype='token', refdomain='std', + reftarget=target) + refnode += nodes.literal(token, title, classes=['xref']) retnodes.append(refnode) pos = m.end() if pos < len(text): @@ -610,8 +629,6 @@ class StandardDomain(Domain): dangling_warnings = { 'term': 'term not in glossary: %(target)s', - 'ref': 'undefined label: %(target)s (if the link has no caption ' - 'the label must precede a section header)', 'numref': 'undefined label: %(target)s', 'keyword': 'unknown keyword: %(target)s', 'doc': 'unknown document: %(target)s', @@ -677,6 +694,20 @@ class StandardDomain(Domain): RemovedInSphinx50Warning, stacklevel=2) self.objects[objtype, name] = (docname, labelid) + @property + def _terms(self) -> Dict[str, Tuple[str, str]]: + """.. note:: Will be removed soon. internal use only.""" + return self.data.setdefault('terms', {}) # (name) -> docname, labelid + + def _note_term(self, term: str, labelid: str, location: Any = None) -> None: + """Note a term for cross reference. + + .. note:: Will be removed soon. internal use only. + """ + self.note_object('term', term, labelid, location) + + self._terms[term.lower()] = (self.env.docname, labelid) + @property def progoptions(self) -> Dict[Tuple[str, str], Tuple[str, str]]: return self.data.setdefault('progoptions', {}) # (program, name) -> docname, labelid @@ -697,6 +728,9 @@ class StandardDomain(Domain): for key, (fn, _l) in list(self.objects.items()): if fn == docname: del self.objects[key] + for key, (fn, _l) in list(self._terms.items()): + if fn == docname: + del self._terms[key] for key, (fn, _l, _l) in list(self.labels.items()): if fn == docname: del self.labels[key] @@ -712,6 +746,9 @@ class StandardDomain(Domain): for key, data in otherdata['objects'].items(): if data[0] in docnames: self.objects[key] = data + for key, data in otherdata['terms'].items(): + if data[0] in docnames: + self._terms[key] = data for key, data in otherdata['labels'].items(): if data[0] in docnames: self.labels[key] = data @@ -742,9 +779,11 @@ class StandardDomain(Domain): name, env.doc2path(self.labels[name][0]), location=node) self.anonlabels[name] = docname, labelid - if node.tagname in ('section', 'rubric'): + if node.tagname == 'section': title = cast(nodes.title, node[0]) sectname = clean_astext(title) + elif node.tagname == 'rubric': + sectname = clean_astext(node) elif self.is_enumerable_node(node): sectname = self.get_numfig_title(node) if not sectname: @@ -854,8 +893,9 @@ class StandardDomain(Domain): if fignumber is None: return contnode except ValueError: - logger.warning(__("no number is assigned for %s: %s"), figtype, labelid, - location=node) + logger.warning(__("Failed to create a cross reference. Any number is not " + "assigned: %s"), + labelid, location=node) return contnode try: @@ -947,19 +987,12 @@ class StandardDomain(Domain): if result: return result else: - for objtype, term in self.objects: - if objtype == 'term' and term.lower() == target.lower(): - docname, labelid = self.objects[objtype, term] - logger.warning(__('term %s not found in case sensitive match.' - 'made a reference to %s instead.'), - target, term, location=node, type='ref', subtype='term') - break + # fallback to case insentive match + if target.lower() in self._terms: + docname, labelid = self._terms[target.lower()] + return make_refnode(builder, fromdocname, docname, labelid, contnode) else: - docname, labelid = '', '' - if not docname: return None - return make_refnode(builder, fromdocname, docname, - labelid, contnode) def _resolve_obj_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, @@ -1107,12 +1140,27 @@ class StandardDomain(Domain): RemovedInSphinx40Warning, stacklevel=2) +def warn_missing_reference(app: "Sphinx", domain: Domain, node: pending_xref) -> bool: + if (domain and domain.name != 'std') or node['reftype'] != 'ref': + return None + else: + target = node['reftarget'] + if target not in domain.anonlabels: # type: ignore + msg = __('undefined label: %s') + else: + msg = __('Failed to create a cross reference. A title or caption not found: %s') + + logger.warning(msg % target, location=node, type='ref', subtype=node['reftype']) + return True + + def setup(app: "Sphinx") -> Dict[str, Any]: app.add_domain(StandardDomain) + app.connect('warn-missing-reference', warn_missing_reference) return { 'version': 'builtin', - 'env_version': 1, + 'env_version': 2, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 8ce9fa365..af3e2b8d5 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -4,18 +4,18 @@ Global creation environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import pickle +import posixpath import warnings from collections import defaultdict from copy import copy from os import path -from typing import Any, Callable, Dict, Generator, Iterator, List, Set, Tuple, Union -from typing import cast +from typing import Any, Callable, Dict, Generator, Iterator, List, Set, Tuple, Union, cast from docutils import nodes from docutils.nodes import Node @@ -25,13 +25,12 @@ from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.domains import Domain from sphinx.environment.adapters.toctree import TocTree -from sphinx.errors import SphinxError, BuildEnvironmentError, DocumentError, ExtensionError +from sphinx.errors import BuildEnvironmentError, DocumentError, ExtensionError, SphinxError from sphinx.events import EventManager from sphinx.locale import __ from sphinx.project import Project from sphinx.transforms import SphinxTransformer -from sphinx.util import DownloadFiles, FilenameUniqDict -from sphinx.util import logging +from sphinx.util import DownloadFiles, FilenameUniqDict, logging from sphinx.util.docutils import LoggingReporter from sphinx.util.i18n import CatalogRepository, docname_to_domain from sphinx.util.nodes import is_translatable @@ -358,9 +357,9 @@ class BuildEnvironment: docdir = path.dirname(self.doc2path(docname or self.docname, base=None)) rel_fn = path.join(docdir, filename) - # the path.abspath() might seem redundant, but otherwise artifacts - # such as ".." will remain in the path - return rel_fn, path.abspath(path.join(self.srcdir, rel_fn)) + + return (posixpath.normpath(rel_fn), + path.normpath(path.join(self.srcdir, rel_fn))) @property def found_docs(self) -> Set[str]: diff --git a/sphinx/environment/adapters/__init__.py b/sphinx/environment/adapters/__init__.py index c9cde6364..1e763cb01 100644 --- a/sphinx/environment/adapters/__init__.py +++ b/sphinx/environment/adapters/__init__.py @@ -4,6 +4,6 @@ Sphinx environment adapters - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/environment/adapters/asset.py b/sphinx/environment/adapters/asset.py index 38e4eb064..f16a5f7d3 100644 --- a/sphinx/environment/adapters/asset.py +++ b/sphinx/environment/adapters/asset.py @@ -4,7 +4,7 @@ Assets adapter for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/environment/adapters/indexentries.py b/sphinx/environment/adapters/indexentries.py index b13ac97c3..e662bfe4a 100644 --- a/sphinx/environment/adapters/indexentries.py +++ b/sphinx/environment/adapters/indexentries.py @@ -4,23 +4,21 @@ Index entries adapters for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re import unicodedata from itertools import groupby -from typing import Any, Dict, Pattern, List, Tuple -from typing import cast +from typing import Any, Dict, List, Pattern, Tuple, cast from sphinx.builders import Builder from sphinx.domains.index import IndexDomain from sphinx.environment import BuildEnvironment from sphinx.errors import NoUri from sphinx.locale import _, __ -from sphinx.util import split_into, logging - +from sphinx.util import logging, split_into logger = logging.getLogger(__name__) diff --git a/sphinx/environment/adapters/toctree.py b/sphinx/environment/adapters/toctree.py index 9a1ef73d4..93555d172 100644 --- a/sphinx/environment/adapters/toctree.py +++ b/sphinx/environment/adapters/toctree.py @@ -4,19 +4,18 @@ Toctree adapter for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Iterable, List -from typing import cast +from typing import Any, Iterable, List, cast from docutils import nodes from docutils.nodes import Element, Node from sphinx import addnodes from sphinx.locale import __ -from sphinx.util import url_re, logging +from sphinx.util import logging, url_re from sphinx.util.matching import Matcher from sphinx.util.nodes import clean_astext, process_only_nodes @@ -321,8 +320,10 @@ class TocTree: toctrees = [] # type: List[Element] if 'includehidden' not in kwargs: kwargs['includehidden'] = True - if 'maxdepth' not in kwargs: + if 'maxdepth' not in kwargs or not kwargs['maxdepth']: kwargs['maxdepth'] = 0 + else: + kwargs['maxdepth'] = int(kwargs['maxdepth']) kwargs['collapse'] = collapse for toctreenode in doctree.traverse(addnodes.toctree): toctree = self.resolve(docname, builder, toctreenode, prune=True, **kwargs) diff --git a/sphinx/environment/collectors/__init__.py b/sphinx/environment/collectors/__init__.py index 53e3a1c72..4b7ac3472 100644 --- a/sphinx/environment/collectors/__init__.py +++ b/sphinx/environment/collectors/__init__.py @@ -4,7 +4,7 @@ The data collector components for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/environment/collectors/asset.py b/sphinx/environment/collectors/asset.py index 3da2a6e4b..b8d7ae4c0 100644 --- a/sphinx/environment/collectors/asset.py +++ b/sphinx/environment/collectors/asset.py @@ -4,7 +4,7 @@ The image collector for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -26,7 +26,6 @@ from sphinx.util import logging from sphinx.util.i18n import get_image_filename_for_language, search_image_for_language from sphinx.util.images import guess_mimetype - logger = logging.getLogger(__name__) diff --git a/sphinx/environment/collectors/dependencies.py b/sphinx/environment/collectors/dependencies.py index cc0af037e..cd8de918f 100644 --- a/sphinx/environment/collectors/dependencies.py +++ b/sphinx/environment/collectors/dependencies.py @@ -4,7 +4,7 @@ The dependencies collector components for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/environment/collectors/indexentries.py b/sphinx/environment/collectors/indexentries.py index a4c0450d2..351cdc2de 100644 --- a/sphinx/environment/collectors/indexentries.py +++ b/sphinx/environment/collectors/indexentries.py @@ -4,7 +4,7 @@ Index entries collector for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,8 +18,7 @@ from sphinx.application import Sphinx from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.environment import BuildEnvironment from sphinx.environment.collectors import EnvironmentCollector -from sphinx.util import split_index_msg, logging - +from sphinx.util import logging, split_index_msg logger = logging.getLogger(__name__) diff --git a/sphinx/environment/collectors/metadata.py b/sphinx/environment/collectors/metadata.py index bcff1f273..7b3628c9a 100644 --- a/sphinx/environment/collectors/metadata.py +++ b/sphinx/environment/collectors/metadata.py @@ -4,12 +4,11 @@ The metadata collector components for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List, Set -from typing import cast +from typing import Any, Dict, List, Set, cast from docutils import nodes diff --git a/sphinx/environment/collectors/title.py b/sphinx/environment/collectors/title.py index 10cd6c5b0..5225155e8 100644 --- a/sphinx/environment/collectors/title.py +++ b/sphinx/environment/collectors/title.py @@ -4,7 +4,7 @@ The title collector components for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py index d6cdc8354..772451c56 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -4,12 +4,11 @@ Toctree collector for sphinx.environment. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List, Set, Tuple, TypeVar -from typing import cast +from typing import Any, Dict, List, Set, Tuple, TypeVar, cast from docutils import nodes from docutils.nodes import Element, Node @@ -21,7 +20,7 @@ from sphinx.environment.adapters.toctree import TocTree from sphinx.environment.collectors import EnvironmentCollector from sphinx.locale import __ from sphinx.transforms import SphinxContentsFilter -from sphinx.util import url_re, logging +from sphinx.util import logging, url_re if False: # For type annotation diff --git a/sphinx/errors.py b/sphinx/errors.py index d9a83712c..c632d8dbc 100644 --- a/sphinx/errors.py +++ b/sphinx/errors.py @@ -5,7 +5,7 @@ Contains SphinxError and a few subclasses (in an extra module to avoid circular import problems). - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/events.py b/sphinx/events.py index 82a52d762..806a6e55d 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -6,7 +6,7 @@ Gracefully adapted from the TextPress system by Armin. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -23,6 +23,7 @@ from sphinx.util import logging if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx @@ -46,10 +47,9 @@ core_events = { 'doctree-read': 'the doctree before being pickled', 'env-merge-info': 'env, read docnames, other env instance', 'missing-reference': 'env, node, contnode', + 'warn-missing-reference': 'domain, node', 'doctree-resolved': 'doctree, docname', 'env-updated': 'env', - 'html-collect-pages': 'builder', - 'html-page-context': 'pagename, context, doctree or None', 'build-finished': 'exception', } diff --git a/sphinx/ext/__init__.py b/sphinx/ext/__init__.py index 4f4fd0127..80c185741 100644 --- a/sphinx/ext/__init__.py +++ b/sphinx/ext/__init__.py @@ -4,6 +4,6 @@ Contains Sphinx features not activated by default. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/ext/apidoc.py b/sphinx/ext/apidoc.py index b01604617..ebbdadbe8 100644 --- a/sphinx/ext/apidoc.py +++ b/sphinx/ext/apidoc.py @@ -10,7 +10,7 @@ Copyright 2008 Société des arts technologiques (SAT), https://sat.qc.ca/ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -24,7 +24,7 @@ from copy import copy from fnmatch import fnmatch from importlib.machinery import EXTENSION_SUFFIXES from os import path -from typing import Any, List, Tuple +from typing import Any, Generator, List, Tuple import sphinx.locale from sphinx import __display_version__, package_dir @@ -264,14 +264,46 @@ def is_skipped_module(filename: str, opts: Any, excludes: List[str]) -> bool: return False +def walk(rootpath: str, excludes: List[str], opts: Any + ) -> Generator[Tuple[str, List[str], List[str]], None, None]: + """Walk through the directory and list files and subdirectories up.""" + followlinks = getattr(opts, 'followlinks', False) + includeprivate = getattr(opts, 'includeprivate', False) + + for root, subs, files in os.walk(rootpath, followlinks=followlinks): + # document only Python module files (that aren't excluded) + files = sorted(f for f in files + if f.endswith(PY_SUFFIXES) and + not is_excluded(path.join(root, f), excludes)) + + # remove hidden ('.') and private ('_') directories, as well as + # excluded dirs + if includeprivate: + exclude_prefixes = ('.',) # type: Tuple[str, ...] + else: + exclude_prefixes = ('.', '_') + + subs[:] = sorted(sub for sub in subs if not sub.startswith(exclude_prefixes) and + not is_excluded(path.join(root, sub), excludes)) + + yield root, subs, files + + +def has_child_module(rootpath: str, excludes: List[str], opts: Any) -> bool: + """Check the given directory contains child modules at least one.""" + for root, subs, files in walk(rootpath, excludes, opts): + if files: + return True + + return False + + def recurse_tree(rootpath: str, excludes: List[str], opts: Any, user_template_dir: str = None) -> List[str]: """ Look for every file in the directory tree and create the corresponding ReST files. """ - followlinks = getattr(opts, 'followlinks', False) - includeprivate = getattr(opts, 'includeprivate', False) implicit_namespaces = getattr(opts, 'implicit_namespaces', False) # check if the base directory is a package and get its name @@ -282,48 +314,36 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any, root_package = None toplevels = [] - for root, subs, files in os.walk(rootpath, followlinks=followlinks): - # document only Python module files (that aren't excluded) - py_files = sorted(f for f in files - if f.endswith(PY_SUFFIXES) and - not is_excluded(path.join(root, f), excludes)) - is_pkg = is_packagedir(None, py_files) + for root, subs, files in walk(rootpath, excludes, opts): + is_pkg = is_packagedir(None, files) is_namespace = not is_pkg and implicit_namespaces if is_pkg: - for f in py_files[:]: + for f in files[:]: if is_initpy(f): - py_files.remove(f) - py_files.insert(0, f) + files.remove(f) + files.insert(0, f) elif root != rootpath: # only accept non-package at toplevel unless using implicit namespaces if not implicit_namespaces: del subs[:] continue - # remove hidden ('.') and private ('_') directories, as well as - # excluded dirs - if includeprivate: - exclude_prefixes = ('.',) # type: Tuple[str, ...] - else: - exclude_prefixes = ('.', '_') - subs[:] = sorted(sub for sub in subs if not sub.startswith(exclude_prefixes) and - not is_excluded(path.join(root, sub), excludes)) if is_pkg or is_namespace: # we are in a package with something to document - if subs or len(py_files) > 1 or not is_skipped_package(root, opts): + if subs or len(files) > 1 or not is_skipped_package(root, opts): subpackage = root[len(rootpath):].lstrip(path.sep).\ replace(path.sep, '.') # if this is not a namespace or # a namespace and there is something there to document - if not is_namespace or len(py_files) > 0: + if not is_namespace or has_child_module(root, excludes, opts): create_package_file(root, root_package, subpackage, - py_files, opts, subs, is_namespace, excludes, + files, opts, subs, is_namespace, excludes, user_template_dir) toplevels.append(module_join(root_package, subpackage)) else: # if we are at the root level, we don't require it to be a package assert root == rootpath and root_package is None - for py_file in py_files: + for py_file in files: if not is_skipped_module(path.join(rootpath, py_file), opts, excludes): module = py_file.split('.')[0] create_module_file(root_package, module, opts, user_template_dir) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index b68fc73c9..1490d1551 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -6,42 +6,41 @@ the doctree, thus avoiding duplication between docstrings and documentation for those who like elaborate docstrings. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -import importlib import re import warnings from inspect import Parameter, Signature from types import ModuleType -from typing import ( - Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union -) -from typing import get_type_hints +from typing import (Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, + TypeVar, Union) from docutils.statemachine import StringList import sphinx from sphinx.application import Sphinx -from sphinx.config import Config, ENUM -from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning +from sphinx.config import ENUM, Config +from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning, + RemovedInSphinx60Warning) from sphinx.environment import BuildEnvironment -from sphinx.ext.autodoc.importer import import_object, get_module_members, get_object_members -from sphinx.ext.autodoc.mock import mock +from sphinx.ext.autodoc.importer import (get_class_members, get_object_members, import_module, + import_object) +from sphinx.ext.autodoc.mock import ismock, mock, undecorate from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer, PycodeError -from sphinx.util import inspect -from sphinx.util import logging +from sphinx.util import inspect, logging from sphinx.util.docstrings import extract_metadata, prepare_docstring -from sphinx.util.inspect import ( - evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature -) +from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr, + stringify_signature) +from sphinx.util.typing import get_type_hints, restify from sphinx.util.typing import stringify as stringify_typehint if False: # For type annotation from typing import Type # NOQA # for python3.5.1 + from sphinx.ext.autodoc.directive import DocumenterBridge @@ -92,7 +91,7 @@ SLOTSATTR = object() def members_option(arg: Any) -> Union[object, List[str]]: """Used to convert the :members: option to auto directives.""" - if arg is None or arg is True: + if arg in (None, True): return ALL elif arg is False: return None @@ -111,14 +110,14 @@ def members_set_option(arg: Any) -> Union[object, Set[str]]: def exclude_members_option(arg: Any) -> Union[object, Set[str]]: """Used to convert the :exclude-members: option.""" - if arg is None: + if arg in (None, True): return EMPTY return {x.strip() for x in arg.split(',') if x.strip()} def inherited_members_option(arg: Any) -> Union[object, Set[str]]: """Used to convert the :members: option to auto directives.""" - if arg is None: + if arg in (None, True): return 'object' else: return arg @@ -126,7 +125,7 @@ def inherited_members_option(arg: Any) -> Union[object, Set[str]]: def member_order_option(arg: Any) -> Optional[str]: """Used to convert the :members: option to auto directives.""" - if arg is None: + if arg in (None, True): return None elif arg in ('alphabetical', 'bysource', 'groupwise'): return arg @@ -138,7 +137,7 @@ SUPPRESS = object() def annotation_option(arg: Any) -> Any: - if arg is None: + if arg in (None, True): # suppress showing the representation of the object return SUPPRESS else: @@ -258,6 +257,35 @@ class Options(dict): return None +class ObjectMember(tuple): + """A member of object. + + This is used for the result of `Documenter.get_object_members()` to + represent each member of the object. + + .. Note:: + + An instance of this class behaves as a tuple of (name, object) + for compatibility to old Sphinx. The behavior will be dropped + in the future. Therefore extensions should not use the tuple + interface. + """ + + def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any: + return super().__new__(cls, (name, obj)) # type: ignore + + def __init__(self, name: str, obj: Any, docstring: Optional[str] = None, + class_: Any = None, skipped: bool = False) -> None: + self.__name__ = name + self.object = obj + self.docstring = docstring + self.skipped = skipped + self.class_ = class_ + + +ObjectMembers = Union[List[ObjectMember], List[Tuple[str, Any]]] + + class Documenter: """ A Documenter knows how to autodocument a single object type. When @@ -299,6 +327,7 @@ class Documenter: def __init__(self, directive: "DocumenterBridge", name: str, indent: str = '') -> None: self.directive = directive + self.config = directive.env.config self.env = directive.env # type: BuildEnvironment self.options = directive.genopt self.name = name @@ -369,7 +398,7 @@ class Documenter: modname = None parents = [] - with mock(self.env.config.autodoc_mock_imports): + with mock(self.config.autodoc_mock_imports): self.modname, self.objpath = self.resolve_name(modname, parents, path, base) if not self.modname: @@ -387,12 +416,14 @@ class Documenter: Returns True if successful, False if an error occurred. """ - with mock(self.env.config.autodoc_mock_imports): + with mock(self.config.autodoc_mock_imports): try: ret = import_object(self.modname, self.objpath, self.objtype, attrgetter=self.get_attr, - warningiserror=self.env.config.autodoc_warningiserror) + warningiserror=self.config.autodoc_warningiserror) self.module, self.parent, self.object_name, self.object = ret + if ismock(self.object): + self.object = undecorate(self.object) return True except ImportError as exc: if raiseerror: @@ -509,8 +540,12 @@ class Documenter: # etc. don't support a prepended module name self.add_line(' :module: %s' % self.modname, sourcename) - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: - """Decode and return lines of the docstring(s) for the object.""" + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + """Decode and return lines of the docstring(s) for the object. + + When it returns None value, autodoc-process-docstring will not be called for this + object. + """ if encoding is not None: warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." % self.__class__.__name__, @@ -519,8 +554,7 @@ class Documenter: warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." % self.__class__.__name__, RemovedInSphinx50Warning, stacklevel=2) - docstring = getdoc(self.object, self.get_attr, - self.env.config.autodoc_inherit_docstrings, + docstring = getdoc(self.object, self.get_attr, self.config.autodoc_inherit_docstrings, self.parent, self.object_name) if docstring: tab_width = self.directive.state.document.settings.tab_width @@ -543,12 +577,27 @@ class Documenter: yield from docstringlines def get_sourcename(self) -> str: - if self.analyzer: - return '%s:docstring of %s' % (self.analyzer.srcname, self.fullname) - return 'docstring of %s' % self.fullname + if (getattr(self.object, '__module__', None) and + getattr(self.object, '__qualname__', None)): + # Get the correct location of docstring from self.object + # to support inherited methods + fullname = '%s.%s' % (self.object.__module__, self.object.__qualname__) + else: + fullname = self.fullname - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: + if self.analyzer: + return '%s:docstring of %s' % (self.analyzer.srcname, fullname) + else: + return 'docstring of %s' % fullname + + def add_content(self, more_content: Optional[StringList], no_docstring: bool = False + ) -> None: """Add content from docstrings, attribute documentation and user.""" + if no_docstring: + warnings.warn("The 'no_docstring' argument to %s.add_content() is deprecated." + % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() if self.analyzer: @@ -567,33 +616,39 @@ class Documenter: # add content from docstrings if not no_docstring: docstrings = self.get_doc() - if not docstrings: - # append at least a dummy docstring, so that the event - # autodoc-process-docstring is fired and can add some - # content if desired - docstrings.append([]) - for i, line in enumerate(self.process_doc(docstrings)): - self.add_line(line, sourcename, i) + if docstrings is None: + # Do not call autodoc-process-docstring on get_doc() returns None. + pass + else: + if not docstrings: + # append at least a dummy docstring, so that the event + # autodoc-process-docstring is fired and can add some + # content if desired + docstrings.append([]) + for i, line in enumerate(self.process_doc(docstrings)): + self.add_line(line, sourcename, i) # add additional content (e.g. from document), if present if more_content: for line, src in zip(more_content.data, more_content.items): self.add_line(line, src[0], src[1]) - def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]: + def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: """Return `(members_check_module, members)` where `members` is a list of `(membername, member)` pairs of the members of *self.object*. If *want_all* is True, return all members. Else, only return those members given by *self.options.members* (which may also be none). """ + warnings.warn('The implementation of Documenter.get_object_members() will be ' + 'removed from Sphinx-6.0.', RemovedInSphinx60Warning) members = get_object_members(self.object, self.objpath, self.get_attr, self.analyzer) if not want_all: if not self.options.members: - return False, [] + return False, [] # type: ignore # specific members given selected = [] - for name in self.options.members: + for name in self.options.members: # type: str if name in members: selected.append((name, members[name].value)) else: @@ -606,7 +661,7 @@ class Documenter: return False, [(m.name, m.value) for m in members.values() if m.directly_defined] - def filter_members(self, members: List[Tuple[str, Any]], want_all: bool + def filter_members(self, members: ObjectMembers, want_all: bool ) -> List[Tuple[str, Any, bool]]: """Filter the given member list. @@ -621,7 +676,7 @@ class Documenter: The user can override the skipping decision by connecting to the ``autodoc-skip-member`` event. """ - def is_filtered_inherited_member(name: str) -> bool: + def is_filtered_inherited_member(name: str, obj: Any) -> bool: if inspect.isclass(self.object): for cls in self.object.__mro__: if cls.__name__ == self.options.inherited_members and cls != self.object: @@ -631,6 +686,8 @@ class Documenter: return False elif name in self.get_attr(cls, '__annotations__', {}): return False + elif isinstance(obj, ObjectMember) and obj.class_ is cls: + return False return False @@ -645,14 +702,15 @@ class Documenter: attr_docs = {} # process members and determine which to skip - for (membername, member) in members: + for obj in members: + membername, member = obj # if isattr is True, the member is documented as an attribute if member is INSTANCEATTR: isattr = True else: isattr = False - doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings, + doc = getdoc(member, self.get_attr, self.config.autodoc_inherit_docstrings, self.parent, self.object_name) if not isinstance(doc, str): # Ignore non-string __doc__ @@ -665,6 +723,11 @@ class Documenter: cls_doc = self.get_attr(cls, '__doc__', None) if cls_doc == doc: doc = None + + if isinstance(obj, ObjectMember) and obj.docstring: + # hack for ClassDocumenter to inject docstring via ObjectMember + doc = obj.docstring + has_doc = bool(doc) metadata = extract_metadata(doc) @@ -678,7 +741,7 @@ class Documenter: isprivate = membername.startswith('_') keep = False - if safe_getattr(member, '__sphinx_mock__', False): + if ismock(member): # mocked module or object pass elif self.options.exclude_members and membername in self.options.exclude_members: @@ -689,7 +752,7 @@ class Documenter: if self.options.special_members and membername in self.options.special_members: if membername == '__doc__': keep = False - elif is_filtered_inherited_member(membername): + elif is_filtered_inherited_member(membername, obj): keep = False else: keep = has_doc or self.options.undoc_members @@ -709,19 +772,24 @@ class Documenter: if has_doc or self.options.undoc_members: if self.options.private_members is None: keep = False - elif is_filtered_inherited_member(membername): + elif is_filtered_inherited_member(membername, obj): keep = False else: keep = membername in self.options.private_members else: keep = False else: - if self.options.members is ALL and is_filtered_inherited_member(membername): + if (self.options.members is ALL and + is_filtered_inherited_member(membername, obj)): keep = False else: # ignore undocumented members if :undoc-members: is not given keep = has_doc or self.options.undoc_members + if isinstance(obj, ObjectMember) and obj.skipped: + # forcedly skipped member (ex. a module attribute not defined in __all__) + keep = False + # give the user a chance to decide whether this member # should be skipped if self.env.app: @@ -776,7 +844,7 @@ class Documenter: documenter = classes[-1](self.directive, full_mname, self.indent) memberdocumenters.append((documenter, isattr)) - member_order = self.options.member_order or self.env.config.autodoc_member_order + member_order = self.options.member_order or self.config.autodoc_member_order memberdocumenters = self.sort_members(memberdocumenters, member_order) for documenter, isattr in memberdocumenters: @@ -813,7 +881,7 @@ class Documenter: return documenters - def generate(self, more_content: Any = None, real_modname: str = None, + def generate(self, more_content: Optional[StringList] = None, real_modname: str = None, check_module: bool = False, all_members: bool = False) -> None: """Generate reST for the object given by *self.name*, and possibly for its members. @@ -923,7 +991,7 @@ class ModuleDocumenter(Documenter): def __init__(self, *args: Any) -> None: super().__init__(*args) merge_members_option(self.options) - self.__all__ = None + self.__all__ = None # type: Optional[Sequence[str]] @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any @@ -947,26 +1015,20 @@ class ModuleDocumenter(Documenter): return ret def import_object(self, raiseerror: bool = False) -> bool: - def is_valid_module_all(__all__: Any) -> bool: - """Check the given *__all__* is valid for a module.""" - if (isinstance(__all__, (list, tuple)) and - all(isinstance(e, str) for e in __all__)): - return True - else: - return False - ret = super().import_object(raiseerror) - if not self.options.ignore_module_all: - __all__ = getattr(self.object, '__all__', None) - if is_valid_module_all(__all__): - # valid __all__ found. copy it to self.__all__ - self.__all__ = __all__ - elif __all__: - # invalid __all__ found. - logger.warning(__('__all__ should be a list of strings, not %r ' - '(in module %s) -- ignoring __all__') % - (__all__, self.fullname), type='autodoc') + try: + if not self.options.ignore_module_all: + self.__all__ = inspect.getall(self.object) + except AttributeError as exc: + # __all__ raises an error. + logger.warning(__('%s.__all__ raises an error. Ignored: %r'), + (self.fullname, exc), type='autodoc') + except ValueError as exc: + # invalid __all__ found. + logger.warning(__('__all__ should be a list of strings, not %r ' + '(in module %s) -- ignoring __all__') % + (exc.args[0], self.fullname), type='autodoc') return ret @@ -983,28 +1045,61 @@ class ModuleDocumenter(Documenter): if self.options.deprecated: self.add_line(' :deprecated:', sourcename) - def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]: + def get_module_members(self) -> Dict[str, ObjectMember]: + """Get members of target module.""" + if self.analyzer: + attr_docs = self.analyzer.attr_docs + else: + attr_docs = {} + + members = {} # type: Dict[str, ObjectMember] + for name in dir(self.object): + try: + value = safe_getattr(self.object, name, None) + if ismock(value): + value = undecorate(value) + docstring = attr_docs.get(('', name), []) + members[name] = ObjectMember(name, value, docstring="\n".join(docstring)) + except AttributeError: + continue + + # annotation only member (ex. attr: int) + try: + for name in inspect.getannotations(self.object): + if name not in members: + docstring = attr_docs.get(('', name), []) + members[name] = ObjectMember(name, INSTANCEATTR, + docstring="\n".join(docstring)) + except AttributeError: + pass + + return members + + def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: + members = self.get_module_members() if want_all: - if self.__all__: - memberlist = self.__all__ - else: + if self.__all__ is None: # for implicit module members, check __module__ to avoid # documenting imported objects - return True, get_module_members(self.object) + return True, list(members.values()) + else: + for member in members.values(): + if member.__name__ not in self.__all__: + member.skipped = True + + return False, list(members.values()) else: memberlist = self.options.members or [] - ret = [] - for mname in memberlist: - try: - ret.append((mname, safe_getattr(self.object, mname))) - except AttributeError: - logger.warning( - __('missing attribute mentioned in :members: or __all__: ' - 'module %s, attribute %s') % - (safe_getattr(self.object, '__name__', '???'), mname), - type='autodoc' - ) - return False, ret + ret = [] + for name in memberlist: + if name in members: + ret.append(members[name]) + else: + logger.warning(__('missing attribute mentioned in :members: option: ' + 'module %s, attribute %s') % + (safe_getattr(self.object, '__name__', '???'), name), + type='autodoc') + return False, ret def sort_members(self, documenters: List[Tuple["Documenter", bool]], order: str) -> List[Tuple["Documenter", bool]]: @@ -1102,6 +1197,8 @@ class DocstringSignatureMixin: valid_names.extend(cls.__name__ for cls in self.object.__mro__) docstrings = self.get_doc() + if docstrings is None: + return None, None self._new_docstrings = docstrings[:] self._signatures = [] result = None @@ -1152,7 +1249,7 @@ class DocstringSignatureMixin: return result - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: if encoding is not None: warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." % self.__class__.__name__, @@ -1162,7 +1259,7 @@ class DocstringSignatureMixin: return super().get_doc(None, ignore) # type: ignore def format_signature(self, **kwargs: Any) -> str: - if self.args is None and self.env.config.autodoc_docstring_signature: # type: ignore + if self.args is None and self.config.autodoc_docstring_signature: # type: ignore # only act if a signature is not explicitly given already, and if # the feature is enabled result = self._find_signature() @@ -1181,7 +1278,7 @@ class DocstringStripSignatureMixin(DocstringSignatureMixin): feature of stripping any function signature from the docstring. """ def format_signature(self, **kwargs: Any) -> str: - if self.args is None and self.env.config.autodoc_docstring_signature: # type: ignore + if self.args is None and self.config.autodoc_docstring_signature: # type: ignore # only act if a signature is not explicitly given already, and if # the feature is enabled result = self._find_signature() @@ -1208,13 +1305,12 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter))) def format_args(self, **kwargs: Any) -> str: - if self.env.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) try: self.env.app.emit('autodoc-before-process-signature', self.object, False) - sig = inspect.signature(self.object, follow_wrapped=True, - type_aliases=self.env.config.autodoc_type_aliases) + sig = inspect.signature(self.object, type_aliases=self.config.autodoc_type_aliases) args = stringify_signature(sig, **kwargs) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), @@ -1223,7 +1319,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ except ValueError: args = '' - if self.env.config.strip_signature_backslash: + if self.config.strip_signature_backslash: # escape backslashes for reST args = args.replace('\\', '\\\\') return args @@ -1240,7 +1336,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ def format_signature(self, **kwargs: Any) -> str: sigs = [] - if self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads: + if (self.analyzer and + '.'.join(self.objpath) in self.analyzer.overloads and + self.config.autodoc_typehints == 'signature'): # Use signatures for overloaded functions instead of the implementation function. overloaded = True else: @@ -1261,20 +1359,33 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ documenter.objpath = [None] sigs.append(documenter.format_signature()) if overloaded: + actual = inspect.signature(self.object, + type_aliases=self.config.autodoc_type_aliases) __globals__ = safe_getattr(self.object, '__globals__', {}) for overload in self.analyzer.overloads.get('.'.join(self.objpath)): + overload = self.merge_default_value(actual, overload) overload = evaluate_signature(overload, __globals__, - self.env.config.autodoc_type_aliases) + self.config.autodoc_type_aliases) sig = stringify_signature(overload, **kwargs) sigs.append(sig) return "\n".join(sigs) + def merge_default_value(self, actual: Signature, overload: Signature) -> Signature: + """Merge default values of actual implementation to the overload variants.""" + parameters = list(overload.parameters.values()) + for i, param in enumerate(parameters): + actual_param = actual.parameters.get(param.name) + if actual_param and param.default == '...': + parameters[i] = param.replace(default=actual_param.default) + + return overload.replace(parameters=parameters) + def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: """Annotate type hint to the first argument of function if needed.""" try: - sig = inspect.signature(func, type_aliases=self.env.config.autodoc_type_aliases) + sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), self.fullname, exc) @@ -1295,19 +1406,6 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ return -class SingledispatchFunctionDocumenter(FunctionDocumenter): - """ - Used to be a specialized Documenter subclass for singledispatch'ed functions. - - Retained for backwards compatibility, now does the same as the FunctionDocumenter - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn("%s is deprecated." % self.__class__.__name__, - RemovedInSphinx50Warning, stacklevel=2) - super().__init__(*args, **kwargs) - - class DecoratorDocumenter(FunctionDocumenter): """ Specialized Documenter subclass for decorator functions. @@ -1389,7 +1487,12 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # This sequence is copied from inspect._signature_from_callable. # ValueError means that no signature could be found, so we keep going. - # First, let's see if it has an overloaded __call__ defined + # First, we check the obj has a __signature__ attribute + if (hasattr(self.object, '__signature__') and + isinstance(self.object.__signature__, Signature)): + return None, None, self.object.__signature__ + + # Next, let's see if it has an overloaded __call__ defined # in its metaclass call = get_user_defined_function_or_method(type(self.object), '__call__') @@ -1401,7 +1504,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: self.env.app.emit('autodoc-before-process-signature', call, True) try: sig = inspect.signature(call, bound_method=True, - type_aliases=self.env.config.autodoc_type_aliases) + type_aliases=self.config.autodoc_type_aliases) return type(self.object), '__call__', sig except ValueError: pass @@ -1417,7 +1520,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: self.env.app.emit('autodoc-before-process-signature', new, True) try: sig = inspect.signature(new, bound_method=True, - type_aliases=self.env.config.autodoc_type_aliases) + type_aliases=self.config.autodoc_type_aliases) return self.object, '__new__', sig except ValueError: pass @@ -1428,7 +1531,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: self.env.app.emit('autodoc-before-process-signature', init, True) try: sig = inspect.signature(init, bound_method=True, - type_aliases=self.env.config.autodoc_type_aliases) + type_aliases=self.config.autodoc_type_aliases) return self.object, '__init__', sig except ValueError: pass @@ -1440,7 +1543,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: self.env.app.emit('autodoc-before-process-signature', self.object, False) try: sig = inspect.signature(self.object, bound_method=False, - type_aliases=self.env.config.autodoc_type_aliases) + type_aliases=self.config.autodoc_type_aliases) return None, None, sig except ValueError: pass @@ -1450,7 +1553,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: return None, None, None def format_args(self, **kwargs: Any) -> str: - if self.env.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) try: @@ -1474,13 +1577,13 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: sigs = [] overloads = self.get_overloaded_signatures() - if overloads: + if overloads and self.config.autodoc_typehints == 'signature': # Use signatures for overloaded methods instead of the implementation method. method = safe_getattr(self._signature_class, self._signature_method_name, None) __globals__ = safe_getattr(method, '__globals__', {}) for overload in overloads: overload = evaluate_signature(overload, __globals__, - self.env.config.autodoc_type_aliases) + self.config.autodoc_type_aliases) parameters = list(overload.parameters.values()) overload = overload.replace(parameters=parameters[1:], @@ -1497,10 +1600,13 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: for cls in self._signature_class.__mro__: try: analyzer = ModuleAnalyzer.for_module(cls.__module__) - analyzer.parse() + analyzer.analyze() qualname = '.'.join([cls.__qualname__, self._signature_method_name]) if qualname in analyzer.overloads: return analyzer.overloads.get(qualname) + elif qualname in analyzer.tagorder: + # the constructor is defined in the class, but not overrided. + return [] except PycodeError: pass @@ -1520,24 +1626,50 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if not self.doc_as_attr and self.options.show_inheritance: sourcename = self.get_sourcename() self.add_line('', sourcename) - if hasattr(self.object, '__bases__') and len(self.object.__bases__): - bases = [':class:`%s`' % b.__name__ - if b.__module__ in ('__builtin__', 'builtins') - else ':class:`%s.%s`' % (b.__module__, b.__qualname__) - for b in self.object.__bases__] - self.add_line(' ' + _('Bases: %s') % ', '.join(bases), - sourcename) - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: + if hasattr(self.object, '__orig_bases__') and len(self.object.__orig_bases__): + # A subclass of generic types + # refs: PEP-560 + bases = [restify(cls) for cls in self.object.__orig_bases__] + self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename) + elif hasattr(self.object, '__bases__') and len(self.object.__bases__): + # A normal class + bases = [restify(cls) for cls in self.object.__bases__] + self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename) + + def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: + members = get_class_members(self.object, self.objpath, self.get_attr) + if not want_all: + if not self.options.members: + return False, [] # type: ignore + # specific members given + selected = [] + for name in self.options.members: # type: str + if name in members: + selected.append(members[name]) + else: + logger.warning(__('missing attribute %s in object %s') % + (name, self.fullname), type='autodoc') + return False, selected + elif self.options.inherited_members: + return False, list(members.values()) + else: + return False, [m for m in members.values() if m.class_ == self.object] + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: if encoding is not None: warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." % self.__class__.__name__, RemovedInSphinx40Warning, stacklevel=2) + if self.doc_as_attr: + # Don't show the docstring of the class when it is an alias. + return None + lines = getattr(self, '_new_docstrings', None) if lines is not None: return lines - content = self.env.config.autoclass_content + content = self.config.autoclass_content docstrings = [] attrdocstring = self.get_attr(self.object, '__doc__', None) @@ -1549,7 +1681,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if content in ('both', 'init'): __init__ = self.get_attr(self.object, '__init__', None) initdocstring = getdoc(__init__, self.get_attr, - self.env.config.autodoc_inherit_docstrings, + self.config.autodoc_inherit_docstrings, self.parent, self.object_name) # for new-style classes, no __init__ means default __init__ if (initdocstring is not None and @@ -1560,7 +1692,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # try __new__ __new__ = self.get_attr(self.object, '__new__', None) initdocstring = getdoc(__new__, self.get_attr, - self.env.config.autodoc_inherit_docstrings, + self.config.autodoc_inherit_docstrings, self.parent, self.object_name) # for new-style classes, no __new__ means default __new__ if (initdocstring is not None and @@ -1576,27 +1708,22 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: tab_width = self.directive.state.document.settings.tab_width return [prepare_docstring(docstring, ignore, tab_width) for docstring in docstrings] - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: + def add_content(self, more_content: Optional[StringList], no_docstring: bool = False + ) -> None: if self.doc_as_attr: - classname = safe_getattr(self.object, '__qualname__', None) - if not classname: - classname = safe_getattr(self.object, '__name__', None) - if classname: - module = safe_getattr(self.object, '__module__', None) - parentmodule = safe_getattr(self.parent, '__module__', None) - if module and module != parentmodule: - classname = str(module) + '.' + str(classname) - content = StringList([_('alias of :class:`%s`') % classname], source='') - super().add_content(content, no_docstring=True) - else: - super().add_content(more_content) + try: + more_content = StringList([_('alias of %s') % restify(self.object)], source='') + except AttributeError: + pass # Invalid class object is passed. + + super().add_content(more_content) def document_members(self, all_members: bool = False) -> None: if self.doc_as_attr: return super().document_members(all_members) - def generate(self, more_content: Any = None, real_modname: str = None, + def generate(self, more_content: Optional[StringList] = None, real_modname: str = None, check_module: bool = False, all_members: bool = False) -> None: # Do not pass real_modname and use the name from the __module__ # attribute of the class. @@ -1624,7 +1751,149 @@ class ExceptionDocumenter(ClassDocumenter): return isinstance(member, type) and issubclass(member, BaseException) -class DataDocumenter(ModuleLevelDocumenter): +class DataDocumenterMixinBase: + # define types of instance variables + config = None # type: Config + env = None # type: BuildEnvironment + modname = None # type: str + parent = None # type: Any + object = None # type: Any + objpath = None # type: List[str] + + def should_suppress_directive_header(self) -> bool: + """Check directive header should be suppressed.""" + return False + + def should_suppress_value_header(self) -> bool: + """Check :value: header should be suppressed.""" + return False + + def update_content(self, more_content: StringList) -> None: + """Update docstring for the NewType object.""" + pass + + +class GenericAliasMixin(DataDocumenterMixinBase): + """ + Mixin for DataDocumenter and AttributeDocumenter to provide the feature for + supporting GenericAliases. + """ + + def should_suppress_directive_header(self) -> bool: + return (inspect.isgenericalias(self.object) or + super().should_suppress_directive_header()) + + def update_content(self, more_content: StringList) -> None: + if inspect.isgenericalias(self.object): + alias = stringify_typehint(self.object) + more_content.append(_('alias of %s') % alias, '') + more_content.append('', '') + + super().update_content(more_content) + + +class NewTypeMixin(DataDocumenterMixinBase): + """ + Mixin for DataDocumenter and AttributeDocumenter to provide the feature for + supporting NewTypes. + """ + + def should_suppress_directive_header(self) -> bool: + return (inspect.isNewType(self.object) or + super().should_suppress_directive_header()) + + def update_content(self, more_content: StringList) -> None: + if inspect.isNewType(self.object): + supertype = restify(self.object.__supertype__) + more_content.append(_('alias of %s') % supertype, '') + more_content.append('', '') + + super().update_content(more_content) + + +class TypeVarMixin(DataDocumenterMixinBase): + """ + Mixin for DataDocumenter and AttributeDocumenter to provide the feature for + supporting TypeVars. + """ + + def should_suppress_directive_header(self) -> bool: + return (isinstance(self.object, TypeVar) or + super().should_suppress_directive_header()) + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if ignore is not None: + warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." + % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + + if isinstance(self.object, TypeVar): + if self.object.__doc__ != TypeVar.__doc__: + return super().get_doc() # type: ignore + else: + return [] + else: + return super().get_doc() # type: ignore + + def update_content(self, more_content: StringList) -> None: + if isinstance(self.object, TypeVar): + attrs = [repr(self.object.__name__)] + for constraint in self.object.__constraints__: + attrs.append(stringify_typehint(constraint)) + if self.object.__covariant__: + attrs.append("covariant=True") + if self.object.__contravariant__: + attrs.append("contravariant=True") + + more_content.append(_('alias of TypeVar(%s)') % ", ".join(attrs), '') + more_content.append('', '') + + super().update_content(more_content) + + +class UninitializedGlobalVariableMixin(DataDocumenterMixinBase): + """ + Mixin for DataDocumenter to provide the feature for supporting uninitialized + (type annotation only) global variables. + """ + + def import_object(self, raiseerror: bool = False) -> bool: + try: + return super().import_object(raiseerror=True) # type: ignore + except ImportError as exc: + # annotation only instance variable (PEP-526) + try: + with mock(self.config.autodoc_mock_imports): + parent = import_module(self.modname, self.config.autodoc_warningiserror) + annotations = get_type_hints(parent, None, + self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: + self.object = UNINITIALIZED_ATTR + self.parent = parent + return True + except ImportError: + pass + + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False + + def should_suppress_value_header(self) -> bool: + return (self.object is UNINITIALIZED_ATTR or + super().should_suppress_value_header()) + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if self.object is UNINITIALIZED_ATTR: + return [] + else: + return super().get_doc(encoding, ignore) # type: ignore + + +class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin, + UninitializedGlobalVariableMixin, ModuleLevelDocumenter): """ Specialized Documenter subclass for data items. """ @@ -1633,53 +1902,68 @@ class DataDocumenter(ModuleLevelDocumenter): priority = -10 option_spec = dict(ModuleLevelDocumenter.option_spec) option_spec["annotation"] = annotation_option + option_spec["no-value"] = bool_option @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any ) -> bool: return isinstance(parent, ModuleDocumenter) and isattr + def update_annotations(self, parent: Any) -> None: + """Update __annotations__ to support type_comment and so on.""" + try: + annotations = dict(inspect.getannotations(parent)) + parent.__annotations__ = annotations + + analyzer = ModuleAnalyzer.for_module(self.modname) + analyzer.analyze() + for (classname, attrname), annotation in analyzer.annotations.items(): + if classname == '' and attrname not in annotations: + annotations[attrname] = annotation + except AttributeError: + pass + + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) + if self.parent: + self.update_annotations(self.parent) + + return ret + + def should_suppress_value_header(self) -> bool: + if super().should_suppress_value_header(): + return True + else: + doc = self.get_doc() + metadata = extract_metadata('\n'.join(sum(doc, []))) + if 'hide-value' in metadata: + return True + + return False + def add_directive_header(self, sig: str) -> None: super().add_directive_header(sig) sourcename = self.get_sourcename() - if not self.options.annotation: + if self.options.annotation is SUPPRESS or self.should_suppress_directive_header(): + pass + elif self.options.annotation: + self.add_line(' :annotation: %s' % self.options.annotation, + sourcename) + else: # obtain annotation for this data - try: - annotations = get_type_hints(self.parent) - except NameError: - # Failed to evaluate ForwardRef (maybe TYPE_CHECKING) - annotations = safe_getattr(self.parent, '__annotations__', {}) - except TypeError: - annotations = {} - except KeyError: - # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084) - annotations = {} - except AttributeError: - # AttributeError is raised on 3.5.2 (fixed by 3.5.3) - annotations = {} - + annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases) if self.objpath[-1] in annotations: objrepr = stringify_typehint(annotations.get(self.objpath[-1])) self.add_line(' :type: ' + objrepr, sourcename) - else: - key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) - if self.analyzer and key in self.analyzer.annotations: - self.add_line(' :type: ' + self.analyzer.annotations[key], - sourcename) try: - if self.object is UNINITIALIZED_ATTR: + if self.options.no_value or self.should_suppress_value_header(): pass else: objrepr = object_description(self.object) self.add_line(' :value: ' + objrepr, sourcename) except ValueError: pass - elif self.options.annotation is SUPPRESS: - pass - else: - self.add_line(' :annotation: %s' % self.options.annotation, - sourcename) def document_members(self, all_members: bool = False) -> None: pass @@ -1688,111 +1972,55 @@ class DataDocumenter(ModuleLevelDocumenter): return self.get_attr(self.parent or self.object, '__module__', None) \ or self.modname - -class DataDeclarationDocumenter(DataDocumenter): - """ - Specialized Documenter subclass for data that cannot be imported - because they are declared without initial value (refs: PEP-526). - """ - objtype = 'datadecl' - directivetype = 'data' - member_order = 60 - - # must be higher than AttributeDocumenter - priority = 11 - - @classmethod - def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any - ) -> bool: - """This documents only INSTANCEATTR members.""" - return (isinstance(parent, ModuleDocumenter) and - isattr and - member is INSTANCEATTR) - - def import_object(self, raiseerror: bool = False) -> bool: - """Never import anything.""" - # disguise as a data - self.objtype = 'data' - self.object = UNINITIALIZED_ATTR + def get_module_comment(self, attrname: str) -> Optional[List[str]]: try: - # import module to obtain type annotation - self.parent = importlib.import_module(self.modname) - except ImportError: + analyzer = ModuleAnalyzer.for_module(self.modname) + analyzer.analyze() + key = ('', attrname) + if key in analyzer.attr_docs: + return list(analyzer.attr_docs[key]) + except PycodeError: pass - return True - - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - """Never try to get a docstring from the object.""" - super().add_content(more_content, no_docstring=True) - - -class GenericAliasDocumenter(DataDocumenter): - """ - Specialized Documenter subclass for GenericAliases. - """ - - objtype = 'genericalias' - directivetype = 'data' - priority = DataDocumenter.priority + 1 - - @classmethod - def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any - ) -> bool: - return inspect.isgenericalias(member) - - def add_directive_header(self, sig: str) -> None: - self.options = Options(self.options) - self.options['annotation'] = SUPPRESS - super().add_directive_header(sig) - - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - name = stringify_typehint(self.object) - content = StringList([_('alias of %s') % name], source='') - super().add_content(content) - - -class TypeVarDocumenter(DataDocumenter): - """ - Specialized Documenter subclass for TypeVars. - """ - - objtype = 'typevar' - directivetype = 'data' - priority = DataDocumenter.priority + 1 - - @classmethod - def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any - ) -> bool: - return isinstance(member, TypeVar) and isattr # type: ignore - - def add_directive_header(self, sig: str) -> None: - self.options = Options(self.options) - self.options['annotation'] = SUPPRESS - super().add_directive_header(sig) + return None def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: - if ignore is not None: - warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." - % self.__class__.__name__, - RemovedInSphinx50Warning, stacklevel=2) - - if self.object.__doc__ != TypeVar.__doc__: - return super().get_doc() + # Check the variable has a docstring-comment + comment = self.get_module_comment(self.objpath[-1]) + if comment: + return [comment] else: - return [] + return super().get_doc(encoding, ignore) - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - attrs = [repr(self.object.__name__)] - for constraint in self.object.__constraints__: - attrs.append(stringify_typehint(constraint)) - if self.object.__covariant__: - attrs.append("covariant=True") - if self.object.__contravariant__: - attrs.append("contravariant=True") + def add_content(self, more_content: Optional[StringList], no_docstring: bool = False + ) -> None: + # Disable analyzing variable comment on Documenter.add_content() to control it on + # DataDocumenter.add_content() + self.analyzer = None - content = StringList([_('alias of TypeVar(%s)') % ", ".join(attrs)], source='') - super().add_content(content) + if not more_content: + more_content = StringList() + + self.update_content(more_content) + super().add_content(more_content, no_docstring=no_docstring) + + +class NewTypeDataDocumenter(DataDocumenter): + """ + Specialized Documenter subclass for NewTypes. + + Note: This must be invoked before FunctionDocumenter because NewType is a kind of + function object. + """ + + objtype = 'newtypedata' + directivetype = 'data' + priority = FunctionDocumenter.priority + 1 + + @classmethod + def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + return inspect.isNewType(member) and isattr class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore @@ -1828,7 +2056,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: return ret def format_args(self, **kwargs: Any) -> str: - if self.env.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) try: @@ -1842,12 +2070,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): self.env.app.emit('autodoc-before-process-signature', self.object, False) sig = inspect.signature(self.object, bound_method=False, - type_aliases=self.env.config.autodoc_type_aliases) + type_aliases=self.config.autodoc_type_aliases) else: self.env.app.emit('autodoc-before-process-signature', self.object, True) sig = inspect.signature(self.object, bound_method=True, - follow_wrapped=True, - type_aliases=self.env.config.autodoc_type_aliases) + type_aliases=self.config.autodoc_type_aliases) args = stringify_signature(sig, **kwargs) except TypeError as exc: logger.warning(__("Failed to get a method signature for %s: %s"), @@ -1856,7 +2083,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: except ValueError: args = '' - if self.env.config.strip_signature_backslash: + if self.config.strip_signature_backslash: # escape backslashes for reST args = args.replace('\\', '\\\\') return args @@ -1882,7 +2109,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: def format_signature(self, **kwargs: Any) -> str: sigs = [] - if self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads: + if (self.analyzer and + '.'.join(self.objpath) in self.analyzer.overloads and + self.config.autodoc_typehints == 'signature'): # Use signatures for overloaded methods instead of the implementation method. overloaded = True else: @@ -1905,10 +2134,18 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: documenter.objpath = [None] sigs.append(documenter.format_signature()) if overloaded: + if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): + actual = inspect.signature(self.object, bound_method=False, + type_aliases=self.config.autodoc_type_aliases) + else: + actual = inspect.signature(self.object, bound_method=True, + type_aliases=self.config.autodoc_type_aliases) + __globals__ = safe_getattr(self.object, '__globals__', {}) for overload in self.analyzer.overloads.get('.'.join(self.objpath)): + overload = self.merge_default_value(actual, overload) overload = evaluate_signature(overload, __globals__, - self.env.config.autodoc_type_aliases) + self.config.autodoc_type_aliases) if not inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): @@ -1919,10 +2156,20 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: return "\n".join(sigs) + def merge_default_value(self, actual: Signature, overload: Signature) -> Signature: + """Merge default values of actual implementation to the overload variants.""" + parameters = list(overload.parameters.values()) + for i, param in enumerate(parameters): + actual_param = actual.parameters.get(param.name) + if actual_param and param.default == '...': + parameters[i] = param.replace(default=actual_param.default) + + return overload.replace(parameters=parameters) + def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: """Annotate type hint to the first argument of function if needed.""" try: - sig = inspect.signature(func, type_aliases=self.env.config.autodoc_type_aliases) + sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a method signature for %s: %s"), self.fullname, exc) @@ -1942,20 +2189,196 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: return -class SingledispatchMethodDocumenter(MethodDocumenter): +class NonDataDescriptorMixin(DataDocumenterMixinBase): """ - Used to be a specialized Documenter subclass for singledispatch'ed methods. + Mixin for AttributeDocumenter to provide the feature for supporting non + data-descriptors. - Retained for backwards compatibility, now does the same as the MethodDocumenter + .. note:: This mix-in must be inherited after other mix-ins. Otherwise, docstring + and :value: header will be suppressed unexpectedly. """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn("%s is deprecated." % self.__class__.__name__, - RemovedInSphinx50Warning, stacklevel=2) - super().__init__(*args, **kwargs) + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) # type: ignore + if ret and not inspect.isattributedescriptor(self.object): + self.non_data_descriptor = True + else: + self.non_data_descriptor = False + + return ret + + def should_suppress_value_header(self) -> bool: + return (not getattr(self, 'non_data_descriptor', False) or + super().should_suppress_directive_header()) + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if getattr(self, 'non_data_descriptor', False): + # the docstring of non datadescriptor is very probably the wrong thing + # to display + return None + else: + return super().get_doc(encoding, ignore) # type: ignore -class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore +class SlotsMixin(DataDocumenterMixinBase): + """ + Mixin for AttributeDocumenter to provide the feature for supporting __slots__. + """ + + def isslotsattribute(self) -> bool: + """Check the subject is an attribute in __slots__.""" + try: + __slots__ = inspect.getslots(self.parent) + if __slots__ and self.objpath[-1] in __slots__: + return True + else: + return False + except (AttributeError, ValueError, TypeError): + return False + + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) # type: ignore + if self.isslotsattribute(): + self.object = SLOTSATTR + + return ret + + def should_suppress_directive_header(self) -> bool: + if self.object is SLOTSATTR: + self._datadescriptor = True + return True + else: + return super().should_suppress_directive_header() + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if self.object is SLOTSATTR: + try: + __slots__ = inspect.getslots(self.parent) + if __slots__ and __slots__.get(self.objpath[-1]): + docstring = prepare_docstring(__slots__[self.objpath[-1]]) + return [docstring] + else: + return [] + except (AttributeError, ValueError) as exc: + logger.warning(__('Invalid __slots__ found on %s. Ignored.'), + (self.parent.__qualname__, exc), type='autodoc') + return [] + else: + return super().get_doc(encoding, ignore) # type: ignore + + +class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase): + """ + Mixin for AttributeDocumenter to provide the feature for supporting runtime + instance attributes (that are defined in __init__() methods with doc-comments). + + Example: + + class Foo: + def __init__(self): + self.attr = None #: This is a target of this mix-in. + """ + + RUNTIME_INSTANCE_ATTRIBUTE = object() + + def is_runtime_instance_attribute(self, parent: Any) -> bool: + """Check the subject is an attribute defined in __init__().""" + # An instance variable defined in __init__(). + if self.get_attribute_comment(parent, self.objpath[-1]): # type: ignore + return True + else: + return False + + def import_object(self, raiseerror: bool = False) -> bool: + """Check the existence of runtime instance attribute when failed to import the + attribute.""" + try: + return super().import_object(raiseerror=True) # type: ignore + except ImportError as exc: + try: + with mock(self.config.autodoc_mock_imports): + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, # type: ignore + warningiserror=self.config.autodoc_warningiserror) + parent = ret[3] + if self.is_runtime_instance_attribute(parent): + self.object = self.RUNTIME_INSTANCE_ATTRIBUTE + self.parent = parent + return True + except ImportError: + pass + + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False + + def should_suppress_value_header(self) -> bool: + return (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE or + super().should_suppress_value_header()) + + +class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase): + """ + Mixin for AttributeDocumenter to provide the feature for supporting uninitialized + instance attributes (PEP-526 styled, annotation only attributes). + + Example: + + class Foo: + attr: int #: This is a target of this mix-in. + """ + + def is_uninitialized_instance_attribute(self, parent: Any) -> bool: + """Check the subject is an annotation only attribute.""" + annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: + return True + else: + return False + + def import_object(self, raiseerror: bool = False) -> bool: + """Check the exisitence of uninitialized instance attribute when failed to import + the attribute.""" + try: + return super().import_object(raiseerror=True) # type: ignore + except ImportError as exc: + try: + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, # type: ignore + warningiserror=self.config.autodoc_warningiserror) + parent = ret[3] + if self.is_uninitialized_instance_attribute(parent): + self.object = UNINITIALIZED_ATTR + self.parent = parent + return True + except ImportError: + pass + + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False + + def should_suppress_value_header(self) -> bool: + return (self.object is UNINITIALIZED_ATTR or + super().should_suppress_value_header()) + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if self.object is UNINITIALIZED_ATTR: + return None + else: + return super().get_doc(encoding, ignore) # type: ignore + + +class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore + TypeVarMixin, RuntimeInstanceAttributeMixin, + UninitializedInstanceAttributeMixin, NonDataDescriptorMixin, + DocstringStripSignatureMixin, ClassLevelDocumenter): """ Specialized Documenter subclass for attributes. """ @@ -1963,6 +2386,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): member_order = 60 option_spec = dict(ModuleLevelDocumenter.option_spec) option_spec["annotation"] = annotation_option + option_spec["no-value"] = bool_option # must be higher than the MethodDocumenter, else it will recognize # some non-data descriptors as methods @@ -1989,39 +2413,55 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): def isinstanceattribute(self) -> bool: """Check the subject is an instance attribute.""" - try: - analyzer = ModuleAnalyzer.for_module(self.modname) - attr_docs = analyzer.find_attr_docs() - if self.objpath: - key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) - if key in attr_docs: + warnings.warn('AttributeDocumenter.isinstanceattribute() is deprecated.', + RemovedInSphinx50Warning) + # uninitialized instance variable (PEP-526) + with mock(self.config.autodoc_mock_imports): + try: + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, + warningiserror=self.config.autodoc_warningiserror) + self.parent = ret[3] + annotations = get_type_hints(self.parent, None, + self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: + self.object = UNINITIALIZED_ATTR return True + except ImportError: + pass - return False - except PycodeError: - return False + return False + + def update_annotations(self, parent: Any) -> None: + """Update __annotations__ to support type_comment and so on.""" + try: + annotations = dict(inspect.getannotations(parent)) + parent.__annotations__ = annotations + + for cls in inspect.getmro(parent): + try: + module = safe_getattr(cls, '__module__') + qualname = safe_getattr(cls, '__qualname__') + + analyzer = ModuleAnalyzer.for_module(module) + analyzer.analyze() + for (classname, attrname), annotation in analyzer.annotations.items(): + if classname == qualname and attrname not in annotations: + annotations[attrname] = annotation + except (AttributeError, PycodeError): + pass + except AttributeError: + pass + except TypeError: + # Failed to set __annotations__ (built-in, extensions, etc.) + pass def import_object(self, raiseerror: bool = False) -> bool: - try: - ret = super().import_object(raiseerror=True) - if inspect.isenumattribute(self.object): - self.object = self.object.value - if inspect.isattributedescriptor(self.object): - self._datadescriptor = True - else: - # if it's not a data descriptor - self._datadescriptor = False - except ImportError as exc: - if self.isinstanceattribute(): - self.object = INSTANCEATTR - self._datadescriptor = False - ret = True - elif raiseerror: - raise - else: - logger.warning(exc.args[0], type='autodoc', subtype='import_object') - self.env.note_reread() - ret = False + ret = super().import_object(raiseerror) + if inspect.isenumattribute(self.object): + self.object = self.object.value + if self.parent: + self.update_annotations(self.parent) return ret @@ -2029,65 +2469,86 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): return self.get_attr(self.parent or self.object, '__module__', None) \ or self.modname + def should_suppress_value_header(self) -> bool: + if super().should_suppress_value_header(): + return True + else: + doc = self.get_doc() + if doc: + metadata = extract_metadata('\n'.join(sum(doc, []))) + if 'hide-value' in metadata: + return True + + return False + def add_directive_header(self, sig: str) -> None: super().add_directive_header(sig) sourcename = self.get_sourcename() - if not self.options.annotation: + if self.options.annotation is SUPPRESS or self.should_suppress_directive_header(): + pass + elif self.options.annotation: + self.add_line(' :annotation: %s' % self.options.annotation, sourcename) + else: # obtain type annotation for this attribute - try: - annotations = get_type_hints(self.parent) - except NameError: - # Failed to evaluate ForwardRef (maybe TYPE_CHECKING) - annotations = safe_getattr(self.parent, '__annotations__', {}) - except TypeError: - annotations = {} - except KeyError: - # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084) - annotations = {} - except AttributeError: - # AttributeError is raised on 3.5.2 (fixed by 3.5.3) - annotations = {} - + annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases) if self.objpath[-1] in annotations: objrepr = stringify_typehint(annotations.get(self.objpath[-1])) self.add_line(' :type: ' + objrepr, sourcename) - else: - key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) - if self.analyzer and key in self.analyzer.annotations: - self.add_line(' :type: ' + self.analyzer.annotations[key], - sourcename) - # data descriptors do not have useful values - if not self._datadescriptor: - try: - if self.object is INSTANCEATTR: - pass - else: - objrepr = object_description(self.object) - self.add_line(' :value: ' + objrepr, sourcename) - except ValueError: + try: + if self.options.no_value or self.should_suppress_value_header(): pass - elif self.options.annotation is SUPPRESS: - pass - else: - self.add_line(' :annotation: %s' % self.options.annotation, sourcename) + else: + objrepr = object_description(self.object) + self.add_line(' :value: ' + objrepr, sourcename) + except ValueError: + pass + + def get_attribute_comment(self, parent: Any, attrname: str) -> Optional[List[str]]: + try: + for cls in inspect.getmro(parent): + try: + module = safe_getattr(cls, '__module__') + qualname = safe_getattr(cls, '__qualname__') + + analyzer = ModuleAnalyzer.for_module(module) + analyzer.analyze() + if qualname and self.objpath: + key = (qualname, attrname) + if key in analyzer.attr_docs: + return list(analyzer.attr_docs[key]) + except (AttributeError, PycodeError): + pass + except (AttributeError, PycodeError): + pass + + return None + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + # Check the attribute has a docstring-comment + comment = self.get_attribute_comment(self.parent, self.objpath[-1]) + if comment: + return [comment] - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: try: # Disable `autodoc_inherit_docstring` temporarily to avoid to obtain # a docstring from the value which descriptor returns unexpectedly. # ref: https://github.com/sphinx-doc/sphinx/issues/7805 - orig = self.env.config.autodoc_inherit_docstrings - self.env.config.autodoc_inherit_docstrings = False # type: ignore + orig = self.config.autodoc_inherit_docstrings + self.config.autodoc_inherit_docstrings = False # type: ignore return super().get_doc(encoding, ignore) finally: - self.env.config.autodoc_inherit_docstrings = orig # type: ignore + self.config.autodoc_inherit_docstrings = orig # type: ignore - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - if not self._datadescriptor: - # if it's not a data descriptor, its docstring is very probably the - # wrong thing to display - no_docstring = True + def add_content(self, more_content: Optional[StringList], no_docstring: bool = False + ) -> None: + # Disable analyzing attribute comment on Documenter.add_content() to control it on + # AttributeDocumenter.add_content() + self.analyzer = None + + if more_content is None: + more_content = StringList() + self.update_content(more_content) super().add_content(more_content, no_docstring) @@ -2122,102 +2583,22 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # self.add_line(' :property:', sourcename) -class InstanceAttributeDocumenter(AttributeDocumenter): +class NewTypeAttributeDocumenter(AttributeDocumenter): """ - Specialized Documenter subclass for attributes that cannot be imported - because they are instance attributes (e.g. assigned in __init__). - """ - objtype = 'instanceattribute' - directivetype = 'attribute' - member_order = 60 + Specialized Documenter subclass for NewTypes. - # must be higher than AttributeDocumenter - priority = 11 + Note: This must be invoked before MethodDocumenter because NewType is a kind of + function object. + """ + + objtype = 'newvarattribute' + directivetype = 'attribute' + priority = MethodDocumenter.priority + 1 @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any ) -> bool: - """This documents only INSTANCEATTR members.""" - return (not isinstance(parent, ModuleDocumenter) and - isattr and - member is INSTANCEATTR) - - def import_parent(self) -> Any: - try: - parent = importlib.import_module(self.modname) - for name in self.objpath[:-1]: - parent = self.get_attr(parent, name) - - return parent - except (ImportError, AttributeError): - return None - - def import_object(self, raiseerror: bool = False) -> bool: - """Never import anything.""" - # disguise as an attribute - self.objtype = 'attribute' - self.object = INSTANCEATTR - self.parent = self.import_parent() - self._datadescriptor = False - return True - - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - """Never try to get a docstring from the object.""" - super().add_content(more_content, no_docstring=True) - - -class SlotsAttributeDocumenter(AttributeDocumenter): - """ - Specialized Documenter subclass for attributes that cannot be imported - because they are attributes in __slots__. - """ - objtype = 'slotsattribute' - directivetype = 'attribute' - member_order = 60 - - # must be higher than AttributeDocumenter - priority = 11 - - @classmethod - def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any - ) -> bool: - """This documents only SLOTSATTR members.""" - return member is SLOTSATTR - - def import_object(self, raiseerror: bool = False) -> bool: - """Never import anything.""" - # disguise as an attribute - self.objtype = 'attribute' - self._datadescriptor = True - - with mock(self.env.config.autodoc_mock_imports): - try: - ret = import_object(self.modname, self.objpath[:-1], 'class', - attrgetter=self.get_attr, - warningiserror=self.env.config.autodoc_warningiserror) - self.module, _, _, self.parent = ret - return True - except ImportError as exc: - if raiseerror: - raise - else: - logger.warning(exc.args[0], type='autodoc', subtype='import_object') - self.env.note_reread() - return False - - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: - """Decode and return lines of the docstring(s) for the object.""" - if ignore is not None: - warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." - % self.__class__.__name__, - RemovedInSphinx50Warning, stacklevel=2) - name = self.objpath[-1] - __slots__ = safe_getattr(self.parent, '__slots__', []) - if isinstance(__slots__, dict) and isinstance(__slots__.get(name), str): - docstring = prepare_docstring(__slots__[name]) - return [docstring] - else: - return [] + return not isinstance(parent, ModuleDocumenter) and inspect.isNewType(member) def get_documenters(app: Sphinx) -> Dict[str, "Type[Documenter]"]: @@ -2243,21 +2624,28 @@ def migrate_autodoc_member_order(app: Sphinx, config: Config) -> None: config.autodoc_member_order = 'alphabetical' # type: ignore +# for compatibility +from sphinx.ext.autodoc.deprecated import DataDeclarationDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import GenericAliasDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import InstanceAttributeDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import SingledispatchFunctionDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import SingledispatchMethodDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import SlotsAttributeDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import TypeVarDocumenter # NOQA + + def setup(app: Sphinx) -> Dict[str, Any]: app.add_autodocumenter(ModuleDocumenter) app.add_autodocumenter(ClassDocumenter) app.add_autodocumenter(ExceptionDocumenter) app.add_autodocumenter(DataDocumenter) - app.add_autodocumenter(DataDeclarationDocumenter) - app.add_autodocumenter(GenericAliasDocumenter) - app.add_autodocumenter(TypeVarDocumenter) + app.add_autodocumenter(NewTypeDataDocumenter) app.add_autodocumenter(FunctionDocumenter) app.add_autodocumenter(DecoratorDocumenter) app.add_autodocumenter(MethodDocumenter) app.add_autodocumenter(AttributeDocumenter) app.add_autodocumenter(PropertyDocumenter) - app.add_autodocumenter(InstanceAttributeDocumenter) - app.add_autodocumenter(SlotsAttributeDocumenter) + app.add_autodocumenter(NewTypeAttributeDocumenter) app.add_config_value('autoclass_content', 'class', True, ENUM('both', 'class', 'init')) app.add_config_value('autodoc_member_order', 'alphabetical', True, diff --git a/sphinx/ext/autodoc/deprecated.py b/sphinx/ext/autodoc/deprecated.py new file mode 100644 index 000000000..80a94f0ea --- /dev/null +++ b/sphinx/ext/autodoc/deprecated.py @@ -0,0 +1,126 @@ +""" + sphinx.ext.autodoc.deprecated + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + The deprecated Documenters for autodoc. + + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import warnings +from typing import Any + +from sphinx.deprecation import RemovedInSphinx50Warning +from sphinx.ext.autodoc import (AttributeDocumenter, DataDocumenter, FunctionDocumenter, + MethodDocumenter) + + +class SingledispatchFunctionDocumenter(FunctionDocumenter): + """ + Used to be a specialized Documenter subclass for singledispatch'ed functions. + + Retained for backwards compatibility, now does the same as the FunctionDocumenter + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) + + +class DataDeclarationDocumenter(DataDocumenter): + """ + Specialized Documenter subclass for data that cannot be imported + because they are declared without initial value (refs: PEP-526). + """ + objtype = 'datadecl' + directivetype = 'data' + member_order = 60 + + # must be higher than AttributeDocumenter + priority = 11 + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) + + +class TypeVarDocumenter(DataDocumenter): + """ + Specialized Documenter subclass for TypeVars. + """ + + objtype = 'typevar' + directivetype = 'data' + priority = DataDocumenter.priority + 1 # type: ignore + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) + + +class SingledispatchMethodDocumenter(MethodDocumenter): + """ + Used to be a specialized Documenter subclass for singledispatch'ed methods. + + Retained for backwards compatibility, now does the same as the MethodDocumenter + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) + + +class InstanceAttributeDocumenter(AttributeDocumenter): + """ + Specialized Documenter subclass for attributes that cannot be imported + because they are instance attributes (e.g. assigned in __init__). + """ + objtype = 'instanceattribute' + directivetype = 'attribute' + member_order = 60 + + # must be higher than AttributeDocumenter + priority = 11 + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) + + +class SlotsAttributeDocumenter(AttributeDocumenter): + """ + Specialized Documenter subclass for attributes that cannot be imported + because they are attributes in __slots__. + """ + objtype = 'slotsattribute' + directivetype = 'attribute' + member_order = 60 + + # must be higher than AttributeDocumenter + priority = 11 + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) + + +class GenericAliasDocumenter(DataDocumenter): + """ + Specialized Documenter subclass for GenericAliases. + """ + + objtype = 'genericalias' + directivetype = 'data' + priority = DataDocumenter.priority + 1 # type: ignore + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index c88600ff2..c932c6f9f 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -2,7 +2,7 @@ sphinx.ext.autodoc.directive ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -16,7 +16,7 @@ from docutils.statemachine import StringList from docutils.utils import Reporter, assemble_option_dict from sphinx.config import Config -from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc import Documenter, Options from sphinx.util import logging @@ -58,7 +58,7 @@ class DocumenterBridge: def __init__(self, env: BuildEnvironment, reporter: Reporter, options: Options, lineno: int, state: Any = None) -> None: self.env = env - self.reporter = reporter + self._reporter = reporter self.genopt = options self.lineno = lineno self.filename_set = set() # type: Set[str] @@ -77,6 +77,12 @@ class DocumenterBridge: def warn(self, msg: str) -> None: logger.warning(msg, location=(self.env.docname, self.lineno)) + @property + def reporter(self) -> Reporter: + warnings.warn('DocumenterBridge.reporter is deprecated.', + RemovedInSphinx50Warning, stacklevel=2) + return self._reporter + def process_documenter_options(documenter: "Type[Documenter]", config: Config, options: Dict ) -> Options: diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 133ce1439..d7c9b93f5 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -4,7 +4,7 @@ Importer utilities for autodoc - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,15 +13,20 @@ import traceback import warnings from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Tuple -from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias -from sphinx.pycode import ModuleAnalyzer +from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning, + deprecated_alias) +from sphinx.ext.autodoc.mock import ismock, undecorate +from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import logging -from sphinx.util.inspect import isclass, isenumclass, safe_getattr +from sphinx.util.inspect import (getannotations, getmro, getslots, isclass, isenumclass, + safe_getattr) if False: # For type annotation from typing import Type # NOQA + from sphinx.ext.autodoc import ObjectMember + logger = logging.getLogger(__name__) @@ -140,6 +145,9 @@ def get_module_members(module: Any) -> List[Tuple[str, Any]]: """Get members of target module.""" from sphinx.ext.autodoc import INSTANCEATTR + warnings.warn('sphinx.ext.autodoc.importer.get_module_members() is deprecated.', + RemovedInSphinx50Warning) + members = {} # type: Dict[str, Tuple[str, Any]] for name in dir(module): try: @@ -149,10 +157,12 @@ def get_module_members(module: Any) -> List[Tuple[str, Any]]: continue # annotation only member (ex. attr: int) - if hasattr(module, '__annotations__'): - for name in module.__annotations__: + try: + for name in getannotations(module): if name not in members: members[name] = (name, INSTANCEATTR) + except AttributeError: + pass return sorted(list(members.values())) @@ -163,21 +173,15 @@ Attribute = NamedTuple('Attribute', [('name', str), def _getmro(obj: Any) -> Tuple["Type", ...]: - """Get __mro__ from given *obj* safely.""" - __mro__ = safe_getattr(obj, '__mro__', None) - if isinstance(__mro__, tuple): - return __mro__ - else: - return tuple() + warnings.warn('sphinx.ext.autodoc.importer._getmro() is deprecated.', + RemovedInSphinx40Warning) + return getmro(obj) def _getannotations(obj: Any) -> Mapping[str, Any]: - """Get __annotations__ from given *obj* safely.""" - __annotations__ = safe_getattr(obj, '__annotations__', None) - if isinstance(__annotations__, Mapping): - return __annotations__ - else: - return {} + warnings.warn('sphinx.ext.autodoc.importer._getannotations() is deprecated.', + RemovedInSphinx40Warning) + return getannotations(obj) def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, @@ -203,11 +207,15 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, members[name] = Attribute(name, True, value) # members in __slots__ - if isclass(subject) and getattr(subject, '__slots__', None) is not None: - from sphinx.ext.autodoc import SLOTSATTR + try: + __slots__ = getslots(subject) + if __slots__: + from sphinx.ext.autodoc import SLOTSATTR - for name in subject.__slots__: - members[name] = Attribute(name, True, SLOTSATTR) + for name in __slots__: + members[name] = Attribute(name, True, SLOTSATTR) + except (AttributeError, TypeError, ValueError): + pass # other members for name in dir(subject): @@ -221,11 +229,14 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, continue # annotation only member (ex. attr: int) - for i, cls in enumerate(_getmro(subject)): - for name in _getannotations(cls): - name = unmangle(cls, name) - if name and name not in members: - members[name] = Attribute(name, i == 0, INSTANCEATTR) + for i, cls in enumerate(getmro(subject)): + try: + for name in getannotations(cls): + name = unmangle(cls, name) + if name and name not in members: + members[name] = Attribute(name, i == 0, INSTANCEATTR) + except AttributeError: + pass if analyzer: # append instance attributes (cf. self.attr1) if analyzer knows @@ -237,9 +248,87 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, return members -from sphinx.ext.autodoc.mock import ( # NOQA - _MockModule, _MockObject, MockFinder, MockLoader, mock -) +def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable + ) -> Dict[str, "ObjectMember"]: + """Get members and attributes of target class.""" + from sphinx.ext.autodoc import INSTANCEATTR, ObjectMember + + # the members directly defined in the class + obj_dict = attrgetter(subject, '__dict__', {}) + + members = {} # type: Dict[str, ObjectMember] + + # enum members + if isenumclass(subject): + for name, value in subject.__members__.items(): + if name not in members: + members[name] = ObjectMember(name, value, class_=subject) + + superclass = subject.__mro__[1] + for name in obj_dict: + if name not in superclass.__dict__: + value = safe_getattr(subject, name) + members[name] = ObjectMember(name, value, class_=subject) + + # members in __slots__ + try: + __slots__ = getslots(subject) + if __slots__: + from sphinx.ext.autodoc import SLOTSATTR + + for name, docstring in __slots__.items(): + members[name] = ObjectMember(name, SLOTSATTR, class_=subject, + docstring=docstring) + except (AttributeError, TypeError, ValueError): + pass + + # other members + for name in dir(subject): + try: + value = attrgetter(subject, name) + if ismock(value): + value = undecorate(value) + + unmangled = unmangle(subject, name) + if unmangled and unmangled not in members: + if name in obj_dict: + members[unmangled] = ObjectMember(unmangled, value, class_=subject) + else: + members[unmangled] = ObjectMember(unmangled, value) + except AttributeError: + continue + + try: + for cls in getmro(subject): + # annotation only member (ex. attr: int) + try: + for name in getannotations(cls): + name = unmangle(cls, name) + if name and name not in members: + members[name] = ObjectMember(name, INSTANCEATTR, class_=cls) + except AttributeError: + pass + + # append instance attributes (cf. self.attr1) if analyzer knows + try: + modname = safe_getattr(cls, '__module__') + qualname = safe_getattr(cls, '__qualname__') + analyzer = ModuleAnalyzer.for_module(modname) + analyzer.analyze() + for (ns, name), docstring in analyzer.attr_docs.items(): + if ns == qualname and name not in members: + members[name] = ObjectMember(name, INSTANCEATTR, class_=cls, + docstring='\n'.join(docstring)) + except (AttributeError, PycodeError): + pass + except AttributeError: + pass + + return members + + +from sphinx.ext.autodoc.mock import (MockFinder, MockLoader, _MockModule, _MockObject, # NOQA + mock) deprecated_alias('sphinx.ext.autodoc.importer', { diff --git a/sphinx/ext/autodoc/mock.py b/sphinx/ext/autodoc/mock.py index 40258a135..d3f4291c2 100644 --- a/sphinx/ext/autodoc/mock.py +++ b/sphinx/ext/autodoc/mock.py @@ -4,7 +4,7 @@ mock for autodoc - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,10 +13,11 @@ import os import sys from importlib.abc import Loader, MetaPathFinder from importlib.machinery import ModuleSpec -from types import FunctionType, MethodType, ModuleType +from types import ModuleType from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union from sphinx.util import logging +from sphinx.util.inspect import safe_getattr logger = logging.getLogger(__name__) @@ -26,6 +27,7 @@ class _MockObject: __display_name__ = '_MockObject' __sphinx_mock__ = True + __sphinx_decorator_args__ = () # type: Tuple[Any, ...] def __new__(cls, *args: Any, **kwargs: Any) -> Any: if len(args) == 3 and isinstance(args[1], tuple): @@ -59,18 +61,19 @@ class _MockObject: return _make_subclass(key, self.__display_name__, self.__class__)() def __call__(self, *args: Any, **kwargs: Any) -> Any: - if args and type(args[0]) in [type, FunctionType, MethodType]: - # Appears to be a decorator, pass through unchanged - return args[0] - return self + call = self.__class__() + call.__sphinx_decorator_args__ = args + return call def __repr__(self) -> str: return self.__display_name__ def _make_subclass(name: str, module: str, superclass: Any = _MockObject, - attributes: Any = None) -> Any: - attrs = {'__module__': module, '__display_name__': module + '.' + name} + attributes: Any = None, decorator_args: Tuple = ()) -> Any: + attrs = {'__module__': module, + '__display_name__': module + '.' + name, + '__sphinx_decorator_args__': decorator_args} attrs.update(attributes or {}) return type(name, (superclass,), attrs) @@ -147,3 +150,38 @@ def mock(modnames: List[str]) -> Generator[None, None, None]: finally: sys.meta_path.remove(finder) finder.invalidate_caches() + + +def ismock(subject: Any) -> bool: + """Check if the object is mocked.""" + # check the object has '__sphinx_mock__' attribute + try: + if safe_getattr(subject, '__sphinx_mock__', None) is None: + return False + except AttributeError: + return False + + # check the object is mocked module + if isinstance(subject, _MockModule): + return True + + try: + # check the object is mocked object + __mro__ = safe_getattr(type(subject), '__mro__', []) + if len(__mro__) > 2 and __mro__[1] is _MockObject: + return True + except AttributeError: + pass + + return False + + +def undecorate(subject: _MockObject) -> Any: + """Unwrap mock if *subject* is decorated by mocked object. + + If not decorated, returns given *subject* itself. + """ + if ismock(subject) and subject.__sphinx_decorator_args__: + return subject.__sphinx_decorator_args__[0] + else: + return subject diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py index 7f11e3d12..4db13c695 100644 --- a/sphinx/ext/autodoc/type_comment.py +++ b/sphinx/ext/autodoc/type_comment.py @@ -4,13 +4,12 @@ Update annotations info of living objects using type_comments. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from inspect import Parameter, Signature, getsource -from typing import Any, Dict, List -from typing import cast +from typing import Any, Dict, List, cast import sphinx from sphinx.application import Sphinx @@ -18,8 +17,7 @@ from sphinx.locale import __ from sphinx.pycode.ast import ast from sphinx.pycode.ast import parse as ast_parse from sphinx.pycode.ast import unparse as ast_unparse -from sphinx.util import inspect -from sphinx.util import logging +from sphinx.util import inspect, logging logger = logging.getLogger(__name__) diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index 4f81a6eae..533b71e42 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -4,14 +4,13 @@ Generating content for autodoc using typehints - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re from collections import OrderedDict -from typing import Any, Dict, Iterable -from typing import cast +from typing import Any, Dict, Iterable, cast from docutils import nodes from docutils.nodes import Element @@ -28,7 +27,7 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any, if callable(obj): annotations = app.env.temp_data.setdefault('annotations', {}) annotation = annotations.setdefault(name, OrderedDict()) - sig = inspect.signature(obj) + sig = inspect.signature(obj, type_aliases=app.config.autodoc_type_aliases) for param in sig.parameters.values(): if param.annotation is not param.empty: annotation[param.name] = typing.stringify(param.annotation) diff --git a/sphinx/ext/autosectionlabel.py b/sphinx/ext/autosectionlabel.py index 4bb401791..be8ad0bc1 100644 --- a/sphinx/ext/autosectionlabel.py +++ b/sphinx/ext/autosectionlabel.py @@ -4,12 +4,11 @@ Allow reference sections by :ref: role using its title. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict -from typing import cast +from typing import Any, Dict, cast from docutils import nodes from docutils.nodes import Node @@ -20,7 +19,6 @@ from sphinx.locale import __ from sphinx.util import logging from sphinx.util.nodes import clean_astext - logger = logging.getLogger(__name__) diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 739636d7e..b268127d0 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -48,7 +48,7 @@ resolved to a Python object, and otherwise it becomes simple emphasis. This can be used as the default role to make links 'smart'. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -60,8 +60,7 @@ import sys import warnings from os import path from types import ModuleType -from typing import Any, Dict, List, Tuple -from typing import cast +from typing import Any, Dict, List, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node, system_message @@ -72,19 +71,19 @@ from docutils.statemachine import StringList import sphinx from sphinx import addnodes from sphinx.application import Sphinx +from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.environment import BuildEnvironment from sphinx.environment.adapters.toctree import TocTree -from sphinx.ext.autodoc import Documenter, INSTANCEATTR +from sphinx.ext.autodoc import INSTANCEATTR, Documenter from sphinx.ext.autodoc.directive import DocumenterBridge, Options from sphinx.ext.autodoc.importer import import_module from sphinx.ext.autodoc.mock import mock from sphinx.locale import __ from sphinx.pycode import ModuleAnalyzer, PycodeError -from sphinx.util import rst, logging -from sphinx.util.docutils import ( - NullReporter, SphinxDirective, SphinxRole, new_document, switch_source_input -) +from sphinx.util import logging, rst +from sphinx.util.docutils import (NullReporter, SphinxDirective, SphinxRole, new_document, + switch_source_input) from sphinx.util.matching import Matcher from sphinx.writers.html import HTMLTranslator @@ -99,7 +98,7 @@ logger = logging.getLogger(__name__) periods_re = re.compile(r'\.(?:\s+)') literal_re = re.compile(r'::\s*$') -WELL_KNOWN_ABBREVIATIONS = (' i.e.',) +WELL_KNOWN_ABBREVIATIONS = ('et al.', ' i.e.',) # -- autosummary_toc node ------------------------------------------------------ @@ -177,8 +176,10 @@ class FakeDirective(DocumenterBridge): def __init__(self) -> None: settings = Struct(tab_width=8) document = Struct(settings=settings) + env = BuildEnvironment() + env.config = Config() state = Struct(document=document) - super().__init__({}, None, Options(), 0, state) # type: ignore + super().__init__(env, None, Options(), 0, state) def get_documenter(app: Sphinx, obj: Any, parent: Any) -> "Type[Documenter]": @@ -691,7 +692,7 @@ def import_ivar_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, A analyzer = ModuleAnalyzer.for_module(modname) if (qualname, attr) in analyzer.find_attr_docs(): return real_name + "." + attr, INSTANCEATTR, obj, modname - except (ImportError, ValueError): + except (ImportError, ValueError, PycodeError): pass raise ImportError diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 7580285aa..0240d2c7c 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -13,7 +13,7 @@ generate: sphinx-autogen -o source/generated source/*.rst - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -34,20 +34,18 @@ from jinja2 import TemplateNotFound from jinja2.sandbox import SandboxedEnvironment import sphinx.locale -from sphinx import __display_version__ -from sphinx import package_dir +from sphinx import __display_version__, package_dir from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.ext.autodoc import Documenter -from sphinx.ext.autosummary import import_by_name, import_ivar_by_name, get_documenter +from sphinx.ext.autodoc.importer import import_module +from sphinx.ext.autosummary import get_documenter, import_by_name, import_ivar_by_name from sphinx.locale import __ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.registry import SphinxComponentRegistry -from sphinx.util import logging -from sphinx.util import rst -from sphinx.util import split_full_qualified_name +from sphinx.util import logging, rst, split_full_qualified_name from sphinx.util.inspect import safe_getattr from sphinx.util.osutil import ensuredir from sphinx.util.template import SphinxTemplateLoader @@ -88,29 +86,29 @@ AutosummaryEntry = NamedTuple('AutosummaryEntry', [('name', str), def setup_documenters(app: Any) -> None: - from sphinx.ext.autodoc import ( - ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, - FunctionDocumenter, MethodDocumenter, AttributeDocumenter, - InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, - SlotsAttributeDocumenter, DataDeclarationDocumenter, GenericAliasDocumenter, - SingledispatchFunctionDocumenter, - ) + from sphinx.ext.autodoc import (AttributeDocumenter, ClassDocumenter, DataDocumenter, + DecoratorDocumenter, ExceptionDocumenter, + FunctionDocumenter, MethodDocumenter, ModuleDocumenter, + NewTypeAttributeDocumenter, NewTypeDataDocumenter, + PropertyDocumenter) documenters = [ ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, - FunctionDocumenter, MethodDocumenter, AttributeDocumenter, - InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, - SlotsAttributeDocumenter, DataDeclarationDocumenter, GenericAliasDocumenter, - SingledispatchFunctionDocumenter, + FunctionDocumenter, MethodDocumenter, NewTypeAttributeDocumenter, + NewTypeDataDocumenter, AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, ] # type: List[Type[Documenter]] for documenter in documenters: app.registry.add_documenter(documenter.objtype, documenter) def _simple_info(msg: str) -> None: + warnings.warn('_simple_info() is deprecated.', + RemovedInSphinx50Warning, stacklevel=2) print(msg) def _simple_warn(msg: str) -> None: + warnings.warn('_simple_warn() is deprecated.', + RemovedInSphinx50Warning, stacklevel=2) print('WARNING: ' + msg, file=sys.stderr) @@ -291,6 +289,13 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any, items = [] # type: List[str] for _, modname, ispkg in pkgutil.iter_modules(obj.__path__): fullname = name + '.' + modname + try: + module = import_module(fullname) + if module and hasattr(module, '__sphinx_mock__'): + continue + except ImportError: + pass + items.append(fullname) public = [x for x in items if not x.split('.')[-1].startswith('_')] return public, items diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index 536b3b9d2..f052b8810 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -5,7 +5,7 @@ Check Python modules and C API for coverage. Mostly written by Josip Dzolonga for the Google Highly Open Participation contest. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,7 +15,7 @@ import pickle import re from importlib import import_module from os import path -from typing import Any, Dict, IO, List, Pattern, Set, Tuple +from typing import IO, Any, Dict, List, Pattern, Set, Tuple import sphinx from sphinx.application import Sphinx diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 7cd89ad32..20afa2ec6 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -5,7 +5,7 @@ Mimic doctest by automatically executing code snippets and checking their results. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, Iterable, List, Sequence, Set, Tuple from docutils import nodes from docutils.nodes import Element, Node, TextElement from docutils.parsers.rst import directives -from packaging.specifiers import SpecifierSet, InvalidSpecifier +from packaging.specifiers import InvalidSpecifier, SpecifierSet from packaging.version import Version import sphinx @@ -36,6 +36,7 @@ from sphinx.util.osutil import relpath if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx diff --git a/sphinx/ext/duration.py b/sphinx/ext/duration.py index 669baf2f1..f714dedfb 100644 --- a/sphinx/ext/duration.py +++ b/sphinx/ext/duration.py @@ -4,15 +4,14 @@ Measure durations of Sphinx processing. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from datetime import datetime, timedelta from itertools import islice from operator import itemgetter -from typing import Any, Dict, List -from typing import cast +from typing import Any, Dict, List, cast from docutils import nodes diff --git a/sphinx/ext/extlinks.py b/sphinx/ext/extlinks.py index ff3adb357..d82c0378b 100644 --- a/sphinx/ext/extlinks.py +++ b/sphinx/ext/extlinks.py @@ -19,7 +19,7 @@ You can also give an explicit caption, e.g. :exmpl:`Foo `. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/ext/githubpages.py b/sphinx/ext/githubpages.py index 4564ce60e..e760bb4b6 100644 --- a/sphinx/ext/githubpages.py +++ b/sphinx/ext/githubpages.py @@ -4,7 +4,7 @@ To publish HTML docs at GitHub Pages, create .nojekyll file. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index d97a7505e..f10285086 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -5,7 +5,7 @@ Allow graphviz-formatted graphs to be included in Sphinx-generated documents inline. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,7 +13,7 @@ import posixpath import re import subprocess from os import path -from subprocess import CalledProcessError, PIPE +from subprocess import PIPE, CalledProcessError from typing import Any, Dict, List, Tuple from docutils import nodes @@ -142,6 +142,7 @@ class Graphviz(SphinxDirective): 'it failed') % filename, line=self.lineno)] else: dotcode = '\n'.join(self.content) + rel_filename = None if not dotcode.strip(): return [self.state_machine.reporter.warning( __('Ignoring "graphviz" directive without content.'), @@ -160,6 +161,8 @@ class Graphviz(SphinxDirective): node['align'] = self.options['align'] if 'class' in self.options: node['classes'] = self.options['class'] + if rel_filename: + node['filename'] = rel_filename if 'caption' not in self.options: self.add_name(node) @@ -182,7 +185,8 @@ class GraphvizSimple(SphinxDirective): 'alt': directives.unchanged, 'align': align_spec, 'caption': directives.unchanged, - 'graphviz_dot': directives.unchanged, + 'layout': directives.unchanged, + 'graphviz_dot': directives.unchanged, # an old alias of `layout` option 'name': directives.unchanged, 'class': directives.class_option, } @@ -194,6 +198,8 @@ class GraphvizSimple(SphinxDirective): node['options'] = {'docname': self.env.docname} if 'graphviz_dot' in self.options: node['options']['graphviz_dot'] = self.options['graphviz_dot'] + if 'layout' in self.options: + node['options']['graphviz_dot'] = self.options['layout'] if 'alt' in self.options: node['alt'] = self.options['alt'] if 'align' in self.options: @@ -210,8 +216,8 @@ class GraphvizSimple(SphinxDirective): return [figure] -def render_dot(self: SphinxTranslator, code: str, options: Dict, - format: str, prefix: str = 'graphviz') -> Tuple[str, str]: +def render_dot(self: SphinxTranslator, code: str, options: Dict, format: str, + prefix: str = 'graphviz', filename: str = None) -> Tuple[str, str]: """Render graphviz code into a PNG or PDF output file.""" graphviz_dot = options.get('graphviz_dot', self.builder.config.graphviz_dot) hashkey = (code + str(options) + str(graphviz_dot) + @@ -235,7 +241,10 @@ def render_dot(self: SphinxTranslator, code: str, options: Dict, dot_args.extend(['-T' + format, '-o' + outfn]) docname = options.get('docname', 'index') - cwd = path.dirname(path.join(self.builder.srcdir, docname)) + if filename: + cwd = path.dirname(path.join(self.builder.srcdir, filename)) + else: + cwd = path.dirname(path.join(self.builder.srcdir, docname)) if format == 'png': dot_args.extend(['-Tcmapx', '-o%s.map' % outfn]) @@ -260,14 +269,14 @@ def render_dot(self: SphinxTranslator, code: str, options: Dict, def render_dot_html(self: HTMLTranslator, node: graphviz, code: str, options: Dict, - prefix: str = 'graphviz', imgcls: str = None, alt: str = None - ) -> Tuple[str, str]: + prefix: str = 'graphviz', imgcls: str = None, alt: str = None, + filename: str = None) -> Tuple[str, str]: format = self.builder.config.graphviz_output_format try: if format not in ('png', 'svg'): raise GraphvizError(__("graphviz_output_format must be one of 'png', " "'svg', but is %r") % format) - fname, outfn = render_dot(self, code, options, format, prefix) + fname, outfn = render_dot(self, code, options, format, prefix, filename) except GraphvizError as exc: logger.warning(__('dot code %r: %s'), code, exc) raise nodes.SkipNode from exc @@ -312,13 +321,14 @@ def render_dot_html(self: HTMLTranslator, node: graphviz, code: str, options: Di def html_visit_graphviz(self: HTMLTranslator, node: graphviz) -> None: - render_dot_html(self, node, node['code'], node['options']) + render_dot_html(self, node, node['code'], node['options'], filename=node.get('filename')) def render_dot_latex(self: LaTeXTranslator, node: graphviz, code: str, - options: Dict, prefix: str = 'graphviz') -> None: + options: Dict, prefix: str = 'graphviz', filename: str = None + ) -> None: try: - fname, outfn = render_dot(self, code, options, 'pdf', prefix) + fname, outfn = render_dot(self, code, options, 'pdf', prefix, filename) except GraphvizError as exc: logger.warning(__('dot code %r: %s'), code, exc) raise nodes.SkipNode from exc @@ -349,7 +359,7 @@ def render_dot_latex(self: LaTeXTranslator, node: graphviz, code: str, def latex_visit_graphviz(self: LaTeXTranslator, node: graphviz) -> None: - render_dot_latex(self, node, node['code'], node['options']) + render_dot_latex(self, node, node['code'], node['options'], filename=node.get('filename')) def render_dot_texinfo(self: TexinfoTranslator, node: graphviz, code: str, diff --git a/sphinx/ext/ifconfig.py b/sphinx/ext/ifconfig.py index bae4554cf..df8545028 100644 --- a/sphinx/ext/ifconfig.py +++ b/sphinx/ext/ifconfig.py @@ -15,7 +15,7 @@ namespace of the project configuration (that is, all variables from ``conf.py`` are available.) - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/ext/imgconverter.py b/sphinx/ext/imgconverter.py index dd13a9879..b0d40b551 100644 --- a/sphinx/ext/imgconverter.py +++ b/sphinx/ext/imgconverter.py @@ -4,13 +4,13 @@ Image converter extension for Sphinx - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import subprocess import sys -from subprocess import CalledProcessError, PIPE +from subprocess import PIPE, CalledProcessError from typing import Any, Dict from sphinx.application import Sphinx @@ -19,7 +19,6 @@ from sphinx.locale import __ from sphinx.transforms.post_transforms.images import ImageConverter from sphinx.util import logging - logger = logging.getLogger(__name__) diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index d11c5d7c5..cef50d548 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -4,7 +4,7 @@ Render math in HTML via dvipng or dvisvgm. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,7 +15,7 @@ import subprocess import sys import tempfile from os import path -from subprocess import CalledProcessError, PIPE +from subprocess import PIPE, CalledProcessError from typing import Any, Dict, List, Tuple from docutils import nodes @@ -93,7 +93,7 @@ depthsvgcomment_re = re.compile(r'') def read_svg_depth(filename: str) -> int: """Read the depth from comment at last line of SVG file """ - with open(filename, 'r') as f: + with open(filename) as f: for line in f: pass # Only last line is checked diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index 71a123b15..63a171087 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -31,7 +31,7 @@ r""" The graph is inserted as a PNG+image map into HTML and a PDF in LaTeX. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -39,8 +39,7 @@ import builtins import inspect import re from importlib import import_module -from typing import Any, Dict, Iterable, List, Tuple -from typing import cast +from typing import Any, Dict, Iterable, List, Tuple, cast from docutils import nodes from docutils.nodes import Node @@ -50,17 +49,14 @@ import sphinx from sphinx import addnodes from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment -from sphinx.ext.graphviz import ( - graphviz, figure_wrapper, - render_dot_html, render_dot_latex, render_dot_texinfo -) +from sphinx.ext.graphviz import (figure_wrapper, graphviz, render_dot_html, render_dot_latex, + render_dot_texinfo) from sphinx.util import md5 from sphinx.util.docutils import SphinxDirective from sphinx.writers.html import HTMLTranslator from sphinx.writers.latex import LaTeXTranslator from sphinx.writers.texinfo import TexinfoTranslator - module_sig_re = re.compile(r'''^(?:([\w.]*)\.)? # module names (\w+) \s* $ # class/final module name ''', re.VERBOSE) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index a6c4ef694..5569ad9de 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -19,7 +19,7 @@ also be specified individually, e.g. if the docs should be buildable without Internet access. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -29,7 +29,7 @@ import posixpath import sys import time from os import path -from typing import Any, Dict, IO, List, Tuple +from typing import IO, Any, Dict, List, Tuple from urllib.parse import urlsplit, urlunsplit from docutils import nodes @@ -42,11 +42,10 @@ from sphinx.builders.html import INVENTORY_FILENAME from sphinx.config import Config from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ -from sphinx.util import requests, logging +from sphinx.util import logging, requests from sphinx.util.inventory import InventoryFile from sphinx.util.typing import Inventory - logger = logging.getLogger(__name__) diff --git a/sphinx/ext/jsmath.py b/sphinx/ext/jsmath.py index 75369a5ca..cec40e224 100644 --- a/sphinx/ext/jsmath.py +++ b/sphinx/ext/jsmath.py @@ -5,18 +5,15 @@ Set up everything for use of JSMath to display math in HTML via JavaScript. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import warnings from typing import Any, Dict -from sphinxcontrib.jsmath import ( # NOQA - html_visit_math, - html_visit_displaymath, - install_jsmath, -) +from sphinxcontrib.jsmath import (html_visit_displaymath, html_visit_math, # NOQA + install_jsmath) import sphinx from sphinx.application import Sphinx diff --git a/sphinx/ext/linkcode.py b/sphinx/ext/linkcode.py index 68e8603f7..5c118a9fb 100644 --- a/sphinx/ext/linkcode.py +++ b/sphinx/ext/linkcode.py @@ -4,7 +4,7 @@ Add external links to module code in Python object descriptions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py index cc3cd4ba3..ff8ef3718 100644 --- a/sphinx/ext/mathjax.py +++ b/sphinx/ext/mathjax.py @@ -6,26 +6,28 @@ Sphinx's HTML writer -- requires the MathJax JavaScript library on your webserver/computer. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import json -from typing import Any, Dict -from typing import cast +from typing import Any, Dict, cast from docutils import nodes import sphinx from sphinx.application import Sphinx -from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.domains.math import MathDomain -from sphinx.environment import BuildEnvironment from sphinx.errors import ExtensionError from sphinx.locale import _ from sphinx.util.math import get_node_equation_number from sphinx.writers.html import HTMLTranslator +# more information for mathjax secure url is here: +# https://docs.mathjax.org/en/latest/start.html#secure-access-to-the-cdn +MATHJAX_URL = ('https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/latest.js?' + 'config=TeX-AMS-MML_HTMLorMML') + def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None: self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight')) @@ -67,25 +69,25 @@ def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None raise nodes.SkipNode -def install_mathjax(app: Sphinx, env: BuildEnvironment) -> None: +def install_mathjax(app: Sphinx, pagename: str, templatename: str, context: Dict, + event_arg: Any) -> None: if app.builder.format != 'html' or app.builder.math_renderer_name != 'mathjax': # type: ignore # NOQA return if not app.config.mathjax_path: raise ExtensionError('mathjax_path config value must be set for the ' 'mathjax extension to work') - builder = cast(StandaloneHTMLBuilder, app.builder) - domain = cast(MathDomain, env.get_domain('math')) - if domain.has_equations(): + domain = cast(MathDomain, app.env.get_domain('math')) + if domain.has_equations(pagename): # Enable mathjax only if equations exists options = {'async': 'async'} if app.config.mathjax_options: options.update(app.config.mathjax_options) - builder.add_js_file(app.config.mathjax_path, **options) + app.add_js_file(app.config.mathjax_path, **options) # type: ignore if app.config.mathjax_config: body = "MathJax.Hub.Config(%s)" % json.dumps(app.config.mathjax_config) - builder.add_js_file(None, type="text/x-mathjax-config", body=body) + app.add_js_file(None, type="text/x-mathjax-config", body=body) def setup(app: Sphinx) -> Dict[str, Any]: @@ -93,15 +95,11 @@ def setup(app: Sphinx) -> Dict[str, Any]: (html_visit_math, None), (html_visit_displaymath, None)) - # more information for mathjax secure url is here: - # https://docs.mathjax.org/en/latest/start.html#secure-access-to-the-cdn - app.add_config_value('mathjax_path', - 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/latest.js?' - 'config=TeX-AMS-MML_HTMLorMML', 'html') + app.add_config_value('mathjax_path', MATHJAX_URL, 'html') app.add_config_value('mathjax_options', {}, 'html') app.add_config_value('mathjax_inline', [r'\(', r'\)'], 'html') app.add_config_value('mathjax_display', [r'\[', r'\]'], 'html') app.add_config_value('mathjax_config', None, 'html') - app.connect('env-updated', install_mathjax) + app.connect('html-page-context', install_mathjax) return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index e62ef7152..4a8c2135a 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -4,7 +4,7 @@ Support for NumPy and Google style docstrings. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -44,6 +44,7 @@ class Config: napoleon_preprocess_types = False napoleon_type_aliases = None napoleon_custom_sections = None + napoleon_attr_annotations = True .. _Google style: https://google.github.io/styleguide/pyguide.html @@ -252,11 +253,19 @@ class Config: * To create a custom "generic" section, just pass a string. * To create an alias for an existing section, pass a tuple containing the alias name and the original, in that order. + * To create a custom section that displays like the parameters or returns + section, pass a tuple containing the custom section name and a string + value, "params_style" or "returns_style". If an entry is just a string, it is interpreted as a header for a generic section. If the entry is a tuple/list/indexed container, the first entry - is the name of the section, the second is the section key to emulate. + is the name of the section, the second is the section key to emulate. If the + second entry value is "params_style" or "returns_style", the custom section + will be displayed like the parameters section or returns section. + napoleon_attr_annotations : :obj:`bool` (Defaults to True) + Use the type annotations of class attributes that are documented in the docstring + but do not have a type in the docstring. """ _config_values = { @@ -274,7 +283,8 @@ class Config: 'napoleon_use_keyword': (True, 'env'), 'napoleon_preprocess_types': (False, 'env'), 'napoleon_type_aliases': (None, 'env'), - 'napoleon_custom_sections': (None, 'env') + 'napoleon_custom_sections': (None, 'env'), + 'napoleon_attr_annotations': (True, 'env'), } def __init__(self, **settings: Any) -> None: @@ -443,8 +453,8 @@ def _skip_member(app: Sphinx, what: str, name: str, obj: Any, if cls_path: try: if '.' in cls_path: - import importlib import functools + import importlib mod = importlib.import_module(obj.__module__) mod_path = cls_path.split('.') diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index d36fdd17a..bd1c44509 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -6,7 +6,7 @@ Classes for docstring parsing and formatting. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -21,6 +21,8 @@ from sphinx.config import Config as SphinxConfig from sphinx.ext.napoleon.iterators import modify_iter from sphinx.locale import _, __ from sphinx.util import logging +from sphinx.util.inspect import stringify_annotation +from sphinx.util.typing import get_type_hints if False: # For type annotation @@ -57,6 +59,19 @@ _default_regex = re.compile( _SINGLETONS = ("None", "True", "False", "Ellipsis") +def _convert_type_spec(_type: str, translations: Dict[str, str] = {}) -> str: + """Convert type specification to reference in reST.""" + if _type in translations: + return translations[_type] + else: + if _type == 'None': + return ':obj:`None`' + else: + return ':class:`%s`' % _type + + return _type + + class GoogleDocstring: """Convert Google style docstrings to reStructuredText. @@ -177,6 +192,8 @@ class GoogleDocstring: 'notes': self._parse_notes_section, 'other parameters': self._parse_other_parameters_section, 'parameters': self._parse_parameters_section, + 'receive': self._parse_receives_section, + 'receives': self._parse_receives_section, 'return': self._parse_returns_section, 'returns': self._parse_returns_section, 'raise': self._parse_raises_section, @@ -261,6 +278,10 @@ class GoogleDocstring: if prefer_type and not _type: _type, _name = _name, _type + + if _type and self._config.napoleon_preprocess_types: + _type = _convert_type_spec(_type, self._config.napoleon_type_aliases or {}) + indent = self._get_indent(line) + 1 _descs = [_desc] + self._dedent(self._consume_indented_block(indent)) _descs = self.__class__(_descs, self._config).lines() @@ -289,7 +310,8 @@ class GoogleDocstring: _descs = self.__class__(_descs, self._config).lines() return _type, _descs - def _consume_returns_section(self) -> List[Tuple[str, str, List[str]]]: + def _consume_returns_section(self, preprocess_types: bool = False + ) -> List[Tuple[str, str, List[str]]]: lines = self._dedent(self._consume_to_next_section()) if lines: before, colon, after = self._partition_field_on_colon(lines[0]) @@ -303,6 +325,10 @@ class GoogleDocstring: _type = before + if (_type and preprocess_types and + self._config.napoleon_preprocess_types): + _type = _convert_type_spec(_type, self._config.napoleon_type_aliases or {}) + _desc = self.__class__(_desc, self._config).lines() return [(_name, _type, _desc,)] else: @@ -545,11 +571,18 @@ class GoogleDocstring: self._sections[entry.lower()] = self._parse_custom_generic_section else: # otherwise, assume entry is container; - # [0] is new section, [1] is the section to alias. - # in the case of key mismatch, just handle as generic section. - self._sections[entry[0].lower()] = \ - self._sections.get(entry[1].lower(), - self._parse_custom_generic_section) + if entry[1] == "params_style": + self._sections[entry[0].lower()] = \ + self._parse_custom_params_style_section + elif entry[1] == "returns_style": + self._sections[entry[0].lower()] = \ + self._parse_custom_returns_style_section + else: + # [0] is new section, [1] is the section to alias. + # in the case of key mismatch, just handle as generic section. + self._sections[entry[0].lower()] = \ + self._sections.get(entry[1].lower(), + self._parse_custom_generic_section) def _parse(self) -> None: self._parsed_lines = self._consume_empty() @@ -600,6 +633,8 @@ class GoogleDocstring: def _parse_attributes_section(self, section: str) -> List[str]: lines = [] for _name, _type, _desc in self._consume_fields(): + if not _type: + _type = self._lookup_annotation(_name) if self._config.napoleon_use_ivar: _name = self._qualify_name(_name, self._obj) field = ':ivar %s: ' % _name @@ -635,6 +670,13 @@ class GoogleDocstring: # for now, no admonition for simple custom sections return self._parse_generic_section(section, False) + def _parse_custom_params_style_section(self, section: str) -> List[str]: + return self._format_fields(section, self._consume_fields()) + + def _parse_custom_returns_style_section(self, section: str) -> List[str]: + fields = self._consume_returns_section(preprocess_types=True) + return self._format_fields(section, fields) + def _parse_usage_section(self, section: str) -> List[str]: header = ['.. rubric:: Usage:', ''] block = ['.. code-block:: python', ''] @@ -699,6 +741,9 @@ class GoogleDocstring: m = self._name_rgx.match(_type) if m and m.group('name'): _type = m.group('name') + elif _xref_regex.match(_type): + pos = _type.find('`') + _type = _type[pos + 1:-1] _type = ' ' + _type if _type else '' _desc = self._strip_empty(_desc) _descs = ' ' + '\n '.join(_desc) if any(_desc) else '' @@ -707,6 +752,15 @@ class GoogleDocstring: lines.append('') return lines + def _parse_receives_section(self, section: str) -> List[str]: + if self._config.napoleon_use_param: + # Allow to declare multiple parameters at once (ex: x, y: int) + fields = self._consume_fields(multiple=True) + return self._format_docutils_params(fields) + else: + fields = self._consume_fields() + return self._format_fields(_('Receives'), fields) + def _parse_references_section(self, section: str) -> List[str]: use_admonition = self._config.napoleon_use_admonition_for_references return self._parse_generic_section(_('References'), use_admonition) @@ -746,7 +800,7 @@ class GoogleDocstring: return self._format_fields(_('Warns'), self._consume_fields()) def _parse_yields_section(self, section: str) -> List[str]: - fields = self._consume_returns_section() + fields = self._consume_returns_section(preprocess_types=True) return self._format_fields(_('Yields'), fields) def _partition_field_on_colon(self, line: str) -> Tuple[str, str, str]: @@ -801,6 +855,21 @@ class GoogleDocstring: lines = lines[start:end + 1] return lines + def _lookup_annotation(self, _name: str) -> str: + if self._config.napoleon_attr_annotations: + if self._what in ("module", "class", "exception") and self._obj: + # cache the class annotations + if not hasattr(self, "_annotations"): + localns = getattr(self._config, "autodoc_type_aliases", {}) + localns.update(getattr( + self._config, "napoleon_type_aliases", {} + ) or {}) + self._annotations = get_type_hints(self._obj, None, localns) + if _name in self._annotations: + return stringify_annotation(self._annotations[_name]) + # No annotation found + return "" + def _recombine_set_tokens(tokens: List[str]) -> List[str]: token_queue = collections.deque(tokens) @@ -1104,6 +1173,13 @@ class NumpyDocstring(GoogleDocstring): _name, _type = line, '' _name, _type = _name.strip(), _type.strip() _name = self._escape_args_and_kwargs(_name) + + if parse_type and not _type: + _type = self._lookup_annotation(_name) + + if prefer_type and not _type: + _type, _name = _name, _type + if self._config.napoleon_preprocess_types: _type = _convert_numpy_type_spec( _type, @@ -1111,14 +1187,13 @@ class NumpyDocstring(GoogleDocstring): translations=self._config.napoleon_type_aliases or {}, ) - if prefer_type and not _type: - _type, _name = _name, _type indent = self._get_indent(line) + 1 _desc = self._dedent(self._consume_indented_block(indent)) _desc = self.__class__(_desc, self._config).lines() return _name, _type, _desc - def _consume_returns_section(self) -> List[Tuple[str, str, List[str]]]: + def _consume_returns_section(self, preprocess_types: bool = False + ) -> List[Tuple[str, str, List[str]]]: return self._consume_fields(prefer_type=True) def _consume_section_header(self) -> str: @@ -1188,6 +1263,22 @@ class NumpyDocstring(GoogleDocstring): items.append((name, list(rest), role)) del rest[:] + def translate(func, description, role): + translations = self._config.napoleon_type_aliases + if role is not None or not translations: + return func, description, role + + translated = translations.get(func, func) + match = self._name_rgx.match(translated) + if not match: + return translated, description, role + + groups = match.groupdict() + role = groups["role"] + new_func = groups["name"] or groups["name2"] + + return new_func, description, role + current_func = None rest = [] # type: List[str] @@ -1218,37 +1309,19 @@ class NumpyDocstring(GoogleDocstring): if not items: return [] - roles = { - 'method': 'meth', - 'meth': 'meth', - 'function': 'func', - 'func': 'func', - 'class': 'class', - 'exception': 'exc', - 'exc': 'exc', - 'object': 'obj', - 'obj': 'obj', - 'module': 'mod', - 'mod': 'mod', - 'data': 'data', - 'constant': 'const', - 'const': 'const', - 'attribute': 'attr', - 'attr': 'attr' - } - if self._what is None: - func_role = 'obj' - else: - func_role = roles.get(self._what, '') + # apply type aliases + items = [ + translate(func, description, role) + for func, description, role in items + ] + lines = [] # type: List[str] last_had_desc = True - for func, desc, role in items: + for name, desc, role in items: if role: - link = ':%s:`%s`' % (role, func) - elif func_role: - link = ':%s:`%s`' % (func_role, func) + link = ':%s:`%s`' % (role, name) else: - link = "`%s`_" % func + link = ':obj:`%s`' % name if desc or last_had_desc: lines += [''] lines += [link] diff --git a/sphinx/ext/napoleon/iterators.py b/sphinx/ext/napoleon/iterators.py index fc41afdb3..e0f5cca9c 100644 --- a/sphinx/ext/napoleon/iterators.py +++ b/sphinx/ext/napoleon/iterators.py @@ -6,7 +6,7 @@ A collection of helpful iterators. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index bfb46903f..640c93935 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -7,13 +7,12 @@ all todos of your project and lists them along with a backlink to the original location. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import warnings -from typing import Any, Dict, Iterable, List, Tuple -from typing import cast +from typing import Any, Dict, Iterable, List, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index dc24a1993..21cff6a03 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -4,12 +4,15 @@ Add links to module code in Python object descriptions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import posixpath import traceback -from typing import Any, Dict, Iterable, Iterator, Set, Tuple +import warnings +from os import path +from typing import Any, Dict, Generator, Iterable, Optional, Set, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node @@ -17,17 +20,32 @@ from docutils.nodes import Element, Node import sphinx from sphinx import addnodes from sphinx.application import Sphinx +from sphinx.builders import Builder +from sphinx.builders.html import StandaloneHTMLBuilder +from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer +from sphinx.transforms.post_transforms import SphinxPostTransform from sphinx.util import get_full_modname, logging, status_iterator from sphinx.util.nodes import make_refnode - logger = logging.getLogger(__name__) -def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> str: +OUTPUT_DIRNAME = '_modules' + + +class viewcode_anchor(Element): + """Node for viewcode anchors. + + This node will be processed in the resolving phase. + For viewcode supported builders, they will be all converted to the anchors. + For not supported builders, they will be removed. + """ + + +def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> Optional[str]: try: return get_full_modname(modname, attribute) except AttributeError: @@ -45,14 +63,21 @@ def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> str: return None +def is_supported_builder(builder: Builder) -> bool: + if builder.format != 'html': + return False + elif builder.name == 'singlehtml': + return False + elif builder.name.startswith('epub') and not builder.config.viewcode_enable_epub: + return False + else: + return True + + def doctree_read(app: Sphinx, doctree: Node) -> None: env = app.builder.env if not hasattr(env, '_viewcode_modules'): env._viewcode_modules = {} # type: ignore - if app.builder.name == "singlehtml": - return - if app.builder.name.startswith("epub") and not env.config.viewcode_enable_epub: - return def has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool: entry = env._viewcode_modules.get(modname, None) # type: ignore @@ -109,13 +134,8 @@ def doctree_read(app: Sphinx, doctree: Node) -> None: # only one link per name, please continue names.add(fullname) - pagename = '_modules/' + modname.replace('.', '/') - inline = nodes.inline('', _('[source]'), classes=['viewcode-link']) - onlynode = addnodes.only(expr='html') - onlynode += addnodes.pending_xref('', inline, reftype='viewcode', refdomain='std', - refexplicit=False, reftarget=pagename, - refid=fullname, refdoc=env.docname) - signode += onlynode + pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/')) + signode += viewcode_anchor(reftarget=pagename, refid=fullname, refdoc=env.docname) def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str], @@ -129,20 +149,92 @@ def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str], env._viewcode_modules.update(other._viewcode_modules) # type: ignore +def env_purge_doc(app: Sphinx, env: BuildEnvironment, docname: str) -> None: + modules = getattr(env, '_viewcode_modules', {}) + + for modname, (code, tags, used, refname) in list(modules.items()): + for fullname in list(used): + if used[fullname] == docname: + used.pop(fullname) + + if len(used) == 0: + modules.pop(modname) + + +class ViewcodeAnchorTransform(SphinxPostTransform): + """Convert or remove viewcode_anchor nodes depends on builder.""" + default_priority = 100 + + def run(self, **kwargs: Any) -> None: + if is_supported_builder(self.app.builder): + self.convert_viewcode_anchors() + else: + self.remove_viewcode_anchors() + + def convert_viewcode_anchors(self) -> None: + for node in self.document.traverse(viewcode_anchor): + anchor = nodes.inline('', _('[source]'), classes=['viewcode-link']) + refnode = make_refnode(self.app.builder, node['refdoc'], node['reftarget'], + node['refid'], anchor) + node.replace_self(refnode) + + def remove_viewcode_anchors(self) -> None: + for node in self.document.traverse(viewcode_anchor): + node.parent.remove(node) + + def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node - ) -> Node: + ) -> Optional[Node]: # resolve our "viewcode" reference nodes -- they need special treatment if node['reftype'] == 'viewcode': + warnings.warn('viewcode extension is no longer use pending_xref node. ' + 'Please update your extension.', RemovedInSphinx50Warning) return make_refnode(app.builder, node['refdoc'], node['reftarget'], node['refid'], contnode) return None -def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: +def get_module_filename(app: Sphinx, modname: str) -> Optional[str]: + """Get module filename for *modname*.""" + source_info = app.emit_firstresult('viewcode-find-source', modname) + if source_info: + return None + else: + try: + filename, source = ModuleAnalyzer.get_module_source(modname) + return filename + except Exception: + return None + + +def should_generate_module_page(app: Sphinx, modname: str) -> bool: + """Check generation of module page is needed.""" + module_filename = get_module_filename(app, modname) + if module_filename is None: + # Always (re-)generate module page when module filename is not found. + return True + + builder = cast(StandaloneHTMLBuilder, app.builder) + basename = modname.replace('.', '/') + builder.out_suffix + page_filename = path.join(app.outdir, '_modules/', basename) + + try: + if path.getmtime(module_filename) <= path.getmtime(page_filename): + # generation is not needed if the HTML page is newer than module file. + return False + except IOError: + pass + + return True + + +def collect_pages(app: Sphinx) -> Generator[Tuple[str, Dict[str, Any], str], None, None]: env = app.builder.env if not hasattr(env, '_viewcode_modules'): return + if not is_supported_builder(app.builder): + return highlighter = app.builder.highlighter # type: ignore urito = app.builder.get_relative_uri @@ -155,9 +247,12 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: app.verbosity, lambda x: x[0]): if not entry: continue + if not should_generate_module_page(app, modname): + continue + code, tags, used, refname = entry # construct a page name for the highlighted source - pagename = '_modules/' + modname.replace('.', '/') + pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/')) # highlight the source using the builder's highlighter if env.config.highlight_language in ('python3', 'default', 'none'): lexer = env.config.highlight_language @@ -189,10 +284,10 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: parent = parent.rsplit('.', 1)[0] if parent in modnames: parents.append({ - 'link': urito(pagename, '_modules/' + - parent.replace('.', '/')), + 'link': urito(pagename, + posixpath.join(OUTPUT_DIRNAME, parent.replace('.', '/'))), 'title': parent}) - parents.append({'link': urito(pagename, '_modules/index'), + parents.append({'link': urito(pagename, posixpath.join(OUTPUT_DIRNAME, 'index')), 'title': _('Module code')}) parents.reverse() # putting it all together @@ -221,7 +316,8 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: html.append('') stack.append(modname + '.') html.append('
  • %s
  • \n' % ( - urito('_modules/index', '_modules/' + modname.replace('.', '/')), + urito(posixpath.join(OUTPUT_DIRNAME, 'index'), + posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/'))), modname)) html.append('' * (len(stack) - 1)) context = { @@ -230,7 +326,7 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: ''.join(html)), } - yield ('_modules/index', context, 'page.html') + yield (posixpath.join(OUTPUT_DIRNAME, 'index'), context, 'page.html') def setup(app: Sphinx) -> Dict[str, Any]: @@ -239,12 +335,14 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('viewcode_follow_imported_members', True, False) app.connect('doctree-read', doctree_read) app.connect('env-merge-info', env_merge_info) + app.connect('env-purge-doc', env_purge_doc) app.connect('html-collect-pages', collect_pages) app.connect('missing-reference', missing_reference) # app.add_config_value('viewcode_include_modules', [], 'env') # app.add_config_value('viewcode_exclude_modules', [], 'env') app.add_event('viewcode-find-source') app.add_event('viewcode-follow-imported') + app.add_post_transform(ViewcodeAnchorTransform) return { 'version': sphinx.__display_version__, 'env_version': 1, diff --git a/sphinx/extension.py b/sphinx/extension.py index fa7a0d490..8129d89af 100644 --- a/sphinx/extension.py +++ b/sphinx/extension.py @@ -4,7 +4,7 @@ Utilities for Sphinx extensions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/highlighting.py b/sphinx/highlighting.py index 4dc9ce543..8425009f7 100644 --- a/sphinx/highlighting.py +++ b/sphinx/highlighting.py @@ -4,7 +4,7 @@ Highlight code blocks using Pygments. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -17,18 +17,16 @@ from pygments.filters import ErrorToken from pygments.formatter import Formatter from pygments.formatters import HtmlFormatter, LatexFormatter from pygments.lexer import Lexer -from pygments.lexers import get_lexer_by_name, guess_lexer -from pygments.lexers import PythonLexer, Python3Lexer, PythonConsoleLexer, \ - CLexer, TextLexer, RstLexer +from pygments.lexers import (CLexer, Python3Lexer, PythonConsoleLexer, PythonLexer, RstLexer, + TextLexer, get_lexer_by_name, guess_lexer) from pygments.style import Style from pygments.styles import get_style_by_name from pygments.util import ClassNotFound from sphinx.locale import __ -from sphinx.pygments_styles import SphinxStyle, NoneStyle +from sphinx.pygments_styles import NoneStyle, SphinxStyle from sphinx.util import logging, texescape - logger = logging.getLogger(__name__) lexers = {} # type: Dict[str, Lexer] diff --git a/sphinx/io.py b/sphinx/io.py index fd30b86a9..3508dd58d 100644 --- a/sphinx/io.py +++ b/sphinx/io.py @@ -4,7 +4,7 @@ Input/Output files - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import codecs @@ -26,21 +26,19 @@ from sphinx import addnodes from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.environment import BuildEnvironment from sphinx.errors import FiletypeNotFoundError -from sphinx.transforms import ( - AutoIndexUpgrader, DoctreeReadEvent, FigureAligner, SphinxTransformer -) -from sphinx.transforms.i18n import ( - PreserveTranslatableMessages, Locale, RemoveTranslatableInline, -) +from sphinx.transforms import (AutoIndexUpgrader, DoctreeReadEvent, FigureAligner, + SphinxTransformer) +from sphinx.transforms.i18n import (Locale, PreserveTranslatableMessages, + RemoveTranslatableInline) from sphinx.transforms.references import SphinxDomains -from sphinx.util import logging, get_filetype -from sphinx.util import UnicodeDecodeErrorHandler +from sphinx.util import UnicodeDecodeErrorHandler, get_filetype, logging from sphinx.util.docutils import LoggingReporter from sphinx.versioning import UIDTransform if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index 52d0257e5..c890455f3 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -4,7 +4,7 @@ Glue code for the jinja2 templating engine. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,7 +12,7 @@ from os import path from pprint import pformat from typing import Any, Callable, Dict, Iterator, List, Tuple, Union -from jinja2 import FileSystemLoader, BaseLoader, TemplateNotFound, contextfunction +from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound, contextfunction from jinja2.environment import Environment from jinja2.sandbox import SandboxedEnvironment from jinja2.utils import open_if_exists diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 5210dc725..bedd9e1cb 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -4,7 +4,7 @@ Locale utilities. - :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/parsers.py b/sphinx/parsers.py index 6a07d1801..17c291af7 100644 --- a/sphinx/parsers.py +++ b/sphinx/parsers.py @@ -4,7 +4,7 @@ A Base class for additional parsers. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -23,8 +23,10 @@ from sphinx.util.rst import append_epilog, prepend_prolog if False: # For type annotation - from docutils.transforms import Transform # NOQA from typing import Type # NOQA # for python3.5.1 + + from docutils.transforms import Transform # NOQA + from sphinx.application import Sphinx diff --git a/sphinx/project.py b/sphinx/project.py index f4afdadad..e5df4013f 100644 --- a/sphinx/project.py +++ b/sphinx/project.py @@ -4,7 +4,7 @@ Utility function and classes for Sphinx projects. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,9 +12,7 @@ import os from glob import glob from sphinx.locale import __ -from sphinx.util import get_matching_files -from sphinx.util import logging -from sphinx.util import path_stabilize +from sphinx.util import get_matching_files, logging, path_stabilize from sphinx.util.matching import compile_matchers from sphinx.util.osutil import SEP, relpath diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 4fef4a394..ab0dfdbf8 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -4,7 +4,7 @@ Utilities parsing and analyzing Python code. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -16,10 +16,10 @@ from importlib import import_module from inspect import Signature from io import StringIO from os import path -from typing import Any, Dict, IO, List, Tuple, Optional +from typing import IO, Any, Dict, List, Optional, Tuple from zipfile import ZipFile -from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.errors import PycodeError from sphinx.pycode.parser import Parser @@ -143,16 +143,26 @@ class ModuleAnalyzer: self._encoding = None self.code = source.read() - # will be filled by parse() + # will be filled by analyze() self.annotations = None # type: Dict[Tuple[str, str], str] self.attr_docs = None # type: Dict[Tuple[str, str], List[str]] self.finals = None # type: List[str] self.overloads = None # type: Dict[str, List[Signature]] self.tagorder = None # type: Dict[str, int] self.tags = None # type: Dict[str, Tuple[str, int, int]] + self._analyzed = False def parse(self) -> None: """Parse the source code.""" + warnings.warn('ModuleAnalyzer.parse() is deprecated.', + RemovedInSphinx50Warning, stacklevel=2) + self.analyze() + + def analyze(self) -> None: + """Analyze the source code.""" + if self._analyzed: + return None + try: parser = Parser(self.code, self._encoding) parser.parse() @@ -169,21 +179,18 @@ class ModuleAnalyzer: self.overloads = parser.overloads self.tags = parser.definitions self.tagorder = parser.deforders + self._analyzed = True except Exception as exc: raise PycodeError('parsing %r failed: %r' % (self.srcname, exc)) from exc def find_attr_docs(self) -> Dict[Tuple[str, str], List[str]]: """Find class and module-level attributes and their documentation.""" - if self.attr_docs is None: - self.parse() - + self.analyze() return self.attr_docs def find_tags(self) -> Dict[str, Tuple[str, int, int]]: """Find class, function and method definitions and their location.""" - if self.tags is None: - self.parse() - + self.analyze() return self.tags @property diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index 17d78f4eb..65534f958 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -4,12 +4,12 @@ Helpers for AST (Abstract Syntax Tree). - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import sys -from typing import Dict, List, Type, Optional +from typing import Dict, List, Optional, Type if sys.version_info > (3, 8): import ast @@ -52,6 +52,10 @@ def parse(code: str, mode: str = 'exec') -> "ast.AST": try: # type_comments parameter is available on py38+ return ast.parse(code, mode=mode, type_comments=True) # type: ignore + except SyntaxError: + # Some syntax error found. To ignore invalid type comments, retry parsing without + # type_comments parameter (refs: https://github.com/sphinx-doc/sphinx/issues/8652). + return ast.parse(code, mode=mode) except TypeError: # fallback to ast module. # typed_ast is used to parse type_comments if installed. diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index be9bfc96d..dca59acd4 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -4,7 +4,7 @@ Utilities parsing and analyzing Python code. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import inspect @@ -14,14 +14,13 @@ import sys import tokenize from collections import OrderedDict from inspect import Signature -from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING +from token import DEDENT, INDENT, NAME, NEWLINE, NUMBER, OP, STRING from tokenize import COMMENT, NL from typing import Any, Dict, List, Optional, Tuple from sphinx.pycode.ast import ast # for py37 or older from sphinx.pycode.ast import parse, unparse - comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$') indent_re = re.compile('^\\s*$') emptyline_re = re.compile('^\\s*(#.*)?$') diff --git a/sphinx/pygments_styles.py b/sphinx/pygments_styles.py index c5b07e75f..1046826af 100644 --- a/sphinx/pygments_styles.py +++ b/sphinx/pygments_styles.py @@ -4,14 +4,14 @@ Sphinx theme specific highlighting styles. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from pygments.style import Style from pygments.styles.friendly import FriendlyStyle -from pygments.token import Generic, Comment, Number, Whitespace, Keyword, \ - Operator, Name, String, Error +from pygments.token import (Comment, Error, Generic, Keyword, Name, Number, Operator, String, + Whitespace) class NoneStyle(Style): diff --git a/sphinx/registry.py b/sphinx/registry.py index d0c00b85f..c6a249e74 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -4,7 +4,7 @@ Sphinx component registry. - :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -38,6 +38,7 @@ from sphinx.util.typing import RoleFunction, TitleGetter if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx from sphinx.ext.autodoc import Documenter @@ -62,7 +63,7 @@ class SphinxComponentRegistry: self.documenters = {} # type: Dict[str, Type[Documenter]] #: css_files; a list of tuple of filename and attributes - self.css_files = [] # type: List[Tuple[str, Dict[str, str]]] + self.css_files = [] # type: List[Tuple[str, Dict[str, Any]]] #: domains; a dict of domain name -> domain class self.domains = {} # type: Dict[str, Type[Domain]] @@ -93,7 +94,7 @@ class SphinxComponentRegistry: self.html_block_math_renderers = {} # type: Dict[str, Tuple[Callable, Callable]] #: js_files; list of JS paths or URLs - self.js_files = [] # type: List[Tuple[str, Dict[str, str]]] + self.js_files = [] # type: List[Tuple[str, Dict[str, Any]]] #: LaTeX packages; list of package names and its options self.latex_packages = [] # type: List[Tuple[str, str]] @@ -360,10 +361,10 @@ class SphinxComponentRegistry: attrgetter: Callable[[Any, str, Any], Any]) -> None: self.autodoc_attrgettrs[typ] = attrgetter - def add_css_files(self, filename: str, **attributes: str) -> None: + def add_css_files(self, filename: str, **attributes: Any) -> None: self.css_files.append((filename, attributes)) - def add_js_file(self, filename: str, **attributes: str) -> None: + def add_js_file(self, filename: str, **attributes: Any) -> None: logger.debug('[app] adding js_file: %r, %r', filename, attributes) self.js_files.append((filename, attributes)) diff --git a/sphinx/roles.py b/sphinx/roles.py index 2ff66d07b..4f9261360 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -4,7 +4,7 @@ Handlers for additional ReST roles. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -21,14 +21,13 @@ from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import _ from sphinx.util import ws_re from sphinx.util.docutils import ReferenceRole, SphinxRole -from sphinx.util.nodes import ( - split_explicit_title, process_index_entry, set_role_source_info -) +from sphinx.util.nodes import process_index_entry, set_role_source_info, split_explicit_title from sphinx.util.typing import RoleFunction if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 4534dd333..6c748aed1 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -4,7 +4,7 @@ Create a full-text search index for offline search. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import html @@ -13,13 +13,12 @@ import re import warnings from importlib import import_module from os import path -from typing import Any, Dict, IO, Iterable, List, Tuple, Set +from typing import IO, Any, Dict, Iterable, List, Set, Tuple from docutils import nodes from docutils.nodes import Node -from sphinx import addnodes -from sphinx import package_dir +from sphinx import addnodes, package_dir from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.environment import BuildEnvironment from sphinx.search.jssplitter import splitter_code @@ -297,8 +296,8 @@ class IndexBuilder: frozen.get('envversion') != self.env.version: raise ValueError('old format') index2fn = frozen['docnames'] - self._filenames = dict(zip(index2fn, frozen['filenames'])) # type: ignore - self._titles = dict(zip(index2fn, frozen['titles'])) # type: ignore + self._filenames = dict(zip(index2fn, frozen['filenames'])) + self._titles = dict(zip(index2fn, frozen['titles'])) def load_terms(mapping: Dict[str, Any]) -> Dict[str, Set[str]]: rv = {} @@ -359,13 +358,13 @@ class IndexBuilder: def get_terms(self, fn2index: Dict) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]: rvs = {}, {} # type: Tuple[Dict[str, List[str]], Dict[str, List[str]]] for rv, mapping in zip(rvs, (self._mapping, self._title_mapping)): - for k, v in mapping.items(): # type: ignore + for k, v in mapping.items(): if len(v) == 1: fn, = v if fn in fn2index: - rv[k] = fn2index[fn] # type: ignore + rv[k] = fn2index[fn] else: - rv[k] = sorted([fn2index[fn] for fn in v if fn in fn2index]) # type: ignore # NOQA + rv[k] = sorted([fn2index[fn] for fn in v if fn in fn2index]) return rvs def freeze(self) -> Dict[str, Any]: diff --git a/sphinx/search/da.py b/sphinx/search/da.py index b04679e10..e164e0271 100644 --- a/sphinx/search/da.py +++ b/sphinx/search/da.py @@ -4,7 +4,7 @@ Danish search language: includes the JS Danish stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - danish_stopwords = parse_stop_word(''' | source: http://snowball.tartarus.org/algorithms/danish/stop.txt og | and diff --git a/sphinx/search/de.py b/sphinx/search/de.py index ae1827bf9..174eb8d2f 100644 --- a/sphinx/search/de.py +++ b/sphinx/search/de.py @@ -4,7 +4,7 @@ German search language: includes the JS German stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - german_stopwords = parse_stop_word(''' |source: http://snowball.tartarus.org/algorithms/german/stop.txt aber | but diff --git a/sphinx/search/en.py b/sphinx/search/en.py index 23a6f9739..b33769cec 100644 --- a/sphinx/search/en.py +++ b/sphinx/search/en.py @@ -4,7 +4,7 @@ English search language: includes the JS porter stemmer. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/search/es.py b/sphinx/search/es.py index 1009961c8..e87584cfc 100644 --- a/sphinx/search/es.py +++ b/sphinx/search/es.py @@ -4,7 +4,7 @@ Spanish search language: includes the JS Spanish stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - spanish_stopwords = parse_stop_word(''' |source: http://snowball.tartarus.org/algorithms/spanish/stop.txt de | from, of diff --git a/sphinx/search/fi.py b/sphinx/search/fi.py index 67bee89fe..ceb38cb5a 100644 --- a/sphinx/search/fi.py +++ b/sphinx/search/fi.py @@ -4,7 +4,7 @@ Finnish search language: includes the JS Finnish stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - finnish_stopwords = parse_stop_word(''' | source: http://snowball.tartarus.org/algorithms/finnish/stop.txt | forms of BE diff --git a/sphinx/search/fr.py b/sphinx/search/fr.py index b15271888..46320a863 100644 --- a/sphinx/search/fr.py +++ b/sphinx/search/fr.py @@ -4,7 +4,7 @@ French search language: includes the JS French stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - french_stopwords = parse_stop_word(''' | source: http://snowball.tartarus.org/algorithms/french/stop.txt au | a + le diff --git a/sphinx/search/hu.py b/sphinx/search/hu.py index 085773383..628dde63e 100644 --- a/sphinx/search/hu.py +++ b/sphinx/search/hu.py @@ -4,7 +4,7 @@ Hungarian search language: includes the JS Hungarian stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - hungarian_stopwords = parse_stop_word(''' | source: http://snowball.tartarus.org/algorithms/hungarian/stop.txt | prepared by Anna Tordai diff --git a/sphinx/search/it.py b/sphinx/search/it.py index e76cd99dd..2ecf4c2c5 100644 --- a/sphinx/search/it.py +++ b/sphinx/search/it.py @@ -4,7 +4,7 @@ Italian search language: includes the JS Italian stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - italian_stopwords = parse_stop_word(''' | source: http://snowball.tartarus.org/algorithms/italian/stop.txt ad | a (to) before vowel diff --git a/sphinx/search/ja.py b/sphinx/search/ja.py index c1e72b8f8..6f10a4239 100644 --- a/sphinx/search/ja.py +++ b/sphinx/search/ja.py @@ -4,7 +4,7 @@ Japanese search language: includes routine to split words. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -33,7 +33,7 @@ try: except ImportError: janome_module = False -from sphinx.errors import SphinxError, ExtensionError +from sphinx.errors import ExtensionError, SphinxError from sphinx.search import SearchLanguage from sphinx.util import import_object diff --git a/sphinx/search/jssplitter.py b/sphinx/search/jssplitter.py index 945579560..9b879772a 100644 --- a/sphinx/search/jssplitter.py +++ b/sphinx/search/jssplitter.py @@ -6,7 +6,7 @@ DO NOT EDIT. This is generated by utils/jssplitter_generator.py - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/search/nl.py b/sphinx/search/nl.py index 0e2e2ef23..f98cdf8ad 100644 --- a/sphinx/search/nl.py +++ b/sphinx/search/nl.py @@ -4,7 +4,7 @@ Dutch search language: includes the JS porter stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - dutch_stopwords = parse_stop_word(''' | source: http://snowball.tartarus.org/algorithms/dutch/stop.txt de | the diff --git a/sphinx/search/no.py b/sphinx/search/no.py index 68c1ac207..c78bb6981 100644 --- a/sphinx/search/no.py +++ b/sphinx/search/no.py @@ -4,7 +4,7 @@ Norwegian search language: includes the JS Norwegian stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - norwegian_stopwords = parse_stop_word(''' | source: http://snowball.tartarus.org/algorithms/norwegian/stop.txt og | and diff --git a/sphinx/search/pt.py b/sphinx/search/pt.py index 2538511f7..a7d99cde3 100644 --- a/sphinx/search/pt.py +++ b/sphinx/search/pt.py @@ -4,7 +4,7 @@ Portuguese search language: includes the JS Portuguese stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word - portuguese_stopwords = parse_stop_word(''' | source: http://snowball.tartarus.org/algorithms/portuguese/stop.txt de | of, from diff --git a/sphinx/search/ro.py b/sphinx/search/ro.py index cfae772c9..6400967c6 100644 --- a/sphinx/search/ro.py +++ b/sphinx/search/ro.py @@ -4,7 +4,7 @@ Romanian search language: includes the JS Romanian stemmer. - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,6 @@ import snowballstemmer from sphinx.search import SearchLanguage - js_stemmer = """ var JSX={};(function(j){function l(b,e){var a=function(){};a.prototype=e.prototype;var c=new a;for(var d in b){b[d].prototype=c}}function L(c,b){for(var a in b.prototype)if(b.prototype.hasOwnProperty(a))c.prototype[a]=b.prototype[a]}function h(a,b,d){function c(a,b,c){delete a[b];a[b]=c;return c}Object.defineProperty(a,b,{get:function(){return c(a,b,d())},set:function(d){c(a,b,d)},enumerable:true,configurable:true})}function M(a,b,c){return a[b]=a[b]/c|0}var E=parseInt;var C=parseFloat;function N(a){return a!==a}var A=isFinite;var z=encodeURIComponent;var y=decodeURIComponent;var x=encodeURI;var w=decodeURI;var u=Object.prototype.toString;var D=Object.prototype.hasOwnProperty;function k(){}j.require=function(b){var a=r[b];return a!==undefined?a:null};j.profilerIsRunning=function(){return k.getResults!=null};j.getProfileResults=function(){return(k.getResults||function(){return{}})()};j.postProfileResults=function(a,b){if(k.postResults==null)throw new Error('profiler has not been turned on');return k.postResults(a,b)};j.resetProfileResults=function(){if(k.resetResults==null)throw new Error('profiler has not been turned on');return k.resetResults()};j.DEBUG=false;function t(){};l([t],Error);function a(a,b,c){this.F=a.length;this.K=a;this.L=b;this.I=c;this.H=null;this.P=null};l([a],Object);function n(){};l([n],Object);function g(){var a;var b;var c;this.G={};a=this.E='';b=this._=0;c=this.A=a.length;this.D=0;this.B=b;this.C=c};l([g],n);function v(a,b){a.E=b.E;a._=b._;a.A=b.A;a.D=b.D;a.B=b.B;a.C=b.C};function d(b,d,c,e){var a;if(b._>=b.A){return false}a=b.E.charCodeAt(b._);if(a>e||a>>3]&1<<(a&7))===0){return false}b._++;return true};function e(a,d,c,e){var b;if(a._>=a.A){return false}b=a.E.charCodeAt(a._);if(b>e||b>>3]&1<<(b&7))===0){a._++;return true}return false};function p(a,d,c,e){var b;if(a._<=a.D){return false}b=a.E.charCodeAt(a._-1);if(b>e||b>>3]&1<<(b&7))===0){a._--;return true}return false};function m(a,b,d){var c;if(a.A-a._>>1);h=0;c=g0){break}if(d===b){break}if(l){break}l=true}}while(true){a=m[b];if(g>=a.F){f._=e+a.F|0;if(a.H==null){return a.I}o=a.H(a.P);f._=e+a.F|0;if(o){return a.I}}b=a.L;if(b<0){return 0}}return-1};function f(d,m,p){var b;var g;var e;var n;var f;var k;var l;var i;var h;var c;var a;var j;var o;b=0;g=p;e=d._;n=d.D;f=0;k=0;l=false;while(true){i=b+(g-b>>1);h=0;c=f=0;j--){if(e-c===n){h=-1;break}h=d.E.charCodeAt(e-1-c)-a.K.charCodeAt(j);if(h!==0){break}c++}if(h<0){g=i;k=c}else{b=i;f=c}if(g-b<=1){if(b>0){break}if(g===b){break}if(l){break}l=true}}while(true){a=m[b];if(f>=a.F){d._=e-a.F|0;if(a.H==null){return a.I}o=a.H(d);d._=e-a.F|0;if(o){return a.I}}b=a.L;if(b<0){return 0}}return-1};function s(a,b,d,e){var c;c=e.length-(d-b);a.E=a.E.slice(0,b)+e+a.E.slice(d);a.A+=c|0;if(a._>=d){a._+=c|0}else if(a._>b){a._=b}return c|0};function c(a,f){var b;var c;var d;var e;b=false;if((c=a.B)<0||c>(d=a.C)||d>(e=a.A)||e>a.E.length?false:true){s(a,a.B,a.C,f);b=true}return b};g.prototype.J=function(){return false};g.prototype.b=function(b){var a;var c;var d;var e;a=this.G['.'+b];if(a==null){c=this.E=b;d=this._=0;e=this.A=c.length;this.D=0;this.B=d;this.C=e;this.J();a=this.E;this.G['.'+b]=a}return a};g.prototype.stemWord=g.prototype.b;g.prototype.c=function(e){var d;var b;var c;var a;var f;var g;var h;d=[];for(b=0;b=this.A){break d}this._++}continue b}this._=i;break b}return true};b.prototype.r_prelude=b.prototype.W;function G(a){var j;var e;var k;var f;var g;var h;var i;var l;b:while(true){j=a._;f=true;d:while(f===true){f=false;e:while(true){e=a._;g=true;a:while(g===true){g=false;if(!d(a,b.g_v,97,259)){break a}a.B=a._;h=true;f:while(h===true){h=false;k=a._;i=true;c:while(i===true){i=false;if(!m(a,1,'u')){break c}a.C=a._;if(!d(a,b.g_v,97,259)){break c}if(!c(a,'U')){return false}break f}a._=k;if(!m(a,1,'i')){break a}a.C=a._;if(!d(a,b.g_v,97,259)){break a}if(!c(a,'I')){return false}}a._=e;break e}l=a._=e;if(l>=a.A){break d}a._++}continue b}a._=j;break b}return true};b.prototype.U=function(){var u;var w;var x;var y;var t;var l;var f;var g;var h;var i;var c;var j;var k;var a;var m;var n;var o;var p;var q;var r;var s;var v;this.I_pV=s=this.A;this.I_p1=s;this.I_p2=s;u=this._;l=true;a:while(l===true){l=false;f=true;g:while(f===true){f=false;w=this._;g=true;b:while(g===true){g=false;if(!d(this,b.g_v,97,259)){break b}h=true;f:while(h===true){h=false;x=this._;i=true;c:while(i===true){i=false;if(!e(this,b.g_v,97,259)){break c}d:while(true){c=true;e:while(c===true){c=false;if(!d(this,b.g_v,97,259)){break e}break d}if(this._>=this.A){break c}this._++}break f}this._=x;if(!d(this,b.g_v,97,259)){break b}c:while(true){j=true;d:while(j===true){j=false;if(!e(this,b.g_v,97,259)){break d}break c}if(this._>=this.A){break b}this._++}}break g}this._=w;if(!e(this,b.g_v,97,259)){break a}k=true;c:while(k===true){k=false;y=this._;a=true;b:while(a===true){a=false;if(!e(this,b.g_v,97,259)){break b}e:while(true){m=true;d:while(m===true){m=false;if(!d(this,b.g_v,97,259)){break d}break e}if(this._>=this.A){break b}this._++}break c}this._=y;if(!d(this,b.g_v,97,259)){break a}if(this._>=this.A){break a}this._++}}this.I_pV=this._}v=this._=u;t=v;n=true;a:while(n===true){n=false;b:while(true){o=true;c:while(o===true){o=false;if(!d(this,b.g_v,97,259)){break c}break b}if(this._>=this.A){break a}this._++}b:while(true){p=true;c:while(p===true){p=false;if(!e(this,b.g_v,97,259)){break c}break b}if(this._>=this.A){break a}this._++}this.I_p1=this._;b:while(true){q=true;c:while(q===true){q=false;if(!d(this,b.g_v,97,259)){break c}break b}if(this._>=this.A){break a}this._++}c:while(true){r=true;b:while(r===true){r=false;if(!e(this,b.g_v,97,259)){break b}break c}if(this._>=this.A){break a}this._++}this.I_p2=this._}this._=t;return true};b.prototype.r_mark_regions=b.prototype.U;function H(a){var x;var y;var z;var u;var v;var l;var f;var g;var h;var i;var j;var k;var c;var m;var n;var o;var p;var q;var r;var s;var t;var w;a.I_pV=t=a.A;a.I_p1=t;a.I_p2=t;x=a._;l=true;a:while(l===true){l=false;f=true;g:while(f===true){f=false;y=a._;g=true;b:while(g===true){g=false;if(!d(a,b.g_v,97,259)){break b}h=true;f:while(h===true){h=false;z=a._;i=true;c:while(i===true){i=false;if(!e(a,b.g_v,97,259)){break c}d:while(true){j=true;e:while(j===true){j=false;if(!d(a,b.g_v,97,259)){break e}break d}if(a._>=a.A){break c}a._++}break f}a._=z;if(!d(a,b.g_v,97,259)){break b}c:while(true){k=true;d:while(k===true){k=false;if(!e(a,b.g_v,97,259)){break d}break c}if(a._>=a.A){break b}a._++}}break g}a._=y;if(!e(a,b.g_v,97,259)){break a}c=true;c:while(c===true){c=false;u=a._;m=true;b:while(m===true){m=false;if(!e(a,b.g_v,97,259)){break b}e:while(true){n=true;d:while(n===true){n=false;if(!d(a,b.g_v,97,259)){break d}break e}if(a._>=a.A){break b}a._++}break c}a._=u;if(!d(a,b.g_v,97,259)){break a}if(a._>=a.A){break a}a._++}}a.I_pV=a._}w=a._=x;v=w;o=true;a:while(o===true){o=false;b:while(true){p=true;c:while(p===true){p=false;if(!d(a,b.g_v,97,259)){break c}break b}if(a._>=a.A){break a}a._++}b:while(true){q=true;c:while(q===true){q=false;if(!e(a,b.g_v,97,259)){break c}break b}if(a._>=a.A){break a}a._++}a.I_p1=a._;b:while(true){r=true;c:while(r===true){r=false;if(!d(a,b.g_v,97,259)){break c}break b}if(a._>=a.A){break a}a._++}c:while(true){s=true;b:while(s===true){s=false;if(!e(a,b.g_v,97,259)){break b}break c}if(a._>=a.A){break a}a._++}a.I_p2=a._}a._=v;return true};b.prototype.V=function(){var a;var e;var d;b:while(true){e=this._;d=true;a:while(d===true){d=false;this.B=this._;a=q(this,b.a_0,3);if(a===0){break a}this.C=this._;switch(a){case 0:break a;case 1:if(!c(this,'i')){return false}break;case 2:if(!c(this,'u')){return false}break;case 3:if(this._>=this.A){break a}this._++;break}continue b}this._=e;break b}return true};b.prototype.r_postlude=b.prototype.V;function I(a){var d;var f;var e;b:while(true){f=a._;e=true;a:while(e===true){e=false;a.B=a._;d=q(a,b.a_0,3);if(d===0){break a}a.C=a._;switch(d){case 0:break a;case 1:if(!c(a,'i')){return false}break;case 2:if(!c(a,'u')){return false}break;case 3:if(a._>=a.A){break a}a._++;break}continue b}a._=f;break b}return true};b.prototype.S=function(){return!(this.I_pV<=this._)?false:true};b.prototype.r_RV=b.prototype.S;b.prototype.Q=function(){return!(this.I_p1<=this._)?false:true};b.prototype.r_R1=b.prototype.Q;b.prototype.R=function(){return!(this.I_p2<=this._)?false:true};b.prototype.r_R2=b.prototype.R;b.prototype.Y=function(){var a;var e;var d;var g;this.C=this._;a=f(this,b.a_1,16);if(a===0){return false}this.B=g=this._;if(!(!(this.I_p1<=g)?false:true)){return false}switch(a){case 0:return false;case 1:if(!c(this,'')){return false}break;case 2:if(!c(this,'a')){return false}break;case 3:if(!c(this,'e')){return false}break;case 4:if(!c(this,'i')){return false}break;case 5:e=this.A-this._;d=true;a:while(d===true){d=false;if(!i(this,2,'ab')){break a}return false}this._=this.A-e;if(!c(this,'i')){return false}break;case 6:if(!c(this,'at')){return false}break;case 7:if(!c(this,'aţi')){return false}break}return true};b.prototype.r_step_0=b.prototype.Y;function J(a){var d;var g;var e;var h;a.C=a._;d=f(a,b.a_1,16);if(d===0){return false}a.B=h=a._;if(!(!(a.I_p1<=h)?false:true)){return false}switch(d){case 0:return false;case 1:if(!c(a,'')){return false}break;case 2:if(!c(a,'a')){return false}break;case 3:if(!c(a,'e')){return false}break;case 4:if(!c(a,'i')){return false}break;case 5:g=a.A-a._;e=true;a:while(e===true){e=false;if(!i(a,2,'ab')){break a}return false}a._=a.A-g;if(!c(a,'i')){return false}break;case 6:if(!c(a,'at')){return false}break;case 7:if(!c(a,'aţi')){return false}break}return true};b.prototype.T=function(){var a;var d;var e;var g;d=this.A-(e=this._);this.C=e;a=f(this,b.a_2,46);if(a===0){return false}this.B=g=this._;if(!(!(this.I_p1<=g)?false:true)){return false}switch(a){case 0:return false;case 1:if(!c(this,'abil')){return false}break;case 2:if(!c(this,'ibil')){return false}break;case 3:if(!c(this,'iv')){return false}break;case 4:if(!c(this,'ic')){return false}break;case 5:if(!c(this,'at')){return false}break;case 6:if(!c(this,'it')){return false}break}this.B_standard_suffix_removed=true;this._=this.A-d;return true};b.prototype.r_combo_suffix=b.prototype.T;function o(a){var d;var e;var g;var h;e=a.A-(g=a._);a.C=g;d=f(a,b.a_2,46);if(d===0){return false}a.B=h=a._;if(!(!(a.I_p1<=h)?false:true)){return false}switch(d){case 0:return false;case 1:if(!c(a,'abil')){return false}break;case 2:if(!c(a,'ibil')){return false}break;case 3:if(!c(a,'iv')){return false}break;case 4:if(!c(a,'ic')){return false}break;case 5:if(!c(a,'at')){return false}break;case 6:if(!c(a,'it')){return false}break}a.B_standard_suffix_removed=true;a._=a.A-e;return true};b.prototype.X=function(){var a;var e;var d;var g;this.B_standard_suffix_removed=false;a:while(true){e=this.A-this._;d=true;b:while(d===true){d=false;if(!o(this)){break b}continue a}this._=this.A-e;break a}this.C=this._;a=f(this,b.a_3,62);if(a===0){return false}this.B=g=this._;if(!(!(this.I_p2<=g)?false:true)){return false}switch(a){case 0:return false;case 1:if(!c(this,'')){return false}break;case 2:if(!i(this,1,'ţ')){return false}this.B=this._;if(!c(this,'t')){return false}break;case 3:if(!c(this,'ist')){return false}break}this.B_standard_suffix_removed=true;return true};b.prototype.r_standard_suffix=b.prototype.X;function K(a){var d;var g;var e;var h;a.B_standard_suffix_removed=false;a:while(true){g=a.A-a._;e=true;b:while(e===true){e=false;if(!o(a)){break b}continue a}a._=a.A-g;break a}a.C=a._;d=f(a,b.a_3,62);if(d===0){return false}a.B=h=a._;if(!(!(a.I_p2<=h)?false:true)){return false}switch(d){case 0:return false;case 1:if(!c(a,'')){return false}break;case 2:if(!i(a,1,'ţ')){return false}a.B=a._;if(!c(a,'t')){return false}break;case 3:if(!c(a,'ist')){return false}break}a.B_standard_suffix_removed=true;return true};b.prototype.Z=function(){var d;var h;var a;var j;var e;var g;var k;var l;var m;h=this.A-(k=this._);if(k=b.A){return false}a=b.E.charCodeAt(b._);if(a>e||a>>3]&1<<(a&7))===0){return false}b._++;return true};function f(b,d,c,e){var a;if(b._<=b.D){return false}a=b.E.charCodeAt(b._-1);if(a>e||a>>3]&1<<(a&7))===0){return false}b._--;return true};function t(a,d,c,e){var b;if(a._<=a.D){return false}b=a.E.charCodeAt(a._-1);if(b>e||b>>3]&1<<(b&7))===0){a._--;return true}return false};function s(a,b,d){var c;if(a.A-a._>1);h=0;c=f=0;j--){if(e-c===n){h=-1;break}h=d.E.charCodeAt(e-1-c)-a.A_.charCodeAt(j);if(h!==0){break}c++}if(h<0){g=i;k=c}else{b=i;f=c}if(g-b<=1){if(b>0){break}if(g===b){break}if(l){break}l=true}}while(true){a=m[b];if(f>=a.G){d._=e-a.G|0;if(a.I==null){return a.J}o=a.I(d);d._=e-a.G|0;if(o){return a.J}}b=a.D_;if(b<0){return 0}}return-1};function n(a,b,d,e){var c;c=e.length-(d-b);a.E=a.E.slice(0,b)+e+a.E.slice(d);a.A+=c|0;if(a._>=d){a._+=c|0}else if(a._>b){a._=b}return c|0};function e(a,f){var b;var c;var d;var e;b=false;if((c=a.B)<0||c>(d=a.C)||d>(e=a.A)||e>a.E.length?false:true){n(a,a.B,a.C,f);b=true}return b};m.prototype.H=function(){return false};m.prototype.B_=function(b){var a;var c;var d;var e;a=this.F['.'+b];if(a==null){c=this.E=b;d=this._=0;e=this.A=c.length;this.D=0;this.B=d;this.C=e;this.H();a=this.E;this.F['.'+b]=a}return a};m.prototype.stemWord=m.prototype.B_;m.prototype.C_=function(e){var d;var b;var c;var a;var f;var g;var h;d=[];for(b=0;b=this.A){break b}this._++}b--;continue a}this._=f;break a}if(b>0){return false}this._=e;return true};a.prototype.r_more_than_one_syllable_word=a.prototype.v;function N(b){var f;var g;var c;var d;var e;f=b._;c=2;a:while(true){g=b._;d=true;b:while(d===true){d=false;c:while(true){e=true;d:while(e===true){e=false;if(!v(b,a.g_vowel,97,305)){break d}break c}if(b._>=b.A){break b}b._++}c--;continue a}b._=g;break a}if(c>0){return false}b._=f;return true};a.prototype.P=function(){var f;var g;var h;var b;var a;var c;var d;var i;var j;var e;b=true;b:while(b===true){b=false;f=this._;a=true;a:while(a===true){a=false;g=this._;c:while(true){c=true;d:while(c===true){c=false;if(!s(this,2,'ad')){break d}break c}if(this._>=this.A){break a}this._++}i=this.I_strlen=2;if(!(i===this.A)){break a}this._=g;break b}j=this._=f;h=j;a:while(true){d=true;c:while(d===true){d=false;if(!s(this,5,'soyad')){break c}break a}if(this._>=this.A){return false}this._++}e=this.I_strlen=5;if(!(e===this.A)){return false}this._=h}return true};a.prototype.r_is_reserved_word=a.prototype.P;function x(a){var g;var h;var i;var c;var b;var d;var e;var j;var k;var f;c=true;b:while(c===true){c=false;g=a._;b=true;a:while(b===true){b=false;h=a._;c:while(true){d=true;d:while(d===true){d=false;if(!s(a,2,'ad')){break d}break c}if(a._>=a.A){break a}a._++}j=a.I_strlen=2;if(!(j===a.A)){break a}a._=h;break b}k=a._=g;i=k;a:while(true){e=true;c:while(e===true){e=false;if(!s(a,5,'soyad')){break c}break a}if(a._>=a.A){return false}a._++}f=a.I_strlen=5;if(!(f===a.A)){return false}a._=i}return true};a.prototype.x=function(){var d;var e;var a;var b;var c;var f;var g;var h;d=this._;a=true;a:while(a===true){a=false;if(!x(this)){break a}return false}f=this._=d;this.D=f;h=this._=g=this.A;e=g-h;b=true;a:while(b===true){b=false;if(!z(this)){break a}}this._=this.A-e;c=true;a:while(c===true){c=false;if(!w(this)){break a}}this._=this.D;return true};a.prototype.r_postlude=a.prototype.x;function O(a){var e;var f;var b;var c;var d;var g;var h;var i;e=a._;b=true;a:while(b===true){b=false;if(!x(a)){break a}return false}g=a._=e;a.D=g;i=a._=h=a.A;f=h-i;c=true;a:while(c===true){c=false;if(!z(a)){break a}}a._=a.A-f;d=true;a:while(d===true){d=false;if(!w(a)){break a}}a._=a.D;return true};a.prototype.H=function(){var c;var a;var b;var d;var e;if(!N(this)){return false}this.D=this._;e=this._=d=this.A;c=d-e;a=true;a:while(a===true){a=false;if(!J(this)){break a}}this._=this.A-c;if(!this.B_continue_stemming_noun_suffixes){return false}b=true;a:while(b===true){b=false;if(!L(this)){break a}}this._=this.D;return!O(this)?false:true};a.prototype.stem=a.prototype.H;a.prototype.L=function(b){return b instanceof a};a.prototype.equals=a.prototype.L;a.prototype.M=function(){var c;var a;var b;var d;c='TurkishStemmer';a=0;for(b=0;b Any - val = getattr(self, option) - if val is None: - setattr(self, option, default) - return default - elif not isinstance(val, str): - raise DistutilsOptionError("'%s' must be a %s (got `%s`)" - % (option, what, val)) - return val - def finalize_options(self): # type: () -> None self.ensure_string_list('builder') diff --git a/sphinx/templates/graphviz/graphviz.css b/sphinx/templates/graphviz/graphviz.css index 8ab69e014..b340734c7 100644 --- a/sphinx/templates/graphviz/graphviz.css +++ b/sphinx/templates/graphviz/graphviz.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- graphviz extension. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/templates/latex/latex.tex_t b/sphinx/templates/latex/latex.tex_t index 5082254e7..e288dda93 100644 --- a/sphinx/templates/latex/latex.tex_t +++ b/sphinx/templates/latex/latex.tex_t @@ -17,6 +17,9 @@ %% turn off hyperref patch of \index as sphinx.xdy xindy module takes care of %% suitable \hyperpage mark-up, working around hyperref-xindy incompatibility \PassOptionsToPackage{hyperindex=false}{hyperref} +%% memoir class requires extra handling +\makeatletter\@ifclassloaded{memoir} +{\ifdefined\memhyperindexfalse\memhyperindexfalse\fi}{}\makeatother <% endif -%> <%= passoptionstopackages %> \PassOptionsToPackage{warn}{textcomp} diff --git a/sphinx/templates/quickstart/Makefile_t b/sphinx/templates/quickstart/Makefile_t index 7b656dff0..af9c56304 100644 --- a/sphinx/templates/quickstart/Makefile_t +++ b/sphinx/templates/quickstart/Makefile_t @@ -49,7 +49,7 @@ help: .PHONY: clean clean: - rm -rf $(BUILDDIR)/* + rm -rf $(BUILDDIR) .PHONY: latexpdf latexpdf: diff --git a/sphinx/testing/__init__.py b/sphinx/testing/__init__.py index dd9f32462..67263eff9 100644 --- a/sphinx/testing/__init__.py +++ b/sphinx/testing/__init__.py @@ -9,6 +9,6 @@ pytest_plugins = 'sphinx.testing.fixtures' - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/testing/comparer.py b/sphinx/testing/comparer.py index ddd818394..642cc8444 100644 --- a/sphinx/testing/comparer.py +++ b/sphinx/testing/comparer.py @@ -4,7 +4,7 @@ Sphinx test comparer for pytest - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import difflib diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py index 9197014bf..1cf66283f 100644 --- a/sphinx/testing/fixtures.py +++ b/sphinx/testing/fixtures.py @@ -4,7 +4,7 @@ Sphinx test fixtures for pytest - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -21,6 +21,20 @@ import pytest from sphinx.testing import util from sphinx.testing.util import SphinxTestApp, SphinxTestAppWrapperForSkipBuilding +DEFAULT_ENABLED_MARKERS = [ + ( + 'sphinx(builder, testroot=None, freshenv=False, confoverrides=None, tags=None,' + ' docutilsconf=None, parallel=0): arguments to initialize the sphinx test application.' + ), + 'test_params(shared_result=...): test parameters.', +] + + +def pytest_configure(config): + # register custom markers + for marker in DEFAULT_ENABLED_MARKERS: + config.addinivalue_line('markers', marker) + @pytest.fixture(scope='session') def rootdir() -> str: @@ -236,3 +250,15 @@ def tempdir(tmpdir: str) -> "util.path": this fixture is for compat with old test implementation. """ return util.path(tmpdir) + + +@pytest.fixture +def rollback_sysmodules(): + """Rollback sys.modules to before testing to unload modules during tests.""" + try: + sysmodules = list(sys.modules) + yield + finally: + for modname in list(sys.modules): + if modname not in sysmodules: + sys.modules.pop(modname) diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index 4c3702e3d..efc5e8f63 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -2,7 +2,7 @@ sphinx.testing.path ~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -11,11 +11,10 @@ import os import shutil import sys import warnings -from typing import Any, Callable, IO, List +from typing import IO, Any, Callable, List from sphinx.deprecation import RemovedInSphinx50Warning - FILESYSTEMENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding() diff --git a/sphinx/testing/restructuredtext.py b/sphinx/testing/restructuredtext.py index b25d7f98a..bb095b410 100644 --- a/sphinx/testing/restructuredtext.py +++ b/sphinx/testing/restructuredtext.py @@ -2,7 +2,7 @@ sphinx.testing.restructuredtext ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index c0dd1feac..7d0cde143 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -4,7 +4,7 @@ Sphinx test suite utilities - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import functools @@ -13,7 +13,7 @@ import re import sys import warnings from io import StringIO -from typing import Any, Dict, Generator, IO, List, Pattern +from typing import IO, Any, Dict, Generator, List, Pattern from xml.etree import ElementTree from docutils import nodes @@ -26,7 +26,6 @@ from sphinx.pycode import ModuleAnalyzer from sphinx.testing.path import path from sphinx.util.osutil import relpath - __all__ = [ 'Struct', 'SphinxTestApp', 'SphinxTestAppWrapperForSkipBuilding', diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index b6692cfb8..c66e9a548 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -1,9 +1,9 @@ \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{footnotehyper-sphinx}% - [2017/10/27 v1.7 hyperref aware footnote.sty for sphinx (JFB)] + [2021/01/26 v1.1b hyperref aware footnote.sty for sphinx (JFB)] %% %% Package: footnotehyper-sphinx -%% Version: based on footnotehyper.sty 2017/03/07 v1.0 +%% Version: based on footnotehyper.sty 2021/01/26 v1.1b %% as available at https://www.ctan.org/pkg/footnotehyper %% License: the one applying to Sphinx %% @@ -16,7 +16,7 @@ %% 3. use of \sphinxunactivateextrasandspace from sphinx.sty, %% 4. macro definition \sphinxfootnotemark, %% 5. macro definition \sphinxlongtablepatch -%% 6. replaced an \undefined by \@undefined +%% 6. replaced some \undefined by \@undefined \DeclareOption*{\PackageWarning{footnotehyper-sphinx}{Option `\CurrentOption' is unknown}}% \ProcessOptions\relax \newbox\FNH@notes @@ -206,9 +206,20 @@ \FNH@@@1.2!3?4,\FNH@@@\relax }% \long\def\FNH@check@a #11.2!3?4,#2\FNH@@@#3{% - \ifx\relax#3\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi - \FNH@bad@makefntext@alert - {\def\FNH@prefntext{#1}\def\FNH@postfntext{#2}\FNH@check@b}% + \ifx\relax#3\FNH@bad@makefntext@alert + \else + \edef\FNH@restore@{\catcode`\noexpand\@\the\catcode`\@\relax}% + \makeatletter + \ifx\@makefntextFB\@undefined + \expandafter\@gobble\else\expandafter\@firstofone\fi + {\@ifclassloaded{memoir}% + {\ifFBFrenchFootnotes\expandafter\@gobble\fi}% + {}}% + \@secondoftwo + \scantokens{\def\FNH@prefntext{#1}\def\FNH@postfntext{#2}}% + \FNH@restore@ + \expandafter\FNH@check@b + \fi }% \def\FNH@check@b #1\relax{% \expandafter\expandafter\expandafter\FNH@check@c diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 2b83ab85b..eb5617dda 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -6,7 +6,7 @@ % \NeedsTeXFormat{LaTeX2e}[1995/12/01] -\ProvidesPackage{sphinx}[2019/09/02 v2.3.0 LaTeX package (Sphinx markup)] +\ProvidesPackage{sphinx}[2021/01/23 v3.5.0 LaTeX package (Sphinx markup)] % provides \ltx@ifundefined % (many packages load ltxcmds: graphicx does for pdftex and lualatex but @@ -239,6 +239,8 @@ \ltx@ifundefined{@removefromreset} {\RequirePackage{remreset}} {}% avoid warning +% To support hlist directive +\RequirePackage{multicol} % to make pdf with correct encoded bookmarks in Japanese % this should precede the hyperref package \ifx\kanjiskip\@undefined @@ -410,6 +412,11 @@ \DisableKeyvalOption{sphinx}{numfigreset} \DisableKeyvalOption{sphinx}{nonumfigreset} \DisableKeyvalOption{sphinx}{mathnumfig} +% To allow hyphenation of first word in narrow contexts; no option, +% customization to be done via 'preamble' key +\newcommand*\sphinxAtStartPar{\nobreak\hskip\z@skip} +% No need for the \hspace{0pt} trick (\hskip\z@skip) with luatex +\ifdefined\directlua\let\sphinxAtStartPar\@empty\fi % user interface: options can be changed midway in a document! \newcommand\sphinxsetup[1]{\setkeys{sphinx}{#1}} @@ -588,12 +595,14 @@ {% classes with \chapter command \fancypagestyle{normal}{ \fancyhf{} - % FIXME: this presupposes "twoside". - % If "oneside" class option, there are warnings in LaTeX log. - \fancyfoot[LE,RO]{{\py@HeaderFamily\thepage}} + \fancyfoot[RO]{{\py@HeaderFamily\thepage}} \fancyfoot[LO]{{\py@HeaderFamily\nouppercase{\rightmark}}} - \fancyfoot[RE]{{\py@HeaderFamily\nouppercase{\leftmark}}} - \fancyhead[LE,RO]{{\py@HeaderFamily \@title\sphinxheadercomma\py@release}} + \fancyhead[RO]{{\py@HeaderFamily \@title\sphinxheadercomma\py@release}} + \if@twoside + \fancyfoot[LE]{{\py@HeaderFamily\thepage}} + \fancyfoot[RE]{{\py@HeaderFamily\nouppercase{\leftmark}}} + \fancyhead[LE]{{\py@HeaderFamily \@title\sphinxheadercomma\py@release}} + \fi \renewcommand{\headrulewidth}{0.4pt} \renewcommand{\footrulewidth}{0.4pt} % define chaptermark with \@chappos when \@chappos is available for Japanese @@ -605,7 +614,8 @@ % page of a chapter `clean.' \fancypagestyle{plain}{ \fancyhf{} - \fancyfoot[LE,RO]{{\py@HeaderFamily\thepage}} + \fancyfoot[RO]{{\py@HeaderFamily\thepage}} + \if@twoside\fancyfoot[LE]{{\py@HeaderFamily\thepage}}\fi \renewcommand{\headrulewidth}{0pt} \renewcommand{\footrulewidth}{0.4pt} } @@ -706,17 +716,17 @@ % Augment the sectioning commands used to get our own font family in place, % and reset some internal data items (\titleformat from titlesec package) \titleformat{\section}{\Large\py@HeaderFamily}% - {\py@TitleColor\thesection}{0.5em}{\py@TitleColor}{\py@NormalColor} + {\py@TitleColor\thesection}{0.5em}{\py@TitleColor} \titleformat{\subsection}{\large\py@HeaderFamily}% - {\py@TitleColor\thesubsection}{0.5em}{\py@TitleColor}{\py@NormalColor} + {\py@TitleColor\thesubsection}{0.5em}{\py@TitleColor} \titleformat{\subsubsection}{\py@HeaderFamily}% - {\py@TitleColor\thesubsubsection}{0.5em}{\py@TitleColor}{\py@NormalColor} + {\py@TitleColor\thesubsubsection}{0.5em}{\py@TitleColor} % By default paragraphs (and subsubsections) will not be numbered because % sphinxmanual.cls and sphinxhowto.cls set secnumdepth to 2 \titleformat{\paragraph}{\py@HeaderFamily}% - {\py@TitleColor\theparagraph}{0.5em}{\py@TitleColor}{\py@NormalColor} + {\py@TitleColor\theparagraph}{0.5em}{\py@TitleColor} \titleformat{\subparagraph}{\py@HeaderFamily}% - {\py@TitleColor\thesubparagraph}{0.5em}{\py@TitleColor}{\py@NormalColor} + {\py@TitleColor\thesubparagraph}{0.5em}{\py@TitleColor} %% GRAPHICS @@ -734,7 +744,7 @@ \AtBeginDocument{\spx@image@maxheight\textheight} % box scratch register -\newdimen\spx@image@box +\newbox\spx@image@box \newcommand*{\sphinxsafeincludegraphics}[2][]{% % #1 contains possibly width=, height=, but no scale= since 1.8.4 \setbox\spx@image@box\hbox{\includegraphics[#1,draft]{#2}}% @@ -841,6 +851,12 @@ %% NUMBERING OF FIGURES, TABLES, AND LITERAL BLOCKS + +% Everything is delayed to \begin{document} to allow hyperref patches into +% \newcounter to solve duplicate label problems for internal hyperlinks to +% code listings (literalblock counter). User or extension re-definitions of +% \theliteralblock, et al., thus have also to be delayed. (changed at 3.5.0) +\AtBeginDocument{% \ltx@ifundefined{c@chapter} {\newcounter{literalblock}}% {\newcounter{literalblock}[chapter]% @@ -881,62 +897,52 @@ \g@addto@macro\spx@preBthefigure{\fi}}% \fi \ifnum\spx@opt@numfigreset>1 - \AtBeginDocument{% \@addtoreset{figure}{section}% \@addtoreset{table}{section}% \@addtoreset{literalblock}{section}% \ifspx@opt@mathnumfig \@addtoreset{equation}{section}% \fi% - }% \g@addto@macro\spx@preAthefigure{\ifnum\c@section>\z@\arabic{section}.}% \g@addto@macro\spx@preBthefigure{\fi}% \fi \ifnum\spx@opt@numfigreset>2 - \AtBeginDocument{% \@addtoreset{figure}{subsection}% \@addtoreset{table}{subsection}% \@addtoreset{literalblock}{subsection}% \ifspx@opt@mathnumfig \@addtoreset{equation}{subsection}% \fi% - }% \g@addto@macro\spx@preAthefigure{\ifnum\c@subsection>\z@\arabic{subsection}.}% \g@addto@macro\spx@preBthefigure{\fi}% \fi \ifnum\spx@opt@numfigreset>3 - \AtBeginDocument{% \@addtoreset{figure}{subsubsection}% \@addtoreset{table}{subsubsection}% \@addtoreset{literalblock}{subsubsection}% \ifspx@opt@mathnumfig \@addtoreset{equation}{subsubsection}% \fi% - }% \g@addto@macro\spx@preAthefigure{\ifnum\c@subsubsection>\z@\arabic{subsubsection}.}% \g@addto@macro\spx@preBthefigure{\fi}% \fi \ifnum\spx@opt@numfigreset>4 - \AtBeginDocument{% \@addtoreset{figure}{paragraph}% \@addtoreset{table}{paragraph}% \@addtoreset{literalblock}{paragraph}% \ifspx@opt@mathnumfig \@addtoreset{equation}{paragraph}% \fi% - }% \g@addto@macro\spx@preAthefigure{\ifnum\c@subparagraph>\z@\arabic{subparagraph}.}% \g@addto@macro\spx@preBthefigure{\fi}% \fi \ifnum\spx@opt@numfigreset>5 - \AtBeginDocument{% \@addtoreset{figure}{subparagraph}% \@addtoreset{table}{subparagraph}% \@addtoreset{literalblock}{subparagraph}% \ifspx@opt@mathnumfig \@addtoreset{equation}{subparagraph}% \fi% - }% \g@addto@macro\spx@preAthefigure{\ifnum\c@subsubparagraph>\z@\arabic{subsubparagraph}.}% \g@addto@macro\spx@preBthefigure{\fi}% \fi @@ -953,7 +959,7 @@ \g@addto@macro\theequation{\arabic{equation}}% \fi \fi - +}% end of big \AtBeginDocument %% LITERAL BLOCKS % @@ -963,9 +969,9 @@ % - with possibly of a top caption, non-separable by pagebreak. % - and usable inside tables or footnotes ("footnotehyper-sphinx"). -% For extensions which use \OriginalVerbatim and compatibility with Sphinx < -% 1.5, we define and use these when (unmodified) Verbatim will be needed. But -% Sphinx >= 1.5 does not modify the \Verbatim macro anymore. +% Prior to Sphinx 1.5, \Verbatim and \endVerbatim were modified by Sphinx. +% The aliases defined here are used in sphinxVerbatim environment and can +% serve as hook-points with no need to modify \Verbatim itself. \let\OriginalVerbatim \Verbatim \let\endOriginalVerbatim\endVerbatim @@ -1860,8 +1866,8 @@ \def\sphinxstyleindexextra #1{ (\emph{#1})} \def\sphinxstyleindexpageref #1{, \pageref{#1}} \def\sphinxstyleindexpagemain#1{\textbf{#1}} -\protected\def\spxentry#1{#1}% will get \let to \sphinxstyleindexentry in index -\protected\def\spxextra#1{#1}% will get \let to \sphinxstyleindexextra in index +\def\spxentry{\@backslashchar spxentry}% let to \sphinxstyleindexentry in index +\def\spxextra{\@backslashchar spxextra}% let to \sphinxstyleindexextra in index \def\sphinxstyleindexlettergroup #1% {{\Large\sffamily#1}\nopagebreak\vspace{1mm}} \def\sphinxstyleindexlettergroupDefault #1% diff --git a/sphinx/themes/agogo/layout.html b/sphinx/themes/agogo/layout.html index 26c37b10e..1d9df693b 100644 --- a/sphinx/themes/agogo/layout.html +++ b/sphinx/themes/agogo/layout.html @@ -5,7 +5,7 @@ Sphinx layout template for the agogo theme, originally written by Andi Albrecht. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- extends "basic/layout.html" %} diff --git a/sphinx/themes/agogo/static/agogo.css_t b/sphinx/themes/agogo/static/agogo.css_t index ff43186da..489ec17ea 100644 --- a/sphinx/themes/agogo/static/agogo.css_t +++ b/sphinx/themes/agogo/static/agogo.css_t @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- agogo theme. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/basic/defindex.html b/sphinx/themes/basic/defindex.html index 2f8477c31..8ca5748ac 100644 --- a/sphinx/themes/basic/defindex.html +++ b/sphinx/themes/basic/defindex.html @@ -4,7 +4,7 @@ Default template for the "index" page. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #}{{ warn('Now base template defindex.html is deprecated.') }} {%- extends "layout.html" %} diff --git a/sphinx/themes/basic/domainindex.html b/sphinx/themes/basic/domainindex.html index f4f742338..1ccab0286 100644 --- a/sphinx/themes/basic/domainindex.html +++ b/sphinx/themes/basic/domainindex.html @@ -4,7 +4,7 @@ Template for domain indices (module index, ...). - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- extends "layout.html" %} diff --git a/sphinx/themes/basic/genindex-single.html b/sphinx/themes/basic/genindex-single.html index b327eb620..1182fdada 100644 --- a/sphinx/themes/basic/genindex-single.html +++ b/sphinx/themes/basic/genindex-single.html @@ -4,7 +4,7 @@ Template for a "single" page of a split index. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {% macro indexentries(firstname, links) %} diff --git a/sphinx/themes/basic/genindex-split.html b/sphinx/themes/basic/genindex-split.html index e9db8574d..8b79dc8dc 100644 --- a/sphinx/themes/basic/genindex-split.html +++ b/sphinx/themes/basic/genindex-split.html @@ -4,7 +4,7 @@ Template for a "split" index overview page. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- extends "layout.html" %} diff --git a/sphinx/themes/basic/genindex.html b/sphinx/themes/basic/genindex.html index fb769737f..7f16c3544 100644 --- a/sphinx/themes/basic/genindex.html +++ b/sphinx/themes/basic/genindex.html @@ -4,7 +4,7 @@ Template for an "all-in-one" index. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- extends "layout.html" %} diff --git a/sphinx/themes/basic/globaltoc.html b/sphinx/themes/basic/globaltoc.html index 1b2f9baee..a47c32cc0 100644 --- a/sphinx/themes/basic/globaltoc.html +++ b/sphinx/themes/basic/globaltoc.html @@ -4,7 +4,7 @@ Sphinx sidebar template: global table of contents. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #}

    {{ _('Table of Contents') }}

    diff --git a/sphinx/themes/basic/layout.html b/sphinx/themes/basic/layout.html index 9163a18a2..bd56681c0 100644 --- a/sphinx/themes/basic/layout.html +++ b/sphinx/themes/basic/layout.html @@ -4,7 +4,7 @@ Master layout template for Sphinx themes. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- block doctype -%}{%- if html5_doctype %} @@ -18,7 +18,7 @@ {%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and (sidebars != []) %} {%- set url_root = pathto('', 1) %} -{# XXX necessary? #} +{# URL root should never be #, then all links are fragments #} {%- if url_root == '#' %}{% set url_root = '' %}{% endif %} {%- if not embedded and docstitle %} {%- set titlesuffix = " — "|safe + docstitle|e %} @@ -88,15 +88,15 @@ {%- endmacro %} {%- macro script() %} - + {%- for js in script_files %} {{ js_tag(js) }} {%- endfor %} {%- endmacro %} {%- macro css() %} - + {%- for css in css_files %} {%- if css|attr("filename") %} {{ css_tag(css) }} diff --git a/sphinx/themes/basic/localtoc.html b/sphinx/themes/basic/localtoc.html index 0e1c5511b..c15ad2708 100644 --- a/sphinx/themes/basic/localtoc.html +++ b/sphinx/themes/basic/localtoc.html @@ -4,7 +4,7 @@ Sphinx sidebar template: local table of contents. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- if display_toc %} diff --git a/sphinx/themes/basic/page.html b/sphinx/themes/basic/page.html index c9c351167..5878bdeeb 100644 --- a/sphinx/themes/basic/page.html +++ b/sphinx/themes/basic/page.html @@ -4,7 +4,7 @@ Master template for simple pages. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- extends "layout.html" %} diff --git a/sphinx/themes/basic/relations.html b/sphinx/themes/basic/relations.html index 5a2113cf0..c4391fe4d 100644 --- a/sphinx/themes/basic/relations.html +++ b/sphinx/themes/basic/relations.html @@ -4,7 +4,7 @@ Sphinx sidebar template: relation links. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- if prev %} diff --git a/sphinx/themes/basic/search.html b/sphinx/themes/basic/search.html index 2673369f2..453a35fb9 100644 --- a/sphinx/themes/basic/search.html +++ b/sphinx/themes/basic/search.html @@ -4,7 +4,7 @@ Template for the search page. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- extends "layout.html" %} @@ -12,6 +12,7 @@ {%- block scripts %} {{ super() }} + {%- endblock %} {% block extrahead %} diff --git a/sphinx/themes/basic/searchbox.html b/sphinx/themes/basic/searchbox.html index 6ce202786..68f933f66 100644 --- a/sphinx/themes/basic/searchbox.html +++ b/sphinx/themes/basic/searchbox.html @@ -4,7 +4,7 @@ Sphinx sidebar template: quick search box. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- if pagename != "search" and builder != "singlehtml" %} diff --git a/sphinx/themes/basic/sourcelink.html b/sphinx/themes/basic/sourcelink.html index 2ff5d1b66..512611230 100644 --- a/sphinx/themes/basic/sourcelink.html +++ b/sphinx/themes/basic/sourcelink.html @@ -4,7 +4,7 @@ Sphinx sidebar template: "show source" link. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- if show_source and has_source and sourcename %} diff --git a/sphinx/themes/basic/static/basic.css_t b/sphinx/themes/basic/static/basic.css_t index 0a71a7a91..3bb3ea705 100644 --- a/sphinx/themes/basic/static/basic.css_t +++ b/sphinx/themes/basic/static/basic.css_t @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- basic theme. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -764,6 +764,7 @@ div.code-block-caption code { } table.highlighttable td.linenos, +span.linenos, div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */ user-select: none; } diff --git a/sphinx/themes/basic/static/doctools.js b/sphinx/themes/basic/static/doctools.js index daccd209d..61ac9d266 100644 --- a/sphinx/themes/basic/static/doctools.js +++ b/sphinx/themes/basic/static/doctools.js @@ -4,7 +4,7 @@ * * Sphinx JavaScript utilities for all documentation. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -29,9 +29,14 @@ if (!window.console || !console.firebug) { /** * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL */ jQuery.urldecode = function(x) { - return decodeURIComponent(x).replace(/\+/g, ' '); + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); }; /** @@ -285,9 +290,10 @@ var Documentation = { initOnKeyListeners: function() { $(document).keydown(function(event) { var activeElementType = document.activeElement.tagName; - // don't navigate when in search box or textarea + // don't navigate when in search box, textarea, dropdown or button if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' - && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) { + && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey + && !event.shiftKey) { switch (event.keyCode) { case 37: // left var prevHref = $('link[rel="prev"]').prop('href'); diff --git a/sphinx/themes/basic/static/language_data.js_t b/sphinx/themes/basic/static/language_data.js_t index 531813877..1425b022a 100644 --- a/sphinx/themes/basic/static/language_data.js_t +++ b/sphinx/themes/basic/static/language_data.js_t @@ -5,7 +5,7 @@ * This script contains the language-specific data used by searchtools.js, * namely the list of stopwords, stemmer, scorer and splitter. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 970d0d975..0dc528fb9 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -4,7 +4,7 @@ * * Sphinx JavaScript utilities for the full-text search. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -59,10 +59,10 @@ var Search = { _pulse_status : -1, htmlToText : function(htmlString) { - var htmlElement = document.createElement('span'); - htmlElement.innerHTML = htmlString; - $(htmlElement).find('.headerlink').remove(); - docContent = $(htmlElement).find('[role=main]')[0]; + var virtualDocument = document.implementation.createHTMLDocument('virtual'); + var htmlElement = $(htmlString, virtualDocument); + htmlElement.find('.headerlink').remove(); + docContent = htmlElement.find('[role=main]')[0]; if(docContent === undefined) { console.warn("Content block not found. Sphinx search tries to obtain it " + "via '[role=main]'. Could you check your theme or template."); @@ -379,6 +379,13 @@ var Search = { return results; }, + /** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + */ + escapeRegExp : function(string) { + return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + }, + /** * search for full-text terms in the index */ @@ -402,13 +409,14 @@ var Search = { ]; // add support for partial matches if (word.length > 2) { + var word_regex = this.escapeRegExp(word); for (var w in terms) { - if (w.match(word) && !terms[word]) { + if (w.match(word_regex) && !terms[word]) { _o.push({files: terms[w], score: Scorer.partialTerm}) } } for (var w in titleterms) { - if (w.match(word) && !titleterms[word]) { + if (w.match(word_regex) && !titleterms[word]) { _o.push({files: titleterms[w], score: Scorer.partialTitle}) } } diff --git a/sphinx/themes/basic/static/underscore-1.12.0.js b/sphinx/themes/basic/static/underscore-1.12.0.js new file mode 100644 index 000000000..3af6352e6 --- /dev/null +++ b/sphinx/themes/basic/static/underscore-1.12.0.js @@ -0,0 +1,2027 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define('underscore', factory) : + (global = global || self, (function () { + var current = global._; + var exports = global._ = factory(); + exports.noConflict = function () { global._ = current; return exports; }; + }())); +}(this, (function () { + // Underscore.js 1.12.0 + // https://underscorejs.org + // (c) 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + // Underscore may be freely distributed under the MIT license. + + // Current version. + var VERSION = '1.12.0'; + + // Establish the root object, `window` (`self`) in the browser, `global` + // on the server, or `this` in some virtual machines. We use `self` + // instead of `window` for `WebWorker` support. + var root = typeof self == 'object' && self.self === self && self || + typeof global == 'object' && global.global === global && global || + Function('return this')() || + {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype; + var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // Modern feature detection. + var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined', + supportsDataView = typeof DataView !== 'undefined'; + + // All **ECMAScript 5+** native function implementations that we hope to use + // are declared here. + var nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeCreate = Object.create, + nativeIsView = supportsArrayBuffer && ArrayBuffer.isView; + + // Create references to these builtin functions because we override them. + var _isNaN = isNaN, + _isFinite = isFinite; + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + // The largest integer that can be represented exactly. + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + + // Some functions take a variable number of arguments, or a few expected + // arguments at the beginning and then a variable number of values to operate + // on. This helper accumulates all remaining arguments past the function’s + // argument length (or an explicit `startIndex`), into an array that becomes + // the last argument. Similar to ES6’s "rest parameter". + function restArguments(func, startIndex) { + startIndex = startIndex == null ? func.length - 1 : +startIndex; + return function() { + var length = Math.max(arguments.length - startIndex, 0), + rest = Array(length), + index = 0; + for (; index < length; index++) { + rest[index] = arguments[index + startIndex]; + } + switch (startIndex) { + case 0: return func.call(this, rest); + case 1: return func.call(this, arguments[0], rest); + case 2: return func.call(this, arguments[0], arguments[1], rest); + } + var args = Array(startIndex + 1); + for (index = 0; index < startIndex; index++) { + args[index] = arguments[index]; + } + args[startIndex] = rest; + return func.apply(this, args); + }; + } + + // Is a given variable an object? + function isObject(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + } + + // Is a given value equal to null? + function isNull(obj) { + return obj === null; + } + + // Is a given variable undefined? + function isUndefined(obj) { + return obj === void 0; + } + + // Is a given value a boolean? + function isBoolean(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + } + + // Is a given value a DOM element? + function isElement(obj) { + return !!(obj && obj.nodeType === 1); + } + + // Internal function for creating a `toString`-based type tester. + function tagTester(name) { + var tag = '[object ' + name + ']'; + return function(obj) { + return toString.call(obj) === tag; + }; + } + + var isString = tagTester('String'); + + var isNumber = tagTester('Number'); + + var isDate = tagTester('Date'); + + var isRegExp = tagTester('RegExp'); + + var isError = tagTester('Error'); + + var isSymbol = tagTester('Symbol'); + + var isArrayBuffer = tagTester('ArrayBuffer'); + + var isFunction = tagTester('Function'); + + // Optimize `isFunction` if appropriate. Work around some `typeof` bugs in old + // v8, IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236). + var nodelist = root.document && root.document.childNodes; + if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') { + isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + var isFunction$1 = isFunction; + + var hasObjectTag = tagTester('Object'); + + // In IE 10 - Edge 13, `DataView` has string tag `'[object Object]'`. + // In IE 11, the most common among them, this problem also applies to + // `Map`, `WeakMap` and `Set`. + var hasStringTagBug = ( + supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8))) + ), + isIE11 = (typeof Map !== 'undefined' && hasObjectTag(new Map)); + + var isDataView = tagTester('DataView'); + + // In IE 10 - Edge 13, we need a different heuristic + // to determine whether an object is a `DataView`. + function ie10IsDataView(obj) { + return obj != null && isFunction$1(obj.getInt8) && isArrayBuffer(obj.buffer); + } + + var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView); + + // Is a given value an array? + // Delegates to ECMA5's native `Array.isArray`. + var isArray = nativeIsArray || tagTester('Array'); + + // Internal function to check whether `key` is an own property name of `obj`. + function has(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + } + + var isArguments = tagTester('Arguments'); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + (function() { + if (!isArguments(arguments)) { + isArguments = function(obj) { + return has(obj, 'callee'); + }; + } + }()); + + var isArguments$1 = isArguments; + + // Is a given object a finite number? + function isFinite$1(obj) { + return !isSymbol(obj) && _isFinite(obj) && !isNaN(parseFloat(obj)); + } + + // Is the given value `NaN`? + function isNaN$1(obj) { + return isNumber(obj) && _isNaN(obj); + } + + // Predicate-generating function. Often useful outside of Underscore. + function constant(value) { + return function() { + return value; + }; + } + + // Common internal logic for `isArrayLike` and `isBufferLike`. + function createSizePropertyCheck(getSizeProperty) { + return function(collection) { + var sizeProperty = getSizeProperty(collection); + return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX; + } + } + + // Internal helper to generate a function to obtain property `key` from `obj`. + function shallowProperty(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + } + + // Internal helper to obtain the `byteLength` property of an object. + var getByteLength = shallowProperty('byteLength'); + + // Internal helper to determine whether we should spend extensive checks against + // `ArrayBuffer` et al. + var isBufferLike = createSizePropertyCheck(getByteLength); + + // Is a given value a typed array? + var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/; + function isTypedArray(obj) { + // `ArrayBuffer.isView` is the most future-proof, so use it when available. + // Otherwise, fall back on the above regular expression. + return nativeIsView ? (nativeIsView(obj) && !isDataView$1(obj)) : + isBufferLike(obj) && typedArrayPattern.test(toString.call(obj)); + } + + var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false); + + // Internal helper to obtain the `length` property of an object. + var getLength = shallowProperty('length'); + + // Internal helper to create a simple lookup structure. + // `collectNonEnumProps` used to depend on `_.contains`, but this led to + // circular imports. `emulatedSet` is a one-off solution that only works for + // arrays of strings. + function emulatedSet(keys) { + var hash = {}; + for (var l = keys.length, i = 0; i < l; ++i) hash[keys[i]] = true; + return { + contains: function(key) { return hash[key]; }, + push: function(key) { + hash[key] = true; + return keys.push(key); + } + }; + } + + // Internal helper. Checks `keys` for the presence of keys in IE < 9 that won't + // be iterated by `for key in ...` and thus missed. Extends `keys` in place if + // needed. + function collectNonEnumProps(obj, keys) { + keys = emulatedSet(keys); + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = isFunction$1(constructor) && constructor.prototype || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (has(obj, prop) && !keys.contains(prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !keys.contains(prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys`. + function keys(obj) { + if (!isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (has(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + } + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + function isEmpty(obj) { + if (obj == null) return true; + // Skip the more expensive `toString`-based type checks if `obj` has no + // `.length`. + var length = getLength(obj); + if (typeof length == 'number' && ( + isArray(obj) || isString(obj) || isArguments$1(obj) + )) return length === 0; + return getLength(keys(obj)) === 0; + } + + // Returns whether an object has a given set of `key:value` pairs. + function isMatch(object, attrs) { + var _keys = keys(attrs), length = _keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = _keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + } + + // If Underscore is called as a function, it returns a wrapped object that can + // be used OO-style. This wrapper holds altered versions of all functions added + // through `_.mixin`. Wrapped objects may be chained. + function _(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + } + + _.VERSION = VERSION; + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxies for some methods used in engine operations + // such as arithmetic and JSON stringification. + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return String(this._wrapped); + }; + + // Internal function to wrap or shallow-copy an ArrayBuffer, + // typed array or DataView to a new view, reusing the buffer. + function toBufferView(bufferSource) { + return new Uint8Array( + bufferSource.buffer || bufferSource, + bufferSource.byteOffset || 0, + getByteLength(bufferSource) + ); + } + + // We use this string twice, so give it a name for minification. + var tagDataView = '[object DataView]'; + + // Internal recursive comparison function for `_.isEqual`. + function eq(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) return b !== b; + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); + } + + // Internal recursive comparison function for `_.isEqual`. + function deepEq(a, b, aStack, bStack) { + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + // Work around a bug in IE 10 - Edge 13. + if (hasStringTagBug && className == '[object Object]' && isDataView$1(a)) { + if (!isDataView$1(b)) return false; + className = tagDataView; + } + switch (className) { + // These types are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN. + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + case '[object Symbol]': + return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + case '[object ArrayBuffer]': + case tagDataView: + // Coerce to typed array so we can fall through. + return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); + } + + var areArrays = className === '[object Array]'; + if (!areArrays && isTypedArray$1(a)) { + var byteLength = getByteLength(a); + if (byteLength !== getByteLength(b)) return false; + if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; + areArrays = true; + } + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && + isFunction$1(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var _keys = keys(a), key; + length = _keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = _keys[length]; + if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + } + + // Perform a deep comparison to check if two objects are equal. + function isEqual(a, b) { + return eq(a, b); + } + + // Retrieve all the enumerable property names of an object. + function allKeys(obj) { + if (!isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + } + + // Since the regular `Object.prototype.toString` type tests don't work for + // some types in IE 11, we use a fingerprinting heuristic instead, based + // on the methods. It's not great, but it's the best we got. + // The fingerprint method lists are defined below. + function ie11fingerprint(methods) { + var length = getLength(methods); + return function(obj) { + if (obj == null) return false; + // `Map`, `WeakMap` and `Set` have no enumerable keys. + var keys = allKeys(obj); + if (getLength(keys)) return false; + for (var i = 0; i < length; i++) { + if (!isFunction$1(obj[methods[i]])) return false; + } + // If we are testing against `WeakMap`, we need to ensure that + // `obj` doesn't have a `forEach` method in order to distinguish + // it from a regular `Map`. + return methods !== weakMapMethods || !isFunction$1(obj[forEachName]); + }; + } + + // In the interest of compact minification, we write + // each string in the fingerprints only once. + var forEachName = 'forEach', + hasName = 'has', + commonInit = ['clear', 'delete'], + mapTail = ['get', hasName, 'set']; + + // `Map`, `WeakMap` and `Set` each have slightly different + // combinations of the above sublists. + var mapMethods = commonInit.concat(forEachName, mapTail), + weakMapMethods = commonInit.concat(mapTail), + setMethods = ['add'].concat(commonInit, forEachName, hasName); + + var isMap = isIE11 ? ie11fingerprint(mapMethods) : tagTester('Map'); + + var isWeakMap = isIE11 ? ie11fingerprint(weakMapMethods) : tagTester('WeakMap'); + + var isSet = isIE11 ? ie11fingerprint(setMethods) : tagTester('Set'); + + var isWeakSet = tagTester('WeakSet'); + + // Retrieve the values of an object's properties. + function values(obj) { + var _keys = keys(obj); + var length = _keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[_keys[i]]; + } + return values; + } + + // Convert an object into a list of `[key, value]` pairs. + // The opposite of `_.object` with one argument. + function pairs(obj) { + var _keys = keys(obj); + var length = _keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [_keys[i], obj[_keys[i]]]; + } + return pairs; + } + + // Invert the keys and values of an object. The values must be serializable. + function invert(obj) { + var result = {}; + var _keys = keys(obj); + for (var i = 0, length = _keys.length; i < length; i++) { + result[obj[_keys[i]]] = _keys[i]; + } + return result; + } + + // Return a sorted list of the function names available on the object. + function functions(obj) { + var names = []; + for (var key in obj) { + if (isFunction$1(obj[key])) names.push(key); + } + return names.sort(); + } + + // An internal function for creating assigner functions. + function createAssigner(keysFunc, defaults) { + return function(obj) { + var length = arguments.length; + if (defaults) obj = Object(obj); + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!defaults || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + } + + // Extend a given object with all the properties in passed-in object(s). + var extend = createAssigner(allKeys); + + // Assigns a given object with all the own properties in the passed-in + // object(s). + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + var extendOwn = createAssigner(keys); + + // Fill in a given object with default properties. + var defaults = createAssigner(allKeys, true); + + // Create a naked function reference for surrogate-prototype-swapping. + function ctor() { + return function(){}; + } + + // An internal function for creating a new object that inherits from another. + function baseCreate(prototype) { + if (!isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + var Ctor = ctor(); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + } + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + function create(prototype, props) { + var result = baseCreate(prototype); + if (props) extendOwn(result, props); + return result; + } + + // Create a (shallow-cloned) duplicate of an object. + function clone(obj) { + if (!isObject(obj)) return obj; + return isArray(obj) ? obj.slice() : extend({}, obj); + } + + // Invokes `interceptor` with the `obj` and then returns `obj`. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + function tap(obj, interceptor) { + interceptor(obj); + return obj; + } + + // Normalize a (deep) property `path` to array. + // Like `_.iteratee`, this function can be customized. + function toPath(path) { + return isArray(path) ? path : [path]; + } + _.toPath = toPath; + + // Internal wrapper for `_.toPath` to enable minification. + // Similar to `cb` for `_.iteratee`. + function toPath$1(path) { + return _.toPath(path); + } + + // Internal function to obtain a nested property in `obj` along `path`. + function deepGet(obj, path) { + var length = path.length; + for (var i = 0; i < length; i++) { + if (obj == null) return void 0; + obj = obj[path[i]]; + } + return length ? obj : void 0; + } + + // Get the value of the (deep) property on `path` from `object`. + // If any property in `path` does not exist or if the value is + // `undefined`, return `defaultValue` instead. + // The `path` is normalized through `_.toPath`. + function get(object, path, defaultValue) { + var value = deepGet(object, toPath$1(path)); + return isUndefined(value) ? defaultValue : value; + } + + // Shortcut function for checking if an object has a given property directly on + // itself (in other words, not on a prototype). Unlike the internal `has` + // function, this public version can also traverse nested properties. + function has$1(obj, path) { + path = toPath$1(path); + var length = path.length; + for (var i = 0; i < length; i++) { + var key = path[i]; + if (!has(obj, key)) return false; + obj = obj[key]; + } + return !!length; + } + + // Keep the identity function around for default iteratees. + function identity(value) { + return value; + } + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + function matcher(attrs) { + attrs = extendOwn({}, attrs); + return function(obj) { + return isMatch(obj, attrs); + }; + } + + // Creates a function that, when passed an object, will traverse that object’s + // properties down the given `path`, specified as an array of keys or indices. + function property(path) { + path = toPath$1(path); + return function(obj) { + return deepGet(obj, path); + }; + } + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + function optimizeCb(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + // The 2-argument case is omitted because we’re not using it. + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + } + + // An internal function to generate callbacks that can be applied to each + // element in a collection, returning the desired result — either `_.identity`, + // an arbitrary callback, a property matcher, or a property accessor. + function baseIteratee(value, context, argCount) { + if (value == null) return identity; + if (isFunction$1(value)) return optimizeCb(value, context, argCount); + if (isObject(value) && !isArray(value)) return matcher(value); + return property(value); + } + + // External wrapper for our callback generator. Users may customize + // `_.iteratee` if they want additional predicate/iteratee shorthand styles. + // This abstraction hides the internal-only `argCount` argument. + function iteratee(value, context) { + return baseIteratee(value, context, Infinity); + } + _.iteratee = iteratee; + + // The function we call internally to generate a callback. It invokes + // `_.iteratee` if overridden, otherwise `baseIteratee`. + function cb(value, context, argCount) { + if (_.iteratee !== iteratee) return _.iteratee(value, context); + return baseIteratee(value, context, argCount); + } + + // Returns the results of applying the `iteratee` to each element of `obj`. + // In contrast to `_.map` it returns an object. + function mapObject(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = keys(obj), + length = _keys.length, + results = {}; + for (var index = 0; index < length; index++) { + var currentKey = _keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + + // Predicate-generating function. Often useful outside of Underscore. + function noop(){} + + // Generates a function for a given object that returns a given property. + function propertyOf(obj) { + if (obj == null) return noop; + return function(path) { + return get(obj, path); + }; + } + + // Run a function **n** times. + function times(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + } + + // Return a random integer between `min` and `max` (inclusive). + function random(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + } + + // A (possibly faster) way to get the current timestamp as an integer. + var now = Date.now || function() { + return new Date().getTime(); + }; + + // Internal helper to generate functions for escaping and unescaping strings + // to/from HTML interpolation. + function createEscaper(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped. + var source = '(?:' + keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + } + + // Internal list of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + // Function for escaping strings to HTML interpolation. + var _escape = createEscaper(escapeMap); + + // Internal list of HTML entities for unescaping. + var unescapeMap = invert(escapeMap); + + // Function for unescaping strings from HTML interpolation. + var _unescape = createEscaper(unescapeMap); + + // By default, Underscore uses ERB-style template delimiters. Change the + // following template settings to use alternative delimiters. + var templateSettings = _.templateSettings = { + evaluate: /<%([\s\S]+?)%>/g, + interpolate: /<%=([\s\S]+?)%>/g, + escape: /<%-([\s\S]+?)%>/g + }; + + // When customizing `_.templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; + + function escapeChar(match) { + return '\\' + escapes[match]; + } + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + function template(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escapeRegExp, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offset. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + var render; + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + } + + // Traverses the children of `obj` along `path`. If a child is a function, it + // is invoked with its parent as context. Returns the value of the final + // child, or `fallback` if any child is undefined. + function result(obj, path, fallback) { + path = toPath$1(path); + var length = path.length; + if (!length) { + return isFunction$1(fallback) ? fallback.call(obj) : fallback; + } + for (var i = 0; i < length; i++) { + var prop = obj == null ? void 0 : obj[path[i]]; + if (prop === void 0) { + prop = fallback; + i = length; // Ensure we don't continue iterating. + } + obj = isFunction$1(prop) ? prop.call(obj) : prop; + } + return obj; + } + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + function uniqueId(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + } + + // Start chaining a wrapped Underscore object. + function chain(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + } + + // Internal function to execute `sourceFunc` bound to `context` with optional + // `args`. Determines whether to execute a function as a constructor or as a + // normal function. + function executeBound(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (isObject(result)) return result; + return self; + } + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. `_` acts + // as a placeholder by default, allowing any combination of arguments to be + // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. + var partial = restArguments(function(func, boundArgs) { + var placeholder = partial.placeholder; + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }); + + partial.placeholder = _; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). + var bind = restArguments(function(func, context, args) { + if (!isFunction$1(func)) throw new TypeError('Bind must be called on a function'); + var bound = restArguments(function(callArgs) { + return executeBound(func, bound, context, this, args.concat(callArgs)); + }); + return bound; + }); + + // Internal helper for collection methods to determine whether a collection + // should be iterated as an array or as an object. + // Related: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var isArrayLike = createSizePropertyCheck(getLength); + + // Internal implementation of a recursive `flatten` function. + function flatten(input, depth, strict, output) { + output = output || []; + if (!depth && depth !== 0) { + depth = Infinity; + } else if (depth <= 0) { + return output.concat(input); + } + var idx = output.length; + for (var i = 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { + // Flatten current level of array or arguments object. + if (depth > 1) { + flatten(value, depth - 1, strict, output); + idx = output.length; + } else { + var j = 0, len = value.length; + while (j < len) output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + } + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + var bindAll = restArguments(function(obj, keys) { + keys = flatten(keys, false, false); + var index = keys.length; + if (index < 1) throw new Error('bindAll must be passed function names'); + while (index--) { + var key = keys[index]; + obj[key] = bind(obj[key], obj); + } + return obj; + }); + + // Memoize an expensive function by storing its results. + function memoize(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + } + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + var delay = restArguments(function(func, wait, args) { + return setTimeout(function() { + return func.apply(null, args); + }, wait); + }); + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + var defer = partial(delay, _, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + function throttle(func, wait, options) { + var timeout, context, args, result; + var previous = 0; + if (!options) options = {}; + + var later = function() { + previous = options.leading === false ? 0 : now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + + var throttled = function() { + var _now = now(); + if (!previous && options.leading === false) previous = _now; + var remaining = wait - (_now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = _now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + + throttled.cancel = function() { + clearTimeout(timeout); + previous = 0; + timeout = context = args = null; + }; + + return throttled; + } + + // When a sequence of calls of the returned function ends, the argument + // function is triggered. The end of a sequence is defined by the `wait` + // parameter. If `immediate` is passed, the argument function will be + // triggered at the beginning of the sequence instead of at the end. + function debounce(func, wait, immediate) { + var timeout, previous, args, result, context; + + var later = function() { + var passed = now() - previous; + if (wait > passed) { + timeout = setTimeout(later, wait - passed); + } else { + timeout = null; + if (!immediate) result = func.apply(context, args); + // This check is needed because `func` can recursively invoke `debounced`. + if (!timeout) args = context = null; + } + }; + + var debounced = restArguments(function(_args) { + context = this; + args = _args; + previous = now(); + if (!timeout) { + timeout = setTimeout(later, wait); + if (immediate) result = func.apply(context, args); + } + return result; + }); + + debounced.cancel = function() { + clearTimeout(timeout); + timeout = args = context = null; + }; + + return debounced; + } + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + function wrap(func, wrapper) { + return partial(wrapper, func); + } + + // Returns a negated version of the passed-in predicate. + function negate(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + } + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + function compose() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + } + + // Returns a function that will only be executed on and after the Nth call. + function after(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + } + + // Returns a function that will only be executed up to (but not including) the + // Nth call. + function before(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + } + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + var once = partial(before, 2); + + // Returns the first key on an object that passes a truth test. + function findKey(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = keys(obj), key; + for (var i = 0, length = _keys.length; i < length; i++) { + key = _keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + } + + // Internal function to generate `_.findIndex` and `_.findLastIndex`. + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a truth test. + var findIndex = createPredicateIndexFinder(1); + + // Returns the last index on an array-like that passes a truth test. + var findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + function sortedIndex(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + } + + // Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions. + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), isNaN$1); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + var indexOf = createIndexFinder(1, findIndex, sortedIndex); + + // Return the position of the last occurrence of an item in an array, + // or -1 if the item is not included in the array. + var lastIndexOf = createIndexFinder(-1, findLastIndex); + + // Return the first value which passes a truth test. + function find(obj, predicate, context) { + var keyFinder = isArrayLike(obj) ? findIndex : findKey; + var key = keyFinder(obj, predicate, context); + if (key !== void 0 && key !== -1) return obj[key]; + } + + // Convenience version of a common use case of `_.find`: getting the first + // object containing specific `key:value` pairs. + function findWhere(obj, attrs) { + return find(obj, matcher(attrs)); + } + + // The cornerstone for collection functions, an `each` + // implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + function each(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var _keys = keys(obj); + for (i = 0, length = _keys.length; i < length; i++) { + iteratee(obj[_keys[i]], _keys[i], obj); + } + } + return obj; + } + + // Return the results of applying the iteratee to each element. + function map(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + + // Internal helper to create a reducing function, iterating left or right. + function createReduce(dir) { + // Wrap code that reassigns argument variables in a separate function than + // the one that accesses `arguments.length` to avoid a perf hit. (#1991) + var reducer = function(obj, iteratee, memo, initial) { + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + index = dir > 0 ? 0 : length - 1; + if (!initial) { + memo = obj[_keys ? _keys[index] : index]; + index += dir; + } + for (; index >= 0 && index < length; index += dir) { + var currentKey = _keys ? _keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + return function(obj, iteratee, memo, context) { + var initial = arguments.length >= 3; + return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); + }; + } + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + var reduce = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + var reduceRight = createReduce(-1); + + // Return all the elements that pass a truth test. + function filter(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + } + + // Return all the elements for which a truth test fails. + function reject(obj, predicate, context) { + return filter(obj, negate(cb(predicate)), context); + } + + // Determine whether all of the elements pass a truth test. + function every(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + } + + // Determine if at least one element in the object passes a truth test. + function some(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + } + + // Determine if the array or object contains a given item (using `===`). + function contains(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return indexOf(obj, item, fromIndex) >= 0; + } + + // Invoke a method (with arguments) on every item in a collection. + var invoke = restArguments(function(obj, path, args) { + var contextPath, func; + if (isFunction$1(path)) { + func = path; + } else { + path = toPath$1(path); + contextPath = path.slice(0, -1); + path = path[path.length - 1]; + } + return map(obj, function(context) { + var method = func; + if (!method) { + if (contextPath && contextPath.length) { + context = deepGet(context, contextPath); + } + if (context == null) return void 0; + method = context[path]; + } + return method == null ? method : method.apply(context, args); + }); + }); + + // Convenience version of a common use case of `_.map`: fetching a property. + function pluck(obj, key) { + return map(obj, property(key)); + } + + // Convenience version of a common use case of `_.filter`: selecting only + // objects containing specific `key:value` pairs. + function where(obj, attrs) { + return filter(obj, matcher(attrs)); + } + + // Return the maximum element (or element-based computation). + function max(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + + // Return the minimum element (or element-based computation). + function min(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + + // Sample **n** random values from a collection using the modern version of the + // [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `_.map`. + function sample(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = values(obj); + return obj[random(obj.length - 1)]; + } + var sample = isArrayLike(obj) ? clone(obj) : values(obj); + var length = getLength(sample); + n = Math.max(Math.min(n, length), 0); + var last = length - 1; + for (var index = 0; index < n; index++) { + var rand = random(index, last); + var temp = sample[index]; + sample[index] = sample[rand]; + sample[rand] = temp; + } + return sample.slice(0, n); + } + + // Shuffle a collection. + function shuffle(obj) { + return sample(obj, Infinity); + } + + // Sort the object's values by a criterion produced by an iteratee. + function sortBy(obj, iteratee, context) { + var index = 0; + iteratee = cb(iteratee, context); + return pluck(map(obj, function(value, key, list) { + return { + value: value, + index: index++, + criteria: iteratee(value, key, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + } + + // An internal function used for aggregate "group by" operations. + function group(behavior, partition) { + return function(obj, iteratee, context) { + var result = partition ? [[], []] : {}; + iteratee = cb(iteratee, context); + each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + } + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + var groupBy = group(function(result, value, key) { + if (has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `_.groupBy`, but for + // when you know that your index values will be unique. + var indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + var countBy = group(function(result, value, key) { + if (has(result, key)) result[key]++; else result[key] = 1; + }); + + // Split a collection into two arrays: one whose elements all pass the given + // truth test, and one whose elements all do not pass the truth test. + var partition = group(function(result, value, pass) { + result[pass ? 0 : 1].push(value); + }, true); + + // Safely create a real, live array from anything iterable. + var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; + function toArray(obj) { + if (!obj) return []; + if (isArray(obj)) return slice.call(obj); + if (isString(obj)) { + // Keep surrogate pair characters together. + return obj.match(reStrSymbol); + } + if (isArrayLike(obj)) return map(obj, identity); + return values(obj); + } + + // Return the number of elements in a collection. + function size(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : keys(obj).length; + } + + // Internal `_.pick` helper function to determine whether `key` is an enumerable + // property name of `obj`. + function keyInObj(value, key, obj) { + return key in obj; + } + + // Return a copy of the object only containing the allowed properties. + var pick = restArguments(function(obj, keys) { + var result = {}, iteratee = keys[0]; + if (obj == null) return result; + if (isFunction$1(iteratee)) { + if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); + keys = allKeys(obj); + } else { + iteratee = keyInObj; + keys = flatten(keys, false, false); + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }); + + // Return a copy of the object without the disallowed properties. + var omit = restArguments(function(obj, keys) { + var iteratee = keys[0], context; + if (isFunction$1(iteratee)) { + iteratee = negate(iteratee); + if (keys.length > 1) context = keys[1]; + } else { + keys = map(flatten(keys, false, false), String); + iteratee = function(value, key) { + return !contains(keys, key); + }; + } + return pick(obj, iteratee, context); + }); + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + function initial(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + } + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. The **guard** check allows it to work with `_.map`. + function first(array, n, guard) { + if (array == null || array.length < 1) return n == null || guard ? void 0 : []; + if (n == null || guard) return array[0]; + return initial(array, array.length - n); + } + + // Returns everything but the first entry of the `array`. Especially useful on + // the `arguments` object. Passing an **n** will return the rest N values in the + // `array`. + function rest(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + } + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + function last(array, n, guard) { + if (array == null || array.length < 1) return n == null || guard ? void 0 : []; + if (n == null || guard) return array[array.length - 1]; + return rest(array, Math.max(0, array.length - n)); + } + + // Trim out all falsy values from an array. + function compact(array) { + return filter(array, Boolean); + } + + // Flatten out an array, either recursively (by default), or up to `depth`. + // Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively. + function flatten$1(array, depth) { + return flatten(array, depth, false); + } + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + var difference = restArguments(function(array, rest) { + rest = flatten(rest, true, true); + return filter(array, function(value){ + return !contains(rest, value); + }); + }); + + // Return a version of the array that does not contain the specified value(s). + var without = restArguments(function(array, otherArrays) { + return difference(array, otherArrays); + }); + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // The faster algorithm will not work with an iteratee if the iteratee + // is not a one-to-one function, so providing an iteratee will disable + // the faster algorithm. + function uniq(array, isSorted, iteratee, context) { + if (!isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted && !iteratee) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!contains(result, value)) { + result.push(value); + } + } + return result; + } + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + var union = restArguments(function(arrays) { + return uniq(flatten(arrays, true, true)); + }); + + // Produce an array that contains every item shared between all the + // passed-in arrays. + function intersection(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (contains(result, item)) continue; + var j; + for (j = 1; j < argsLength; j++) { + if (!contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + } + + // Complement of zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices. + function unzip(array) { + var length = array && max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = pluck(array, index); + } + return result; + } + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + var zip = restArguments(unzip); + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. Passing by pairs is the reverse of `_.pairs`. + function object(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + } + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](https://docs.python.org/library/functions.html#range). + function range(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + if (!step) { + step = stop < start ? -1 : 1; + } + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + } + + // Chunk a single array into multiple arrays, each containing `count` or fewer + // items. + function chunk(array, count) { + if (count == null || count < 1) return []; + var result = []; + var i = 0, length = array.length; + while (i < length) { + result.push(slice.call(array, i, i += count)); + } + return result; + } + + // Helper function to continue chaining intermediate results. + function chainResult(instance, obj) { + return instance._chain ? _(obj).chain() : obj; + } + + // Add your own custom functions to the Underscore object. + function mixin(obj) { + each(functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return chainResult(this, func.apply(_, args)); + }; + }); + return _; + } + + // Add all mutator `Array` functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + if (obj != null) { + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) { + delete obj[0]; + } + } + return chainResult(this, obj); + }; + }); + + // Add all accessor `Array` functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + if (obj != null) obj = method.apply(obj, arguments); + return chainResult(this, obj); + }; + }); + + // Named Exports + + var allExports = { + __proto__: null, + VERSION: VERSION, + restArguments: restArguments, + isObject: isObject, + isNull: isNull, + isUndefined: isUndefined, + isBoolean: isBoolean, + isElement: isElement, + isString: isString, + isNumber: isNumber, + isDate: isDate, + isRegExp: isRegExp, + isError: isError, + isSymbol: isSymbol, + isArrayBuffer: isArrayBuffer, + isDataView: isDataView$1, + isArray: isArray, + isFunction: isFunction$1, + isArguments: isArguments$1, + isFinite: isFinite$1, + isNaN: isNaN$1, + isTypedArray: isTypedArray$1, + isEmpty: isEmpty, + isMatch: isMatch, + isEqual: isEqual, + isMap: isMap, + isWeakMap: isWeakMap, + isSet: isSet, + isWeakSet: isWeakSet, + keys: keys, + allKeys: allKeys, + values: values, + pairs: pairs, + invert: invert, + functions: functions, + methods: functions, + extend: extend, + extendOwn: extendOwn, + assign: extendOwn, + defaults: defaults, + create: create, + clone: clone, + tap: tap, + get: get, + has: has$1, + mapObject: mapObject, + identity: identity, + constant: constant, + noop: noop, + toPath: toPath, + property: property, + propertyOf: propertyOf, + matcher: matcher, + matches: matcher, + times: times, + random: random, + now: now, + escape: _escape, + unescape: _unescape, + templateSettings: templateSettings, + template: template, + result: result, + uniqueId: uniqueId, + chain: chain, + iteratee: iteratee, + partial: partial, + bind: bind, + bindAll: bindAll, + memoize: memoize, + delay: delay, + defer: defer, + throttle: throttle, + debounce: debounce, + wrap: wrap, + negate: negate, + compose: compose, + after: after, + before: before, + once: once, + findKey: findKey, + findIndex: findIndex, + findLastIndex: findLastIndex, + sortedIndex: sortedIndex, + indexOf: indexOf, + lastIndexOf: lastIndexOf, + find: find, + detect: find, + findWhere: findWhere, + each: each, + forEach: each, + map: map, + collect: map, + reduce: reduce, + foldl: reduce, + inject: reduce, + reduceRight: reduceRight, + foldr: reduceRight, + filter: filter, + select: filter, + reject: reject, + every: every, + all: every, + some: some, + any: some, + contains: contains, + includes: contains, + include: contains, + invoke: invoke, + pluck: pluck, + where: where, + max: max, + min: min, + shuffle: shuffle, + sample: sample, + sortBy: sortBy, + groupBy: groupBy, + indexBy: indexBy, + countBy: countBy, + partition: partition, + toArray: toArray, + size: size, + pick: pick, + omit: omit, + first: first, + head: first, + take: first, + initial: initial, + last: last, + rest: rest, + tail: rest, + drop: rest, + compact: compact, + flatten: flatten$1, + without: without, + uniq: uniq, + unique: uniq, + union: union, + intersection: intersection, + difference: difference, + unzip: unzip, + transpose: unzip, + zip: zip, + object: object, + range: range, + chunk: chunk, + mixin: mixin, + 'default': _ + }; + + // Default Export + + // Add all of the Underscore functions to the wrapper object. + var _$1 = mixin(allExports); + // Legacy Node.js API. + _$1._ = _$1; + + return _$1; + +}))); +//# sourceMappingURL=underscore.js.map diff --git a/sphinx/themes/basic/static/underscore-1.3.1.js b/sphinx/themes/basic/static/underscore-1.3.1.js deleted file mode 100644 index 208d4cd89..000000000 --- a/sphinx/themes/basic/static/underscore-1.3.1.js +++ /dev/null @@ -1,999 +0,0 @@ -// Underscore.js 1.3.1 -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore is freely distributable under the MIT license. -// Portions of Underscore are inspired or borrowed from Prototype, -// Oliver Steele's Functional, and John Resig's Micro-Templating. -// For all details and documentation: -// http://documentcloud.github.com/underscore - -(function() { - - // Baseline setup - // -------------- - - // Establish the root object, `window` in the browser, or `global` on the server. - var root = this; - - // Save the previous value of the `_` variable. - var previousUnderscore = root._; - - // Establish the object that gets returned to break out of a loop iteration. - var breaker = {}; - - // Save bytes in the minified (but not gzipped) version: - var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; - - // Create quick reference variables for speed access to core prototypes. - var slice = ArrayProto.slice, - unshift = ArrayProto.unshift, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; - - // All **ECMAScript 5** native function implementations that we hope to use - // are declared here. - var - nativeForEach = ArrayProto.forEach, - nativeMap = ArrayProto.map, - nativeReduce = ArrayProto.reduce, - nativeReduceRight = ArrayProto.reduceRight, - nativeFilter = ArrayProto.filter, - nativeEvery = ArrayProto.every, - nativeSome = ArrayProto.some, - nativeIndexOf = ArrayProto.indexOf, - nativeLastIndexOf = ArrayProto.lastIndexOf, - nativeIsArray = Array.isArray, - nativeKeys = Object.keys, - nativeBind = FuncProto.bind; - - // Create a safe reference to the Underscore object for use below. - var _ = function(obj) { return new wrapper(obj); }; - - // Export the Underscore object for **Node.js**, with - // backwards-compatibility for the old `require()` API. If we're in - // the browser, add `_` as a global object via a string identifier, - // for Closure Compiler "advanced" mode. - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = _; - } - exports._ = _; - } else { - root['_'] = _; - } - - // Current version. - _.VERSION = '1.3.1'; - - // Collection Functions - // -------------------- - - // The cornerstone, an `each` implementation, aka `forEach`. - // Handles objects with the built-in `forEach`, arrays, and raw objects. - // Delegates to **ECMAScript 5**'s native `forEach` if available. - var each = _.each = _.forEach = function(obj, iterator, context) { - if (obj == null) return; - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; - } - } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } - } - } - }; - - // Return the results of applying the iterator to each element. - // Delegates to **ECMAScript 5**'s native `map` if available. - _.map = _.collect = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); - each(obj, function(value, index, list) { - results[results.length] = iterator.call(context, value, index, list); - }); - if (obj.length === +obj.length) results.length = obj.length; - return results; - }; - - // **Reduce** builds up a single result from a list of values, aka `inject`, - // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. - _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduce && obj.reduce === nativeReduce) { - if (context) iterator = _.bind(iterator, context); - return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); - } - each(obj, function(value, index, list) { - if (!initial) { - memo = value; - initial = true; - } else { - memo = iterator.call(context, memo, value, index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // The right-associative version of reduce, also known as `foldr`. - // Delegates to **ECMAScript 5**'s native `reduceRight` if available. - _.reduceRight = _.foldr = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { - if (context) iterator = _.bind(iterator, context); - return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); - } - var reversed = _.toArray(obj).reverse(); - if (context && !initial) iterator = _.bind(iterator, context); - return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); - }; - - // Return the first value which passes a truth test. Aliased as `detect`. - _.find = _.detect = function(obj, iterator, context) { - var result; - any(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) { - result = value; - return true; - } - }); - return result; - }; - - // Return all the elements that pass a truth test. - // Delegates to **ECMAScript 5**'s native `filter` if available. - // Aliased as `select`. - _.filter = _.select = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); - each(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Return all the elements for which a truth test fails. - _.reject = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Determine whether all of the elements match a truth test. - // Delegates to **ECMAScript 5**'s native `every` if available. - // Aliased as `all`. - _.every = _.all = function(obj, iterator, context) { - var result = true; - if (obj == null) return result; - if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); - each(obj, function(value, index, list) { - if (!(result = result && iterator.call(context, value, index, list))) return breaker; - }); - return result; - }; - - // Determine if at least one element in the object matches a truth test. - // Delegates to **ECMAScript 5**'s native `some` if available. - // Aliased as `any`. - var any = _.some = _.any = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = false; - if (obj == null) return result; - if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); - each(obj, function(value, index, list) { - if (result || (result = iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if a given value is included in the array or object using `===`. - // Aliased as `contains`. - _.include = _.contains = function(obj, target) { - var found = false; - if (obj == null) return found; - if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { - return value === target; - }); - return found; - }; - - // Invoke a method (with arguments) on every item in a collection. - _.invoke = function(obj, method) { - var args = slice.call(arguments, 2); - return _.map(obj, function(value) { - return (_.isFunction(method) ? method || value : value[method]).apply(value, args); - }); - }; - - // Convenience version of a common use case of `map`: fetching a property. - _.pluck = function(obj, key) { - return _.map(obj, function(value){ return value[key]; }); - }; - - // Return the maximum element or (element-based computation). - _.max = function(obj, iterator, context) { - if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); - if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -Infinity}; - each(obj, function(value, index, list) { - var computed = iterator ? iterator.call(context, value, index, list) : value; - computed >= result.computed && (result = {value : value, computed : computed}); - }); - return result.value; - }; - - // Return the minimum element (or element-based computation). - _.min = function(obj, iterator, context) { - if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); - if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : Infinity}; - each(obj, function(value, index, list) { - var computed = iterator ? iterator.call(context, value, index, list) : value; - computed < result.computed && (result = {value : value, computed : computed}); - }); - return result.value; - }; - - // Shuffle an array. - _.shuffle = function(obj) { - var shuffled = [], rand; - each(obj, function(value, index, list) { - if (index == 0) { - shuffled[0] = value; - } else { - rand = Math.floor(Math.random() * (index + 1)); - shuffled[index] = shuffled[rand]; - shuffled[rand] = value; - } - }); - return shuffled; - }; - - // Sort the object's values by a criterion produced by an iterator. - _.sortBy = function(obj, iterator, context) { - return _.pluck(_.map(obj, function(value, index, list) { - return { - value : value, - criteria : iterator.call(context, value, index, list) - }; - }).sort(function(left, right) { - var a = left.criteria, b = right.criteria; - return a < b ? -1 : a > b ? 1 : 0; - }), 'value'); - }; - - // Groups the object's values by a criterion. Pass either a string attribute - // to group by, or a function that returns the criterion. - _.groupBy = function(obj, val) { - var result = {}; - var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; - each(obj, function(value, index) { - var key = iterator(value, index); - (result[key] || (result[key] = [])).push(value); - }); - return result; - }; - - // Use a comparator function to figure out at what index an object should - // be inserted so as to maintain order. Uses binary search. - _.sortedIndex = function(array, obj, iterator) { - iterator || (iterator = _.identity); - var low = 0, high = array.length; - while (low < high) { - var mid = (low + high) >> 1; - iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; - } - return low; - }; - - // Safely convert anything iterable into a real, live array. - _.toArray = function(iterable) { - if (!iterable) return []; - if (iterable.toArray) return iterable.toArray(); - if (_.isArray(iterable)) return slice.call(iterable); - if (_.isArguments(iterable)) return slice.call(iterable); - return _.values(iterable); - }; - - // Return the number of elements in an object. - _.size = function(obj) { - return _.toArray(obj).length; - }; - - // Array Functions - // --------------- - - // Get the first element of an array. Passing **n** will return the first N - // values in the array. Aliased as `head`. The **guard** check allows it to work - // with `_.map`. - _.first = _.head = function(array, n, guard) { - return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; - }; - - // Returns everything but the last entry of the array. Especcialy useful on - // the arguments object. Passing **n** will return all the values in - // the array, excluding the last N. The **guard** check allows it to work with - // `_.map`. - _.initial = function(array, n, guard) { - return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); - }; - - // Get the last element of an array. Passing **n** will return the last N - // values in the array. The **guard** check allows it to work with `_.map`. - _.last = function(array, n, guard) { - if ((n != null) && !guard) { - return slice.call(array, Math.max(array.length - n, 0)); - } else { - return array[array.length - 1]; - } - }; - - // Returns everything but the first entry of the array. Aliased as `tail`. - // Especially useful on the arguments object. Passing an **index** will return - // the rest of the values in the array from that index onward. The **guard** - // check allows it to work with `_.map`. - _.rest = _.tail = function(array, index, guard) { - return slice.call(array, (index == null) || guard ? 1 : index); - }; - - // Trim out all falsy values from an array. - _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); - }; - - // Return a completely flattened version of an array. - _.flatten = function(array, shallow) { - return _.reduce(array, function(memo, value) { - if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); - memo[memo.length] = value; - return memo; - }, []); - }; - - // Return a version of the array that does not contain the specified value(s). - _.without = function(array) { - return _.difference(array, slice.call(arguments, 1)); - }; - - // Produce a duplicate-free version of the array. If the array has already - // been sorted, you have the option of using a faster algorithm. - // Aliased as `unique`. - _.uniq = _.unique = function(array, isSorted, iterator) { - var initial = iterator ? _.map(array, iterator) : array; - var result = []; - _.reduce(initial, function(memo, el, i) { - if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { - memo[memo.length] = el; - result[result.length] = array[i]; - } - return memo; - }, []); - return result; - }; - - // Produce an array that contains the union: each distinct element from all of - // the passed-in arrays. - _.union = function() { - return _.uniq(_.flatten(arguments, true)); - }; - - // Produce an array that contains every item shared between all the - // passed-in arrays. (Aliased as "intersect" for back-compat.) - _.intersection = _.intersect = function(array) { - var rest = slice.call(arguments, 1); - return _.filter(_.uniq(array), function(item) { - return _.every(rest, function(other) { - return _.indexOf(other, item) >= 0; - }); - }); - }; - - // Take the difference between one array and a number of other arrays. - // Only the elements present in just the first array will remain. - _.difference = function(array) { - var rest = _.flatten(slice.call(arguments, 1)); - return _.filter(array, function(value){ return !_.include(rest, value); }); - }; - - // Zip together multiple lists into a single array -- elements that share - // an index go together. - _.zip = function() { - var args = slice.call(arguments); - var length = _.max(_.pluck(args, 'length')); - var results = new Array(length); - for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); - return results; - }; - - // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), - // we need this function. Return the position of the first occurrence of an - // item in an array, or -1 if the item is not included in the array. - // Delegates to **ECMAScript 5**'s native `indexOf` if available. - // If the array is large and already in sort order, pass `true` - // for **isSorted** to use binary search. - _.indexOf = function(array, item, isSorted) { - if (array == null) return -1; - var i, l; - if (isSorted) { - i = _.sortedIndex(array, item); - return array[i] === item ? i : -1; - } - if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); - for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; - return -1; - }; - - // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. - _.lastIndexOf = function(array, item) { - if (array == null) return -1; - if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); - var i = array.length; - while (i--) if (i in array && array[i] === item) return i; - return -1; - }; - - // Generate an integer Array containing an arithmetic progression. A port of - // the native Python `range()` function. See - // [the Python documentation](http://docs.python.org/library/functions.html#range). - _.range = function(start, stop, step) { - if (arguments.length <= 1) { - stop = start || 0; - start = 0; - } - step = arguments[2] || 1; - - var len = Math.max(Math.ceil((stop - start) / step), 0); - var idx = 0; - var range = new Array(len); - - while(idx < len) { - range[idx++] = start; - start += step; - } - - return range; - }; - - // Function (ahem) Functions - // ------------------ - - // Reusable constructor function for prototype setting. - var ctor = function(){}; - - // Create a function bound to a given object (assigning `this`, and arguments, - // optionally). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; - if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - if (!_.isFunction(func)) throw new TypeError; - args = slice.call(arguments, 2); - return bound = function() { - if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); - ctor.prototype = func.prototype; - var self = new ctor; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) return result; - return self; - }; - }; - - // Bind all of an object's methods to that object. Useful for ensuring that - // all callbacks defined on an object belong to it. - _.bindAll = function(obj) { - var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); - each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); - return obj; - }; - - // Memoize an expensive function by storing its results. - _.memoize = function(func, hasher) { - var memo = {}; - hasher || (hasher = _.identity); - return function() { - var key = hasher.apply(this, arguments); - return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); - }; - }; - - // Delays a function for the given number of milliseconds, and then calls - // it with the arguments supplied. - _.delay = function(func, wait) { - var args = slice.call(arguments, 2); - return setTimeout(function(){ return func.apply(func, args); }, wait); - }; - - // Defers a function, scheduling it to run after the current call stack has - // cleared. - _.defer = function(func) { - return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); - }; - - // Returns a function, that, when invoked, will only be triggered at most once - // during a given window of time. - _.throttle = function(func, wait) { - var context, args, timeout, throttling, more; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); - return function() { - context = this; args = arguments; - var later = function() { - timeout = null; - if (more) func.apply(context, args); - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - func.apply(context, args); - } - whenDone(); - throttling = true; - }; - }; - - // Returns a function, that, as long as it continues to be invoked, will not - // be triggered. The function will be called after it stops being called for - // N milliseconds. - _.debounce = function(func, wait) { - var timeout; - return function() { - var context = this, args = arguments; - var later = function() { - timeout = null; - func.apply(context, args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }; - - // Returns a function that will be executed at most one time, no matter how - // often you call it. Useful for lazy initialization. - _.once = function(func) { - var ran = false, memo; - return function() { - if (ran) return memo; - ran = true; - return memo = func.apply(this, arguments); - }; - }; - - // Returns the first function passed as an argument to the second, - // allowing you to adjust arguments, run code before and after, and - // conditionally execute the original function. - _.wrap = function(func, wrapper) { - return function() { - var args = [func].concat(slice.call(arguments, 0)); - return wrapper.apply(this, args); - }; - }; - - // Returns a function that is the composition of a list of functions, each - // consuming the return value of the function that follows. - _.compose = function() { - var funcs = arguments; - return function() { - var args = arguments; - for (var i = funcs.length - 1; i >= 0; i--) { - args = [funcs[i].apply(this, args)]; - } - return args[0]; - }; - }; - - // Returns a function that will only be executed after being called N times. - _.after = function(times, func) { - if (times <= 0) return func(); - return function() { - if (--times < 1) { return func.apply(this, arguments); } - }; - }; - - // Object Functions - // ---------------- - - // Retrieve the names of an object's properties. - // Delegates to **ECMAScript 5**'s native `Object.keys` - _.keys = nativeKeys || function(obj) { - if (obj !== Object(obj)) throw new TypeError('Invalid object'); - var keys = []; - for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; - return keys; - }; - - // Retrieve the values of an object's properties. - _.values = function(obj) { - return _.map(obj, _.identity); - }; - - // Return a sorted list of the function names available on the object. - // Aliased as `methods` - _.functions = _.methods = function(obj) { - var names = []; - for (var key in obj) { - if (_.isFunction(obj[key])) names.push(key); - } - return names.sort(); - }; - - // Extend a given object with all the properties in passed-in object(s). - _.extend = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Fill in a given object with default properties. - _.defaults = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Create a (shallow-cloned) duplicate of an object. - _.clone = function(obj) { - if (!_.isObject(obj)) return obj; - return _.isArray(obj) ? obj.slice() : _.extend({}, obj); - }; - - // Invokes interceptor with the obj, and then returns obj. - // The primary purpose of this method is to "tap into" a method chain, in - // order to perform operations on intermediate results within the chain. - _.tap = function(obj, interceptor) { - interceptor(obj); - return obj; - }; - - // Internal recursive comparison function. - function eq(a, b, stack) { - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. - if (a === b) return a !== 0 || 1 / a == 1 / b; - // A strict comparison is necessary because `null == undefined`. - if (a == null || b == null) return a === b; - // Unwrap any wrapped objects. - if (a._chain) a = a._wrapped; - if (b._chain) b = b._wrapped; - // Invoke a custom `isEqual` method if one is provided. - if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); - if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className != toString.call(b)) return false; - switch (className) { - // Strings, numbers, dates, and booleans are compared by value. - case '[object String]': - // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is - // equivalent to `new String("5")`. - return a == String(b); - case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for - // other numeric values. - return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); - case '[object Date]': - case '[object Boolean]': - // Coerce dates and booleans to numeric primitive values. Dates are compared by their - // millisecond representations. Note that invalid dates with millisecond representations - // of `NaN` are not equivalent. - return +a == +b; - // RegExps are compared by their source patterns and flags. - case '[object RegExp]': - return a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - } - if (typeof a != 'object' || typeof b != 'object') return false; - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - var length = stack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (stack[length] == a) return true; - } - // Add the first object to the stack of traversed objects. - stack.push(a); - var size = 0, result = true; - // Recursively compare objects and arrays. - if (className == '[object Array]') { - // Compare array lengths to determine if a deep comparison is necessary. - size = a.length; - result = size == b.length; - if (result) { - // Deep compare the contents, ignoring non-numeric properties. - while (size--) { - // Ensure commutative equality for sparse arrays. - if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; - } - } - } else { - // Objects with different constructors are not equivalent. - if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; - // Deep compare objects. - for (var key in a) { - if (_.has(a, key)) { - // Count the expected number of properties. - size++; - // Deep compare each member. - if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; - } - } - // Ensure that both objects contain the same number of properties. - if (result) { - for (key in b) { - if (_.has(b, key) && !(size--)) break; - } - result = !size; - } - } - // Remove the first object from the stack of traversed objects. - stack.pop(); - return result; - } - - // Perform a deep comparison to check if two objects are equal. - _.isEqual = function(a, b) { - return eq(a, b, []); - }; - - // Is a given array, string, or object empty? - // An "empty" object has no enumerable own-properties. - _.isEmpty = function(obj) { - if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; - for (var key in obj) if (_.has(obj, key)) return false; - return true; - }; - - // Is a given value a DOM element? - _.isElement = function(obj) { - return !!(obj && obj.nodeType == 1); - }; - - // Is a given value an array? - // Delegates to ECMA5's native Array.isArray - _.isArray = nativeIsArray || function(obj) { - return toString.call(obj) == '[object Array]'; - }; - - // Is a given variable an object? - _.isObject = function(obj) { - return obj === Object(obj); - }; - - // Is a given variable an arguments object? - _.isArguments = function(obj) { - return toString.call(obj) == '[object Arguments]'; - }; - if (!_.isArguments(arguments)) { - _.isArguments = function(obj) { - return !!(obj && _.has(obj, 'callee')); - }; - } - - // Is a given value a function? - _.isFunction = function(obj) { - return toString.call(obj) == '[object Function]'; - }; - - // Is a given value a string? - _.isString = function(obj) { - return toString.call(obj) == '[object String]'; - }; - - // Is a given value a number? - _.isNumber = function(obj) { - return toString.call(obj) == '[object Number]'; - }; - - // Is the given value `NaN`? - _.isNaN = function(obj) { - // `NaN` is the only value for which `===` is not reflexive. - return obj !== obj; - }; - - // Is a given value a boolean? - _.isBoolean = function(obj) { - return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; - }; - - // Is a given value a date? - _.isDate = function(obj) { - return toString.call(obj) == '[object Date]'; - }; - - // Is the given value a regular expression? - _.isRegExp = function(obj) { - return toString.call(obj) == '[object RegExp]'; - }; - - // Is a given value equal to null? - _.isNull = function(obj) { - return obj === null; - }; - - // Is a given variable undefined? - _.isUndefined = function(obj) { - return obj === void 0; - }; - - // Has own property? - _.has = function(obj, key) { - return hasOwnProperty.call(obj, key); - }; - - // Utility Functions - // ----------------- - - // Run Underscore.js in *noConflict* mode, returning the `_` variable to its - // previous owner. Returns a reference to the Underscore object. - _.noConflict = function() { - root._ = previousUnderscore; - return this; - }; - - // Keep the identity function around for default iterators. - _.identity = function(value) { - return value; - }; - - // Run a function **n** times. - _.times = function (n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); - }; - - // Escape a string for HTML interpolation. - _.escape = function(string) { - return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); - }; - - // Add your own custom functions to the Underscore object, ensuring that - // they're correctly added to the OOP wrapper as well. - _.mixin = function(obj) { - each(_.functions(obj), function(name){ - addToWrapper(name, _[name] = obj[name]); - }); - }; - - // Generate a unique integer id (unique within the entire client session). - // Useful for temporary DOM ids. - var idCounter = 0; - _.uniqueId = function(prefix) { - var id = idCounter++; - return prefix ? prefix + id : id; - }; - - // By default, Underscore uses ERB-style template delimiters, change the - // following template settings to use alternative delimiters. - _.templateSettings = { - evaluate : /<%([\s\S]+?)%>/g, - interpolate : /<%=([\s\S]+?)%>/g, - escape : /<%-([\s\S]+?)%>/g - }; - - // When customizing `templateSettings`, if you don't want to define an - // interpolation, evaluation or escaping regex, we need one that is - // guaranteed not to match. - var noMatch = /.^/; - - // Within an interpolation, evaluation, or escaping, remove HTML escaping - // that had been previously added. - var unescape = function(code) { - return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'"); - }; - - // JavaScript micro-templating, similar to John Resig's implementation. - // Underscore templating handles arbitrary delimiters, preserves whitespace, - // and correctly escapes quotes within interpolated code. - _.template = function(str, data) { - var c = _.templateSettings; - var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + - 'with(obj||{}){__p.push(\'' + - str.replace(/\\/g, '\\\\') - .replace(/'/g, "\\'") - .replace(c.escape || noMatch, function(match, code) { - return "',_.escape(" + unescape(code) + "),'"; - }) - .replace(c.interpolate || noMatch, function(match, code) { - return "'," + unescape(code) + ",'"; - }) - .replace(c.evaluate || noMatch, function(match, code) { - return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('"; - }) - .replace(/\r/g, '\\r') - .replace(/\n/g, '\\n') - .replace(/\t/g, '\\t') - + "');}return __p.join('');"; - var func = new Function('obj', '_', tmpl); - if (data) return func(data, _); - return function(data) { - return func.call(this, data, _); - }; - }; - - // Add a "chain" function, which will delegate to the wrapper. - _.chain = function(obj) { - return _(obj).chain(); - }; - - // The OOP Wrapper - // --------------- - - // If Underscore is called as a function, it returns a wrapped object that - // can be used OO-style. This wrapper holds altered versions of all the - // underscore functions. Wrapped objects may be chained. - var wrapper = function(obj) { this._wrapped = obj; }; - - // Expose `wrapper.prototype` as `_.prototype` - _.prototype = wrapper.prototype; - - // Helper function to continue chaining intermediate results. - var result = function(obj, chain) { - return chain ? _(obj).chain() : obj; - }; - - // A method to easily add functions to the OOP wrapper. - var addToWrapper = function(name, func) { - wrapper.prototype[name] = function() { - var args = slice.call(arguments); - unshift.call(args, this._wrapped); - return result(func.apply(_, args), this._chain); - }; - }; - - // Add all of the Underscore functions to the wrapper object. - _.mixin(_); - - // Add all mutator Array functions to the wrapper. - each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { - var method = ArrayProto[name]; - wrapper.prototype[name] = function() { - var wrapped = this._wrapped; - method.apply(wrapped, arguments); - var length = wrapped.length; - if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; - return result(wrapped, this._chain); - }; - }); - - // Add all accessor Array functions to the wrapper. - each(['concat', 'join', 'slice'], function(name) { - var method = ArrayProto[name]; - wrapper.prototype[name] = function() { - return result(method.apply(this._wrapped, arguments), this._chain); - }; - }); - - // Start chaining a wrapped Underscore object. - wrapper.prototype.chain = function() { - this._chain = true; - return this; - }; - - // Extracts the result from a wrapped and chained object. - wrapper.prototype.value = function() { - return this._wrapped; - }; - -}).call(this); diff --git a/sphinx/themes/basic/static/underscore.js b/sphinx/themes/basic/static/underscore.js index 5b55f32be..166240ef2 100644 --- a/sphinx/themes/basic/static/underscore.js +++ b/sphinx/themes/basic/static/underscore.js @@ -1,31 +1,6 @@ -// Underscore.js 1.3.1 -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore is freely distributable under the MIT license. -// Portions of Underscore are inspired or borrowed from Prototype, -// Oliver Steele's Functional, and John Resig's Micro-Templating. -// For all details and documentation: -// http://documentcloud.github.com/underscore -(function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return a==String(c);case "[object Number]":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source== -c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&q(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&q(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c, -h)&&!f--)break;g=!f}}d.pop();return g}var r=this,G=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,H=k.unshift,l=o.toString,I=o.hasOwnProperty,w=k.forEach,x=k.map,y=k.reduce,z=k.reduceRight,A=k.filter,B=k.every,C=k.some,p=k.indexOf,D=k.lastIndexOf,o=Array.isArray,J=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else r._=b;b.VERSION="1.3.1";var j=b.each= -b.forEach=function(a,c,d){if(a!=null)if(w&&a.forEach===w)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e2;a== -null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect= -function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e= -e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= -function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a, -c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}}; -b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments, -1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)}; -b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"}; -b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a), -function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+ -u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]= -function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain= -true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); +!function(n,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define("underscore",r):(n=n||self,function(){var t=n._,e=n._=r();e.noConflict=function(){return n._=t,e}}())}(this,(function(){ +// Underscore.js 1.12.0 +// https://underscorejs.org +// (c) 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. +var n="1.12.0",r="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||Function("return this")()||{},t=Array.prototype,e=Object.prototype,u="undefined"!=typeof Symbol?Symbol.prototype:null,o=t.push,i=t.slice,a=e.toString,f=e.hasOwnProperty,c="undefined"!=typeof ArrayBuffer,l="undefined"!=typeof DataView,s=Array.isArray,p=Object.keys,v=Object.create,h=c&&ArrayBuffer.isView,y=isNaN,g=isFinite,d=!{toString:null}.propertyIsEnumerable("toString"),b=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"],m=Math.pow(2,53)-1;function j(n,r){return r=null==r?n.length-1:+r,function(){for(var t=Math.max(arguments.length-r,0),e=Array(t),u=0;u=0&&t<=m}}function $(n){return function(r){return null==r?void 0:r[n]}}var G=$("byteLength"),H=J(G),Q=/\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;var X=c?function(n){return h?h(n)&&!q(n):H(n)&&Q.test(a.call(n))}:K(!1),Y=$("length");function Z(n,r){r=function(n){for(var r={},t=n.length,e=0;e":">",'"':""","'":"'","`":"`"},Kn=Ln(Cn),Jn=Ln(_n(Cn)),$n=tn.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},Gn=/(.)^/,Hn={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},Qn=/\\|'|\r|\n|\u2028|\u2029/g;function Xn(n){return"\\"+Hn[n]}var Yn=0;function Zn(n,r,t,e,u){if(!(e instanceof r))return n.apply(t,u);var o=Mn(n.prototype),i=n.apply(o,u);return _(i)?i:o}var nr=j((function(n,r){var t=nr.placeholder,e=function(){for(var u=0,o=r.length,i=Array(o),a=0;a1)er(a,r-1,t,e),u=e.length;else for(var f=0,c=a.length;f0&&(t=r.apply(this,arguments)),n<=1&&(r=null),t}}var cr=nr(fr,2);function lr(n,r,t){r=qn(r,t);for(var e,u=nn(n),o=0,i=u.length;o0?0:u-1;o>=0&&o0?a=o>=0?o:Math.max(o+f,a):f=o>=0?Math.min(o+1,f):o+f+1;else if(t&&o&&f)return e[o=t(e,u)]===u?o:-1;if(u!=u)return(o=r(i.call(e,a,f),C))>=0?o+a:-1;for(o=n>0?a:f-1;o>=0&&o0?0:i-1;for(u||(e=r[o?o[a]:a],a+=n);a>=0&&a=3;return r(n,Fn(t,u,4),e,o)}}var wr=_r(1),Ar=_r(-1);function xr(n,r,t){var e=[];return r=qn(r,t),mr(n,(function(n,t,u){r(n,t,u)&&e.push(n)})),e}function Sr(n,r,t){r=qn(r,t);for(var e=!tr(n)&&nn(n),u=(e||n).length,o=0;o=0}var Er=j((function(n,r,t){var e,u;return D(r)?u=r:(r=Nn(r),e=r.slice(0,-1),r=r[r.length-1]),jr(n,(function(n){var o=u;if(!o){if(e&&e.length&&(n=In(n,e)),null==n)return;o=n[r]}return null==o?o:o.apply(n,t)}))}));function Br(n,r){return jr(n,Rn(r))}function Nr(n,r,t){var e,u,o=-1/0,i=-1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var a=0,f=(n=tr(n)?n:jn(n)).length;ao&&(o=e);else r=qn(r,t),mr(n,(function(n,t,e){((u=r(n,t,e))>i||u===-1/0&&o===-1/0)&&(o=n,i=u)}));return o}function Ir(n,r,t){if(null==r||t)return tr(n)||(n=jn(n)),n[Wn(n.length-1)];var e=tr(n)?En(n):jn(n),u=Y(e);r=Math.max(Math.min(r,u),0);for(var o=u-1,i=0;i1&&(e=Fn(e,r[1])),r=an(n)):(e=Pr,r=er(r,!1,!1),n=Object(n));for(var u=0,o=r.length;u1&&(t=r[1])):(r=jr(er(r,!1,!1),String),e=function(n,t){return!Mr(r,t)}),qr(n,e,t)}));function Wr(n,r,t){return i.call(n,0,Math.max(0,n.length-(null==r||t?1:r)))}function zr(n,r,t){return null==n||n.length<1?null==r||t?void 0:[]:null==r||t?n[0]:Wr(n,n.length-r)}function Lr(n,r,t){return i.call(n,null==r||t?1:r)}var Cr=j((function(n,r){return r=er(r,!0,!0),xr(n,(function(n){return!Mr(r,n)}))})),Kr=j((function(n,r){return Cr(n,r)}));function Jr(n,r,t,e){A(r)||(e=t,t=r,r=!1),null!=t&&(t=qn(t,e));for(var u=[],o=[],i=0,a=Y(n);ir?(e&&(clearTimeout(e),e=null),a=c,i=n.apply(u,o),e||(u=o=null)):e||!1===t.trailing||(e=setTimeout(f,l)),i};return c.cancel=function(){clearTimeout(e),a=0,e=u=o=null},c},debounce:function(n,r,t){var e,u,o=function(r,t){e=null,t&&(u=n.apply(r,t))},i=j((function(i){if(e&&clearTimeout(e),t){var a=!e;e=setTimeout(o,r),a&&(u=n.apply(this,i))}else e=or(o,r,this,i);return u}));return i.cancel=function(){clearTimeout(e),e=null},i},wrap:function(n,r){return nr(r,n)},negate:ar,compose:function(){var n=arguments,r=n.length-1;return function(){for(var t=r,e=n[r].apply(this,arguments);t--;)e=n[t].call(this,e);return e}},after:function(n,r){return function(){if(--n<1)return r.apply(this,arguments)}},before:fr,once:cr,findKey:lr,findIndex:pr,findLastIndex:vr,sortedIndex:hr,indexOf:gr,lastIndexOf:dr,find:br,detect:br,findWhere:function(n,r){return br(n,Dn(r))},each:mr,forEach:mr,map:jr,collect:jr,reduce:wr,foldl:wr,inject:wr,reduceRight:Ar,foldr:Ar,filter:xr,select:xr,reject:function(n,r,t){return xr(n,ar(qn(r)),t)},every:Sr,all:Sr,some:Or,any:Or,contains:Mr,includes:Mr,include:Mr,invoke:Er,pluck:Br,where:function(n,r){return xr(n,Dn(r))},max:Nr,min:function(n,r,t){var e,u,o=1/0,i=1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var a=0,f=(n=tr(n)?n:jn(n)).length;ae||void 0===t)return 1;if(t * Humdinger * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/nature/static/nature.css_t b/sphinx/themes/nature/static/nature.css_t index 7df474a91..3dd748dda 100644 --- a/sphinx/themes/nature/static/nature.css_t +++ b/sphinx/themes/nature/static/nature.css_t @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- nature theme. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/nonav/layout.html b/sphinx/themes/nonav/layout.html index 256b9a228..d39800c18 100644 --- a/sphinx/themes/nonav/layout.html +++ b/sphinx/themes/nonav/layout.html @@ -4,7 +4,7 @@ Sphinx layout template for the any help system theme. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- extends "basic/layout.html" %} diff --git a/sphinx/themes/nonav/static/nonav.css b/sphinx/themes/nonav/static/nonav.css index d37c76221..63abb9f0d 100644 --- a/sphinx/themes/nonav/static/nonav.css +++ b/sphinx/themes/nonav/static/nonav.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- nonav theme. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/pyramid/static/epub.css b/sphinx/themes/pyramid/static/epub.css index 270e9570e..6e5722fd6 100644 --- a/sphinx/themes/pyramid/static/epub.css +++ b/sphinx/themes/pyramid/static/epub.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- default theme. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/pyramid/static/pyramid.css_t b/sphinx/themes/pyramid/static/pyramid.css_t index 0bab3d1a2..9857f2850 100644 --- a/sphinx/themes/pyramid/static/pyramid.css_t +++ b/sphinx/themes/pyramid/static/pyramid.css_t @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- pylons theme. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/scrolls/layout.html b/sphinx/themes/scrolls/layout.html index 7866a5162..066d9f668 100644 --- a/sphinx/themes/scrolls/layout.html +++ b/sphinx/themes/scrolls/layout.html @@ -5,7 +5,7 @@ Sphinx layout template for the scrolls theme, originally written by Armin Ronacher. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #} {%- extends "basic/layout.html" %} diff --git a/sphinx/themes/scrolls/static/scrolls.css_t b/sphinx/themes/scrolls/static/scrolls.css_t index e484f8c4f..f039d9e2b 100644 --- a/sphinx/themes/scrolls/static/scrolls.css_t +++ b/sphinx/themes/scrolls/static/scrolls.css_t @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- scrolls theme. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t b/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t index 8eca63278..4be04ce71 100644 --- a/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t +++ b/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t @@ -5,7 +5,7 @@ * Sphinx stylesheet -- sphinxdoc theme. Originally created by * Armin Ronacher for Werkzeug. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/themes/traditional/static/traditional.css_t b/sphinx/themes/traditional/static/traditional.css_t index 0120f83a5..4371d8e89 100644 --- a/sphinx/themes/traditional/static/traditional.css_t +++ b/sphinx/themes/traditional/static/traditional.css_t @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- traditional docs.python.org theme. * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/sphinx/theming.py b/sphinx/theming.py index 087ee7f24..30e4dffdb 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -4,7 +4,7 @@ Theming support for HTML builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index beb983f2a..45640308f 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -4,7 +4,7 @@ Docutils transforms used by Sphinx when reading documents. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -23,8 +23,7 @@ from sphinx import addnodes from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.locale import _, __ -from sphinx.util import docutils -from sphinx.util import logging +from sphinx.util import docutils, logging from sphinx.util.docutils import new_document from sphinx.util.i18n import format_date from sphinx.util.nodes import NodeMatcher, apply_source_workaround, is_smartquotable @@ -401,9 +400,8 @@ class ManpageLink(SphinxTransform): node.attributes.update(info) -from sphinx.domains.citation import ( # NOQA - CitationDefinitionTransform, CitationReferenceTransform -) +from sphinx.domains.citation import CitationDefinitionTransform # NOQA +from sphinx.domains.citation import CitationReferenceTransform # NOQA deprecated_alias('sphinx.transforms', { diff --git a/sphinx/transforms/compact_bullet_list.py b/sphinx/transforms/compact_bullet_list.py index 1cde3b1f0..18042358e 100644 --- a/sphinx/transforms/compact_bullet_list.py +++ b/sphinx/transforms/compact_bullet_list.py @@ -4,12 +4,11 @@ Docutils transforms used by Sphinx when reading documents. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List -from typing import cast +from typing import Any, Dict, List, cast from docutils import nodes from docutils.nodes import Node diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index c935ab195..d588f0411 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -4,7 +4,7 @@ Docutils transforms used by Sphinx when reading documents. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -20,18 +20,18 @@ from docutils.utils import relative_path from sphinx import addnodes from sphinx.config import Config from sphinx.domains.std import make_glossary_term, split_term_classifiers -from sphinx.locale import __, init as init_locale +from sphinx.locale import __ +from sphinx.locale import init as init_locale from sphinx.transforms import SphinxTransform -from sphinx.util import split_index_msg, logging, get_filetype +from sphinx.util import get_filetype, logging, split_index_msg from sphinx.util.i18n import docname_to_domain -from sphinx.util.nodes import ( - LITERAL_TYPE_NODES, IMAGE_TYPE_NODES, NodeMatcher, - extract_messages, is_pending_meta, traverse_translatable_index, -) +from sphinx.util.nodes import (IMAGE_TYPE_NODES, LITERAL_TYPE_NODES, NodeMatcher, + extract_messages, is_pending_meta, traverse_translatable_index) if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx @@ -311,6 +311,7 @@ class Locale(SphinxTransform): refname = newf.get('refname') refs = old_foot_namerefs.get(refname, []) if not refs: + newf.parent.remove(newf) continue oldf = refs.pop(0) diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index 7dc14af52..1c424050a 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -4,12 +4,11 @@ Docutils transforms used by Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List, Tuple, Type -from typing import cast +from typing import Any, Dict, List, Optional, Tuple, Type, cast from docutils import nodes from docutils.nodes import Element @@ -25,7 +24,6 @@ from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator from sphinx.util.nodes import process_only_nodes - logger = logging.getLogger(__name__) @@ -152,7 +150,7 @@ class ReferencesResolver(SphinxPostTransform): return newnode def warn_missing_reference(self, refdoc: str, typ: str, target: str, - node: pending_xref, domain: Domain) -> None: + node: pending_xref, domain: Optional[Domain]) -> None: warn = node.get('refwarn') if self.config.nitpicky: warn = True @@ -166,7 +164,10 @@ class ReferencesResolver(SphinxPostTransform): warn = False if not warn: return - if domain and typ in domain.dangling_warnings: + + if self.app.emit_firstresult('warn-missing-reference', domain, node): + return + elif domain and typ in domain.dangling_warnings: msg = domain.dangling_warnings[typ] elif node.get('refdomain', 'std') not in ('', 'std'): msg = (__('%s:%s reference target not found: %%(target)s') % diff --git a/sphinx/transforms/post_transforms/code.py b/sphinx/transforms/post_transforms/code.py index 2012d6e11..20df1db3c 100644 --- a/sphinx/transforms/post_transforms/code.py +++ b/sphinx/transforms/post_transforms/code.py @@ -4,7 +4,7 @@ transforms for code-blocks. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -20,7 +20,6 @@ from sphinx.application import Sphinx from sphinx.ext import doctest from sphinx.transforms import SphinxTransform - HighlightSetting = NamedTuple('HighlightSetting', [('language', str), ('force', bool), ('lineno_threshold', int)]) diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py index 949c09dde..2603e0458 100644 --- a/sphinx/transforms/post_transforms/images.py +++ b/sphinx/transforms/post_transforms/images.py @@ -4,7 +4,7 @@ Docutils transforms used by Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,11 +18,9 @@ from docutils import nodes from sphinx.application import Sphinx from sphinx.locale import __ from sphinx.transforms import SphinxTransform -from sphinx.util import epoch_to_rfc1123, rfc1123_to_epoch, sha1 -from sphinx.util import logging, requests -from sphinx.util.images import guess_mimetype, get_image_extension, parse_data_uri -from sphinx.util.osutil import ensuredir, movefile - +from sphinx.util import epoch_to_rfc1123, logging, requests, rfc1123_to_epoch, sha1 +from sphinx.util.images import get_image_extension, guess_mimetype, parse_data_uri +from sphinx.util.osutil import ensuredir logger = logging.getLogger(__name__) @@ -101,7 +99,7 @@ class ImageDownloader(BaseImageConverter): # append a suffix if URI does not contain suffix ext = get_image_extension(mimetype) newpath = os.path.join(self.imagedir, dirname, basename + ext) - movefile(path, newpath) + os.replace(path, newpath) self.app.env.original_image_uri.pop(path) self.app.env.original_image_uri[newpath] = node['uri'] path = newpath @@ -199,15 +197,15 @@ class ImageConverter(BaseImageConverter): def match(self, node: nodes.image) -> bool: if not self.app.builder.supported_image_types: return False + elif set(node['candidates']) & set(self.app.builder.supported_image_types): + # builder supports the image; no need to convert + return False elif self.available is None: # store the value to the class variable to share it during the build self.__class__.available = self.is_available() if not self.available: return False - elif set(node['candidates']) & set(self.app.builder.supported_image_types): - # builder supports the image; no need to convert - return False else: rule = self.get_conversion_rule(node) if rule: diff --git a/sphinx/transforms/references.py b/sphinx/transforms/references.py index e74d9657d..cd564d9eb 100644 --- a/sphinx/transforms/references.py +++ b/sphinx/transforms/references.py @@ -4,14 +4,13 @@ Docutils transforms used by Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from typing import Any, Dict -from docutils import nodes -from docutils.transforms.references import DanglingReferences, Substitutions +from docutils.transforms.references import DanglingReferences from sphinx.transforms import SphinxTransform @@ -20,17 +19,6 @@ if False: from sphinx.application import Sphinx -class SubstitutionDefinitionsRemover(SphinxTransform): - """Remove ``substitution_definition node from doctrees.""" - - # should be invoked after Substitutions process - default_priority = Substitutions.default_priority + 1 - - def apply(self, **kwargs: Any) -> None: - for node in self.document.traverse(nodes.substitution_definition): - node.parent.remove(node) - - class SphinxDanglingReferences(DanglingReferences): """DanglingReferences transform which does not output info messages.""" @@ -56,7 +44,6 @@ class SphinxDomains(SphinxTransform): def setup(app: "Sphinx") -> Dict[str, Any]: - app.add_transform(SubstitutionDefinitionsRemover) app.add_transform(SphinxDanglingReferences) app.add_transform(SphinxDomains) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 082c5caa3..2fbc182e5 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -4,7 +4,7 @@ Utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -25,33 +25,29 @@ from datetime import datetime from importlib import import_module from os import path from time import mktime, strptime -from typing import Any, Callable, Dict, IO, Iterable, Iterator, List, Pattern, Set, Tuple -from urllib.parse import urlsplit, urlunsplit, quote_plus, parse_qsl, urlencode +from typing import IO, Any, Callable, Dict, Iterable, Iterator, List, Pattern, Set, Tuple +from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning -from sphinx.errors import ( - PycodeError, SphinxParallelError, ExtensionError, FiletypeNotFoundError -) +from sphinx.errors import (ExtensionError, FiletypeNotFoundError, PycodeError, + SphinxParallelError) from sphinx.locale import __ -from sphinx.util import logging -from sphinx.util.console import strip_colors, colorize, bold, term_width_line # type: ignore -from sphinx.util.typing import PathMatcher from sphinx.util import smartypants # noqa - +from sphinx.util import logging +from sphinx.util.console import bold, colorize, strip_colors, term_width_line # type: ignore +from sphinx.util.matching import patfilter # noqa +from sphinx.util.nodes import (caption_ref_re, explicit_title_re, # noqa + nested_parse_with_titles, split_explicit_title) # import other utilities; partly for backwards compatibility, so don't # prune unused ones indiscriminately -from sphinx.util.osutil import ( # noqa - SEP, os_path, relative_uri, ensuredir, walk, mtimes_of_files, movefile, - copyfile, copytimes, make_filename) -from sphinx.util.nodes import ( # noqa - nested_parse_with_titles, split_explicit_title, explicit_title_re, - caption_ref_re) -from sphinx.util.matching import patfilter # noqa - +from sphinx.util.osutil import (SEP, copyfile, copytimes, ensuredir, make_filename, # noqa + movefile, mtimes_of_files, os_path, relative_uri, walk) +from sphinx.util.typing import PathMatcher if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx @@ -241,10 +237,12 @@ _DEBUG_HEADER = '''\ def save_traceback(app: "Sphinx") -> str: """Save the current exception's traceback in a temporary file.""" - import sphinx - import jinja2 - import docutils import platform + + import docutils + import jinja2 + + import sphinx exc = sys.exc_info()[1] if isinstance(exc, SphinxParallelError): exc_format = '(Error in parallel process)\n' + exc.traceback diff --git a/sphinx/util/build_phase.py b/sphinx/util/build_phase.py index d6193b400..07a5ee7cd 100644 --- a/sphinx/util/build_phase.py +++ b/sphinx/util/build_phase.py @@ -4,7 +4,7 @@ Build phase of Sphinx application. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py index 0edea128c..1549fbf75 100644 --- a/sphinx/util/cfamily.py +++ b/sphinx/util/cfamily.py @@ -4,16 +4,14 @@ Utility functions common to the C and C++ domains. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re import warnings from copy import deepcopy -from typing import ( - Any, Callable, List, Match, Optional, Pattern, Tuple, Union -) +from typing import Any, Callable, List, Match, Optional, Pattern, Tuple, Union from docutils import nodes from docutils.nodes import TextElement diff --git a/sphinx/util/compat.py b/sphinx/util/compat.py index 7c55c4ec7..dbe902be7 100644 --- a/sphinx/util/compat.py +++ b/sphinx/util/compat.py @@ -4,7 +4,7 @@ modules for backward compatibility - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/console.py b/sphinx/util/console.py index d429be602..3ea5b9573 100644 --- a/sphinx/util/console.py +++ b/sphinx/util/console.py @@ -4,7 +4,7 @@ Format colored console output. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -32,9 +32,9 @@ def terminal_safe(s: str) -> str: def get_terminal_width() -> int: """Borrowed from the py lib.""" try: - import termios import fcntl import struct + import termios call = fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('hhhh', 0, 0, 0, 0)) height, width = struct.unpack('hhhh', call)[:2] terminal_width = width diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py index c07bc7f66..3fc72340a 100644 --- a/sphinx/util/docfields.py +++ b/sphinx/util/docfields.py @@ -5,13 +5,12 @@ "Doc fields" are reST field lists in object descriptions that will be domain-specifically transformed to a more appealing presentation. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import warnings -from typing import Any, Dict, List, Tuple, Union -from typing import cast +from typing import Any, Dict, List, Tuple, Union, cast from docutils import nodes from docutils.nodes import Node @@ -23,8 +22,9 @@ from sphinx.util.typing import TextlikeNode if False: # For type annotation from typing import Type # for python3.5.1 - from sphinx.environment import BuildEnvironment + from sphinx.directive import ObjectDescription + from sphinx.environment import BuildEnvironment def _is_single_paragraph(node: nodes.field_body) -> bool: @@ -295,6 +295,7 @@ class DocFieldTransformer: self.directive.domain, target, contnode=content[0], + env=self.directive.state.document.settings.env ) if _is_single_paragraph(field_body): paragraph = cast(nodes.paragraph, field_body[0]) diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 67a008643..ac778af87 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -4,7 +4,7 @@ Utilities for docstring processing. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -17,7 +17,6 @@ from docutils.parsers.rst.states import Body from sphinx.deprecation import RemovedInSphinx50Warning - field_list_item_re = re.compile(Body.patterns['field_marker']) diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 3ba7813b6..ce50c7ab1 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -4,7 +4,7 @@ Utility functions for docutils. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,8 +15,7 @@ from copy import copy from distutils.version import LooseVersion from os import path from types import ModuleType -from typing import Any, Callable, Dict, Generator, IO, List, Optional, Set, Tuple -from typing import cast +from typing import IO, Any, Callable, Dict, Generator, List, Optional, Set, Tuple, cast import docutils from docutils import nodes @@ -24,7 +23,7 @@ from docutils.io import FileOutput from docutils.nodes import Element, Node, system_message from docutils.parsers.rst import Directive, directives, roles from docutils.parsers.rst.states import Inliner -from docutils.statemachine import StateMachine, State, StringList +from docutils.statemachine import State, StateMachine, StringList from docutils.utils import Reporter, unescape from sphinx.errors import SphinxError @@ -37,6 +36,7 @@ report_re = re.compile('^(.+?:(?:\\d+)?): \\((DEBUG|INFO|WARNING|ERROR|SEVERE)/( if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.builders import Builder from sphinx.config import Config from sphinx.environment import BuildEnvironment diff --git a/sphinx/util/fileutil.py b/sphinx/util/fileutil.py index eec1ae463..466c28135 100644 --- a/sphinx/util/fileutil.py +++ b/sphinx/util/fileutil.py @@ -4,7 +4,7 @@ File utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 41407f4e1..8341dfffe 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -4,7 +4,7 @@ Builder superclass for all builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import gettext @@ -34,7 +34,7 @@ if False: logger = logging.getLogger(__name__) -LocaleFileInfoBase = namedtuple('CatalogInfo', 'base_dir,domain,charset') +LocaleFileInfoBase = namedtuple('LocaleFileInfoBase', 'base_dir,domain,charset') class CatalogInfo(LocaleFileInfoBase): @@ -236,7 +236,9 @@ date_format_mappings = { '%X': 'medium', # Locale’s appropriate time representation. '%y': 'YY', # Year without century as a zero-padded decimal number. '%Y': 'yyyy', # Year with century as a decimal number. - '%Z': 'zzzz', # Time zone name (no characters if no time zone exists). + '%Z': 'zzz', # Time zone name (no characters if no time zone exists). + '%z': 'ZZZ', # UTC offset in the form ±HHMM[SS[.ffffff]] + # (empty string if the object is naive). '%%': '%', } diff --git a/sphinx/util/images.py b/sphinx/util/images.py index 0ddf64908..81a321818 100644 --- a/sphinx/util/images.py +++ b/sphinx/util/images.py @@ -4,7 +4,7 @@ Image utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index f2cd8070b..202e170c1 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -4,7 +4,7 @@ Helpers for inspecting Python modules. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,12 +18,9 @@ import types import typing import warnings from functools import partial, partialmethod -from inspect import ( # NOQA - Parameter, isclass, ismethod, ismethoddescriptor, ismodule -) +from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA from io import StringIO -from typing import Any, Callable, Dict, Mapping, List, Optional, Tuple -from typing import cast +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, cast from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.pycode.ast import ast # for py35-37 @@ -33,16 +30,16 @@ from sphinx.util.typing import ForwardRef from sphinx.util.typing import stringify as stringify_annotation if sys.version_info > (3, 7): - from types import ( - ClassMethodDescriptorType, - MethodDescriptorType, - WrapperDescriptorType - ) + from types import ClassMethodDescriptorType, MethodDescriptorType, WrapperDescriptorType else: ClassMethodDescriptorType = type(object.__init__) 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) @@ -62,14 +59,6 @@ def getargspec(func: Callable) -> Any: methods.""" warnings.warn('sphinx.ext.inspect.getargspec() is deprecated', RemovedInSphinx50Warning, stacklevel=2) - # On 3.5+, signature(int) or similar raises ValueError. On 3.4, it - # succeeds with a bogus signature. We want a TypeError uniformly, to - # match historical behavior. - if (isinstance(func, type) and - is_builtin_class_method(func, "__new__") and - is_builtin_class_method(func, "__init__")): - raise TypeError( - "can't compute signature for built-in type {}".format(func)) sig = inspect.signature(func) @@ -122,7 +111,11 @@ def getargspec(func: Callable) -> Any: def unwrap(obj: Any) -> Any: """Get an original object from wrapped object (wrapped functions).""" try: - return inspect.unwrap(obj) + if hasattr(obj, '__sphinx_mock__'): + # Skip unwrapping mock object to avoid RecursionError + return obj + else: + return inspect.unwrap(obj) except ValueError: # might be a mock object return obj @@ -148,6 +141,81 @@ def unwrap_all(obj: Any, *, stop: Callable = None) -> Any: return obj +def getall(obj: Any) -> Optional[Sequence[str]]: + """Get __all__ attribute of the module as dict. + + Return None if given *obj* does not have __all__. + Raises AttributeError if given *obj* raises an error on accessing __all__. + Raises ValueError if given *obj* have invalid __all__. + """ + __all__ = safe_getattr(obj, '__all__', None) + if __all__ is None: + return None + else: + if (isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__)): + return __all__ + else: + raise ValueError(__all__) + + +def getannotations(obj: Any) -> Mapping[str, Any]: + """Get __annotations__ from given *obj* safely. + + Raises AttributeError if given *obj* raises an error on accessing __attribute__. + """ + __annotations__ = safe_getattr(obj, '__annotations__', None) + if isinstance(__annotations__, Mapping): + return __annotations__ + else: + return {} + + +def getmro(obj: Any) -> Tuple["Type", ...]: + """Get __mro__ from given *obj* safely. + + Raises AttributeError if given *obj* raises an error on accessing __mro__. + """ + __mro__ = safe_getattr(obj, '__mro__', None) + if isinstance(__mro__, tuple): + return __mro__ + else: + return tuple() + + +def getslots(obj: Any) -> Optional[Dict]: + """Get __slots__ attribute of the class as dict. + + Return None if gienv *obj* does not have __slots__. + Raises AttributeError if given *obj* raises an error on accessing __slots__. + Raises TypeError if given *obj* is not a class. + Raises ValueError if given *obj* have invalid __slots__. + """ + if not inspect.isclass(obj): + raise TypeError + + __slots__ = safe_getattr(obj, '__slots__', None) + if __slots__ is None: + return None + elif isinstance(__slots__, dict): + return __slots__ + elif isinstance(__slots__, str): + return {__slots__: None} + elif isinstance(__slots__, (list, tuple)): + return {e: None for e in __slots__} + else: + raise ValueError + + +def isNewType(obj: Any) -> bool: + """Check the if object is a kind of NewType.""" + __module__ = safe_getattr(obj, '__module__', None) + __qualname__ = safe_getattr(obj, '__qualname__', None) + if __module__ == 'typing' and __qualname__ == 'NewType..new_type': + return True + else: + return False + + def isenumclass(x: Any) -> bool: """Check if the object is subclass of enum.""" return inspect.isclass(x) and issubclass(x, enum.Enum) @@ -304,7 +372,7 @@ def iscoroutinefunction(obj: Any) -> bool: def isproperty(obj: Any) -> bool: """Check if the object is property.""" - if sys.version_info > (3, 8): + if sys.version_info >= (3, 8): from functools import cached_property # cached_property is available since py3.8 if isinstance(obj, cached_property): return True @@ -320,6 +388,9 @@ def isgenericalias(obj: Any) -> bool: elif (hasattr(types, 'GenericAlias') and # only for py39+ isinstance(obj, types.GenericAlias)): # type: ignore return True + elif (hasattr(typing, '_SpecialGenericAlias') and # for py39+ + isinstance(obj, typing._SpecialGenericAlias)): # type: ignore + return True else: return False @@ -428,6 +499,19 @@ def is_builtin_class_method(obj: Any, attr_name: str) -> bool: return getattr(builtins, name, None) is cls +class DefaultValue: + """A simple wrapper for default value of the parameters of overload functions.""" + + def __init__(self, value: str) -> None: + self.value = value + + def __eq__(self, other: object) -> bool: + return self.value == other + + def __repr__(self) -> str: + return self.value + + def _should_unwrap(subject: Callable) -> bool: """Check the function should be unwrapped on getting signature.""" if (safe_getattr(subject, '__globals__', None) and @@ -439,14 +523,20 @@ def _should_unwrap(subject: Callable) -> bool: return False -def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False, +def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = None, type_aliases: Dict = {}) -> inspect.Signature: """Return a Signature object for the given *subject*. :param bound_method: Specify *subject* is a bound method or not :param follow_wrapped: Same as ``inspect.signature()``. - Defaults to ``False`` (get a signature of *subject*). """ + + if follow_wrapped is None: + follow_wrapped = True + else: + warnings.warn('The follow_wrapped argument of sphinx.util.inspect.signature() is ' + 'deprecated', RemovedInSphinx50Warning, stacklevel=2) + try: try: if _should_unwrap(subject): @@ -469,10 +559,10 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo raise try: - # Update unresolved annotations using ``get_type_hints()``. + # Resolve annotations using ``get_type_hints()`` and type_aliases. annotations = typing.get_type_hints(subject, None, type_aliases) for i, param in enumerate(parameters): - if isinstance(param.annotation, str) and param.name in annotations: + if param.name in annotations: parameters[i] = param.replace(annotation=annotations[param.name]) if 'return' in annotations: return_annotation = annotations['return'] @@ -627,7 +717,7 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signatu if defaults[i] is Parameter.empty: default = Parameter.empty else: - default = ast_unparse(defaults[i], code) + default = DefaultValue(ast_unparse(defaults[i], code)) annotation = ast_unparse(arg.annotation, code) or Parameter.empty params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY, @@ -637,7 +727,7 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signatu if defaults[i + posonlyargs] is Parameter.empty: default = Parameter.empty else: - default = ast_unparse(defaults[i + posonlyargs], code) + default = DefaultValue(ast_unparse(defaults[i + posonlyargs], code)) annotation = ast_unparse(arg.annotation, code) or Parameter.empty params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD, diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py index 1e3572323..b1af1bfb9 100644 --- a/sphinx/util/inventory.py +++ b/sphinx/util/inventory.py @@ -4,18 +4,17 @@ Inventory utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import re import zlib -from typing import Callable, IO, Iterator +from typing import IO, Callable, Iterator from sphinx.util import logging from sphinx.util.typing import Inventory - BUFSIZE = 16 * 1024 logger = logging.getLogger(__name__) diff --git a/sphinx/util/jsdump.py b/sphinx/util/jsdump.py index 1f0e258cb..114fd7075 100644 --- a/sphinx/util/jsdump.py +++ b/sphinx/util/jsdump.py @@ -5,12 +5,12 @@ This module implements a simple JavaScript serializer. Uses the basestring encode function from simplejson by Bob Ippolito. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re -from typing import Any, Dict, IO, List, Match, Union +from typing import IO, Any, Dict, List, Match, Union _str_re = re.compile(r'"(\\\\|\\"|[^"])*"') _int_re = re.compile(r'\d+') diff --git a/sphinx/util/jsonimpl.py b/sphinx/util/jsonimpl.py index 35501f03a..b038fd4db 100644 --- a/sphinx/util/jsonimpl.py +++ b/sphinx/util/jsonimpl.py @@ -4,18 +4,17 @@ JSON serializer implementation wrapper. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import json import warnings from collections import UserString -from typing import Any, IO +from typing import IO, Any from sphinx.deprecation import RemovedInSphinx40Warning - warnings.warn('sphinx.util.jsonimpl is deprecated', RemovedInSphinx40Warning, stacklevel=2) diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index 5889f3860..09780723a 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -4,7 +4,7 @@ Logging utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,7 +12,7 @@ import logging import logging.handlers from collections import defaultdict from contextlib import contextmanager -from typing import Any, Dict, Generator, IO, List, Tuple, Union +from typing import IO, Any, Dict, Generator, List, Tuple, Union from docutils import nodes from docutils.nodes import Node @@ -24,6 +24,7 @@ from sphinx.util.console import colorize if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.application import Sphinx diff --git a/sphinx/util/matching.py b/sphinx/util/matching.py index 2d37866a6..d33ae0333 100644 --- a/sphinx/util/matching.py +++ b/sphinx/util/matching.py @@ -4,7 +4,7 @@ Pattern-matching utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/math.py b/sphinx/util/math.py index 2a9bd66d7..229e09d36 100644 --- a/sphinx/util/math.py +++ b/sphinx/util/math.py @@ -4,7 +4,7 @@ Utility functions for math. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index b4d796f61..d5e43e716 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -4,15 +4,14 @@ Docutils node-related utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re import unicodedata import warnings -from typing import Any, Callable, Iterable, List, Set, Tuple -from typing import cast +from typing import Any, Callable, Iterable, List, Set, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node @@ -28,6 +27,7 @@ from sphinx.util import logging if False: # For type annotation from typing import Type # for python3.5.1 + from sphinx.builders import Builder from sphinx.domain import IndexEntry from sphinx.environment import BuildEnvironment @@ -469,46 +469,46 @@ def _make_id(string: str) -> str: _non_id_chars = re.compile('[^a-zA-Z0-9._]+') _non_id_at_ends = re.compile('^[-0-9._]+|-+$') _non_id_translate = { - 0x00f8: u'o', # o with stroke - 0x0111: u'd', # d with stroke - 0x0127: u'h', # h with stroke - 0x0131: u'i', # dotless i - 0x0142: u'l', # l with stroke - 0x0167: u't', # t with stroke - 0x0180: u'b', # b with stroke - 0x0183: u'b', # b with topbar - 0x0188: u'c', # c with hook - 0x018c: u'd', # d with topbar - 0x0192: u'f', # f with hook - 0x0199: u'k', # k with hook - 0x019a: u'l', # l with bar - 0x019e: u'n', # n with long right leg - 0x01a5: u'p', # p with hook - 0x01ab: u't', # t with palatal hook - 0x01ad: u't', # t with hook - 0x01b4: u'y', # y with hook - 0x01b6: u'z', # z with stroke - 0x01e5: u'g', # g with stroke - 0x0225: u'z', # z with hook - 0x0234: u'l', # l with curl - 0x0235: u'n', # n with curl - 0x0236: u't', # t with curl - 0x0237: u'j', # dotless j - 0x023c: u'c', # c with stroke - 0x023f: u's', # s with swash tail - 0x0240: u'z', # z with swash tail - 0x0247: u'e', # e with stroke - 0x0249: u'j', # j with stroke - 0x024b: u'q', # q with hook tail - 0x024d: u'r', # r with stroke - 0x024f: u'y', # y with stroke + 0x00f8: 'o', # o with stroke + 0x0111: 'd', # d with stroke + 0x0127: 'h', # h with stroke + 0x0131: 'i', # dotless i + 0x0142: 'l', # l with stroke + 0x0167: 't', # t with stroke + 0x0180: 'b', # b with stroke + 0x0183: 'b', # b with topbar + 0x0188: 'c', # c with hook + 0x018c: 'd', # d with topbar + 0x0192: 'f', # f with hook + 0x0199: 'k', # k with hook + 0x019a: 'l', # l with bar + 0x019e: 'n', # n with long right leg + 0x01a5: 'p', # p with hook + 0x01ab: 't', # t with palatal hook + 0x01ad: 't', # t with hook + 0x01b4: 'y', # y with hook + 0x01b6: 'z', # z with stroke + 0x01e5: 'g', # g with stroke + 0x0225: 'z', # z with hook + 0x0234: 'l', # l with curl + 0x0235: 'n', # n with curl + 0x0236: 't', # t with curl + 0x0237: 'j', # dotless j + 0x023c: 'c', # c with stroke + 0x023f: 's', # s with swash tail + 0x0240: 'z', # z with swash tail + 0x0247: 'e', # e with stroke + 0x0249: 'j', # j with stroke + 0x024b: 'q', # q with hook tail + 0x024d: 'r', # r with stroke + 0x024f: 'y', # y with stroke } _non_id_translate_digraphs = { - 0x00df: u'sz', # ligature sz - 0x00e6: u'ae', # ae - 0x0153: u'oe', # ligature oe - 0x0238: u'db', # db digraph - 0x0239: u'qp', # qp digraph + 0x00df: 'sz', # ligature sz + 0x00e6: 'ae', # ae + 0x0153: 'oe', # ligature oe + 0x0238: 'db', # db digraph + 0x0239: 'qp', # qp digraph } diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 0390b038d..53bffd929 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -4,7 +4,7 @@ Operating system-related utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -20,7 +20,7 @@ from io import StringIO from os import path from typing import Any, Generator, Iterator, List, Optional, Tuple -from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning try: # for ALT Linux (#6712) @@ -103,6 +103,9 @@ def mtimes_of_files(dirnames: List[str], suffix: str) -> Iterator[float]: def movefile(source: str, dest: str) -> None: """Move a file, removing the destination if it exists.""" + warnings.warn('sphinx.util.osutil.movefile() is deprecated for removal. ' + 'Please use os.replace() instead.', + RemovedInSphinx50Warning, stacklevel=2) if os.path.exists(dest): try: os.unlink(dest) @@ -222,14 +225,14 @@ class FileAvoidWrite: self._io.close() try: - with open(self._path) as old_f: + with open(self._path, encoding='utf-8') as old_f: old_content = old_f.read() if old_content == buf: return except OSError: pass - with open(self._path, 'w') as f: + with open(self._path, 'w', encoding='utf-8') as f: f.write(buf) def __enter__(self) -> "FileAvoidWrite": diff --git a/sphinx/util/parallel.py b/sphinx/util/parallel.py index ddcdaa316..ab27a5128 100644 --- a/sphinx/util/parallel.py +++ b/sphinx/util/parallel.py @@ -4,7 +4,7 @@ Parallel building utilities. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/png.py b/sphinx/util/png.py index 22c35d991..2ab5a836f 100644 --- a/sphinx/util/png.py +++ b/sphinx/util/png.py @@ -4,7 +4,7 @@ PNG image manipulation helpers. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,7 +12,6 @@ import binascii import struct from typing import Optional - LEN_IEND = 12 LEN_DEPTH = 22 diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 50eee5ce3..87c38f72e 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -4,7 +4,7 @@ Stuff for Python version compatibility. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -21,7 +21,6 @@ from sphinx.util import logging from sphinx.util.console import terminal_safe from sphinx.util.typing import NoneType - logger = logging.getLogger(__name__) @@ -32,8 +31,8 @@ logger = logging.getLogger(__name__) # support for running 2to3 over config files def convert_with_2to3(filepath: str) -> str: try: - from lib2to3.refactor import RefactoringTool, get_fixers_from_package from lib2to3.pgen2.parse import ParseError + from lib2to3.refactor import RefactoringTool, get_fixers_from_package except ImportError as exc: # python 3.9.0a6+ emits PendingDeprecationWarning for lib2to3. # Additionally, removal of the module is still discussed at PEP-594. diff --git a/sphinx/util/requests.py b/sphinx/util/requests.py index b3fc8bc35..8ae435d41 100644 --- a/sphinx/util/requests.py +++ b/sphinx/util/requests.py @@ -4,7 +4,7 @@ Simple requests package loader - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,6 +18,7 @@ import requests import sphinx from sphinx.config import Config +from sphinx.deprecation import RemovedInSphinx50Warning try: from requests.packages.urllib3.exceptions import SSLError @@ -43,6 +44,10 @@ useragent_header = [('User-Agent', def is_ssl_error(exc: Exception) -> bool: """Check an exception is SSLError.""" + warnings.warn( + "is_ssl_error() is outdated and likely returns incorrect results " + "for modern versions of Requests.", + RemovedInSphinx50Warning) if isinstance(exc, SSLError): return True else: diff --git a/sphinx/util/rst.py b/sphinx/util/rst.py index e93849b70..79ede3432 100644 --- a/sphinx/util/rst.py +++ b/sphinx/util/rst.py @@ -4,7 +4,7 @@ reST helper functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,12 +18,10 @@ from docutils.parsers.rst import roles from docutils.parsers.rst.languages import en as english from docutils.statemachine import StringList from docutils.utils import Reporter -from jinja2 import Environment -from jinja2 import environmentfilter +from jinja2 import Environment, environmentfilter from sphinx.locale import __ -from sphinx.util import docutils -from sphinx.util import logging +from sphinx.util import docutils, logging logger = logging.getLogger(__name__) diff --git a/sphinx/util/smartypants.py b/sphinx/util/smartypants.py index 43f8bc724..ec6b2172c 100644 --- a/sphinx/util/smartypants.py +++ b/sphinx/util/smartypants.py @@ -32,7 +32,6 @@ from docutils.utils import smartquotes from sphinx.util.docutils import __version_info__ as docutils_version - langquotes = {'af': '“”‘’', 'af-x-altquot': '„”‚’', 'bg': '„“‚‘', # Bulgarian, https://bg.wikipedia.org/wiki/Кавички diff --git a/sphinx/util/stemmer/__init__.py b/sphinx/util/stemmer/__init__.py index 94aaa136d..6470dfe2b 100644 --- a/sphinx/util/stemmer/__init__.py +++ b/sphinx/util/stemmer/__init__.py @@ -4,7 +4,7 @@ Word stemming utilities for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/tags.py b/sphinx/util/tags.py index 1662a06f0..c50231220 100644 --- a/sphinx/util/tags.py +++ b/sphinx/util/tags.py @@ -2,7 +2,7 @@ sphinx.util.tags ~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/template.py b/sphinx/util/template.py index 8785928a9..a61008602 100644 --- a/sphinx/util/template.py +++ b/sphinx/util/template.py @@ -4,7 +4,7 @@ Templates utility functions for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/util/texescape.py b/sphinx/util/texescape.py index afa1c349e..b67dcfe82 100644 --- a/sphinx/util/texescape.py +++ b/sphinx/util/texescape.py @@ -4,7 +4,7 @@ TeX escaping helper. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,7 +13,6 @@ from typing import Dict from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias - tex_replacements = [ # map TeX special chars ('$', r'\$'), diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 8eca67220..e85c40cdf 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -4,18 +4,18 @@ The composit types for Sphinx. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import sys import typing -from typing import Any, Callable, Dict, Generator, List, Tuple, TypeVar, Union +from struct import Struct +from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, TypeVar, Union from docutils import nodes from docutils.parsers.rst.states import Inliner - if sys.version_info > (3, 7): from typing import ForwardRef else: @@ -30,6 +30,10 @@ else: ref = _ForwardRef(self.arg) return ref._eval_type(globalns, localns) +if False: + # For type annotation + from typing import Type # NOQA # for python3.5.1 + # An entry of Directive.option_spec DirectiveOption = Callable[[str], Any] @@ -54,21 +58,247 @@ TitleGetter = Callable[[nodes.Node], str] Inventory = Dict[str, Dict[str, Tuple[str, str, str, str]]] +def get_type_hints(obj: Any, globalns: Dict = None, localns: Dict = None) -> Dict[str, Any]: + """Return a dictionary containing type hints for a function, method, module or class object. + + This is a simple wrapper of `typing.get_type_hints()` that does not raise an error on + runtime. + """ + from sphinx.util.inspect import safe_getattr # lazy loading + + try: + return typing.get_type_hints(obj, globalns, localns) + except NameError: + # Failed to evaluate ForwardRef (maybe TYPE_CHECKING) + return safe_getattr(obj, '__annotations__', {}) + except TypeError: + return {} + except KeyError: + # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084) + return {} + except AttributeError: + # AttributeError is raised on 3.5.2 (fixed by 3.5.3) + return {} + + def is_system_TypeVar(typ: Any) -> bool: """Check *typ* is system defined TypeVar.""" modname = getattr(typ, '__module__', '') - return modname == 'typing' and isinstance(typ, TypeVar) # type: ignore + return modname == 'typing' and isinstance(typ, TypeVar) + + +def restify(cls: Optional["Type"]) -> str: + """Convert python class to a reST reference.""" + from sphinx.util import inspect # lazy loading + + if cls is None or cls is NoneType: + return ':obj:`None`' + elif cls is Ellipsis: + return '...' + elif cls is Struct: + # Before Python 3.9, struct.Struct class has incorrect __module__. + return ':class:`struct.Struct`' + elif inspect.isNewType(cls): + return ':class:`%s`' % cls.__name__ + elif cls.__module__ in ('__builtin__', 'builtins'): + return ':class:`%s`' % cls.__name__ + else: + if sys.version_info >= (3, 7): # py37+ + return _restify_py37(cls) + else: + return _restify_py36(cls) + + +def _restify_py37(cls: Optional["Type"]) -> str: + """Convert python class to a reST reference.""" + from sphinx.util import inspect # lazy loading + + if (inspect.isgenericalias(cls) and + cls.__module__ == 'typing' and cls.__origin__ is Union): + # Union + if len(cls.__args__) > 1 and cls.__args__[-1] is NoneType: + if len(cls.__args__) > 2: + args = ', '.join(restify(a) for a in cls.__args__[:-1]) + return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % args + else: + return ':obj:`Optional`\\ [%s]' % restify(cls.__args__[0]) + else: + args = ', '.join(restify(a) for a in cls.__args__) + return ':obj:`Union`\\ [%s]' % args + elif inspect.isgenericalias(cls): + if getattr(cls, '_name', None): + if cls.__module__ == 'typing': + text = ':class:`%s`' % cls._name + else: + text = ':class:`%s.%s`' % (cls.__module__, cls._name) + else: + text = restify(cls.__origin__) + + if not hasattr(cls, '__args__'): + pass + elif all(is_system_TypeVar(a) for a in cls.__args__): + # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT]) + pass + elif cls.__module__ == 'typing' and cls._name == 'Callable': + args = ', '.join(restify(a) for a in cls.__args__[:-1]) + text += r"\ [[%s], %s]" % (args, restify(cls.__args__[-1])) + elif cls.__args__: + text += r"\ [%s]" % ", ".join(restify(a) for a in cls.__args__) + + return text + elif hasattr(cls, '__qualname__'): + if cls.__module__ == 'typing': + return ':class:`%s`' % cls.__qualname__ + else: + return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__) + elif hasattr(cls, '_name'): + # SpecialForm + if cls.__module__ == 'typing': + return ':obj:`%s`' % cls._name + else: + return ':obj:`%s.%s`' % (cls.__module__, cls._name) + elif isinstance(cls, ForwardRef): + return ':class:`%s`' % cls.__forward_arg__ + else: + # not a class (ex. TypeVar) + return ':obj:`%s.%s`' % (cls.__module__, cls.__name__) + + +def _restify_py36(cls: Optional["Type"]) -> str: + module = getattr(cls, '__module__', None) + if module == 'typing': + if getattr(cls, '_name', None): + qualname = cls._name + elif getattr(cls, '__qualname__', None): + qualname = cls.__qualname__ + elif getattr(cls, '__forward_arg__', None): + qualname = cls.__forward_arg__ + elif getattr(cls, '__origin__', None): + qualname = stringify(cls.__origin__) # ex. Union + else: + qualname = repr(cls).replace('typing.', '') + elif hasattr(cls, '__qualname__'): + qualname = '%s.%s' % (module, cls.__qualname__) + else: + qualname = repr(cls) + + if (isinstance(cls, typing.TupleMeta) and # type: ignore + not hasattr(cls, '__tuple_params__')): # for Python 3.6 + params = cls.__args__ + if params: + param_str = ', '.join(restify(p) for p in params) + return ':class:`%s`\\ [%s]' % (qualname, param_str) + else: + return ':class:`%s`' % qualname + elif isinstance(cls, typing.GenericMeta): + params = None + if hasattr(cls, '__args__'): + # for Python 3.5.2+ + if cls.__args__ is None or len(cls.__args__) <= 2: # type: ignore # NOQA + params = cls.__args__ # type: ignore + elif cls.__origin__ == Generator: # type: ignore + params = cls.__args__ # type: ignore + else: # typing.Callable + args = ', '.join(restify(arg) for arg in cls.__args__[:-1]) # type: ignore + result = restify(cls.__args__[-1]) # type: ignore + return ':class:`%s`\\ [[%s], %s]' % (qualname, args, result) + elif hasattr(cls, '__parameters__'): + # for Python 3.5.0 and 3.5.1 + params = cls.__parameters__ # type: ignore + + if params: + param_str = ', '.join(restify(p) for p in params) + return ':class:`%s`\\ [%s]' % (qualname, param_str) + else: + return ':class:`%s`' % qualname + elif (hasattr(typing, 'UnionMeta') and + isinstance(cls, typing.UnionMeta) and # type: ignore + hasattr(cls, '__union_params__')): # for Python 3.5 + params = cls.__union_params__ + if params is not None: + if len(params) == 2 and params[1] is NoneType: + return ':obj:`Optional`\\ [%s]' % restify(params[0]) + else: + param_str = ', '.join(restify(p) for p in params) + return ':obj:`%s`\\ [%s]' % (qualname, param_str) + else: + return ':obj:`%s`' % qualname + elif (hasattr(cls, '__origin__') and + cls.__origin__ is typing.Union): # for Python 3.5.2+ + params = cls.__args__ + if params is not None: + if len(params) > 1 and params[-1] is NoneType: + if len(params) > 2: + param_str = ", ".join(restify(p) for p in params[:-1]) + return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % param_str + else: + return ':obj:`Optional`\\ [%s]' % restify(params[0]) + else: + param_str = ', '.join(restify(p) for p in params) + return ':obj:`Union`\\ [%s]' % param_str + else: + return ':obj:`Union`' + elif (isinstance(cls, typing.CallableMeta) and # type: ignore + getattr(cls, '__args__', None) is not None and + hasattr(cls, '__result__')): # for Python 3.5 + # Skipped in the case of plain typing.Callable + args = cls.__args__ + if args is None: + return qualname + elif args is Ellipsis: + args_str = '...' + else: + formatted_args = (restify(a) for a in args) # type: ignore + args_str = '[%s]' % ', '.join(formatted_args) + + return ':class:`%s`\\ [%s, %s]' % (qualname, args_str, stringify(cls.__result__)) + elif (isinstance(cls, typing.TupleMeta) and # type: ignore + hasattr(cls, '__tuple_params__') and + hasattr(cls, '__tuple_use_ellipsis__')): # for Python 3.5 + params = cls.__tuple_params__ + if params is not None: + param_strings = [restify(p) for p in params] + if cls.__tuple_use_ellipsis__: + param_strings.append('...') + return ':class:`%s`\\ [%s]' % (qualname, ', '.join(param_strings)) + else: + return ':class:`%s`' % qualname + elif hasattr(cls, '__qualname__'): + if cls.__module__ == 'typing': + return ':class:`%s`' % cls.__qualname__ + else: + return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__) + elif hasattr(cls, '_name'): + # SpecialForm + if cls.__module__ == 'typing': + return ':obj:`%s`' % cls._name + else: + return ':obj:`%s.%s`' % (cls.__module__, cls._name) + elif hasattr(cls, '__name__'): + # not a class (ex. TypeVar) + return ':obj:`%s.%s`' % (cls.__module__, cls.__name__) + else: + # others (ex. Any) + if cls.__module__ == 'typing': + return ':obj:`%s`' % qualname + else: + return ':obj:`%s.%s`' % (cls.__module__, qualname) def stringify(annotation: Any) -> str: """Stringify type annotation object.""" + from sphinx.util import inspect # lazy loading + if isinstance(annotation, str): if annotation.startswith("'") and annotation.endswith("'"): # might be a double Forward-ref'ed type. Go unquoting. - return annotation[1:-2] + return annotation[1:-1] else: return annotation - elif isinstance(annotation, TypeVar): # type: ignore + elif isinstance(annotation, TypeVar): + return annotation.__name__ + elif inspect.isNewType(annotation): + # Could not get the module where it defiend return annotation.__name__ elif not annotation: return repr(annotation) @@ -79,6 +309,9 @@ def stringify(annotation: Any) -> str: return annotation.__qualname__ elif annotation is Ellipsis: return '...' + elif annotation is Struct: + # Before Python 3.9, struct.Struct class has incorrect __module__. + return 'struct.Struct' if sys.version_info >= (3, 7): # py37+ return _stringify_py37(annotation) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 7307b13d1..6e5a9eb26 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -5,7 +5,7 @@ Implements the low-level algorithms Sphinx uses for the versioning of doctrees. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import pickle diff --git a/sphinx/writers/__init__.py b/sphinx/writers/__init__.py index 97543df0d..34a5d36f3 100644 --- a/sphinx/writers/__init__.py +++ b/sphinx/writers/__init__.py @@ -4,6 +4,6 @@ Custom docutils writers. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 8813c2d12..d3e7e03a4 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -4,7 +4,7 @@ docutils writers handling Sphinx' custom nodes. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,17 +13,17 @@ import os import posixpath import re import warnings -from typing import Any, Iterable, Tuple -from typing import cast +from typing import Any, Iterable, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node, Text -from docutils.writers.html4css1 import Writer, HTMLTranslator as BaseTranslator +from docutils.writers.html4css1 import HTMLTranslator as BaseTranslator +from docutils.writers.html4css1 import Writer from sphinx import addnodes from sphinx.builders import Builder -from sphinx.deprecation import RemovedInSphinx40Warning -from sphinx.locale import admonitionlabels, _, __ +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning +from sphinx.locale import _, __, admonitionlabels from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator from sphinx.util.images import get_image_size @@ -100,11 +100,6 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): self.docnames = [self.builder.current_docname] # for singlehtml builder self.manpages_url = self.config.manpages_url self.protect_literal_text = 0 - self.permalink_text = self.config.html_add_permalinks - # support backwards-compatible setting to a bool - if not isinstance(self.permalink_text, str): - self.permalink_text = '¶' if self.permalink_text else '' - self.permalink_text = self.encode(self.permalink_text) self.secnumber_suffix = self.config.html_secnumber_suffix self.param_separator = '' self.optional_param_level = 0 @@ -129,8 +124,10 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): def visit_desc_signature(self, node: Element) -> None: # the id is set automatically self.body.append(self.starttag(node, 'dt')) + self.protect_literal_text += 1 def depart_desc_signature(self, node: Element) -> None: + self.protect_literal_text -= 1 if not node.get('is_multiline'): self.add_permalink_ref(node, _('Permalink to this definition')) self.body.append('\n') @@ -315,7 +312,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): if figure_id in self.builder.fignumbers.get(key, {}): self.body.append('') - prefix = self.builder.config.numfig_format.get(figtype) + prefix = self.config.numfig_format.get(figtype) if prefix is None: msg = __('numfig_format is not defined for %s') % figtype logger.warning(msg) @@ -333,9 +330,10 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): append_fignumber(figtype, node['ids'][0]) def add_permalink_ref(self, node: Element, title: str) -> None: - if node['ids'] and self.permalink_text and self.builder.add_permalinks: + if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks: format = '%s' - self.body.append(format % (node['ids'][0], title, self.permalink_text)) + self.body.append(format % (node['ids'][0], title, + self.config.html_permalinks_icon)) def generate_targets_for_listing(self, node: Element) -> None: """Generate hyperlink targets for listings. @@ -398,6 +396,10 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): # there's a classifier. pass else: + if isinstance(node.parent.parent.parent, addnodes.glossary): + # add permalink if glossary terms + self.add_permalink_ref(node, _('Permalink to this term')) + self.body.append('') # overwritten @@ -410,7 +412,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): def depart_title(self, node: Element) -> None: close_tag = self.context[-1] - if (self.permalink_text and self.builder.add_permalinks and + if (self.config.html_permalinks and self.builder.add_permalinks and node.parent.hasattr('ids') and node.parent['ids']): # add permalink anchor if close_tag.startswith('%s' % ( _('Permalink to this headline'), - self.permalink_text)) + self.config.html_permalinks_icon)) elif isinstance(node.parent, nodes.table): self.body.append('') self.add_permalink_ref(node.parent, _('Permalink to this table')) @@ -439,14 +441,10 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): linenos = node.get('linenos', False) highlight_args = node.get('highlight_args', {}) highlight_args['force'] = node.get('force', False) - if lang is self.builder.config.highlight_language: - # only pass highlighter options for original language - opts = self.builder.config.highlight_options - else: - opts = {} + opts = self.config.highlight_options.get(lang, {}) - if linenos and self.builder.config.html_codeblock_linenos_style: - linenos = self.builder.config.html_codeblock_linenos_style + if linenos and self.config.html_codeblock_linenos_style: + linenos = self.config.html_codeblock_linenos_style highlighted = self.highlighter.highlight_block( node.rawsource, lang, opts=opts, linenos=linenos, @@ -842,3 +840,9 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): def unknown_visit(self, node: Node) -> None: raise NotImplementedError('Unknown node: ' + node.__class__.__name__) + + @property + def permalink_text(self) -> str: + warnings.warn('HTMLTranslator.permalink_text is deprecated.', + RemovedInSphinx50Warning, stacklevel=2) + return self.config.html_permalinks_icon diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index 4ceeaafdf..5666e4d02 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -4,7 +4,7 @@ Experimental docutils writers for HTML5 handling Sphinx' custom nodes. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,8 +12,7 @@ import os import posixpath import re import warnings -from typing import Any, Iterable, Tuple -from typing import cast +from typing import Any, Iterable, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node, Text @@ -21,8 +20,8 @@ from docutils.writers.html5_polyglot import HTMLTranslator as BaseTranslator from sphinx import addnodes from sphinx.builders import Builder -from sphinx.deprecation import RemovedInSphinx40Warning -from sphinx.locale import admonitionlabels, _, __ +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning +from sphinx.locale import _, __, admonitionlabels from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator from sphinx.util.images import get_image_size @@ -72,11 +71,6 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): self.docnames = [self.builder.current_docname] # for singlehtml builder self.manpages_url = self.config.manpages_url self.protect_literal_text = 0 - self.permalink_text = self.config.html_add_permalinks - # support backwards-compatible setting to a bool - if not isinstance(self.permalink_text, str): - self.permalink_text = '¶' if self.permalink_text else '' - self.permalink_text = self.encode(self.permalink_text) self.secnumber_suffix = self.config.html_secnumber_suffix self.param_separator = '' self.optional_param_level = 0 @@ -101,8 +95,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): def visit_desc_signature(self, node: Element) -> None: # the id is set automatically self.body.append(self.starttag(node, 'dt')) + self.protect_literal_text += 1 def depart_desc_signature(self, node: Element) -> None: + self.protect_literal_text -= 1 if not node.get('is_multiline'): self.add_permalink_ref(node, _('Permalink to this definition')) self.body.append('\n') @@ -287,7 +283,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): if figure_id in self.builder.fignumbers.get(key, {}): self.body.append('') - prefix = self.builder.config.numfig_format.get(figtype) + prefix = self.config.numfig_format.get(figtype) if prefix is None: msg = __('numfig_format is not defined for %s') % figtype logger.warning(msg) @@ -305,9 +301,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): append_fignumber(figtype, node['ids'][0]) def add_permalink_ref(self, node: Element, title: str) -> None: - if node['ids'] and self.permalink_text and self.builder.add_permalinks: + if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks: format = '%s' - self.body.append(format % (node['ids'][0], title, self.permalink_text)) + self.body.append(format % (node['ids'][0], title, + self.config.html_permalinks_icon)) # overwritten def visit_bullet_list(self, node: Element) -> None: @@ -350,6 +347,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # there's a classifier. pass else: + if isinstance(node.parent.parent.parent, addnodes.glossary): + # add permalink if glossary terms + self.add_permalink_ref(node, _('Permalink to this term')) + self.body.append('') # overwritten @@ -362,8 +363,8 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): def depart_title(self, node: Element) -> None: close_tag = self.context[-1] - if (self.permalink_text and self.builder.add_permalinks and - node.parent.hasattr('ids') and node.parent['ids']): + if (self.config.html_permalinks and self.builder.add_permalinks and + node.parent.hasattr('ids') and node.parent['ids']): # add permalink anchor if close_tag.startswith('%s' % ( _('Permalink to this headline'), - self.permalink_text)) + self.config.html_permalinks_icon)) elif isinstance(node.parent, nodes.table): self.body.append('') self.add_permalink_ref(node.parent, _('Permalink to this table')) @@ -391,14 +392,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): linenos = node.get('linenos', False) highlight_args = node.get('highlight_args', {}) highlight_args['force'] = node.get('force', False) - if lang is self.builder.config.highlight_language: - # only pass highlighter options for original language - opts = self.builder.config.highlight_options - else: - opts = {} + opts = self.config.highlight_options.get(lang, {}) - if linenos and self.builder.config.html_codeblock_linenos_style: - linenos = self.builder.config.html_codeblock_linenos_style + if linenos and self.config.html_codeblock_linenos_style: + linenos = self.config.html_codeblock_linenos_style highlighted = self.highlighter.highlight_block( node.rawsource, lang, opts=opts, linenos=linenos, @@ -791,3 +788,9 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): def unknown_visit(self, node: Node) -> None: raise NotImplementedError('Unknown node: ' + node.__class__.__name__) + + @property + def permalink_text(self) -> str: + warnings.warn('HTMLTranslator.permalink_text is deprecated.', + RemovedInSphinx50Warning, stacklevel=2) + return self.config.html_permalinks_icon diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index b77202951..da0d39e24 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -7,7 +7,7 @@ Much of this code is adapted from Dave Kuhlman's "docpy" writer from his docutils sandbox. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,22 +15,19 @@ import re import warnings from collections import defaultdict from os import path -from typing import Any, Dict, Iterable, Iterator, List, Tuple, Set, Union -from typing import cast +from typing import Any, Dict, Iterable, Iterator, List, Set, Tuple, Union, cast from docutils import nodes, writers from docutils.nodes import Element, Node, Text -from sphinx import addnodes -from sphinx import highlighting -from sphinx.deprecation import ( - RemovedInSphinx40Warning, RemovedInSphinx50Warning, deprecated_alias -) +from sphinx import addnodes, highlighting +from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning, + deprecated_alias) from sphinx.domains import IndexEntry from sphinx.domains.std import StandardDomain from sphinx.errors import SphinxError -from sphinx.locale import admonitionlabels, _, __ -from sphinx.util import split_into, logging, texescape +from sphinx.locale import _, __, admonitionlabels +from sphinx.util import logging, split_into, texescape from sphinx.util.docutils import SphinxTranslator from sphinx.util.nodes import clean_astext, get_prev_node from sphinx.util.template import LaTeXRenderer @@ -526,7 +523,7 @@ class LaTeXTranslator(SphinxTranslator): ret = [] # latex_domain_indices can be False/True or a list of index names - indices_config = self.builder.config.latex_domain_indices + indices_config = self.config.latex_domain_indices if indices_config: for domain in self.builder.env.domains.values(): for indexcls in domain.indices: @@ -546,7 +543,7 @@ class LaTeXTranslator(SphinxTranslator): def render(self, template_name: str, variables: Dict) -> str: renderer = LaTeXRenderer(latex_engine=self.config.latex_engine) - for template_dir in self.builder.config.templates_path: + for template_dir in self.config.templates_path: template = path.join(self.builder.confdir, template_dir, template_name) if path.exists(template): @@ -818,7 +815,8 @@ class LaTeXTranslator(SphinxTranslator): pass def visit_seealso(self, node: Element) -> None: - self.body.append('\n\n\\sphinxstrong{%s:}\n\n' % admonitionlabels['seealso']) + self.body.append('\n\n\\sphinxstrong{%s:}\n\\nopagebreak\n\n' + % admonitionlabels['seealso']) def depart_seealso(self, node: Element) -> None: self.body.append("\n\n") @@ -965,7 +963,7 @@ class LaTeXTranslator(SphinxTranslator): cell = self.table.cell() context = '' if cell.width > 1: - if self.builder.config.latex_use_latex_multicolumn: + if self.config.latex_use_latex_multicolumn: if self.table.col == 0: self.body.append('\\multicolumn{%d}{|l|}{%%\n' % cell.width) else: @@ -1163,7 +1161,10 @@ class LaTeXTranslator(SphinxTranslator): # (first one is label node) pass else: - self.body.append('\n') + # the \sphinxAtStartPar is to allow hyphenation of first word of + # a paragraph in narrow contexts such as in a table cell + # added as two items (cf. line trimming in depart_entry()) + self.body.extend(['\n', '\\sphinxAtStartPar\n']) def depart_paragraph(self, node: Element) -> None: self.body.append('\n') @@ -1177,9 +1178,11 @@ class LaTeXTranslator(SphinxTranslator): self.body.append('\n\\end{center}') def visit_hlist(self, node: Element) -> None: - # for now, we don't support a more compact list format - # don't add individual itemize environments, but one for all columns self.compact_list += 1 + ncolumns = node['ncolumns'] + if self.compact_list > 1: + self.body.append('\\setlength{\\multicolsep}{0pt}\n') + self.body.append('\\begin{multicols}{' + ncolumns + '}\\raggedright\n') self.body.append('\\begin{itemize}\\setlength{\\itemsep}{0pt}' '\\setlength{\\parskip}{0pt}\n') if self.table: @@ -1187,12 +1190,17 @@ class LaTeXTranslator(SphinxTranslator): def depart_hlist(self, node: Element) -> None: self.compact_list -= 1 - self.body.append('\\end{itemize}\n') + self.body.append('\\end{itemize}\\raggedcolumns\\end{multicols}\n') def visit_hlistcol(self, node: Element) -> None: pass def depart_hlistcol(self, node: Element) -> None: + # \columnbreak would guarantee same columns as in html ouput. But + # some testing with long items showed that columns may be too uneven. + # And in case only of short items, the automatic column breaks should + # match the ones pre-computed by the hlist() directive. + # self.body.append('\\columnbreak\n') pass def latex_image_length(self, width_str: str, scale: int = 100) -> str: @@ -1207,7 +1215,6 @@ class LaTeXTranslator(SphinxTranslator): return isinstance(node.parent, nodes.TextElement) def visit_image(self, node: Element) -> None: - attrs = node.attributes pre = [] # type: List[str] # in reverse order post = [] # type: List[str] @@ -1217,27 +1224,27 @@ class LaTeXTranslator(SphinxTranslator): is_inline = self.is_inline(node.parent) else: is_inline = self.is_inline(node) - if 'width' in attrs: - if 'scale' in attrs: - w = self.latex_image_length(attrs['width'], attrs['scale']) + if 'width' in node: + if 'scale' in node: + w = self.latex_image_length(node['width'], node['scale']) else: - w = self.latex_image_length(attrs['width']) + w = self.latex_image_length(node['width']) if w: include_graphics_options.append('width=%s' % w) - if 'height' in attrs: - if 'scale' in attrs: - h = self.latex_image_length(attrs['height'], attrs['scale']) + if 'height' in node: + if 'scale' in node: + h = self.latex_image_length(node['height'], node['scale']) else: - h = self.latex_image_length(attrs['height']) + h = self.latex_image_length(node['height']) if h: include_graphics_options.append('height=%s' % h) - if 'scale' in attrs: + if 'scale' in node: if not include_graphics_options: # if no "width" nor "height", \sphinxincludegraphics will fit # to the available text width if oversized after rescaling. include_graphics_options.append('scale=%s' - % (float(attrs['scale']) / 100.0)) - if 'align' in attrs: + % (float(node['scale']) / 100.0)) + if 'align' in node: align_prepost = { # By default latex aligns the top of an image. (1, 'top'): ('', ''), @@ -1252,8 +1259,8 @@ class LaTeXTranslator(SphinxTranslator): (0, 'right'): ('{\\hspace*{\\fill}', '}'), } try: - pre.append(align_prepost[is_inline, attrs['align']][0]) - post.append(align_prepost[is_inline, attrs['align']][1]) + pre.append(align_prepost[is_inline, node['align']][0]) + post.append(align_prepost[is_inline, node['align']][1]) except KeyError: pass if self.in_parsed_literal: @@ -1545,7 +1552,7 @@ class LaTeXTranslator(SphinxTranslator): id = self.curfilestack[-1] + ':' + uri[1:] self.body.append(self.hyperlink(id)) self.body.append(r'\emph{') - if self.builder.config.latex_show_pagerefs and not \ + if self.config.latex_show_pagerefs and not \ self.in_production_list: self.context.append('}}} (%s)' % self.hyperpageref(id)) else: @@ -1569,8 +1576,7 @@ class LaTeXTranslator(SphinxTranslator): self.body.append(r'\sphinxtermref{') else: self.body.append(r'\sphinxcrossref{') - if self.builder.config.latex_show_pagerefs and not \ - self.in_production_list: + if self.config.latex_show_pagerefs and not self.in_production_list: self.context.append('}}} (%s)' % self.hyperpageref(id)) else: self.context.append('}}}') @@ -1754,11 +1760,7 @@ class LaTeXTranslator(SphinxTranslator): linenos = node.get('linenos', False) highlight_args = node.get('highlight_args', {}) highlight_args['force'] = node.get('force', False) - if lang is self.builder.config.highlight_language: - # only pass highlighter options for original language - opts = self.builder.config.highlight_options - else: - opts = {} + opts = self.config.highlight_options.get(lang, {}) hlcode = self.highlighter.highlight_block( node.rawsource, lang, opts=opts, linenos=linenos, @@ -2020,12 +2022,12 @@ class LaTeXTranslator(SphinxTranslator): else: from sphinx.util.math import wrap_displaymath self.body.append(wrap_displaymath(node.astext(), label, - self.builder.config.math_number_all)) + self.config.math_number_all)) raise nodes.SkipNode def visit_math_reference(self, node: Element) -> None: label = "equation:%s:%s" % (node['docname'], node['target']) - eqref_format = self.builder.config.math_eqref_format + eqref_format = self.config.math_eqref_format if eqref_format: try: ref = r'\ref{%s}' % label @@ -2090,7 +2092,7 @@ class LaTeXTranslator(SphinxTranslator): warnings.warn('generate_numfig_format() is deprecated.', RemovedInSphinx40Warning, stacklevel=2) ret = [] # type: List[str] - figure = self.builder.config.numfig_format['figure'].split('%s', 1) + figure = self.config.numfig_format['figure'].split('%s', 1) if len(figure) == 1: ret.append('\\def\\fnum@figure{%s}\n' % self.escape(figure[0]).strip()) else: @@ -2101,7 +2103,7 @@ class LaTeXTranslator(SphinxTranslator): self.escape(figure[1])) ret.append('\\makeatother\n') - table = self.builder.config.numfig_format['table'].split('%s', 1) + table = self.config.numfig_format['table'].split('%s', 1) if len(table) == 1: ret.append('\\def\\fnum@table{%s}\n' % self.escape(table[0]).strip()) else: @@ -2112,7 +2114,7 @@ class LaTeXTranslator(SphinxTranslator): self.escape(table[1])) ret.append('\\makeatother\n') - codeblock = self.builder.config.numfig_format['code-block'].split('%s', 1) + codeblock = self.config.numfig_format['code-block'].split('%s', 1) if len(codeblock) == 1: pass # FIXME else: @@ -2128,7 +2130,6 @@ class LaTeXTranslator(SphinxTranslator): from sphinx.builders.latex import constants # NOQA from sphinx.builders.latex.util import ExtBabel # NOQA - deprecated_alias('sphinx.writers.latex', { 'ADDITIONAL_SETTINGS': constants.ADDITIONAL_SETTINGS, @@ -2161,4 +2162,6 @@ deprecated_alias('sphinx.writers.latex', # FIXME: Workaround to avoid circular import # refs: https://github.com/sphinx-doc/sphinx/issues/5433 -from sphinx.builders.latex.nodes import HYPERLINK_SUPPORT_NODES, captioned_literal_block, footnotetext # NOQA +from sphinx.builders.latex.nodes import ( # NOQA isort:skip + HYPERLINK_SUPPORT_NODES, captioned_literal_block, footnotetext, +) diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index 7da2f4e8f..9ef429ba3 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -4,31 +4,27 @@ Manual page writer, extended for Sphinx custom nodes. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import warnings -from typing import Any, Dict, Iterable -from typing import cast +from typing import Any, Dict, Iterable, cast from docutils import nodes from docutils.nodes import Element, Node, TextElement -from docutils.writers.manpage import ( - Writer, - Translator as BaseTranslator -) +from docutils.writers.manpage import Translator as BaseTranslator +from docutils.writers.manpage import Writer from sphinx import addnodes from sphinx.builders import Builder from sphinx.deprecation import RemovedInSphinx40Warning -from sphinx.locale import admonitionlabels, _ +from sphinx.locale import _, admonitionlabels from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator from sphinx.util.i18n import format_date from sphinx.util.nodes import NodeMatcher - logger = logging.getLogger(__name__) @@ -300,8 +296,7 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator): if uri.startswith('mailto:') or uri.startswith('http:') or \ uri.startswith('https:') or uri.startswith('ftp:'): # if configured, put the URL after the link - if self.builder.config.man_show_urls and \ - node.astext() != uri: + if self.config.man_show_urls and node.astext() != uri: if uri.startswith('mailto:'): uri = uri[7:] self.body.extend([ diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 5ad2831dd..6518d10da 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -4,7 +4,7 @@ Custom docutils writer for Texinfo. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,18 +12,18 @@ import re import textwrap import warnings from os import path -from typing import Any, Dict, Iterable, Iterator, List, Optional, Pattern, Set, Tuple, Union -from typing import cast +from typing import (Any, Dict, Iterable, Iterator, List, Optional, Pattern, Set, Tuple, Union, + cast) from docutils import nodes, writers from docutils.nodes import Element, Node, Text -from sphinx import addnodes, __display_version__ +from sphinx import __display_version__, addnodes from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.domains import IndexEntry from sphinx.domains.index import IndexDomain from sphinx.errors import ExtensionError -from sphinx.locale import admonitionlabels, _, __ +from sphinx.locale import _, __, admonitionlabels from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator from sphinx.util.i18n import format_date @@ -233,12 +233,12 @@ class TexinfoTranslator(SphinxTranslator): 'author': self.settings.author, # if empty, use basename of input file 'filename': self.settings.texinfo_filename, - 'release': self.escape(self.builder.config.release), - 'project': self.escape(self.builder.config.project), - 'copyright': self.escape(self.builder.config.copyright), - 'date': self.escape(self.builder.config.today or - format_date(self.builder.config.today_fmt or _('%b %d, %Y'), - language=self.builder.config.language)) + 'release': self.escape(self.config.release), + 'project': self.escape(self.config.project), + 'copyright': self.escape(self.config.copyright), + 'date': self.escape(self.config.today or + format_date(self.config.today_fmt or _('%b %d, %Y'), + language=self.config.language)) }) # title title = self.settings.title # type: str @@ -369,7 +369,7 @@ class TexinfoTranslator(SphinxTranslator): """Return an escaped string suitable for use as an argument to a Texinfo command.""" s = self.escape(s) - # commas are the argument delimeters + # commas are the argument delimiters s = s.replace(',', '@comma{}') # normalize white space s = ' '.join(s.split()).strip() @@ -434,7 +434,7 @@ class TexinfoTranslator(SphinxTranslator): self.add_menu_entries(entries) if (node_name != 'Top' or not self.node_menus[entries[0]] or - self.builder.config.texinfo_no_detailmenu): + self.config.texinfo_no_detailmenu): self.body.append('\n@end menu\n') return @@ -484,7 +484,7 @@ class TexinfoTranslator(SphinxTranslator): ret.append('@end menu\n') return ''.join(ret) - indices_config = self.builder.config.texinfo_domain_indices + indices_config = self.config.texinfo_domain_indices if indices_config: for domain in self.builder.env.domains.values(): for indexcls in domain.indices: @@ -739,7 +739,7 @@ class TexinfoTranslator(SphinxTranslator): else: uri = self.escape_arg(uri) name = self.escape_arg(name) - show_urls = self.builder.config.texinfo_show_urls + show_urls = self.config.texinfo_show_urls if self.in_footnote: show_urls = 'inline' if not name or uri == name: @@ -1206,11 +1206,10 @@ class TexinfoTranslator(SphinxTranslator): # ignore remote images return name, ext = path.splitext(uri) - attrs = node.attributes # width and height ignored in non-tex output - width = self.tex_image_length(attrs.get('width', '')) - height = self.tex_image_length(attrs.get('height', '')) - alt = self.escape_arg(attrs.get('alt', '')) + width = self.tex_image_length(node.get('width', '')) + height = self.tex_image_length(node.get('height', '')) + alt = self.escape_arg(node.get('alt', '')) filename = "%s-figures/%s" % (self.elements['filename'][:-5], name) # type: ignore self.body.append('\n@image{%s,%s,%s,%s,%s}\n' % (filename, width, height, alt, ext[1:])) @@ -1242,6 +1241,15 @@ class TexinfoTranslator(SphinxTranslator): def depart_legend(self, node: Element) -> None: pass + def visit_substitution_reference(self, node: Element) -> None: + pass + + def depart_substitution_reference(self, node: Element) -> None: + pass + + def visit_substitution_definition(self, node: Element) -> None: + raise nodes.SkipNode + def visit_system_message(self, node: Element) -> None: self.body.append('\n@verbatim\n' '\n' @@ -1386,9 +1394,8 @@ class TexinfoTranslator(SphinxTranslator): # use the full name of the objtype for the category try: domain = self.builder.env.get_domain(node.parent['domain']) - primary = self.builder.config.primary_domain name = domain.get_type_name(domain.object_types[objtype], - primary == domain.name) + self.config.primary_domain == domain.name) except (KeyError, ExtensionError): name = objtype # by convention, the deffn category should be capitalized like a title diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 796362260..c0ebe32a2 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -4,23 +4,22 @@ Custom docutils writer for plain text. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import math import os import re import textwrap -from itertools import groupby, chain -from typing import Any, Dict, Generator, List, Iterable, Optional, Set, Tuple, Union -from typing import cast +from itertools import chain, groupby +from typing import Any, Dict, Generator, Iterable, List, Optional, Set, Tuple, Union, cast from docutils import nodes, writers from docutils.nodes import Element, Node, Text from docutils.utils import column_width from sphinx import addnodes -from sphinx.locale import admonitionlabels, _ +from sphinx.locale import _, admonitionlabels from sphinx.util.docutils import SphinxTranslator if False: @@ -1013,6 +1012,9 @@ class TextTranslator(SphinxTranslator): def visit_toctree(self, node: Element) -> None: raise nodes.SkipNode + def visit_substitution_definition(self, node: Element) -> None: + raise nodes.SkipNode + def visit_pending_xref(self, node: Element) -> None: pass diff --git a/sphinx/writers/xml.py b/sphinx/writers/xml.py index d007898ba..19fa3c1ef 100644 --- a/sphinx/writers/xml.py +++ b/sphinx/writers/xml.py @@ -4,7 +4,7 @@ Docutils-native XML and pseudo-XML writers. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/certs/cert.pem b/tests/certs/cert.pem new file mode 100644 index 000000000..6f8c35c6b --- /dev/null +++ b/tests/certs/cert.pem @@ -0,0 +1,50 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9fzHGBPNaZNcN +nL/1nvO2xJR/E64vFua3QfPQQ5HpigjrK/HRUlRGztRKJ+CEjCXNYNfQ4dUcV45o +k5uPH3U1CkAw2d/We+kZnAHkNuw4mRC0ohdzpUByyDOA5WtUWPn9SwhXCVz6fM7e +I52auvzpUE6soVDM3nucnqZDJ3Ua9KgB02FrqX13S76Uq+uf8Q2hpTruO/nBzB4p +6xFwJJ1taXEEWi8swg6HO8/+0x0AeripV6JieNUptEFuV9kLvRz9qGg0CO2f7AdI +jNeFDGrgO7qJ+VxXV9Gnbi6ph4vsUwtJZB3phRGGomdgiRd6PSma81nvTe1z69x/ +g+8P091pAgMBAAECggEAIrTABfd0JpMffAPAeJjjJA8+70NIfKFiIiA3Kmalu7Mn +TQMgZ+j/PHS3FtnU2hHc/o+FF2G1KVqz311heUYWrl8xQIE26M6K88DJ6+VPQFJw +Z9TkHK8gbaVTIYFjNfCR4J00atRxLgNb0/2L6QHkPksSDbYB2XPKCfZYlyYL4aKq +dePghFu9ePXhUXooPCqke+kP0b8OmHzPlmJpxbeb8ujiox2+4wYjN8lWPz8xHv8i +IM7V5hAbPIaQfu/joKrRKk+Kk8UqGurkKQ75KLLL+1oaJO/GLTQ4bk5tpRgfWPda +aEBzSPrnqame2CKUWtBughuRWSxdTIMvdXIC/ym1gQKBgQDx6Nyio/L6I5CdlXwC +HAzBCy1mnj70Kj97tQc+A/0z8dD7fCSE/oo8IiEKixcjnaSxHk8VjINF/w17n63W +8neE7pVsuDwxfhiQ9ZRI1WpV0LsFEoTrEWG7Ax8UzbHXCQbNJ9SI0HJRo9UN0f/Z +t+ZT+HNUzdcpCwTvdRVDisbXcQKBgQDIiMz58GFEwdGPXJKEhSyQ3kSQBjeqo0Vl +wMDuDvFEckHl/p1RnDo0lzaq6FivOX84ymvGNdQW14TnQp3A/mkQ5o6k/e1pfAA6 +X0Y6tBH/QppVo5sFvOufyn02k48k5pFAjLHH9L9i0dyWqq4V6PgA2uk4qilFxEg/ +CJEVfq4ZeQKBgQCZPHKWq9f8T48J42kcRPxnRFdMC63BKQnxqOifhhNcVi+VPjw7 +6qlSEiRv80+DBhcPAy4BbnKxYjD+QFX0NL80+5S3u7SVfVS+bnGx+U5UcdYmDmcY +KHiJ6B5GJU4j8tnWFwbwa2ofAPKywHWbSnyicF1OON20aACGVtpTYJM4YQKBgBW4 +09NDGZY0FHoeAfT+4/vxR6X+NmtyciL6hSuETNgoNEEwmmPrs1ZdBtvufSTF6qUB +MDlxPT8YK1pNmf78z+63ur3ej6f8eZ3ZEidruANZeJRMO4+cjj1p1rRhuYC6xQMj ++mH5ff27U9SyOlc/PBYDoH212PCouVaym9yjM0KpAoGBALr583slY55ESOthLrfX +1ecoET5xxRm431XbZMnxu0uUvHWNfqoojtmD7laclb9HwkpShPB6PT1egBIvDWWM +bVUuXzJ8gP0tIG3dHgiiUlld3ahOiaMYSU77uLFBRWv5sQqfewLuFvlzHn/2ZSt7 +TcipT4f67b18W8iuLJELEs57 +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDuTCCAqGgAwIBAgIUUNvkPwe0W8C2I0+KnLpMaQ+S+vowDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCRlIxETAPBgNVBAgMCEJyZXRhZ25lMQ8wDQYDVQQHDAZS +ZW5uZXMxGjAYBgNVBAoMEVNwaGlueCB0ZXN0IHN1aXRlMRIwEAYDVQQDDAlsb2Nh +bGhvc3QwHhcNMjAxMTE1MTcyNDExWhcNMzAxMTEzMTcyNDExWjBhMQswCQYDVQQG +EwJGUjERMA8GA1UECAwIQnJldGFnbmUxDzANBgNVBAcMBlJlbm5lczEaMBgGA1UE +CgwRU3BoaW54IHRlc3Qgc3VpdGUxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL1/McYE81pk1w2cv/We87bElH8Tri8W +5rdB89BDkemKCOsr8dFSVEbO1Eon4ISMJc1g19Dh1RxXjmiTm48fdTUKQDDZ39Z7 +6RmcAeQ27DiZELSiF3OlQHLIM4Dla1RY+f1LCFcJXPp8zt4jnZq6/OlQTqyhUMze +e5yepkMndRr0qAHTYWupfXdLvpSr65/xDaGlOu47+cHMHinrEXAknW1pcQRaLyzC +Doc7z/7THQB6uKlXomJ41Sm0QW5X2Qu9HP2oaDQI7Z/sB0iM14UMauA7uon5XFdX +0aduLqmHi+xTC0lkHemFEYaiZ2CJF3o9KZrzWe9N7XPr3H+D7w/T3WkCAwEAAaNp +MGcwHQYDVR0OBBYEFN1iHZj88N6eI2FlRzza52xzOU5EMB8GA1UdIwQYMBaAFN1i +HZj88N6eI2FlRzza52xzOU5EMA8GA1UdEwEB/wQFMAMBAf8wFAYDVR0RBA0wC4IJ +bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQBVUZm1iw7N7uZu/SF3hailxS+1 +3KChItWu3ZOIjlmDIkaJ9kWqP2ficUg3tBUx6/UOjHQAwRC4rj87BoSV2mEy+0OX +fyy+ER/BeHYly5v+hpjVojVKeqysk5CKttZM+cOibT2SzLLYf0InNqZRQRJco+nL +QNR0hVo/Lz6Mf1gF2ywf9bXSF3+XECU4K6sVm4QpFbJNm+fHqJBuh1LXHRrcTAsP +LM6PBnd3P5QTcr/G0s/tYMPmero9YHZUO8FMvMVoI2K8k6/duG/EbBaNzriRI1OM +PpZGCWxbJfyApnzc5lGAG4zJnV/wpOyNhKJuW9N1fr2oEwPpJlS3VzrgeKcY +-----END CERTIFICATE----- diff --git a/tests/conftest.py b/tests/conftest.py index 6a08dba76..5580f672b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ pytest config for sphinx/tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,8 +13,8 @@ import docutils import pytest import sphinx -from sphinx.testing.path import path from sphinx.testing import comparer +from sphinx.testing.path import path pytest_plugins = 'sphinx.testing.fixtures' diff --git a/tests/ext_napoleon_pep526_data_google.py b/tests/ext_napoleon_pep526_data_google.py new file mode 100644 index 000000000..95e84f470 --- /dev/null +++ b/tests/ext_napoleon_pep526_data_google.py @@ -0,0 +1,18 @@ +""" +Test module for napoleon PEP 526 compatiblity with google style +""" + +module_level_var: int = 99 +"""This is an example module level variable""" + + +class PEP526GoogleClass: + """Sample class with PEP 526 annotations and google docstring + + Attributes: + attr1: Attr1 description. + attr2: Attr2 description. + """ + + attr1: int + attr2: str diff --git a/tests/ext_napoleon_pep526_data_numpy.py b/tests/ext_napoleon_pep526_data_numpy.py new file mode 100644 index 000000000..d13ba31fb --- /dev/null +++ b/tests/ext_napoleon_pep526_data_numpy.py @@ -0,0 +1,22 @@ +""" +Test module for napoleon PEP 526 compatiblity with numpy style +""" + +module_level_var: int = 99 +"""This is an example module level variable""" + + +class PEP526NumpyClass: + """ + Sample class with PEP 526 annotations and numpy docstring + + Attributes + ---------- + attr1: + Attr1 description + + attr2: + Attr2 description + """ + attr1: int + attr2: str diff --git a/tests/js/doctools.js b/tests/js/doctools.js index 54246f635..a28f3ceb3 100644 --- a/tests/js/doctools.js +++ b/tests/js/doctools.js @@ -12,6 +12,10 @@ describe('jQuery extensions', function() { expect(jQuery.urldecode(test_encoded_string)).toEqual(test_decoded_string); }); + it('+ should result in " "', function() { + expect(jQuery.urldecode('+')).toEqual(' '); + }); + }); describe('getQueryParameters', function() { diff --git a/tests/js/searchtools.js b/tests/js/searchtools.js new file mode 100644 index 000000000..007eeb7db --- /dev/null +++ b/tests/js/searchtools.js @@ -0,0 +1,32 @@ +describe('Basic html theme search', function() { + + describe('terms search', function() { + + it('should find "C++" when in index', function() { + index = { + docnames:["index"], + filenames:["index.rst"], + terms:{'c++':0}, + titles:["<no title>"], + titleterms:{} + } + Search.setIndex(index); + searchterms = ['c++']; + excluded = []; + terms = index.terms; + titleterms = index.titleterms; + + hits = [[ + "index", + "<no title>", + "", + null, + 2, + "index.rst" + ]]; + expect(Search.performTermsSearch(searchterms, excluded, terms, titleterms)).toEqual(hits); + }); + + }); + +}); diff --git a/tests/roots/test-api-set-translator/conf.py b/tests/roots/test-api-set-translator/conf.py index 15f45a679..671f3905a 100644 --- a/tests/roots/test-api-set-translator/conf.py +++ b/tests/roots/test-api-set-translator/conf.py @@ -11,7 +11,6 @@ from sphinx.writers.manpage import ManualPageTranslator from sphinx.writers.texinfo import TexinfoTranslator from sphinx.writers.text import TextTranslator - project = 'test' diff --git a/tests/roots/test-apidoc-toc/mypackage/main.py b/tests/roots/test-apidoc-toc/mypackage/main.py index 813db805e..c43573a38 100755 --- a/tests/roots/test-apidoc-toc/mypackage/main.py +++ b/tests/roots/test-apidoc-toc/mypackage/main.py @@ -5,7 +5,6 @@ import os import mod_resource import mod_something - if __name__ == "__main__": print("Hello, world! -> something returns: {}".format(mod_something.something())) diff --git a/tests/roots/test-changes/conf.py b/tests/roots/test-changes/conf.py index 9d6d39631..ec67b9c59 100644 --- a/tests/roots/test-changes/conf.py +++ b/tests/roots/test-changes/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- project = 'Sphinx ChangesBuilder tests' -copyright = '2007-2020 by the Sphinx team, see AUTHORS' +copyright = '2007-2021 by the Sphinx team, see AUTHORS' version = '0.6' release = '0.6alpha1' diff --git a/tests/roots/test-domain-c-intersphinx/conf.py b/tests/roots/test-domain-c-intersphinx/conf.py new file mode 100644 index 000000000..c176af775 --- /dev/null +++ b/tests/roots/test-domain-c-intersphinx/conf.py @@ -0,0 +1,4 @@ +exclude_patterns = ['_build'] +extensions = [ + 'sphinx.ext.intersphinx', +] diff --git a/tests/roots/test-domain-c-intersphinx/index.rst b/tests/roots/test-domain-c-intersphinx/index.rst new file mode 100644 index 000000000..5d6d3e098 --- /dev/null +++ b/tests/roots/test-domain-c-intersphinx/index.rst @@ -0,0 +1,62 @@ +.. c:member:: void __member = _member + + - :any:`_member` + - :c:member:`_member` + - :c:var:`_member` + - :c:data:`_member` + +.. c:member:: void __var = _var + + - :any:`_var` + - :c:member:`_var` + - :c:var:`_var` + - :c:data:`_var` + +.. c:member:: void __function = _function + + - :any:`_function` + - :c:func:`_function` + - :c:type:`_function` + +.. c:member:: void __macro = _macro + + - :any:`_macro` + - :c:macro:`_macro` + +.. c:type:: _struct __struct + struct _struct __structTagged + + - :any:`_struct` + - :c:struct:`_struct` + - :c:type:`_struct` + +.. c:type:: _union __union + union _union __unionTagged + + - :any:`_union` + - :c:union:`_union` + - :c:type:`_union` + +.. c:type:: _enum __enum + enum _enum __enumTagged + + - :any:`_enum` + - :c:enum:`_enum` + - :c:type:`_enum` + +.. c:member:: void __enumerator = _enumerator + + - :any:`_enumerator` + - :c:enumerator:`_enumerator` + +.. c:type:: _type __type + + - :any:`_type` + - :c:type:`_type` + +.. c:member:: void __functionParam = _functionParam.param + + - :any:`_functionParam.param` + - :c:member:`_functionParam.param` + - :c:var:`_functionParam.param` + - :c:data:`_functionParam.param` diff --git a/tests/roots/test-domain-c/function_param_target.rst b/tests/roots/test-domain-c/function_param_target.rst new file mode 100644 index 000000000..05de01445 --- /dev/null +++ b/tests/roots/test-domain-c/function_param_target.rst @@ -0,0 +1,5 @@ +.. c:function:: void f(int i) + + - :c:var:`i` + +- :c:var:`f.i` diff --git a/tests/roots/test-domain-c/ns_lookup.rst b/tests/roots/test-domain-c/ns_lookup.rst new file mode 100644 index 000000000..87f9d68e7 --- /dev/null +++ b/tests/roots/test-domain-c/ns_lookup.rst @@ -0,0 +1,13 @@ +.. c:namespace:: ns_lookup + +.. c:var:: int i + +.. c:function:: void f(int j) + + - :c:var:`i` + - :c:var:`j` + - :c:expr:`i` + - :c:expr:`j` + +- :c:var:`i` +- :c:expr:`i` diff --git a/tests/roots/test-domain-cpp-intersphinx/conf.py b/tests/roots/test-domain-cpp-intersphinx/conf.py new file mode 100644 index 000000000..c176af775 --- /dev/null +++ b/tests/roots/test-domain-cpp-intersphinx/conf.py @@ -0,0 +1,4 @@ +exclude_patterns = ['_build'] +extensions = [ + 'sphinx.ext.intersphinx', +] diff --git a/tests/roots/test-domain-cpp-intersphinx/index.rst b/tests/roots/test-domain-cpp-intersphinx/index.rst new file mode 100644 index 000000000..9ed9493ed --- /dev/null +++ b/tests/roots/test-domain-cpp-intersphinx/index.rst @@ -0,0 +1,112 @@ +.. cpp:type:: _class __class + + - :any:`_class` + - :cpp:any:`_class` + - :cpp:class:`_class` + - :cpp:struct:`_class` + - :cpp:type:`_class` + +.. cpp:type:: _struct __struct + + - :any:`_struct` + - :cpp:any:`_struct` + - :cpp:class:`_struct` + - :cpp:struct:`_struct` + - :cpp:type:`_struct` + +.. cpp:type:: _union __union + + - :any:`_union` + - :cpp:any:`_union` + - :cpp:union:`_union` + - :cpp:type:`_union` + +.. cpp:member:: void __function = _function + + - :any:`_function` + - :cpp:any:`_function` + - :cpp:func:`_function` + - :cpp:type:`_function` + +.. cpp:member:: void __member = _member + + - :any:`_member` + - :cpp:any:`_member` + - :cpp:member:`_member` + - :cpp:var:`_member` + +.. cpp:member:: void __var = _var + + - :any:`_var` + - :cpp:any:`_var` + - :cpp:member:`_var` + - :cpp:var:`_var` + +.. cpp:type:: _type __type + + - :any:`_type` + - :cpp:any:`_type` + - :cpp:type:`_type` + +.. cpp:function:: template<_concept T> void __concept() + + - :any:`_concept` + - :cpp:any:`_concept` + - :cpp:concept:`_concept` + +.. cpp:type:: _enum __enum + + - :any:`_enum` + - :cpp:any:`_enum` + - :cpp:enum:`_enum` + - :cpp:type:`_enum` + +.. cpp:type:: _enumStruct __enumStruct + + - :any:`_enumStruct` + - :cpp:any:`_enumStruct` + - :cpp:enum:`_enumStruct` + - :cpp:type:`_enumStruct` + +.. cpp:type:: _enumClass __enumClass + + - :any:`_enumClass` + - :cpp:any:`_enumClass` + - :cpp:enum:`_enumClass` + - :cpp:type:`_enumClass` + +.. cpp:member:: void __enumerator = _enumerator + + - :any:`_enumerator` + - :cpp:any:`_enumerator` + - :cpp:enumerator:`_enumerator` + +.. cpp:member:: void __scopedEnumerator = _enumStruct::_scopedEnumerator + + - :any:`_enumStruct::_scopedEnumerator` + - :cpp:any:`_enumStruct::_scopedEnumerator` + - :cpp:enumerator:`_enumStruct::_scopedEnumerator` + +.. cpp:member:: void __enumerator2 = _enum::_enumerator + + - :any:`_enum::_enumerator` + - :cpp:any:`_enum::_enumerator` + - :cpp:enumerator:`_enum::_enumerator` + +.. cpp:member:: void __functionParam = _functionParam::param + + - :any:`_functionParam::param` + - :cpp:any:`_functionParam::param` + - :cpp:member:`_functionParam::param` + - :cpp:var:`_functionParam::param` + +.. cpp:type:: _templateParam::TParam __templateParam + + - :any:`_templateParam::TParam` + - :cpp:any:`_templateParam::TParam` + - :cpp:type:`_templateParam::TParam` + - :cpp:member:`_templateParam::TParam` + - :cpp:var:`_templateParam::TParam` + - :cpp:class:`_templateParam::TParam` + - :cpp:struct:`_templateParam::TParam` + - :cpp:union:`_templateParam::TParam` diff --git a/tests/roots/test-domain-cpp/roles-targets-ok.rst b/tests/roots/test-domain-cpp/roles-targets-ok.rst index e70b9259f..783f7b985 100644 --- a/tests/roots/test-domain-cpp/roles-targets-ok.rst +++ b/tests/roots/test-domain-cpp/roles-targets-ok.rst @@ -123,37 +123,37 @@ :class:`TParamType` :struct:`TParamType` :union:`TParamType` - :func:`TParamType` + function :member:`TParamType` :var:`TParamType` :type:`TParamType` - :concept:`TParamType` - :enum:`TParamType` - :enumerator:`TParamType` + concept + enum + enumerator :cpp:any:`TParamVar` :class:`TParamVar` :struct:`TParamVar` :union:`TParamVar` - :func:`TParamVar` + function :member:`TParamVar` :var:`TParamVar` :type:`TParamVar` - :concept:`TParamVar` - :enum:`TParamVar` - :enumerator:`TParamVar` + concept + enum + enumerator :cpp:any:`TParamTemplate` :class:`TParamTemplate` :struct:`TParamTemplate` :union:`TParamTemplate` - :func:`TParamTemplate` + function :member:`TParamTemplate` :var:`TParamTemplate` :type:`TParamTemplate` - :concept:`TParamTemplate` - :enum:`TParamTemplate` - :enumerator:`TParamTemplate` + concept + enum + enumerator .. function:: void FunctionParams(int FunctionParam) diff --git a/tests/roots/test-domain-cpp/roles-targets-warn.rst b/tests/roots/test-domain-cpp/roles-targets-warn.rst index decebe170..57083ff15 100644 --- a/tests/roots/test-domain-cpp/roles-targets-warn.rst +++ b/tests/roots/test-domain-cpp/roles-targets-warn.rst @@ -114,35 +114,35 @@ class struct union - func + :func:`TParamType` member var type - concept - enum - enumerator + :concept:`TParamType` + :enum:`TParamType` + :enumerator:`TParamType` class struct union - func + :func:`TParamVar` member var type - concept - enum - enumerator + :concept:`TParamVar` + :enum:`TParamVar` + :enumerator:`TParamVar` class struct union - func + :func:`TParamTemplate` member var type - concept - enum - enumerator + :concept:`TParamTemplate` + :enum:`TParamTemplate` + :enumerator:`TParamTemplate` .. function:: void FunctionParams(int FunctionParam) diff --git a/tests/roots/test-domain-py-xref-warning/conf.py b/tests/roots/test-domain-py-xref-warning/conf.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/roots/test-domain-py-xref-warning/index.rst b/tests/roots/test-domain-py-xref-warning/index.rst new file mode 100644 index 000000000..6f2cab795 --- /dev/null +++ b/tests/roots/test-domain-py-xref-warning/index.rst @@ -0,0 +1,7 @@ +test-domain-py-xref-warning +=========================== + +.. _existing-label: + +:ref:`no-label` +:ref:`existing-label` diff --git a/tests/roots/test-domain-py/abbr.rst b/tests/roots/test-domain-py/abbr.rst new file mode 100644 index 000000000..67f11578b --- /dev/null +++ b/tests/roots/test-domain-py/abbr.rst @@ -0,0 +1,10 @@ +abbrev +====== + +.. currentmodule:: module_a.submodule + +* normal: :py:meth:`module_a.submodule.ModTopLevel.mod_child_1` +* relative: :py:meth:`.ModTopLevel.mod_child_1` +* short name: :py:meth:`~module_a.submodule.ModTopLevel.mod_child_1` +* relative + short name: :py:meth:`~.ModTopLevel.mod_child_1` +* short name + relative: :py:meth:`~.ModTopLevel.mod_child_1` diff --git a/tests/roots/test-ext-autodoc/autodoc_dummy_module.py b/tests/roots/test-ext-autodoc/autodoc_dummy_module.py index 5cc427ea4..1e9ed19dc 100644 --- a/tests/roots/test-ext-autodoc/autodoc_dummy_module.py +++ b/tests/roots/test-ext-autodoc/autodoc_dummy_module.py @@ -1,4 +1,4 @@ -from dummy import * # NOQA +from dummy import * # NOQA def test(): diff --git a/tests/roots/test-ext-autodoc/target/__init__.py b/tests/roots/test-ext-autodoc/target/__init__.py index b6684ee85..e49f9a141 100644 --- a/tests/roots/test-ext-autodoc/target/__init__.py +++ b/tests/roots/test-ext-autodoc/target/__init__.py @@ -3,7 +3,6 @@ from io import StringIO from sphinx.util import save_traceback # NOQA - __all__ = ['Class'] #: documentation for the integer diff --git a/tests/roots/test-ext-autodoc/target/annotations.py b/tests/roots/test-ext-autodoc/target/annotations.py index 667149b26..ef600e2af 100644 --- a/tests/roots/test-ext-autodoc/target/annotations.py +++ b/tests/roots/test-ext-autodoc/target/annotations.py @@ -1,9 +1,15 @@ from __future__ import annotations + from typing import overload - myint = int +#: docstring +variable: myint + +#: docstring +variable2 = None # type: myint + def sum(x: myint, y: myint) -> myint: """docstring""" @@ -23,3 +29,13 @@ def mult(x: float, y: float) -> float: def mult(x, y): """docstring""" return x, y + + +class Foo: + """docstring""" + + #: docstring + attr1: myint + + def __init__(self): + self.attr2: myint = None #: docstring diff --git a/tests/roots/test-ext-autodoc/target/classes.py b/tests/roots/test-ext-autodoc/target/classes.py index dc471a6f3..a3b4c6477 100644 --- a/tests/roots/test-ext-autodoc/target/classes.py +++ b/tests/roots/test-ext-autodoc/target/classes.py @@ -1,3 +1,7 @@ +from inspect import Parameter, Signature +from typing import List, Union + + class Foo: pass @@ -10,3 +14,19 @@ class Bar: class Baz: def __new__(cls, x, y): pass + + +class Qux: + __signature__ = Signature(parameters=[Parameter('foo', Parameter.POSITIONAL_OR_KEYWORD), + Parameter('bar', Parameter.POSITIONAL_OR_KEYWORD)]) + + def __init__(self, x, y): + pass + + +class Quux(List[Union[int, float]]): + """A subclass of List[Union[int, float]]""" + pass + + +Alias = Foo diff --git a/tests/roots/test-ext-autodoc/target/decorator.py b/tests/roots/test-ext-autodoc/target/decorator.py index 61398b324..faad3fff9 100644 --- a/tests/roots/test-ext-autodoc/target/decorator.py +++ b/tests/roots/test-ext-autodoc/target/decorator.py @@ -29,3 +29,25 @@ class Bar: @deco1 def meth(self, name=None, age=None): pass + + +class Baz: + @deco1 + def __init__(self, name=None, age=None): + pass + + +class Qux: + @deco1 + def __new__(self, name=None, age=None): + pass + + +class _Metaclass(type): + @deco1 + def __call__(self, name=None, age=None): + pass + + +class Quux(metaclass=_Metaclass): + pass diff --git a/tests/roots/test-ext-autodoc/target/empty_all.py b/tests/roots/test-ext-autodoc/target/empty_all.py new file mode 100644 index 000000000..c094cff70 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/empty_all.py @@ -0,0 +1,16 @@ +""" +docsting of empty_all module. +""" +__all__ = [] + + +def foo(): + """docstring""" + + +def bar(): + """docstring""" + + +def baz(): + """docstring""" diff --git a/tests/roots/test-ext-autodoc/target/generic_class.py b/tests/roots/test-ext-autodoc/target/generic_class.py index cf4c5ed37..e6ff38abc 100644 --- a/tests/roots/test-ext-autodoc/target/generic_class.py +++ b/tests/roots/test-ext-autodoc/target/generic_class.py @@ -1,5 +1,4 @@ -from typing import TypeVar, Generic - +from typing import Generic, TypeVar T = TypeVar('T') diff --git a/tests/roots/test-ext-autodoc/target/genericalias.py b/tests/roots/test-ext-autodoc/target/genericalias.py index 78b68cd63..9909efca1 100644 --- a/tests/roots/test-ext-autodoc/target/genericalias.py +++ b/tests/roots/test-ext-autodoc/target/genericalias.py @@ -1,6 +1,11 @@ -from typing import List, Callable +from typing import Callable, List #: A list of int T = List[int] C = Callable[[int], None] # a generic alias not having a doccomment + + +class Class: + #: A list of int + T = List[int] diff --git a/tests/roots/test-ext-autodoc/target/hide_value.py b/tests/roots/test-ext-autodoc/target/hide_value.py new file mode 100644 index 000000000..1d53aabe9 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/hide_value.py @@ -0,0 +1,19 @@ +#: docstring +#: +#: :meta hide-value: +SENTINEL1 = object() + +#: :meta hide-value: +SENTINEL2 = object() + + +class Foo: + """docstring""" + + #: docstring + #: + #: :meta hide-value: + SENTINEL1 = object() + + #: :meta hide-value: + SENTINEL2 = object() diff --git a/tests/roots/test-ext-autodoc/target/instance_variable.py b/tests/roots/test-ext-autodoc/target/instance_variable.py new file mode 100644 index 000000000..ae86d1edb --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/instance_variable.py @@ -0,0 +1,10 @@ +class Foo: + def __init__(self): + self.attr1 = None #: docstring foo + self.attr2 = None #: docstring foo + + +class Bar(Foo): + def __init__(self): + self.attr2 = None #: docstring bar + self.attr3 = None #: docstring bar diff --git a/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py b/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py index 7ad521fc2..0a6f49653 100644 --- a/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py +++ b/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py @@ -1,5 +1,6 @@ from .foo import bar + class foo: """docstring of target.name_conflict::foo.""" pass diff --git a/tests/roots/test-ext-autodoc/target/need_mocks.py b/tests/roots/test-ext-autodoc/target/need_mocks.py index b8a661581..a29954184 100644 --- a/tests/roots/test-ext-autodoc/target/need_mocks.py +++ b/tests/roots/test-ext-autodoc/target/need_mocks.py @@ -1,15 +1,15 @@ -import missing_module # NOQA -import missing_package1.missing_module1 # NOQA -from missing_module import missing_name # NOQA -from missing_package2 import missing_module2 # NOQA -from missing_package3.missing_module3 import missing_name # NOQA +import missing_module # NOQA +import missing_package1.missing_module1 # NOQA +from missing_module import missing_name # NOQA +from missing_package2 import missing_module2 # NOQA +from missing_package3.missing_module3 import missing_name # NOQA -import sphinx.missing_module4 # NOQA -from sphinx.missing_module4 import missing_name2 # NOQA +import sphinx.missing_module4 # NOQA +from sphinx.missing_module4 import missing_name2 # NOQA -@missing_name +@missing_name(int) def decoratedFunction(): """decoratedFunction docstring""" return None @@ -28,4 +28,9 @@ class TestAutodoc(object): return None +class Inherited(missing_module.Class): + """docstring""" + pass + + sphinx.missing_module4.missing_function(len(missing_name2)) diff --git a/tests/roots/test-ext-autodoc/target/overload.py b/tests/roots/test-ext-autodoc/target/overload.py index cc4e509f2..1b395ee5b 100644 --- a/tests/roots/test-ext-autodoc/target/overload.py +++ b/tests/roots/test-ext-autodoc/target/overload.py @@ -2,21 +2,21 @@ from typing import Any, overload @overload -def sum(x: int, y: int) -> int: +def sum(x: int, y: int = 0) -> int: ... @overload -def sum(x: "float", y: "float") -> "float": +def sum(x: "float", y: "float" = 0.0) -> "float": ... @overload -def sum(x: str, y: str) -> str: +def sum(x: str, y: str = ...) -> str: ... -def sum(x, y): +def sum(x, y=None): """docstring""" return x + y @@ -25,18 +25,18 @@ class Math: """docstring""" @overload - def sum(self, x: int, y: int) -> int: + def sum(self, x: int, y: int = 0) -> int: ... @overload - def sum(self, x: "float", y: "float") -> "float": + def sum(self, x: "float", y: "float" = 0.0) -> "float": ... @overload - def sum(self, x: str, y: str) -> str: + def sum(self, x: str, y: str = ...) -> str: ... - def sum(self, x, y): + def sum(self, x, y=None): """docstring""" return x + y diff --git a/tests/roots/test-ext-autodoc/target/private.py b/tests/roots/test-ext-autodoc/target/private.py index a39ce085e..02d174863 100644 --- a/tests/roots/test-ext-autodoc/target/private.py +++ b/tests/roots/test-ext-autodoc/target/private.py @@ -9,3 +9,7 @@ def _public_function(name): :meta public: """ + + +PRIVATE_CONSTANT = None #: :meta private: +_PUBLIC_CONSTANT = None #: :meta public: diff --git a/tests/roots/test-ext-autodoc/target/singledispatch.py b/tests/roots/test-ext-autodoc/target/singledispatch.py index 33dcae43a..3fa81dcae 100644 --- a/tests/roots/test-ext-autodoc/target/singledispatch.py +++ b/tests/roots/test-ext-autodoc/target/singledispatch.py @@ -1,5 +1,5 @@ -from functools import singledispatch import inspect +from functools import singledispatch def assign_signature(func): diff --git a/tests/roots/test-ext-autodoc/target/slots.py b/tests/roots/test-ext-autodoc/target/slots.py index 44e750320..32822fd38 100644 --- a/tests/roots/test-ext-autodoc/target/slots.py +++ b/tests/roots/test-ext-autodoc/target/slots.py @@ -1,11 +1,21 @@ class Foo: + """docstring""" + __slots__ = ['attr'] class Bar: + """docstring""" + __slots__ = {'attr1': 'docstring of attr1', 'attr2': 'docstring of attr2', 'attr3': None} def __init__(self): self.attr2 = None #: docstring of instance attr2 + + +class Baz: + """docstring""" + + __slots__ = 'attr' diff --git a/tests/roots/test-ext-autodoc/target/typed_vars.py b/tests/roots/test-ext-autodoc/target/typed_vars.py index ba9657f18..f909b80f1 100644 --- a/tests/roots/test-ext-autodoc/target/typed_vars.py +++ b/tests/roots/test-ext-autodoc/target/typed_vars.py @@ -29,3 +29,6 @@ class Class: class Derived(Class): attr7: int + + +Alias = Derived diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py index 1a70eca67..2c9039650 100644 --- a/tests/roots/test-ext-autodoc/target/typehints.py +++ b/tests/roots/test-ext-autodoc/target/typehints.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union +from typing import Any, Tuple, Union def incr(a: int, b: int = 1) -> int: @@ -11,7 +11,7 @@ def decr(a, b = 1): class Math: - def __init__(self, s: str, o: object = None) -> None: + def __init__(self, s: str, o: Any = None) -> None: pass def incr(self, a: int, b: int = 1) -> int: diff --git a/tests/roots/test-ext-autodoc/target/typevar.py b/tests/roots/test-ext-autodoc/target/typevar.py index 9c6b0eab0..864fea20c 100644 --- a/tests/roots/test-ext-autodoc/target/typevar.py +++ b/tests/roots/test-ext-autodoc/target/typevar.py @@ -1,4 +1,4 @@ -from typing import TypeVar +from typing import NewType, TypeVar #: T1 T1 = TypeVar("T1") @@ -13,3 +13,14 @@ T4 = TypeVar("T4", covariant=True) #: T5 T5 = TypeVar("T5", contravariant=True) + +#: T6 +T6 = NewType("T6", int) + + +class Class: + #: T1 + T1 = TypeVar("T1") + + #: T6 + T6 = NewType("T6", int) diff --git a/tests/roots/test-ext-autosummary-imported_members/conf.py b/tests/roots/test-ext-autosummary-imported_members/conf.py index 4cfff02dc..77af668ab 100644 --- a/tests/roots/test-ext-autosummary-imported_members/conf.py +++ b/tests/roots/test-ext-autosummary-imported_members/conf.py @@ -1,5 +1,6 @@ import os import sys + sys.path.insert(0, os.path.abspath('.')) extensions = ['sphinx.ext.autosummary'] diff --git a/tests/roots/test-ext-autosummary-mock_imports/conf.py b/tests/roots/test-ext-autosummary-mock_imports/conf.py index 1097e3c04..121f81459 100644 --- a/tests/roots/test-ext-autosummary-mock_imports/conf.py +++ b/tests/roots/test-ext-autosummary-mock_imports/conf.py @@ -1,5 +1,6 @@ import os import sys + sys.path.insert(0, os.path.abspath('.')) extensions = ['sphinx.ext.autosummary'] diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py index 77eee8b5c..ca3475459 100644 --- a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py @@ -1,7 +1,6 @@ from os import path # NOQA from typing import Union - #: module variable CONSTANT1 = None CONSTANT2 = None diff --git a/tests/roots/test-ext-doctest-skipif/conf.py b/tests/roots/test-ext-doctest-skipif/conf.py index dfbc06f05..c863dbc01 100644 --- a/tests/roots/test-ext-doctest-skipif/conf.py +++ b/tests/roots/test-ext-doctest-skipif/conf.py @@ -6,7 +6,7 @@ source_suffix = '.txt' exclude_patterns = ['_build'] doctest_global_setup = ''' -from test_ext_doctest import record +from tests.test_ext_doctest import record record('doctest_global_setup', 'body', True) ''' diff --git a/tests/roots/test-ext-doctest/doctest.txt b/tests/roots/test-ext-doctest/doctest.txt index e45bc2721..04780cfbb 100644 --- a/tests/roots/test-ext-doctest/doctest.txt +++ b/tests/roots/test-ext-doctest/doctest.txt @@ -139,7 +139,7 @@ Special directives .. testcleanup:: * - import test_ext_doctest + from tests import test_ext_doctest test_ext_doctest.cleanup_call() non-ASCII result diff --git a/tests/roots/test-ext-math/index.rst b/tests/roots/test-ext-math/index.rst index 4237b73ff..221284aeb 100644 --- a/tests/roots/test-ext-math/index.rst +++ b/tests/roots/test-ext-math/index.rst @@ -6,6 +6,7 @@ Test Math math page + nomath .. math:: a^2+b^2=c^2 diff --git a/tests/roots/test-ext-math/nomath.rst b/tests/roots/test-ext-math/nomath.rst new file mode 100644 index 000000000..e69de29bb diff --git a/tests/roots/test-ext-viewcode-find/not_a_package/__init__.py b/tests/roots/test-ext-viewcode-find/not_a_package/__init__.py index 8efc933b8..f79ec81f5 100644 --- a/tests/roots/test-ext-viewcode-find/not_a_package/__init__.py +++ b/tests/roots/test-ext-viewcode-find/not_a_package/__init__.py @@ -1 +1 @@ -from .submodule import func1, Class1 # NOQA +from .submodule import Class1, func1 # NOQA diff --git a/tests/roots/test-ext-viewcode/spam/__init__.py b/tests/roots/test-ext-viewcode/spam/__init__.py index 2d5ca8239..c438f6050 100644 --- a/tests/roots/test-ext-viewcode/spam/__init__.py +++ b/tests/roots/test-ext-viewcode/spam/__init__.py @@ -1,2 +1,2 @@ -from .mod1 import func1, Class1 # NOQA -from .mod2 import func2, Class2 # NOQA +from .mod1 import Class1, func1 # NOQA +from .mod2 import Class2, func2 # NOQA diff --git a/tests/roots/test-ext-viewcode/spam/mod3.py b/tests/roots/test-ext-viewcode/spam/mod3.py index f7b6afbe0..812c9b586 100644 --- a/tests/roots/test-ext-viewcode/spam/mod3.py +++ b/tests/roots/test-ext-viewcode/spam/mod3.py @@ -1,2 +1,3 @@ from spam.mod1 import Class3 + __all__ = ('Class3',) diff --git a/tests/roots/test-highlight_options/conf.py b/tests/roots/test-highlight_options/conf.py new file mode 100644 index 000000000..90997d444 --- /dev/null +++ b/tests/roots/test-highlight_options/conf.py @@ -0,0 +1,4 @@ +highlight_options = { + 'default': {'default_option': True}, + 'python': {'python_option': True} +} diff --git a/tests/roots/test-highlight_options/index.rst b/tests/roots/test-highlight_options/index.rst new file mode 100644 index 000000000..389041ace --- /dev/null +++ b/tests/roots/test-highlight_options/index.rst @@ -0,0 +1,14 @@ +test-highlight_options +====================== + +.. code-block:: + + blah blah blah + +.. code-block:: python + + blah blah blah + +.. code-block:: java + + blah blah blah diff --git a/tests/roots/test-html_assets/conf.py b/tests/roots/test-html_assets/conf.py index ec53011c2..7f94bbbce 100644 --- a/tests/roots/test-html_assets/conf.py +++ b/tests/roots/test-html_assets/conf.py @@ -4,7 +4,9 @@ version = '1.4.4' html_static_path = ['static', 'subdir'] html_extra_path = ['extra', 'subdir'] html_css_files = ['css/style.css', - ('https://example.com/custom.css', {'title': 'title', 'media': 'print'})] + ('https://example.com/custom.css', + {'title': 'title', 'media': 'print', 'priority': 400})] html_js_files = ['js/custom.js', - ('https://example.com/script.js', {'async': 'async'})] + ('https://example.com/script.js', + {'async': 'async', 'priority': 400})] exclude_patterns = ['**/_build', '**/.htpasswd'] diff --git a/tests/roots/test-latex-equations/expects/latex-equations.tex b/tests/roots/test-latex-equations/expects/latex-equations.tex index ce07a0128..5374a6721 100644 --- a/tests/roots/test-latex-equations/expects/latex-equations.tex +++ b/tests/roots/test-latex-equations/expects/latex-equations.tex @@ -1,13 +1,18 @@ + +\sphinxAtStartPar Equation without a label. \begin{equation*} \begin{split}E = mc^2\end{split} \end{equation*} +\sphinxAtStartPar Equation with label. \begin{equation}\label{equation:equations:test} \begin{split}E = hv\end{split} \end{equation} +\sphinxAtStartPar Second equation without label. \begin{equation*} \begin{split}c^2 = a^2 + b^2\end{split} \end{equation*} +\sphinxAtStartPar Equation with label \eqref{equation:equations:test} is important. diff --git a/tests/roots/test-latex-table/complex.rst b/tests/roots/test-latex-table/complex.rst index f3f927a3e..fa84f8266 100644 --- a/tests/roots/test-latex-table/complex.rst +++ b/tests/roots/test-latex-table/complex.rst @@ -11,9 +11,9 @@ grid table +---------+ +---------+ | cell2-1 | | cell2-3 | + +---------+---------+ -| | cell3-2 | +| | cell3-2-par1 | +---------+ | -| cell4-1 | | +| cell4-1 | cell3-2-par2 | +---------+---------+---------+ | cell5-1 | +---------+---------+---------+ diff --git a/tests/roots/test-latex-table/expects/complex_spanning_cell.tex b/tests/roots/test-latex-table/expects/complex_spanning_cell.tex index 5d524c257..966f39a95 100644 --- a/tests/roots/test-latex-table/expects/complex_spanning_cell.tex +++ b/tests/roots/test-latex-table/expects/complex_spanning_cell.tex @@ -1,10 +1,13 @@ \label{\detokenize{complex:complex-spanning-cell}} +\sphinxAtStartPar table having … \begin{itemize} \item {} +\sphinxAtStartPar consecutive multirow at top of row (1\sphinxhyphen{}1 and 1\sphinxhyphen{}2) \item {} +\sphinxAtStartPar consecutive multirow at end of row (1\sphinxhyphen{}4 and 1\sphinxhyphen{}5) \end{itemize} @@ -16,26 +19,31 @@ consecutive multirow at end of row (1\sphinxhyphen{}4 and 1\sphinxhyphen{}5) \hline \sphinxmultirow{3}{1}{% \begin{varwidth}[t]{\sphinxcolwidth{1}{5}} +\sphinxAtStartPar cell1\sphinxhyphen{}1 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% }% &\sphinxmultirow{3}{2}{% \begin{varwidth}[t]{\sphinxcolwidth{1}{5}} +\sphinxAtStartPar cell1\sphinxhyphen{}2 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% }% & +\sphinxAtStartPar cell1\sphinxhyphen{}3 &\sphinxmultirow{3}{4}{% \begin{varwidth}[t]{\sphinxcolwidth{1}{5}} +\sphinxAtStartPar cell1\sphinxhyphen{}4 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% }% &\sphinxmultirow{2}{5}{% \begin{varwidth}[t]{\sphinxcolwidth{1}{5}} +\sphinxAtStartPar cell1\sphinxhyphen{}5 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% @@ -43,12 +51,14 @@ cell1\sphinxhyphen{}5 \\ \cline{3-3}\sphinxtablestrut{1}&\sphinxtablestrut{2}&\sphinxmultirow{2}{6}{% \begin{varwidth}[t]{\sphinxcolwidth{1}{5}} +\sphinxAtStartPar cell2\sphinxhyphen{}3 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% }% &\sphinxtablestrut{4}&\sphinxtablestrut{5}\\ \cline{5-5}\sphinxtablestrut{1}&\sphinxtablestrut{2}&\sphinxtablestrut{6}&\sphinxtablestrut{4}& +\sphinxAtStartPar cell3\sphinxhyphen{}5 \\ \hline diff --git a/tests/roots/test-latex-table/expects/gridtable.tex b/tests/roots/test-latex-table/expects/gridtable.tex index 28b0b086b..a71c9e202 100644 --- a/tests/roots/test-latex-table/expects/gridtable.tex +++ b/tests/roots/test-latex-table/expects/gridtable.tex @@ -5,46 +5,60 @@ \begin{tabulary}{\linewidth}[t]{|T|T|T|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 &\sphinxstyletheadfamily +\sphinxAtStartPar header3 \\ \hline +\sphinxAtStartPar cell1\sphinxhyphen{}1 &\sphinxmultirow{2}{5}{% \begin{varwidth}[t]{\sphinxcolwidth{1}{3}} +\sphinxAtStartPar cell1\sphinxhyphen{}2 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% }% & +\sphinxAtStartPar cell1\sphinxhyphen{}3 \\ \cline{1-1}\cline{3-3}\sphinxmultirow{2}{7}{% \begin{varwidth}[t]{\sphinxcolwidth{1}{3}} +\sphinxAtStartPar cell2\sphinxhyphen{}1 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% }% &\sphinxtablestrut{5}& +\sphinxAtStartPar cell2\sphinxhyphen{}3 \\ \cline{2-3}\sphinxtablestrut{7}&\sphinxstartmulticolumn{2}% \sphinxmultirow{2}{9}{% \begin{varwidth}[t]{\sphinxcolwidth{2}{3}} -cell3\sphinxhyphen{}2 +\sphinxAtStartPar +cell3\sphinxhyphen{}2\sphinxhyphen{}par1 + +\sphinxAtStartPar +cell3\sphinxhyphen{}2\sphinxhyphen{}par2 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% }% \sphinxstopmulticolumn \\ \cline{1-1} +\sphinxAtStartPar cell4\sphinxhyphen{}1 &\multicolumn{2}{l|}{\sphinxtablestrut{9}}\\ \hline\sphinxstartmulticolumn{3}% \begin{varwidth}[t]{\sphinxcolwidth{3}{3}} +\sphinxAtStartPar cell5\sphinxhyphen{}1 \par \vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}% diff --git a/tests/roots/test-latex-table/expects/longtable.tex b/tests/roots/test-latex-table/expects/longtable.tex index 9febfcef5..e2138ad58 100644 --- a/tests/roots/test-latex-table/expects/longtable.tex +++ b/tests/roots/test-latex-table/expects/longtable.tex @@ -3,8 +3,10 @@ \begin{savenotes}\sphinxatlongtablestart\begin{longtable}[c]{|l|l|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -14,8 +16,10 @@ header2 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -27,18 +31,24 @@ header2 \endlastfoot +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/longtable_having_align.tex b/tests/roots/test-latex-table/expects/longtable_having_align.tex index 1969e19d2..764ef55f3 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_align.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_align.tex @@ -3,8 +3,10 @@ \begin{savenotes}\sphinxatlongtablestart\begin{longtable}[r]{|l|l|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -14,8 +16,10 @@ header2 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -27,18 +31,24 @@ header2 \endlastfoot +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/longtable_having_caption.tex b/tests/roots/test-latex-table/expects/longtable_having_caption.tex index f0041e9ec..0ca5506be 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_caption.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_caption.tex @@ -5,8 +5,10 @@ \caption{caption for longtable\strut}\label{\detokenize{longtable:id1}}\\*[\sphinxlongtablecapskipadjust] \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -16,8 +18,10 @@ header2 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -29,18 +33,24 @@ header2 \endlastfoot +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/longtable_having_problematic_cell.tex b/tests/roots/test-latex-table/expects/longtable_having_problematic_cell.tex index 050527b69..9551a0a3b 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_problematic_cell.tex @@ -3,8 +3,10 @@ \begin{savenotes}\sphinxatlongtablestart\begin{longtable}[c]{|*{2}{\X{1}{2}|}} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -14,8 +16,10 @@ header2 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -28,23 +32,30 @@ header2 \endlastfoot \begin{itemize} \item {} +\sphinxAtStartPar item1 \item {} +\sphinxAtStartPar item2 \end{itemize} & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/longtable_having_stub_columns_and_problematic_cell.tex b/tests/roots/test-latex-table/expects/longtable_having_stub_columns_and_problematic_cell.tex index 68e74c5f4..e54f8acec 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_stub_columns_and_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_stub_columns_and_problematic_cell.tex @@ -3,10 +3,13 @@ \begin{savenotes}\sphinxatlongtablestart\begin{longtable}[c]{|*{3}{\X{1}{3}|}} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 &\sphinxstyletheadfamily +\sphinxAtStartPar header3 \\ \hline @@ -16,10 +19,13 @@ header3 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 &\sphinxstyletheadfamily +\sphinxAtStartPar header3 \\ \hline @@ -32,22 +38,29 @@ header3 \endlastfoot \sphinxstyletheadfamily \begin{itemize} \item {} +\sphinxAtStartPar instub1\sphinxhyphen{}1a \item {} +\sphinxAtStartPar instub1\sphinxhyphen{}1b \end{itemize} &\sphinxstyletheadfamily +\sphinxAtStartPar instub1\sphinxhyphen{}2 & +\sphinxAtStartPar notinstub1\sphinxhyphen{}3 \\ \hline\sphinxstyletheadfamily +\sphinxAtStartPar cell2\sphinxhyphen{}1 &\sphinxstyletheadfamily +\sphinxAtStartPar cell2\sphinxhyphen{}2 & +\sphinxAtStartPar cell2\sphinxhyphen{}3 \\ \hline diff --git a/tests/roots/test-latex-table/expects/longtable_having_verbatim.tex b/tests/roots/test-latex-table/expects/longtable_having_verbatim.tex index c7213b906..a0e7ecfcd 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_verbatim.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_verbatim.tex @@ -3,8 +3,10 @@ \begin{savenotes}\sphinxatlongtablestart\begin{longtable}[c]{|*{2}{\X{1}{2}|}} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -14,8 +16,10 @@ header2 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -31,16 +35,21 @@ header2 \PYG{n}{hello} \PYG{n}{world} \end{sphinxVerbatimintable} & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/longtable_having_widths.tex b/tests/roots/test-latex-table/expects/longtable_having_widths.tex index 884fd9f8a..cdd0e7a2b 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_widths.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_widths.tex @@ -3,8 +3,10 @@ \begin{savenotes}\sphinxatlongtablestart\begin{longtable}[c]{|\X{30}{100}|\X{70}{100}|} \hline\noalign{\phantomsection\label{\detokenize{longtable:namedlongtable}}\label{\detokenize{longtable:mylongtable}}}% \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -14,8 +16,10 @@ header2 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -27,21 +31,28 @@ header2 \endlastfoot +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline \end{longtable}\sphinxatlongtableend\end{savenotes} +\sphinxAtStartPar See {\hyperref[\detokenize{longtable:mylongtable}]{\sphinxcrossref{mylongtable}}}, same as {\hyperref[\detokenize{longtable:namedlongtable}]{\sphinxcrossref{\DUrole{std,std-ref}{this one}}}}. diff --git a/tests/roots/test-latex-table/expects/longtable_having_widths_and_problematic_cell.tex b/tests/roots/test-latex-table/expects/longtable_having_widths_and_problematic_cell.tex index 17c5ec4cc..ea868ffe4 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_widths_and_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_widths_and_problematic_cell.tex @@ -3,8 +3,10 @@ \begin{savenotes}\sphinxatlongtablestart\begin{longtable}[c]{|\X{30}{100}|\X{70}{100}|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -14,8 +16,10 @@ header2 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -28,23 +32,30 @@ header2 \endlastfoot \begin{itemize} \item {} +\sphinxAtStartPar item1 \item {} +\sphinxAtStartPar item2 \end{itemize} & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/longtable_with_tabularcolumn.tex b/tests/roots/test-latex-table/expects/longtable_with_tabularcolumn.tex index 2fbbbc4ef..426086de5 100644 --- a/tests/roots/test-latex-table/expects/longtable_with_tabularcolumn.tex +++ b/tests/roots/test-latex-table/expects/longtable_with_tabularcolumn.tex @@ -3,8 +3,10 @@ \begin{savenotes}\sphinxatlongtablestart\begin{longtable}[c]{|c|c|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -14,8 +16,10 @@ header2 {\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\ \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -27,18 +31,24 @@ header2 \endlastfoot +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/simple_table.tex b/tests/roots/test-latex-table/expects/simple_table.tex index 8044a6cc4..a06bfb1cf 100644 --- a/tests/roots/test-latex-table/expects/simple_table.tex +++ b/tests/roots/test-latex-table/expects/simple_table.tex @@ -5,23 +5,31 @@ \begin{tabulary}{\linewidth}[t]{|T|T|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/table_having_caption.tex b/tests/roots/test-latex-table/expects/table_having_caption.tex index d4423a05d..33a5f1d8f 100644 --- a/tests/roots/test-latex-table/expects/table_having_caption.tex +++ b/tests/roots/test-latex-table/expects/table_having_caption.tex @@ -9,23 +9,31 @@ \begin{tabulary}{\linewidth}[t]{|T|T|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/table_having_problematic_cell.tex b/tests/roots/test-latex-table/expects/table_having_problematic_cell.tex index 7a9b0f293..c5c57e2f7 100644 --- a/tests/roots/test-latex-table/expects/table_having_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/table_having_problematic_cell.tex @@ -5,29 +5,38 @@ \begin{tabular}[t]{|*{2}{\X{1}{2}|}} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline\begin{itemize} \item {} +\sphinxAtStartPar item1 \item {} +\sphinxAtStartPar item2 \end{itemize} & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/table_having_stub_columns_and_problematic_cell.tex b/tests/roots/test-latex-table/expects/table_having_stub_columns_and_problematic_cell.tex index 700fc4663..13c48a213 100644 --- a/tests/roots/test-latex-table/expects/table_having_stub_columns_and_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/table_having_stub_columns_and_problematic_cell.tex @@ -5,30 +5,40 @@ \begin{tabular}[t]{|*{3}{\X{1}{3}|}} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 &\sphinxstyletheadfamily +\sphinxAtStartPar header3 \\ \hline\sphinxstyletheadfamily \begin{itemize} \item {} +\sphinxAtStartPar instub1\sphinxhyphen{}1a \item {} +\sphinxAtStartPar instub1\sphinxhyphen{}1b \end{itemize} &\sphinxstyletheadfamily +\sphinxAtStartPar instub1\sphinxhyphen{}2 & +\sphinxAtStartPar notinstub1\sphinxhyphen{}3 \\ \hline\sphinxstyletheadfamily +\sphinxAtStartPar cell2\sphinxhyphen{}1 &\sphinxstyletheadfamily +\sphinxAtStartPar cell2\sphinxhyphen{}2 & +\sphinxAtStartPar cell2\sphinxhyphen{}3 \\ \hline diff --git a/tests/roots/test-latex-table/expects/table_having_threeparagraphs_cell_in_first_col.tex b/tests/roots/test-latex-table/expects/table_having_threeparagraphs_cell_in_first_col.tex index 6d3e81021..c1a440558 100644 --- a/tests/roots/test-latex-table/expects/table_having_threeparagraphs_cell_in_first_col.tex +++ b/tests/roots/test-latex-table/expects/table_having_threeparagraphs_cell_in_first_col.tex @@ -5,13 +5,17 @@ \begin{tabulary}{\linewidth}[t]{|T|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 \\ \hline +\sphinxAtStartPar cell1\sphinxhyphen{}1\sphinxhyphen{}par1 +\sphinxAtStartPar cell1\sphinxhyphen{}1\sphinxhyphen{}par2 +\sphinxAtStartPar cell1\sphinxhyphen{}1\sphinxhyphen{}par3 \\ \hline diff --git a/tests/roots/test-latex-table/expects/table_having_verbatim.tex b/tests/roots/test-latex-table/expects/table_having_verbatim.tex index f66bb8001..23faac55e 100644 --- a/tests/roots/test-latex-table/expects/table_having_verbatim.tex +++ b/tests/roots/test-latex-table/expects/table_having_verbatim.tex @@ -5,8 +5,10 @@ \begin{tabular}[t]{|*{2}{\X{1}{2}|}} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline @@ -14,16 +16,21 @@ header2 \PYG{n}{hello} \PYG{n}{world} \end{sphinxVerbatimintable} & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/table_having_widths.tex b/tests/roots/test-latex-table/expects/table_having_widths.tex index 094596bec..d01a40576 100644 --- a/tests/roots/test-latex-table/expects/table_having_widths.tex +++ b/tests/roots/test-latex-table/expects/table_having_widths.tex @@ -6,23 +6,31 @@ \begin{tabular}[t]{|\X{30}{100}|\X{70}{100}|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline @@ -30,4 +38,5 @@ cell3\sphinxhyphen{}2 \par \sphinxattableend\end{savenotes} +\sphinxAtStartPar See {\hyperref[\detokenize{tabular:mytabular}]{\sphinxcrossref{\DUrole{std,std-ref}{this}}}}, same as {\hyperref[\detokenize{tabular:namedtabular}]{\sphinxcrossref{namedtabular}}}. diff --git a/tests/roots/test-latex-table/expects/table_having_widths_and_problematic_cell.tex b/tests/roots/test-latex-table/expects/table_having_widths_and_problematic_cell.tex index a636b022e..ca6b697e5 100644 --- a/tests/roots/test-latex-table/expects/table_having_widths_and_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/table_having_widths_and_problematic_cell.tex @@ -5,29 +5,38 @@ \begin{tabular}[t]{|\X{30}{100}|\X{70}{100}|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline\begin{itemize} \item {} +\sphinxAtStartPar item1 \item {} +\sphinxAtStartPar item2 \end{itemize} & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/tabular_having_widths.tex b/tests/roots/test-latex-table/expects/tabular_having_widths.tex index 5ee1542d4..596ba4868 100644 --- a/tests/roots/test-latex-table/expects/tabular_having_widths.tex +++ b/tests/roots/test-latex-table/expects/tabular_having_widths.tex @@ -5,23 +5,31 @@ \begin{tabular}[t]{|\X{30}{100}|\X{70}{100}|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/tabularcolumn.tex b/tests/roots/test-latex-table/expects/tabularcolumn.tex index 02e9af440..c020e0cb4 100644 --- a/tests/roots/test-latex-table/expects/tabularcolumn.tex +++ b/tests/roots/test-latex-table/expects/tabularcolumn.tex @@ -5,23 +5,31 @@ \begin{tabulary}{\linewidth}[t]{|c|c|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-latex-table/expects/tabulary_having_widths.tex b/tests/roots/test-latex-table/expects/tabulary_having_widths.tex index 06d347fa3..0b42fb0cf 100644 --- a/tests/roots/test-latex-table/expects/tabulary_having_widths.tex +++ b/tests/roots/test-latex-table/expects/tabulary_having_widths.tex @@ -5,23 +5,31 @@ \begin{tabulary}{\linewidth}[t]{|T|T|} \hline \sphinxstyletheadfamily +\sphinxAtStartPar header1 &\sphinxstyletheadfamily +\sphinxAtStartPar header2 \\ \hline +\sphinxAtStartPar cell1\sphinxhyphen{}1 & +\sphinxAtStartPar cell1\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell2\sphinxhyphen{}1 & +\sphinxAtStartPar cell2\sphinxhyphen{}2 \\ \hline +\sphinxAtStartPar cell3\sphinxhyphen{}1 & +\sphinxAtStartPar cell3\sphinxhyphen{}2 \\ \hline diff --git a/tests/roots/test-linkcheck-localserver-anchor/conf.py b/tests/roots/test-linkcheck-localserver-anchor/conf.py new file mode 100644 index 000000000..2ba1f85e8 --- /dev/null +++ b/tests/roots/test-linkcheck-localserver-anchor/conf.py @@ -0,0 +1,2 @@ +exclude_patterns = ['_build'] +linkcheck_anchors = True diff --git a/tests/roots/test-linkcheck-localserver-anchor/index.rst b/tests/roots/test-linkcheck-localserver-anchor/index.rst new file mode 100644 index 000000000..807fe964b --- /dev/null +++ b/tests/roots/test-linkcheck-localserver-anchor/index.rst @@ -0,0 +1 @@ +`local server `_ diff --git a/tests/roots/test-linkcheck-localserver-https/conf.py b/tests/roots/test-linkcheck-localserver-https/conf.py new file mode 100644 index 000000000..a45d22e28 --- /dev/null +++ b/tests/roots/test-linkcheck-localserver-https/conf.py @@ -0,0 +1 @@ +exclude_patterns = ['_build'] diff --git a/tests/roots/test-linkcheck-localserver-https/index.rst b/tests/roots/test-linkcheck-localserver-https/index.rst new file mode 100644 index 000000000..fea598359 --- /dev/null +++ b/tests/roots/test-linkcheck-localserver-https/index.rst @@ -0,0 +1 @@ +`HTTPS server `_ diff --git a/tests/roots/test-linkcheck-localserver/conf.py b/tests/roots/test-linkcheck-localserver/conf.py index 2ba1f85e8..a45d22e28 100644 --- a/tests/roots/test-linkcheck-localserver/conf.py +++ b/tests/roots/test-linkcheck-localserver/conf.py @@ -1,2 +1 @@ exclude_patterns = ['_build'] -linkcheck_anchors = True diff --git a/tests/roots/test-linkcheck-localserver/index.rst b/tests/roots/test-linkcheck-localserver/index.rst index 807fe964b..c617e942f 100644 --- a/tests/roots/test-linkcheck-localserver/index.rst +++ b/tests/roots/test-linkcheck-localserver/index.rst @@ -1 +1 @@ -`local server `_ +`local server `_ diff --git a/tests/roots/test-prolog/conf.py b/tests/roots/test-prolog/conf.py index e5ce87151..f6be09c5e 100644 --- a/tests/roots/test-prolog/conf.py +++ b/tests/roots/test-prolog/conf.py @@ -1,5 +1,6 @@ import os import sys + sys.path.insert(0, os.path.abspath('.')) diff --git a/tests/roots/test-pycode-egg/src/setup.py b/tests/roots/test-pycode-egg/src/setup.py index 6ad4986e4..1dfb1de29 100644 --- a/tests/roots/test-pycode-egg/src/setup.py +++ b/tests/roots/test-pycode-egg/src/setup.py @@ -1,5 +1,4 @@ from setuptools import setup - setup(name='sample', py_modules=['sample']) diff --git a/tests/roots/test-root/autodoc_target.py b/tests/roots/test-root/autodoc_target.py index a1540d90a..a49ffc1ff 100644 --- a/tests/roots/test-root/autodoc_target.py +++ b/tests/roots/test-root/autodoc_target.py @@ -1,7 +1,6 @@ import enum from io import StringIO - __all__ = ['Class'] #: documentation for the integer diff --git a/tests/roots/test-root/conf.py b/tests/roots/test-root/conf.py index b3cc12ae0..34cafa767 100644 --- a/tests/roots/test-root/conf.py +++ b/tests/roots/test-root/conf.py @@ -6,7 +6,6 @@ from docutils.parsers.rst import Directive from sphinx import addnodes - sys.path.append(os.path.abspath('.')) extensions = ['sphinx.ext.autodoc', @@ -32,8 +31,6 @@ pygments_style = 'sphinx' show_authors = True numfig = True -rst_epilog = '.. |subst| replace:: global substitution' - html_sidebars = {'**': ['localtoc.html', 'relations.html', 'sourcelink.html', 'customsb.html', 'searchbox.html'], 'index': ['contentssb.html', 'localtoc.html', 'globaltoc.html']} diff --git a/tests/roots/test-root/markup.txt b/tests/roots/test-root/markup.txt index e6adef55e..7cd3e95ea 100644 --- a/tests/roots/test-root/markup.txt +++ b/tests/roots/test-root/markup.txt @@ -22,7 +22,9 @@ Meta markup Generic reST ------------ -A |subst| (the definition is in rst_epilog). +A |subst|! + +.. |subst| replace:: global substitution .. highlight:: none diff --git a/tests/roots/test-theming/setup.py b/tests/roots/test-theming/setup.py index 02ee259c0..7b504e807 100644 --- a/tests/roots/test-theming/setup.py +++ b/tests/roots/test-theming/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( name='test-theme', diff --git a/tests/roots/test-warnings/conf.py b/tests/roots/test-warnings/conf.py index 9d12e2d79..25b8aba07 100644 --- a/tests/roots/test-warnings/conf.py +++ b/tests/roots/test-warnings/conf.py @@ -1,5 +1,6 @@ import os import sys + sys.path.append(os.path.abspath('.')) extensions = ['sphinx.ext.autodoc'] diff --git a/tests/test_api_translator.py b/tests/test_api_translator.py index 47895a675..fddcd9d65 100644 --- a/tests/test_api_translator.py +++ b/tests/test_api_translator.py @@ -4,7 +4,7 @@ Test the Sphinx API for translator. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_application.py b/tests/test_application.py index a089a4bc0..94ddd04cf 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -4,7 +4,7 @@ Test the Sphinx class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_build.py b/tests/test_build.py index 9dcf78165..62de3ea5f 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -4,7 +4,7 @@ Test all builders. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -36,7 +36,11 @@ def nonascii_srcdir(request, rootdir, sphinx_test_tempdir): if not srcdir.exists(): (rootdir / 'test-root').copytree(srcdir) except UnicodeEncodeError: + # Now Python 3.7+ follows PEP-540 and uses utf-8 encoding for filesystem by default. + # So this error handling will be no longer used (after dropping python 3.6 support). srcdir = basedir / 'all' + if not srcdir.exists(): + (rootdir / 'test-root').copytree(srcdir) else: # add a doc with a non-ASCII file name to the source dir (srcdir / (test_name + '.txt')).write_text(dedent(""" diff --git a/tests/test_build_changes.py b/tests/test_build_changes.py index 3aedd03f5..c6cbd497d 100644 --- a/tests/test_build_changes.py +++ b/tests/test_build_changes.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- """ test_build_changes ~~~~~~~~~~~~~~~~~~ Test the ChangesBuilder class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_build_dirhtml.py b/tests/test_build_dirhtml.py index e89e6888d..086b8426b 100644 --- a/tests/test_build_dirhtml.py +++ b/tests/test_build_dirhtml.py @@ -4,7 +4,7 @@ Test dirhtml builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py index c17954fd3..982f4c1b9 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -4,13 +4,13 @@ Test the HTML builder and check output against XPath. - :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import subprocess -from subprocess import CalledProcessError, PIPE +from subprocess import PIPE, CalledProcessError from xml.etree import ElementTree import pytest diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 074602ea9..92ec384b5 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -4,7 +4,7 @@ Test the build process with gettext builder with the test root. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,7 +12,7 @@ import gettext import os import re import subprocess -from subprocess import CalledProcessError, PIPE +from subprocess import PIPE, CalledProcessError import pytest diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 8cd541481..8d02f5bed 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -4,14 +4,15 @@ Test the HTML builder and check output against XPath. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import re from distutils.version import LooseVersion -from itertools import cycle, chain +from itertools import chain, cycle +from unittest.mock import ANY, call, patch import pygments import pytest @@ -23,7 +24,6 @@ from sphinx.testing.util import strip_escseq from sphinx.util import docutils, md5 from sphinx.util.inventory import InventoryFile - ENV_WARNINGS = """\ %(root)s/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \ WARNING: Explicit markup ends without a blank line; unexpected unindent. @@ -178,8 +178,8 @@ def test_html4_output(app, status, warning): ], 'autodoc.html': [ (".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''), - (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'\*\*'), - (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'kwds'), + (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'), + (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'), (".//dd/p", r'Return spam\.'), ], 'extapi.html': [ @@ -254,11 +254,12 @@ def test_html4_output(app, status, warning): (".//p[@class='centered']/strong", 'LICENSE'), # a glossary (".//dl/dt[@id='term-boson']", 'boson'), + (".//dl/dt[@id='term-boson']/a", '¶'), # a production list (".//pre/strong", 'try_stmt'), (".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'), # tests for ``only`` directive - (".//p", 'A global substitution.'), + (".//p", 'A global substitution!'), (".//p", 'In HTML.'), (".//p", 'In both.'), (".//p", 'Always present'), @@ -278,8 +279,10 @@ def test_html4_output(app, status, warning): 'objects.html': [ (".//dt[@id='mod.Cls.meth1']", ''), (".//dt[@id='errmod.Error']", ''), - (".//dt/code", r'long\(parameter,\s* list\)'), - (".//dt/code", 'another one'), + (".//dt/code/span", r'long\(parameter,'), + (".//dt/code/span", r'list\)'), + (".//dt/code/span", 'another'), + (".//dt/code/span", 'one'), (".//a[@href='#mod.Cls'][@class='reference internal']", ''), (".//dl[@class='std userdesc']", ''), (".//dt[@id='userdesc-myobj']", ''), @@ -661,7 +664,7 @@ def test_numfig_without_numbered_toctree_warn(app, warning): warnings = warning.getvalue() assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings - assert 'index.rst:55: WARNING: no number is assigned for section: index' in warnings + assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings @@ -769,7 +772,7 @@ def test_numfig_with_numbered_toctree_warn(app, warning): app.build() warnings = warning.getvalue() assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings - assert 'index.rst:55: WARNING: no number is assigned for section: index' in warnings + assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings @@ -874,7 +877,7 @@ def test_numfig_with_prefix_warn(app, warning): app.build() warnings = warning.getvalue() assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings - assert 'index.rst:55: WARNING: no number is assigned for section: index' in warnings + assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings @@ -980,7 +983,7 @@ def test_numfig_with_secnum_depth_warn(app, warning): app.build() warnings = warning.getvalue() assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings - assert 'index.rst:55: WARNING: no number is assigned for section: index' in warnings + assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings @@ -1229,6 +1232,35 @@ def test_html_assets(app): '' in content) +@pytest.mark.sphinx('html', testroot='html_assets') +def test_assets_order(app): + app.add_css_file('normal.css') + app.add_css_file('early.css', priority=100) + app.add_css_file('late.css', priority=750) + app.add_css_file('lazy.css', priority=900) + app.add_js_file('normal.js') + app.add_js_file('early.js', priority=100) + app.add_js_file('late.js', priority=750) + app.add_js_file('lazy.js', priority=900) + + app.builder.build_all() + content = (app.outdir / 'index.html').read_text() + + # css_files + expected = ['_static/pygments.css', '_static/alabaster.css', '_static/early.css', + 'https://example.com/custom.css', '_static/normal.css', '_static/late.css', + '_static/css/style.css', '_static/lazy.css'] + pattern = '.*'.join('href="%s"' % f for f in expected) + assert re.search(pattern, content, re.S) + + # js_files + expected = ['_static/early.js', '_static/jquery.js', '_static/underscore.js', + '_static/doctools.js', 'https://example.com/script.js', '_static/normal.js', + '_static/late.js', '_static/js/custom.js', '_static/lazy.js'] + pattern = '.*'.join('src="%s"' % f for f in expected) + assert re.search(pattern, content, re.S) + + @pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_copy_source': False}) def test_html_copy_source(app): app.builder.build_all() @@ -1603,3 +1635,56 @@ def test_html_codeblock_linenos_style_inline(app): assert '1' in content else: assert '1 ' in content + + +@pytest.mark.sphinx('html', testroot='highlight_options') +def test_highlight_options(app): + subject = app.builder.highlighter + with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight: + app.build() + + call_args = highlight.call_args_list + assert len(call_args) == 3 + assert call_args[0] == call(ANY, 'default', force=False, linenos=False, + location=ANY, opts={'default_option': True}) + assert call_args[1] == call(ANY, 'python', force=False, linenos=False, + location=ANY, opts={'python_option': True}) + assert call_args[2] == call(ANY, 'java', force=False, linenos=False, + location=ANY, opts={}) + + +@pytest.mark.sphinx('html', testroot='highlight_options', + confoverrides={'highlight_options': {'default_option': True}}) +def test_highlight_options_old(app): + subject = app.builder.highlighter + with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight: + app.build() + + call_args = highlight.call_args_list + assert len(call_args) == 3 + assert call_args[0] == call(ANY, 'default', force=False, linenos=False, + location=ANY, opts={'default_option': True}) + assert call_args[1] == call(ANY, 'python', force=False, linenos=False, + location=ANY, opts={}) + assert call_args[2] == call(ANY, 'java', force=False, linenos=False, + location=ANY, opts={}) + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'html_permalinks': False}) +def test_html_permalink_disable(app): + app.build() + content = (app.outdir / 'index.html').read_text() + + assert '

    The basic Sphinx documentation for testing

    ' in content + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'html_permalinks_icon': '[PERMALINK]'}) +def test_html_permalink_icon(app): + app.build() + content = (app.outdir / 'index.html').read_text() + + assert ('

    The basic Sphinx documentation for testing[PERMALINK]

    ' in content) diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 8dbe2a48c..a67871f60 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -4,7 +4,7 @@ Test the build process with LaTeX builder with the test root. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,19 +13,19 @@ import re import subprocess from itertools import product from shutil import copyfile -from subprocess import CalledProcessError, PIPE +from subprocess import PIPE, CalledProcessError import pytest -from test_build_html import ENV_WARNINGS from sphinx.builders.latex import default_latex_documents from sphinx.config import Config -from sphinx.errors import SphinxError, ThemeError +from sphinx.errors import SphinxError from sphinx.testing.util import strip_escseq from sphinx.util import docutils from sphinx.util.osutil import cd, ensuredir from sphinx.writers.latex import LaTeXTranslator +from .test_build_html import ENV_WARNINGS LATEX_ENGINES = ['pdflatex', 'lualatex', 'xelatex'] DOCCLASSES = ['howto', 'manual'] @@ -730,13 +730,14 @@ def test_footnote(app, status, warning): '\\end{footnote}') in result assert '\\begin{footnote}[3]\\sphinxAtStartFootnote\nnamed\n%\n\\end{footnote}' in result assert '\\sphinxcite{footnote:bar}' in result - assert ('\\bibitem[bar]{footnote:bar}\ncite\n') in result + assert ('\\bibitem[bar]{footnote:bar}\n\\sphinxAtStartPar\ncite\n') in result assert '\\sphinxcaption{Table caption \\sphinxfootnotemark[4]' in result assert ('\\hline%\n\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' 'footnote in table caption\n%\n\\end{footnotetext}\\ignorespaces %\n' '\\begin{footnotetext}[5]\\sphinxAtStartFootnote\n' - 'footnote in table header\n%\n\\end{footnotetext}\\ignorespaces \n' - 'VIDIOC\\_CROPCAP\n&\n') in result + 'footnote in table header\n%\n\\end{footnotetext}\\ignorespaces ' + '\n\\sphinxAtStartPar\n' + 'VIDIOC\\_CROPCAP\n&\n\\sphinxAtStartPar\n') in result assert ('Information about VIDIOC\\_CROPCAP %\n' '\\begin{footnote}[6]\\sphinxAtStartFootnote\n' 'footnote in table not in header\n%\n\\end{footnote}\n\\\\\n\\hline\n' @@ -764,7 +765,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): assert ('\\caption{This is the figure caption with a footnote to ' '\\sphinxfootnotemark[7].}\\label{\\detokenize{index:id29}}\\end{figure}\n' '%\n\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' - 'Footnote in caption\n%\n\\end{footnotetext}')in result + 'Footnote in caption\n%\n\\end{footnotetext}') in result assert ('\\sphinxcaption{footnote \\sphinxfootnotemark[8] in ' 'caption of normal table}\\label{\\detokenize{index:id30}}') in result assert ('\\caption{footnote \\sphinxfootnotemark[9] ' @@ -776,7 +777,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): assert ('This is a reference to the code\\sphinxhyphen{}block in the footnote:\n' '{\\hyperref[\\detokenize{index:codeblockinfootnote}]' '{\\sphinxcrossref{\\DUrole{std,std-ref}{I am in a footnote}}}}') in result - assert ('&\nThis is one more footnote with some code in it %\n' + assert ('&\n\\sphinxAtStartPar\nThis is one more footnote with some code in it %\n' '\\begin{footnote}[11]\\sphinxAtStartFootnote\n' 'Third footnote in longtable\n') in result assert ('\\end{sphinxVerbatim}\n%\n\\end{footnote}.\n') in result @@ -816,13 +817,15 @@ def test_latex_show_urls_is_inline(app, status, warning): assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde} ' '(http://sphinx\\sphinxhyphen{}doc.org/\\textasciitilde{}test/)') in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{URL in term} ' - '(http://sphinx\\sphinxhyphen{}doc.org/)}] \\leavevmode\nDescription' in result) + '(http://sphinx\\sphinxhyphen{}doc.org/)}] ' + '\\leavevmode\n\\sphinxAtStartPar\nDescription' in result) assert ('\\item[{Footnote in term \\sphinxfootnotemark[6]}] ' '\\leavevmode%\n\\begin{footnotetext}[6]\\sphinxAtStartFootnote\n' - 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces \n' - 'Description') in result + 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' + '\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist} ' - '(http://sphinx\\sphinxhyphen{}doc.org/)}] \\leavevmode\nDescription') in result + '(http://sphinx\\sphinxhyphen{}doc.org/)}] ' + '\\leavevmode\n\\sphinxAtStartPar\nDescription') in result assert '\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' '{sphinx\\sphinxhyphen{}dev@googlegroups.com}') in result @@ -867,16 +870,16 @@ def test_latex_show_urls_is_footnote(app, status, warning): '{URL in term}\\sphinxfootnotemark[9]}] ' '\\leavevmode%\n\\begin{footnotetext}[9]\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' - '\\end{footnotetext}\\ignorespaces \nDescription') in result + '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{Footnote in term \\sphinxfootnotemark[11]}] ' '\\leavevmode%\n\\begin{footnotetext}[11]\\sphinxAtStartFootnote\n' - 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces \n' - 'Description') in result + 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' + '\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}' '\\sphinxfootnotemark[10]}] ' '\\leavevmode%\n\\begin{footnotetext}[10]\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' - '\\end{footnotetext}\\ignorespaces \nDescription') in result + '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result) assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' '{sphinx\\sphinxhyphen{}dev@googlegroups.com}\n') in result @@ -913,13 +916,13 @@ def test_latex_show_urls_is_no(app, status, warning): 'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result assert '\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}' in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{URL in term}}] ' - '\\leavevmode\nDescription') in result + '\\leavevmode\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{Footnote in term \\sphinxfootnotemark[6]}] ' '\\leavevmode%\n\\begin{footnotetext}[6]\\sphinxAtStartFootnote\n' - 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces \n' - 'Description') in result + 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' + '\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}}] ' - '\\leavevmode\nDescription') in result + '\\leavevmode\n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result) assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' '{sphinx\\sphinxhyphen{}dev@googlegroups.com}\n') in result @@ -1354,7 +1357,7 @@ def test_latex_index(app, status, warning): '\\index{equation@\\spxentry{equation}}equation:\n' in result) assert ('\n\\index{Einstein@\\spxentry{Einstein}}' '\\index{relativity@\\spxentry{relativity}}' - '\\ignorespaces \nand') in result + '\\ignorespaces \n\\sphinxAtStartPar\nand') in result assert ('\n\\index{main \\sphinxleftcurlybrace{}@\\spxentry{' 'main \\sphinxleftcurlybrace{}}}\\ignorespaces ' in result) @@ -1403,7 +1406,7 @@ def test_latex_thebibliography(app, status, warning): result = (app.outdir / 'python.tex').read_text() print(result) assert ('\\begin{sphinxthebibliography}{AuthorYe}\n' - '\\bibitem[AuthorYear]{index:authoryear}\n' + '\\bibitem[AuthorYear]{index:authoryear}\n\\sphinxAtStartPar\n' 'Author, Title, Year\n' '\\end{sphinxthebibliography}\n' in result) assert '\\sphinxcite{index:authoryear}' in result @@ -1447,7 +1450,8 @@ def test_latex_labels(app, status, warning): r'\end{figure}' in result) assert (r'\caption{labeled figure}' '\\label{\\detokenize{index:figure3}}\n' - '\\begin{sphinxlegend}\nwith a legend\n\\end{sphinxlegend}\n' + '\\begin{sphinxlegend}\n\\sphinxAtStartPar\n' + 'with a legend\n\\end{sphinxlegend}\n' r'\end{figure}' in result) # code-blocks diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index a78587668..60b62435c 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -4,21 +4,34 @@ Test the build process with manpage builder with the test root. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import http.server import json import re -import threading +import textwrap +import time +import wsgiref.handlers +from datetime import datetime +from typing import Dict from unittest import mock + import pytest +import requests + +from sphinx.builders.linkcheck import CheckExternalLinksBuilder, RateLimit +from sphinx.util.console import strip_colors + +from .utils import CERT_FILE, http_server, https_server + +ts_re = re.compile(r".*\[(?P.*)\].*") @pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) -def test_defaults(app, status, warning): - app.builder.build_all() +def test_defaults(app): + app.build() assert (app.outdir / 'output.txt').exists() content = (app.outdir / 'output.txt').read_text() @@ -38,8 +51,8 @@ def test_defaults(app, status, warning): @pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) -def test_defaults_json(app, status, warning): - app.builder.build_all() +def test_defaults_json(app): + app.build() assert (app.outdir / 'output.json').exists() content = (app.outdir / 'output.json').read_text() @@ -55,7 +68,7 @@ def test_defaults_json(app, status, warning): assert len(rows) == 10 # the output order of the rows is not stable # due to possible variance in network latency - rowsby = {row["uri"]:row for row in rows} + rowsby = {row["uri"]: row for row in rows} assert rowsby["https://www.google.com#!bar"] == { 'filename': 'links.txt', 'lineno': 10, @@ -99,8 +112,8 @@ def test_defaults_json(app, status, warning): 'https://www.google.com/image2.png', 'path/to/notfound'] }) -def test_anchors_ignored(app, status, warning): - app.builder.build_all() +def test_anchors_ignored(app): + app.build() assert (app.outdir / 'output.txt').exists() content = (app.outdir / 'output.txt').read_text() @@ -108,14 +121,15 @@ def test_anchors_ignored(app, status, warning): # expect all ok when excluding #top assert not content -@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) -def test_raises_for_invalid_status(app, status, warning): - server_thread = HttpServerThread(InternalServerErrorHandler, daemon=True) - server_thread.start() - try: - app.builder.build_all() - finally: - server_thread.terminate() + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True) +def test_raises_for_invalid_status(app): + class InternalServerErrorHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_error(500, "Internal Server Error") + + with http_server(InternalServerErrorHandler): + app.build() content = (app.outdir / 'output.txt').read_text() assert content == ( "index.rst:1: [broken] http://localhost:7777/#anchor: " @@ -124,75 +138,438 @@ def test_raises_for_invalid_status(app, status, warning): ) +class HeadersDumperHandler(http.server.BaseHTTPRequestHandler): + def do_HEAD(self): + self.do_GET() + + def do_GET(self): + self.send_response(200, "OK") + self.end_headers() + print(self.headers.as_string()) + + @pytest.mark.sphinx( - 'linkcheck', testroot='linkcheck', freshenv=True, + 'linkcheck', testroot='linkcheck-localserver', freshenv=True, confoverrides={'linkcheck_auth': [ - (r'.+google\.com/image.+', 'authinfo1'), - (r'.+google\.com.+', 'authinfo2'), - ] - }) -def test_auth(app, status, warning): - mock_req = mock.MagicMock() - mock_req.return_value = 'fake-response' - - with mock.patch.multiple('requests', get=mock_req, head=mock_req): - app.builder.build_all() - for c_args, c_kwargs in mock_req.call_args_list: - if 'google.com/image' in c_args[0]: - assert c_kwargs['auth'] == 'authinfo1' - elif 'google.com' in c_args[0]: - assert c_kwargs['auth'] == 'authinfo2' - else: - assert not c_kwargs['auth'] + (r'^$', ('no', 'match')), + (r'^http://localhost:7777/$', ('user1', 'password')), + (r'.*local.*', ('user2', 'hunter2')), + ]}) +def test_auth_header_uses_first_match(app, capsys): + with http_server(HeadersDumperHandler): + app.build() + stdout, stderr = capsys.readouterr() + auth = requests.auth._basic_auth_str('user1', 'password') + assert "Authorization: %s\n" % auth in stdout @pytest.mark.sphinx( - 'linkcheck', testroot='linkcheck', freshenv=True, + 'linkcheck', testroot='linkcheck-localserver', freshenv=True, + confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]}) +def test_auth_header_no_match(app, capsys): + with http_server(HeadersDumperHandler): + app.build() + stdout, stderr = capsys.readouterr() + assert "Authorization" not in stdout + + +@pytest.mark.sphinx( + 'linkcheck', testroot='linkcheck-localserver', freshenv=True, confoverrides={'linkcheck_request_headers': { - "https://localhost:7777/": { + "http://localhost:7777/": { "Accept": "text/html", }, - "http://www.sphinx-doc.org": { # no slash at the end - "Accept": "application/json", - }, "*": { "X-Secret": "open sesami", } }}) -def test_linkcheck_request_headers(app, status, warning): - mock_req = mock.MagicMock() - mock_req.return_value = 'fake-response' +def test_linkcheck_request_headers(app, capsys): + with http_server(HeadersDumperHandler): + app.build() - with mock.patch.multiple('requests', get=mock_req, head=mock_req): - app.builder.build_all() - for args, kwargs in mock_req.call_args_list: - url = args[0] - headers = kwargs.get('headers', {}) - if "https://localhost:7777" in url: - assert headers["Accept"] == "text/html" - elif 'http://www.sphinx-doc.org' in url: - assert headers["Accept"] == "application/json" - elif 'https://www.google.com' in url: - assert headers["Accept"] == "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8" - assert headers["X-Secret"] == "open sesami" + stdout, _stderr = capsys.readouterr() + assert "Accept: text/html\n" in stdout + assert "X-Secret" not in stdout + assert "sesami" not in stdout + + +@pytest.mark.sphinx( + 'linkcheck', testroot='linkcheck-localserver', freshenv=True, + confoverrides={'linkcheck_request_headers': { + "http://localhost:7777": {"Accept": "application/json"}, + "*": {"X-Secret": "open sesami"} + }}) +def test_linkcheck_request_headers_no_slash(app, capsys): + with http_server(HeadersDumperHandler): + app.build() + + stdout, _stderr = capsys.readouterr() + assert "Accept: application/json\n" in stdout + assert "X-Secret" not in stdout + assert "sesami" not in stdout + + +@pytest.mark.sphinx( + 'linkcheck', testroot='linkcheck-localserver', freshenv=True, + confoverrides={'linkcheck_request_headers': { + "http://do.not.match.org": {"Accept": "application/json"}, + "*": {"X-Secret": "open sesami"} + }}) +def test_linkcheck_request_headers_default(app, capsys): + with http_server(HeadersDumperHandler): + app.build() + + stdout, _stderr = capsys.readouterr() + assert "Accepts: application/json\n" not in stdout + assert "X-Secret: open sesami\n" in stdout + + +def make_redirect_handler(*, support_head): + class RedirectOnceHandler(http.server.BaseHTTPRequestHandler): + def do_HEAD(self): + if support_head: + self.do_GET() else: - assert headers["Accept"] == "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8" + self.send_response(405, "Method Not Allowed") + self.end_headers() + + def do_GET(self): + if self.path == "/?redirected=1": + self.send_response(204, "No content") + else: + self.send_response(302, "Found") + self.send_header("Location", "http://localhost:7777/?redirected=1") + self.end_headers() + + def log_date_time_string(self): + """Strip date and time from logged messages for assertions.""" + return "" + + return RedirectOnceHandler -class HttpServerThread(threading.Thread): - def __init__(self, handler, *args, **kwargs): - super().__init__(*args, **kwargs) - self.server = http.server.HTTPServer(("localhost", 7777), handler) - - def run(self): - self.server.serve_forever(poll_interval=0.01) - - def terminate(self): - self.server.shutdown() - self.server.server_close() - self.join() +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) +def test_follows_redirects_on_HEAD(app, capsys): + with http_server(make_redirect_handler(support_head=True)): + app.build() + stdout, stderr = capsys.readouterr() + content = (app.outdir / 'output.txt').read_text() + assert content == ( + "index.rst:1: [redirected with Found] " + "http://localhost:7777/ to http://localhost:7777/?redirected=1\n" + ) + assert stderr == textwrap.dedent( + """\ + 127.0.0.1 - - [] "HEAD / HTTP/1.1" 302 - + 127.0.0.1 - - [] "HEAD /?redirected=1 HTTP/1.1" 204 - + """ + ) -class InternalServerErrorHandler(http.server.BaseHTTPRequestHandler): +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) +def test_follows_redirects_on_GET(app, capsys): + with http_server(make_redirect_handler(support_head=False)): + app.build() + stdout, stderr = capsys.readouterr() + content = (app.outdir / 'output.txt').read_text() + assert content == ( + "index.rst:1: [redirected with Found] " + "http://localhost:7777/ to http://localhost:7777/?redirected=1\n" + ) + assert stderr == textwrap.dedent( + """\ + 127.0.0.1 - - [] "HEAD / HTTP/1.1" 405 - + 127.0.0.1 - - [] "GET / HTTP/1.1" 302 - + 127.0.0.1 - - [] "GET /?redirected=1 HTTP/1.1" 204 - + """ + ) + + +class OKHandler(http.server.BaseHTTPRequestHandler): + def do_HEAD(self): + self.send_response(200, "OK") + self.end_headers() + def do_GET(self): - self.send_error(500, "Internal Server Error") + self.do_HEAD() + self.wfile.write(b"ok\n") + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) +def test_invalid_ssl(app): + # Link indicates SSL should be used (https) but the server does not handle it. + with http_server(OKHandler): + app.build() + + with open(app.outdir / 'output.json') as fp: + content = json.load(fp) + assert content["status"] == "broken" + assert content["filename"] == "index.rst" + assert content["lineno"] == 1 + assert content["uri"] == "https://localhost:7777/" + assert "SSLError" in content["info"] + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) +def test_connect_to_selfsigned_fails(app): + with https_server(OKHandler): + app.build() + + with open(app.outdir / 'output.json') as fp: + content = json.load(fp) + assert content["status"] == "broken" + assert content["filename"] == "index.rst" + assert content["lineno"] == 1 + assert content["uri"] == "https://localhost:7777/" + assert "[SSL: CERTIFICATE_VERIFY_FAILED]" in content["info"] + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) +def test_connect_to_selfsigned_with_tls_verify_false(app): + app.config.tls_verify = False + with https_server(OKHandler): + app.build() + + with open(app.outdir / 'output.json') as fp: + content = json.load(fp) + assert content == { + "code": 0, + "status": "working", + "filename": "index.rst", + "lineno": 1, + "uri": "https://localhost:7777/", + "info": "", + } + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) +def test_connect_to_selfsigned_with_tls_cacerts(app): + app.config.tls_cacerts = CERT_FILE + with https_server(OKHandler): + app.build() + + with open(app.outdir / 'output.json') as fp: + content = json.load(fp) + assert content == { + "code": 0, + "status": "working", + "filename": "index.rst", + "lineno": 1, + "uri": "https://localhost:7777/", + "info": "", + } + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) +def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app): + monkeypatch.setenv("REQUESTS_CA_BUNDLE", CERT_FILE) + with https_server(OKHandler): + app.build() + + with open(app.outdir / 'output.json') as fp: + content = json.load(fp) + assert content == { + "code": 0, + "status": "working", + "filename": "index.rst", + "lineno": 1, + "uri": "https://localhost:7777/", + "info": "", + } + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) +def test_connect_to_selfsigned_nonexistent_cert_file(app): + app.config.tls_cacerts = "does/not/exist" + with https_server(OKHandler): + app.build() + + with open(app.outdir / 'output.json') as fp: + content = json.load(fp) + assert content == { + "code": 0, + "status": "broken", + "filename": "index.rst", + "lineno": 1, + "uri": "https://localhost:7777/", + "info": "Could not find a suitable TLS CA certificate bundle, invalid path: does/not/exist", + } + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) +def test_TooManyRedirects_on_HEAD(app): + class InfiniteRedirectOnHeadHandler(http.server.BaseHTTPRequestHandler): + def do_HEAD(self): + self.send_response(302, "Found") + self.send_header("Location", "http://localhost:7777/") + self.end_headers() + + def do_GET(self): + self.send_response(200, "OK") + self.end_headers() + self.wfile.write(b"ok\n") + + with http_server(InfiniteRedirectOnHeadHandler): + app.build() + + with open(app.outdir / 'output.json') as fp: + content = json.load(fp) + assert content == { + "code": 0, + "status": "working", + "filename": "index.rst", + "lineno": 1, + "uri": "http://localhost:7777/", + "info": "", + } + + +def make_retry_after_handler(responses): + class RetryAfterHandler(http.server.BaseHTTPRequestHandler): + def do_HEAD(self): + status, retry_after = responses.pop(0) + self.send_response(status) + if retry_after: + self.send_header('Retry-After', retry_after) + self.end_headers() + + def log_date_time_string(self): + """Strip date and time from logged messages for assertions.""" + return "" + + return RetryAfterHandler + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) +def test_too_many_requests_retry_after_int_delay(app, capsys, status): + with http_server(make_retry_after_handler([(429, "0"), (200, None)])), \ + mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0), \ + mock.patch("sphinx.builders.linkcheck.QUEUE_POLL_SECS", 0.01): + app.build() + content = (app.outdir / 'output.json').read_text() + assert json.loads(content) == { + "filename": "index.rst", + "lineno": 1, + "status": "working", + "code": 0, + "uri": "http://localhost:7777/", + "info": "", + } + rate_limit_log = "-rate limited- http://localhost:7777/ | sleeping...\n" + assert rate_limit_log in strip_colors(status.getvalue()) + _stdout, stderr = capsys.readouterr() + assert stderr == textwrap.dedent( + """\ + 127.0.0.1 - - [] "HEAD / HTTP/1.1" 429 - + 127.0.0.1 - - [] "HEAD / HTTP/1.1" 200 - + """ + ) + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) +def test_too_many_requests_retry_after_HTTP_date(app, capsys): + now = datetime.now().timetuple() + retry_after = wsgiref.handlers.format_date_time(time.mktime(now)) + with http_server(make_retry_after_handler([(429, retry_after), (200, None)])): + app.build() + content = (app.outdir / 'output.json').read_text() + assert json.loads(content) == { + "filename": "index.rst", + "lineno": 1, + "status": "working", + "code": 0, + "uri": "http://localhost:7777/", + "info": "", + } + _stdout, stderr = capsys.readouterr() + assert stderr == textwrap.dedent( + """\ + 127.0.0.1 - - [] "HEAD / HTTP/1.1" 429 - + 127.0.0.1 - - [] "HEAD / HTTP/1.1" 200 - + """ + ) + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) +def test_too_many_requests_retry_after_without_header(app, capsys): + with http_server(make_retry_after_handler([(429, None), (200, None)])),\ + mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0): + app.build() + content = (app.outdir / 'output.json').read_text() + assert json.loads(content) == { + "filename": "index.rst", + "lineno": 1, + "status": "working", + "code": 0, + "uri": "http://localhost:7777/", + "info": "", + } + _stdout, stderr = capsys.readouterr() + assert stderr == textwrap.dedent( + """\ + 127.0.0.1 - - [] "HEAD / HTTP/1.1" 429 - + 127.0.0.1 - - [] "HEAD / HTTP/1.1" 200 - + """ + ) + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) +def test_too_many_requests_user_timeout(app, capsys): + app.config.linkcheck_rate_limit_timeout = 0.0 + with http_server(make_retry_after_handler([(429, None)])): + app.build() + content = (app.outdir / 'output.json').read_text() + assert json.loads(content) == { + "filename": "index.rst", + "lineno": 1, + "status": "broken", + "code": 0, + "uri": "http://localhost:7777/", + "info": "429 Client Error: Too Many Requests for url: http://localhost:7777/", + } + + +class FakeResponse: + headers = {} # type: Dict[str, str] + url = "http://localhost/" + + +def test_limit_rate_default_sleep(app): + checker = CheckExternalLinksBuilder(app) + checker.rate_limits = {} + with mock.patch('time.time', return_value=0.0): + next_check = checker.limit_rate(FakeResponse()) + assert next_check == 60.0 + + +def test_limit_rate_user_max_delay(app): + app.config.linkcheck_rate_limit_timeout = 0.0 + checker = CheckExternalLinksBuilder(app) + checker.rate_limits = {} + next_check = checker.limit_rate(FakeResponse()) + assert next_check is None + + +def test_limit_rate_doubles_previous_wait_time(app): + checker = CheckExternalLinksBuilder(app) + checker.rate_limits = {"localhost": RateLimit(60.0, 0.0)} + with mock.patch('time.time', return_value=0.0): + next_check = checker.limit_rate(FakeResponse()) + assert next_check == 120.0 + + +def test_limit_rate_clips_wait_time_to_max_time(app): + checker = CheckExternalLinksBuilder(app) + app.config.linkcheck_rate_limit_timeout = 90.0 + checker.rate_limits = {"localhost": RateLimit(60.0, 0.0)} + with mock.patch('time.time', return_value=0.0): + next_check = checker.limit_rate(FakeResponse()) + assert next_check == 90.0 + + +def test_limit_rate_bails_out_after_waiting_max_time(app): + checker = CheckExternalLinksBuilder(app) + app.config.linkcheck_rate_limit_timeout = 90.0 + checker.rate_limits = {"localhost": RateLimit(90.0, 0.0)} + next_check = checker.limit_rate(FakeResponse()) + assert next_check is None diff --git a/tests/test_build_manpage.py b/tests/test_build_manpage.py index d4b1a320e..a017abc69 100644 --- a/tests/test_build_manpage.py +++ b/tests/test_build_manpage.py @@ -4,7 +4,7 @@ Test the build process with manpage builder with the test root. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_build_texinfo.py b/tests/test_build_texinfo.py index 9833218d7..546ccaabf 100644 --- a/tests/test_build_texinfo.py +++ b/tests/test_build_texinfo.py @@ -4,18 +4,17 @@ Test the build process with Texinfo builder with the test root. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import re import subprocess -from subprocess import CalledProcessError, PIPE +from subprocess import PIPE, CalledProcessError from unittest.mock import Mock import pytest -from test_build_html import ENV_WARNINGS from sphinx.builders.texinfo import default_texinfo_documents from sphinx.config import Config @@ -23,6 +22,7 @@ from sphinx.testing.util import strip_escseq from sphinx.util.docutils import new_document 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 diff --git a/tests/test_build_text.py b/tests/test_build_text.py index 8c00f5550..e3d8ff111 100644 --- a/tests/test_build_text.py +++ b/tests/test_build_text.py @@ -4,14 +4,14 @@ Test the build process with Text builder with the test root. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import pytest from docutils.utils import column_width -from sphinx.writers.text import MAXWIDTH, Table, Cell +from sphinx.writers.text import MAXWIDTH, Cell, Table def with_text_app(*args, **kw): diff --git a/tests/test_builder.py b/tests/test_builder.py index 4a37db84d..ed3f8cdaa 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -4,7 +4,7 @@ Test the Builder class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import pytest diff --git a/tests/test_catalogs.py b/tests/test_catalogs.py index e603a5812..986979fe2 100644 --- a/tests/test_catalogs.py +++ b/tests/test_catalogs.py @@ -4,7 +4,7 @@ Test the base build process. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import shutil diff --git a/tests/test_config.py b/tests/test_config.py index 552cbc90d..9a0b617d5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,7 @@ Test the sphinx.config.Config class and its handling in the Application class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,8 +14,8 @@ from unittest import mock import pytest import sphinx -from sphinx.config import Config, ENUM, check_confval_types -from sphinx.errors import ExtensionError, ConfigError, VersionRequirementError +from sphinx.config import ENUM, Config, check_confval_types +from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError from sphinx.testing.path import path diff --git a/tests/test_correct_year.py b/tests/test_correct_year.py index 7dc3ea89d..6afcce2b5 100644 --- a/tests/test_correct_year.py +++ b/tests/test_correct_year.py @@ -4,7 +4,7 @@ Test copyright year adjustment - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import pytest diff --git a/tests/test_directive_code.py b/tests/test_directive_code.py index eda331645..0ae11baf3 100644 --- a/tests/test_directive_code.py +++ b/tests/test_directive_code.py @@ -4,7 +4,7 @@ Test the code-block directive. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -250,6 +250,14 @@ def test_LiteralIncludeReader_dedent(literal_inc_path): " pass\n" "\n") + # dedent: None + options = {'lines': '9-11', 'dedent': None} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("def baz():\n" + " pass\n" + "\n") + @pytest.mark.xfail(os.name != 'posix', reason="Not working on windows") def test_LiteralIncludeReader_tabwidth(testroot): diff --git a/tests/test_directive_only.py b/tests/test_directive_only.py index 3de22c71f..72cbc6bd7 100644 --- a/tests/test_directive_only.py +++ b/tests/test_directive_only.py @@ -4,7 +4,7 @@ Test the only directive with the test root. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_directive_other.py b/tests/test_directive_other.py index 52e4a937c..09877208c 100644 --- a/tests/test_directive_other.py +++ b/tests/test_directive_other.py @@ -4,7 +4,7 @@ Test the other directives. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_directive_patch.py b/tests/test_directive_patch.py index 8572ff672..7dc568b1d 100644 --- a/tests/test_directive_patch.py +++ b/tests/test_directive_patch.py @@ -4,7 +4,7 @@ Test the patched directives. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_docutilsconf.py b/tests/test_docutilsconf.py index 02a7fc62b..9f12fd004 100644 --- a/tests/test_docutilsconf.py +++ b/tests/test_docutilsconf.py @@ -4,7 +4,7 @@ Test docutils.conf support for several writers. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index b6f72287e..2cfcf74fa 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -4,15 +4,19 @@ Tests the C Domain - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ + +import zlib +from xml.etree import ElementTree + import pytest from sphinx import addnodes from sphinx.addnodes import desc -from sphinx.domains.c import DefinitionParser, DefinitionError -from sphinx.domains.c import _max_id, _id_prefix, Symbol +from sphinx.domains.c import DefinitionError, DefinitionParser, Symbol, _id_prefix, _max_id +from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node @@ -52,8 +56,8 @@ def _check(name, input, idDict, output, key, asTextOutput): print("Result: ", res) print("Expected: ", outputAst) raise DefinitionError("") - rootSymbol = Symbol(None, None, None, None) - symbol = rootSymbol.add_declaration(ast, docname="TestDoc") + rootSymbol = Symbol(None, None, None, None, None) + symbol = rootSymbol.add_declaration(ast, docname="TestDoc", line=42) parentNode = addnodes.desc() signode = addnodes.desc_signature(input, '') parentNode += signode @@ -74,12 +78,12 @@ def _check(name, input, idDict, output, key, asTextOutput): idExpected.append(idExpected[i - 1]) idActual = [None] for i in range(1, _max_id + 1): - #try: + # try: id = ast.get_id(version=i) assert id is not None idActual.append(id[len(_id_prefix[i]):]) - #except NoOldIdError: - # idActual.append(None) + # except NoOldIdError: + # idActual.append(None) res = [True] for i in range(1, _max_id + 1): @@ -93,7 +97,7 @@ def _check(name, input, idDict, output, key, asTextOutput): print("Error in id version %d." % i) print("result: %s" % idActual[i]) print("expected: %s" % idExpected[i]) - #print(rootSymbol.dump(0)) + # print(rootSymbol.dump(0)) raise DefinitionError("") @@ -105,7 +109,7 @@ def check(name, input, idDict, output=None, key=None, asTextOutput=None): if name != 'macro': # Second, check with semicolon _check(name, input + ' ;', idDict, output + ';', key, - asTextOutput + ';' if asTextOutput is not None else None) + asTextOutput + ';' if asTextOutput is not None else None) def test_expressions(): @@ -421,7 +425,7 @@ def test_nested_name(): check('function', 'void f(.A.B a)', {1: "f"}) -def test_union_definitions(): +def test_struct_definitions(): check('struct', '{key}A', {1: 'A'}) @@ -481,7 +485,7 @@ def test_attributes(): # style: user-defined paren check('member', 'paren_attr() int f', {1: 'f'}) check('member', 'paren_attr(a) int f', {1: 'f'}) - check('member', 'paren_attr("") int f',{1: 'f'}) + check('member', 'paren_attr("") int f', {1: 'f'}) check('member', 'paren_attr(()[{}][]{}) int f', {1: 'f'}) with pytest.raises(DefinitionError): parse('member', 'paren_attr(() int f') @@ -520,7 +524,7 @@ def test_attributes(): def filter_warnings(warning, file): - lines = warning.getvalue().split("\n"); + lines = warning.getvalue().split("\n") res = [l for l in lines if "domain-c" in l and "{}.rst".format(file) in l and "WARNING: document isn't included in any toctree" not in l] print("Filtered warnings for file '{}':".format(file)) @@ -529,6 +533,25 @@ def filter_warnings(warning, file): return res +def extract_role_links(app, filename): + t = (app.outdir / filename).read_text() + lis = [l for l in t.split('\n') if l.startswith(" {key}has_var>', {2: 'I0E7has_varI1TNSt6void_tIDTadN1T3varEEEEE'}) - check('class', 'template {key}T', {2: 'IDpE1TIJPFi2TsEEE'}) check('class', 'template {key}T<(Is)...>', @@ -1001,7 +1002,7 @@ def test_build_domain_cpp_warn_template_param_qualified_name(app, status, warnin @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) -def test_build_domain_cpp_backslash_ok(app, status, warning): +def test_build_domain_cpp_backslash_ok_true(app, status, warning): app.builder.build_all() ws = filter_warnings(warning, "backslash") assert len(ws) == 0 @@ -1016,7 +1017,7 @@ def test_build_domain_cpp_semicolon(app, status, warning): @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True, 'strip_signature_backslash': True}) -def test_build_domain_cpp_backslash_ok(app, status, warning): +def test_build_domain_cpp_backslash_ok_false(app, status, warning): app.builder.build_all() ws = filter_warnings(warning, "backslash") assert len(ws) == 1 @@ -1050,8 +1051,8 @@ def test_build_domain_cpp_misuse_of_roles(app, status, warning): ('concept', ['concept']), ('enum', ['type', 'enum']), ('enumerator', ['enumerator']), - ('tParam', ['class', 'struct', 'union', 'func', 'member', 'var', 'type', 'concept', 'enum', 'enumerator', 'functionParam']), ('functionParam', ['member', 'var']), + ('templateParam', ['class', 'struct', 'union', 'member', 'var', 'type']), ] warn = [] for targetType, roles in ok: @@ -1059,6 +1060,9 @@ def test_build_domain_cpp_misuse_of_roles(app, status, warning): for r in allRoles: if r not in roles: warn.append("WARNING: cpp:{} targets a {} (".format(r, txtTargetType)) + if targetType == 'templateParam': + warn.append("WARNING: cpp:{} targets a {} (".format(r, txtTargetType)) + warn.append("WARNING: cpp:{} targets a {} (".format(r, txtTargetType)) warn = list(sorted(warn)) for w in ws: assert "targets a" in w @@ -1236,3 +1240,81 @@ def test_noindexentry(app): assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) assert_node(doctree[0], addnodes.index, entries=[('single', 'f (C++ function)', '_CPPv41fv', '', None)]) assert_node(doctree[2], addnodes.index, entries=[]) + + +def test_mix_decl_duplicate(app, warning): + # Issue 8270 + text = (".. cpp:struct:: A\n" + ".. cpp:function:: void A()\n" + ".. cpp:struct:: A\n") + restructuredtext.parse(app, text) + ws = warning.getvalue().split("\n") + assert len(ws) == 5 + assert "index.rst:2: WARNING: Duplicate C++ declaration, also defined at index:1." in ws[0] + assert "Declaration is '.. cpp:function:: void A()'." in ws[1] + assert "index.rst:3: WARNING: Duplicate C++ declaration, also defined at index:1." in ws[2] + assert "Declaration is '.. cpp:struct:: A'." in ws[3] + assert ws[4] == "" + + +@pytest.mark.sphinx(testroot='domain-cpp-intersphinx', confoverrides={'nitpicky': True}) +def test_intersphinx(tempdir, app, status, warning): + origSource = """\ +.. cpp:class:: _class +.. cpp:struct:: _struct +.. cpp:union:: _union +.. cpp:function:: void _function() +.. cpp:member:: int _member +.. cpp:var:: int _var +.. cpp:type:: _type +.. cpp:concept:: template _concept +.. cpp:enum:: _enum + + .. cpp:enumerator:: _enumerator + +.. cpp:enum-struct:: _enumStruct + + .. cpp:enumerator:: _scopedEnumerator + +.. cpp:enum-class:: _enumClass +.. cpp:function:: void _functionParam(int param) +.. cpp:function:: template void _templateParam() +""" # noqa + inv_file = tempdir / 'inventory' + inv_file.write_bytes(b'''\ +# Sphinx inventory version 2 +# Project: C Intersphinx Test +# Version: +# The remainder of this file is compressed using zlib. +''' + zlib.compress(b'''\ +_class cpp:class 1 index.html#_CPPv46$ - +_concept cpp:concept 1 index.html#_CPPv4I0E8$ - +_concept::T cpp:templateParam 1 index.html#_CPPv4I0E8_concept - +_enum cpp:enum 1 index.html#_CPPv45$ - +_enum::_enumerator cpp:enumerator 1 index.html#_CPPv4N5_enum11_enumeratorE - +_enumClass cpp:enum 1 index.html#_CPPv410$ - +_enumStruct cpp:enum 1 index.html#_CPPv411$ - +_enumStruct::_scopedEnumerator cpp:enumerator 1 index.html#_CPPv4N11_enumStruct17_scopedEnumeratorE - +_enumerator cpp:enumerator 1 index.html#_CPPv4N5_enum11_enumeratorE - +_function cpp:function 1 index.html#_CPPv49_functionv - +_functionParam cpp:function 1 index.html#_CPPv414_functionParami - +_functionParam::param cpp:functionParam 1 index.html#_CPPv414_functionParami - +_member cpp:member 1 index.html#_CPPv47$ - +_struct cpp:class 1 index.html#_CPPv47$ - +_templateParam cpp:function 1 index.html#_CPPv4I0E14_templateParamvv - +_templateParam::TParam cpp:templateParam 1 index.html#_CPPv4I0E14_templateParamvv - +_type cpp:type 1 index.html#_CPPv45$ - +_union cpp:union 1 index.html#_CPPv46$ - +_var cpp:member 1 index.html#_CPPv44$ - +''')) # noqa + app.config.intersphinx_mapping = { + 'https://localhost/intersphinx/cpp/': inv_file, + } + app.config.intersphinx_cache_limit = 0 + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + app.builder.build_all() + ws = filter_warnings(warning, "index") + assert len(ws) == 0 diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py index 9d0b59b91..1fb865d4b 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domain_js.py @@ -4,7 +4,7 @@ Tests the JavaScript Domain - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,10 +14,8 @@ import pytest from docutils import nodes from sphinx import addnodes -from sphinx.addnodes import ( - desc, desc_annotation, desc_content, desc_name, - desc_parameter, desc_parameterlist, desc_signature -) +from sphinx.addnodes import (desc, desc_annotation, desc_content, desc_name, desc_parameter, + desc_parameterlist, desc_signature) from sphinx.domains.javascript import JavaScriptDomain from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 8040af9cc..fe117659a 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -4,10 +4,11 @@ Tests the Python Domain - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import re import sys from unittest.mock import Mock @@ -15,15 +16,13 @@ import pytest from docutils import nodes from sphinx import addnodes -from sphinx.addnodes import ( - desc, desc_addname, desc_annotation, desc_content, desc_name, desc_optional, - desc_parameter, desc_parameterlist, desc_returns, desc_signature, - desc_sig_name, desc_sig_operator, desc_sig_punctuation, pending_xref, -) +from sphinx.addnodes import (desc, desc_addname, desc_annotation, desc_content, desc_name, + desc_optional, desc_parameter, desc_parameterlist, desc_returns, + desc_sig_name, desc_sig_operator, desc_sig_punctuation, + desc_signature, pending_xref) from sphinx.domains import IndexEntry -from sphinx.domains.python import ( - py_sig_re, _parse_annotation, _pseudo_parse_arglist, PythonDomain, PythonModuleIndex -) +from sphinx.domains.python import (PythonDomain, PythonModuleIndex, _parse_annotation, + _pseudo_parse_arglist, py_sig_re) from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node @@ -134,6 +133,29 @@ def test_domain_py_xrefs(app, status, warning): assert len(refnodes) == 2 +@pytest.mark.sphinx('html', testroot='domain-py') +def test_domain_py_xrefs_abbreviations(app, status, warning): + app.builder.build_all() + + content = (app.outdir / 'abbr.html').read_text() + assert re.search(r'normal: <.*>module_a.submodule.ModTopLevel.mod_child_1\(\)' + r'<.*>', + content) + assert re.search(r'relative: <.*>ModTopLevel.mod_child_1\(\)<.*>', + content) + assert re.search(r'short name: <.*>mod_child_1\(\)<.*>', + content) + assert re.search(r'relative \+ short name: <.*>mod_child_1\(\)<.*>', + content) + assert re.search(r'short name \+ relative: <.*>mod_child_1\(\)<.*>', + content) + + @pytest.mark.sphinx('dummy', testroot='domain-py') def test_domain_py_objects(app, status, warning): app.builder.build_all() @@ -313,7 +335,7 @@ def test_pyfunction_signature(app): def test_pyfunction_signature_full(app): text = (".. py:function:: hello(a: str, b = 1, *args: str, " - "c: bool = True, **kwargs: str) -> str") + "c: bool = True, d: tuple = (1, 2), **kwargs: str) -> str") doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, [desc, ([desc_signature, ([desc_name, "hello"], @@ -343,6 +365,14 @@ def test_pyfunction_signature_full(app): [desc_sig_operator, "="], " ", [nodes.inline, "True"])], + [desc_parameter, ([desc_sig_name, "d"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, pending_xref, "tuple"], + " ", + [desc_sig_operator, "="], + " ", + [nodes.inline, "(1, 2)"])], [desc_parameter, ([desc_sig_operator, "**"], [desc_sig_name, "kwargs"], [desc_sig_punctuation, ":"], @@ -768,6 +798,53 @@ def test_pydecoratormethod_signature(app): assert domain.objects['deco'] == ('index', 'deco', 'method') +def test_info_field_list(app): + text = (".. py:module:: example\n" + ".. py:class:: Class\n" + "\n" + " :param str name: blah blah\n" + " :param age: blah blah\n" + " :type age: int\n") + doctree = restructuredtext.parse(app, text) + print(doctree) + + assert_node(doctree, (nodes.target, + addnodes.index, + addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "class "], + [desc_addname, "example."], + [desc_name, "Class"])], + [desc_content, nodes.field_list, nodes.field])])) + assert_node(doctree[3][1][0][0], + ([nodes.field_name, "Parameters"], + [nodes.field_body, nodes.bullet_list, ([nodes.list_item, nodes.paragraph], + [nodes.list_item, nodes.paragraph])])) + + # :param str name: + assert_node(doctree[3][1][0][0][1][0][0][0], + ([addnodes.literal_strong, "name"], + " (", + [pending_xref, addnodes.literal_emphasis, "str"], + ")", + " -- ", + "blah blah")) + assert_node(doctree[3][1][0][0][1][0][0][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="str", + **{"py:module": "example", "py:class": "Class"}) + + # :param age: + :type age: + assert_node(doctree[3][1][0][0][1][0][1][0], + ([addnodes.literal_strong, "age"], + " (", + [pending_xref, addnodes.literal_emphasis, "int"], + ")", + " -- ", + "blah blah")) + assert_node(doctree[3][1][0][0][1][0][1][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="int", + **{"py:module": "example", "py:class": "Class"}) + + @pytest.mark.sphinx(freshenv=True) def test_module_index(app): text = (".. py:module:: docutils\n" @@ -851,3 +928,11 @@ def test_noindexentry(app): assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) assert_node(doctree[0], addnodes.index, entries=[('single', 'f (built-in class)', 'f', '', None)]) assert_node(doctree[2], addnodes.index, entries=[]) + + +@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()) diff --git a/tests/test_domain_rst.py b/tests/test_domain_rst.py index 86fe7ef3f..ed542ed35 100644 --- a/tests/test_domain_rst.py +++ b/tests/test_domain_rst.py @@ -4,14 +4,13 @@ Tests the reStructuredText domain. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from sphinx import addnodes -from sphinx.addnodes import ( - desc, desc_addname, desc_annotation, desc_content, desc_name, desc_signature -) +from sphinx.addnodes import (desc, desc_addname, desc_annotation, desc_content, desc_name, + desc_signature) from sphinx.domains.rst import parse_directive from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py index 33a000a3f..cf32e7964 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domain_std.py @@ -4,24 +4,20 @@ Tests the std domain - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -import pytest - from unittest import mock +import pytest from docutils import nodes from docutils.nodes import definition, definition_list, definition_list_item, term - from html5lib import HTMLParser from sphinx import addnodes -from sphinx.addnodes import ( - desc, desc_addname, desc_content, desc_name, desc_signature, glossary, index, - pending_xref -) +from sphinx.addnodes import (desc, desc_addname, desc_content, desc_name, desc_signature, + glossary, index, pending_xref) from sphinx.domains.std import StandardDomain from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node @@ -95,6 +91,28 @@ def test_get_full_qualified_name(): assert domain.get_full_qualified_name(node) == 'ls.-l' +def test_cmd_option_with_optional_value(app): + text = ".. option:: -j[=N]" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (index, + [desc, ([desc_signature, ([desc_name, '-j'], + [desc_addname, '[=N]'])], + [desc_content, ()])])) + objects = list(app.env.get_domain("std").get_objects()) + assert ('-j', '-j', 'cmdoption', 'index', 'cmdoption-j', 1) in objects + + +def test_cmd_option_starting_with_bracket(app): + text = ".. option:: [enable=]PATTERN" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (index, + [desc, ([desc_signature, ([desc_name, '[enable'], + [desc_addname, '=]PATTERN'])], + [desc_content, ()])])) + objects = list(app.env.get_domain("std").get_objects()) + assert ('[enable', '[enable', 'cmdoption', 'index', 'cmdoption-arg-enable', 1) in objects + + def test_glossary(app): text = (".. glossary::\n" "\n" @@ -337,7 +355,7 @@ def test_multiple_cmdoptions(app): def test_productionlist(app, status, warning): app.builder.build_all() - warnings = warning.getvalue().split("\n"); + warnings = warning.getvalue().split("\n") assert len(warnings) == 2 assert warnings[-1] == '' assert "Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1" in warnings[0] @@ -387,6 +405,22 @@ def test_productionlist(app, status, warning): assert "A ::= B C D E F G" in text +def test_productionlist2(app): + text = (".. productionlist:: P2\n" + " A: `:A` `A`\n" + " B: `P1:B` `~P1:B`\n") + doctree = restructuredtext.parse(app, text) + refnodes = list(doctree.traverse(pending_xref)) + assert_node(refnodes[0], pending_xref, reftarget="A") + assert_node(refnodes[1], pending_xref, reftarget="P2:A") + assert_node(refnodes[2], pending_xref, reftarget="P1:B") + assert_node(refnodes[3], pending_xref, reftarget="P1:B") + assert_node(refnodes[0], [pending_xref, nodes.literal, "A"]) + assert_node(refnodes[1], [pending_xref, nodes.literal, "A"]) + assert_node(refnodes[2], [pending_xref, nodes.literal, "P1:B"]) + assert_node(refnodes[3], [pending_xref, nodes.literal, "B"]) + + def test_disabled_docref(app): text = (":doc:`index`\n" ":doc:`!index`\n") @@ -394,3 +428,13 @@ def test_disabled_docref(app): assert_node(doctree, ([nodes.paragraph, ([pending_xref, nodes.inline, "index"], "\n", [nodes.inline, "index"])],)) + + +def test_labeled_rubric(app): + text = (".. _label:\n" + ".. rubric:: blah *blah* blah\n") + restructuredtext.parse(app, text) + + domain = app.env.get_domain("std") + assert 'label' in domain.labels + assert domain.labels['label'] == ('index', 'label', 'blah blah blah') diff --git a/tests/test_environment.py b/tests/test_environment.py index 7290eb6a0..9791c2d5b 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -4,16 +4,17 @@ Test the BuildEnvironment class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import shutil + import pytest from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.builders.latex import LaTeXBuilder -from sphinx.environment import CONFIG_OK, CONFIG_CHANGED, CONFIG_EXTENSIONS_CHANGED, CONFIG_NEW +from sphinx.environment import CONFIG_CHANGED, CONFIG_EXTENSIONS_CHANGED, CONFIG_NEW, CONFIG_OK from sphinx.testing.comparer import PathComparer @@ -137,6 +138,11 @@ def test_env_relfn2path(app): assert relfn == '../logo.jpg' assert absfn == app.srcdir.parent / 'logo.jpg' + # relative path traversal + relfn, absfn = app.env.relfn2path('subdir/../logo.jpg', 'index') + assert relfn == 'logo.jpg' + assert absfn == app.srcdir / 'logo.jpg' + # omit docname (w/ current docname) app.env.temp_data['docname'] = 'subdir/document' relfn, absfn = app.env.relfn2path('images/logo.jpg') diff --git a/tests/test_environment_indexentries.py b/tests/test_environment_indexentries.py index c15226d85..1b549bacb 100644 --- a/tests/test_environment_indexentries.py +++ b/tests/test_environment_indexentries.py @@ -4,7 +4,7 @@ Test the sphinx.environment.managers.indexentries. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -23,7 +23,7 @@ def test_create_single_index(app): ".. index:: Sphinx\n" ".. index:: Ель\n" ".. index:: ёлка\n" - ".. index:: ‏תירבע‎\n" + ".. index:: ‏עברית‎\n" ".. index:: 9-symbol\n" ".. index:: &-symbol\n" ".. index:: £100\n") @@ -38,9 +38,12 @@ def test_create_single_index(app): ('upgrade', [('', '#index-3')])], None]), ('Python', [[('', '#index-1')], [], None])]) assert index[3] == ('S', [('Sphinx', [[('', '#index-4')], [], None])]) - assert index[4] == ('Е', [('ёлка', [[('', '#index-6')], [], None]), - ('Ель', [[('', '#index-5')], [], None])]) - assert index[5] == ('ת', [('‏תירבע‎', [[('', '#index-7')], [], None])]) + assert index[4] == ('Е', + [('ёлка', [[('', '#index-6')], [], None]), + ('Ель', [[('', '#index-5')], [], None])]) + # Here the word starts with U+200F RIGHT-TO-LEFT MARK, which should be + # ignored when getting the first letter. + assert index[5] == ('ע', [('‏עברית‎', [[('', '#index-7')], [], None])]) @pytest.mark.sphinx('dummy', freshenv=True) @@ -69,8 +72,9 @@ def test_create_pair_index(app): ('ёлка', [('', '#index-5')]), ('Ель', [('', '#index-4')])], None])]) - assert index[6] == ('Е', [('ёлка', [[], [('Sphinx', [('', '#index-5')])], None]), - ('Ель', [[], [('Sphinx', [('', '#index-4')])], None])]) + assert index[6] == ('Е', + [('ёлка', [[], [('Sphinx', [('', '#index-5')])], None]), + ('Ель', [[], [('Sphinx', [('', '#index-4')])], None])]) @pytest.mark.sphinx('dummy', freshenv=True) diff --git a/tests/test_environment_toctree.py b/tests/test_environment_toctree.py index 4059e5cb2..41b3f727c 100644 --- a/tests/test_environment_toctree.py +++ b/tests/test_environment_toctree.py @@ -4,13 +4,13 @@ Test the sphinx.environment.managers.toctree. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import pytest from docutils import nodes -from docutils.nodes import bullet_list, list_item, caption, comment, reference +from docutils.nodes import bullet_list, caption, comment, list_item, reference from sphinx import addnodes from sphinx.addnodes import compact_paragraph, only diff --git a/tests/test_events.py b/tests/test_events.py index 4fbe03a17..bfff7a30f 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -4,7 +4,7 @@ Test the EventManager class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_ext_apidoc.py b/tests/test_ext_apidoc.py index e19a1d7ba..d6c45c268 100644 --- a/tests/test_ext_apidoc.py +++ b/tests/test_ext_apidoc.py @@ -4,7 +4,7 @@ Test the sphinx.apidoc module. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -216,6 +216,8 @@ def test_trailing_underscore(make_app, apidoc): def test_excludes(apidoc): outdir = apidoc.outdir assert (outdir / 'conf.py').isfile() + assert (outdir / 'a.rst').isfile() + assert (outdir / 'a.b.rst').isfile() assert (outdir / 'a.b.c.rst').isfile() # generated because not empty assert not (outdir / 'a.b.e.rst').isfile() # skipped because of empty after excludes assert (outdir / 'a.b.x.rst').isfile() @@ -231,6 +233,8 @@ def test_excludes_subpackage_should_be_skipped(apidoc): """Subpackage exclusion should work.""" outdir = apidoc.outdir assert (outdir / 'conf.py').isfile() + assert (outdir / 'a.rst').isfile() + assert (outdir / 'a.b.rst').isfile() assert (outdir / 'a.b.c.rst').isfile() # generated because not empty assert not (outdir / 'a.b.e.f.rst').isfile() # skipped because 'b/e' subpackage is skipped @@ -244,6 +248,8 @@ def test_excludes_module_should_be_skipped(apidoc): """Module exclusion should work.""" outdir = apidoc.outdir assert (outdir / 'conf.py').isfile() + assert (outdir / 'a.rst').isfile() + assert (outdir / 'a.b.rst').isfile() assert (outdir / 'a.b.c.rst').isfile() # generated because not empty assert not (outdir / 'a.b.e.f.rst').isfile() # skipped because of empty after excludes @@ -257,6 +263,8 @@ def test_excludes_module_should_not_be_skipped(apidoc): """Module should be included if no excludes are used.""" outdir = apidoc.outdir assert (outdir / 'conf.py').isfile() + assert (outdir / 'a.rst').isfile() + assert (outdir / 'a.b.rst').isfile() assert (outdir / 'a.b.c.rst').isfile() # generated because not empty assert (outdir / 'a.b.e.f.rst').isfile() # skipped because of empty after excludes diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index e3ec11124..1f083c8ae 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -5,7 +5,7 @@ Test the autodoc extension. This tests mainly the Documenters; the auto directives are tested in a test source file translated by test_build. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -17,7 +17,7 @@ import pytest from docutils.statemachine import ViewList from sphinx import addnodes -from sphinx.ext.autodoc import ModuleLevelDocumenter, ALL, Options +from sphinx.ext.autodoc import ALL, ModuleLevelDocumenter, Options from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options from sphinx.testing.util import SphinxTestApp, Struct # NOQA from sphinx.util.docutils import LoggingReporter @@ -177,7 +177,6 @@ def test_format_signature(app): for C in (D, E): assert formatsig('class', 'D', C, None, None) == '()' - class SomeMeta(type): def __call__(cls, a, b=None): return type.__call__(cls, a, b) @@ -209,7 +208,6 @@ def test_format_signature(app): assert formatsig('class', 'C', C, None, None) == '(a, b=None)' assert formatsig('class', 'C', D, 'a, b', 'X') == '(a, b) -> X' - class ListSubclass(list): pass @@ -219,7 +217,6 @@ def test_format_signature(app): else: assert formatsig('class', 'C', ListSubclass, None, None) == '' - class ExceptionSubclass(Exception): pass @@ -227,7 +224,6 @@ def test_format_signature(app): if getattr(Exception, '__text_signature__', None) is None: assert formatsig('class', 'C', ExceptionSubclass, None, None) == '' - # __init__ have signature at first line of docstring directive.env.config.autoclass_content = 'both' @@ -371,11 +367,6 @@ def test_get_doc(app): """Döcstring""" assert getdocl('function', f) == ['Döcstring'] - # already-unicode docstrings must be taken literally - def f(): - """Döcstring""" - assert getdocl('function', f) == ['Döcstring'] - # verify that method docstrings get extracted in both normal case # and in case of bound method posing as a function class J: # NOQA @@ -822,6 +813,7 @@ def test_autodoc_special_members(app): actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', + ' .. py:attribute:: Class.__annotations__', ' .. py:attribute:: Class.__dict__', ' .. py:method:: Class.__init__(arg)', ' .. py:attribute:: Class.__module__', @@ -965,7 +957,7 @@ def test_autodoc_inner_class(app): ' .. py:attribute:: Outer.factory', ' :module: target', '', - ' alias of :class:`builtins.dict`' + ' alias of :class:`dict`' ] actual = do_autodoc(app, 'class', 'target.Outer.Inner', options) @@ -1336,6 +1328,8 @@ def test_slots(app): '.. py:class:: Bar()', ' :module: target.slots', '', + ' docstring', + '', '', ' .. py:attribute:: Bar.attr1', ' :module: target.slots', @@ -1353,9 +1347,21 @@ def test_slots(app): ' :module: target.slots', '', '', + '.. py:class:: Baz()', + ' :module: target.slots', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Baz.attr', + ' :module: target.slots', + '', + '', '.. py:class:: Foo()', ' :module: target.slots', '', + ' docstring', + '', '', ' .. py:attribute:: Foo.attr', ' :module: target.slots', @@ -1717,6 +1723,11 @@ def test_autodoc_typed_instance_variables(app): '.. py:module:: target.typed_vars', '', '', + '.. py:attribute:: Alias', + ' :module: target.typed_vars', + '', + ' alias of :class:`target.typed_vars.Derived`', + '', '.. py:class:: Class()', ' :module: target.typed_vars', '', @@ -1826,9 +1837,31 @@ def test_autodoc_typed_inherited_instance_variables(app): '', ' .. py:attribute:: Derived.attr3', ' :module: target.typed_vars', + ' :type: int', ' :value: 0', '', '', + ' .. py:attribute:: Derived.attr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr4', + '', + '', + ' .. py:attribute:: Derived.attr5', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr5', + '', + '', + ' .. py:attribute:: Derived.attr6', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr6', + '', + '', ' .. py:attribute:: Derived.attr7', ' :module: target.typed_vars', ' :type: int', @@ -1852,10 +1885,19 @@ def test_autodoc_GenericAlias(app): '.. py:module:: target.genericalias', '', '', + '.. py:class:: Class()', + ' :module: target.genericalias', + '', + '', + ' .. py:attribute:: Class.T', + ' :module: target.genericalias', + '', + ' alias of :class:`List`\\ [:class:`int`]', + '', '.. py:attribute:: T', ' :module: target.genericalias', '', - ' alias of :class:`typing.List`', + ' alias of :class:`List`\\ [:class:`int`]', ] else: assert list(actual) == [ @@ -1863,12 +1905,25 @@ def test_autodoc_GenericAlias(app): '.. py:module:: target.genericalias', '', '', + '.. py:class:: Class()', + ' :module: target.genericalias', + '', + '', + ' .. py:attribute:: Class.T', + ' :module: target.genericalias', + '', + ' A list of int', + '', + ' alias of List[int]', + '', + '', '.. py:data:: T', ' :module: target.genericalias', '', ' A list of int', '', ' alias of List[int]', + '', ] @@ -1882,6 +1937,26 @@ def test_autodoc_TypeVar(app): '.. py:module:: target.typevar', '', '', + '.. py:class:: Class()', + ' :module: target.typevar', + '', + '', + ' .. py:attribute:: Class.T1', + ' :module: target.typevar', + '', + ' T1', + '', + " alias of TypeVar('T1')", + '', + '', + ' .. py:attribute:: Class.T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :class:`int`', + '', + '', '.. py:data:: T1', ' :module: target.typevar', '', @@ -1889,6 +1964,7 @@ def test_autodoc_TypeVar(app): '', " alias of TypeVar('T1')", '', + '', '.. py:data:: T3', ' :module: target.typevar', '', @@ -1896,6 +1972,7 @@ def test_autodoc_TypeVar(app): '', " alias of TypeVar('T3', int, str)", '', + '', '.. py:data:: T4', ' :module: target.typevar', '', @@ -1903,12 +1980,22 @@ def test_autodoc_TypeVar(app): '', " alias of TypeVar('T4', covariant=True)", '', + '', '.. py:data:: T5', ' :module: target.typevar', '', ' T5', '', " alias of TypeVar('T5', contravariant=True)", + '', + '', + '.. py:data:: T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :class:`int`', + '', ] @@ -1979,19 +2066,26 @@ def test_autodoc_for_egged_code(app): def test_singledispatch(app): options = {"members": None} actual = do_autodoc(app, 'module', 'target.singledispatch', options) - assert list(actual) == [ - '', - '.. py:module:: target.singledispatch', - '', - '', - '.. py:function:: func(arg, kwarg=None)', - ' func(arg: int, kwarg=None)', - ' func(arg: str, kwarg=None)', - ' :module: target.singledispatch', - '', - ' A function for general use.', - '', - ] + if sys.version_info < (3, 6): + # check the result via "in" because the order of singledispatch signatures is + # usually changed (because dict is not OrderedDict yet!) + assert '.. py:function:: func(arg, kwarg=None)' in actual + assert ' func(arg: int, kwarg=None)' in actual + assert ' func(arg: str, kwarg=None)' in actual + else: + assert list(actual) == [ + '', + '.. py:module:: target.singledispatch', + '', + '', + '.. py:function:: func(arg, kwarg=None)', + ' func(arg: int, kwarg=None)', + ' func(arg: str, kwarg=None)', + ' :module: target.singledispatch', + '', + ' A function for general use.', + '', + ] @pytest.mark.skipif(sys.version_info < (3, 8), @@ -2139,17 +2233,17 @@ def test_overload(app): ' docstring', '', '', - ' .. py:method:: Math.sum(x: int, y: int) -> int', - ' Math.sum(x: float, y: float) -> float', - ' Math.sum(x: str, y: str) -> str', + ' .. py:method:: Math.sum(x: int, y: int = 0) -> int', + ' Math.sum(x: float, y: float = 0.0) -> float', + ' Math.sum(x: str, y: str = None) -> str', ' :module: target.overload', '', ' docstring', '', '', - '.. py:function:: sum(x: int, y: int) -> int', - ' sum(x: float, y: float) -> float', - ' sum(x: str, y: str) -> str', + '.. py:function:: sum(x: int, y: int = 0) -> int', + ' sum(x: float, y: float = 0.0) -> float', + ' sum(x: str, y: str = None) -> str', ' :module: target.overload', '', ' docstring', @@ -2296,3 +2390,49 @@ def test_name_mangling(app): ' name of Foo', '', ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_hide_value(app): + options = {'members': True} + actual = do_autodoc(app, 'module', 'target.hide_value', options) + assert list(actual) == [ + '', + '.. py:module:: target.hide_value', + '', + '', + '.. py:class:: Foo()', + ' :module: target.hide_value', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.SENTINEL1', + ' :module: target.hide_value', + '', + ' docstring', + '', + ' :meta hide-value:', + '', + '', + ' .. py:attribute:: Foo.SENTINEL2', + ' :module: target.hide_value', + '', + ' :meta hide-value:', + '', + '', + '.. py:data:: SENTINEL1', + ' :module: target.hide_value', + '', + ' docstring', + '', + ' :meta hide-value:', + '', + '', + '.. py:data:: SENTINEL2', + ' :module: target.hide_value', + '', + ' :meta hide-value:', + '', + ] diff --git a/tests/test_ext_autodoc_autoattribute.py b/tests/test_ext_autodoc_autoattribute.py new file mode 100644 index 000000000..48e897b2e --- /dev/null +++ b/tests/test_ext_autodoc_autoattribute.py @@ -0,0 +1,217 @@ +""" + test_ext_autodoc_autoattribute + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test the autodoc extension. This tests mainly the Documenters; the auto + directives are tested in a test source file translated by test_build. + + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys + +import pytest + +from .test_ext_autodoc import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute(app): + actual = do_autodoc(app, 'attribute', 'target.Class.attr') + assert list(actual) == [ + '', + '.. py:attribute:: Class.attr', + ' :module: target', + " :value: 'bar'", + '', + ' should be documented -- süß', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_novalue(app): + options = {'no-value': True} + actual = do_autodoc(app, 'attribute', 'target.Class.attr', options) + assert list(actual) == [ + '', + '.. py:attribute:: Class.attr', + ' :module: target', + '', + ' should be documented -- süß', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_typed_variable(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Class.attr2') + assert list(actual) == [ + '', + '.. py:attribute:: Class.attr2', + ' :module: target.typed_vars', + ' :type: int', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_typed_variable_in_alias(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Alias.attr2') + assert list(actual) == [ + '', + '.. py:attribute:: Alias.attr2', + ' :module: target.typed_vars', + ' :type: int', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_instance_variable(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Class.attr4') + assert list(actual) == [ + '', + '.. py:attribute:: Class.attr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr4', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_instance_variable_in_alias(app): + actual = do_autodoc(app, 'attribute', 'target.typed_vars.Alias.attr4') + assert list(actual) == [ + '', + '.. py:attribute:: Alias.attr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ' attr4', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_slots_variable_list(app): + actual = do_autodoc(app, 'attribute', 'target.slots.Foo.attr') + assert list(actual) == [ + '', + '.. py:attribute:: Foo.attr', + ' :module: target.slots', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_slots_variable_dict(app): + actual = do_autodoc(app, 'attribute', 'target.slots.Bar.attr1') + assert list(actual) == [ + '', + '.. py:attribute:: Bar.attr1', + ' :module: target.slots', + '', + ' docstring of attr1', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_slots_variable_str(app): + actual = do_autodoc(app, 'attribute', 'target.slots.Baz.attr') + assert list(actual) == [ + '', + '.. py:attribute:: Baz.attr', + ' :module: target.slots', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_GenericAlias(app): + actual = do_autodoc(app, 'attribute', 'target.genericalias.Class.T') + if sys.version_info < (3, 7): + assert list(actual) == [ + '', + '.. py:attribute:: Class.T', + ' :module: target.genericalias', + ' :value: typing.List[int]', + '', + ' A list of int', + '', + ] + else: + assert list(actual) == [ + '', + '.. py:attribute:: Class.T', + ' :module: target.genericalias', + '', + ' A list of int', + '', + ' alias of List[int]', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_NewType(app): + actual = do_autodoc(app, 'attribute', 'target.typevar.Class.T6') + assert list(actual) == [ + '', + '.. py:attribute:: Class.T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :class:`int`', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_TypeVar(app): + actual = do_autodoc(app, 'attribute', 'target.typevar.Class.T1') + assert list(actual) == [ + '', + '.. py:attribute:: Class.T1', + ' :module: target.typevar', + '', + ' T1', + '', + " alias of TypeVar('T1')", + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_hide_value(app): + actual = do_autodoc(app, 'attribute', 'target.hide_value.Foo.SENTINEL1') + assert list(actual) == [ + '', + '.. py:attribute:: Foo.SENTINEL1', + ' :module: target.hide_value', + '', + ' docstring', + '', + ' :meta hide-value:', + '', + ] + + actual = do_autodoc(app, 'attribute', 'target.hide_value.Foo.SENTINEL2') + assert list(actual) == [ + '', + '.. py:attribute:: Foo.SENTINEL2', + ' :module: target.hide_value', + '', + ' :meta hide-value:', + '', + ] diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py new file mode 100644 index 000000000..488b72263 --- /dev/null +++ b/tests/test_ext_autodoc_autoclass.py @@ -0,0 +1,193 @@ +""" + test_ext_autodoc_autoclass + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test the autodoc extension. This tests mainly the Documenters; the auto + directives are tested in a test source file translated by test_build. + + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys + +import pytest + +from .test_ext_autodoc import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_classes(app): + actual = do_autodoc(app, 'function', 'target.classes.Foo') + assert list(actual) == [ + '', + '.. py:function:: Foo()', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Bar') + assert list(actual) == [ + '', + '.. py:function:: Bar(x, y)', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Baz') + assert list(actual) == [ + '', + '.. py:function:: Baz(x, y)', + ' :module: target.classes', + '', + ] + + actual = do_autodoc(app, 'function', 'target.classes.Qux') + assert list(actual) == [ + '', + '.. py:function:: Qux(foo, bar)', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_instance_variable(app): + options = {'members': True} + actual = do_autodoc(app, 'class', 'target.instance_variable.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar()', + ' :module: target.instance_variable', + '', + '', + ' .. py:attribute:: Bar.attr2', + ' :module: target.instance_variable', + '', + ' docstring bar', + '', + '', + ' .. py:attribute:: Bar.attr3', + ' :module: target.instance_variable', + '', + ' docstring bar', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_inherited_instance_variable(app): + options = {'members': True, + 'inherited-members': True} + actual = do_autodoc(app, 'class', 'target.instance_variable.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar()', + ' :module: target.instance_variable', + '', + '', + ' .. py:attribute:: Bar.attr1', + ' :module: target.instance_variable', + '', + ' docstring foo', + '', + '', + ' .. py:attribute:: Bar.attr2', + ' :module: target.instance_variable', + '', + ' docstring bar', + '', + '', + ' .. py:attribute:: Bar.attr3', + ' :module: target.instance_variable', + '', + ' docstring bar', + '', + ] + + +def test_decorators(app): + actual = do_autodoc(app, 'class', 'target.decorator.Baz') + assert list(actual) == [ + '', + '.. py:class:: Baz(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + actual = do_autodoc(app, 'class', 'target.decorator.Qux') + assert list(actual) == [ + '', + '.. py:class:: Qux(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + actual = do_autodoc(app, 'class', 'target.decorator.Quux') + assert list(actual) == [ + '', + '.. py:class:: Quux(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_slots_attribute(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.slots.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar()', + ' :module: target.slots', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Bar.attr1', + ' :module: target.slots', + '', + ' docstring of attr1', + '', + '', + ' .. py:attribute:: Bar.attr2', + ' :module: target.slots', + '', + ' docstring of instance attr2', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_show_inheritance_for_subclass_of_generic_type(app): + options = {'show-inheritance': True} + actual = do_autodoc(app, 'class', 'target.classes.Quux', options) + assert list(actual) == [ + '', + '.. py:class:: Quux(iterable=(), /)', + ' :module: target.classes', + '', + ' Bases: :class:`List`\\ [:obj:`Union`\\ [:class:`int`, :class:`float`]]', + '', + ' A subclass of List[Union[int, float]]', + '', + ] + + +def test_class_alias(app): + def autodoc_process_docstring(*args): + """A handler always raises an error. + This confirms this handler is never called for class aliases. + """ + raise + + app.connect('autodoc-process-docstring', autodoc_process_docstring) + actual = do_autodoc(app, 'class', 'target.classes.Alias') + assert list(actual) == [ + '', + '.. py:attribute:: Alias', + ' :module: target.classes', + '', + ' alias of :class:`target.classes.Foo`', + ] diff --git a/tests/test_ext_autodoc_autodata.py b/tests/test_ext_autodoc_autodata.py new file mode 100644 index 000000000..907d70aa1 --- /dev/null +++ b/tests/test_ext_autodoc_autodata.py @@ -0,0 +1,157 @@ +""" + test_ext_autodoc_autodata + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test the autodoc extension. This tests mainly the Documenters; the auto + directives are tested in a test source file translated by test_build. + + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys + +import pytest + +from .test_ext_autodoc import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata(app): + actual = do_autodoc(app, 'data', 'target.integer') + assert list(actual) == [ + '', + '.. py:data:: integer', + ' :module: target', + ' :value: 1', + '', + ' documentation for the integer', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_novalue(app): + options = {'no-value': True} + actual = do_autodoc(app, 'data', 'target.integer', options) + assert list(actual) == [ + '', + '.. py:data:: integer', + ' :module: target', + '', + ' documentation for the integer', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_typed_variable(app): + actual = do_autodoc(app, 'data', 'target.typed_vars.attr2') + assert list(actual) == [ + '', + '.. py:data:: attr2', + ' :module: target.typed_vars', + ' :type: str', + '', + ' attr2', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_type_comment(app): + actual = do_autodoc(app, 'data', 'target.typed_vars.attr3') + assert list(actual) == [ + '', + '.. py:data:: attr3', + ' :module: target.typed_vars', + ' :type: str', + " :value: ''", + '', + ' attr3', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_GenericAlias(app): + actual = do_autodoc(app, 'data', 'target.genericalias.T') + if sys.version_info < (3, 7): + assert list(actual) == [ + '', + '.. py:data:: T', + ' :module: target.genericalias', + ' :value: typing.List[int]', + '', + ' A list of int', + '', + ] + else: + assert list(actual) == [ + '', + '.. py:data:: T', + ' :module: target.genericalias', + '', + ' A list of int', + '', + ' alias of List[int]', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_NewType(app): + actual = do_autodoc(app, 'data', 'target.typevar.T6') + assert list(actual) == [ + '', + '.. py:data:: T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :class:`int`', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_TypeVar(app): + actual = do_autodoc(app, 'data', 'target.typevar.T1') + assert list(actual) == [ + '', + '.. py:data:: T1', + ' :module: target.typevar', + '', + ' T1', + '', + " alias of TypeVar('T1')", + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_hide_value(app): + actual = do_autodoc(app, 'data', 'target.hide_value.SENTINEL1') + assert list(actual) == [ + '', + '.. py:data:: SENTINEL1', + ' :module: target.hide_value', + '', + ' docstring', + '', + ' :meta hide-value:', + '', + ] + + actual = do_autodoc(app, 'data', 'target.hide_value.SENTINEL2') + assert list(actual) == [ + '', + '.. py:data:: SENTINEL2', + ' :module: target.hide_value', + '', + ' :meta hide-value:', + '', + ] diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_ext_autodoc_autofunction.py index bb292bc6a..aa0476687 100644 --- a/tests/test_ext_autodoc_autofunction.py +++ b/tests/test_ext_autodoc_autofunction.py @@ -5,13 +5,15 @@ Test the autodoc extension. This tests mainly the Documenters; the auto directives are tested in a test source file translated by test_build. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import sys + import pytest -from test_ext_autodoc import do_autodoc +from .test_ext_autodoc import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') @@ -40,6 +42,14 @@ def test_classes(app): '', ] + actual = do_autodoc(app, 'function', 'target.classes.Qux') + assert list(actual) == [ + '', + '.. py:function:: Qux(foo, bar)', + ' :module: target.classes', + '', + ] + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_callable(app): @@ -108,16 +118,23 @@ def test_decorated(app): def test_singledispatch(app): options = {} actual = do_autodoc(app, 'function', 'target.singledispatch.func', options) - assert list(actual) == [ - '', - '.. py:function:: func(arg, kwarg=None)', - ' func(arg: int, kwarg=None)', - ' func(arg: str, kwarg=None)', - ' :module: target.singledispatch', - '', - ' A function for general use.', - '', - ] + if sys.version_info < (3, 6): + # check the result via "in" because the order of singledispatch signatures is + # usually changed (because dict is not OrderedDict yet!) + assert '.. py:function:: func(arg, kwarg=None)' in actual + assert ' func(arg: int, kwarg=None)' in actual + assert ' func(arg: str, kwarg=None)' in actual + else: + assert list(actual) == [ + '', + '.. py:function:: func(arg, kwarg=None)', + ' func(arg: int, kwarg=None)', + ' func(arg: str, kwarg=None)', + ' :module: target.singledispatch', + '', + ' A function for general use.', + '', + ] @pytest.mark.sphinx('html', testroot='ext-autodoc') diff --git a/tests/test_ext_autodoc_automodule.py b/tests/test_ext_autodoc_automodule.py new file mode 100644 index 000000000..df57724b3 --- /dev/null +++ b/tests/test_ext_autodoc_automodule.py @@ -0,0 +1,44 @@ +""" + test_ext_autodoc_autocmodule + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test the autodoc extension. This tests mainly the Documenters; the auto + directives are tested in a test source file translated by test_build. + + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys + +import pytest + +from .test_ext_autodoc import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_empty_all(app): + options = {'members': True} + actual = do_autodoc(app, 'module', 'target.empty_all', options) + assert list(actual) == [ + '', + '.. py:module:: target.empty_all', + '', + 'docsting of empty_all module.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_mock_imports': ['missing_module', + 'missing_package1', + 'missing_package2', + 'missing_package3', + 'sphinx.missing_module4']}) +@pytest.mark.usefixtures("rollback_sysmodules") +def test_subclass_of_mocked_object(app): + sys.modules.pop('target', None) # unload target module to clear the module cache + + options = {'members': True} + actual = do_autodoc(app, 'module', 'target.need_mocks', options) + assert '.. py:class:: Inherited(*args: Any, **kwargs: Any)' in actual diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 41ccb230e..5b75cd972 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -4,7 +4,7 @@ Test the autodoc extension. This tests mainly for config variables - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,7 +15,7 @@ import pytest from sphinx.testing import restructuredtext -from test_ext_autodoc import do_autodoc +from .test_ext_autodoc import do_autodoc IS_PYPY = platform.python_implementation() == 'PyPy' @@ -429,7 +429,10 @@ def test_autoclass_content_and_docstring_signature_both(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') +@pytest.mark.usefixtures("rollback_sysmodules") def test_mocked_module_imports(app, warning): + sys.modules.pop('target', None) # unload target module to clear the module cache + # no autodoc_mock_imports options = {"members": 'TestAutodoc,decoratedFunction,func'} actual = do_autodoc(app, 'module', 'target.need_mocks', options) @@ -490,7 +493,7 @@ def test_autodoc_typehints_signature(app): '.. py:module:: target.typehints', '', '', - '.. py:class:: Math(s: str, o: object = None)', + '.. py:class:: Math(s: str, o: Optional[Any] = None)', ' :module: target.typehints', '', '', @@ -610,6 +613,54 @@ def test_autodoc_typehints_none(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': 'none'}) +def test_autodoc_typehints_none_for_overload(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.overload', options) + assert list(actual) == [ + '', + '.. py:module:: target.overload', + '', + '', + '.. py:class:: Bar(x, y)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Baz(x, y)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Foo(x, y)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:class:: Math()', + ' :module: target.overload', + '', + ' docstring', + '', + '', + ' .. py:method:: Math.sum(x, y=None)', + ' :module: target.overload', + '', + ' docstring', + '', + '', + '.. py:function:: sum(x, y=None)', + ' :module: target.overload', + '', + ' docstring', + '', + ] + + @pytest.mark.sphinx('text', testroot='ext-autodoc', confoverrides={'autodoc_typehints': "description"}) def test_autodoc_typehints_description(app): @@ -653,6 +704,26 @@ def test_autodoc_type_aliases(app): '.. py:module:: target.annotations', '', '', + '.. py:class:: Foo()', + ' :module: target.annotations', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr1', + ' :module: target.annotations', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr2', + ' :module: target.annotations', + ' :type: int', + '', + ' docstring', + '', + '', '.. py:function:: mult(x: int, y: int) -> int', ' mult(x: float, y: float) -> float', ' :module: target.annotations', @@ -665,6 +736,21 @@ def test_autodoc_type_aliases(app): '', ' docstring', '', + '', + '.. py:data:: variable', + ' :module: target.annotations', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:data:: variable2', + ' :module: target.annotations', + ' :type: int', + ' :value: None', + '', + ' docstring', + '', ] # define aliases @@ -675,6 +761,26 @@ def test_autodoc_type_aliases(app): '.. py:module:: target.annotations', '', '', + '.. py:class:: Foo()', + ' :module: target.annotations', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr1', + ' :module: target.annotations', + ' :type: myint', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr2', + ' :module: target.annotations', + ' :type: myint', + '', + ' docstring', + '', + '', '.. py:function:: mult(x: myint, y: myint) -> myint', ' mult(x: float, y: float) -> float', ' :module: target.annotations', @@ -687,9 +793,46 @@ def test_autodoc_type_aliases(app): '', ' docstring', '', + '', + '.. py:data:: variable', + ' :module: target.annotations', + ' :type: myint', + '', + ' docstring', + '', + '', + '.. py:data:: variable2', + ' :module: target.annotations', + ' :type: myint', + ' :value: None', + '', + ' docstring', + '', ] +@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') +@pytest.mark.sphinx('text', testroot='ext-autodoc', + srcdir='autodoc_typehints_description_and_type_aliases', + confoverrides={'autodoc_typehints': "description", + 'autodoc_type_aliases': {'myint': 'myint'}}) +def test_autodoc_typehints_description_and_type_aliases(app): + (app.srcdir / 'annotations.rst').write_text('.. autofunction:: target.annotations.sum') + app.build() + context = (app.outdir / 'annotations.txt').read_text() + assert ('target.annotations.sum(x, y)\n' + '\n' + ' docstring\n' + '\n' + ' Parameters:\n' + ' * **x** (*myint*) --\n' + '\n' + ' * **y** (*myint*) --\n' + '\n' + ' Return type:\n' + ' myint\n' == context) + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options(app): # no settings diff --git a/tests/test_ext_autodoc_events.py b/tests/test_ext_autodoc_events.py index 7ddc952ab..561ac49de 100644 --- a/tests/test_ext_autodoc_events.py +++ b/tests/test_ext_autodoc_events.py @@ -4,14 +4,15 @@ Test the autodoc extension. This tests mainly for autodoc events - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import pytest from sphinx.ext.autodoc import between, cut_lines -from test_ext_autodoc import do_autodoc + +from .test_ext_autodoc import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') @@ -33,6 +34,23 @@ def test_process_docstring(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_process_docstring_for_nondatadescriptor(app): + def on_process_docstring(app, what, name, obj, options, lines): + raise + + app.connect('autodoc-process-docstring', on_process_docstring) + + actual = do_autodoc(app, 'attribute', 'target.AttCls.a1') + assert list(actual) == [ + '', + '.. py:attribute:: AttCls.a1', + ' :module: target', + ' :value: hello world', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_cut_lines(app): app.connect('autodoc-process-docstring', @@ -80,3 +98,28 @@ def test_between_exclude(app): ' third line', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_skip_module_member(app): + def autodoc_skip_member(app, what, name, obj, skip, options): + if name == "Class": + return True # Skip "Class" class in __all__ + elif name == "raises": + return False # Show "raises()" function (not in __all__) + + app.connect('autodoc-skip-member', autodoc_skip_member) + + options = {"members": None} + actual = do_autodoc(app, 'module', 'target', options) + assert list(actual) == [ + '', + '.. py:module:: target', + '', + '', + '.. py:function:: raises(exc, func, *args, **kwds)', + ' :module: target', + '', + ' Raise AssertionError if ``func(*args, **kwds)`` does not raise *exc*.', + '', + ] diff --git a/tests/test_ext_autodoc_mock.py b/tests/test_ext_autodoc_mock.py index a29170f75..497bd8a6b 100644 --- a/tests/test_ext_autodoc_mock.py +++ b/tests/test_ext_autodoc_mock.py @@ -4,7 +4,7 @@ Test the autodoc extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,7 +15,7 @@ from typing import TypeVar import pytest -from sphinx.ext.autodoc.mock import _MockModule, _MockObject, mock +from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock, undecorate def test_MockModule(): @@ -115,17 +115,38 @@ def test_mock_decorator(): @mock.function_deco def func(): - """docstring""" + pass class Foo: @mock.method_deco def meth(self): - """docstring""" + pass @mock.class_deco class Bar: - """docstring""" + pass - assert func.__doc__ == "docstring" - assert Foo.meth.__doc__ == "docstring" - assert Bar.__doc__ == "docstring" + @mock.funcion_deco(Foo) + class Baz: + pass + + assert undecorate(func).__name__ == "func" + assert undecorate(Foo.meth).__name__ == "meth" + assert undecorate(Bar).__name__ == "Bar" + assert undecorate(Baz).__name__ == "Baz" + + +def test_ismock(): + with mock(['sphinx.unknown']): + mod1 = import_module('sphinx.unknown') + mod2 = import_module('sphinx.application') + + class Inherited(mod1.Class): + pass + + assert ismock(mod1) is True + assert ismock(mod1.Class) is True + assert ismock(Inherited) is False + + assert ismock(mod2) is False + assert ismock(mod2.Sphinx) is False diff --git a/tests/test_ext_autodoc_private_members.py b/tests/test_ext_autodoc_private_members.py index 9d7ff487a..0de74b898 100644 --- a/tests/test_ext_autodoc_private_members.py +++ b/tests/test_ext_autodoc_private_members.py @@ -4,13 +4,13 @@ Test the autodoc extension. This tests mainly for private-members option. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import pytest -from test_ext_autodoc import do_autodoc +from .test_ext_autodoc import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') @@ -23,6 +23,13 @@ def test_private_field(app): '.. py:module:: target.private', '', '', + '.. py:data:: _PUBLIC_CONSTANT', + ' :module: target.private', + ' :value: None', + '', + ' :meta public:', + '', + '', '.. py:function:: _public_function(name)', ' :module: target.private', '', @@ -44,6 +51,20 @@ def test_private_field_and_private_members(app): '.. py:module:: target.private', '', '', + '.. py:data:: PRIVATE_CONSTANT', + ' :module: target.private', + ' :value: None', + '', + ' :meta private:', + '', + '', + '.. py:data:: _PUBLIC_CONSTANT', + ' :module: target.private', + ' :value: None', + '', + ' :meta public:', + '', + '', '.. py:function:: _public_function(name)', ' :module: target.private', '', @@ -66,13 +87,20 @@ def test_private_field_and_private_members(app): def test_private_members(app): app.config.autoclass_content = 'class' options = {"members": None, - "private-members": "_public_function"} + "private-members": "_PUBLIC_CONSTANT,_public_function"} actual = do_autodoc(app, 'module', 'target.private', options) assert list(actual) == [ '', '.. py:module:: target.private', '', '', + '.. py:data:: _PUBLIC_CONSTANT', + ' :module: target.private', + ' :value: None', + '', + ' :meta public:', + '', + '', '.. py:function:: _public_function(name)', ' :module: target.private', '', diff --git a/tests/test_ext_autosectionlabel.py b/tests/test_ext_autosectionlabel.py index 310435d8e..0afe13b41 100644 --- a/tests/test_ext_autosectionlabel.py +++ b/tests/test_ext_autosectionlabel.py @@ -4,7 +4,7 @@ Test sphinx.ext.autosectionlabel extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index 96b39ce0e..da7c53ec9 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -4,7 +4,7 @@ Test the autosummary extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -16,13 +16,11 @@ import pytest from docutils import nodes from sphinx import addnodes -from sphinx.ext.autosummary import ( - autosummary_table, autosummary_toc, mangle_signature, import_by_name, extract_summary -) -from sphinx.ext.autosummary.generate import ( - AutosummaryEntry, generate_autosummary_content, generate_autosummary_docs, - main as autogen_main -) +from sphinx.ext.autosummary import (autosummary_table, autosummary_toc, extract_summary, + import_by_name, mangle_signature) +from sphinx.ext.autosummary.generate import (AutosummaryEntry, generate_autosummary_content, + generate_autosummary_docs) +from sphinx.ext.autosummary.generate import main as autogen_main from sphinx.testing.util import assert_node, etree_parse from sphinx.util.docutils import new_document from sphinx.util.osutil import cd @@ -99,6 +97,9 @@ def test_extract_summary(capsys): doc = ['Blabla, i.e. bla.'] assert extract_summary(doc, document) == ' '.join(doc) + doc = ['Blabla, et al. bla.'] + assert extract_summary(doc, document) == ' '.join(doc) + # literal doc = ['blah blah::'] assert extract_summary(doc, document) == 'blah blah.' @@ -377,7 +378,10 @@ def test_autosummary_generate_overwrite2(app_params, make_app): @pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive') +@pytest.mark.usefixtures("rollback_sysmodules") def test_autosummary_recursive(app, status, warning): + sys.modules.pop('package', None) # unload target module to clear the module cache + app.build() # autosummary having :recursive: option @@ -401,6 +405,20 @@ def test_autosummary_recursive(app, status, warning): assert 'package.package.module' in content +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive', + srcdir='test_autosummary_recursive_skips_mocked_modules', + confoverrides={'autosummary_mock_imports': ['package.package']}) +@pytest.mark.usefixtures("rollback_sysmodules") +def test_autosummary_recursive_skips_mocked_modules(app, status, warning): + sys.modules.pop('package', None) # unload target module to clear the module cache + app.build() + + assert (app.srcdir / 'generated' / 'package.rst').exists() + assert (app.srcdir / 'generated' / 'package.module.rst').exists() + assert (app.srcdir / 'generated' / 'package.package.rst').exists() is False + assert (app.srcdir / 'generated' / 'package.package.module.rst').exists() is False + + @pytest.mark.sphinx('dummy', testroot='ext-autosummary-filename-map') def test_autosummary_filename_map(app, status, warning): app.build() diff --git a/tests/test_ext_coverage.py b/tests/test_ext_coverage.py index 033a1c1b1..6172c502d 100644 --- a/tests/test_ext_coverage.py +++ b/tests/test_ext_coverage.py @@ -4,7 +4,7 @@ Test the coverage builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_ext_doctest.py b/tests/test_ext_doctest.py index ebf7e19d3..729067b4d 100644 --- a/tests/test_ext_doctest.py +++ b/tests/test_ext_doctest.py @@ -4,14 +4,14 @@ Test the doctest extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os from collections import Counter -from docutils import nodes import pytest +from docutils import nodes from packaging.specifiers import InvalidSpecifier from packaging.version import InvalidVersion diff --git a/tests/test_ext_duration.py b/tests/test_ext_duration.py index 51b3e63aa..681530cef 100644 --- a/tests/test_ext_duration.py +++ b/tests/test_ext_duration.py @@ -4,11 +4,12 @@ Test sphinx.ext.duration extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re + import pytest diff --git a/tests/test_ext_githubpages.py b/tests/test_ext_githubpages.py index 41dbe4f60..5c13a8f97 100644 --- a/tests/test_ext_githubpages.py +++ b/tests/test_ext_githubpages.py @@ -4,7 +4,7 @@ Test sphinx.ext.githubpages extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_ext_graphviz.py b/tests/test_ext_graphviz.py index a8de950ce..a19261e96 100644 --- a/tests/test_ext_graphviz.py +++ b/tests/test_ext_graphviz.py @@ -4,7 +4,7 @@ Test sphinx.ext.graphviz extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_ext_ifconfig.py b/tests/test_ext_ifconfig.py index 232ddf0d8..eea2386c3 100644 --- a/tests/test_ext_ifconfig.py +++ b/tests/test_ext_ifconfig.py @@ -4,7 +4,7 @@ Test sphinx.ext.ifconfig extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_ext_imgconverter.py b/tests/test_ext_imgconverter.py index 075446b0a..5f6f236cf 100644 --- a/tests/test_ext_imgconverter.py +++ b/tests/test_ext_imgconverter.py @@ -4,7 +4,7 @@ Test sphinx.ext.imgconverter extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py index 2ecb3f4e4..eada88e91 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_ext_inheritance_diagram.py @@ -4,19 +4,18 @@ Test sphinx.ext.inheritance_diagram extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -import re import os +import re import sys import pytest -from sphinx.ext.inheritance_diagram import ( - InheritanceDiagram, InheritanceException, import_classes -) +from sphinx.ext.inheritance_diagram import (InheritanceDiagram, InheritanceException, + import_classes) @pytest.mark.sphinx(buildername="html", testroot="inheritance") diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 53faa7a37..a87677525 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -4,27 +4,27 @@ Test the intersphinx extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import http.server import os import unittest -from io import BytesIO from unittest import mock import pytest -import requests from docutils import nodes -from test_util_inventory import inventory_v2, inventory_v2_not_having_version from sphinx import addnodes -from sphinx.ext.intersphinx import ( - load_mappings, missing_reference, normalize_intersphinx_mapping, _strip_basic_auth, - _get_safe_url, fetch_inventory, INVENTORY_FILENAME, inspect_main -) +from sphinx.ext.intersphinx import (INVENTORY_FILENAME, _get_safe_url, _strip_basic_auth, + fetch_inventory, inspect_main, load_mappings, + missing_reference, normalize_intersphinx_mapping) from sphinx.ext.intersphinx import setup as intersphinx_setup +from .test_util_inventory import inventory_v2, inventory_v2_not_having_version +from .utils import http_server + def fake_node(domain, type, target, content, **attrs): contnode = nodes.emphasis(content, content) @@ -46,7 +46,7 @@ def reference_check(app, *args, **kwds): @mock.patch('sphinx.ext.intersphinx._read_from_url') def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, warning): intersphinx_setup(app) - _read_from_url().readline.return_value = '# Sphinx inventory version 2'.encode() + _read_from_url().readline.return_value = b'# Sphinx inventory version 2' # same uri and inv, not redirected _read_from_url().url = 'http://hostname/' + INVENTORY_FILENAME @@ -250,10 +250,10 @@ def test_missing_reference_cppdomain(tempdir, app, status, warning): 'Bar' in html) assert ('foons' in html) + ' title="(in foo v2.0)">foons' in html) assert ('bartype' in html) + ' title="(in foo v2.0)">bartype' in html) def test_missing_reference_jsdomain(tempdir, app, status, warning): @@ -433,24 +433,22 @@ def test_inspect_main_file(capsys, tempdir): assert stderr == "" -@mock.patch('requests.get') -def test_inspect_main_url(fake_get, capsys): +def test_inspect_main_url(capsys): """inspect_main interface, with url argument""" - raw = BytesIO(inventory_v2) - real_read = raw.read + class InventoryHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200, "OK") + self.end_headers() + self.wfile.write(inventory_v2) - def fake_read(*args, **kwargs): - return real_read() + def log_message(*args, **kwargs): + # Silenced. + pass - raw.read = fake_read - url = 'http://hostname/' + INVENTORY_FILENAME - resp = requests.Response() - resp.status_code = 200 - resp.url = url - resp.raw = raw - fake_get.return_value = resp + url = 'http://localhost:7777/' + INVENTORY_FILENAME - inspect_main([url]) + with http_server(InventoryHandler): + inspect_main([url]) stdout, stderr = capsys.readouterr() assert stdout.startswith("c:function\n") diff --git a/tests/test_ext_math.py b/tests/test_ext_math.py index 4df7d47c7..10c8c4866 100644 --- a/tests/test_ext_math.py +++ b/tests/test_ext_math.py @@ -4,7 +4,7 @@ Test math extensions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,6 +15,7 @@ import warnings import pytest from docutils import nodes +from sphinx.ext.mathjax import MATHJAX_URL from sphinx.testing.util import assert_node @@ -224,6 +225,18 @@ def test_mathjax_config(app, status, warning): '' in content) +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_only_if_document_having_math(app, status, warning): + app.builder.build_all() + + content = (app.outdir / 'index.html').read_text() + assert MATHJAX_URL in content + + content = (app.outdir / 'nomath.html').read_text() + assert MATHJAX_URL not in content + + @pytest.mark.sphinx('html', testroot='basic', confoverrides={'extensions': ['sphinx.ext.mathjax']}) def test_mathjax_is_not_installed_if_no_equations(app, status, warning): diff --git a/tests/test_ext_napoleon.py b/tests/test_ext_napoleon.py index 9f356ce9e..d6334b9eb 100644 --- a/tests/test_ext_napoleon.py +++ b/tests/test_ext_napoleon.py @@ -5,7 +5,7 @@ Tests for :mod:`sphinx.ext.napoleon.__init__` module. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,8 +14,8 @@ from collections import namedtuple from unittest import TestCase, mock from sphinx.application import Sphinx +from sphinx.ext.napoleon import Config, _process_docstring, _skip_member, setup from sphinx.testing.util import simple_decorator -from sphinx.ext.napoleon import _process_docstring, _skip_member, Config, setup def _private_doc(): diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index bbc075edd..362e6fb8e 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -5,11 +5,12 @@ Tests for :mod:`sphinx.ext.napoleon.docstring` module. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re +import sys from collections import namedtuple from contextlib import contextmanager from inspect import cleandoc @@ -19,13 +20,13 @@ from unittest import TestCase, mock import pytest from sphinx.ext.napoleon import Config -from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring -from sphinx.ext.napoleon.docstring import ( - _tokenize_type_spec, - _recombine_set_tokens, - _convert_numpy_type_spec, - _token_type -) +from sphinx.ext.napoleon.docstring import (GoogleDocstring, NumpyDocstring, + _convert_numpy_type_spec, _recombine_set_tokens, + _token_type, _tokenize_type_spec) + +if sys.version_info >= (3, 6): + from .ext_napoleon_pep526_data_google import PEP526GoogleClass + from .ext_napoleon_pep526_data_numpy import PEP526NumpyClass class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))): @@ -302,6 +303,34 @@ class GoogleDocstringTest(BaseDocstringTest): """ Single line summary + Receive: + arg1 (list(int)): Description + arg2 (list[int]): Description + """, + """ + Single line summary + + :Receives: * **arg1** (*list(int)*) -- Description + * **arg2** (*list[int]*) -- Description + """ + ), ( + """ + Single line summary + + Receives: + arg1 (list(int)): Description + arg2 (list[int]): Description + """, + """ + Single line summary + + :Receives: * **arg1** (*list(int)*) -- Description + * **arg2** (*list[int]*) -- Description + """ + ), ( + """ + Single line summary + Yield: str:Extended description of yielded value @@ -1043,10 +1072,27 @@ You should listen to me! Sooper Warning: Stop hitting yourself! """, """:Warns: **Stop hitting yourself!** +"""), + ("""\ +Params Style: + arg1 (int): Description of arg1 + arg2 (str): Description of arg2 + +""", """\ +:Params Style: * **arg1** (*int*) -- Description of arg1 + * **arg2** (*str*) -- Description of arg2 +"""), + ("""\ +Returns Style: + description of custom section + +""", """:Returns Style: description of custom section """)) testConfig = Config(napoleon_custom_sections=['Really Important Details', - ('Sooper Warning', 'warns')]) + ('Sooper Warning', 'warns'), + ('Params Style', 'params_style'), + ('Returns Style', 'returns_style')]) for docstring, expected in docstrings: actual = str(GoogleDocstring(docstring, testConfig)) @@ -1074,7 +1120,7 @@ Methods: description -""" +""" # NOQA config = Config() actual = str(GoogleDocstring(docstring, config=config, app=None, what='module', options={'noindex': True})) @@ -1093,6 +1139,55 @@ Do as you please :keyword gotham_is_yours: shall interfere. :kwtype gotham_is_yours: None +""" + self.assertEqual(expected, actual) + + def test_pep526_annotations(self): + if sys.version_info >= (3, 6): + # Test class attributes annotations + config = Config( + napoleon_attr_annotations=True + ) + actual = str(GoogleDocstring(cleandoc(PEP526GoogleClass.__doc__), config, app=None, what="class", + obj=PEP526GoogleClass)) + expected = """\ +Sample class with PEP 526 annotations and google docstring + +.. attribute:: attr1 + + Attr1 description. + + :type: int + +.. attribute:: attr2 + + Attr2 description. + + :type: str +""" + self.assertEqual(expected, actual) + + def test_preprocess_types(self): + docstring = """\ +Do as you please + +Yield: + str:Extended +""" + actual = str(GoogleDocstring(docstring)) + expected = """\ +Do as you please + +:Yields: *str* -- Extended +""" + self.assertEqual(expected, actual) + + config = Config(napoleon_preprocess_types=True) + actual = str(GoogleDocstring(docstring, config)) + expected = """\ +Do as you please + +:Yields: :class:`str` -- Extended """ self.assertEqual(expected, actual) @@ -1177,7 +1272,7 @@ class NumpyDocstringTest(BaseDocstringTest): """ Single line summary - :returns: *str* -- Extended + :returns: :class:`str` -- Extended description of return value """ ), ( @@ -1193,7 +1288,7 @@ class NumpyDocstringTest(BaseDocstringTest): """ Single line summary - :returns: *str* -- Extended + :returns: :class:`str` -- Extended description of return value """ ), ( @@ -1237,6 +1332,48 @@ class NumpyDocstringTest(BaseDocstringTest): """ Single line summary + Receive + ------- + arg1:str + Extended + description of arg1 + arg2 : int + Extended + description of arg2 + """, + """ + Single line summary + + :Receives: * **arg1** (:class:`str`) -- Extended + description of arg1 + * **arg2** (:class:`int`) -- Extended + description of arg2 + """ + ), ( + """ + Single line summary + + Receives + -------- + arg1:str + Extended + description of arg1 + arg2 : int + Extended + description of arg2 + """, + """ + Single line summary + + :Receives: * **arg1** (:class:`str`) -- Extended + description of arg1 + * **arg2** (:class:`int`) -- Extended + description of arg2 + """ + ), ( + """ + Single line summary + Yield ----- str @@ -1246,7 +1383,7 @@ class NumpyDocstringTest(BaseDocstringTest): """ Single line summary - :Yields: *str* -- Extended + :Yields: :class:`str` -- Extended description of yielded value """ ), ( @@ -1262,7 +1399,7 @@ class NumpyDocstringTest(BaseDocstringTest): """ Single line summary - :Yields: *str* -- Extended + :Yields: :class:`str` -- Extended description of yielded value """ )] @@ -1455,9 +1592,38 @@ numpy.multivariate_normal(mean, cov, shape=None, spam=None) .. seealso:: - :meth:`some`, :meth:`other`, :meth:`funcs` + :obj:`some`, :obj:`other`, :obj:`funcs` \n\ - :meth:`otherfunc` + :obj:`otherfunc` + relationship +""" + self.assertEqual(expected, actual) + + docstring = """\ +numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +See Also +-------- +some, other, :func:`funcs` +otherfunc : relationship + +""" + translations = { + "other": "MyClass.other", + "otherfunc": ":func:`~my_package.otherfunc`", + } + config = Config(napoleon_type_aliases=translations) + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "method")) + + expected = """\ +numpy.multivariate_normal(mean, cov, shape=None, spam=None) + +.. seealso:: + + :obj:`some`, :obj:`MyClass.other`, :func:`funcs` + \n\ + :func:`~my_package.otherfunc` relationship """ self.assertEqual(expected, actual) @@ -1526,6 +1692,52 @@ arg_ : type self.assertEqual(expected, actual) + def test_return_types(self): + docstring = dedent(""" + Returns + ------- + DataFrame + a dataframe + """) + expected = dedent(""" + :returns: a dataframe + :rtype: :class:`~pandas.DataFrame` + """) + translations = { + "DataFrame": "~pandas.DataFrame", + } + config = Config( + napoleon_use_param=True, + napoleon_use_rtype=True, + napoleon_preprocess_types=True, + napoleon_type_aliases=translations, + ) + actual = str(NumpyDocstring(docstring, config)) + self.assertEqual(expected, actual) + + def test_yield_types(self): + docstring = dedent(""" + Example Function + + Yields + ------ + scalar or array-like + The result of the computation + """) + expected = dedent(""" + Example Function + + :Yields: :term:`scalar` or :class:`array-like ` -- The result of the computation + """) + translations = { + "scalar": ":term:`scalar`", + "array-like": ":class:`array-like `", + } + config = Config(napoleon_type_aliases=translations, napoleon_preprocess_types=True) + app = mock.Mock() + actual = str(NumpyDocstring(docstring, config, app, "method")) + self.assertEqual(expected, actual) + def test_raises_types(self): docstrings = [(""" Example Function @@ -1690,6 +1902,34 @@ Example Function (""" Example Function +Raises +------ +CustomError + If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises package.CustomError: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + +Raises +------ +AnotherError + If the dimensions couldn't be parsed. + +""", """ +Example Function + +:raises ~package.AnotherError: If the dimensions couldn't be parsed. +"""), + ################################ + (""" +Example Function + Raises ------ :class:`exc.InvalidDimensionsError` @@ -1702,7 +1942,11 @@ Example Function :raises exc.InvalidArgumentsError: """)] for docstring, expected in docstrings: - config = Config() + translations = { + "CustomError": "package.CustomError", + "AnotherError": ":py:exc:`~package.AnotherError`", + } + config = Config(napoleon_type_aliases=translations, napoleon_preprocess_types=True) app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "method")) self.assertEqual(expected, actual) @@ -2119,7 +2363,7 @@ definition_after_normal_text : int ["{", "'F'", ", ", "'C'", ", ", "'N or C'", "}", ", ", "default", " ", "'F'"], ["str", ", ", "default", ": ", "'F or C'"], ["int", ", ", "default", ": ", "None"], - ["int", ", " , "default", " ", "None"], + ["int", ", ", "default", " ", "None"], ["int", ", ", "default", " ", ":obj:`None`"], ['"ma{icious"'], [r"'with \'quotes\''"], @@ -2299,3 +2543,29 @@ class TestNumpyDocstring: actual = numpy_docstring._escape_args_and_kwargs(name) assert actual == expected + + def test_pep526_annotations(self): + if sys.version_info >= (3, 6): + # test class attributes annotations + config = Config( + napoleon_attr_annotations=True + ) + actual = str(NumpyDocstring(cleandoc(PEP526NumpyClass.__doc__), config, app=None, what="class", + obj=PEP526NumpyClass)) + expected = """\ +Sample class with PEP 526 annotations and numpy docstring + +.. attribute:: attr1 + + Attr1 description + + :type: int + +.. attribute:: attr2 + + Attr2 description + + :type: str +""" + print(actual) + assert expected == actual diff --git a/tests/test_ext_napoleon_iterators.py b/tests/test_ext_napoleon_iterators.py index de28031cb..45b558794 100644 --- a/tests/test_ext_napoleon_iterators.py +++ b/tests/test_ext_napoleon_iterators.py @@ -5,13 +5,13 @@ Tests for :mod:`sphinx.ext.napoleon.iterators` module. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from unittest import TestCase -from sphinx.ext.napoleon.iterators import peek_iter, modify_iter +from sphinx.ext.napoleon.iterators import modify_iter, peek_iter class BaseIteratorsTest(TestCase): diff --git a/tests/test_ext_todo.py b/tests/test_ext_todo.py index 7b4fdeabe..b6fb2549c 100644 --- a/tests/test_ext_todo.py +++ b/tests/test_ext_todo.py @@ -4,7 +4,7 @@ Test sphinx.ext.todo extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_ext_viewcode.py b/tests/test_ext_viewcode.py index 3d9ea27d7..d75fb7196 100644 --- a/tests/test_ext_viewcode.py +++ b/tests/test_ext_viewcode.py @@ -4,7 +4,7 @@ Test sphinx.ext.viewcode extension. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -49,6 +49,27 @@ def test_viewcode(app, status, warning): ' """
    \n') in result +@pytest.mark.sphinx('epub', testroot='ext-viewcode') +def test_viewcode_epub_default(app, status, warning): + app.builder.build_all() + + assert not (app.outdir / '_modules/spam/mod1.xhtml').exists() + + result = (app.outdir / 'index.xhtml').read_text() + assert result.count('href="_modules/spam/mod1.xhtml#func1"') == 0 + + +@pytest.mark.sphinx('epub', testroot='ext-viewcode', + confoverrides={'viewcode_enable_epub': True}) +def test_viewcode_epub_enabled(app, status, warning): + app.builder.build_all() + + assert (app.outdir / '_modules/spam/mod1.xhtml').exists() + + result = (app.outdir / 'index.xhtml').read_text() + assert result.count('href="_modules/spam/mod1.xhtml#func1"') == 2 + + @pytest.mark.sphinx(testroot='ext-viewcode', tags=['test_linkcode']) def test_linkcode(app, status, warning): app.builder.build(['objects']) diff --git a/tests/test_highlighting.py b/tests/test_highlighting.py index c2b470a6b..a51e25688 100644 --- a/tests/test_highlighting.py +++ b/tests/test_highlighting.py @@ -4,7 +4,7 @@ Test the Pygments highlighting bridge. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,7 +12,7 @@ from unittest import mock from pygments.formatters.html import HtmlFormatter from pygments.lexer import RegexLexer -from pygments.token import Text, Name +from pygments.token import Name, Text from sphinx.highlighting import PygmentsBridge diff --git a/tests/test_intl.py b/tests/test_intl.py index c0b87d5ce..00f599595 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -5,7 +5,7 @@ Test message patching for internationalization purposes. Runs the text builder in the test root. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,16 +13,13 @@ import os import re import pytest -from babel.messages import pofile, mofile +from babel.messages import mofile, pofile from babel.messages.catalog import Catalog from docutils import nodes from sphinx import locale -from sphinx.testing.util import ( - path, etree_parse, strip_escseq, - assert_re_search, assert_not_re_search, assert_startswith, assert_node -) - +from sphinx.testing.util import (assert_node, assert_not_re_search, assert_re_search, + assert_startswith, etree_parse, path, strip_escseq) sphinx_intl = pytest.mark.sphinx( testroot='intl', @@ -92,15 +89,6 @@ def assert_count(expected_expr, result, count): assert len(re.findall(*find_pair)) == count, find_pair -@sphinx_intl -@pytest.mark.sphinx('text') -@pytest.mark.test_params(shared_result='test_intl_basic') -def test_text_toctree(app): - app.build() - result = (app.outdir / 'index.txt').read_text() - assert_startswith(result, "CONTENTS\n********\n\nTABLE OF CONTENTS\n") - - @sphinx_intl @pytest.mark.sphinx('text') @pytest.mark.test_params(shared_result='test_intl_basic') @@ -439,11 +427,16 @@ def test_text_admonitions(app): @pytest.mark.test_params(shared_result='test_intl_gettext') def test_gettext_toctree(app): app.build() - # --- toctree + # --- toctree (index.rst) expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'index.po') actual = read_po(app.outdir / 'index.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] + # --- toctree (toctree.rst) + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') + actual = read_po(app.outdir / 'toctree.pot') + for expect_msg in [m for m in expect if m.id]: + assert expect_msg.id in [m.id for m in actual if m.id] @sphinx_intl @@ -470,24 +463,17 @@ def test_text_table(app): assert expect_msg.string in result -@sphinx_intl -@pytest.mark.sphinx('gettext') -@pytest.mark.test_params(shared_result='test_intl_gettext') -def test_gettext_toctree(app): - app.build() - # --- toctree - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') - actual = read_po(app.outdir / 'toctree.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] - - @sphinx_intl @pytest.mark.sphinx('text') @pytest.mark.test_params(shared_result='test_intl_basic') def test_text_toctree(app): app.build() - # --- toctree + # --- toctree (index.rst) + # Note: index.rst contains contents that is not shown in text. + result = (app.outdir / 'index.txt').read_text() + assert 'CONTENTS' in result + assert 'TABLE OF CONTENTS' in result + # --- toctree (toctree.rst) result = (app.outdir / 'toctree.txt').read_text() expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') for expect_msg in [m for m in expect if m.id]: diff --git a/tests/test_locale.py b/tests/test_locale.py index a744a5ff7..5d3dbf17f 100644 --- a/tests/test_locale.py +++ b/tests/test_locale.py @@ -4,7 +4,7 @@ Test locale. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_markup.py b/tests/test_markup.py index 5fb835605..f8fff1c2d 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -4,28 +4,26 @@ Test various Sphinx-specific markup extensions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re import pytest -from docutils import frontend, utils, nodes +from docutils import frontend, nodes, utils from docutils.parsers.rst import Parser as RstParser from sphinx import addnodes from sphinx.builders.html.transforms import KeyboardTransform from sphinx.builders.latex import LaTeXBuilder -from sphinx.builders.latex.theming import ThemeFactory from sphinx.roles import XRefRole from sphinx.testing.util import Struct, assert_node from sphinx.transforms import SphinxSmartQuotes -from sphinx.util import docutils -from sphinx.util import texescape +from sphinx.util import docutils, texescape from sphinx.util.docutils import sphinx_domains -from sphinx.writers.html import HTMLWriter, HTMLTranslator -from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator +from sphinx.writers.html import HTMLTranslator, HTMLWriter +from sphinx.writers.latex import LaTeXTranslator, LaTeXWriter @pytest.fixture @@ -160,7 +158,8 @@ def get_verifier(verify, verify_re): ':pep:`8`', ('

    PEP 8

    '), - ('\\index{Python Enhancement Proposals@\\spxentry{Python Enhancement Proposals}' + ('\\sphinxAtStartPar\n' + '\\index{Python Enhancement Proposals@\\spxentry{Python Enhancement Proposals}' '!PEP 8@\\spxentry{PEP 8}}\\sphinxhref{http://www.python.org/dev/peps/pep-0008}' '{\\sphinxstylestrong{PEP 8}}') ), @@ -171,7 +170,8 @@ def get_verifier(verify, verify_re): ('

    ' 'PEP 8#id1

    '), - ('\\index{Python Enhancement Proposals@\\spxentry{Python Enhancement Proposals}' + ('\\sphinxAtStartPar\n' + '\\index{Python Enhancement Proposals@\\spxentry{Python Enhancement Proposals}' '!PEP 8\\#id1@\\spxentry{PEP 8\\#id1}}\\sphinxhref' '{http://www.python.org/dev/peps/pep-0008\\#id1}' '{\\sphinxstylestrong{PEP 8\\#id1}}') @@ -182,7 +182,8 @@ def get_verifier(verify, verify_re): ':rfc:`2324`', ('

    RFC 2324

    '), - ('\\index{RFC@\\spxentry{RFC}!RFC 2324@\\spxentry{RFC 2324}}' + ('\\sphinxAtStartPar\n' + '\\index{RFC@\\spxentry{RFC}!RFC 2324@\\spxentry{RFC 2324}}' '\\sphinxhref{http://tools.ietf.org/html/rfc2324.html}' '{\\sphinxstylestrong{RFC 2324}}') ), @@ -193,7 +194,8 @@ def get_verifier(verify, verify_re): ('

    ' 'RFC 2324#id1

    '), - ('\\index{RFC@\\spxentry{RFC}!RFC 2324\\#id1@\\spxentry{RFC 2324\\#id1}}' + ('\\sphinxAtStartPar\n' + '\\index{RFC@\\spxentry{RFC}!RFC 2324\\#id1@\\spxentry{RFC 2324\\#id1}}' '\\sphinxhref{http://tools.ietf.org/html/rfc2324.html\\#id1}' '{\\sphinxstylestrong{RFC 2324\\#id1}}') ), @@ -203,14 +205,14 @@ def get_verifier(verify, verify_re): '``code sample``', ('

    ' 'code   sample

    '), - r'\\sphinxcode{\\sphinxupquote{code sample}}', + r'\\sphinxAtStartPar\n\\sphinxcode{\\sphinxupquote{code sample}}', ), ( # interpolation of arrows in menuselection 'verify', ':menuselection:`a --> b`', ('

    a \N{TRIANGULAR BULLET} b

    '), - '\\sphinxmenuselection{a \\(\\rightarrow\\) b}', + '\\sphinxAtStartPar\n\\sphinxmenuselection{a \\(\\rightarrow\\) b}', ), ( # interpolation of ampersands in menuselection @@ -218,7 +220,9 @@ def get_verifier(verify, verify_re): ':menuselection:`&Foo -&&- &Bar`', ('

    Foo ' '-&- Bar

    '), - r'\sphinxmenuselection{\sphinxaccelerator{F}oo \sphinxhyphen{}\&\sphinxhyphen{} \sphinxaccelerator{B}ar}', + ('\\sphinxAtStartPar\n' + r'\sphinxmenuselection{\sphinxaccelerator{F}oo \sphinxhyphen{}' + r'\&\sphinxhyphen{} \sphinxaccelerator{B}ar}'), ), ( # interpolation of ampersands in guilabel @@ -226,38 +230,51 @@ def get_verifier(verify, verify_re): ':guilabel:`&Foo -&&- &Bar`', ('

    Foo ' '-&- Bar

    '), - r'\sphinxguilabel{\sphinxaccelerator{F}oo \sphinxhyphen{}\&\sphinxhyphen{} \sphinxaccelerator{B}ar}', + ('\\sphinxAtStartPar\n' + r'\sphinxguilabel{\sphinxaccelerator{F}oo \sphinxhyphen{}\&\sphinxhyphen{} \sphinxaccelerator{B}ar}'), ), ( # no ampersands in guilabel 'verify', ':guilabel:`Foo`', '

    Foo

    ', - r'\sphinxguilabel{Foo}', + '\\sphinxAtStartPar\n\\sphinxguilabel{Foo}', ), ( # kbd role 'verify', ':kbd:`space`', '

    space

    ', - '\\sphinxkeyboard{\\sphinxupquote{space}}', + '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{space}}', ), ( # kbd role 'verify', ':kbd:`Control+X`', - ('

    ' + ('

    ' 'Control' '+' 'X' '

    '), - '\\sphinxkeyboard{\\sphinxupquote{Control+X}}', + '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{Control+X}}', + ), + ( + # kbd role + 'verify', + ':kbd:`Alt+^`', + ('

    ' + 'Alt' + '+' + '^' + '

    '), + ('\\sphinxAtStartPar\n' + '\\sphinxkeyboard{\\sphinxupquote{Alt+\\textasciicircum{}}}'), ), ( # kbd role 'verify', ':kbd:`M-x M-s`', - ('

    ' + ('

    ' 'M' '-' 'x' @@ -266,7 +283,24 @@ def get_verifier(verify, verify_re): '-' 's' '

    '), - '\\sphinxkeyboard{\\sphinxupquote{M\\sphinxhyphen{}x M\\sphinxhyphen{}s}}', + ('\\sphinxAtStartPar\n' + '\\sphinxkeyboard{\\sphinxupquote{M\\sphinxhyphen{}x M\\sphinxhyphen{}s}}'), + ), + ( + # kbd role + 'verify', + ':kbd:`-`', + '

    -

    ', + ('\\sphinxAtStartPar\n' + '\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}'), + ), + ( + # kbd role + 'verify', + ':kbd:`Caps Lock`', + '

    Caps Lock

    ', + ('\\sphinxAtStartPar\n' + '\\sphinxkeyboard{\\sphinxupquote{Caps Lock}}'), ), ( # non-interpolation of dashes in option role @@ -274,14 +308,15 @@ def get_verifier(verify, verify_re): ':option:`--with-option`', ('

    ' '--with-option

    $'), - r'\\sphinxcode{\\sphinxupquote{\\sphinxhyphen{}\\sphinxhyphen{}with\\sphinxhyphen{}option}}$', + (r'\\sphinxAtStartPar\n' + r'\\sphinxcode{\\sphinxupquote{\\sphinxhyphen{}\\sphinxhyphen{}with\\sphinxhyphen{}option}}$'), ), ( # verify smarty-pants quotes 'verify', '"John"', '

    “John”

    ', - "“John”", + "\\sphinxAtStartPar\n“John”", ), ( # ... but not in literal text @@ -289,21 +324,21 @@ def get_verifier(verify, verify_re): '``"John"``', ('

    ' '"John"

    '), - '\\sphinxcode{\\sphinxupquote{"John"}}', + '\\sphinxAtStartPar\n\\sphinxcode{\\sphinxupquote{"John"}}', ), ( # verify classes for inline roles 'verify', ':manpage:`mp(1)`', '

    mp(1)

    ', - '\\sphinxstyleliteralemphasis{\\sphinxupquote{mp(1)}}', + '\\sphinxAtStartPar\n\\sphinxstyleliteralemphasis{\\sphinxupquote{mp(1)}}', ), ( # correct escaping in normal mode 'verify', 'Γ\\\\∞$', None, - 'Γ\\textbackslash{}\\(\\infty\\)\\$', + '\\sphinxAtStartPar\nΓ\\textbackslash{}\\(\\infty\\)\\$', ), ( # in verbatim code fragments @@ -319,7 +354,7 @@ def get_verifier(verify, verify_re): 'verify_re', '`test `_', None, - r'\\sphinxhref{https://www.google.com/~me/}{test}.*', + r'\\sphinxAtStartPar\n\\sphinxhref{https://www.google.com/~me/}{test}.*', ), ( # description list: simple @@ -340,8 +375,12 @@ def get_verifier(verify, verify_re): # glossary (description list): multiple terms 'verify', '.. glossary::\n\n term1\n term2\n description', - ('
    \n
    term1
    ' - '
    term2
    description
    \n
    '), + ('
    \n' + '
    term1
    ' + '
    term2
    ' + '
    description
    \n
    '), None, ), ]) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index e345488e6..465386430 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -4,7 +4,7 @@ Test our handling of metadata in files with bibliographic metadata. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_parser.py b/tests/test_parser.py index f76e004d6..df22750e3 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -4,7 +4,7 @@ Tests parsers module. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_project.py b/tests/test_project.py index 50b06f7b8..03fa1d49f 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -4,7 +4,7 @@ Tests project module. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,7 +13,6 @@ from collections import OrderedDict import pytest from sphinx.project import Project -from sphinx.testing.comparer import PathComparer def test_project_discover(rootdir): diff --git a/tests/test_pycode.py b/tests/test_pycode.py index 20f01f17d..bbcc42a52 100644 --- a/tests/test_pycode.py +++ b/tests/test_pycode.py @@ -4,20 +4,22 @@ Test pycode. - :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import sys + import pytest import sphinx -from sphinx.pycode import ModuleAnalyzer from sphinx.errors import PycodeError +from sphinx.pycode import ModuleAnalyzer SPHINX_MODULE_PATH = os.path.splitext(sphinx.__file__)[0] + '.py' + def test_ModuleAnalyzer_get_module_source(): assert ModuleAnalyzer.get_module_source('sphinx') == (sphinx.__file__, sphinx.__loader__.get_source('sphinx')) diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py index bbff64dd0..e80062351 100644 --- a/tests/test_pycode_ast.py +++ b/tests/test_pycode_ast.py @@ -4,7 +4,7 @@ Test pycode.ast - :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_pycode_parser.py b/tests/test_pycode_parser.py index 71847f04f..9169315fc 100644 --- a/tests/test_pycode_parser.py +++ b/tests/test_pycode_parser.py @@ -4,7 +4,7 @@ Test pycode.parser. - :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index bdd7073d1..11086f5f6 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -4,7 +4,7 @@ Test the sphinx.quickstart module. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,10 +15,9 @@ import pytest from sphinx import application from sphinx.cmd import quickstart as qs -from sphinx.util.console import nocolor, coloron +from sphinx.util.console import coloron, nocolor from sphinx.util.pycompat import execfile_ - warnfile = StringIO() diff --git a/tests/test_roles.py b/tests/test_roles.py index d1ed7f439..553a50853 100644 --- a/tests/test_roles.py +++ b/tests/test_roles.py @@ -4,7 +4,7 @@ Test sphinx.roles - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_search.py b/tests/test_search.py index a4cefbc67..1ceb6d55b 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -4,12 +4,12 @@ Test the search index builder. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from io import BytesIO from collections import namedtuple +from io import BytesIO import pytest from docutils import frontend, utils diff --git a/tests/test_setup_command.py b/tests/test_setup_command.py index 14a687ada..52dad14fd 100644 --- a/tests/test_setup_command.py +++ b/tests/test_setup_command.py @@ -4,7 +4,7 @@ Test setup_command for distutils. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_smartquotes.py b/tests/test_smartquotes.py index 6276e6a74..0cc0681dc 100644 --- a/tests/test_smartquotes.py +++ b/tests/test_smartquotes.py @@ -4,7 +4,7 @@ Test smart quotes. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_templating.py b/tests/test_templating.py index f2c1d563b..3a7c58216 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -4,7 +4,7 @@ Test templating. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_theming.py b/tests/test_theming.py index 93671eab8..0dded0725 100644 --- a/tests/test_theming.py +++ b/tests/test_theming.py @@ -4,15 +4,15 @@ Test the Theme class. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import alabaster - import pytest + from sphinx.theming import ThemeError @@ -81,7 +81,7 @@ def test_js_source(app, status, warning): jquery_src = (app.outdir / '_static' / 'jquery-{v}.js'.format(v=v)).read_text() assert 'jQuery JavaScript Library v{v}'.format(v=v) in jquery_src, msg - v = '1.3.1' + v = '1.12.0' msg = 'underscore.js version does not match to {v}'.format(v=v) underscore_min = (app.outdir / '_static' / 'underscore.js').read_text() assert 'Underscore.js {v}'.format(v=v) in underscore_min, msg diff --git a/tests/test_toctree.py b/tests/test_toctree.py index 38886769a..7a9f7a578 100644 --- a/tests/test_toctree.py +++ b/tests/test_toctree.py @@ -4,7 +4,7 @@ Test the HTML builder and check output against XPath. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re diff --git a/tests/test_transforms_post_transforms_code.py b/tests/test_transforms_post_transforms_code.py index 2e5da05ae..449cba78a 100644 --- a/tests/test_transforms_post_transforms_code.py +++ b/tests/test_transforms_post_transforms_code.py @@ -2,7 +2,7 @@ test_transforms_post_transforms_code ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_util.py b/tests/test_util.py index 434d96d3a..9b25aac58 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -4,7 +4,7 @@ Tests util functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,14 +14,11 @@ from unittest.mock import patch import pytest -import sphinx -from sphinx.errors import ExtensionError, PycodeError +from sphinx.errors import ExtensionError from sphinx.testing.util import strip_escseq -from sphinx.util import ( - SkipProgressMessage, display_chunk, encode_uri, ensuredir, - import_object, parselinenos, progress_message, status_iterator, xmlname_checker -) -from sphinx.util import logging +from sphinx.util import (SkipProgressMessage, display_chunk, encode_uri, ensuredir, + import_object, logging, parselinenos, progress_message, + status_iterator, xmlname_checker) def test_encode_uri(): diff --git a/tests/test_util_docstrings.py b/tests/test_util_docstrings.py index a57e3f9b1..543feca2a 100644 --- a/tests/test_util_docstrings.py +++ b/tests/test_util_docstrings.py @@ -4,13 +4,11 @@ Test sphinx.util.docstrings. - :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from sphinx.util.docstrings import ( - extract_metadata, prepare_docstring, prepare_commentdoc -) +from sphinx.util.docstrings import extract_metadata, prepare_commentdoc, prepare_docstring def test_extract_metadata(): diff --git a/tests/test_util_docutils.py b/tests/test_util_docutils.py index a22cf277a..9c4e373cf 100644 --- a/tests/test_util_docutils.py +++ b/tests/test_util_docutils.py @@ -4,7 +4,7 @@ Tests util.utils functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -12,9 +12,8 @@ import os from docutils import nodes -from sphinx.util.docutils import ( - SphinxFileOutput, SphinxTranslator, docutils_namespace, new_document, register_node -) +from sphinx.util.docutils import (SphinxFileOutput, SphinxTranslator, docutils_namespace, + new_document, register_node) def test_register_node(): diff --git a/tests/test_util_fileutil.py b/tests/test_util_fileutil.py index e9c80d5b6..fd2f186c2 100644 --- a/tests/test_util_fileutil.py +++ b/tests/test_util_fileutil.py @@ -4,7 +4,7 @@ Tests sphinx.util.fileutil functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_util_i18n.py b/tests/test_util_i18n.py index 7e3a7e2c9..180350e86 100644 --- a/tests/test_util_i18n.py +++ b/tests/test_util_i18n.py @@ -4,7 +4,7 @@ Test i18n util. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -87,6 +87,12 @@ def test_format_date(): assert i18n.format_date(format, date=datet) == 'Feb 7, 2016, 5:11:17 AM' assert i18n.format_date(format, date=date) == 'Feb 7, 2016' + # timezone + format = '%Z' + assert i18n.format_date(format, date=datet) == 'UTC' + format = '%z' + assert i18n.format_date(format, date=datet) == '+0000' + @pytest.mark.xfail(os.name != 'posix', reason="Path separators don't match on windows") def test_get_filename_for_language(app): diff --git a/tests/test_util_images.py b/tests/test_util_images.py index 2a256577f..bf71f4c49 100644 --- a/tests/test_util_images.py +++ b/tests/test_util_images.py @@ -4,15 +4,14 @@ Test images util. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import pytest -from sphinx.util.images import ( - get_image_size, guess_mimetype, get_image_extension, parse_data_uri -) +from sphinx.util.images import (get_image_extension, get_image_size, guess_mimetype, + parse_data_uri) GIF_FILENAME = 'img.gif' PNG_FILENAME = 'img.png' diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index c21eaaa16..863f44921 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -4,11 +4,10 @@ Tests util.inspect functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -import _testcapi import ast import datetime import functools @@ -16,10 +15,11 @@ import sys import types from inspect import Parameter +import _testcapi import pytest from sphinx.util import inspect -from sphinx.util.inspect import stringify_signature, is_builtin_class_method +from sphinx.util.inspect import stringify_signature def test_signature(): @@ -100,7 +100,7 @@ def test_signature_methods(): # wrapped bound method sig = inspect.signature(wrapped_bound_method) - assert stringify_signature(sig) == '(*args, **kwargs)' + assert stringify_signature(sig) == '(arg1, **kwargs)' def test_signature_partialmethod(): @@ -129,8 +129,8 @@ def test_signature_partialmethod(): def test_signature_annotations(): - from typing_test_data import (f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, - f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, f21, Node) + from .typing_test_data import (Node, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, + f13, f14, f15, f16, f17, f18, f19, f20, f21) # Class annotations sig = inspect.signature(f0) @@ -162,7 +162,7 @@ def test_signature_annotations(): # Space around '=' for defaults sig = inspect.signature(f7) - assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None' + assert stringify_signature(sig) == '(x: Optional[int] = None, y: dict = {}) -> None' # Callable types sig = inspect.signature(f8) @@ -177,7 +177,10 @@ def test_signature_annotations(): # Instance annotations sig = inspect.signature(f11) - assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None' + if sys.version_info < (3, 10): + assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None' + else: + assert stringify_signature(sig) == '(x: CustomAnnotation(), y: 123) -> None' # tuple with more than two items sig = inspect.signature(f12) @@ -223,10 +226,10 @@ def test_signature_annotations(): if (3, 5, 0) <= sys.version_info < (3, 5, 3): assert stringify_signature(sig) == '(self) -> List[Node]' else: - assert stringify_signature(sig) == '(self) -> List[typing_test_data.Node]' + assert stringify_signature(sig) == '(self) -> List[tests.typing_test_data.Node]' sig = inspect.signature(Node.__init__) - assert stringify_signature(sig) == '(self, parent: Optional[Node]) -> None' + assert stringify_signature(sig) == '(self, parent: Optional[tests.typing_test_data.Node]) -> None' # show_annotation is False sig = inspect.signature(f7) @@ -234,13 +237,13 @@ def test_signature_annotations(): # show_return_annotation is False sig = inspect.signature(f7) - assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})' + assert stringify_signature(sig, show_return_annotation=False) == '(x: Optional[int] = None, y: dict = {})' @pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') @pytest.mark.sphinx(testroot='ext-autodoc') def test_signature_annotations_py38(app): - from target.pep570 import foo, bar, baz, qux + from target.pep570 import bar, baz, foo, qux # case: separator at head sig = inspect.signature(foo) @@ -490,6 +493,28 @@ def test_dict_customtype(): assert ": 2" in description +def test_getslots(): + class Foo: + pass + + class Bar: + __slots__ = ['attr'] + + class Baz: + __slots__ = {'attr': 'docstring'} + + class Qux: + __slots__ = 'attr' + + assert inspect.getslots(Foo) is None + assert inspect.getslots(Bar) == {'attr': None} + assert inspect.getslots(Baz) == {'attr': 'docstring'} + assert inspect.getslots(Qux) == {'attr': None} + + with pytest.raises(TypeError): + inspect.getslots(Bar()) + + @pytest.mark.sphinx(testroot='ext-autodoc') def test_isclassmethod(app): from target.methods import Base, Inherited @@ -528,8 +553,7 @@ def test_iscoroutinefunction(app): @pytest.mark.sphinx(testroot='ext-autodoc') def test_isfunction(app): - from target.functions import builtin_func, partial_builtin_func - from target.functions import func, partial_func + from target.functions import builtin_func, func, partial_builtin_func, partial_func from target.methods import Base assert inspect.isfunction(func) is True # function @@ -543,8 +567,7 @@ def test_isfunction(app): @pytest.mark.sphinx(testroot='ext-autodoc') def test_isbuiltin(app): - from target.functions import builtin_func, partial_builtin_func - from target.functions import func, partial_func + from target.functions import builtin_func, func, partial_builtin_func, partial_func from target.methods import Base assert inspect.isbuiltin(builtin_func) is True # builtin function diff --git a/tests/test_util_inventory.py b/tests/test_util_inventory.py index 2183313e1..f12ae5011 100644 --- a/tests/test_util_inventory.py +++ b/tests/test_util_inventory.py @@ -4,7 +4,7 @@ Test inventory util functions. - :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,20 +14,20 @@ from io import BytesIO from sphinx.ext.intersphinx import InventoryFile -inventory_v1 = '''\ +inventory_v1 = b'''\ # Sphinx inventory version 1 # Project: foo # Version: 1.0 module mod foo.html module.cls class foo.html -'''.encode() +''' -inventory_v2 = '''\ +inventory_v2 = b'''\ # Sphinx inventory version 2 # Project: foo # Version: 2.0 # The remainder of this file is compressed with zlib. -'''.encode() + zlib.compress('''\ +''' + zlib.compress(b'''\ module1 py:module 0 foo.html#module-module1 Long Module desc module2 py:module 0 foo.html#module-$ - module1.func py:function 1 sub/foo.html#$ - @@ -47,16 +47,16 @@ foo.bar js:class 1 index.html#foo.bar - foo.bar.baz js:method 1 index.html#foo.bar.baz - foo.bar.qux js:data 1 index.html#foo.bar.qux - a term including:colon std:term -1 glossary.html#term-a-term-including-colon - -'''.encode()) +''') -inventory_v2_not_having_version = '''\ +inventory_v2_not_having_version = b'''\ # Sphinx inventory version 2 # Project: foo # Version: # The remainder of this file is compressed with zlib. -'''.encode() + zlib.compress('''\ +''' + zlib.compress(b'''\ module1 py:module 0 foo.html#module-module1 Long Module desc -'''.encode()) +''') def test_read_inventory_v1(): diff --git a/tests/test_util_logging.py b/tests/test_util_logging.py index 36034cd92..aedbe9e64 100644 --- a/tests/test_util_logging.py +++ b/tests/test_util_logging.py @@ -4,7 +4,7 @@ Test logging util. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_util_matching.py b/tests/test_util_matching.py index 0a6bdc7b3..008dc6267 100644 --- a/tests/test_util_matching.py +++ b/tests/test_util_matching.py @@ -4,10 +4,10 @@ Tests sphinx.util.matching functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from sphinx.util.matching import compile_matchers, Matcher +from sphinx.util.matching import Matcher, compile_matchers def test_compile_matchers(): diff --git a/tests/test_util_nodes.py b/tests/test_util_nodes.py index 8fe9ee773..cb2ae70a8 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util_nodes.py @@ -4,22 +4,20 @@ Tests uti.nodes functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from textwrap import dedent from typing import Any import pytest -from docutils import frontend -from docutils import nodes +from docutils import frontend, nodes from docutils.parsers import rst from docutils.utils import new_document from sphinx.transforms import ApplySourceWorkaround -from sphinx.util.nodes import ( - NodeMatcher, extract_messages, clean_astext, make_id, split_explicit_title -) +from sphinx.util.nodes import (NodeMatcher, clean_astext, extract_messages, make_id, + split_explicit_title) def _transform(doctree): diff --git a/tests/test_util_pycompat.py b/tests/test_util_pycompat.py index 67e61bb58..0e395fa7a 100644 --- a/tests/test_util_pycompat.py +++ b/tests/test_util_pycompat.py @@ -4,7 +4,7 @@ Tests sphinx.util.pycompat functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_util_rst.py b/tests/test_util_rst.py index dd78fe5ba..7e14eb3f7 100644 --- a/tests/test_util_rst.py +++ b/tests/test_util_rst.py @@ -4,16 +4,14 @@ Tests sphinx.util.rst functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from docutils.statemachine import StringList from jinja2 import Environment -from sphinx.util.rst import ( - append_epilog, escape, heading, prepend_prolog, textwidth -) +from sphinx.util.rst import append_epilog, escape, heading, prepend_prolog, textwidth def test_escape(): diff --git a/tests/test_util_template.py b/tests/test_util_template.py index 58ebd7fbb..bc325ac9d 100644 --- a/tests/test_util_template.py +++ b/tests/test_util_template.py @@ -4,7 +4,7 @@ Tests sphinx.util.template functions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 3f8ddbb37..9da505814 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -4,19 +4,19 @@ Tests util.typing functions. - :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import sys from numbers import Integral -from typing import ( - Any, Dict, Generator, List, TypeVar, Union, Callable, Tuple, Optional, Generic -) +from struct import Struct +from typing import (Any, Callable, Dict, Generator, List, NewType, Optional, Tuple, TypeVar, + Union) import pytest -from sphinx.util.typing import stringify +from sphinx.util.typing import restify, stringify class MyClass1: @@ -26,7 +26,10 @@ class MyClass1: class MyClass2(MyClass1): __qualname__ = '' + T = TypeVar('T') +MyInt = NewType('MyInt', int) + class MyList(List[T]): pass @@ -36,11 +39,94 @@ class BrokenType: __args__ = int +def test_restify(): + assert restify(int) == ":class:`int`" + assert restify(str) == ":class:`str`" + assert restify(None) == ":obj:`None`" + assert restify(Integral) == ":class:`numbers.Integral`" + assert restify(Struct) == ":class:`struct.Struct`" + assert restify(Any) == ":obj:`Any`" + + +def test_restify_type_hints_containers(): + assert restify(List) == ":class:`List`" + assert restify(Dict) == ":class:`Dict`" + assert restify(List[int]) == ":class:`List`\\ [:class:`int`]" + assert restify(List[str]) == ":class:`List`\\ [:class:`str`]" + assert restify(Dict[str, float]) == ":class:`Dict`\\ [:class:`str`, :class:`float`]" + assert restify(Tuple[str, str, str]) == ":class:`Tuple`\\ [:class:`str`, :class:`str`, :class:`str`]" + assert restify(Tuple[str, ...]) == ":class:`Tuple`\\ [:class:`str`, ...]" + assert restify(List[Dict[str, Tuple]]) == ":class:`List`\\ [:class:`Dict`\\ [:class:`str`, :class:`Tuple`]]" + assert restify(MyList[Tuple[int, int]]) == ":class:`tests.test_util_typing.MyList`\\ [:class:`Tuple`\\ [:class:`int`, :class:`int`]]" + assert restify(Generator[None, None, None]) == ":class:`Generator`\\ [:obj:`None`, :obj:`None`, :obj:`None`]" + + +def test_restify_type_hints_Callable(): + assert restify(Callable) == ":class:`Callable`" + + if sys.version_info >= (3, 7): + assert restify(Callable[[str], int]) == ":class:`Callable`\\ [[:class:`str`], :class:`int`]" + assert restify(Callable[..., int]) == ":class:`Callable`\\ [[...], :class:`int`]" + else: + assert restify(Callable[[str], int]) == ":class:`Callable`\\ [:class:`str`, :class:`int`]" + assert restify(Callable[..., int]) == ":class:`Callable`\\ [..., :class:`int`]" + + +def test_restify_type_hints_Union(): + assert restify(Optional[int]) == ":obj:`Optional`\\ [:class:`int`]" + assert restify(Union[str, None]) == ":obj:`Optional`\\ [:class:`str`]" + assert restify(Union[int, str]) == ":obj:`Union`\\ [:class:`int`, :class:`str`]" + + if sys.version_info >= (3, 7): + assert restify(Union[int, Integral]) == ":obj:`Union`\\ [:class:`int`, :class:`numbers.Integral`]" + assert (restify(Union[MyClass1, MyClass2]) == + ":obj:`Union`\\ [:class:`tests.test_util_typing.MyClass1`, :class:`tests.test_util_typing.`]") + else: + assert restify(Union[int, Integral]) == ":class:`numbers.Integral`" + assert restify(Union[MyClass1, MyClass2]) == ":class:`tests.test_util_typing.MyClass1`" + + +@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') +def test_restify_type_hints_typevars(): + T = TypeVar('T') + T_co = TypeVar('T_co', covariant=True) + T_contra = TypeVar('T_contra', contravariant=True) + + assert restify(T) == ":obj:`tests.test_util_typing.T`" + assert restify(T_co) == ":obj:`tests.test_util_typing.T_co`" + assert restify(T_contra) == ":obj:`tests.test_util_typing.T_contra`" + assert restify(List[T]) == ":class:`List`\\ [:obj:`tests.test_util_typing.T`]" + assert restify(MyInt) == ":class:`MyInt`" + + +def test_restify_type_hints_custom_class(): + assert restify(MyClass1) == ":class:`tests.test_util_typing.MyClass1`" + assert restify(MyClass2) == ":class:`tests.test_util_typing.`" + + +def test_restify_type_hints_alias(): + MyStr = str + MyTuple = Tuple[str, str] + assert restify(MyStr) == ":class:`str`" + assert restify(MyTuple) == ":class:`Tuple`\\ [:class:`str`, :class:`str`]" # type: ignore + + +@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') +def test_restify_type_ForwardRef(): + from typing import ForwardRef # type: ignore + assert restify(ForwardRef("myint")) == ":class:`myint`" + + +def test_restify_broken_type_hints(): + assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`' + + def test_stringify(): assert stringify(int) == "int" assert stringify(str) == "str" assert stringify(None) == "None" assert stringify(Integral) == "numbers.Integral" + assert restify(Struct) == ":class:`struct.Struct`" assert stringify(Any) == "Any" @@ -53,14 +139,14 @@ def test_stringify_type_hints_containers(): assert stringify(Tuple[str, str, str]) == "Tuple[str, str, str]" assert stringify(Tuple[str, ...]) == "Tuple[str, ...]" assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]" - assert stringify(MyList[Tuple[int, int]]) == "test_util_typing.MyList[Tuple[int, int]]" + assert stringify(MyList[Tuple[int, int]]) == "tests.test_util_typing.MyList[Tuple[int, int]]" assert stringify(Generator[None, None, None]) == "Generator[None, None, None]" @pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') def test_stringify_Annotated(): - from typing import Annotated - assert stringify(Annotated[str, "foo", "bar"]) == "str" + from typing import Annotated # type: ignore + assert stringify(Annotated[str, "foo", "bar"]) == "str" # NOQA def test_stringify_type_hints_string(): @@ -90,10 +176,10 @@ def test_stringify_type_hints_Union(): if sys.version_info >= (3, 7): assert stringify(Union[int, Integral]) == "Union[int, numbers.Integral]" assert (stringify(Union[MyClass1, MyClass2]) == - "Union[test_util_typing.MyClass1, test_util_typing.]") + "Union[tests.test_util_typing.MyClass1, tests.test_util_typing.]") else: assert stringify(Union[int, Integral]) == "numbers.Integral" - assert stringify(Union[MyClass1, MyClass2]) == "test_util_typing.MyClass1" + assert stringify(Union[MyClass1, MyClass2]) == "tests.test_util_typing.MyClass1" def test_stringify_type_hints_typevars(): @@ -105,11 +191,12 @@ def test_stringify_type_hints_typevars(): assert stringify(T_co) == "T_co" assert stringify(T_contra) == "T_contra" assert stringify(List[T]) == "List[T]" + assert stringify(MyInt) == "MyInt" def test_stringify_type_hints_custom_class(): - assert stringify(MyClass1) == "test_util_typing.MyClass1" - assert stringify(MyClass2) == "test_util_typing." + assert stringify(MyClass1) == "tests.test_util_typing.MyClass1" + assert stringify(MyClass2) == "tests.test_util_typing." def test_stringify_type_hints_alias(): @@ -120,4 +207,4 @@ def test_stringify_type_hints_alias(): def test_stringify_broken_type_hints(): - assert stringify(BrokenType) == 'test_util_typing.BrokenType' + assert stringify(BrokenType) == 'tests.test_util_typing.BrokenType' diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 7fe3bce6f..33fb045ce 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -4,7 +4,7 @@ Test the versioning implementation. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -15,8 +15,7 @@ from docutils.parsers.rst.directives.html import MetaBody from sphinx import addnodes from sphinx.testing.util import SphinxTestApp -from sphinx.versioning import add_uids, merge_doctrees, get_ratio - +from sphinx.versioning import add_uids, get_ratio, merge_doctrees app = original = original_uids = None diff --git a/tests/test_writer_latex.py b/tests/test_writer_latex.py index 160b14fa1..c3f6a622b 100644 --- a/tests/test_writer_latex.py +++ b/tests/test_writer_latex.py @@ -4,7 +4,7 @@ Test the LaTeX writer - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/tests/typing_test_data.py b/tests/typing_test_data.py index 6c0357911..c2db7d95b 100644 --- a/tests/typing_test_data.py +++ b/tests/typing_test_data.py @@ -1,6 +1,6 @@ from inspect import Signature from numbers import Integral -from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional +from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union def f0(x: int, y: Integral) -> None: @@ -77,7 +77,7 @@ def f14() -> Any: pass -def f15(x: "Unknown", y: "int") -> Any: +def f15(x: "Unknown", y: "int") -> Any: # type: ignore # NOQA pass diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..9430c9beb --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,49 @@ +import contextlib +import http.server +import pathlib +import ssl +import threading + +# Generated with: +# $ openssl req -new -x509 -days 3650 -nodes -out cert.pem \ +# -keyout cert.pem -addext "subjectAltName = DNS:localhost" +CERT_FILE = str(pathlib.Path(__file__).parent / "certs" / "cert.pem") + + +class HttpServerThread(threading.Thread): + def __init__(self, handler, *args, **kwargs): + super().__init__(*args, **kwargs) + self.server = http.server.HTTPServer(("localhost", 7777), handler) + + def run(self): + self.server.serve_forever(poll_interval=0.01) + + def terminate(self): + self.server.shutdown() + self.server.server_close() + self.join() + + +class HttpsServerThread(HttpServerThread): + def __init__(self, handler, *args, **kwargs): + super().__init__(handler, *args, **kwargs) + self.server.socket = ssl.wrap_socket( + self.server.socket, + certfile=CERT_FILE, + server_side=True, + ) + + +def create_server(thread_class): + def server(handler): + server_thread = thread_class(handler, daemon=True) + server_thread.start() + try: + yield server_thread + finally: + server_thread.terminate() + return contextlib.contextmanager(server) + + +http_server = create_server(HttpServerThread) +https_server = create_server(HttpsServerThread) diff --git a/tox.ini b/tox.ini index a61299979..21a0faec3 100644 --- a/tox.ini +++ b/tox.ini @@ -25,10 +25,10 @@ deps = extras = test setenv = - PYTHONWARNINGS = all,ignore::ImportWarning:importlib._bootstrap_external,ignore::DeprecationWarning:site,ignore::DeprecationWarning:distutils - PYTEST_ADDOPTS = --color yes + PYTHONWARNINGS = all,ignore::ImportWarning:importlib._bootstrap_external,ignore::DeprecationWarning:site,ignore::DeprecationWarning:distutils,ignore::DeprecationWarning:pip._vendor.packaging.version + PYTEST_ADDOPTS = {env:PYTEST_ADDOPTS:} --color yes commands= - pytest --durations 25 {posargs} + python -X dev -m pytest --durations 25 {posargs} [testenv:flake8] basepython = python3 @@ -41,6 +41,17 @@ extras = commands = flake8 {posargs} +[testenv:isort] +basepython = python3 +description = + Run import sorting checks. +whitelist_externals = + isort +extras = + lint +commands = + isort --check-only --diff . + [testenv:coverage] basepython = python3 description = diff --git a/utils/doclinter.py b/utils/doclinter.py index bb11decaf..f8df20bf7 100644 --- a/utils/doclinter.py +++ b/utils/doclinter.py @@ -13,7 +13,6 @@ 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::') diff --git a/utils/release-checklist b/utils/release-checklist index 582d26685..477ddcbbe 100644 --- a/utils/release-checklist +++ b/utils/release-checklist @@ -4,7 +4,7 @@ Release checklist for stable releases ------------------- -* open https://travis-ci.org/sphinx-doc/sphinx/branches and check **X.Y** branch is green +* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.Y.x" and all tests has passed * Run ``git fetch; git status`` and check nothing changed * ``python utils/bump_version.py X.Y.Z`` * Check diff by ``git diff`` @@ -18,17 +18,17 @@ for stable releases * ``python utils/bump_version.py --in-develop X.Y.Zb0`` (ex. 1.5.3b0) * Check diff by ``git diff`` * ``git commit -am 'Bump version'`` -* ``git push origin X.Y --tags`` -* ``git checkout master`` -* ``git merge X.Y`` -* ``git push origin master`` +* ``git push origin X.Y.x --tags`` +* ``git checkout X.x`` +* ``git merge X.Y.x`` +* ``git push origin X.x`` * Add new version/milestone to tracker categories * Write announcement and send to sphinx-dev, sphinx-users and python-announce for first beta releases ----------------------- -* open https://travis-ci.org/sphinx-doc/sphinx/branches and check **master** branch is green +* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:master" and all tests has passed * Run ``git fetch; git status`` and check nothing changed * Run ``python setup.py extract_messages`` * Run ``(cd sphinx/locale; tx push -s)`` @@ -43,10 +43,10 @@ for first beta releases * ``python utils/bump_version.py --in-develop X.Y.0b2`` (ex. 1.6.0b2) * Check diff by ``git diff`` * ``git commit -am 'Bump version'`` -* ``git checkout -b X.Y`` -* ``git push origin X.Y --tags`` +* ``git checkout -b X.x`` +* ``git push origin X.x --tags`` * ``git checkout master`` -* ``git merge X.Y`` +* ``git merge X.x`` * ``python utils/bump_version.py --in-develop A.B.0b0`` (ex. 1.7.0b0) * Check diff by ``git diff`` * ``git commit -am 'Bump version'`` @@ -58,7 +58,7 @@ for first beta releases for other beta releases ----------------------- -* open https://travis-ci.org/sphinx-doc/sphinx/branches and check **X.Y** branch is green +* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.x" and all tests has passed * Run ``git fetch; git status`` and check nothing changed * ``python utils/bump_version.py X.Y.0bN`` * Check diff by ``git diff`` @@ -71,9 +71,9 @@ for other beta releases * ``python utils/bump_version.py --in-develop X.Y.0bM`` (ex. 1.6.0b3) * Check diff by `git diff`` * ``git commit -am 'Bump version'`` -* ``git push origin X.Y --tags`` +* ``git push origin X.x --tags`` * ``git checkout master`` -* ``git merge X.Y`` +* ``git merge X.x`` * ``git push origin master`` * Add new version/milestone to tracker categories * Write announcement and send to sphinx-dev, sphinx-users and python-announce @@ -81,7 +81,7 @@ for other beta releases for major releases ------------------ -* open https://travis-ci.org/sphinx-doc/sphinx/branches and check **X.Y** branch is green +* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.x" and all tests has passed * Run ``git fetch; git status`` and check nothing changed * Run ``(cd sphinx/locale; tx pull -a -f)`` * Run ``python setup.py compile_catalog`` @@ -99,9 +99,9 @@ for major releases * ``python utils/bump_version.py --in-develop X.Y.1b0`` (ex. 1.6.1b0) * Check diff by ``git diff`` * ``git commit -am 'Bump version'`` -* ``git push origin X.Y --tags`` +* ``git push origin X.x --tags`` * ``git checkout master`` -* ``git merge X.Y`` +* ``git merge X.x`` * ``git push origin master`` * open https://github.com/sphinx-doc/sphinx/settings/branches and make ``A.B`` branch *not* protected * ``git checkout A.B`` (checkout old stable)