Merge branch '3.x' into 7993_texinfo_for_nested_desc

This commit is contained in:
Takeshi KOMIYA 2020-08-01 15:50:55 +09:00 committed by GitHub
commit f71b26ac56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1149 additions and 255 deletions

15
CHANGES
View File

@ -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
--------

View File

@ -10,7 +10,6 @@ Sphinx documentation contents
development/index
man/index
theming
templating
latex
extdev/index

View File

@ -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

View File

@ -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

View File

@ -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.

336
doc/development/theming.rst Normal file
View File

@ -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 ``<link rel="stylesheet">`` 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_<name>``.
.. 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 </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 </usage/restructuredtext/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
<div>
{{ my_func("some string") }}
</div>
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.

View File

@ -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

View File

@ -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

View File

@ -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 ``<link rel="stylesheet">`` 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_<name>``.
.. 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 <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.

View File

@ -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
---------------------

View File

@ -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 <mapping>`",
}
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 <mapping>`
.. versionadded:: 3.2

View File

@ -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

View File

@ -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:

View File

@ -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/

View File

@ -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:

View File

@ -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:

View File

@ -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(')'):

View File

@ -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:

View File

@ -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):
"""

View File

@ -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)

View File

@ -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',

View File

@ -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()

View File

@ -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')
}

View File

@ -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'(?<!:):(?!:)')
_xref_or_code_regex = re.compile(
r'((?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)|'
r'(?:``.+``))')
_xref_regex = re.compile(
r'(?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)'
)
_bullet_list_regex = re.compile(r'^(\*|\+|\-)(\s+\S|\s*$)')
_enumerated_list_regex = re.compile(
r'^(?P<paren>\()?'
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

View File

@ -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
))))

View File

@ -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

View File

@ -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'])

View File

@ -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;
}

View File

@ -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__:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -11,6 +11,8 @@ Some additional anchors to exercise ignore code
* `Example Bar invalid <https://www.google.com/#top>`_
* `Example anchor invalid <http://www.sphinx-doc.org/en/1.7/intro.html#does-not-exist>`_
* `Complete nonsense <https://localhost:7777/doesnotexist>`_
* `Example valid local file <conf.py>`_
* `Example invalid local file <path/to/notfound>`_
.. image:: https://www.google.com/image.png
.. figure:: https://www.google.com/image2.png

View File

@ -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()

View File

@ -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():

View File

@ -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"],))

View File

@ -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()

View File

@ -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 <mapping>`, *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 <mapping>`",
}
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)

View File

@ -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.')

View File

@ -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))