mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '3.x' into 7993_texinfo_for_nested_desc
This commit is contained in:
commit
f71b26ac56
15
CHANGES
15
CHANGES
@ -18,16 +18,23 @@ Features added
|
|||||||
--------------
|
--------------
|
||||||
|
|
||||||
* #2076: autodoc: Allow overriding of exclude-members in skip-member function
|
* #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
|
* #7849: html: Add :confval:`html_codeblock_linenos_style` to change the style
|
||||||
of line numbers for code-blocks
|
of line numbers for code-blocks
|
||||||
* #7853: C and C++, support parameterized GNU style attributes.
|
* #7853: C and C++, support parameterized GNU style attributes.
|
||||||
* #7888: napoleon: Add aliases Warn and Raise.
|
* #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
|
* C, added :rst:dir:`c:alias` directive for inserting copies
|
||||||
of existing declarations.
|
of existing declarations.
|
||||||
* #7745: html: inventory is broken if the docname contains a space
|
* #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
|
* #7902: html theme: Add a new option :confval:`globaltoc_maxdepth` to control
|
||||||
the behavior of globaltoc in sidebar
|
the behavior of globaltoc in sidebar
|
||||||
* #7840: i18n: Optimize the dependencies check on bootstrap
|
* #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.
|
* #7052: add ``:noindexentry:`` to the Python, C, C++, and Javascript domains.
|
||||||
Update the documentation to better reflect the relationship between this option
|
Update the documentation to better reflect the relationship between this option
|
||||||
and the ``:noindex:`` option.
|
and the ``:noindex:`` option.
|
||||||
@ -37,6 +44,7 @@ Features added
|
|||||||
The warnings printed from this functionality can be suppressed by setting
|
The warnings printed from this functionality can be suppressed by setting
|
||||||
:confval:`c_warn_on_allowed_pre_v3`` to ``True``.
|
:confval:`c_warn_on_allowed_pre_v3`` to ``True``.
|
||||||
The functionality is immediately deprecated.
|
The functionality is immediately deprecated.
|
||||||
|
* #7999: C, add support for named variadic macro arguments.
|
||||||
|
|
||||||
Bugs fixed
|
Bugs fixed
|
||||||
----------
|
----------
|
||||||
@ -47,6 +55,7 @@ Bugs fixed
|
|||||||
* #7901: autodoc: type annotations for overloaded functions are not resolved
|
* #7901: autodoc: type annotations for overloaded functions are not resolved
|
||||||
* #904: autodoc: An instance attribute cause a crash of autofunction directive
|
* #904: autodoc: An instance attribute cause a crash of autofunction directive
|
||||||
* #1362: autodoc: ``private-members`` option does not work for class attributes
|
* #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
|
* #7839: autosummary: cannot handle umlauts in function names
|
||||||
* #7865: autosummary: Failed to extract summary line when abbreviations found
|
* #7865: autosummary: Failed to extract summary line when abbreviations found
|
||||||
* #7866: autosummary: Failed to extract correct summary line when docstring
|
* #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
|
* #7691: linkcheck: HEAD requests are not used for checking
|
||||||
* #4888: i18n: Failed to add an explicit title to ``:ref:`` role on translation
|
* #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
|
* #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
|
* #7968: i18n: The content of ``math`` directive is interpreted as reST on
|
||||||
translation
|
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: TypeError is raised for nested object descriptions
|
||||||
* #7993: texinfo: a warning not supporting desc_signature_line node is shown
|
* #7993: texinfo: a warning not supporting desc_signature_line node is shown
|
||||||
* #7869: :rst:role:`abbr` role without an explanation will show the explanation
|
* #7869: :rst:role:`abbr` role without an explanation will show the explanation
|
||||||
@ -71,6 +85,7 @@ Bugs fixed
|
|||||||
nothing.
|
nothing.
|
||||||
* #7619: Duplicated node IDs are generated if node has multiple IDs
|
* #7619: Duplicated node IDs are generated if node has multiple IDs
|
||||||
* #2050: Symbols sections are appeared twice in the index page
|
* #2050: Symbols sections are appeared twice in the index page
|
||||||
|
* #8017: Fix circular import in sphinx.addnodes
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
--------
|
--------
|
||||||
|
@ -10,7 +10,6 @@ Sphinx documentation contents
|
|||||||
development/index
|
development/index
|
||||||
man/index
|
man/index
|
||||||
|
|
||||||
theming
|
|
||||||
templating
|
templating
|
||||||
latex
|
latex
|
||||||
extdev/index
|
extdev/index
|
||||||
|
34
doc/development/builders.rst
Normal file
34
doc/development/builders.rst
Normal 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
|
@ -10,4 +10,12 @@ wish to use Sphinx with existing extensions, refer to :doc:`/usage/index`.
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
overview
|
||||||
tutorials/index
|
tutorials/index
|
||||||
|
builders
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:caption: Theming
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
theming
|
||||||
|
32
doc/development/overview.rst
Normal file
32
doc/development/overview.rst
Normal 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
336
doc/development/theming.rst
Normal 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.
|
@ -1,8 +1,11 @@
|
|||||||
|
.. _extension-tutorials-index:
|
||||||
|
|
||||||
Extension tutorials
|
Extension tutorials
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Refer to the following tutorials to get started with extension development.
|
Refer to the following tutorials to get started with extension development.
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:caption: Directive tutorials
|
:caption: Directive tutorials
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
@ -3,54 +3,41 @@
|
|||||||
Developing extensions for Sphinx
|
Developing extensions for Sphinx
|
||||||
================================
|
================================
|
||||||
|
|
||||||
Since many projects will need special features in their documentation, Sphinx is
|
Since many projects will need special features in their documentation, Sphinx
|
||||||
designed to be extensible on several levels.
|
is designed to be extensible on several levels.
|
||||||
|
|
||||||
This is what you can do in an extension: First, you can add new
|
Here are a few things you can do in an extension:
|
||||||
: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.
|
|
||||||
|
|
||||||
An extension is simply a Python module. When an extension is loaded, Sphinx
|
* Add new :term:`builder`\s to support new output formats or actions on the
|
||||||
imports this module and executes its ``setup()`` function, which in turn
|
parsed documents.
|
||||||
notifies Sphinx of everything the extension offers -- see the extension tutorial
|
* Register custom reStructuredText roles and directives, extending the markup
|
||||||
for examples.
|
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
|
An extension is simply a Python module with a ``setup()`` function. A user
|
||||||
``setup()`` function. All other extensions to load must be listed in the
|
activates the extension by placing the extension's module name
|
||||||
:confval:`extensions` configuration value.
|
(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
|
The configuration file itself can be treated as an extension if it
|
||||||
that they do not have to be listed in the :confval:`extensions` configuration
|
contains a ``setup()`` function. All other extensions to load must be
|
||||||
value.
|
listed in the :confval:`extensions` configuration value.
|
||||||
|
|
||||||
Builder extensions should define an entry point in the ``sphinx.builders``
|
The rest of this page describes some high-level aspects of developing
|
||||||
group. The name of the entry point needs to match your builder's
|
extensions and various parts of Sphinx's behavior that you can control.
|
||||||
:attr:`~.Builder.name` attribute, which is the name passed to the
|
For some examples of how extensions can be built and used to control different
|
||||||
:option:`sphinx-build -b` option. The entry point value should equal the
|
parts of Sphinx, see the :ref:`extension-tutorials-index`.
|
||||||
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
|
|
||||||
|
|
||||||
.. _important-objects:
|
.. _important-objects:
|
||||||
|
|
||||||
@ -192,6 +179,11 @@ as metadata of the extension. Metadata keys currently recognized are:
|
|||||||
APIs used for writing extensions
|
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::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
159
doc/theming.rst
159
doc/theming.rst
@ -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.
|
|
@ -195,6 +195,15 @@ also use these config values:
|
|||||||
|
|
||||||
.. versionadded:: 2.1
|
.. 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
|
Customizing templates
|
||||||
---------------------
|
---------------------
|
||||||
|
@ -274,11 +274,12 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`::
|
|||||||
napoleon_use_ivar = False
|
napoleon_use_ivar = False
|
||||||
napoleon_use_param = True
|
napoleon_use_param = True
|
||||||
napoleon_use_rtype = True
|
napoleon_use_rtype = True
|
||||||
|
napoleon_type_aliases = None
|
||||||
|
|
||||||
.. _Google style:
|
.. _Google style:
|
||||||
https://google.github.io/styleguide/pyguide.html
|
https://google.github.io/styleguide/pyguide.html
|
||||||
.. _NumPy style:
|
.. _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
|
.. confval:: napoleon_google_docstring
|
||||||
|
|
||||||
@ -435,7 +436,7 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`::
|
|||||||
:param arg1: Description of `arg1`
|
:param arg1: Description of `arg1`
|
||||||
:type arg1: str
|
:type arg1: str
|
||||||
:param arg2: Description of `arg2`, defaults to 0
|
:param arg2: Description of `arg2`, defaults to 0
|
||||||
:type arg2: int, optional
|
:type arg2: :class:`int`, *optional*
|
||||||
|
|
||||||
**If False**::
|
**If False**::
|
||||||
|
|
||||||
@ -480,3 +481,33 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`::
|
|||||||
**If False**::
|
**If False**::
|
||||||
|
|
||||||
:returns: *bool* -- True if successful, False otherwise
|
: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
|
||||||
|
@ -656,9 +656,43 @@ __ http://pygments.org/docs/lexers
|
|||||||
string are included. The ``start-at`` and ``end-at`` options behave in a
|
string are included. The ``start-at`` and ``end-at`` options behave in a
|
||||||
similar way, but the lines containing the matched string are included.
|
similar way, but the lines containing the matched string are included.
|
||||||
|
|
||||||
With lines selected using ``start-after`` it is still possible to use
|
``start-after``/``start-at`` and ``end-before``/``end-at`` can have same string.
|
||||||
``lines``, the first allowed line having by convention the line number
|
``start-after``/``start-at`` filter lines before the line that contains
|
||||||
``1``.
|
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
|
When lines have been selected in any of the ways described above, the line
|
||||||
numbers in ``emphasize-lines`` refer to those selected lines, counted
|
numbers in ``emphasize-lines`` refer to those selected lines, counted
|
||||||
|
@ -9,7 +9,14 @@ fields marked up like this::
|
|||||||
|
|
||||||
:fieldname: Field content
|
: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:
|
.. _metadata:
|
||||||
|
|
||||||
@ -17,11 +24,20 @@ File-wide metadata
|
|||||||
------------------
|
------------------
|
||||||
|
|
||||||
A field list near the top of a file is normally parsed by docutils as the
|
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
|
*docinfo* and shown on the page. However, in Sphinx, a field list preceding
|
||||||
other metadata. However, in Sphinx, a field list preceding any other markup is
|
any other markup is moved from the *docinfo* to the Sphinx environment as
|
||||||
moved from the *docinfo* to the Sphinx environment as document metadata and is
|
document metadata, and is not displayed in the output.
|
||||||
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.
|
.. 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:
|
At the moment, these metadata fields are recognized:
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
.. _html-themes:
|
.. _html-themes:
|
||||||
|
|
||||||
HTML
|
HTML Theming
|
||||||
====
|
============
|
||||||
|
|
||||||
Sphinx provides a number of builders for HTML and HTML-based formats.
|
Sphinx provides a number of builders for HTML and HTML-based formats.
|
||||||
|
|
||||||
@ -21,7 +21,8 @@ Themes
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
This section provides information about using pre-existing HTML themes. If
|
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
|
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.
|
theme is a collection of HTML templates, stylesheet(s) and other static files.
|
||||||
@ -80,7 +81,7 @@ zipfile-based theme::
|
|||||||
html_theme = "dotted"
|
html_theme = "dotted"
|
||||||
|
|
||||||
For more information on the design of themes, including information about
|
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:
|
.. _builtin-themes:
|
||||||
|
|
||||||
@ -363,6 +364,7 @@ sphinx-themes.org__.
|
|||||||
.. versionchanged:: 1.4
|
.. versionchanged:: 1.4
|
||||||
**sphinx_rtd_theme** has become optional.
|
**sphinx_rtd_theme** has become optional.
|
||||||
|
|
||||||
|
|
||||||
.. __: https://pypi.org/search/?q=&o=&c=Framework+%3A%3A+Sphinx+%3A%3A+Theme
|
.. __: 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://github.com/search?utf8=%E2%9C%93&q=sphinx+theme&type=
|
||||||
.. __: https://sphinx-themes.org/
|
.. __: https://sphinx-themes.org/
|
||||||
|
@ -15,7 +15,6 @@ from docutils import nodes
|
|||||||
from docutils.nodes import Element, Node
|
from docutils.nodes import Element, Node
|
||||||
|
|
||||||
from sphinx.deprecation import RemovedInSphinx40Warning
|
from sphinx.deprecation import RemovedInSphinx40Warning
|
||||||
from sphinx.util import docutils
|
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
# For type annotation
|
# For type annotation
|
||||||
@ -34,6 +33,7 @@ class document(nodes.document):
|
|||||||
|
|
||||||
def set_id(self, node: Element, msgnode: Element = None,
|
def set_id(self, node: Element, msgnode: Element = None,
|
||||||
suggested_prefix: str = '') -> str:
|
suggested_prefix: str = '') -> str:
|
||||||
|
from sphinx.util import docutils
|
||||||
if docutils.__version_info__ >= (0, 16):
|
if docutils.__version_info__ >= (0, 16):
|
||||||
ret = super().set_id(node, msgnode, suggested_prefix) # type: ignore
|
ret = super().set_id(node, msgnode, suggested_prefix) # type: ignore
|
||||||
else:
|
else:
|
||||||
|
@ -35,6 +35,8 @@ from sphinx.util.requests import is_ssl_error
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
uri_re = re.compile('([a-z]+:)?//') # matches to foo:// and // (a protocol relative URL)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_REQUEST_HEADERS = {
|
DEFAULT_REQUEST_HEADERS = {
|
||||||
'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
|
'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]:
|
def check() -> Tuple[str, str, int]:
|
||||||
# check for various conditions without bothering the network
|
# 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
|
return 'unchecked', '', 0
|
||||||
elif not uri.startswith(('http:', 'https:')):
|
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:
|
elif uri in self.good:
|
||||||
return 'working', 'old', 0
|
return 'working', 'old', 0
|
||||||
elif uri in self.broken:
|
elif uri in self.broken:
|
||||||
|
@ -1200,13 +1200,17 @@ class ASTTypeWithInit(ASTBase):
|
|||||||
|
|
||||||
|
|
||||||
class ASTMacroParameter(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.arg = arg
|
||||||
self.ellipsis = ellipsis
|
self.ellipsis = ellipsis
|
||||||
|
self.variadic = variadic
|
||||||
|
|
||||||
def _stringify(self, transform: StringifyTransform) -> str:
|
def _stringify(self, transform: StringifyTransform) -> str:
|
||||||
if self.ellipsis:
|
if self.ellipsis:
|
||||||
return '...'
|
return '...'
|
||||||
|
elif self.variadic:
|
||||||
|
return transform(self.arg) + '...'
|
||||||
else:
|
else:
|
||||||
return transform(self.arg)
|
return transform(self.arg)
|
||||||
|
|
||||||
@ -1215,6 +1219,9 @@ class ASTMacroParameter(ASTBase):
|
|||||||
verify_description_mode(mode)
|
verify_description_mode(mode)
|
||||||
if self.ellipsis:
|
if self.ellipsis:
|
||||||
signode += nodes.Text('...')
|
signode += nodes.Text('...')
|
||||||
|
elif self.variadic:
|
||||||
|
name = str(self)
|
||||||
|
signode += nodes.emphasis(name, name)
|
||||||
else:
|
else:
|
||||||
self.arg.describe_signature(signode, mode, env, symbol=symbol)
|
self.arg.describe_signature(signode, mode, env, symbol=symbol)
|
||||||
|
|
||||||
@ -2915,9 +2922,16 @@ class DefinitionParser(BaseParser):
|
|||||||
if not self.match(identifier_re):
|
if not self.match(identifier_re):
|
||||||
self.fail("Expected identifier in macro parameters.")
|
self.fail("Expected identifier in macro parameters.")
|
||||||
nn = ASTNestedName([ASTIdentifier(self.matched_text)], rooted=False)
|
nn = ASTNestedName([ASTIdentifier(self.matched_text)], rooted=False)
|
||||||
arg = ASTMacroParameter(nn)
|
# Allow named variadic args:
|
||||||
args.append(arg)
|
# https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html
|
||||||
self.skip_ws()
|
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(','):
|
if self.skip_string_and_ws(','):
|
||||||
continue
|
continue
|
||||||
elif self.skip_string_and_ws(')'):
|
elif self.skip_string_and_ws(')'):
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
import builtins
|
import builtins
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import typing
|
import typing
|
||||||
import warnings
|
import warnings
|
||||||
from inspect import Parameter
|
from inspect import Parameter
|
||||||
@ -134,6 +135,19 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
else:
|
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
|
raise SyntaxError # unsupported syntax
|
||||||
|
|
||||||
if env is None:
|
if env is None:
|
||||||
|
@ -223,6 +223,11 @@ class Cmdoption(ObjectDescription):
|
|||||||
node_id = make_id(self.env, self.state.document, prefix, optname)
|
node_id = make_id(self.env, self.state.document, prefix, optname)
|
||||||
signode['ids'].append(node_id)
|
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)
|
self.state.document.note_explicit_target(signode)
|
||||||
|
|
||||||
domain = cast(StandardDomain, self.env.get_domain('std'))
|
domain = cast(StandardDomain, self.env.get_domain('std'))
|
||||||
@ -239,6 +244,14 @@ class Cmdoption(ObjectDescription):
|
|||||||
entry = '; '.join([descr, option])
|
entry = '; '.join([descr, option])
|
||||||
self.indexnode['entries'].append(('pair', entry, signode['ids'][0], '', None))
|
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):
|
class Program(SphinxDirective):
|
||||||
"""
|
"""
|
||||||
|
@ -58,17 +58,13 @@ class ImageCollector(EnvironmentCollector):
|
|||||||
elif imguri.find('://') != -1:
|
elif imguri.find('://') != -1:
|
||||||
candidates['?'] = imguri
|
candidates['?'] = imguri
|
||||||
continue
|
continue
|
||||||
rel_imgpath, full_imgpath = app.env.relfn2path(imguri, docname)
|
|
||||||
if app.config.language:
|
if imguri.endswith(os.extsep + '*'):
|
||||||
# substitute figures (ex. foo.png -> foo.en.png)
|
# Update `node['uri']` to a relative path from srcdir
|
||||||
i18n_full_imgpath = search_image_for_language(full_imgpath, app.env)
|
# from a relative path from current document.
|
||||||
if i18n_full_imgpath != full_imgpath:
|
rel_imgpath, full_imgpath = app.env.relfn2path(imguri, docname)
|
||||||
full_imgpath = i18n_full_imgpath
|
node['uri'] = rel_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 app.config.language:
|
if app.config.language:
|
||||||
# Search language-specific figures at first
|
# Search language-specific figures at first
|
||||||
i18n_imguri = get_image_filename_for_language(imguri, app.env)
|
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)
|
self.collect_candidates(app.env, full_imgpath, candidates, node)
|
||||||
else:
|
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
|
# map image paths to unique image names (so that they can be put
|
||||||
# into a single directory)
|
# into a single directory)
|
||||||
|
@ -252,7 +252,9 @@ class Autosummary(SphinxDirective):
|
|||||||
tree_prefix = self.options['toctree'].strip()
|
tree_prefix = self.options['toctree'].strip()
|
||||||
docnames = []
|
docnames = []
|
||||||
excluded = Matcher(self.config.exclude_patterns)
|
excluded = Matcher(self.config.exclude_patterns)
|
||||||
|
filename_map = self.config.autosummary_filename_map
|
||||||
for name, sig, summary, real_name in items:
|
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.join(tree_prefix, real_name)
|
||||||
docname = posixpath.normpath(posixpath.join(dirname, docname))
|
docname = posixpath.normpath(posixpath.join(dirname, docname))
|
||||||
if docname not in self.env.found_docs:
|
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.add_role('autolink', AutoLink())
|
||||||
app.connect('builder-inited', process_generate_options)
|
app.connect('builder-inited', process_generate_options)
|
||||||
app.add_config_value('autosummary_context', {}, True)
|
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', [], True, [bool])
|
||||||
app.add_config_value('autosummary_generate_overwrite', True, False)
|
app.add_config_value('autosummary_generate_overwrite', True, False)
|
||||||
app.add_config_value('autosummary_mock_imports',
|
app.add_config_value('autosummary_mock_imports',
|
||||||
|
@ -74,6 +74,7 @@ class DummyApplication:
|
|||||||
self.warningiserror = False
|
self.warningiserror = False
|
||||||
|
|
||||||
self.config.add('autosummary_context', {}, True, None)
|
self.config.add('autosummary_context', {}, True, None)
|
||||||
|
self.config.add('autosummary_filename_map', {}, True, None)
|
||||||
self.config.init_values()
|
self.config.init_values()
|
||||||
|
|
||||||
def emit_firstresult(self, *args: Any) -> None:
|
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
|
# keep track of new files
|
||||||
new_files = []
|
new_files = []
|
||||||
|
|
||||||
|
if app:
|
||||||
|
filename_map = app.config.autosummary_filename_map
|
||||||
|
else:
|
||||||
|
filename_map = {}
|
||||||
|
|
||||||
# write
|
# write
|
||||||
for entry in sorted(set(items), key=str):
|
for entry in sorted(set(items), key=str):
|
||||||
if entry.path is None:
|
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,
|
imported_members, app, entry.recursive, context,
|
||||||
modname, qualname)
|
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):
|
if os.path.isfile(filename):
|
||||||
with open(filename, encoding=encoding) as f:
|
with open(filename, encoding=encoding) as f:
|
||||||
old_content = f.read()
|
old_content = f.read()
|
||||||
|
@ -41,6 +41,7 @@ class Config:
|
|||||||
napoleon_use_param = True
|
napoleon_use_param = True
|
||||||
napoleon_use_rtype = True
|
napoleon_use_rtype = True
|
||||||
napoleon_use_keyword = True
|
napoleon_use_keyword = True
|
||||||
|
napoleon_type_aliases = None
|
||||||
napoleon_custom_sections = None
|
napoleon_custom_sections = None
|
||||||
|
|
||||||
.. _Google style:
|
.. _Google style:
|
||||||
@ -236,6 +237,10 @@ class Config:
|
|||||||
|
|
||||||
:returns: *bool* -- True if successful, False otherwise
|
: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)
|
napoleon_custom_sections : :obj:`list` (Defaults to None)
|
||||||
Add a list of custom sections to include, expanding the list of parsed sections.
|
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_param': (True, 'env'),
|
||||||
'napoleon_use_rtype': (True, 'env'),
|
'napoleon_use_rtype': (True, 'env'),
|
||||||
'napoleon_use_keyword': (True, 'env'),
|
'napoleon_use_keyword': (True, 'env'),
|
||||||
|
'napoleon_type_aliases': (None, 'env'),
|
||||||
'napoleon_custom_sections': (None, 'env')
|
'napoleon_custom_sections': (None, 'env')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
:license: BSD, see LICENSE for details.
|
:license: BSD, see LICENSE for details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@ -18,13 +19,15 @@ from typing import Any, Callable, Dict, List, Tuple, Union
|
|||||||
from sphinx.application import Sphinx
|
from sphinx.application import Sphinx
|
||||||
from sphinx.config import Config as SphinxConfig
|
from sphinx.config import Config as SphinxConfig
|
||||||
from sphinx.ext.napoleon.iterators import modify_iter
|
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:
|
if False:
|
||||||
# For type annotation
|
# For type annotation
|
||||||
from typing import Type # for python3.5.1
|
from typing import Type # for python3.5.1
|
||||||
|
|
||||||
|
|
||||||
_directive_regex = re.compile(r'\.\. \S+::')
|
_directive_regex = re.compile(r'\.\. \S+::')
|
||||||
_google_section_regex = re.compile(r'^(\s|\w)+:\s*$')
|
_google_section_regex = re.compile(r'^(\s|\w)+:\s*$')
|
||||||
_google_typed_arg_regex = re.compile(r'\s*(.+?)\s*\(\s*(.*[^\s]+)\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(
|
_xref_or_code_regex = re.compile(
|
||||||
r'((?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)|'
|
r'((?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)|'
|
||||||
r'(?:``.+``))')
|
r'(?:``.+``))')
|
||||||
|
_xref_regex = re.compile(
|
||||||
|
r'(?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)'
|
||||||
|
)
|
||||||
_bullet_list_regex = re.compile(r'^(\*|\+|\-)(\s+\S|\s*$)')
|
_bullet_list_regex = re.compile(r'^(\*|\+|\-)(\s+\S|\s*$)')
|
||||||
_enumerated_list_regex = re.compile(
|
_enumerated_list_regex = re.compile(
|
||||||
r'^(?P<paren>\()?'
|
r'^(?P<paren>\()?'
|
||||||
r'(\d+|#|[ivxlcdm]+|[IVXLCDM]+|[a-zA-Z])'
|
r'(\d+|#|[ivxlcdm]+|[IVXLCDM]+|[a-zA-Z])'
|
||||||
r'(?(paren)\)|\.)(\s+\S|\s*$)')
|
r'(?(paren)\)|\.)(\s+\S|\s*$)')
|
||||||
|
_token_regex = re.compile(
|
||||||
|
r"(\sor\s|\sof\s|:\s|,\s|[{]|[}]"
|
||||||
|
r'|"(?:\\"|[^"])*"'
|
||||||
|
r"|'(?:\\'|[^'])*')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GoogleDocstring:
|
class GoogleDocstring:
|
||||||
@ -780,6 +791,165 @@ class GoogleDocstring:
|
|||||||
return lines
|
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):
|
class NumpyDocstring(GoogleDocstring):
|
||||||
"""Convert NumPy style docstrings to reStructuredText.
|
"""Convert NumPy style docstrings to reStructuredText.
|
||||||
|
|
||||||
@ -879,6 +1049,15 @@ class NumpyDocstring(GoogleDocstring):
|
|||||||
self._directive_sections = ['.. index::']
|
self._directive_sections = ['.. index::']
|
||||||
super().__init__(docstring, config, app, what, name, obj, options)
|
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
|
def _consume_field(self, parse_type: bool = True, prefer_type: bool = False
|
||||||
) -> Tuple[str, str, List[str]]:
|
) -> Tuple[str, str, List[str]]:
|
||||||
line = next(self._line_iter)
|
line = next(self._line_iter)
|
||||||
@ -888,6 +1067,12 @@ class NumpyDocstring(GoogleDocstring):
|
|||||||
_name, _type = line, ''
|
_name, _type = line, ''
|
||||||
_name, _type = _name.strip(), _type.strip()
|
_name, _type = _name.strip(), _type.strip()
|
||||||
_name = self._escape_args_and_kwargs(_name)
|
_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:
|
if prefer_type and not _type:
|
||||||
_type, _name = _name, _type
|
_type, _name = _name, _type
|
||||||
|
@ -117,7 +117,7 @@ var Stemmer = function() {
|
|||||||
len(word) == 0 or not (
|
len(word) == 0 or not (
|
||||||
((len(word) < 3) and (12353 < ord(word[0]) < 12436)) or
|
((len(word) < 3) and (12353 < ord(word[0]) < 12436)) or
|
||||||
(ord(word[0]) < 256 and (
|
(ord(word[0]) < 256 and (
|
||||||
len(word) < 3 or word in self.stopwords or word.isdigit()
|
len(word) < 3 or word in self.stopwords
|
||||||
))))
|
))))
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,7 +105,8 @@ class BuildDoc(Command):
|
|||||||
self.config_dir = None # type: str
|
self.config_dir = None # type: str
|
||||||
self.link_index = False
|
self.link_index = False
|
||||||
self.copyright = ''
|
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.traceback = False
|
||||||
self.nitpicky = False
|
self.nitpicky = False
|
||||||
self.keep_going = False
|
self.keep_going = False
|
||||||
|
@ -79,7 +79,7 @@ def app_params(request: Any, test_params: Dict, shared_result: SharedResult,
|
|||||||
|
|
||||||
if test_params['shared_result']:
|
if test_params['shared_result']:
|
||||||
if 'srcdir' in kwargs:
|
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.')
|
'srcdir in same time.')
|
||||||
kwargs['srcdir'] = test_params['shared_result']
|
kwargs['srcdir'] = test_params['shared_result']
|
||||||
restore = shared_result.restore(test_params['shared_result'])
|
restore = shared_result.restore(test_params['shared_result'])
|
||||||
|
@ -166,8 +166,7 @@ var Search = {
|
|||||||
objectterms.push(tmp[i].toLowerCase());
|
objectterms.push(tmp[i].toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i].match(/^\d+$/) ||
|
if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i] === "") {
|
||||||
tmp[i] === "") {
|
|
||||||
// skip this "word"
|
// skip this "word"
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -459,7 +459,7 @@ class SphinxTranslator(nodes.NodeVisitor):
|
|||||||
The priority of visitor method is:
|
The priority of visitor method is:
|
||||||
|
|
||||||
1. ``self.visit_{node_class}()``
|
1. ``self.visit_{node_class}()``
|
||||||
2. ``self.visit_{supre_node_class}()``
|
2. ``self.visit_{super_node_class}()``
|
||||||
3. ``self.unknown_visit()``
|
3. ``self.unknown_visit()``
|
||||||
"""
|
"""
|
||||||
for node_class in node.__class__.__mro__:
|
for node_class in node.__class__.__mro__:
|
||||||
|
@ -320,8 +320,8 @@ def search_image_for_language(filename: str, env: "BuildEnvironment") -> str:
|
|||||||
return filename
|
return filename
|
||||||
|
|
||||||
translated = get_image_filename_for_language(filename, env)
|
translated = get_image_filename_for_language(filename, env)
|
||||||
dirname = path.dirname(env.docname)
|
_, abspath = env.relfn2path(translated)
|
||||||
if path.exists(path.join(env.srcdir, dirname, translated)):
|
if path.exists(abspath):
|
||||||
return translated
|
return translated
|
||||||
else:
|
else:
|
||||||
return filename
|
return filename
|
||||||
|
@ -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
|
def evaluate_signature(sig: inspect.Signature, globalns: Dict = None, localns: Dict = None
|
||||||
) -> inspect.Signature:
|
) -> inspect.Signature:
|
||||||
"""Evaluate unresolved type annotations in a signature object."""
|
"""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:
|
def evaluate(annotation: Any, globalns: Dict, localns: Dict) -> Any:
|
||||||
"""Evaluate unresolved type annotation."""
|
"""Evaluate unresolved type annotation."""
|
||||||
try:
|
try:
|
||||||
if isinstance(annotation, str):
|
if isinstance(annotation, str):
|
||||||
ref = ForwardRef(annotation, True)
|
ref = ForwardRef(annotation, True)
|
||||||
annotation = ref._evaluate(globalns, localns)
|
annotation = evaluate_forwardref(ref, globalns, localns)
|
||||||
|
|
||||||
if isinstance(annotation, ForwardRef):
|
if isinstance(annotation, ForwardRef):
|
||||||
annotation = annotation._evaluate(globalns, localns)
|
annotation = evaluate_forwardref(ref, globalns, localns)
|
||||||
elif isinstance(annotation, str):
|
elif isinstance(annotation, str):
|
||||||
# might be a ForwardRef'ed annotation in overloaded functions
|
# might be a ForwardRef'ed annotation in overloaded functions
|
||||||
ref = ForwardRef(annotation, True)
|
ref = ForwardRef(annotation, True)
|
||||||
annotation = ref._evaluate(globalns, localns)
|
annotation = evaluate_forwardref(ref, globalns, localns)
|
||||||
except (NameError, TypeError):
|
except (NameError, TypeError):
|
||||||
# failed to evaluate type. skipped.
|
# failed to evaluate type. skipped.
|
||||||
pass
|
pass
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import typing
|
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 import nodes
|
||||||
from docutils.parsers.rst.states import Inliner
|
from docutils.parsers.rst.states import Inliner
|
||||||
@ -164,6 +164,8 @@ def _stringify_py36(annotation: Any) -> str:
|
|||||||
# for Python 3.5.2+
|
# for Python 3.5.2+
|
||||||
if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA
|
if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA
|
||||||
params = annotation.__args__ # type: ignore
|
params = annotation.__args__ # type: ignore
|
||||||
|
elif annotation.__origin__ == Generator: # type: ignore
|
||||||
|
params = annotation.__args__ # type: ignore
|
||||||
else: # typing.Callable
|
else: # typing.Callable
|
||||||
args = ', '.join(stringify(arg) for arg
|
args = ', '.join(stringify(arg) for arg
|
||||||
in annotation.__args__[:-1]) # type: ignore
|
in annotation.__args__[:-1]) # type: ignore
|
||||||
|
@ -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
|
11
tests/roots/test-ext-autosummary-filename-map/conf.py
Normal file
11
tests/roots/test-ext-autosummary-filename-map/conf.py
Normal 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"
|
||||||
|
}
|
9
tests/roots/test-ext-autosummary-filename-map/index.rst
Normal file
9
tests/roots/test-ext-autosummary-filename-map/index.rst
Normal 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
|
@ -11,6 +11,8 @@ Some additional anchors to exercise ignore code
|
|||||||
* `Example Bar invalid <https://www.google.com/#top>`_
|
* `Example Bar invalid <https://www.google.com/#top>`_
|
||||||
* `Example anchor invalid <http://www.sphinx-doc.org/en/1.7/intro.html#does-not-exist>`_
|
* `Example anchor invalid <http://www.sphinx-doc.org/en/1.7/intro.html#does-not-exist>`_
|
||||||
* `Complete nonsense <https://localhost:7777/doesnotexist>`_
|
* `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
|
.. image:: https://www.google.com/image.png
|
||||||
.. figure:: https://www.google.com/image2.png
|
.. figure:: https://www.google.com/image2.png
|
||||||
|
@ -30,7 +30,9 @@ def test_defaults(app, status, warning):
|
|||||||
# images should fail
|
# 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/image.png" in content
|
||||||
assert "Not Found for url: https://www.google.com/image2.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)
|
@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True)
|
||||||
@ -47,8 +49,8 @@ def test_defaults_json(app, status, warning):
|
|||||||
"info"]:
|
"info"]:
|
||||||
assert attr in row
|
assert attr in row
|
||||||
|
|
||||||
assert len(content.splitlines()) == 8
|
assert len(content.splitlines()) == 10
|
||||||
assert len(rows) == 8
|
assert len(rows) == 10
|
||||||
# the output order of the rows is not stable
|
# the output order of the rows is not stable
|
||||||
# due to possible variance in network latency
|
# due to possible variance in network latency
|
||||||
rowsby = {row["uri"]:row for row in rows}
|
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 dnerow['uri'] == 'https://localhost:7777/doesnotexist'
|
||||||
assert rowsby['https://www.google.com/image2.png'] == {
|
assert rowsby['https://www.google.com/image2.png'] == {
|
||||||
'filename': 'links.txt',
|
'filename': 'links.txt',
|
||||||
'lineno': 16,
|
'lineno': 18,
|
||||||
'status': 'broken',
|
'status': 'broken',
|
||||||
'code': 0,
|
'code': 0,
|
||||||
'uri': 'https://www.google.com/image2.png',
|
'uri': 'https://www.google.com/image2.png',
|
||||||
@ -92,7 +94,8 @@ def test_defaults_json(app, status, warning):
|
|||||||
'https://localhost:7777/doesnotexist',
|
'https://localhost:7777/doesnotexist',
|
||||||
'http://www.sphinx-doc.org/en/1.7/intro.html#',
|
'http://www.sphinx-doc.org/en/1.7/intro.html#',
|
||||||
'https://www.google.com/image.png',
|
'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):
|
def test_anchors_ignored(app, status, warning):
|
||||||
app.builder.build_all()
|
app.builder.build_all()
|
||||||
|
@ -296,6 +296,10 @@ def test_macro_definitions():
|
|||||||
check('macro', 'M(arg, ...)', {1: 'M'})
|
check('macro', 'M(arg, ...)', {1: 'M'})
|
||||||
check('macro', 'M(arg1, arg2, ...)', {1: 'M'})
|
check('macro', 'M(arg1, arg2, ...)', {1: 'M'})
|
||||||
check('macro', 'M(arg1, arg2, arg3, ...)', {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():
|
def test_member_definitions():
|
||||||
|
@ -262,6 +262,14 @@ def test_parse_annotation(app):
|
|||||||
[desc_sig_punctuation, ")"],
|
[desc_sig_punctuation, ")"],
|
||||||
[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)
|
doctree = _parse_annotation("Callable[[int, int], int]", app.env)
|
||||||
assert_node(doctree, ([pending_xref, "Callable"],
|
assert_node(doctree, ([pending_xref, "Callable"],
|
||||||
[desc_sig_punctuation, "["],
|
[desc_sig_punctuation, "["],
|
||||||
@ -274,6 +282,12 @@ def test_parse_annotation(app):
|
|||||||
[pending_xref, "int"],
|
[pending_xref, "int"],
|
||||||
[desc_sig_punctuation, "]"]))
|
[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)
|
# None type makes an object-reference (not a class reference)
|
||||||
doctree = _parse_annotation("None", app.env)
|
doctree = _parse_annotation("None", app.env)
|
||||||
assert_node(doctree, ([pending_xref, "None"],))
|
assert_node(doctree, ([pending_xref, "None"],))
|
||||||
|
@ -394,6 +394,20 @@ def test_autosummary_recursive(app, status, warning):
|
|||||||
assert 'package.package.module' in content
|
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)
|
@pytest.mark.sphinx('latex', **default_kw)
|
||||||
def test_autosummary_latex_table_colspec(app, status, warning):
|
def test_autosummary_latex_table_colspec(app, status, warning):
|
||||||
app.builder.build_all()
|
app.builder.build_all()
|
||||||
|
@ -9,13 +9,21 @@
|
|||||||
:license: BSD, see LICENSE for details.
|
:license: BSD, see LICENSE for details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from contextlib import contextmanager
|
||||||
from inspect import cleandoc
|
from inspect import cleandoc
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from unittest import TestCase, mock
|
from unittest import TestCase, mock
|
||||||
|
|
||||||
from sphinx.ext.napoleon import Config
|
from sphinx.ext.napoleon import Config
|
||||||
from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring
|
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'))):
|
class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))):
|
||||||
@ -1976,6 +1984,154 @@ definition_after_normal_text : int
|
|||||||
actual = str(NumpyDocstring(docstring, config))
|
actual = str(NumpyDocstring(docstring, config))
|
||||||
self.assertEqual(expected, actual)
|
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):
|
def test_keywords_with_types(self):
|
||||||
docstring = """\
|
docstring = """\
|
||||||
Do as you please
|
Do as you please
|
||||||
@ -1991,3 +2147,38 @@ Do as you please
|
|||||||
:kwtype gotham_is_yours: None
|
:kwtype gotham_is_yours: None
|
||||||
"""
|
"""
|
||||||
self.assertEqual(expected, actual)
|
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)
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from numbers import Integral
|
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
|
import pytest
|
||||||
|
|
||||||
@ -48,6 +50,7 @@ def test_stringify_type_hints_containers():
|
|||||||
assert stringify(Tuple[str, ...]) == "Tuple[str, ...]"
|
assert stringify(Tuple[str, ...]) == "Tuple[str, ...]"
|
||||||
assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]"
|
assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]"
|
||||||
assert stringify(MyList[Tuple[int, int]]) == "test_util_typing.MyList[Tuple[int, int]]"
|
assert stringify(MyList[Tuple[int, int]]) == "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.')
|
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
|
||||||
|
@ -50,6 +50,9 @@ def lint(path: str) -> int:
|
|||||||
if re.match(r'^\s*\.\. ', line):
|
if re.match(r'^\s*\.\. ', line):
|
||||||
# ignore directives and hyperlink targets
|
# ignore directives and hyperlink targets
|
||||||
pass
|
pass
|
||||||
|
elif re.match(r'^\s*``[^`]+``$', line):
|
||||||
|
# ignore a very long literal string
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
print('%s:%d: the line is too long (%d > %d).' %
|
print('%s:%d: the line is too long (%d > %d).' %
|
||||||
(path, i + 1, len(line), MAX_LINE_LENGTH))
|
(path, i + 1, len(line), MAX_LINE_LENGTH))
|
||||||
|
Loading…
Reference in New Issue
Block a user