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