From 8709791007cf4c8cbc745e314ac6d3d5f6d24ccf Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 26 Jun 2024 14:57:38 -0500 Subject: [PATCH] Backport PR #28289: Promote mpltype Sphinx role to a public extension --- doc/api/index.rst | 1 + .../next_api_changes/development/28289-ES.rst | 7 + doc/api/sphinxext_roles.rst | 7 + doc/conf.py | 2 +- doc/sphinxext/custom_roles.py | 89 ----------- lib/matplotlib/sphinxext/meson.build | 1 + lib/matplotlib/sphinxext/roles.py | 147 ++++++++++++++++++ pyproject.toml | 4 +- 8 files changed, 166 insertions(+), 92 deletions(-) create mode 100644 doc/api/next_api_changes/development/28289-ES.rst create mode 100644 doc/api/sphinxext_roles.rst delete mode 100644 doc/sphinxext/custom_roles.py create mode 100644 lib/matplotlib/sphinxext/roles.py diff --git a/doc/api/index.rst b/doc/api/index.rst index e55a0ed3c5b2..70c3b5343e7a 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -126,6 +126,7 @@ Alphabetical list of modules: sphinxext_mathmpl_api.rst sphinxext_plot_directive_api.rst sphinxext_figmpl_directive_api.rst + sphinxext_roles.rst spines_api.rst style_api.rst table_api.rst diff --git a/doc/api/next_api_changes/development/28289-ES.rst b/doc/api/next_api_changes/development/28289-ES.rst new file mode 100644 index 000000000000..f891c63a64bf --- /dev/null +++ b/doc/api/next_api_changes/development/28289-ES.rst @@ -0,0 +1,7 @@ +Documentation-specific custom Sphinx roles are now semi-public +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For third-party packages that derive types from Matplotlib, our use of custom roles may +prevent Sphinx from building their docs. These custom Sphinx roles are now public solely +for the purposes of use within projects that derive from Matplotlib types. See +:mod:`matplotlib.sphinxext.roles` for details. diff --git a/doc/api/sphinxext_roles.rst b/doc/api/sphinxext_roles.rst new file mode 100644 index 000000000000..99959ff05d14 --- /dev/null +++ b/doc/api/sphinxext_roles.rst @@ -0,0 +1,7 @@ +============================== +``matplotlib.sphinxext.roles`` +============================== + +.. automodule:: matplotlib.sphinxext.roles + :no-undoc-members: + :private-members: _rcparam_role, _mpltype_role diff --git a/doc/conf.py b/doc/conf.py index 92d78f896ca2..f43806a8b4c0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -116,9 +116,9 @@ def _parse_skip_subdirs_file(): 'sphinx_gallery.gen_gallery', 'matplotlib.sphinxext.mathmpl', 'matplotlib.sphinxext.plot_directive', + 'matplotlib.sphinxext.roles', 'matplotlib.sphinxext.figmpl_directive', 'sphinxcontrib.inkscapeconverter', - 'sphinxext.custom_roles', 'sphinxext.github', 'sphinxext.math_symbol_table', 'sphinxext.missing_references', diff --git a/doc/sphinxext/custom_roles.py b/doc/sphinxext/custom_roles.py deleted file mode 100644 index d76c92709865..000000000000 --- a/doc/sphinxext/custom_roles.py +++ /dev/null @@ -1,89 +0,0 @@ -from urllib.parse import urlsplit, urlunsplit - -from docutils import nodes - -from matplotlib import rcParamsDefault - - -class QueryReference(nodes.Inline, nodes.TextElement): - """ - Wraps a reference or pending reference to add a query string. - - The query string is generated from the attributes added to this node. - - Also equivalent to a `~docutils.nodes.literal` node. - """ - - def to_query_string(self): - """Generate query string from node attributes.""" - return '&'.join(f'{name}={value}' for name, value in self.attlist()) - - -def visit_query_reference_node(self, node): - """ - Resolve *node* into query strings on its ``reference`` children. - - Then act as if this is a `~docutils.nodes.literal`. - """ - query = node.to_query_string() - for refnode in node.findall(nodes.reference): - uri = urlsplit(refnode['refuri'])._replace(query=query) - refnode['refuri'] = urlunsplit(uri) - - self.visit_literal(node) - - -def depart_query_reference_node(self, node): - """ - Act as if this is a `~docutils.nodes.literal`. - """ - self.depart_literal(node) - - -def rcparam_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - # Generate a pending cross-reference so that Sphinx will ensure this link - # isn't broken at some point in the future. - title = f'rcParams["{text}"]' - target = 'matplotlibrc-sample' - ref_nodes, messages = inliner.interpreted(title, f'{title} <{target}>', - 'ref', lineno) - - qr = QueryReference(rawtext, highlight=text) - qr += ref_nodes - node_list = [qr] - - # The default backend would be printed as "agg", but that's not correct (as - # the default is actually determined by fallback). - if text in rcParamsDefault and text != "backend": - node_list.extend([ - nodes.Text(' (default: '), - nodes.literal('', repr(rcParamsDefault[text])), - nodes.Text(')'), - ]) - - return node_list, messages - - -def mpltype_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - mpltype = text - type_to_link_target = { - 'color': 'colors_def', - } - if mpltype not in type_to_link_target: - raise ValueError(f"Unknown mpltype: {mpltype!r}") - - node_list, messages = inliner.interpreted( - mpltype, f'{mpltype} <{type_to_link_target[mpltype]}>', 'ref', lineno) - return node_list, messages - - -def setup(app): - app.add_role("rc", rcparam_role) - app.add_role("mpltype", mpltype_role) - app.add_node( - QueryReference, - html=(visit_query_reference_node, depart_query_reference_node), - latex=(visit_query_reference_node, depart_query_reference_node), - text=(visit_query_reference_node, depart_query_reference_node), - ) - return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/lib/matplotlib/sphinxext/meson.build b/lib/matplotlib/sphinxext/meson.build index 5dc7388384eb..35bb96fecbe1 100644 --- a/lib/matplotlib/sphinxext/meson.build +++ b/lib/matplotlib/sphinxext/meson.build @@ -3,6 +3,7 @@ python_sources = [ 'figmpl_directive.py', 'mathmpl.py', 'plot_directive.py', + 'roles.py', ] typing_sources = [ diff --git a/lib/matplotlib/sphinxext/roles.py b/lib/matplotlib/sphinxext/roles.py new file mode 100644 index 000000000000..301adcd8a5f5 --- /dev/null +++ b/lib/matplotlib/sphinxext/roles.py @@ -0,0 +1,147 @@ +""" +Custom roles for the Matplotlib documentation. + +.. warning:: + + These roles are considered semi-public. They are only intended to be used in + the Matplotlib documentation. + +However, it can happen that downstream packages end up pulling these roles into +their documentation, which will result in documentation build errors. The following +describes the exact mechanism and how to fix the errors. + +There are two ways, Matplotlib docstrings can end up in downstream documentation. +You have to subclass a Matplotlib class and either use the ``:inherited-members:`` +option in your autodoc configuration, or you have to override a method without +specifying a new docstring; the new method will inherit the original docstring and +still render in your autodoc. If the docstring contains one of the custom sphinx +roles, you'll see one of the following error messages: + +.. code-block:: none + + Unknown interpreted text role "mpltype". + Unknown interpreted text role "rc". + +To fix this, you can add this module as extension to your sphinx :file:`conf.py`:: + + extensions = [ + 'matplotlib.sphinxext.roles', + # Other extensions. + ] + +.. warning:: + + Direct use of these roles in other packages is not officially supported. We + reserve the right to modify or remove these roles without prior notification. +""" + +from urllib.parse import urlsplit, urlunsplit + +from docutils import nodes + +import matplotlib +from matplotlib import rcParamsDefault + + +class _QueryReference(nodes.Inline, nodes.TextElement): + """ + Wraps a reference or pending reference to add a query string. + + The query string is generated from the attributes added to this node. + + Also equivalent to a `~docutils.nodes.literal` node. + """ + + def to_query_string(self): + """Generate query string from node attributes.""" + return '&'.join(f'{name}={value}' for name, value in self.attlist()) + + +def _visit_query_reference_node(self, node): + """ + Resolve *node* into query strings on its ``reference`` children. + + Then act as if this is a `~docutils.nodes.literal`. + """ + query = node.to_query_string() + for refnode in node.findall(nodes.reference): + uri = urlsplit(refnode['refuri'])._replace(query=query) + refnode['refuri'] = urlunsplit(uri) + + self.visit_literal(node) + + +def _depart_query_reference_node(self, node): + """ + Act as if this is a `~docutils.nodes.literal`. + """ + self.depart_literal(node) + + +def _rcparam_role(name, rawtext, text, lineno, inliner, options=None, content=None): + """ + Sphinx role ``:rc:`` to highlight and link ``rcParams`` entries. + + Usage: Give the desired ``rcParams`` key as parameter. + + :code:`:rc:`figure.dpi`` will render as: :rc:`figure.dpi` + """ + # Generate a pending cross-reference so that Sphinx will ensure this link + # isn't broken at some point in the future. + title = f'rcParams["{text}"]' + target = 'matplotlibrc-sample' + ref_nodes, messages = inliner.interpreted(title, f'{title} <{target}>', + 'ref', lineno) + + qr = _QueryReference(rawtext, highlight=text) + qr += ref_nodes + node_list = [qr] + + # The default backend would be printed as "agg", but that's not correct (as + # the default is actually determined by fallback). + if text in rcParamsDefault and text != "backend": + node_list.extend([ + nodes.Text(' (default: '), + nodes.literal('', repr(rcParamsDefault[text])), + nodes.Text(')'), + ]) + + return node_list, messages + + +def _mpltype_role(name, rawtext, text, lineno, inliner, options=None, content=None): + """ + Sphinx role ``:mpltype:`` for custom matplotlib types. + + In Matplotlib, there are a number of type-like concepts that do not have a + direct type representation; example: color. This role allows to properly + highlight them in the docs and link to their definition. + + Currently supported values: + + - :code:`:mpltype:`color`` will render as: :mpltype:`color` + + """ + mpltype = text + type_to_link_target = { + 'color': 'colors_def', + } + if mpltype not in type_to_link_target: + raise ValueError(f"Unknown mpltype: {mpltype!r}") + + node_list, messages = inliner.interpreted( + mpltype, f'{mpltype} <{type_to_link_target[mpltype]}>', 'ref', lineno) + return node_list, messages + + +def setup(app): + app.add_role("rc", _rcparam_role) + app.add_role("mpltype", _mpltype_role) + app.add_node( + _QueryReference, + html=(_visit_query_reference_node, _depart_query_reference_node), + latex=(_visit_query_reference_node, _depart_query_reference_node), + text=(_visit_query_reference_node, _depart_query_reference_node), + ) + return {"version": matplotlib.__version__, + "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/pyproject.toml b/pyproject.toml index a9fb7df68450..52bbe308c0f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -283,11 +283,11 @@ ignore_directives = [ "include" ] ignore_roles = [ - # sphinxext.custom_roles - "rc", # matplotlib.sphinxext.mathmpl "mathmpl", "math-stix", + # matplotlib.sphinxext.roles + "rc", # sphinxext.github "ghissue", "ghpull",