summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES98
-rw-r--r--CONTRIBUTING.rst16
-rw-r--r--doc/conf.py2
-rw-r--r--doc/development/tutorials/recipe.rst13
-rw-r--r--doc/extdev/deprecated.rst25
-rw-r--r--doc/usage/configuration.rst13
-rw-r--r--doc/usage/extensions/doctest.rst12
-rw-r--r--doc/usage/quickstart.rst25
-rw-r--r--setup.py2
-rw-r--r--sphinx/builders/html/__init__.py (renamed from sphinx/builders/html.py)16
-rw-r--r--sphinx/config.py3
-rw-r--r--sphinx/directives/__init__.py12
-rw-r--r--sphinx/domains/cpp.py4
-rw-r--r--sphinx/domains/python.py53
-rw-r--r--sphinx/environment/__init__.py8
-rw-r--r--sphinx/ext/apidoc.py79
-rw-r--r--sphinx/ext/autodoc/__init__.py8
-rw-r--r--sphinx/ext/autodoc/importer.py4
-rw-r--r--sphinx/ext/autodoc/type_comment.py96
-rw-r--r--sphinx/ext/autodoc/typehints.py20
-rw-r--r--sphinx/ext/coverage.py2
-rw-r--r--sphinx/ext/duration.py4
-rw-r--r--sphinx/ext/inheritance_diagram.py2
-rw-r--r--sphinx/ext/napoleon/docstring.py4
-rw-r--r--sphinx/ext/viewcode.py10
-rw-r--r--sphinx/highlighting.py7
-rw-r--r--sphinx/parsers.py11
-rw-r--r--sphinx/pycode/ast.py68
-rw-r--r--sphinx/texinputs/Makefile_t4
-rw-r--r--sphinx/transforms/__init__.py4
-rw-r--r--sphinx/transforms/post_transforms/images.py4
-rw-r--r--sphinx/util/__init__.py2
-rw-r--r--sphinx/util/images.py2
-rw-r--r--sphinx/util/inspect.py61
-rw-r--r--sphinx/util/inventory.py2
-rw-r--r--sphinx/util/pycompat.py2
-rw-r--r--sphinx/writers/html.py8
-rw-r--r--sphinx/writers/html5.py8
-rw-r--r--sphinx/writers/text.py1
-rw-r--r--tests/roots/test-builder-dirhtml/bar.rst4
-rw-r--r--tests/roots/test-builder-dirhtml/conf.py0
-rw-r--r--tests/roots/test-builder-dirhtml/foo/foo_1.rst4
-rw-r--r--tests/roots/test-builder-dirhtml/foo/foo_2.rst4
-rw-r--r--tests/roots/test-builder-dirhtml/foo/index.rst9
-rw-r--r--tests/roots/test-builder-dirhtml/index.rst9
-rw-r--r--tests/roots/test-ext-autodoc/target/__init__.py4
-rw-r--r--tests/roots/test-ext-autodoc/target/pep570.py10
-rw-r--r--tests/roots/test-ext-autodoc/target/typehints.py20
-rw-r--r--tests/roots/test-html_scaled_image_link/conf.py0
-rw-r--r--tests/roots/test-html_scaled_image_link/img.pngbin0 -> 66247 bytes
-rw-r--r--tests/roots/test-html_scaled_image_link/index.rst11
-rw-r--r--tests/roots/test-inheritance/diagram_w_nested_classes.rst5
-rw-r--r--tests/roots/test-inheritance/dummy/test_nested.py12
-rw-r--r--tests/test_autodoc.py13
-rw-r--r--tests/test_build_dirhtml.py48
-rw-r--r--tests/test_build_html.py19
-rw-r--r--tests/test_domain_js.py53
-rw-r--r--tests/test_domain_py.py71
-rw-r--r--tests/test_ext_autodoc_configs.py24
-rw-r--r--tests/test_ext_inheritance_diagram.py8
-rw-r--r--tests/test_highlighting.py2
-rw-r--r--tests/test_pycode_ast.py12
-rw-r--r--tests/test_util.py12
-rw-r--r--tests/test_util_inspect.py168
-rw-r--r--tox.ini8
65 files changed, 992 insertions, 253 deletions
diff --git a/CHANGES b/CHANGES
index b22dbd9d5..d324f11fc 100644
--- a/CHANGES
+++ b/CHANGES
@@ -7,6 +7,7 @@ Dependencies
* LaTeX: drop dependency on :program:`extractbb` for image inclusion in
Japanese documents as ``.xbb`` files are unneeded by :program:`dvipdfmx`
since TeXLive2015 (refs: #6189)
+* babel-2.0 or above is available (Unpinned)
Incompatible changes
--------------------
@@ -27,9 +28,12 @@ Deprecated
----------
* ``desc_signature['first']``
+* ``sphinx.directives.DescDirective``
* ``sphinx.domains.std.StandardDomain.add_object()``
+* ``sphinx.parsers.Parser.app``
* ``sphinx.testing.path.Path.text()``
* ``sphinx.testing.path.Path.bytes()``
+* ``sphinx.util.inspect.getargspec()``
Features added
--------------
@@ -43,11 +47,15 @@ Features added
* #6558: glossary: emit a warning for duplicated glossary entry
* #6558: std domain: emit a warning for duplicated generic objects
* #6830: py domain: Add new event: :event:`object-description-transform`
+* py domain: Support lambda functions in function signature
* Support priority of event handlers. For more detail, see
:py:meth:`.Sphinx.connect()`
* #3077: Implement the scoping for :rst:dir:`productionlist` as indicated
in the documentation.
* #1027: Support backslash line continuation in :rst:dir:`productionlist`.
+* #7108: config: Allow to show an error message from conf.py via ``ConfigError``
+* #7032: html: :confval:`html_scaled_image_link` will be disabled for images having
+ ``no-scaled-link`` class
Bugs fixed
----------
@@ -58,11 +66,14 @@ Bugs fixed
declarations.
* C++, suppress warnings for directly dependent typenames in cross references
generated automatically in signatures.
+* #5637: autodoc: Incorrect handling of nested class names on show-inheritance
+* #5637: inheritance_diagram: Incorrect handling of nested class names
+* #7139: ``code-block:: guess`` does not work
Testing
--------
-Release 2.4.0 (in development)
+Release 2.4.3 (in development)
==============================
Dependencies
@@ -74,11 +85,53 @@ Incompatible changes
Deprecated
----------
+Features added
+--------------
+
+Bugs fixed
+----------
+
+Testing
+--------
+
+Release 2.4.2 (released Feb 19, 2020)
+=====================================
+
+Bugs fixed
+----------
+
+* #7138: autodoc: ``autodoc.typehints`` crashed when variable has unbound object
+ as a value
+* #7156: autodoc: separator for keyword only arguments is not shown
+* #7146: autodoc: IndexError is raised on suppressed type_comment found
+* #7161: autodoc: typehints extension does not support parallel build
+* #7178: autodoc: TypeError is raised on fetching type annotations
+* #7151: crashed when extension assigns a value to ``env.indexentries``
+* #7170: text: Remove debug print
+* #7137: viewcode: Avoid to crash when non-python code given
+
+Release 2.4.1 (released Feb 11, 2020)
+=====================================
+
+Bugs fixed
+----------
+
+* #7120: html: crashed when on scaling SVG images which have float dimentions
+* #7126: autodoc: TypeError: 'getset_descriptor' object is not iterable
+
+Release 2.4.0 (released Feb 09, 2020)
+=====================================
+
+Deprecated
+----------
+
* The ``decode`` argument of ``sphinx.pycode.ModuleAnalyzer()``
* ``sphinx.directives.other.Index``
* ``sphinx.environment.temp_data['gloss_entries']``
* ``sphinx.environment.BuildEnvironment.indexentries``
* ``sphinx.environment.collectors.indexentries.IndexEntriesCollector``
+* ``sphinx.ext.apidoc.INITPY``
+* ``sphinx.ext.apidoc.shall_skip()``
* ``sphinx.io.FiletypeNotFoundError``
* ``sphinx.io.get_filetype()``
* ``sphinx.pycode.ModuleAnalyzer.encoding``
@@ -106,6 +159,8 @@ Features added
* #6446: duration: Add ``sphinx.ext.durations`` to inspect which documents slow
down the build
* #6837: LaTeX: Support a nested table
+* #7115: LaTeX: Allow to override LATEXOPTS and LATEXMKOPTS via environment
+ variable
* #6966: graphviz: Support ``:class:`` option
* #6696: html: ``:scale:`` option of image/figure directive not working for SVG
images (imagesize-1.2.0 or above is required)
@@ -133,6 +188,7 @@ Bugs fixed
----------
* #6925: html: Remove redundant type="text/javascript" from <script> elements
+* #7112: html: SVG image is not layouted as float even if aligned
* #6906, #6907: autodoc: failed to read the source codes encoeded in cp1251
* #6961: latex: warning for babel shown twice
* #7059: latex: LaTeX compilation falls into infinite loop (wrapfig issue)
@@ -140,6 +196,7 @@ Bugs fixed
* #6559: Wrong node-ids are generated in glossary directive
* #6986: apidoc: misdetects module name for .so file inside module
* #6899: apidoc: private members are not shown even if ``--private`` given
+* #6327: apidoc: Support a python package consisted of __init__.so file
* #6999: napoleon: fails to parse tilde in :exc: role
* #7019: gettext: Absolute path used in message catalogs
* #7023: autodoc: nested partial functions are not listed
@@ -153,34 +210,19 @@ Bugs fixed
modifier keys are ignored, which means the feature can interfere with browser
features
* #7090: std domain: Can't assign numfig-numbers for custom container nodes
+* #7106: std domain: enumerated nodes are marked as duplicated when extensions
+ call ``note_explicit_target()``
+* #7095: dirhtml: Cross references are broken via intersphinx and ``:doc:`` role
+* C++:
-Testing
---------
-
-Release 2.3.2 (in development)
-==============================
-
-Dependencies
-------------
-
-Incompatible changes
---------------------
-
-Deprecated
-----------
-
-Features added
---------------
-
-Bugs fixed
-----------
-
-* C++, don't crash when using the ``struct`` role in some cases.
-* C++, don't warn when using the ``var``/``member`` role for function
- parameters.
-
-Testing
---------
+ - Don't crash when using the ``struct`` role in some cases.
+ - Don't warn when using the ``var``/``member`` role for function
+ parameters.
+ - Render call and braced-init expressions correctly.
+* #7097: Filenames of images generated by
+ ``sphinx.transforms.post_transforms.images.ImageConverter``
+ or its subclasses (used for latex build) are now sanitized,
+ to prevent broken paths
Release 2.3.1 (released Dec 22, 2019)
=====================================
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 8c74b184a..6fd56fb9d 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -61,7 +61,7 @@ of the core developers before it is merged into the main repository.
#. If you feel uncomfortable or uncertain about an issue or your changes, feel
free to email the *sphinx-dev* mailing list.
#. Fork `the repository`_ on GitHub to start making your changes to the
- ``master`` branch for next MAJOR version, or ``X.Y`` branch for next
+ ``master`` branch for next MAJOR version, or ``A.x`` branch for next
MINOR version (see `Branch Model`_).
#. Write a test which shows that the bug was fixed or that the feature works
as expected.
@@ -94,10 +94,10 @@ These are the basic steps needed to start developing on Sphinx.
Sphinx adopts Semantic Versioning 2.0.0 (refs: https://semver.org/ ).
For changes that preserves backwards-compatibility of API and features,
- they should be included in the next MINOR release, use the ``X.Y`` branch.
+ they should be included in the next MINOR release, use the ``A.x`` branch.
::
- git checkout X.Y
+ git checkout A.x
For incompatible or other substantial changes that should wait until the
next MAJOR release, use the ``master`` branch.
@@ -199,7 +199,7 @@ These are the basic steps needed to start developing on Sphinx.
git push origin feature-xyz
#. Submit a pull request from your branch to the respective branch (``master``
- or ``X.Y``).
+ or ``A.x``).
#. Wait for a core developer to review your changes.
@@ -325,8 +325,8 @@ Versioning 2.0.0 (refs: https://semver.org/ ).
All changes including incompatible behaviors and public API updates are
allowed.
-``X.Y``
- Where ``X.Y`` is the ``MAJOR.MINOR`` release. Used to maintain current
+``A.x`` (ex. ``2.x``)
+ Where ``A.x`` is the ``MAJOR.MINOR`` release. Used to maintain current
MINOR release. All changes are allowed if the change preserves
backwards-compatibility of API and features.
@@ -334,8 +334,8 @@ Versioning 2.0.0 (refs: https://semver.org/ ).
new MAJOR version is released, the old ``MAJOR.MINOR`` branch will be
deleted and replaced by an equivalent tag.
-``X.Y.Z``
- Where ``X.Y.Z`` is the ``MAJOR.MINOR.PATCH`` release. Only
+``A.B.x`` (ex. ``2.4.x``)
+ Where ``A.B.x`` is the ``MAJOR.MINOR.PATCH`` release. Only
backwards-compatible bug fixes are allowed. In Sphinx project, PATCH
version is used for urgent bug fix.
diff --git a/doc/conf.py b/doc/conf.py
index aa513edf8..9951aed2d 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -14,7 +14,7 @@ templates_path = ['_templates']
exclude_patterns = ['_build']
project = 'Sphinx'
-copyright = '2007-2019, Georg Brandl and the Sphinx team'
+copyright = '2007-2020, Georg Brandl and the Sphinx team'
version = sphinx.__display_version__
release = version
show_authors = True
diff --git a/doc/development/tutorials/recipe.rst b/doc/development/tutorials/recipe.rst
index 2a3aa6408..474888cb0 100644
--- a/doc/development/tutorials/recipe.rst
+++ b/doc/development/tutorials/recipe.rst
@@ -75,7 +75,7 @@ The first thing to examine is the ``RecipeDirective`` directive:
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
- :lines: 17-37
+ :pyobject: RecipeDirective
Unlike :doc:`helloworld` and :doc:`todo`, this directive doesn't derive from
:class:`docutils.parsers.rst.Directive` and doesn't define a ``run`` method.
@@ -103,7 +103,12 @@ reStructuredText in the body.
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
- :lines: 40-102
+ :pyobject: IngredientIndex
+
+.. literalinclude:: examples/recipe.py
+ :language: python
+ :linenos:
+ :pyobject: RecipeIndex
Both ``IngredientIndex`` and ``RecipeIndex`` are derived from :class:`Index`.
They implement custom logic to generate a tuple of values that define the
@@ -126,7 +131,7 @@ creating here.
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
- :lines: 105-155
+ :pyobject: RecipeDomain
There are some interesting things to note about this ``recipe`` domain and domains
in general. Firstly, we actually register our directives, roles and indices
@@ -164,7 +169,7 @@ hook the various parts of our extension into Sphinx. Let's look at the
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
- :lines: 158-
+ :pyobject: setup
This looks a little different to what we're used to seeing. There are no calls
to :meth:`~Sphinx.add_directive` or even :meth:`~Sphinx.add_role`. Instead, we
diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst
index be33c26a5..484c76aba 100644
--- a/doc/extdev/deprecated.rst
+++ b/doc/extdev/deprecated.rst
@@ -31,11 +31,21 @@ The following is a list of deprecated interfaces.
- 3.0
- N/A
+ * - ``sphinx.directives.DescDirective``
+ - 3.0
+ - 5.0
+ - ``sphinx.directives.ObjectDescription``
+
* - ``sphinx.domains.std.StandardDomain.add_object()``
- 3.0
- 5.0
- ``sphinx.domains.std.StandardDomain.note_object()``
+ * - ``sphinx.parsers.Parser.app``
+ - 3.0
+ - 5.0
+ - N/A
+
* - ``sphinx.testing.path.Path.text()``
- 3.0
- 5.0
@@ -46,6 +56,11 @@ The following is a list of deprecated interfaces.
- 5.0
- ``sphinx.testing.path.Path.read_bytes()``
+ * - ``sphinx.util.inspect.getargspec()``
+ - 3.0
+ - 5.0
+ - ``inspect.getargspec()``
+
* - ``decode`` argument of ``sphinx.pycode.ModuleAnalyzer()``
- 2.4
- 4.0
@@ -76,6 +91,16 @@ The following is a list of deprecated interfaces.
- 4.0
- ``sphinx.errors.FiletypeNotFoundError``
+ * - ``sphinx.ext.apidoc.INITPY``
+ - 2.4
+ - 4.0
+ - N/A
+
+ * - ``sphinx.ext.apidoc.shall_skip()``
+ - 2.4
+ - 4.0
+ - ``sphinx.ext.apidoc.is_skipped_package``
+
* - ``sphinx.io.get_filetype()``
- 2.4
- 4.0
diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst
index 7fc09c037..cfc5db065 100644
--- a/doc/usage/configuration.rst
+++ b/doc/usage/configuration.rst
@@ -1357,8 +1357,21 @@ that use Sphinx's HTMLWriter class.
'target' option or scale related options: 'scale', 'width', 'height'.
The default is ``True``.
+ Document authors can this feature manually with giving ``no-scaled-link``
+ class to the image:
+
+ .. code-block:: rst
+
+ .. image:: sphinx.png
+ :scale: 50%
+ :class: no-scaled-link
+
.. versionadded:: 1.3
+ .. versionchanged:: 2.4
+
+ It is disabled for images having ``no-scaled-link`` class
+
.. confval:: html_math_renderer
The name of math_renderer extension for HTML output. The default is
diff --git a/doc/usage/extensions/doctest.rst b/doc/usage/extensions/doctest.rst
index 79a75536e..62d8577eb 100644
--- a/doc/usage/extensions/doctest.rst
+++ b/doc/usage/extensions/doctest.rst
@@ -11,11 +11,15 @@
pair: testing; snippets
-This extension allows you to test snippets in the documentation in a natural
-way. It works by collecting specially-marked up code blocks and running them as
-doctest tests.
+It is often helpful to include snippets of code in your documentation and
+demonstrate the results of executing them. But it is important to ensure that
+the documentation stays up-to-date with the code.
-Within one document, test code is partitioned in *groups*, where each group
+This extension allows you to test such code snippets in the documentation in
+a natural way. If you mark the code blocks as shown here, the ``doctest``
+builder will collect them and run them as doctest tests.
+
+Within each document, you can assign each snippet to a *group*. Each group
consists of:
* zero or more *setup code* blocks (e.g. importing the module to test)
diff --git a/doc/usage/quickstart.rst b/doc/usage/quickstart.rst
index 8566552e0..b5462a388 100644
--- a/doc/usage/quickstart.rst
+++ b/doc/usage/quickstart.rst
@@ -26,9 +26,6 @@ configuration values from a few questions it asks you. To use this, run:
$ sphinx-quickstart
-Answer each question asked. Be sure to say "yes" to the ``autodoc`` extension, as
-we will use this later.
-
There is also an automatic "API documentation" generator called
:program:`sphinx-apidoc`; see :doc:`/man/sphinx-apidoc` for details.
@@ -37,12 +34,11 @@ Defining document structure
---------------------------
Let's assume you've run :program:`sphinx-quickstart`. It created a source
-directory with :file:`conf.py` and a master document, :file:`index.rst` (if you
-accepted the defaults). The main function of the :term:`master document` is to
-serve as a welcome page, and to contain the root of the "table of contents
-tree" (or *toctree*). This is one of the main things that Sphinx adds to
-reStructuredText, a way to connect multiple files to a single hierarchy of
-documents.
+directory with :file:`conf.py` and a master document, :file:`index.rst`. The
+main function of the :term:`master document` is to serve as a welcome page, and
+to contain the root of the "table of contents tree" (or *toctree*). This is one
+of the main things that Sphinx adds to reStructuredText, a way to connect
+multiple files to a single hierarchy of documents.
.. sidebar:: reStructuredText directives
@@ -233,8 +229,7 @@ customize a config value that is not automatically added by
Keep in mind that the file uses Python syntax for strings, numbers, lists and
so on. The file is saved in UTF-8 by default, as indicated by the encoding
-declaration in the first line. If you use non-ASCII characters in any string
-value, you need to use Python Unicode strings (like ``project = u'Exposé'``).
+declaration in the first line.
|more| See :doc:`/usage/configuration` for documentation of all available
config values.
@@ -252,10 +247,12 @@ module that provides additional features for Sphinx projects) called *autodoc*.
In order to use *autodoc*, you need to activate it in :file:`conf.py` by
putting the string ``'sphinx.ext.autodoc'`` into the list assigned to the
-:confval:`extensions` config value. Then, you have a few additional directives
-at your disposal.
+:confval:`extensions` config value::
+
+ extensions = ['sphinx.ext.autodoc']
-For example, to document the function ``io.open()``, reading its signature and
+Then, you have a few additional directives at your disposal. For example, to
+document the function ``io.open()``, reading its signature and
docstring from the source file, you'd write this::
.. autofunction:: io.open
diff --git a/setup.py b/setup.py
index 19522777b..bb6273033 100644
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@ install_requires = [
'Pygments>=2.0',
'docutils>=0.12',
'snowballstemmer>=1.1',
- 'babel>=1.3,!=2.0',
+ 'babel>=1.3',
'alabaster>=0.7,<0.8',
'imagesize',
'requests>=2.5.0',
diff --git a/sphinx/builders/html.py b/sphinx/builders/html/__init__.py
index 80c99d3b8..a0f6a9e55 100644
--- a/sphinx/builders/html.py
+++ b/sphinx/builders/html/__init__.py
@@ -807,13 +807,17 @@ class StandaloneHTMLBuilder(Builder):
if self.config.html_scaled_image_link and self.html_scaled_image_link:
for node in doctree.traverse(nodes.image):
- scale_keys = ('scale', 'width', 'height')
- if not any((key in node) for key in scale_keys) or \
- isinstance(node.parent, nodes.reference):
- # docutils does unfortunately not preserve the
- # ``target`` attribute on images, so we need to check
- # the parent node here.
+ if not any((key in node) for key in ['scale', 'width', 'height']):
+ # resizing options are not given. scaled image link is available
+ # only for resized images.
continue
+ elif isinstance(node.parent, nodes.reference):
+ # A image having hyperlink target
+ continue
+ elif 'no-scaled-link' in node['classes']:
+ # scaled image link is disabled for this node
+ continue
+
uri = node['uri']
reference = nodes.reference('', '', internal=True)
if uri in self.images:
diff --git a/sphinx/config.py b/sphinx/config.py
index ca99fd5b7..87007c33d 100644
--- a/sphinx/config.py
+++ b/sphinx/config.py
@@ -324,6 +324,9 @@ def eval_config_file(filename: str, tags: Tags) -> Dict[str, Any]:
msg = __("The configuration file (or one of the modules it imports) "
"called sys.exit()")
raise ConfigError(msg)
+ except ConfigError:
+ # pass through ConfigError from conf.py as is. It will be shown in console.
+ raise
except Exception:
msg = __("There is a programmable error in your configuration file:\n\n%s")
raise ConfigError(msg % traceback.format_exc())
diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py
index 6b9e5b828..fd447b551 100644
--- a/sphinx/directives/__init__.py
+++ b/sphinx/directives/__init__.py
@@ -18,7 +18,9 @@ from docutils.parsers.rst import directives, roles
from sphinx import addnodes
from sphinx.addnodes import desc_signature
-from sphinx.deprecation import RemovedInSphinx40Warning, 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
@@ -284,9 +286,11 @@ deprecated_alias('sphinx.directives',
},
RemovedInSphinx40Warning)
-
-# backwards compatible old name (will be marked deprecated in 3.0)
-DescDirective = ObjectDescription
+deprecated_alias('sphinx.directives',
+ {
+ 'DescDirective': ObjectDescription,
+ },
+ RemovedInSphinx50Warning)
def setup(app: "Sphinx") -> Dict[str, Any]:
diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py
index c6ca546cc..3a23ff15a 100644
--- a/sphinx/domains/cpp.py
+++ b/sphinx/domains/cpp.py
@@ -3007,7 +3007,7 @@ class ASTParenExprList(ASTBase):
signode.append(nodes.Text(', '))
else:
first = False
- e.describe_signature(signode, mode, env, symbol)
+ e.describe_signature(signode, mode, env, symbol)
signode.append(nodes.Text(')'))
@@ -3034,7 +3034,7 @@ class ASTBracedInitList(ASTBase):
signode.append(nodes.Text(', '))
else:
first = False
- e.describe_signature(signode, mode, env, symbol)
+ e.describe_signature(signode, mode, env, symbol)
if self.trailingComma:
signode.append(nodes.Text(','))
signode.append(nodes.Text('}'))
diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py
index 6732ec166..9353dcb4c 100644
--- a/sphinx/domains/python.py
+++ b/sphinx/domains/python.py
@@ -10,6 +10,7 @@
import re
import warnings
+from inspect import Parameter
from typing import Any, Dict, Iterable, Iterator, List, Tuple
from typing import cast
@@ -30,6 +31,7 @@ from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.docutils import SphinxDirective
+from sphinx.util.inspect import signature_from_str
from sphinx.util.nodes import make_refnode
from sphinx.util.typing import TextlikeNode
@@ -62,6 +64,47 @@ pairindextypes = {
}
+def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist:
+ """Parse a list of arguments using AST parser"""
+ params = addnodes.desc_parameterlist(arglist)
+ sig = signature_from_str('(%s)' % arglist)
+ last_kind = None
+ for param in sig.parameters.values():
+ if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
+ # PEP-570: Separator for Positional Only Parameter: /
+ params += nodes.Text('/')
+ if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
+ param.POSITIONAL_ONLY,
+ None):
+ # PEP-3102: Separator for Keyword Only Parameter: *
+ params += nodes.Text('*')
+
+ node = addnodes.desc_parameter()
+ if param.kind == param.VAR_POSITIONAL:
+ node += nodes.Text('*' + param.name)
+ elif param.kind == param.VAR_KEYWORD:
+ node += nodes.Text('**' + param.name)
+ else:
+ node += nodes.Text(param.name)
+
+ if param.annotation is not param.empty:
+ node += nodes.Text(': ' + param.annotation)
+ if param.default is not param.empty:
+ if param.annotation is not param.empty:
+ node += nodes.Text(' = ' + str(param.default))
+ else:
+ node += nodes.Text('=' + str(param.default))
+
+ params += node
+ last_kind = param.kind
+
+ if last_kind == Parameter.POSITIONAL_ONLY:
+ # PEP-570: Separator for Positional Only Parameter: /
+ params += nodes.Text('/')
+
+ return params
+
+
def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None:
""""Parse" a list of arguments separated by commas.
@@ -284,7 +327,15 @@ class PyObject(ObjectDescription):
signode += addnodes.desc_name(name, name)
if arglist:
- _pseudo_parse_arglist(signode, arglist)
+ try:
+ signode += _parse_arglist(arglist)
+ except SyntaxError:
+ # fallback to parse arglist original parser.
+ # it supports to represent optional arguments (ex. "func(foo [, bar])")
+ _pseudo_parse_arglist(signode, arglist)
+ except NotImplementedError as exc:
+ logger.warning(exc)
+ _pseudo_parse_arglist(signode, arglist)
else:
if self.needs_arglist():
# for callables, add an empty parameter list
diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py
index 341f22a27..d7c74a2fe 100644
--- a/sphinx/environment/__init__.py
+++ b/sphinx/environment/__init__.py
@@ -644,3 +644,11 @@ class BuildEnvironment:
from sphinx.domains.index import IndexDomain
domain = cast(IndexDomain, self.get_domain('index'))
return domain.entries
+
+ @indexentries.setter
+ def indexentries(self, entries: Dict[str, List[Tuple[str, str, str, str, str]]]) -> None:
+ warnings.warn('env.indexentries() is deprecated. Please use IndexDomain instead.',
+ RemovedInSphinx40Warning, stacklevel=2)
+ from sphinx.domains.index import IndexDomain
+ domain = cast(IndexDomain, self.get_domain('index'))
+ domain.data['entries'] = entries
diff --git a/sphinx/ext/apidoc.py b/sphinx/ext/apidoc.py
index 99cf67016..a9196d3a6 100644
--- a/sphinx/ext/apidoc.py
+++ b/sphinx/ext/apidoc.py
@@ -29,7 +29,7 @@ from typing import Any, List, Tuple
import sphinx.locale
from sphinx import __display_version__, package_dir
from sphinx.cmd.quickstart import EXTENSIONS
-from sphinx.deprecation import RemovedInSphinx40Warning
+from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.locale import __
from sphinx.util import rst
from sphinx.util.osutil import FileAvoidWrite, ensuredir
@@ -46,7 +46,6 @@ else:
'show-inheritance',
]
-INITPY = '__init__.py'
PY_SUFFIXES = ('.py', '.pyx') + tuple(EXTENSION_SUFFIXES)
template_dir = path.join(package_dir, 'templates', 'apidoc')
@@ -66,11 +65,31 @@ def makename(package: str, module: str) -> str:
return name
+def is_initpy(filename: str) -> bool:
+ """Check *filename* is __init__ file or not."""
+ basename = path.basename(filename)
+ for suffix in sorted(PY_SUFFIXES, key=len, reverse=True):
+ if basename == '__init__' + suffix:
+ return True
+ else:
+ return False
+
+
def module_join(*modnames: str) -> str:
"""Join module names with dots."""
return '.'.join(filter(None, modnames))
+def is_packagedir(dirname: str = None, files: List[str] = None) -> bool:
+ """Check given *files* contains __init__ file."""
+ if files is None and dirname is None:
+ return False
+
+ if files is None:
+ files = os.listdir(dirname)
+ return any(f for f in files if is_initpy(f))
+
+
def write_file(name: str, text: str, opts: Any) -> None:
"""Write the output file for module/package <name>."""
quiet = getattr(opts, 'quiet', None)
@@ -132,15 +151,14 @@ def create_package_file(root: str, master_package: str, subroot: str, py_files:
opts: Any, subs: List[str], is_namespace: bool,
excludes: List[str] = [], user_template_dir: str = None) -> None:
"""Build the text of the file and write the file."""
- # build a list of sub packages (directories containing an INITPY file)
- subpackages = [sub for sub in subs if not
- shall_skip(path.join(root, sub, INITPY), opts, excludes)]
+ # build a list of sub packages (directories containing an __init__ file)
subpackages = [module_join(master_package, subroot, pkgname)
- for pkgname in subpackages]
+ for pkgname in subs
+ if not is_skipped_package(path.join(root, pkgname), opts, excludes)]
# build a list of sub modules
submodules = [sub.split('.')[0] for sub in py_files
if not is_skipped_module(path.join(root, sub), opts, excludes) and
- sub != INITPY]
+ not is_initpy(sub)]
submodules = [module_join(master_package, subroot, modname)
for modname in submodules]
options = copy(OPTIONS)
@@ -189,12 +207,14 @@ def create_modules_toc_file(modules: List[str], opts: Any, name: str = 'modules'
def shall_skip(module: str, opts: Any, excludes: List[str] = []) -> bool:
"""Check if we want to skip this module."""
+ warnings.warn('shall_skip() is deprecated.',
+ RemovedInSphinx40Warning)
# skip if the file doesn't exist and not using implicit namespaces
if not opts.implicit_namespaces and not path.exists(module):
return True
# Are we a package (here defined as __init__.py, not the folder in itself)
- if os.path.basename(module) == INITPY:
+ if is_initpy(module):
# Yes, check if we have any non-excluded modules at all here
all_skipped = True
basemodule = path.dirname(module)
@@ -207,12 +227,30 @@ def shall_skip(module: str, opts: Any, excludes: List[str] = []) -> bool:
# skip if it has a "private" name and this is selected
filename = path.basename(module)
- if filename != '__init__.py' and filename.startswith('_') and \
- not opts.includeprivate:
+ if is_initpy(filename) and filename.startswith('_') and not opts.includeprivate:
return True
return False
+def is_skipped_package(dirname: str, opts: Any, excludes: List[str] = []) -> bool:
+ """Check if we want to skip this module."""
+ if not path.isdir(dirname):
+ return False
+
+ files = glob.glob(path.join(dirname, '*.py'))
+ regular_package = any(f for f in files if is_initpy(f))
+ if not regular_package and not opts.implicit_namespaces:
+ # *dirname* is not both a regular package and an implicit namespace pacage
+ return True
+
+ # Check there is some showable module inside package
+ if all(is_excluded(path.join(dirname, f), excludes) for f in files):
+ # all submodules are excluded
+ return True
+ else:
+ return False
+
+
def is_skipped_module(filename: str, opts: Any, excludes: List[str]) -> bool:
"""Check if we want to skip this module."""
if not path.exists(filename):
@@ -236,7 +274,7 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any,
implicit_namespaces = getattr(opts, 'implicit_namespaces', False)
# check if the base directory is a package and get its name
- if INITPY in os.listdir(rootpath) or implicit_namespaces:
+ if is_packagedir(rootpath) or implicit_namespaces:
root_package = rootpath.split(path.sep)[-1]
else:
# otherwise, the base is a directory with packages
@@ -248,11 +286,13 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any,
py_files = sorted(f for f in files
if f.endswith(PY_SUFFIXES) and
not is_excluded(path.join(root, f), excludes))
- is_pkg = INITPY in py_files
- is_namespace = INITPY not in py_files and implicit_namespaces
+ is_pkg = is_packagedir(None, py_files)
+ is_namespace = not is_pkg and implicit_namespaces
if is_pkg:
- py_files.remove(INITPY)
- py_files.insert(0, INITPY)
+ for f in py_files[:]:
+ if is_initpy(f):
+ py_files.remove(f)
+ py_files.insert(0, f)
elif root != rootpath:
# only accept non-package at toplevel unless using implicit namespaces
if not implicit_namespaces:
@@ -269,7 +309,7 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any,
if is_pkg or is_namespace:
# we are in a package with something to document
- if subs or len(py_files) > 1 or not shall_skip(path.join(root, INITPY), opts):
+ if subs or len(py_files) > 1 or not is_skipped_package(root, opts):
subpackage = root[len(rootpath):].lstrip(path.sep).\
replace(path.sep, '.')
# if this is not a namespace or
@@ -475,6 +515,13 @@ def main(argv: List[str] = sys.argv[1:]) -> int:
return 0
+deprecated_alias('sphinx.ext.apidoc',
+ {
+ 'INITPY': '__init__.py',
+ },
+ RemovedInSphinx40Warning)
+
+
# So program can be started with "python -m sphinx.apidoc ..."
if __name__ == "__main__":
main()
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index b9404804b..7abd6c879 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -1065,7 +1065,7 @@ class DecoratorDocumenter(FunctionDocumenter):
# must be lower than FunctionDocumenter
priority = -1
- def format_args(self, **kwargs):
+ def format_args(self, **kwargs: Any) -> Any:
args = super().format_args(**kwargs)
if ',' in args:
return args
@@ -1146,7 +1146,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
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.__name__)
+ else ':class:`%s.%s`' % (b.__module__, b.__qualname__)
for b in self.object.__bases__]
self.add_line(' ' + _('Bases: %s') % ', '.join(bases),
sourcename)
@@ -1266,7 +1266,7 @@ class DataDocumenter(ModuleLevelDocumenter):
if not self.options.annotation:
# obtain annotation for this data
annotations = getattr(self.parent, '__annotations__', {})
- if self.objpath[-1] in annotations:
+ if annotations and self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
@@ -1454,7 +1454,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
if not self._datadescriptor:
# obtain annotation for this attribute
annotations = getattr(self.parent, '__annotations__', {})
- if self.objpath[-1] in annotations:
+ if annotations and self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py
index 3d4fb1805..e98b97915 100644
--- a/sphinx/ext/autodoc/importer.py
+++ b/sphinx/ext/autodoc/importer.py
@@ -12,7 +12,7 @@ import importlib
import traceback
import warnings
from collections import namedtuple
-from typing import Any, Callable, Dict, List, Tuple
+from typing import Any, Callable, Dict, List, Mapping, Tuple
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.util import logging
@@ -164,7 +164,7 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
continue
# annotation only member (ex. attr: int)
- if hasattr(subject, '__annotations__'):
+ if hasattr(subject, '__annotations__') and isinstance(subject.__annotations__, Mapping):
for name in subject.__annotations__:
if name not in members:
members[name] = Attribute(name, True, INSTANCEATTR)
diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py
index c94020bf0..a7eb2c4a2 100644
--- a/sphinx/ext/autodoc/type_comment.py
+++ b/sphinx/ext/autodoc/type_comment.py
@@ -8,13 +8,14 @@
:license: BSD, see LICENSE for details.
"""
-import ast
-from inspect import getsource
-from typing import Any, Dict
+from inspect import Parameter, Signature, getsource
+from typing import Any, Dict, List
from typing import cast
import sphinx
from sphinx.application import Sphinx
+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
@@ -23,11 +24,73 @@ from sphinx.util import logging
logger = logging.getLogger(__name__)
-def get_type_comment(obj: Any) -> ast.FunctionDef:
+def not_suppressed(argtypes: List[ast.AST] = []) -> bool:
+ """Check given *argtypes* is suppressed type_comment or not."""
+ if len(argtypes) == 0: # no argtypees
+ return False
+ elif len(argtypes) == 1 and ast_unparse(argtypes[0]) == "...": # suppressed
+ # Note: To support multiple versions of python, this uses ``ast_unparse()`` for
+ # comparison with Ellipsis. Since 3.8, ast.Constant has been used to represent
+ # Ellipsis node instead of ast.Ellipsis.
+ return False
+ else: # not suppressed
+ return True
+
+
+def signature_from_ast(node: ast.FunctionDef, bound_method: bool,
+ type_comment: ast.FunctionDef) -> Signature:
+ """Return a Signature object for the given *node*.
+
+ :param bound_method: Specify *node* is a bound method or not
+ """
+ params = []
+ if hasattr(node.args, "posonlyargs"): # for py38+
+ for arg in node.args.posonlyargs: # type: ignore
+ param = Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=arg.type_comment)
+ params.append(param)
+
+ for arg in node.args.args:
+ param = Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD,
+ annotation=arg.type_comment or Parameter.empty)
+ params.append(param)
+
+ if node.args.vararg:
+ param = Parameter(node.args.vararg.arg, Parameter.VAR_POSITIONAL,
+ annotation=arg.type_comment or Parameter.empty)
+ params.append(param)
+
+ for arg in node.args.kwonlyargs:
+ param = Parameter(arg.arg, Parameter.KEYWORD_ONLY,
+ annotation=arg.type_comment or Parameter.empty)
+ params.append(param)
+
+ if node.args.kwarg:
+ param = Parameter(node.args.kwarg.arg, Parameter.VAR_KEYWORD,
+ annotation=arg.type_comment or Parameter.empty)
+ params.append(param)
+
+ # Remove first parameter when *obj* is bound_method
+ if bound_method and params:
+ params.pop(0)
+
+ # merge type_comment into signature
+ if not_suppressed(type_comment.argtypes): # type: ignore
+ for i, param in enumerate(params):
+ params[i] = param.replace(annotation=type_comment.argtypes[i]) # type: ignore
+
+ if node.returns:
+ return Signature(params, return_annotation=node.returns)
+ elif type_comment.returns:
+ return Signature(params, return_annotation=ast_unparse(type_comment.returns))
+ else:
+ return Signature(params)
+
+
+def get_type_comment(obj: Any, bound_method: bool = False) -> Signature:
"""Get type_comment'ed FunctionDef object from living object.
This tries to parse original code for living object and returns
- AST node for given *obj*. It requires py38+ or typed_ast module.
+ Signature for given *obj*. It requires py38+ or typed_ast module.
"""
try:
source = getsource(obj)
@@ -41,7 +104,8 @@ def get_type_comment(obj: Any) -> ast.FunctionDef:
subject = cast(ast.FunctionDef, module.body[0]) # type: ignore
if getattr(subject, "type_comment", None):
- return ast_parse(subject.type_comment, mode='func_type') # type: ignore
+ function = ast_parse(subject.type_comment, mode='func_type')
+ return signature_from_ast(subject, bound_method, function) # type: ignore
else:
return None
except (OSError, TypeError): # failed to load source code
@@ -53,19 +117,19 @@ def get_type_comment(obj: Any) -> ast.FunctionDef:
def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method: bool) -> None:
"""Update annotations info of *obj* using type_comments."""
try:
- function = get_type_comment(obj)
- if function and hasattr(function, 'argtypes'):
- if function.argtypes != [ast.Ellipsis]: # type: ignore
- sig = inspect.signature(obj, bound_method)
- for i, param in enumerate(sig.parameters.values()):
- if param.name not in obj.__annotations__:
- annotation = ast_unparse(function.argtypes[i]) # type: ignore
- obj.__annotations__[param.name] = annotation
+ type_sig = get_type_comment(obj, bound_method)
+ if type_sig:
+ sig = inspect.signature(obj, bound_method)
+ for param in sig.parameters.values():
+ if param.name not in obj.__annotations__:
+ annotation = type_sig.parameters[param.name].annotation
+ if annotation is not Parameter.empty:
+ obj.__annotations__[param.name] = ast_unparse(annotation)
if 'return' not in obj.__annotations__:
- obj.__annotations__['return'] = ast_unparse(function.returns) # type: ignore
+ obj.__annotations__['return'] = type_sig.return_annotation
except NotImplementedError as exc: # failed to ast.unparse()
- logger.warning("Failed to parse type_comment for %r: %s", obj, exc)
+ logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc)
def setup(app: Sphinx) -> Dict[str, Any]:
diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py
index d7b4ee96b..64782dc1c 100644
--- a/sphinx/ext/autodoc/typehints.py
+++ b/sphinx/ext/autodoc/typehints.py
@@ -18,19 +18,19 @@ from docutils.nodes import Element
from sphinx import addnodes
from sphinx.application import Sphinx
-from sphinx.config import ENUM
+from sphinx.config import Config, ENUM
from sphinx.util import inspect, typing
-def config_inited(app, config):
+def config_inited(app: Sphinx, config: Config) -> None:
if config.autodoc_typehints == 'description':
# HACK: override this to make autodoc suppressing typehints in signatures
- config.autodoc_typehints = 'none'
+ config.autodoc_typehints = 'none' # type: ignore
# preserve user settings
- app._autodoc_typehints_description = True
+ app._autodoc_typehints_description = True # type: ignore
else:
- app._autodoc_typehints_description = False
+ app._autodoc_typehints_description = False # type: ignore
def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
@@ -46,7 +46,7 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
annotation[param.name] = typing.stringify(param.annotation)
if sig.return_annotation is not sig.empty:
annotation['return'] = typing.stringify(sig.return_annotation)
- except TypeError:
+ except (TypeError, ValueError):
pass
@@ -140,10 +140,16 @@ def modify_field_list(node: nodes.field_list, annotations: Dict[str, str]) -> No
node += field
-def setup(app):
+def setup(app: Sphinx) -> Dict[str, Any]:
app.setup_extension('sphinx.ext.autodoc')
app.config.values['autodoc_typehints'] = ('signature', True,
ENUM("signature", "description", "none"))
app.connect('config-inited', config_inited)
app.connect('autodoc-process-signature', record_typehints)
app.connect('object-description-transform', merge_typehints)
+
+ return {
+ 'version': 'builtin',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py
index 987cf2919..e8157848f 100644
--- a/sphinx/ext/coverage.py
+++ b/sphinx/ext/coverage.py
@@ -123,7 +123,7 @@ class CoverageBuilder(Builder):
op.write(' * %-50s [%9s]\n' % (name, typ))
op.write('\n')
- def ignore_pyobj(self, full_name):
+ def ignore_pyobj(self, full_name: str) -> bool:
for exp in self.py_ignorexps:
if exp.search(full_name):
return True
diff --git a/sphinx/ext/duration.py b/sphinx/ext/duration.py
index 02e60cf7e..669baf2f1 100644
--- a/sphinx/ext/duration.py
+++ b/sphinx/ext/duration.py
@@ -11,8 +11,8 @@
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 Dict, List
from docutils import nodes
@@ -82,7 +82,7 @@ def on_build_finished(app: Sphinx, error: Exception) -> None:
logger.info('%d.%03d %s', d.seconds, d.microseconds / 1000, docname)
-def setup(app):
+def setup(app: Sphinx) -> Dict[str, Any]:
app.add_domain(DurationDomain)
app.connect('builder-inited', on_builder_inited)
app.connect('source-read', on_source_read)
diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py
index 83e277523..db2a15b14 100644
--- a/sphinx/ext/inheritance_diagram.py
+++ b/sphinx/ext/inheritance_diagram.py
@@ -229,7 +229,7 @@ class InheritanceGraph:
if module in ('__builtin__', 'builtins'):
fullname = cls.__name__
else:
- fullname = '%s.%s' % (module, cls.__name__)
+ fullname = '%s.%s' % (module, cls.__qualname__)
if parts == 0:
result = fullname
else:
diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py
index 81f2496cb..7f6ebe478 100644
--- a/sphinx/ext/napoleon/docstring.py
+++ b/sphinx/ext/napoleon/docstring.py
@@ -561,7 +561,7 @@ class GoogleDocstring:
lines = self._consume_to_next_section()
self._parsed_lines.extend(lines)
- def _parse_admonition(self, admonition, section):
+ def _parse_admonition(self, admonition: str, section: str) -> List[str]:
# type (str, str) -> List[str]
lines = self._consume_to_next_section()
return self._format_admonition(admonition, lines)
@@ -603,7 +603,7 @@ class GoogleDocstring:
label = labels.get(section.lower(), section)
return self._parse_generic_section(label, use_admonition)
- def _parse_custom_generic_section(self, section):
+ def _parse_custom_generic_section(self, section: str) -> List[str]:
# for now, no admonition for simple custom sections
return self._parse_generic_section(section, False)
diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py
index 09fcd695f..dc24a1993 100644
--- a/sphinx/ext/viewcode.py
+++ b/sphinx/ext/viewcode.py
@@ -54,20 +54,20 @@ def doctree_read(app: Sphinx, doctree: Node) -> None:
if app.builder.name.startswith("epub") and not env.config.viewcode_enable_epub:
return
- def has_tag(modname, fullname, docname, refname):
+ def has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool:
entry = env._viewcode_modules.get(modname, None) # type: ignore
if entry is False:
- return
+ return False
code_tags = app.emit_firstresult('viewcode-find-source', modname)
if code_tags is None:
try:
analyzer = ModuleAnalyzer.for_module(modname)
+ analyzer.find_tags()
except Exception:
env._viewcode_modules[modname] = False # type: ignore
- return
+ return False
- analyzer.find_tags()
code = analyzer.code
tags = analyzer.tags
else:
@@ -81,6 +81,8 @@ def doctree_read(app: Sphinx, doctree: Node) -> None:
used[fullname] = docname
return True
+ return False
+
for objnode in doctree.traverse(addnodes.desc):
if objnode.get('domain') != 'py':
continue
diff --git a/sphinx/highlighting.py b/sphinx/highlighting.py
index f3445d1bd..4dc9ce543 100644
--- a/sphinx/highlighting.py
+++ b/sphinx/highlighting.py
@@ -105,11 +105,6 @@ class PygmentsBridge:
lang = 'pycon3'
else:
lang = 'python3'
- elif lang == 'guess':
- try:
- lexer = guess_lexer(source)
- except Exception:
- lexer = lexers['none']
if lang in lexers:
# just return custom lexers here (without installing raiseonerror filter)
@@ -119,7 +114,7 @@ class PygmentsBridge:
else:
try:
if lang == 'guess':
- lexer = guess_lexer(lang, **opts)
+ lexer = guess_lexer(source, **opts)
else:
lexer = get_lexer_by_name(lang, **opts)
except ClassNotFound:
diff --git a/sphinx/parsers.py b/sphinx/parsers.py
index 2a61971f3..3974d1c66 100644
--- a/sphinx/parsers.py
+++ b/sphinx/parsers.py
@@ -8,6 +8,7 @@
:license: BSD, see LICENSE for details.
"""
+import warnings
from typing import Any, Dict, List, Union
import docutils.parsers
@@ -17,6 +18,7 @@ from docutils.parsers.rst import states
from docutils.statemachine import StringList
from docutils.transforms.universal import SmartQuotes
+from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.util.rst import append_epilog, prepend_prolog
if False:
@@ -47,6 +49,8 @@ class Parser(docutils.parsers.Parser):
.. deprecated:: 1.6
``warn()`` and ``info()`` is deprecated. Use :mod:`sphinx.util.logging` instead.
+ .. deprecated:: 3.0
+ parser.app is deprecated.
"""
def set_application(self, app: "Sphinx") -> None:
@@ -54,10 +58,15 @@ class Parser(docutils.parsers.Parser):
:param sphinx.application.Sphinx app: Sphinx application object
"""
- self.app = app
+ self._app = app
self.config = app.config
self.env = app.env
+ @property
+ def app(self) -> "Sphinx":
+ warnings.warn('parser.app is deprecated.', RemovedInSphinx50Warning)
+ return self._app
+
class RSTParser(docutils.parsers.rst.Parser, Parser):
"""A reST parser for Sphinx."""
diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py
index 22207b715..52617e3bc 100644
--- a/sphinx/pycode/ast.py
+++ b/sphinx/pycode/ast.py
@@ -9,6 +9,7 @@
"""
import sys
+from typing import List
if sys.version_info > (3, 8):
import ast
@@ -40,6 +41,13 @@ def unparse(node: ast.AST) -> str:
return None
elif isinstance(node, str):
return node
+ elif isinstance(node, ast.arg):
+ if node.annotation:
+ return "%s: %s" % (node.arg, unparse(node.annotation))
+ else:
+ return node.arg
+ elif isinstance(node, ast.arguments):
+ return unparse_arguments(node)
elif isinstance(node, ast.Attribute):
return "%s.%s" % (unparse(node.value), node.attr)
elif isinstance(node, ast.Bytes):
@@ -58,7 +66,7 @@ def unparse(node: ast.AST) -> str:
elif isinstance(node, ast.Index):
return unparse(node.value)
elif isinstance(node, ast.Lambda):
- return "<function <lambda>>" # TODO
+ return "lambda %s: ..." % unparse(node.args)
elif isinstance(node, ast.List):
return "[" + ", ".join(unparse(e) for e in node.elts) + "]"
elif isinstance(node, ast.Name):
@@ -80,3 +88,61 @@ def unparse(node: ast.AST) -> str:
return repr(node.value)
else:
raise NotImplementedError('Unable to parse %s object' % type(node).__name__)
+
+
+def unparse_arguments(node: ast.arguments) -> str:
+ """Unparse an arguments to string."""
+ defaults = list(node.defaults)
+ positionals = len(node.args)
+ posonlyargs = 0
+ if hasattr(node, "posonlyargs"): # for py38+
+ posonlyargs += len(node.posonlyargs) # type:ignore
+ positionals += posonlyargs
+ for _ in range(len(defaults), positionals):
+ defaults.insert(0, None)
+
+ kw_defaults = list(node.kw_defaults)
+ for _ in range(len(kw_defaults), len(node.kwonlyargs)):
+ kw_defaults.insert(0, None)
+
+ args = [] # type: List[str]
+ if hasattr(node, "posonlyargs"): # for py38+
+ for i, arg in enumerate(node.posonlyargs): # type: ignore
+ name = unparse(arg)
+ if defaults[i]:
+ if arg.annotation:
+ name += " = %s" % unparse(defaults[i])
+ else:
+ name += "=%s" % unparse(defaults[i])
+ args.append(name)
+
+ if node.posonlyargs: # type: ignore
+ args.append('/')
+
+ for i, arg in enumerate(node.args):
+ name = unparse(arg)
+ if defaults[i + posonlyargs]:
+ if arg.annotation:
+ name += " = %s" % unparse(defaults[i + posonlyargs])
+ else:
+ name += "=%s" % unparse(defaults[i + posonlyargs])
+ args.append(name)
+
+ if node.vararg:
+ args.append("*" + unparse(node.vararg))
+
+ if node.kwonlyargs and not node.vararg:
+ args.append('*')
+ for i, arg in enumerate(node.kwonlyargs):
+ name = unparse(arg)
+ if kw_defaults[i]:
+ if arg.annotation:
+ name += " = %s" % unparse(kw_defaults[i])
+ else:
+ name += "=%s" % unparse(kw_defaults[i])
+ args.append(name)
+
+ if node.kwarg:
+ args.append("**" + unparse(node.kwarg))
+
+ return ", ".join(args)
diff --git a/sphinx/texinputs/Makefile_t b/sphinx/texinputs/Makefile_t
index 90cee829b..96bb0fed1 100644
--- a/sphinx/texinputs/Makefile_t
+++ b/sphinx/texinputs/Makefile_t
@@ -14,13 +14,13 @@ ALLPS = $(addsuffix .ps,$(ALLDOCS))
# Prefix for archive names
ARCHIVEPREFIX =
# Additional LaTeX options (passed via variables in latexmkrc/latexmkjarc file)
-export LATEXOPTS =
+export LATEXOPTS ?=
# Additional latexmk options
{% if latex_engine == 'xelatex' -%}
# with latexmk version 4.52b or higher set LATEXMKOPTS to -xelatex either here
# or on command line for faster builds.
{% endif -%}
-LATEXMKOPTS =
+LATEXMKOPTS ?=
{% if xindy_use -%}
export XINDYOPTS = {{ xindy_lang_option }} -M sphinx.xdy
{% if latex_engine == 'pdflatex' -%}
diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py
index 922b22e46..a00f04fdf 100644
--- a/sphinx/transforms/__init__.py
+++ b/sphinx/transforms/__init__.py
@@ -173,7 +173,9 @@ class AutoNumbering(SphinxTransform):
domain = self.env.get_domain('std') # type: StandardDomain
for node in self.document.traverse(nodes.Element):
- if domain.is_enumerable_node(node) and domain.get_numfig_title(node) is not None:
+ if (domain.is_enumerable_node(node) and
+ domain.get_numfig_title(node) is not None and
+ node['ids'] == []):
self.document.note_implicit_target(node)
diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py
index 758e92f0d..d1b513b27 100644
--- a/sphinx/transforms/post_transforms/images.py
+++ b/sphinx/transforms/post_transforms/images.py
@@ -9,6 +9,7 @@
"""
import os
+import re
from hashlib import sha1
from math import ceil
from typing import Any, Dict, List, Tuple
@@ -27,6 +28,7 @@ from sphinx.util.osutil import ensuredir, movefile
logger = logging.getLogger(__name__)
MAX_FILENAME_LEN = 32
+CRITICAL_PATH_CHAR_RE = re.compile('[:;<>|*" ]')
class BaseImageConverter(SphinxTransform):
@@ -65,6 +67,7 @@ class ImageDownloader(BaseImageConverter):
if basename == '' or len(basename) > MAX_FILENAME_LEN:
filename, ext = os.path.splitext(node['uri'])
basename = sha1(filename.encode()).hexdigest() + ext
+ basename = re.sub(CRITICAL_PATH_CHAR_RE, "_", basename)
dirname = node['uri'].replace('://', '/').translate({ord("?"): "/",
ord("&"): "/"})
@@ -146,6 +149,7 @@ class DataURIExtractor(BaseImageConverter):
def get_filename_for(filename: str, mimetype: str) -> str:
basename = os.path.basename(filename)
+ basename = re.sub(CRITICAL_PATH_CHAR_RE, "_", basename)
return os.path.splitext(basename)[0] + get_image_extension(mimetype)
diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py
index 954315b86..12ae051f1 100644
--- a/sphinx/util/__init__.py
+++ b/sphinx/util/__init__.py
@@ -649,7 +649,7 @@ class progress_message:
def __call__(self, f: Callable) -> Callable:
@functools.wraps(f)
- def wrapper(*args, **kwargs):
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
with self:
return f(*args, **kwargs)
diff --git a/sphinx/util/images.py b/sphinx/util/images.py
index 17bd95685..568682b37 100644
--- a/sphinx/util/images.py
+++ b/sphinx/util/images.py
@@ -41,6 +41,8 @@ def get_image_size(filename: str) -> Tuple[int, int]:
size = imagesize.get(filename)
if size[0] == -1:
size = None
+ elif isinstance(size[0], float) or isinstance(size[1], float):
+ size = (int(size[0]), int(size[1]))
if size is None and Image: # fallback to Pillow
with Image.open(filename) as im:
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index af8102402..5bf3ffbbf 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -21,8 +21,11 @@ from inspect import ( # NOQA
)
from io import StringIO
from typing import Any, Callable, Mapping, List, Tuple
+from typing import cast
-from sphinx.deprecation import RemovedInSphinx40Warning
+from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
+from sphinx.pycode.ast import ast # for py35-37
+from sphinx.pycode.ast import unparse as ast_unparse
from sphinx.util import logging
from sphinx.util.typing import stringify as stringify_annotation
@@ -51,9 +54,11 @@ memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software
# Foundation; All Rights Reserved
-def getargspec(func):
+def getargspec(func: Callable) -> Any:
"""Like inspect.getfullargspec but supports bound methods, and wrapped
methods."""
+ warnings.warn('sphinx.ext.inspect.getargspec() is deprecated',
+ RemovedInSphinx50Warning)
# 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.
@@ -387,9 +392,9 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
# PEP-570: Separator for Positional Only Parameter: /
args.append('/')
- elif param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
- param.POSITIONAL_ONLY,
- None):
+ if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
+ param.POSITIONAL_ONLY,
+ None):
# PEP-3102: Separator for Keyword Only Parameter: *
args.append('*')
@@ -427,6 +432,52 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
return '(%s) -> %s' % (', '.join(args), annotation)
+def signature_from_str(signature: str) -> inspect.Signature:
+ """Create a Signature object from string."""
+ module = ast.parse('def func' + signature + ': pass')
+ definition = cast(ast.FunctionDef, module.body[0]) # type: ignore
+
+ # parameters
+ args = definition.args
+ params = []
+
+ if hasattr(args, "posonlyargs"):
+ for arg in args.posonlyargs: # type: ignore
+ annotation = ast_unparse(arg.annotation) or Parameter.empty
+ params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY,
+ annotation=annotation))
+
+ for i, arg in enumerate(args.args):
+ if len(args.args) - i <= len(args.defaults):
+ default = ast_unparse(args.defaults[-len(args.args) + i])
+ else:
+ default = Parameter.empty
+
+ annotation = ast_unparse(arg.annotation) or Parameter.empty
+ params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD,
+ default=default, annotation=annotation))
+
+ if args.vararg:
+ annotation = ast_unparse(args.vararg.annotation) or Parameter.empty
+ params.append(Parameter(args.vararg.arg, Parameter.VAR_POSITIONAL,
+ annotation=annotation))
+
+ for i, arg in enumerate(args.kwonlyargs):
+ default = ast_unparse(args.kw_defaults[i])
+ annotation = ast_unparse(arg.annotation) or Parameter.empty
+ params.append(Parameter(arg.arg, Parameter.KEYWORD_ONLY, default=default,
+ annotation=annotation))
+
+ if args.kwarg:
+ annotation = ast_unparse(args.kwarg.annotation) or Parameter.empty
+ params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD,
+ annotation=annotation))
+
+ return_annotation = ast_unparse(definition.returns) or Parameter.empty
+
+ return inspect.Signature(params, return_annotation=return_annotation)
+
+
class Signature:
"""The Signature object represents the call signature of a callable object and
its return annotation.
diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py
index 64f4ef4e3..9b647ccac 100644
--- a/sphinx/util/inventory.py
+++ b/sphinx/util/inventory.py
@@ -122,7 +122,7 @@ class InventoryFile:
for line in stream.read_compressed_lines():
# be careful to handle names with embedded spaces correctly
- m = re.match(r'(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+(\S+)\s+(.*)',
+ m = re.match(r'(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+?(\S*)\s+(.*)',
line.rstrip())
if not m:
continue
diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py
index ba4f83a71..061bbcb6d 100644
--- a/sphinx/util/pycompat.py
+++ b/sphinx/util/pycompat.py
@@ -52,7 +52,7 @@ class UnicodeMixin:
.. deprecated:: 2.0
"""
- def __str__(self):
+ def __str__(self) -> str:
warnings.warn('UnicodeMixin is deprecated',
RemovedInSphinx40Warning, stacklevel=2)
return self.__unicode__() # type: ignore
diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py
index 32de55031..e74c0334f 100644
--- a/sphinx/writers/html.py
+++ b/sphinx/writers/html.py
@@ -604,11 +604,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator):
atts['height'] = int(atts['height']) * scale
atts['alt'] = node.get('alt', uri)
if 'align' in node:
- self.body.append('<div align="%s" class="align-%s">' %
- (node['align'], node['align']))
- self.context.append('</div>\n')
- else:
- self.context.append('')
+ atts['class'] = 'align-%s' % node['align']
self.body.append(self.emptytag(node, 'img', '', **atts))
return
@@ -617,7 +613,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator):
# overwritten
def depart_image(self, node: Element) -> None:
if node['uri'].lower().endswith(('svg', 'svgz')):
- self.body.append(self.context.pop())
+ pass
else:
super().depart_image(node)
diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py
index c6d4b4f99..0c00a1fa4 100644
--- a/sphinx/writers/html5.py
+++ b/sphinx/writers/html5.py
@@ -545,11 +545,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
atts['height'] = int(atts['height']) * scale
atts['alt'] = node.get('alt', uri)
if 'align' in node:
- self.body.append('<div align="%s" class="align-%s">' %
- (node['align'], node['align']))
- self.context.append('</div>\n')
- else:
- self.context.append('')
+ atts['class'] = 'align-%s' % node['align']
self.body.append(self.emptytag(node, 'img', '', **atts))
return
@@ -558,7 +554,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
# overwritten
def depart_image(self, node: Element) -> None:
if node['uri'].lower().endswith(('svg', 'svgz')):
- self.body.append(self.context.pop())
+ pass
else:
super().depart_image(node)
diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py
index 9b9f8e62a..ac3ba9dae 100644
--- a/sphinx/writers/text.py
+++ b/sphinx/writers/text.py
@@ -916,7 +916,6 @@ class TextTranslator(SphinxTranslator):
def _depart_admonition(self, node: Element) -> None:
label = admonitionlabels[node.tagname]
indent = sum(self.stateindent) + len(label)
- print(self.states[-1])
if (len(self.states[-1]) == 1 and
self.states[-1][0][0] == 0 and
MAXWIDTH - indent >= sum(len(s) for s in self.states[-1][0][1])):
diff --git a/tests/roots/test-builder-dirhtml/bar.rst b/tests/roots/test-builder-dirhtml/bar.rst
new file mode 100644
index 000000000..11f287a18
--- /dev/null
+++ b/tests/roots/test-builder-dirhtml/bar.rst
@@ -0,0 +1,4 @@
+.. _bar:
+
+bar
+===
diff --git a/tests/roots/test-builder-dirhtml/conf.py b/tests/roots/test-builder-dirhtml/conf.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/roots/test-builder-dirhtml/conf.py
diff --git a/tests/roots/test-builder-dirhtml/foo/foo_1.rst b/tests/roots/test-builder-dirhtml/foo/foo_1.rst
new file mode 100644
index 000000000..6db0ea57e
--- /dev/null
+++ b/tests/roots/test-builder-dirhtml/foo/foo_1.rst
@@ -0,0 +1,4 @@
+.. _foo_1:
+
+foo/foo_1
+=========
diff --git a/tests/roots/test-builder-dirhtml/foo/foo_2.rst b/tests/roots/test-builder-dirhtml/foo/foo_2.rst
new file mode 100644
index 000000000..fae7f26ef
--- /dev/null
+++ b/tests/roots/test-builder-dirhtml/foo/foo_2.rst
@@ -0,0 +1,4 @@
+.. _foo_2:
+
+foo/foo_2
+=========
diff --git a/tests/roots/test-builder-dirhtml/foo/index.rst b/tests/roots/test-builder-dirhtml/foo/index.rst
new file mode 100644
index 000000000..92d473c6b
--- /dev/null
+++ b/tests/roots/test-builder-dirhtml/foo/index.rst
@@ -0,0 +1,9 @@
+.. _foo:
+
+foo/index
+=========
+
+.. toctree::
+
+ foo_1
+ foo_2
diff --git a/tests/roots/test-builder-dirhtml/index.rst b/tests/roots/test-builder-dirhtml/index.rst
new file mode 100644
index 000000000..274e17793
--- /dev/null
+++ b/tests/roots/test-builder-dirhtml/index.rst
@@ -0,0 +1,9 @@
+.. _index:
+
+index
+=====
+
+.. toctree::
+
+ foo/index
+ bar
diff --git a/tests/roots/test-ext-autodoc/target/__init__.py b/tests/roots/test-ext-autodoc/target/__init__.py
index f6970a36c..e28eeef8a 100644
--- a/tests/roots/test-ext-autodoc/target/__init__.py
+++ b/tests/roots/test-ext-autodoc/target/__init__.py
@@ -110,6 +110,10 @@ class Outer(object):
factory = dict
+class InnerChild(Outer.Inner):
+ """InnerChild docstring"""
+
+
class DocstringSig(object):
def meth(self):
"""meth(FOO, BAR=1) -> BAZ
diff --git a/tests/roots/test-ext-autodoc/target/pep570.py b/tests/roots/test-ext-autodoc/target/pep570.py
index 904692eeb..1a77eae93 100644
--- a/tests/roots/test-ext-autodoc/target/pep570.py
+++ b/tests/roots/test-ext-autodoc/target/pep570.py
@@ -1,5 +1,11 @@
-def foo(a, b, /, c, d):
+def foo(*, a, b):
pass
-def bar(a, b, /):
+def bar(a, b, /, c, d):
+ pass
+
+def baz(a, /, *, b):
+ pass
+
+def qux(a, b, /):
pass
diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py
index 842530c13..ab5bfb624 100644
--- a/tests/roots/test-ext-autodoc/target/typehints.py
+++ b/tests/roots/test-ext-autodoc/target/typehints.py
@@ -18,7 +18,27 @@ class Math:
# type: (int, int) -> int
return a - b
+ def nothing(self):
+ # type: () -> None
+ pass
+
+ def horse(self,
+ a, # type: str
+ b, # type: int
+ ):
+ # type: (...) -> None
+ return
+
def complex_func(arg1, arg2, arg3=None, *args, **kwargs):
# type: (str, List[int], Tuple[int, Union[str, Unknown]], *str, **str) -> None
pass
+
+
+def missing_attr(c,
+ a, # type: str
+ b=None # type: Optional[str]
+ ):
+ # type: (...) -> str
+ return a + (b or "")
+
diff --git a/tests/roots/test-html_scaled_image_link/conf.py b/tests/roots/test-html_scaled_image_link/conf.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/roots/test-html_scaled_image_link/conf.py
diff --git a/tests/roots/test-html_scaled_image_link/img.png b/tests/roots/test-html_scaled_image_link/img.png
new file mode 100644
index 000000000..a97e86d66
--- /dev/null
+++ b/tests/roots/test-html_scaled_image_link/img.png
Binary files differ
diff --git a/tests/roots/test-html_scaled_image_link/index.rst b/tests/roots/test-html_scaled_image_link/index.rst
new file mode 100644
index 000000000..0e4794058
--- /dev/null
+++ b/tests/roots/test-html_scaled_image_link/index.rst
@@ -0,0 +1,11 @@
+test-html_scaled_image_link
+===========================
+
+.. image:: img.png
+
+.. image:: img.png
+ :scale: 50%
+
+.. image:: img.png
+ :scale: 50%
+ :class: no-scaled-link
diff --git a/tests/roots/test-inheritance/diagram_w_nested_classes.rst b/tests/roots/test-inheritance/diagram_w_nested_classes.rst
new file mode 100644
index 000000000..7fa02175f
--- /dev/null
+++ b/tests/roots/test-inheritance/diagram_w_nested_classes.rst
@@ -0,0 +1,5 @@
+Diagram with Nested Classes
+===========================
+
+.. inheritance-diagram::
+ dummy.test_nested
diff --git a/tests/roots/test-inheritance/dummy/test_nested.py b/tests/roots/test-inheritance/dummy/test_nested.py
new file mode 100644
index 000000000..1e732aab5
--- /dev/null
+++ b/tests/roots/test-inheritance/dummy/test_nested.py
@@ -0,0 +1,12 @@
+"""
+ Test with nested classes.
+"""
+
+
+class A(object):
+ class B(object):
+ pass
+
+
+class C(A.B):
+ pass
diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py
index 52e9bf5c5..e6b4cc5b6 100644
--- a/tests/test_autodoc.py
+++ b/tests/test_autodoc.py
@@ -685,6 +685,7 @@ def test_autodoc_ignore_module_all(app):
assert list(filter(lambda l: 'class::' in l, actual)) == [
'.. py:class:: Class(arg)',
'.. py:class:: CustomDict',
+ '.. py:class:: InnerChild',
'.. py:class:: InstAttCls()',
'.. py:class:: Outer',
' .. py:class:: Outer.Inner',
@@ -775,6 +776,18 @@ def test_autodoc_inner_class(app):
' ',
]
+ options['show-inheritance'] = True
+ actual = do_autodoc(app, 'class', 'target.InnerChild', options)
+ assert list(actual) == [
+ '',
+ '.. py:class:: InnerChild',
+ ' :module: target', '',
+ ' Bases: :class:`target.Outer.Inner`',
+ '',
+ ' InnerChild docstring',
+ ' '
+ ]
+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_classmethod(app):
diff --git a/tests/test_build_dirhtml.py b/tests/test_build_dirhtml.py
new file mode 100644
index 000000000..e89e6888d
--- /dev/null
+++ b/tests/test_build_dirhtml.py
@@ -0,0 +1,48 @@
+"""
+ test_build_dirhtml
+ ~~~~~~~~~~~~~~~~~~
+
+ Test dirhtml builder.
+
+ :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import posixpath
+
+import pytest
+
+from sphinx.util.inventory import InventoryFile
+
+
+@pytest.mark.sphinx(buildername='dirhtml', testroot='builder-dirhtml')
+def test_dirhtml(app, status, warning):
+ app.build()
+
+ assert (app.outdir / 'index.html').exists()
+ assert (app.outdir / 'foo/index.html').exists()
+ assert (app.outdir / 'foo/foo_1/index.html').exists()
+ assert (app.outdir / 'foo/foo_2/index.html').exists()
+ assert (app.outdir / 'bar/index.html').exists()
+
+ content = (app.outdir / 'index.html').read_text()
+ assert 'href="foo/"' in content
+ assert 'href="foo/foo_1/"' in content
+ assert 'href="foo/foo_2/"' in content
+ assert 'href="bar/"' in content
+
+ # objects.inv (refs: #7095)
+ with (app.outdir / 'objects.inv').open('rb') as f:
+ invdata = InventoryFile.load(f, 'path/to', posixpath.join)
+
+ assert 'index' in invdata.get('std:doc')
+ assert ('Python', '', 'path/to/', '-') == invdata['std:doc']['index']
+
+ assert 'foo/index' in invdata.get('std:doc')
+ assert ('Python', '', 'path/to/foo/', '-') == invdata['std:doc']['foo/index']
+
+ assert 'index' in invdata.get('std:label')
+ assert ('Python', '', 'path/to/#index', '-') == invdata['std:label']['index']
+
+ assert 'foo' in invdata.get('std:label')
+ assert ('Python', '', 'path/to/foo/#foo', 'foo/index') == invdata['std:label']['foo']
diff --git a/tests/test_build_html.py b/tests/test_build_html.py
index ac44ca7e4..25ef87644 100644
--- a/tests/test_build_html.py
+++ b/tests/test_build_html.py
@@ -1542,3 +1542,22 @@ def test_validate_html_static_path(app):
]
validate_html_static_path(app, app.config)
assert app.config.html_static_path == ['_static']
+
+
+@pytest.mark.sphinx(testroot='html_scaled_image_link')
+def test_html_scaled_image_link(app):
+ app.build()
+ context = (app.outdir / 'index.html').text()
+
+ # no scaled parameters
+ assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context)
+
+ # scaled_image_link
+ assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">'
+ '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" /></a>',
+ context)
+
+ # no-scaled-link class disables the feature
+ assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"'
+ ' src="_images/img.png" style="[^"]+" />',
+ context)
diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py
index 581856502..6726c7bbe 100644
--- a/tests/test_domain_js.py
+++ b/tests/test_domain_js.py
@@ -14,7 +14,12 @@ 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.domains.javascript import JavaScriptDomain
+from sphinx.testing import restructuredtext
from sphinx.testing.util import assert_node
@@ -158,3 +163,51 @@ def test_get_full_qualified_name():
kwargs = {'js:module': 'module1', 'js:object': 'Class'}
node = nodes.reference(reftarget='func', **kwargs)
assert domain.get_full_qualified_name(node) == 'module1.Class.func'
+
+
+def test_js_module(app):
+ text = ".. js:module:: sphinx"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (nodes.target,
+ addnodes.index))
+ assert_node(doctree[0], nodes.target, ids=["module-sphinx"])
+ assert_node(doctree[1], addnodes.index,
+ entries=[("single", "sphinx (module)", "module-sphinx", "", None)])
+
+
+def test_js_function(app):
+ text = ".. js:function:: sum(a, b)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "sum"],
+ desc_parameterlist)],
+ [desc_content, ()])]))
+ assert_node(doctree[1][0][1], [desc_parameterlist, ([desc_parameter, "a"],
+ [desc_parameter, "b"])])
+ assert_node(doctree[0], addnodes.index,
+ entries=[("single", "sum() (built-in function)", "sum", "", None)])
+ assert_node(doctree[1], addnodes.desc, domain="js", objtype="function", noindex=False)
+
+
+def test_js_class(app):
+ text = ".. js:class:: Application"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_annotation, "class "],
+ [desc_name, "Application"],
+ [desc_parameterlist, ()])],
+ [desc_content, ()])]))
+ assert_node(doctree[0], addnodes.index,
+ entries=[("single", "Application() (class)", "Application", "", None)])
+ assert_node(doctree[1], addnodes.desc, domain="js", objtype="class", noindex=False)
+
+
+def test_js_data(app):
+ text = ".. js:data:: name"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, desc_name, "name"],
+ [desc_content, ()])]))
+ assert_node(doctree[0], addnodes.index,
+ entries=[("single", "name (global variable or constant)", "name", "", None)])
+ assert_node(doctree[1], addnodes.desc, domain="js", objtype="data", noindex=False)
diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py
index f78c1e9d8..8e9228537 100644
--- a/tests/test_domain_py.py
+++ b/tests/test_domain_py.py
@@ -8,6 +8,7 @@
:license: BSD, see LICENSE for details.
"""
+import sys
from unittest.mock import Mock
import pytest
@@ -168,7 +169,7 @@ def test_domain_py_objects(app, status, warning):
def test_resolve_xref_for_properties(app, status, warning):
app.builder.build_all()
- content = (app.outdir / 'module.html').text()
+ content = (app.outdir / 'module.html').read_text()
assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"'
' title="module_a.submodule.ModTopLevel.prop">'
'<code class="xref py py-attr docutils literal notranslate"><span class="pre">'
@@ -241,7 +242,73 @@ def test_pyfunction_signature(app):
desc_content)]))
assert_node(doctree[1], addnodes.desc, desctype="function",
domain="py", objtype="function", noindex=False)
- assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, "name: str"])
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, desc_parameter, ("name",
+ ": str")])
+
+
+def test_pyfunction_signature_full(app):
+ text = (".. py:function:: hello(a: str, b = 1, *args: str, "
+ "c: bool = True, **kwargs: str) -> str")
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index,
+ [desc, ([desc_signature, ([desc_name, "hello"],
+ desc_parameterlist,
+ [desc_returns, "str"])],
+ desc_content)]))
+ assert_node(doctree[1], addnodes.desc, desctype="function",
+ domain="py", objtype="function", noindex=False)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, ("a",
+ ": str")],
+ [desc_parameter, ("b",
+ "=1")],
+ [desc_parameter, ("*args",
+ ": str")],
+ [desc_parameter, ("c",
+ ": bool",
+ " = True")],
+ [desc_parameter, ("**kwargs",
+ ": str")])])
+
+
+@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
+def test_pyfunction_signature_full_py38(app):
+ # case: separator at head
+ text = ".. py:function:: hello(*, a)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ("*",
+ [desc_parameter, ("a",
+ "=None")])])
+
+ # case: separator in the middle
+ text = ".. py:function:: hello(a, /, b, *, c)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, "a"],
+ "/",
+ [desc_parameter, "b"],
+ "*",
+ [desc_parameter, ("c",
+ "=None")])])
+
+ # case: separator in the middle (2)
+ text = ".. py:function:: hello(a, /, *, b)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, "a"],
+ "/",
+ "*",
+ [desc_parameter, ("b",
+ "=None")])])
+
+ # case: separator at tail
+ text = ".. py:function:: hello(a, /)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, "a"],
+ "/")])
def test_optional_pyfunction_signature(app):
diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py
index a9af8a272..b90772f6e 100644
--- a/tests/test_ext_autodoc_configs.py
+++ b/tests/test_ext_autodoc_configs.py
@@ -482,9 +482,17 @@ def test_autodoc_typehints_signature(app):
' :module: target.typehints',
' ',
' ',
+ ' .. py:method:: Math.horse(a: str, b: int) -> None',
+ ' :module: target.typehints',
+ ' ',
+ ' ',
' .. py:method:: Math.incr(a: int, b: int = 1) -> int',
' :module: target.typehints',
' ',
+ ' ',
+ ' .. py:method:: Math.nothing() -> None',
+ ' :module: target.typehints',
+ ' ',
'',
'.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, '
'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None',
@@ -497,6 +505,10 @@ def test_autodoc_typehints_signature(app):
'',
'.. py:function:: incr(a: int, b: int = 1) -> int',
' :module: target.typehints',
+ '',
+ '',
+ '.. py:function:: missing_attr(c, a: str, b: Optional[str] = None) -> str',
+ ' :module: target.typehints',
''
]
@@ -521,9 +533,17 @@ def test_autodoc_typehints_none(app):
' :module: target.typehints',
' ',
' ',
+ ' .. py:method:: Math.horse(a, b)',
+ ' :module: target.typehints',
+ ' ',
+ ' ',
' .. py:method:: Math.incr(a, b=1)',
' :module: target.typehints',
' ',
+ ' ',
+ ' .. py:method:: Math.nothing()',
+ ' :module: target.typehints',
+ ' ',
'',
'.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)',
' :module: target.typehints',
@@ -535,6 +555,10 @@ def test_autodoc_typehints_none(app):
'',
'.. py:function:: incr(a, b=1)',
' :module: target.typehints',
+ '',
+ '',
+ '.. py:function:: missing_attr(c, a, b=None)',
+ ' :module: target.typehints',
''
]
diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py
index baeaa401e..3125f2c6e 100644
--- a/tests/test_ext_inheritance_diagram.py
+++ b/tests/test_ext_inheritance_diagram.py
@@ -132,6 +132,14 @@ def test_inheritance_diagram(app, status, warning):
('dummy.test.A', 'dummy.test.A', [], None),
]
+ # inheritance diagram involving a base class nested within another class
+ for cls in graphs['diagram_w_nested_classes'].class_info:
+ assert cls in [
+ ('dummy.test_nested.A', 'dummy.test_nested.A', [], None),
+ ('dummy.test_nested.C', 'dummy.test_nested.C', ['dummy.test_nested.A.B'], None),
+ ('dummy.test_nested.A.B', 'dummy.test_nested.A.B', [], None)
+ ]
+
@pytest.mark.sphinx('html', testroot='ext-inheritance_diagram')
@pytest.mark.usefixtures('if_graphviz_found')
diff --git a/tests/test_highlighting.py b/tests/test_highlighting.py
index 525af2547..c2b470a6b 100644
--- a/tests/test_highlighting.py
+++ b/tests/test_highlighting.py
@@ -40,7 +40,7 @@ class ComplainOnUnhighlighted(PygmentsBridge):
def test_add_lexer(app, status, warning):
- app.add_lexer('test', MyLexer())
+ app.add_lexer('test', MyLexer)
bridge = PygmentsBridge('html')
ret = bridge.highlight_block('ab', 'test')
diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py
index af7e34a86..d195e5c6f 100644
--- a/tests/test_pycode_ast.py
+++ b/tests/test_pycode_ast.py
@@ -8,6 +8,8 @@
:license: BSD, see LICENSE for details.
"""
+import sys
+
import pytest
from sphinx.pycode import ast
@@ -23,7 +25,7 @@ from sphinx.pycode import ast
("...", "..."), # Ellipsis
("Tuple[int, int]", "Tuple[int, int]"), # Index, Subscript
("lambda x, y: x + y",
- "<function <lambda>>"), # Lambda
+ "lambda x, y: ..."), # Lambda
("[1, 2, 3]", "[1, 2, 3]"), # List
("sys", "sys"), # Name, NameConstant
("1234", "1234"), # Num
@@ -38,3 +40,11 @@ def test_unparse(source, expected):
def test_unparse_None():
assert ast.unparse(None) is None
+
+
+@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
+def test_unparse_py38():
+ source = "lambda x=0, /, y=1, *args, z, **kwargs: x + y + z"
+ expected = "lambda x=0, /, y=1, *args, z, **kwargs: ..."
+ module = ast.parse(source)
+ assert ast.unparse(module.body[0].value) == expected
diff --git a/tests/test_util.py b/tests/test_util.py
index f794c4f74..434d96d3a 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -18,7 +18,7 @@ import sphinx
from sphinx.errors import ExtensionError, PycodeError
from sphinx.testing.util import strip_escseq
from sphinx.util import (
- SkipProgressMessage, display_chunk, encode_uri, ensuredir, get_module_source,
+ SkipProgressMessage, display_chunk, encode_uri, ensuredir,
import_object, parselinenos, progress_message, status_iterator, xmlname_checker
)
from sphinx.util import logging
@@ -61,16 +61,6 @@ def test_display_chunk():
assert display_chunk(('hello', 'sphinx', 'world')) == 'hello .. world'
-def test_get_module_source():
- assert get_module_source('sphinx') == ('file', sphinx.__file__)
-
- # failed to obtain source information from builtin modules
- with pytest.raises(PycodeError):
- get_module_source('builtins')
- with pytest.raises(PycodeError):
- get_module_source('itertools')
-
-
def test_import_object():
module = import_object('sphinx')
assert module.__name__ == 'sphinx'
diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py
index 34844c9bf..5c9520370 100644
--- a/tests/test_util_inspect.py
+++ b/tests/test_util_inspect.py
@@ -13,6 +13,7 @@ import datetime
import functools
import sys
import types
+from inspect import Parameter
import pytest
@@ -20,76 +21,6 @@ from sphinx.util import inspect
from sphinx.util.inspect import stringify_signature
-def test_getargspec():
- def func(a, b, c=1, d=2, *e, **f):
- pass
-
- spec = inspect.getargspec(func)
- assert spec.args == ['a', 'b', 'c', 'd']
- assert spec.varargs == 'e'
- assert spec.varkw == 'f'
- assert spec.defaults == (1, 2)
- assert spec.kwonlyargs == []
- assert spec.kwonlydefaults is None
- assert spec.annotations == {}
-
-
-def test_getargspec_partial():
- def func1(a, b, c=1, d=2, *e, **f):
- pass
-
- partial = functools.partial(func1, 10, c=11)
- spec = inspect.getargspec(partial)
- assert spec.args == ['b']
- assert spec.varargs is None
- assert spec.varkw == 'f'
- assert spec.defaults is None
- assert spec.kwonlyargs == ['c', 'd']
- assert spec.kwonlydefaults == {'c': 11, 'd': 2}
- assert spec.annotations == {}
-
-
-def test_getargspec_partial2():
- def fun(a, b, c=1, d=2):
- pass
- p = functools.partial(fun, 10, c=11)
-
- def f_expected(b, *, c=11, d=2):
- pass
- expected = inspect.getargspec(f_expected)
-
- assert expected == inspect.getargspec(p)
-
-
-def test_getargspec_builtin_type():
- with pytest.raises(TypeError):
- inspect.getargspec(int)
-
-
-def test_getargspec_bound_methods():
- def f_expected_unbound(self, arg1, **kwargs):
- pass
- expected_unbound = inspect.getargspec(f_expected_unbound)
-
- def f_expected_bound(arg1, **kwargs):
- pass
- expected_bound = inspect.getargspec(f_expected_bound)
-
- class Foo:
- def method(self, arg1, **kwargs):
- pass
-
- bound_method = Foo().method
-
- @functools.wraps(bound_method)
- def wrapped_bound_method(*args, **kwargs):
- pass
-
- assert expected_unbound == inspect.getargspec(Foo.method)
- assert expected_bound == inspect.getargspec(bound_method)
- assert expected_bound == inspect.getargspec(wrapped_bound_method)
-
-
def test_signature():
# literals
with pytest.raises(TypeError):
@@ -298,17 +229,108 @@ def test_signature_annotations():
@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
+ from target.pep570 import foo, bar, baz, qux
- # case: separator in the middle
+ # case: separator at head
sig = inspect.signature(foo)
+ assert stringify_signature(sig) == '(*, a, b)'
+
+ # case: separator in the middle
+ sig = inspect.signature(bar)
assert stringify_signature(sig) == '(a, b, /, c, d)'
+ sig = inspect.signature(baz)
+ assert stringify_signature(sig) == '(a, /, *, b)'
+
# case: separator at tail
- sig = inspect.signature(bar)
+ sig = inspect.signature(qux)
assert stringify_signature(sig) == '(a, b, /)'
+def test_signature_from_str_basic():
+ signature = '(a, b, *args, c=0, d="blah", **kwargs)'
+ sig = inspect.signature_from_str(signature)
+ assert list(sig.parameters.keys()) == ['a', 'b', 'args', 'c', 'd', 'kwargs']
+ assert sig.parameters['a'].name == 'a'
+ assert sig.parameters['a'].kind == Parameter.POSITIONAL_OR_KEYWORD
+ assert sig.parameters['a'].default == Parameter.empty
+ assert sig.parameters['a'].annotation == Parameter.empty
+ assert sig.parameters['b'].name == 'b'
+ assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD
+ assert sig.parameters['b'].default == Parameter.empty
+ assert sig.parameters['b'].annotation == Parameter.empty
+ assert sig.parameters['args'].name == 'args'
+ assert sig.parameters['args'].kind == Parameter.VAR_POSITIONAL
+ assert sig.parameters['args'].default == Parameter.empty
+ assert sig.parameters['args'].annotation == Parameter.empty
+ assert sig.parameters['c'].name == 'c'
+ assert sig.parameters['c'].kind == Parameter.KEYWORD_ONLY
+ assert sig.parameters['c'].default == '0'
+ assert sig.parameters['c'].annotation == Parameter.empty
+ assert sig.parameters['d'].name == 'd'
+ assert sig.parameters['d'].kind == Parameter.KEYWORD_ONLY
+ assert sig.parameters['d'].default == "'blah'"
+ assert sig.parameters['d'].annotation == Parameter.empty
+ assert sig.parameters['kwargs'].name == 'kwargs'
+ assert sig.parameters['kwargs'].kind == Parameter.VAR_KEYWORD
+ assert sig.parameters['kwargs'].default == Parameter.empty
+ assert sig.parameters['kwargs'].annotation == Parameter.empty
+ assert sig.return_annotation == Parameter.empty
+
+
+def test_signature_from_str_default_values():
+ signature = ('(a=0, b=0.0, c="str", d=b"bytes", e=..., f=True, '
+ 'g=[1, 2, 3], h={"a": 1}, i={1, 2, 3}, '
+ 'j=lambda x, y: None, k=None, l=object(), m=foo.bar.CONSTANT)')
+ sig = inspect.signature_from_str(signature)
+ assert sig.parameters['a'].default == '0'
+ assert sig.parameters['b'].default == '0.0'
+ assert sig.parameters['c'].default == "'str'"
+ assert sig.parameters['d'].default == "b'bytes'"
+ assert sig.parameters['e'].default == '...'
+ assert sig.parameters['f'].default == 'True'
+ assert sig.parameters['g'].default == '[1, 2, 3]'
+ assert sig.parameters['h'].default == "{'a': 1}"
+ assert sig.parameters['i'].default == '{1, 2, 3}'
+ assert sig.parameters['j'].default == 'lambda x, y: ...'
+ assert sig.parameters['k'].default == 'None'
+ assert sig.parameters['l'].default == 'object()'
+ assert sig.parameters['m'].default == 'foo.bar.CONSTANT'
+
+
+def test_signature_from_str_annotations():
+ signature = '(a: int, *args: bytes, b: str = "blah", **kwargs: float) -> None'
+ sig = inspect.signature_from_str(signature)
+ assert list(sig.parameters.keys()) == ['a', 'args', 'b', 'kwargs']
+ assert sig.parameters['a'].annotation == "int"
+ assert sig.parameters['args'].annotation == "bytes"
+ assert sig.parameters['b'].annotation == "str"
+ assert sig.parameters['kwargs'].annotation == "float"
+ assert sig.return_annotation == 'None'
+
+
+def test_signature_from_str_complex_annotations():
+ sig = inspect.signature_from_str('() -> Tuple[str, int, ...]')
+ assert sig.return_annotation == 'Tuple[str, int, ...]'
+
+ sig = inspect.signature_from_str('() -> Callable[[int, int], int]')
+ assert sig.return_annotation == 'Callable[[int, int], int]'
+
+
+@pytest.mark.skipif(sys.version_info < (3, 8),
+ reason='python-3.8 or above is required')
+def test_signature_from_str_positionaly_only_args():
+ sig = inspect.signature_from_str('(a, /, b)')
+ assert list(sig.parameters.keys()) == ['a', 'b']
+ assert sig.parameters['a'].kind == Parameter.POSITIONAL_ONLY
+ assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD
+
+
+def test_signature_from_str_invalid():
+ with pytest.raises(SyntaxError):
+ inspect.signature_from_str('')
+
+
def test_safe_getattr_with_default():
class Foo:
def __getattr__(self, item):
diff --git a/tox.ini b/tox.ini
index 1ac0bff79..7607aac65 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,7 +11,7 @@ description =
du{12,13,14}: Run unit tests with the given version of docutils.
deps =
coverage < 5.0 # refs: https://github.com/sphinx-doc/sphinx/pull/6924
- git+https://github.com/html5lib/html5lib-python ; python_version >= "3.9" # refs: https://github.com/html5lib/html5lib-python/issues/419
+ git+https://github.com/html5lib/html5lib-python # refs: https://github.com/html5lib/html5lib-python/issues/419
du12: docutils==0.12
du13: docutils==0.13.1
du14: docutils==0.14
@@ -20,7 +20,7 @@ deps =
extras =
test
setenv =
- PYTHONWARNINGS = all,ignore::ImportWarning:pkgutil,ignore::ImportWarning:importlib._bootstrap,ignore::ImportWarning:importlib._bootstrap_external,ignore::ImportWarning:pytest_cov.plugin,ignore::DeprecationWarning:site,ignore::DeprecationWarning:_pytest.assertion.rewrite,ignore::DeprecationWarning:_pytest.fixtures,ignore::DeprecationWarning:distutils
+ PYTHONWARNINGS = all,ignore::ImportWarning:importlib._bootstrap_external,ignore::DeprecationWarning:site,ignore::DeprecationWarning:distutils
commands=
pytest --durations 25 {posargs}
@@ -28,10 +28,12 @@ commands=
basepython = python3
description =
Run style checks.
+whitelist_externals =
+ flake8
extras =
lint
commands =
- flake8
+ flake8 {posargs}
[testenv:pylint]
basepython = python3