diff --git a/CHANGES b/CHANGES
index 3c03f06c9..eadace6dc 100644
--- a/CHANGES
+++ b/CHANGES
@@ -18,16 +18,23 @@ Features added
--------------
* #2076: autodoc: Allow overriding of exclude-members in skip-member function
+* #2024: autosummary: Add :confval:`autosummary_filename_map` to avoid conflict
+ of filenames between two object with different case
* #7849: html: Add :confval:`html_codeblock_linenos_style` to change the style
of line numbers for code-blocks
* #7853: C and C++, support parameterized GNU style attributes.
* #7888: napoleon: Add aliases Warn and Raise.
+* #7690: napoleon: parse type strings and make them hyperlinks as possible. The
+ conversion rule can be updated via :confval:`napoleon_type_aliases`
* C, added :rst:dir:`c:alias` directive for inserting copies
of existing declarations.
* #7745: html: inventory is broken if the docname contains a space
+* #7991: html search: Allow searching for numbers
* #7902: html theme: Add a new option :confval:`globaltoc_maxdepth` to control
the behavior of globaltoc in sidebar
* #7840: i18n: Optimize the dependencies check on bootstrap
+* #5208: linkcheck: Support checks for local links
+* #5090: setuptools: Link verbosity to distutils' -v and -q option
* #7052: add ``:noindexentry:`` to the Python, C, C++, and Javascript domains.
Update the documentation to better reflect the relationship between this option
and the ``:noindex:`` option.
@@ -37,6 +44,7 @@ Features added
The warnings printed from this functionality can be suppressed by setting
:confval:`c_warn_on_allowed_pre_v3`` to ``True``.
The functionality is immediately deprecated.
+* #7999: C, add support for named variadic macro arguments.
Bugs fixed
----------
@@ -47,6 +55,7 @@ Bugs fixed
* #7901: autodoc: type annotations for overloaded functions are not resolved
* #904: autodoc: An instance attribute cause a crash of autofunction directive
* #1362: autodoc: ``private-members`` option does not work for class attributes
+* #7983: autodoc: Generator type annotation is wrongly rendered in py36
* #7839: autosummary: cannot handle umlauts in function names
* #7865: autosummary: Failed to extract summary line when abbreviations found
* #7866: autosummary: Failed to extract correct summary line when docstring
@@ -61,8 +70,13 @@ Bugs fixed
* #7691: linkcheck: HEAD requests are not used for checking
* #4888: i18n: Failed to add an explicit title to ``:ref:`` role on translation
* #7928: py domain: failed to resolve a type annotation for the attribute
+* #8008: py domain: failed to parse a type annotation containing ellipsis
+* #7994: std domain: option directive does not generate old node_id compatible
+ with 2.x or older
* #7968: i18n: The content of ``math`` directive is interpreted as reST on
translation
+* #7768: i18n: The ``root`` element for :confval:`figure_language_filename` is
+ not a path that user specifies in the document
* #7993: texinfo: TypeError is raised for nested object descriptions
* #7993: texinfo: a warning not supporting desc_signature_line node is shown
* #7869: :rst:role:`abbr` role without an explanation will show the explanation
@@ -71,6 +85,7 @@ Bugs fixed
nothing.
* #7619: Duplicated node IDs are generated if node has multiple IDs
* #2050: Symbols sections are appeared twice in the index page
+* #8017: Fix circular import in sphinx.addnodes
Testing
--------
diff --git a/doc/contents.rst b/doc/contents.rst
index 17a3d4b54..eb6946292 100644
--- a/doc/contents.rst
+++ b/doc/contents.rst
@@ -10,7 +10,6 @@ Sphinx documentation contents
development/index
man/index
- theming
templating
latex
extdev/index
diff --git a/doc/development/builders.rst b/doc/development/builders.rst
new file mode 100644
index 000000000..bb6777023
--- /dev/null
+++ b/doc/development/builders.rst
@@ -0,0 +1,34 @@
+Configuring builders
+====================
+
+Discover builders by entry point
+--------------------------------
+
+.. versionadded:: 1.6
+
+:term:`builder` extensions can be discovered by means of `entry points`_ so
+that they do not have to be listed in the :confval:`extensions` configuration
+value.
+
+Builder extensions should define an entry point in the ``sphinx.builders``
+group. The name of the entry point needs to match your builder's
+:attr:`~.Builder.name` attribute, which is the name passed to the
+:option:`sphinx-build -b` option. The entry point value should equal the
+dotted name of the extension module. Here is an example of how an entry point
+for 'mybuilder' can be defined in the extension's ``setup.py``
+
+.. code-block:: python
+
+ setup(
+ # ...
+ entry_points={
+ 'sphinx.builders': [
+ 'mybuilder = my.extension.module',
+ ],
+ }
+ )
+
+Note that it is still necessary to register the builder using
+:meth:`~.Sphinx.add_builder` in the extension's :func:`setup` function.
+
+.. _entry points: https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
diff --git a/doc/development/index.rst b/doc/development/index.rst
index 6a3406a20..04918acd6 100644
--- a/doc/development/index.rst
+++ b/doc/development/index.rst
@@ -10,4 +10,12 @@ wish to use Sphinx with existing extensions, refer to :doc:`/usage/index`.
.. toctree::
:maxdepth: 2
+ overview
tutorials/index
+ builders
+
+.. toctree::
+ :caption: Theming
+ :maxdepth: 2
+
+ theming
diff --git a/doc/development/overview.rst b/doc/development/overview.rst
new file mode 100644
index 000000000..ad474999a
--- /dev/null
+++ b/doc/development/overview.rst
@@ -0,0 +1,32 @@
+Developing extensions overview
+==============================
+
+This page contains general information about developing Sphinx extensions.
+
+Make an extension depend on another extension
+---------------------------------------------
+
+Sometimes your extension depends on the functionality of another
+Sphinx extension. Most Sphinx extensions are activated in a
+project's :file:`conf.py` file, but this is not available to you as an
+extension developer.
+
+.. module:: sphinx.application
+ :noindex:
+
+To ensure that another extension is activated as a part of your own extension,
+use the :meth:`Sphinx.setup_extension` method. This will
+activate another extension at run-time, ensuring that you have access to its
+functionality.
+
+For example, the following code activates the ``recommonmark`` extension:
+
+.. code-block:: python
+
+ def setup(app):
+ app.setup_extension("recommonmark")
+
+.. note::
+
+ Since your extension will depend on another, make sure to include
+ it as a part of your extension's installation requirements.
diff --git a/doc/development/theming.rst b/doc/development/theming.rst
new file mode 100644
index 000000000..5de10158a
--- /dev/null
+++ b/doc/development/theming.rst
@@ -0,0 +1,336 @@
+HTML theme development
+======================
+
+.. versionadded:: 0.6
+
+.. note::
+
+ This document provides information about creating your own theme. If you
+ simply wish to use a pre-existing HTML themes, refer to
+ :doc:`/usage/theming`.
+
+Sphinx supports changing the appearance of its HTML output via *themes*. A
+theme is a collection of HTML templates, stylesheet(s) and other static files.
+Additionally, it has a configuration file which specifies from which theme to
+inherit, which highlighting style to use, and what options exist for customizing
+the theme's look and feel.
+
+Themes are meant to be project-unaware, so they can be used for different
+projects without change.
+
+.. note::
+
+ See :ref:`dev-extensions` for more information that may
+ be helpful in developing themes.
+
+
+Creating themes
+---------------
+
+Themes take the form of either a directory or a zipfile (whose name is the
+theme name), containing the following:
+
+* A :file:`theme.conf` file.
+* HTML templates, if needed.
+* A ``static/`` directory containing any static files that will be copied to the
+ output static directory on build. These can be images, styles, script files.
+
+The :file:`theme.conf` file is in INI format [1]_ (readable by the standard
+Python :mod:`ConfigParser` module) and has the following structure:
+
+.. sourcecode:: ini
+
+ [theme]
+ inherit = base theme
+ stylesheet = main CSS name
+ pygments_style = stylename
+ sidebars = localtoc.html, relations.html, sourcelink.html, searchbox.html
+
+ [options]
+ variable = default value
+
+* The **inherit** setting gives the name of a "base theme", or ``none``. The
+ base theme will be used to locate missing templates (most themes will not have
+ to supply most templates if they use ``basic`` as the base theme), its options
+ will be inherited, and all of its static files will be used as well. If you
+ want to also inherit the stylesheet, include it via CSS' ``@import`` in your
+ own.
+
+* The **stylesheet** setting gives the name of a CSS file which will be
+ referenced in the HTML header. If you need more than one CSS file, either
+ include one from the other via CSS' ``@import``, or use a custom HTML template
+ that adds ```` tags as necessary. Setting the
+ :confval:`html_style` config value will override this setting.
+
+* The **pygments_style** setting gives the name of a Pygments style to use for
+ highlighting. This can be overridden by the user in the
+ :confval:`pygments_style` config value.
+
+* The **pygments_dark_style** setting gives the name of a Pygments style to use
+ for highlighting when the CSS media query ``(prefers-color-scheme: dark)``
+ evaluates to true. It is injected into the page using
+ :meth:`~Sphinx.add_css_file()`.
+
+* The **sidebars** setting gives the comma separated list of sidebar templates
+ for constructing sidebars. This can be overridden by the user in the
+ :confval:`html_sidebars` config value.
+
+* The **options** section contains pairs of variable names and default values.
+ These options can be overridden by the user in :confval:`html_theme_options`
+ and are accessible from all templates as ``theme_``.
+
+.. versionadded:: 1.7
+ sidebar settings
+
+
+.. _distribute-your-theme:
+
+Distribute your theme as a Python package
+-----------------------------------------
+
+As a way to distribute your theme, you can use Python package. Python package
+brings to users easy setting up ways.
+
+To distribute your theme as a Python package, please define an entry point
+called ``sphinx.html_themes`` in your ``setup.py`` file, and write a ``setup()``
+function to register your themes using ``add_html_theme()`` API in it::
+
+ # 'setup.py'
+ setup(
+ ...
+ entry_points = {
+ 'sphinx.html_themes': [
+ 'name_of_theme = your_package',
+ ]
+ },
+ ...
+ )
+
+ # 'your_package.py'
+ from os import path
+
+ def setup(app):
+ app.add_html_theme('name_of_theme', path.abspath(path.dirname(__file__)))
+
+If your theme package contains two or more themes, please call
+``add_html_theme()`` twice or more.
+
+.. versionadded:: 1.2
+ 'sphinx_themes' entry_points feature.
+
+.. deprecated:: 1.6
+ ``sphinx_themes`` entry_points has been deprecated.
+
+.. versionadded:: 1.6
+ ``sphinx.html_themes`` entry_points feature.
+
+
+Templating
+----------
+
+The :doc:`guide to templating ` is helpful if you want to write your
+own templates. What is important to keep in mind is the order in which Sphinx
+searches for templates:
+
+* First, in the user's ``templates_path`` directories.
+* Then, in the selected theme.
+* Then, in its base theme, its base's base theme, etc.
+
+When extending a template in the base theme with the same name, use the theme
+name as an explicit directory: ``{% extends "basic/layout.html" %}``. From a
+user ``templates_path`` template, you can still use the "exclamation mark"
+syntax as described in the templating document.
+
+
+.. _theming-static-templates:
+
+Static templates
+~~~~~~~~~~~~~~~~
+
+Since theme options are meant for the user to configure a theme more easily,
+without having to write a custom stylesheet, it is necessary to be able to
+template static files as well as HTML files. Therefore, Sphinx supports
+so-called "static templates", like this:
+
+If the name of a file in the ``static/`` directory of a theme (or in the user's
+static path, for that matter) ends with ``_t``, it will be processed by the
+template engine. The ``_t`` will be left from the final file name. For
+example, the *classic* theme has a file ``static/classic.css_t`` which uses
+templating to put the color options into the stylesheet. When a documentation
+is built with the classic theme, the output directory will contain a
+``_static/classic.css`` file where all template tags have been processed.
+
+
+Use custom page metadata in HTML templates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Any key / value pairs in :doc:`field lists `
+that are placed *before* the page's title will be available to the Jinja
+template when building the page within the :data:`meta` attribute. For example,
+if a page had the following text before its first title:
+
+.. code-block:: rst
+
+ :mykey: My value
+
+ My first title
+ --------------
+
+Then it could be accessed within a Jinja template like so:
+
+.. code-block:: jinja
+
+ {%- if meta is mapping %}
+ {{ meta.get("mykey") }}
+ {%- endif %}
+
+Note the check that ``meta`` is a dictionary ("mapping" in Jinja
+terminology) to ensure that using it in this way is valid.
+
+
+Defining custom template functions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes it is useful to define your own function in Python that you wish to
+then use in a template. For example, if you'd like to insert a template value
+with logic that depends on the user's configuration in the project, or if you'd
+like to include non-trivial checks and provide friendly error messages for
+incorrect configuration in the template.
+
+To define your own template function, you'll need to define two functions
+inside your module:
+
+* A **page context event handler** (or **registration**) function. This is
+ connected to the :class:`.Sphinx` application via an event callback.
+* A **template function** that you will use in your Jinja template.
+
+First, define the registration function, which accepts the arguments for
+:event:`html-page-context`.
+
+Within the registration function, define the template function that you'd like to use
+within Jinja. The template function should return a string or Python objects (lists,
+dictionaries) with strings inside that Jinja uses in the templating process
+
+.. note::
+
+ The template function will have access to all of the variables that
+ are passed to the registration function.
+
+At the end of the registration function, add the template function to the
+Sphinx application's context with ``context['template_func'] = template_func``.
+
+Finally, in your extension's ``setup()`` function, add your registration
+function as a callback for :event:`html-page-context`.
+
+.. code-block:: python
+
+ # The registration function
+ def setup_my_func(app, pagename, templatename, context, doctree):
+ # The template function
+ def my_func(mystring):
+ return "Your string is %s" % mystring
+ # Add it to the page's context
+ context['my_func'] = my_func
+
+ # Your extension's setup function
+ def setup(app):
+ app.connect("html-page-context", setup_my_func)
+
+Now, you will have access to this function in jinja like so:
+
+.. code-block:: jinja
+
+
+ {{ my_func("some string") }}
+
+
+
+Add your own static files to the build assets
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you are packaging your own build assets with an extension
+(e.g., a CSS or JavaScript file), you need to ensure that they are placed
+in the ``_static/`` folder of HTML outputs. To do so, you may copy them directly
+into a build's ``_static/`` folder at build time, generally via an event hook.
+Here is some sample code to accomplish this:
+
+.. code-block:: python
+
+ def copy_custom_files(app, exc):
+ if app.builder.format == 'html' and not exc:
+ staticdir = path.join(app.builder.outdir, '_static')
+ copy_asset_file('path/to/myextension/_static/myjsfile.js', staticdir)
+
+ def setup(app):
+ app.connect('builder-inited', copy_custom_files)
+
+
+Inject JavaScript based on user configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If your extension makes use of JavaScript, it can be useful to allow users
+to control its behavior using their Sphinx configuration. However, this can
+be difficult to do if your JavaScript comes in the form of a static library
+(which will not be built with Jinja).
+
+There are two ways to inject variables into the JavaScript space based on user
+configuration.
+
+First, you may append ``_t`` to the end of any static files included with your
+extension. This will cause Sphinx to process these files with the templating
+engine, allowing you to embed variables and control behavior.
+
+For example, the following JavaScript structure:
+
+.. code-block:: bash
+
+ mymodule/
+ ├── _static
+ │ └── myjsfile.js_t
+ └── mymodule.py
+
+Will result in the following static file placed in your HTML's build output:
+
+.. code-block:: bash
+
+ _build/
+ └── html
+ └── _static
+ └── myjsfile.js
+
+See :ref:`theming-static-templates` for more information.
+
+Second, you may use the :meth:`Sphinx.add_js_file` method without pointing it
+to a file. Normally, this method is used to insert a new JavaScript file
+into your site. However, if you do *not* pass a file path, but instead pass
+a string to the "body" argument, then this text will be inserted as JavaScript
+into your site's head. This allows you to insert variables into your project's
+JavaScript from Python.
+
+For example, the following code will read in a user-configured value and then
+insert this value as a JavaScript variable, which your extension's JavaScript
+code may use:
+
+.. code-block:: python
+
+ # This function reads in a variable and inserts it into JavaScript
+ def add_js_variable(app):
+ # This is a configuration that you've specified for users in `conf.py`
+ js_variable = app.config['my_javascript_variable']
+ js_text = "var my_variable = '%s';" % js_variable
+ app.add_js_file(None, body=js_text)
+ # We connect this function to the step after the builder is initialized
+ def setup(app):
+ # Tell Sphinx about this configuration variable
+ app.add_config_value('my_javascript_variable')
+ # Run the function after the builder is initialized
+ app.connect('builder-inited', add_js_variable)
+
+As a result, in your theme you can use code that depends on the presence of
+this variable. Users can control the variable's value by defining it in their
+:file:`conf.py` file.
+
+
+.. [1] It is not an executable Python file, as opposed to :file:`conf.py`,
+ because that would pose an unnecessary security risk if themes are
+ shared.
diff --git a/doc/development/tutorials/index.rst b/doc/development/tutorials/index.rst
index a79e6a8b6..be126b3ca 100644
--- a/doc/development/tutorials/index.rst
+++ b/doc/development/tutorials/index.rst
@@ -1,8 +1,11 @@
+.. _extension-tutorials-index:
+
Extension tutorials
===================
Refer to the following tutorials to get started with extension development.
+
.. toctree::
:caption: Directive tutorials
:maxdepth: 1
diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst
index 266da52b7..ad04951f3 100644
--- a/doc/extdev/index.rst
+++ b/doc/extdev/index.rst
@@ -3,54 +3,41 @@
Developing extensions for Sphinx
================================
-Since many projects will need special features in their documentation, Sphinx is
-designed to be extensible on several levels.
+Since many projects will need special features in their documentation, Sphinx
+is designed to be extensible on several levels.
-This is what you can do in an extension: First, you can add new
-:term:`builder`\s to support new output formats or actions on the parsed
-documents. Then, it is possible to register custom reStructuredText roles and
-directives, extending the markup. And finally, there are so-called "hook
-points" at strategic places throughout the build process, where an extension can
-register a hook and run specialized code.
+Here are a few things you can do in an extension:
-An extension is simply a Python module. When an extension is loaded, Sphinx
-imports this module and executes its ``setup()`` function, which in turn
-notifies Sphinx of everything the extension offers -- see the extension tutorial
-for examples.
+* Add new :term:`builder`\s to support new output formats or actions on the
+ parsed documents.
+* Register custom reStructuredText roles and directives, extending the markup
+ using the :doc:`markupapi`.
+* Add custom code to so-called "hook points" at strategic places throughout the
+ build process, allowing you to register a hook and run specialized code.
+ For example, see the :ref:`events`.
-The configuration file itself can be treated as an extension if it contains a
-``setup()`` function. All other extensions to load must be listed in the
-:confval:`extensions` configuration value.
+An extension is simply a Python module with a ``setup()`` function. A user
+activates the extension by placing the extension's module name
+(or a sub-module) in their :confval:`extensions` configuration value.
-Discovery of builders by entry point
-------------------------------------
+When :program:`sphinx-build` is executed, Sphinx will attempt to import each
+module that is listed, and execute ``yourmodule.setup(app)``. This
+function is used to prepare the extension (e.g., by executing Python code),
+linking resources that Sphinx uses in the build process (like CSS or HTML
+files), and notifying Sphinx of everything the extension offers (such
+as directive or role definitions). The ``app`` argument is an instance of
+:class:`.Sphinx` and gives you control over most aspects of the Sphinx build.
-.. versionadded:: 1.6
+.. note::
-:term:`builder` extensions can be discovered by means of `entry points`_ so
-that they do not have to be listed in the :confval:`extensions` configuration
-value.
+ The configuration file itself can be treated as an extension if it
+ contains a ``setup()`` function. All other extensions to load must be
+ listed in the :confval:`extensions` configuration value.
-Builder extensions should define an entry point in the ``sphinx.builders``
-group. The name of the entry point needs to match your builder's
-:attr:`~.Builder.name` attribute, which is the name passed to the
-:option:`sphinx-build -b` option. The entry point value should equal the
-dotted name of the extension module. Here is an example of how an entry point
-for 'mybuilder' can be defined in the extension's ``setup.py``::
-
- setup(
- # ...
- entry_points={
- 'sphinx.builders': [
- 'mybuilder = my.extension.module',
- ],
- }
- )
-
-Note that it is still necessary to register the builder using
-:meth:`~.Sphinx.add_builder` in the extension's :func:`setup` function.
-
-.. _entry points: https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
+The rest of this page describes some high-level aspects of developing
+extensions and various parts of Sphinx's behavior that you can control.
+For some examples of how extensions can be built and used to control different
+parts of Sphinx, see the :ref:`extension-tutorials-index`.
.. _important-objects:
@@ -192,6 +179,11 @@ as metadata of the extension. Metadata keys currently recognized are:
APIs used for writing extensions
--------------------------------
+These sections provide a more complete description of the tools at your
+disposal when developing Sphinx extensions. Some are core to Sphinx
+(such as the :doc:`appapi`) while others trigger specific behavior
+(such as the :doc:`i18n`)
+
.. toctree::
:maxdepth: 2
diff --git a/doc/theming.rst b/doc/theming.rst
deleted file mode 100644
index 6a154affd..000000000
--- a/doc/theming.rst
+++ /dev/null
@@ -1,159 +0,0 @@
-.. highlight:: python
-
-HTML theming support
-====================
-
-.. versionadded:: 0.6
-
-.. note::
-
- This document provides information about creating your own theme. If you
- simply wish to use a pre-existing HTML themes, refer to
- :doc:`/usage/theming`.
-
-Sphinx supports changing the appearance of its HTML output via *themes*. A
-theme is a collection of HTML templates, stylesheet(s) and other static files.
-Additionally, it has a configuration file which specifies from which theme to
-inherit, which highlighting style to use, and what options exist for customizing
-the theme's look and feel.
-
-Themes are meant to be project-unaware, so they can be used for different
-projects without change.
-
-
-Creating themes
----------------
-
-Themes take the form of either a directory or a zipfile (whose name is the
-theme name), containing the following:
-
-* A :file:`theme.conf` file.
-* HTML templates, if needed.
-* A ``static/`` directory containing any static files that will be copied to the
- output static directory on build. These can be images, styles, script files.
-
-The :file:`theme.conf` file is in INI format [1]_ (readable by the standard
-Python :mod:`ConfigParser` module) and has the following structure:
-
-.. sourcecode:: ini
-
- [theme]
- inherit = base theme
- stylesheet = main CSS name
- pygments_style = stylename
- sidebars = localtoc.html, relations.html, sourcelink.html, searchbox.html
-
- [options]
- variable = default value
-
-* The **inherit** setting gives the name of a "base theme", or ``none``. The
- base theme will be used to locate missing templates (most themes will not have
- to supply most templates if they use ``basic`` as the base theme), its options
- will be inherited, and all of its static files will be used as well. If you
- want to also inherit the stylesheet, include it via CSS' ``@import`` in your
- own.
-
-* The **stylesheet** setting gives the name of a CSS file which will be
- referenced in the HTML header. If you need more than one CSS file, either
- include one from the other via CSS' ``@import``, or use a custom HTML template
- that adds ```` tags as necessary. Setting the
- :confval:`html_style` config value will override this setting.
-
-* The **pygments_style** setting gives the name of a Pygments style to use for
- highlighting. This can be overridden by the user in the
- :confval:`pygments_style` config value.
-
-* The **pygments_dark_style** setting gives the name of a Pygments style to use
- for highlighting when the CSS media query ``(prefers-color-scheme: dark)``
- evaluates to true. It is injected into the page using
- :meth:`~Sphinx.add_css_file()`.
-
-* The **sidebars** setting gives the comma separated list of sidebar templates
- for constructing sidebars. This can be overridden by the user in the
- :confval:`html_sidebars` config value.
-
-* The **options** section contains pairs of variable names and default values.
- These options can be overridden by the user in :confval:`html_theme_options`
- and are accessible from all templates as ``theme_``.
-
-.. versionadded:: 1.7
- sidebar settings
-
-
-.. _distribute-your-theme:
-
-Distribute your theme as a Python package
------------------------------------------
-
-As a way to distribute your theme, you can use Python package. Python package
-brings to users easy setting up ways.
-
-To distribute your theme as a Python package, please define an entry point
-called ``sphinx.html_themes`` in your ``setup.py`` file, and write a ``setup()``
-function to register your themes using ``add_html_theme()`` API in it::
-
- # 'setup.py'
- setup(
- ...
- entry_points = {
- 'sphinx.html_themes': [
- 'name_of_theme = your_package',
- ]
- },
- ...
- )
-
- # 'your_package.py'
- from os import path
-
- def setup(app):
- app.add_html_theme('name_of_theme', path.abspath(path.dirname(__file__)))
-
-If your theme package contains two or more themes, please call
-``add_html_theme()`` twice or more.
-
-.. versionadded:: 1.2
- 'sphinx_themes' entry_points feature.
-
-.. deprecated:: 1.6
- ``sphinx_themes`` entry_points has been deprecated.
-
-.. versionadded:: 1.6
- ``sphinx.html_themes`` entry_points feature.
-
-
-Templating
-----------
-
-The :doc:`guide to templating ` is helpful if you want to write your
-own templates. What is important to keep in mind is the order in which Sphinx
-searches for templates:
-
-* First, in the user's ``templates_path`` directories.
-* Then, in the selected theme.
-* Then, in its base theme, its base's base theme, etc.
-
-When extending a template in the base theme with the same name, use the theme
-name as an explicit directory: ``{% extends "basic/layout.html" %}``. From a
-user ``templates_path`` template, you can still use the "exclamation mark"
-syntax as described in the templating document.
-
-Static templates
-~~~~~~~~~~~~~~~~
-
-Since theme options are meant for the user to configure a theme more easily,
-without having to write a custom stylesheet, it is necessary to be able to
-template static files as well as HTML files. Therefore, Sphinx supports
-so-called "static templates", like this:
-
-If the name of a file in the ``static/`` directory of a theme (or in the user's
-static path, for that matter) ends with ``_t``, it will be processed by the
-template engine. The ``_t`` will be left from the final file name. For
-example, the *classic* theme has a file ``static/classic.css_t`` which uses
-templating to put the color options into the stylesheet. When a documentation
-is built with the classic theme, the output directory will contain a
-``_static/classic.css`` file where all template tags have been processed.
-
-.. [1] It is not an executable Python file, as opposed to :file:`conf.py`,
- because that would pose an unnecessary security risk if themes are
- shared.
diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst
index 60e66edbd..d50abd89e 100644
--- a/doc/usage/extensions/autosummary.rst
+++ b/doc/usage/extensions/autosummary.rst
@@ -195,6 +195,15 @@ also use these config values:
.. versionadded:: 2.1
+.. confval:: autosummary_filename_map
+
+ A dict mapping object names to filenames. This is necessary to avoid
+ filename conflicts where multiple objects have names that are
+ indistinguishable when case is ignored, on file systems where filenames
+ are case-insensitive.
+
+ .. versionadded:: 3.2
+
Customizing templates
---------------------
diff --git a/doc/usage/extensions/napoleon.rst b/doc/usage/extensions/napoleon.rst
index 76c423dc0..b16577e2d 100644
--- a/doc/usage/extensions/napoleon.rst
+++ b/doc/usage/extensions/napoleon.rst
@@ -274,11 +274,12 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`::
napoleon_use_ivar = False
napoleon_use_param = True
napoleon_use_rtype = True
+ napoleon_type_aliases = None
.. _Google style:
https://google.github.io/styleguide/pyguide.html
.. _NumPy style:
- https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt
+ https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard
.. confval:: napoleon_google_docstring
@@ -435,7 +436,7 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`::
:param arg1: Description of `arg1`
:type arg1: str
:param arg2: Description of `arg2`, defaults to 0
- :type arg2: int, optional
+ :type arg2: :class:`int`, *optional*
**If False**::
@@ -480,3 +481,33 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`::
**If False**::
:returns: *bool* -- True if successful, False otherwise
+
+.. confval:: napoleon_type_aliases
+
+ A mapping to translate type names to other names or references. Works
+ only when ``napoleon_use_param = True``. *Defaults to None.*
+
+ With::
+
+ napoleon_type_aliases = {
+ "CustomType": "mypackage.CustomType",
+ "dict-like": ":term:`dict-like `",
+ }
+
+ This `NumPy style`_ snippet::
+
+ Parameters
+ ----------
+ arg1 : CustomType
+ Description of `arg1`
+ arg2 : dict-like
+ Description of `arg2`
+
+ becomes::
+
+ :param arg1: Description of `arg1`
+ :type arg1: mypackage.CustomType
+ :param arg2: Description of `arg2`
+ :type arg2: :term:`dict-like `
+
+ .. versionadded:: 3.2
diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst
index 7b9bd2f80..fcdbc3f16 100644
--- a/doc/usage/restructuredtext/directives.rst
+++ b/doc/usage/restructuredtext/directives.rst
@@ -656,9 +656,43 @@ __ http://pygments.org/docs/lexers
string are included. The ``start-at`` and ``end-at`` options behave in a
similar way, but the lines containing the matched string are included.
- With lines selected using ``start-after`` it is still possible to use
- ``lines``, the first allowed line having by convention the line number
- ``1``.
+ ``start-after``/``start-at`` and ``end-before``/``end-at`` can have same string.
+ ``start-after``/``start-at`` filter lines before the line that contains
+ option string (``start-at`` will keep the line). Then ``end-before``/``end-at``
+ filter lines after the line that contains option string (``end-at`` will keep
+ the line and ``end-before`` skip the first line).
+
+ .. note::
+
+ If you want to select only ``[second-section]`` of ini file like the
+ following, you can use ``:start-at: [second-section]`` and
+ ``:end-before: [third-section]``:
+
+ .. code-block:: ini
+
+ [first-section]
+
+ var_in_first=true
+
+ [second-section]
+
+ var_in_second=true
+
+ [third-section]
+
+ var_in_third=true
+
+ Useful cases of these option is working with tag comments.
+ ``:start-after: [initialized]`` and ``:end-before: [initialized]`` options
+ keep lines between comments:
+
+ .. code-block:: py
+
+ if __name__ == "__main__":
+ # [initialize]
+ app.start(":8000")
+ # [initialize]
+
When lines have been selected in any of the ways described above, the line
numbers in ``emphasize-lines`` refer to those selected lines, counted
diff --git a/doc/usage/restructuredtext/field-lists.rst b/doc/usage/restructuredtext/field-lists.rst
index 28b3cfe1b..5fc897d62 100644
--- a/doc/usage/restructuredtext/field-lists.rst
+++ b/doc/usage/restructuredtext/field-lists.rst
@@ -9,7 +9,14 @@ fields marked up like this::
:fieldname: Field content
-Sphinx provides custom behavior for bibliographic fields compared to docutils.
+Sphinx extends standard docutils behavior for field lists and adds some extra
+functionality that is covered in this section.
+
+.. note::
+
+ The values of field lists will be parsed as
+ strings. You cannot use Python collections such as lists or dictionaries.
+
.. _metadata:
@@ -17,11 +24,20 @@ File-wide metadata
------------------
A field list near the top of a file is normally parsed by docutils as the
-*docinfo* which is generally used to record the author, date of publication and
-other metadata. However, in Sphinx, a field list preceding any other markup is
-moved from the *docinfo* to the Sphinx environment as document metadata and is
-not displayed in the output; a field list appearing after the document title
-will be part of the *docinfo* as normal and will be displayed in the output.
+*docinfo* and shown on the page. However, in Sphinx, a field list preceding
+any other markup is moved from the *docinfo* to the Sphinx environment as
+document metadata, and is not displayed in the output.
+
+.. note::
+
+ A field list appearing after the document title *will* be part of the
+ *docinfo* as normal and will be displayed in the output.
+
+
+Special metadata fields
+-----------------------
+
+Sphinx provides custom behavior for bibliographic fields compared to docutils.
At the moment, these metadata fields are recognized:
diff --git a/doc/usage/theming.rst b/doc/usage/theming.rst
index 5474e9620..e5362b9f0 100644
--- a/doc/usage/theming.rst
+++ b/doc/usage/theming.rst
@@ -2,8 +2,8 @@
.. _html-themes:
-HTML
-====
+HTML Theming
+============
Sphinx provides a number of builders for HTML and HTML-based formats.
@@ -21,7 +21,8 @@ Themes
.. note::
This section provides information about using pre-existing HTML themes. If
- you wish to create your own theme, refer to :doc:`/theming`.
+ you wish to create your own theme, refer to
+ :doc:`/development/theming`.
Sphinx supports changing the appearance of its HTML output via *themes*. A
theme is a collection of HTML templates, stylesheet(s) and other static files.
@@ -80,7 +81,7 @@ zipfile-based theme::
html_theme = "dotted"
For more information on the design of themes, including information about
-writing your own themes, refer to :doc:`/theming`.
+writing your own themes, refer to :doc:`/development/theming`.
.. _builtin-themes:
@@ -363,6 +364,7 @@ sphinx-themes.org__.
.. versionchanged:: 1.4
**sphinx_rtd_theme** has become optional.
+
.. __: https://pypi.org/search/?q=&o=&c=Framework+%3A%3A+Sphinx+%3A%3A+Theme
.. __: https://github.com/search?utf8=%E2%9C%93&q=sphinx+theme&type=
.. __: https://sphinx-themes.org/
diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py
index 84b0c1427..33503bb08 100644
--- a/sphinx/addnodes.py
+++ b/sphinx/addnodes.py
@@ -15,7 +15,6 @@ from docutils import nodes
from docutils.nodes import Element, Node
from sphinx.deprecation import RemovedInSphinx40Warning
-from sphinx.util import docutils
if False:
# For type annotation
@@ -34,6 +33,7 @@ class document(nodes.document):
def set_id(self, node: Element, msgnode: Element = None,
suggested_prefix: str = '') -> str:
+ from sphinx.util import docutils
if docutils.__version_info__ >= (0, 16):
ret = super().set_id(node, msgnode, suggested_prefix) # type: ignore
else:
diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py
index dd5317087..9b54afc7c 100644
--- a/sphinx/builders/linkcheck.py
+++ b/sphinx/builders/linkcheck.py
@@ -35,6 +35,8 @@ 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)
+
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
@@ -210,10 +212,21 @@ class CheckExternalLinksBuilder(Builder):
def check() -> Tuple[str, str, int]:
# check for various conditions without bothering the network
- if len(uri) == 0 or uri.startswith(('#', 'mailto:', 'ftp:')):
+ if len(uri) == 0 or uri.startswith(('#', 'mailto:')):
return 'unchecked', '', 0
elif not uri.startswith(('http:', 'https:')):
- return 'local', '', 0
+ if uri_re.match(uri):
+ # non supported URI schemes (ex. ftp)
+ return 'unchecked', '', 0
+ else:
+ if path.exists(path.join(self.srcdir, uri)):
+ return 'working', '', 0
+ else:
+ for rex in self.to_ignore:
+ if rex.match(uri):
+ return 'ignored', '', 0
+ else:
+ return 'broken', '', 0
elif uri in self.good:
return 'working', 'old', 0
elif uri in self.broken:
diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py
index 642fee55e..65786b5de 100644
--- a/sphinx/domains/c.py
+++ b/sphinx/domains/c.py
@@ -1200,13 +1200,17 @@ class ASTTypeWithInit(ASTBase):
class ASTMacroParameter(ASTBase):
- def __init__(self, arg: ASTNestedName, ellipsis: bool = False) -> None:
+ def __init__(self, arg: ASTNestedName, ellipsis: bool = False,
+ variadic: bool = False) -> None:
self.arg = arg
self.ellipsis = ellipsis
+ self.variadic = variadic
def _stringify(self, transform: StringifyTransform) -> str:
if self.ellipsis:
return '...'
+ elif self.variadic:
+ return transform(self.arg) + '...'
else:
return transform(self.arg)
@@ -1215,6 +1219,9 @@ class ASTMacroParameter(ASTBase):
verify_description_mode(mode)
if self.ellipsis:
signode += nodes.Text('...')
+ elif self.variadic:
+ name = str(self)
+ signode += nodes.emphasis(name, name)
else:
self.arg.describe_signature(signode, mode, env, symbol=symbol)
@@ -2915,9 +2922,16 @@ class DefinitionParser(BaseParser):
if not self.match(identifier_re):
self.fail("Expected identifier in macro parameters.")
nn = ASTNestedName([ASTIdentifier(self.matched_text)], rooted=False)
- arg = ASTMacroParameter(nn)
- args.append(arg)
+ # Allow named variadic args:
+ # https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html
self.skip_ws()
+ if self.skip_string_and_ws('...'):
+ args.append(ASTMacroParameter(nn, False, True))
+ self.skip_ws()
+ if not self.skip_string(')'):
+ self.fail('Expected ")" after "..." in macro parameters.')
+ break
+ args.append(ASTMacroParameter(nn))
if self.skip_string_and_ws(','):
continue
elif self.skip_string_and_ws(')'):
diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py
index fb167828f..f4bc58b69 100644
--- a/sphinx/domains/python.py
+++ b/sphinx/domains/python.py
@@ -11,6 +11,7 @@
import builtins
import inspect
import re
+import sys
import typing
import warnings
from inspect import Parameter
@@ -134,6 +135,19 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod
return result
else:
+ if sys.version_info >= (3, 6):
+ if isinstance(node, ast.Constant):
+ if node.value is Ellipsis:
+ return [addnodes.desc_sig_punctuation('', "...")]
+ else:
+ return [nodes.Text(node.value)]
+
+ if sys.version_info < (3, 8):
+ if isinstance(node, ast.Ellipsis):
+ return [addnodes.desc_sig_punctuation('', "...")]
+ elif isinstance(node, ast.NameConstant):
+ return [nodes.Text(node.value)]
+
raise SyntaxError # unsupported syntax
if env is None:
diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py
index 016f84ebc..7eaaa531d 100644
--- a/sphinx/domains/std.py
+++ b/sphinx/domains/std.py
@@ -223,6 +223,11 @@ class Cmdoption(ObjectDescription):
node_id = make_id(self.env, self.state.document, prefix, optname)
signode['ids'].append(node_id)
+ old_node_id = self.make_old_id(prefix, optname)
+ if old_node_id not in self.state.document.ids and \
+ old_node_id not in signode['ids']:
+ signode['ids'].append(old_node_id)
+
self.state.document.note_explicit_target(signode)
domain = cast(StandardDomain, self.env.get_domain('std'))
@@ -239,6 +244,14 @@ class Cmdoption(ObjectDescription):
entry = '; '.join([descr, option])
self.indexnode['entries'].append(('pair', entry, signode['ids'][0], '', None))
+ def make_old_id(self, prefix: str, optname: str) -> str:
+ """Generate old styled node_id for cmdoption.
+
+ .. note:: Old Styled node_id was used until Sphinx-3.0.
+ This will be removed in Sphinx-5.0.
+ """
+ return nodes.make_id(prefix + '-' + optname)
+
class Program(SphinxDirective):
"""
diff --git a/sphinx/environment/collectors/asset.py b/sphinx/environment/collectors/asset.py
index 06a0d5198..3da2a6e4b 100644
--- a/sphinx/environment/collectors/asset.py
+++ b/sphinx/environment/collectors/asset.py
@@ -58,17 +58,13 @@ class ImageCollector(EnvironmentCollector):
elif imguri.find('://') != -1:
candidates['?'] = imguri
continue
- rel_imgpath, full_imgpath = app.env.relfn2path(imguri, docname)
- if app.config.language:
- # substitute figures (ex. foo.png -> foo.en.png)
- i18n_full_imgpath = search_image_for_language(full_imgpath, app.env)
- if i18n_full_imgpath != full_imgpath:
- full_imgpath = i18n_full_imgpath
- rel_imgpath = relative_path(path.join(app.srcdir, 'dummy'),
- i18n_full_imgpath)
- # set imgpath as default URI
- node['uri'] = rel_imgpath
- if rel_imgpath.endswith(os.extsep + '*'):
+
+ if imguri.endswith(os.extsep + '*'):
+ # Update `node['uri']` to a relative path from srcdir
+ # from a relative path from current document.
+ rel_imgpath, full_imgpath = app.env.relfn2path(imguri, docname)
+ node['uri'] = rel_imgpath
+
if app.config.language:
# Search language-specific figures at first
i18n_imguri = get_image_filename_for_language(imguri, app.env)
@@ -77,7 +73,15 @@ class ImageCollector(EnvironmentCollector):
self.collect_candidates(app.env, full_imgpath, candidates, node)
else:
- candidates['*'] = rel_imgpath
+ if app.config.language:
+ # substitute imguri by figure_language_filename
+ # (ex. foo.png -> foo.en.png)
+ imguri = search_image_for_language(imguri, app.env)
+
+ # Update `node['uri']` to a relative path from srcdir
+ # from a relative path from current document.
+ node['uri'], _ = app.env.relfn2path(imguri, docname)
+ candidates['*'] = node['uri']
# map image paths to unique image names (so that they can be put
# into a single directory)
diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py
index 0984377c5..350604387 100644
--- a/sphinx/ext/autosummary/__init__.py
+++ b/sphinx/ext/autosummary/__init__.py
@@ -252,7 +252,9 @@ class Autosummary(SphinxDirective):
tree_prefix = self.options['toctree'].strip()
docnames = []
excluded = Matcher(self.config.exclude_patterns)
+ filename_map = self.config.autosummary_filename_map
for name, sig, summary, real_name in items:
+ real_name = filename_map.get(real_name, real_name)
docname = posixpath.join(tree_prefix, real_name)
docname = posixpath.normpath(posixpath.join(dirname, docname))
if docname not in self.env.found_docs:
@@ -785,6 +787,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_role('autolink', AutoLink())
app.connect('builder-inited', process_generate_options)
app.add_config_value('autosummary_context', {}, True)
+ app.add_config_value('autosummary_filename_map', {}, 'html')
app.add_config_value('autosummary_generate', [], True, [bool])
app.add_config_value('autosummary_generate_overwrite', True, False)
app.add_config_value('autosummary_mock_imports',
diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py
index d908e2088..c1b50de57 100644
--- a/sphinx/ext/autosummary/generate.py
+++ b/sphinx/ext/autosummary/generate.py
@@ -74,6 +74,7 @@ class DummyApplication:
self.warningiserror = False
self.config.add('autosummary_context', {}, True, None)
+ self.config.add('autosummary_filename_map', {}, True, None)
self.config.init_values()
def emit_firstresult(self, *args: Any) -> None:
@@ -393,6 +394,11 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None,
# keep track of new files
new_files = []
+ if app:
+ filename_map = app.config.autosummary_filename_map
+ else:
+ filename_map = {}
+
# write
for entry in sorted(set(items), key=str):
if entry.path is None:
@@ -418,7 +424,7 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None,
imported_members, app, entry.recursive, context,
modname, qualname)
- filename = os.path.join(path, name + suffix)
+ filename = os.path.join(path, filename_map.get(name, name) + suffix)
if os.path.isfile(filename):
with open(filename, encoding=encoding) as f:
old_content = f.read()
diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py
index 2b1818425..6d7406ead 100644
--- a/sphinx/ext/napoleon/__init__.py
+++ b/sphinx/ext/napoleon/__init__.py
@@ -41,6 +41,7 @@ class Config:
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_use_keyword = True
+ napoleon_type_aliases = None
napoleon_custom_sections = None
.. _Google style:
@@ -236,6 +237,10 @@ class Config:
:returns: *bool* -- True if successful, False otherwise
+ napoleon_type_aliases : :obj:`dict` (Defaults to None)
+ Add a mapping of strings to string, translating types in numpy
+ style docstrings. Only works when ``napoleon_use_param = True``.
+
napoleon_custom_sections : :obj:`list` (Defaults to None)
Add a list of custom sections to include, expanding the list of parsed sections.
@@ -263,6 +268,7 @@ class Config:
'napoleon_use_param': (True, 'env'),
'napoleon_use_rtype': (True, 'env'),
'napoleon_use_keyword': (True, 'env'),
+ 'napoleon_type_aliases': (None, 'env'),
'napoleon_custom_sections': (None, 'env')
}
diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py
index 5857fcf92..95fb1e538 100644
--- a/sphinx/ext/napoleon/docstring.py
+++ b/sphinx/ext/napoleon/docstring.py
@@ -10,6 +10,7 @@
:license: BSD, see LICENSE for details.
"""
+import collections
import inspect
import re
from functools import partial
@@ -18,13 +19,15 @@ from typing import Any, Callable, Dict, List, Tuple, Union
from sphinx.application import Sphinx
from sphinx.config import Config as SphinxConfig
from sphinx.ext.napoleon.iterators import modify_iter
-from sphinx.locale import _
+from sphinx.locale import _, __
+from sphinx.util import logging
+
+logger = logging.getLogger(__name__)
if False:
# For type annotation
from typing import Type # for python3.5.1
-
_directive_regex = re.compile(r'\.\. \S+::')
_google_section_regex = re.compile(r'^(\s|\w)+:\s*$')
_google_typed_arg_regex = re.compile(r'\s*(.+?)\s*\(\s*(.*[^\s]+)\s*\)')
@@ -33,11 +36,19 @@ _single_colon_regex = re.compile(r'(?\()?'
r'(\d+|#|[ivxlcdm]+|[IVXLCDM]+|[a-zA-Z])'
r'(?(paren)\)|\.)(\s+\S|\s*$)')
+_token_regex = re.compile(
+ r"(\sor\s|\sof\s|:\s|,\s|[{]|[}]"
+ r'|"(?:\\"|[^"])*"'
+ r"|'(?:\\'|[^'])*')"
+)
class GoogleDocstring:
@@ -780,6 +791,165 @@ class GoogleDocstring:
return lines
+def _recombine_set_tokens(tokens: List[str]) -> List[str]:
+ token_queue = collections.deque(tokens)
+ keywords = ("optional", "default")
+
+ def takewhile_set(tokens):
+ open_braces = 0
+ previous_token = None
+ while True:
+ try:
+ token = tokens.popleft()
+ except IndexError:
+ break
+
+ if token == ", ":
+ previous_token = token
+ continue
+
+ if token in keywords:
+ tokens.appendleft(token)
+ if previous_token is not None:
+ tokens.appendleft(previous_token)
+ break
+
+ if previous_token is not None:
+ yield previous_token
+ previous_token = None
+
+ if token == "{":
+ open_braces += 1
+ elif token == "}":
+ open_braces -= 1
+
+ yield token
+
+ if open_braces == 0:
+ break
+
+ def combine_set(tokens):
+ while True:
+ try:
+ token = tokens.popleft()
+ except IndexError:
+ break
+
+ if token == "{":
+ tokens.appendleft("{")
+ yield "".join(takewhile_set(tokens))
+ else:
+ yield token
+
+ return list(combine_set(token_queue))
+
+
+def _tokenize_type_spec(spec: str) -> List[str]:
+ def postprocess(item):
+ if item.startswith("default"):
+ return [item[:7], item[7:]]
+ else:
+ return [item]
+
+ tokens = list(
+ item
+ for raw_token in _token_regex.split(spec)
+ for item in postprocess(raw_token)
+ if item
+ )
+ return tokens
+
+
+def _token_type(token: str, location: str = None) -> str:
+ if token.startswith(" ") or token.endswith(" "):
+ type_ = "delimiter"
+ elif (
+ token.isnumeric() or
+ (token.startswith("{") and token.endswith("}")) or
+ (token.startswith('"') and token.endswith('"')) or
+ (token.startswith("'") and token.endswith("'"))
+ ):
+ type_ = "literal"
+ elif token.startswith("{"):
+ logger.warning(
+ __("invalid value set (missing closing brace): %s"),
+ token,
+ location=location,
+ )
+ type_ = "literal"
+ elif token.endswith("}"):
+ logger.warning(
+ __("invalid value set (missing opening brace): %s"),
+ token,
+ location=location,
+ )
+ type_ = "literal"
+ elif token.startswith("'") or token.startswith('"'):
+ logger.warning(
+ __("malformed string literal (missing closing quote): %s"),
+ token,
+ location=location,
+ )
+ type_ = "literal"
+ elif token.endswith("'") or token.endswith('"'):
+ logger.warning(
+ __("malformed string literal (missing opening quote): %s"),
+ token,
+ location=location,
+ )
+ type_ = "literal"
+ elif token in ("optional", "default"):
+ # default is not a official keyword (yet) but supported by the
+ # reference implementation (numpydoc) and widely used
+ type_ = "control"
+ elif _xref_regex.match(token):
+ type_ = "reference"
+ else:
+ type_ = "obj"
+
+ return type_
+
+
+def _convert_numpy_type_spec(_type: str, location: str = None, translations: dict = {}) -> str:
+ def convert_obj(obj, translations, default_translation):
+ translation = translations.get(obj, obj)
+
+ # use :class: (the default) only if obj is not a standard singleton (None, True, False)
+ if translation in ("None", "True", "False") and default_translation == ":class:`%s`":
+ default_translation = ":obj:`%s`"
+
+ if _xref_regex.match(translation) is None:
+ translation = default_translation % translation
+
+ return translation
+
+ tokens = _tokenize_type_spec(_type)
+ combined_tokens = _recombine_set_tokens(tokens)
+ types = [
+ (token, _token_type(token, location))
+ for token in combined_tokens
+ ]
+
+ # don't use the object role if it's not necessary
+ default_translation = (
+ ":class:`%s`"
+ if not all(type_ == "obj" for _, type_ in types)
+ else "%s"
+ )
+
+ converters = {
+ "literal": lambda x: "``%s``" % x,
+ "obj": lambda x: convert_obj(x, translations, default_translation),
+ "control": lambda x: "*%s*" % x,
+ "delimiter": lambda x: x,
+ "reference": lambda x: x,
+ }
+
+ converted = "".join(converters.get(type_)(token) for token, type_ in types)
+
+ return converted
+
+
class NumpyDocstring(GoogleDocstring):
"""Convert NumPy style docstrings to reStructuredText.
@@ -879,6 +1049,15 @@ class NumpyDocstring(GoogleDocstring):
self._directive_sections = ['.. index::']
super().__init__(docstring, config, app, what, name, obj, options)
+ def _get_location(self) -> str:
+ filepath = inspect.getfile(self._obj) if self._obj is not None else ""
+ name = self._name
+
+ if filepath is None and name is None:
+ return None
+
+ return ":".join([filepath, "docstring of %s" % name])
+
def _consume_field(self, parse_type: bool = True, prefer_type: bool = False
) -> Tuple[str, str, List[str]]:
line = next(self._line_iter)
@@ -888,6 +1067,12 @@ class NumpyDocstring(GoogleDocstring):
_name, _type = line, ''
_name, _type = _name.strip(), _type.strip()
_name = self._escape_args_and_kwargs(_name)
+ if self._config.napoleon_use_param:
+ _type = _convert_numpy_type_spec(
+ _type,
+ location=self._get_location(),
+ translations=self._config.napoleon_type_aliases or {},
+ )
if prefer_type and not _type:
_type, _name = _name, _type
diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py
index b531145f4..4534dd333 100644
--- a/sphinx/search/__init__.py
+++ b/sphinx/search/__init__.py
@@ -117,7 +117,7 @@ var Stemmer = function() {
len(word) == 0 or not (
((len(word) < 3) and (12353 < ord(word[0]) < 12436)) or
(ord(word[0]) < 256 and (
- len(word) < 3 or word in self.stopwords or word.isdigit()
+ len(word) < 3 or word in self.stopwords
))))
diff --git a/sphinx/setup_command.py b/sphinx/setup_command.py
index 24beab856..29a9dace7 100644
--- a/sphinx/setup_command.py
+++ b/sphinx/setup_command.py
@@ -105,7 +105,8 @@ class BuildDoc(Command):
self.config_dir = None # type: str
self.link_index = False
self.copyright = ''
- self.verbosity = 0
+ # Link verbosity to distutils' (which uses 1 by default).
+ self.verbosity = self.distribution.verbose - 1 # type: ignore
self.traceback = False
self.nitpicky = False
self.keep_going = False
diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py
index eec3b4208..9197014bf 100644
--- a/sphinx/testing/fixtures.py
+++ b/sphinx/testing/fixtures.py
@@ -79,7 +79,7 @@ def app_params(request: Any, test_params: Dict, shared_result: SharedResult,
if test_params['shared_result']:
if 'srcdir' in kwargs:
- raise pytest.Exception('You can not spcify shared_result and '
+ raise pytest.Exception('You can not specify shared_result and '
'srcdir in same time.')
kwargs['srcdir'] = test_params['shared_result']
restore = shared_result.restore(test_params['shared_result'])
diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js
index ab5649965..970d0d975 100644
--- a/sphinx/themes/basic/static/searchtools.js
+++ b/sphinx/themes/basic/static/searchtools.js
@@ -166,8 +166,7 @@ var Search = {
objectterms.push(tmp[i].toLowerCase());
}
- if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i].match(/^\d+$/) ||
- tmp[i] === "") {
+ if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i] === "") {
// skip this "word"
continue;
}
diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py
index ce299d27a..3ba7813b6 100644
--- a/sphinx/util/docutils.py
+++ b/sphinx/util/docutils.py
@@ -459,7 +459,7 @@ class SphinxTranslator(nodes.NodeVisitor):
The priority of visitor method is:
1. ``self.visit_{node_class}()``
- 2. ``self.visit_{supre_node_class}()``
+ 2. ``self.visit_{super_node_class}()``
3. ``self.unknown_visit()``
"""
for node_class in node.__class__.__mro__:
diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py
index 499f2316f..b8839d8b0 100644
--- a/sphinx/util/i18n.py
+++ b/sphinx/util/i18n.py
@@ -320,8 +320,8 @@ def search_image_for_language(filename: str, env: "BuildEnvironment") -> str:
return filename
translated = get_image_filename_for_language(filename, env)
- dirname = path.dirname(env.docname)
- if path.exists(path.join(env.srcdir, dirname, translated)):
+ _, abspath = env.relfn2path(translated)
+ if path.exists(abspath):
return translated
else:
return filename
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index ade2be924..4bb320939 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -497,19 +497,26 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
def evaluate_signature(sig: inspect.Signature, globalns: Dict = None, localns: Dict = None
) -> inspect.Signature:
"""Evaluate unresolved type annotations in a signature object."""
+ def evaluate_forwardref(ref: ForwardRef, globalns: Dict, localns: Dict) -> Any:
+ """Evaluate a forward reference."""
+ if sys.version_info > (3, 10):
+ return ref._evaluate(globalns, localns, frozenset())
+ else:
+ return ref._evaluate(globalns, localns)
+
def evaluate(annotation: Any, globalns: Dict, localns: Dict) -> Any:
"""Evaluate unresolved type annotation."""
try:
if isinstance(annotation, str):
ref = ForwardRef(annotation, True)
- annotation = ref._evaluate(globalns, localns)
+ annotation = evaluate_forwardref(ref, globalns, localns)
if isinstance(annotation, ForwardRef):
- annotation = annotation._evaluate(globalns, localns)
+ annotation = evaluate_forwardref(ref, globalns, localns)
elif isinstance(annotation, str):
# might be a ForwardRef'ed annotation in overloaded functions
ref = ForwardRef(annotation, True)
- annotation = ref._evaluate(globalns, localns)
+ annotation = evaluate_forwardref(ref, globalns, localns)
except (NameError, TypeError):
# failed to evaluate type. skipped.
pass
diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py
index 86f9c6e5c..d71ca1b2d 100644
--- a/sphinx/util/typing.py
+++ b/sphinx/util/typing.py
@@ -10,7 +10,7 @@
import sys
import typing
-from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union
+from typing import Any, Callable, Dict, Generator, List, Tuple, TypeVar, Union
from docutils import nodes
from docutils.parsers.rst.states import Inliner
@@ -164,6 +164,8 @@ def _stringify_py36(annotation: Any) -> str:
# for Python 3.5.2+
if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA
params = annotation.__args__ # type: ignore
+ elif annotation.__origin__ == Generator: # type: ignore
+ params = annotation.__args__ # type: ignore
else: # typing.Callable
args = ', '.join(stringify(arg) for arg
in annotation.__args__[:-1]) # type: ignore
diff --git a/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py
new file mode 100644
index 000000000..1f57eeb25
--- /dev/null
+++ b/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py
@@ -0,0 +1,21 @@
+from os import path # NOQA
+from typing import Union
+
+
+class Foo:
+ class Bar:
+ pass
+
+ def __init__(self):
+ pass
+
+ def bar(self):
+ pass
+
+ @property
+ def baz(self):
+ pass
+
+
+def bar(x: Union[int, str], y: int = 1) -> None:
+ pass
diff --git a/tests/roots/test-ext-autosummary-filename-map/conf.py b/tests/roots/test-ext-autosummary-filename-map/conf.py
new file mode 100644
index 000000000..17e2fa445
--- /dev/null
+++ b/tests/roots/test-ext-autosummary-filename-map/conf.py
@@ -0,0 +1,11 @@
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath('.'))
+
+extensions = ['sphinx.ext.autosummary']
+autosummary_generate = True
+autosummary_filename_map = {
+ "autosummary_dummy_module": "module_mangled",
+ "autosummary_dummy_module.bar": "bar"
+}
diff --git a/tests/roots/test-ext-autosummary-filename-map/index.rst b/tests/roots/test-ext-autosummary-filename-map/index.rst
new file mode 100644
index 000000000..57d902b6a
--- /dev/null
+++ b/tests/roots/test-ext-autosummary-filename-map/index.rst
@@ -0,0 +1,9 @@
+
+.. autosummary::
+ :toctree: generated
+ :caption: An autosummary
+
+ autosummary_dummy_module
+ autosummary_dummy_module.Foo
+ autosummary_dummy_module.Foo.bar
+ autosummary_dummy_module.bar
diff --git a/tests/roots/test-linkcheck/links.txt b/tests/roots/test-linkcheck/links.txt
index fa8f11e4c..90759ee63 100644
--- a/tests/roots/test-linkcheck/links.txt
+++ b/tests/roots/test-linkcheck/links.txt
@@ -11,6 +11,8 @@ Some additional anchors to exercise ignore code
* `Example Bar invalid `_
* `Example anchor invalid `_
* `Complete nonsense `_
+* `Example valid local file `_
+* `Example invalid local file `_
.. image:: https://www.google.com/image.png
.. figure:: https://www.google.com/image2.png
diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py
index d1fec550f..7d85f10c5 100644
--- a/tests/test_build_linkcheck.py
+++ b/tests/test_build_linkcheck.py
@@ -30,7 +30,9 @@ def test_defaults(app, status, warning):
# images should fail
assert "Not Found for url: https://www.google.com/image.png" in content
assert "Not Found for url: https://www.google.com/image2.png" in content
- assert len(content.splitlines()) == 5
+ # looking for local file should fail
+ assert "[broken] path/to/notfound" in content
+ assert len(content.splitlines()) == 6
@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True)
@@ -47,8 +49,8 @@ def test_defaults_json(app, status, warning):
"info"]:
assert attr in row
- assert len(content.splitlines()) == 8
- assert len(rows) == 8
+ assert len(content.splitlines()) == 10
+ 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}
@@ -69,7 +71,7 @@ def test_defaults_json(app, status, warning):
assert dnerow['uri'] == 'https://localhost:7777/doesnotexist'
assert rowsby['https://www.google.com/image2.png'] == {
'filename': 'links.txt',
- 'lineno': 16,
+ 'lineno': 18,
'status': 'broken',
'code': 0,
'uri': 'https://www.google.com/image2.png',
@@ -92,7 +94,8 @@ def test_defaults_json(app, status, warning):
'https://localhost:7777/doesnotexist',
'http://www.sphinx-doc.org/en/1.7/intro.html#',
'https://www.google.com/image.png',
- 'https://www.google.com/image2.png']
+ 'https://www.google.com/image2.png',
+ 'path/to/notfound']
})
def test_anchors_ignored(app, status, warning):
app.builder.build_all()
diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py
index efef104f9..71bf251e9 100644
--- a/tests/test_domain_c.py
+++ b/tests/test_domain_c.py
@@ -296,6 +296,10 @@ def test_macro_definitions():
check('macro', 'M(arg, ...)', {1: 'M'})
check('macro', 'M(arg1, arg2, ...)', {1: 'M'})
check('macro', 'M(arg1, arg2, arg3, ...)', {1: 'M'})
+ # GNU extension
+ check('macro', 'M(arg1, arg2, arg3...)', {1: 'M'})
+ with pytest.raises(DefinitionError):
+ check('macro', 'M(arg1, arg2..., arg3)', {1: 'M'})
def test_member_definitions():
diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py
index ccf539b6d..b98f37912 100644
--- a/tests/test_domain_py.py
+++ b/tests/test_domain_py.py
@@ -262,6 +262,14 @@ def test_parse_annotation(app):
[desc_sig_punctuation, ")"],
[desc_sig_punctuation, "]"]))
+ doctree = _parse_annotation("Tuple[int, ...]", app.env)
+ assert_node(doctree, ([pending_xref, "Tuple"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "int"],
+ [desc_sig_punctuation, ", "],
+ [desc_sig_punctuation, "..."],
+ [desc_sig_punctuation, "]"]))
+
doctree = _parse_annotation("Callable[[int, int], int]", app.env)
assert_node(doctree, ([pending_xref, "Callable"],
[desc_sig_punctuation, "["],
@@ -274,6 +282,12 @@ def test_parse_annotation(app):
[pending_xref, "int"],
[desc_sig_punctuation, "]"]))
+ doctree = _parse_annotation("List[None]", app.env)
+ assert_node(doctree, ([pending_xref, "List"],
+ [desc_sig_punctuation, "["],
+ [pending_xref, "None"],
+ [desc_sig_punctuation, "]"]))
+
# None type makes an object-reference (not a class reference)
doctree = _parse_annotation("None", app.env)
assert_node(doctree, ([pending_xref, "None"],))
diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py
index ede19f632..567a8caea 100644
--- a/tests/test_ext_autosummary.py
+++ b/tests/test_ext_autosummary.py
@@ -394,6 +394,20 @@ def test_autosummary_recursive(app, status, warning):
assert 'package.package.module' in content
+@pytest.mark.sphinx('dummy', testroot='ext-autosummary-filename-map')
+def test_autosummary_filename_map(app, status, warning):
+ app.build()
+
+ assert (app.srcdir / 'generated' / 'module_mangled.rst').exists()
+ assert not (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').exists()
+ assert (app.srcdir / 'generated' / 'bar.rst').exists()
+ assert not (app.srcdir / 'generated' / 'autosummary_dummy_module.bar.rst').exists()
+ assert (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').exists()
+
+ html_warnings = app._warning.getvalue()
+ assert html_warnings == ''
+
+
@pytest.mark.sphinx('latex', **default_kw)
def test_autosummary_latex_table_colspec(app, status, warning):
app.builder.build_all()
diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py
index f9cd40104..56812d193 100644
--- a/tests/test_ext_napoleon_docstring.py
+++ b/tests/test_ext_napoleon_docstring.py
@@ -9,13 +9,21 @@
:license: BSD, see LICENSE for details.
"""
+import re
from collections import namedtuple
+from contextlib import contextmanager
from inspect import cleandoc
from textwrap import dedent
from unittest import TestCase, mock
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
+)
class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))):
@@ -1976,6 +1984,154 @@ definition_after_normal_text : int
actual = str(NumpyDocstring(docstring, config))
self.assertEqual(expected, actual)
+ def test_token_type(self):
+ tokens = (
+ ("1", "literal"),
+ ("'string'", "literal"),
+ ('"another_string"', "literal"),
+ ("{1, 2}", "literal"),
+ ("{'va{ue', 'set'}", "literal"),
+ ("optional", "control"),
+ ("default", "control"),
+ (", ", "delimiter"),
+ (" of ", "delimiter"),
+ (" or ", "delimiter"),
+ (": ", "delimiter"),
+ ("True", "obj"),
+ ("None", "obj"),
+ ("name", "obj"),
+ (":py:class:`Enum`", "reference"),
+ )
+
+ for token, expected in tokens:
+ actual = _token_type(token)
+ self.assertEqual(expected, actual)
+
+ def test_tokenize_type_spec(self):
+ specs = (
+ "str",
+ "int or float or None, optional",
+ '{"F", "C", "N"}',
+ "{'F', 'C', 'N'}, default: 'F'",
+ "{'F', 'C', 'N or C'}, default 'F'",
+ '"ma{icious"',
+ r"'with \'quotes\''",
+ )
+
+ tokens = (
+ ["str"],
+ ["int", " or ", "float", " or ", "None", ", ", "optional"],
+ ["{", '"F"', ", ", '"C"', ", ", '"N"', "}"],
+ ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "'F'"],
+ ["{", "'F'", ", ", "'C'", ", ", "'N or C'", "}", ", ", "default", " ", "'F'"],
+ ['"ma{icious"'],
+ [r"'with \'quotes\''"],
+ )
+
+ for spec, expected in zip(specs, tokens):
+ actual = _tokenize_type_spec(spec)
+ self.assertEqual(expected, actual)
+
+ def test_recombine_set_tokens(self):
+ tokens = (
+ ["{", "1", ", ", "2", "}"],
+ ["{", '"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"],
+ ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "None"],
+ )
+
+ combined_tokens = (
+ ["{1, 2}"],
+ ['{"F", "C", "N"}', ", ", "optional"],
+ ["{'F', 'C', 'N'}", ", ", "default", ": ", "None"],
+ )
+
+ for tokens_, expected in zip(tokens, combined_tokens):
+ actual = _recombine_set_tokens(tokens_)
+ self.assertEqual(expected, actual)
+
+ def test_recombine_set_tokens_invalid(self):
+ tokens = (
+ ["{", "1", ", ", "2"],
+ ['"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"],
+ ["{", "1", ", ", "2", ", ", "default", ": ", "None"],
+ )
+ combined_tokens = (
+ ["{1, 2"],
+ ['"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"],
+ ["{1, 2", ", ", "default", ": ", "None"],
+ )
+
+ for tokens_, expected in zip(tokens, combined_tokens):
+ actual = _recombine_set_tokens(tokens_)
+ self.assertEqual(expected, actual)
+
+ def test_convert_numpy_type_spec(self):
+ translations = {
+ "DataFrame": "pandas.DataFrame",
+ }
+
+ specs = (
+ "",
+ "optional",
+ "str, optional",
+ "int or float or None, default: None",
+ '{"F", "C", "N"}',
+ "{'F', 'C', 'N'}, default: 'N'",
+ "DataFrame, optional",
+ )
+
+ converted = (
+ "",
+ "*optional*",
+ ":class:`str`, *optional*",
+ ":class:`int` or :class:`float` or :obj:`None`, *default*: :obj:`None`",
+ '``{"F", "C", "N"}``',
+ "``{'F', 'C', 'N'}``, *default*: ``'N'``",
+ ":class:`pandas.DataFrame`, *optional*",
+ )
+
+ for spec, expected in zip(specs, converted):
+ actual = _convert_numpy_type_spec(spec, translations=translations)
+ self.assertEqual(expected, actual)
+
+ def test_parameter_types(self):
+ docstring = dedent("""\
+ Parameters
+ ----------
+ param1 : DataFrame
+ the data to work on
+ param2 : int or float or None
+ a parameter with different types
+ param3 : dict-like, optional
+ a optional mapping
+ param4 : int or float or None, optional
+ a optional parameter with different types
+ param5 : {"F", "C", "N"}, optional
+ a optional parameter with fixed values
+ """)
+ expected = dedent("""\
+ :param param1: the data to work on
+ :type param1: DataFrame
+ :param param2: a parameter with different types
+ :type param2: :class:`int` or :class:`float` or :obj:`None`
+ :param param3: a optional mapping
+ :type param3: :term:`dict-like `, *optional*
+ :param param4: a optional parameter with different types
+ :type param4: :class:`int` or :class:`float` or :obj:`None`, *optional*
+ :param param5: a optional parameter with fixed values
+ :type param5: ``{"F", "C", "N"}``, *optional*
+ """)
+ translations = {
+ "dict-like": ":term:`dict-like `",
+ }
+ config = Config(
+ napoleon_use_param=True,
+ napoleon_use_rtype=True,
+ napoleon_type_aliases=translations,
+ )
+ actual = str(NumpyDocstring(docstring, config))
+ self.assertEqual(expected, actual)
+
def test_keywords_with_types(self):
docstring = """\
Do as you please
@@ -1991,3 +2147,38 @@ Do as you please
:kwtype gotham_is_yours: None
"""
self.assertEqual(expected, actual)
+
+@contextmanager
+def warns(warning, match):
+ match_re = re.compile(match)
+ try:
+ yield warning
+ finally:
+ raw_warnings = warning.getvalue()
+ warnings = [w for w in raw_warnings.split("\n") if w.strip()]
+
+ assert len(warnings) == 1 and all(match_re.match(w) for w in warnings)
+ warning.truncate(0)
+
+
+class TestNumpyDocstring:
+ def test_token_type_invalid(self, warning):
+ tokens = (
+ "{1, 2",
+ "}",
+ "'abc",
+ "def'",
+ '"ghi',
+ 'jkl"',
+ )
+ errors = (
+ r".+: invalid value set \(missing closing brace\):",
+ r".+: invalid value set \(missing opening brace\):",
+ r".+: malformed string literal \(missing closing quote\):",
+ r".+: malformed string literal \(missing opening quote\):",
+ r".+: malformed string literal \(missing closing quote\):",
+ r".+: malformed string literal \(missing opening quote\):",
+ )
+ for token, error in zip(tokens, errors):
+ with warns(warning, match=error):
+ _token_type(token)
diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py
index 41d2a19c2..932fdbfc0 100644
--- a/tests/test_util_typing.py
+++ b/tests/test_util_typing.py
@@ -10,7 +10,9 @@
import sys
from numbers import Integral
-from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional, Generic
+from typing import (
+ Any, Dict, Generator, List, TypeVar, Union, Callable, Tuple, Optional, Generic
+)
import pytest
@@ -48,6 +50,7 @@ def test_stringify_type_hints_containers():
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(Generator[None, None, None]) == "Generator[None, None, None]"
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
diff --git a/utils/doclinter.py b/utils/doclinter.py
index 52b2fe892..bb11decaf 100644
--- a/utils/doclinter.py
+++ b/utils/doclinter.py
@@ -50,6 +50,9 @@ def lint(path: str) -> int:
if re.match(r'^\s*\.\. ', line):
# ignore directives and hyperlink targets
pass
+ elif re.match(r'^\s*``[^`]+``$', line):
+ # ignore a very long literal string
+ pass
else:
print('%s:%d: the line is too long (%d > %d).' %
(path, i + 1, len(line), MAX_LINE_LENGTH))