summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHassan Kibirige <has2k1@gmail.com>2017-12-29 20:09:43 -0600
committerEric Larson <larson.eric.d@gmail.com>2019-04-17 21:21:19 -0400
commit477ff10470784baa344229fde5880e4e2bffb545 (patch)
treefc396f488ccd60818db465e314c096ec937627fd
parentc2e8b8f5fea8f0adde207436869cec0841e85262 (diff)
downloadnumpydoc-477ff10470784baa344229fde5880e4e2bffb545.tar.gz
Add cross-reference links to parameter types
Tokens of the type description that are determined to be "link-worthy" are enclosed in a new role called `xref_param_type`. This role when when processed adds a `pending_xref` node to the DOM. If these types cross-references are not resolved when the build ends, sphinx does not complain. This forgives errors made when deciding whether tokens are "link-worthy". And provided text from the type description is not lost in the processing, the only unwanted outcome is a type link (due to coincidence) when none was desired.
-rw-r--r--doc/install.rst47
-rw-r--r--numpydoc/docscrape_sphinx.py24
-rw-r--r--numpydoc/numpydoc.py11
-rw-r--r--numpydoc/tests/test_docscrape.py79
-rw-r--r--numpydoc/tests/test_xref.py128
-rw-r--r--numpydoc/xref.py185
6 files changed, 469 insertions, 5 deletions
diff --git a/doc/install.rst b/doc/install.rst
index 1fa0fde..c6d6209 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -47,6 +47,53 @@ numpydoc_attributes_as_param_list : bool
as the Parameter section. If it's False, the Attributes section will be
formatted as the Methods section using an autosummary table.
``True`` by default.
+numpydoc_xref_param_type : bool
+ Whether to create cross-references for the parameter types in the
+ ``Parameters``, ``Other Parameters``, ``Returns`` and ``Yields``
+ sections of the docstring.
+ ``True`` by default.
+numpydoc_xref_aliases : dict
+ Mappings to fully qualified paths (or correct ReST references) for the
+ aliases/shortcuts used when specifying the types of parameters.
+ The keys should not have any spaces. Together with the ``intersphinx``
+ extension, you can map to links in any documentation.
+ The default is an empty ``dict``.
+
+ If you have the following ``intersphinx`` namespace configuration::
+
+ intersphinx_mapping = {
+ 'python': ('https://docs.python.org/3/', None),
+ 'numpy': ('https://docs.scipy.org/doc/numpy', None),
+ }
+
+ A useful ``dict`` may look like the following::
+
+ numpydoc_xref_aliases = {
+ # python
+ 'sequence': ':term:`python:sequence`',
+ 'iterable': ':term:`python:iterable`',
+ 'string': 'str',
+ # numpy
+ 'array': 'numpy.ndarray',
+ 'dtype': 'numpy.dtype',
+ 'ndarray': 'numpy.ndarray',
+ 'matrix': 'numpy.matrix',
+ 'array-like': ':term:`numpy:array_like`',
+ 'array_like': ':term:`numpy:array_like`',
+ }
+
+ This option depends on the ``numpydoc_xref_param_type`` option
+ being ``True``.
+
+numpydoc_xref_ignore : set
+ Words not to cross-reference. Most likely, these are common words
+ used in parameter type descriptions that may be confused for
+ classes of the same name. For example::
+
+ numpydoc_xref_ignore = {'type', 'optional', 'default'}
+
+ The default is an empty set.
+
numpydoc_edit_link : bool
.. deprecated:: edit your HTML template instead
diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py
index 5f7843b..942c78f 100644
--- a/numpydoc/docscrape_sphinx.py
+++ b/numpydoc/docscrape_sphinx.py
@@ -17,6 +17,7 @@ import sphinx
from sphinx.jinja2glue import BuiltinTemplateLoader
from .docscrape import NumpyDocString, FunctionDoc, ClassDoc
+from .xref import make_xref_param_type
if sys.version_info[0] >= 3:
sixu = lambda s: s
@@ -37,6 +38,9 @@ class SphinxDocString(NumpyDocString):
self.use_blockquotes = config.get('use_blockquotes', False)
self.class_members_toctree = config.get('class_members_toctree', True)
self.attributes_as_param_list = config.get('attributes_as_param_list', True)
+ self.xref_param_type = config.get('xref_param_type', False)
+ self.xref_aliases = config.get('xref_aliases', dict())
+ self.xref_ignore = config.get('xref_ignore', set())
self.template = config.get('template', None)
if self.template is None:
template_dirs = [os.path.join(os.path.dirname(__file__), 'templates')]
@@ -79,11 +83,17 @@ class SphinxDocString(NumpyDocString):
out += self._str_field_list(name)
out += ['']
for param in self[name]:
+ param_type = param.type
+ if param_type and self.xref_param_type:
+ param_type = make_xref_param_type(
+ param_type,
+ self.xref_aliases,
+ self.xref_ignore)
if param.name:
out += self._str_indent([named_fmt % (param.name.strip(),
- param.type)])
+ param_type)])
else:
- out += self._str_indent([unnamed_fmt % param.type.strip()])
+ out += self._str_indent([unnamed_fmt % param_type.strip()])
if not param.desc:
out += self._str_indent(['..'], 8)
else:
@@ -213,8 +223,14 @@ class SphinxDocString(NumpyDocString):
parts = []
if display_param:
parts.append(display_param)
- if param.type:
- parts.append(param.type)
+ param_type = param.type
+ if param_type:
+ if self.xref_param_type:
+ param_type = make_xref_param_type(
+ param_type,
+ self.xref_aliases,
+ self.xref_ignore)
+ parts.append(param_type)
out += self._str_indent([' : '.join(parts)])
if desc and self.use_blockquotes:
diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py
index f6f262c..b8e993f 100644
--- a/numpydoc/numpydoc.py
+++ b/numpydoc/numpydoc.py
@@ -37,6 +37,7 @@ if sphinx.__version__ < '1.0.1':
raise RuntimeError("Sphinx 1.0.1 or newer is required")
from .docscrape_sphinx import get_doc_object
+from .xref import xref_param_type_role
from . import __version__
if sys.version_info[0] >= 3:
@@ -154,7 +155,11 @@ def mangle_docstrings(app, what, name, obj, options, lines):
app.config.numpydoc_show_inherited_class_members,
'class_members_toctree': app.config.numpydoc_class_members_toctree,
'attributes_as_param_list':
- app.config.numpydoc_attributes_as_param_list}
+ app.config.numpydoc_attributes_as_param_list,
+ 'xref_param_type': app.config.numpydoc_xref_param_type,
+ 'xref_aliases': app.config.numpydoc_xref_aliases,
+ 'xref_ignore': app.config.numpydoc_xref_ignore,
+ }
cfg.update(options or {})
u_NL = sixu('\n')
@@ -218,6 +223,7 @@ def setup(app, get_doc_object_=get_doc_object):
app.setup_extension('sphinx.ext.autosummary')
+ app.add_role('xref_param_type', xref_param_type_role)
app.connect('autodoc-process-docstring', mangle_docstrings)
app.connect('autodoc-process-signature', mangle_signature)
app.connect('doctree-read', relabel_references)
@@ -230,6 +236,9 @@ def setup(app, get_doc_object_=get_doc_object):
app.add_config_value('numpydoc_class_members_toctree', True, True)
app.add_config_value('numpydoc_citation_re', '[a-z0-9_.-]+', True)
app.add_config_value('numpydoc_attributes_as_param_list', True, True)
+ app.add_config_value('numpydoc_xref_param_type', True, True)
+ app.add_config_value('numpydoc_xref_aliases', dict(), True)
+ app.add_config_value('numpydoc_xref_ignore', set(), True)
# Extra mangling domains
app.add_domain(NumpyPythonDomain)
diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py
index c6f9d08..2f5a688 100644
--- a/numpydoc/tests/test_docscrape.py
+++ b/numpydoc/tests/test_docscrape.py
@@ -1423,6 +1423,85 @@ A top section before
''')
+xref_doc_txt = """
+Test xref in Parameters, Other Parameters and Returns
+
+Parameters
+----------
+p1 : int
+ Integer value
+
+p2 : float, optional
+ Integer value
+
+Other Parameters
+----------------
+p3 : list[int]
+ List of integers
+p4 : :class:`pandas.DataFrame`
+ A dataframe
+p5 : sequence of int
+ A sequence
+
+Returns
+-------
+out : array
+ Numerical return value
+"""
+
+
+xref_doc_txt_expected = """
+Test xref in Parameters, Other Parameters and Returns
+
+
+:Parameters:
+
+ p1 : :xref_param_type:`int`
+ Integer value
+
+ p2 : :xref_param_type:`float`, optional
+ Integer value
+
+:Returns:
+
+ out : :xref_param_type:`array <numpy.ndarray>`
+ Numerical return value
+
+
+:Other Parameters:
+
+ p3 : :xref_param_type:`list`\[:xref_param_type:`int`]
+ List of integers
+
+ p4 : :class:`pandas.DataFrame`
+ A dataframe
+
+ p5 : :term:`python:sequence` of :xref_param_type:`int`
+ A sequence
+"""
+
+
+def test_xref():
+ xref_aliases = {
+ 'sequence': ':term:`python:sequence`',
+ 'iterable': ':term:`python:iterable`',
+ 'array': 'numpy.ndarray',
+ }
+
+ xref_ignore = {'of', 'default', 'optional'}
+
+ doc = SphinxDocString(
+ xref_doc_txt,
+ config=dict(
+ xref_param_type=True,
+ xref_aliases=xref_aliases,
+ xref_ignore=xref_ignore
+ )
+ )
+
+ line_by_line_compare(str(doc), xref_doc_txt_expected)
+
+
if __name__ == "__main__":
import pytest
pytest.main()
diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py
new file mode 100644
index 0000000..8786d4b
--- /dev/null
+++ b/numpydoc/tests/test_xref.py
@@ -0,0 +1,128 @@
+# -*- encoding:utf-8 -*-
+from __future__ import division, absolute_import, print_function
+
+from nose.tools import assert_equal
+from numpydoc.xref import make_xref_param_type
+
+xref_aliases = {
+ # python
+ 'sequence': ':term:`python:sequence`',
+ 'iterable': ':term:`python:iterable`',
+ 'string': 'str',
+ # numpy
+ 'array': 'numpy.ndarray',
+ 'dtype': 'numpy.dtype',
+ 'ndarray': 'numpy.ndarray',
+ 'matrix': 'numpy.matrix',
+ 'array-like': ':term:`numpy:array_like`',
+ 'array_like': ':term:`numpy:array_like`',
+}
+
+# Comes mainly from numpy
+data = """
+(...) array_like, float, optional
+(...) :term:`numpy:array_like`, :xref_param_type:`float`, optional
+
+(2,) ndarray
+(2,) :xref_param_type:`ndarray <numpy.ndarray>`
+
+(...,M,N) array_like
+(...,M,N) :term:`numpy:array_like`
+
+(..., M, N) array_like
+(..., :xref_param_type:`M`, :xref_param_type:`N`) :term:`numpy:array_like`
+
+(float, float), optional
+(:xref_param_type:`float`, :xref_param_type:`float`), optional
+
+1-D array or sequence
+1-D :xref_param_type:`array <numpy.ndarray>` or :term:`python:sequence`
+
+array of str or unicode-like
+:xref_param_type:`array <numpy.ndarray>` of :xref_param_type:`str` or unicode-like
+
+array_like of float
+:term:`numpy:array_like` of :xref_param_type:`float`
+
+bool or callable
+:xref_param_type:`bool` or :xref_param_type:`callable`
+
+int in [0, 255]
+:xref_param_type:`int` in [0, 255]
+
+int or None, optional
+:xref_param_type:`int` or :xref_param_type:`None`, optional
+
+list of str or array_like
+:xref_param_type:`list` of :xref_param_type:`str` or :term:`numpy:array_like`
+
+sequence of array_like
+:term:`python:sequence` of :term:`numpy:array_like`
+
+str or pathlib.Path
+:xref_param_type:`str` or :xref_param_type:`pathlib.Path`
+
+{'', string}, optional
+{'', :xref_param_type:`string <str>`}, optional
+
+{'C', 'F', 'A', or 'K'}, optional
+{'C', 'F', 'A', or 'K'}, optional
+
+{'linear', 'lower', 'higher', 'midpoint', 'nearest'}
+{'linear', 'lower', 'higher', 'midpoint', 'nearest'}
+
+{False, True, 'greedy', 'optimal'}
+{:xref_param_type:`False`, :xref_param_type:`True`, 'greedy', 'optimal'}
+
+{{'begin', 1}, {'end', 0}}, {string, int}
+{{'begin', 1}, {'end', 0}}, {:xref_param_type:`string <str>`, :xref_param_type:`int`}
+
+callable f'(x,*args)
+:xref_param_type:`callable` f'(x,*args)
+
+callable ``fhess(x, *args)``, optional
+:xref_param_type:`callable` ``fhess(x, *args)``, optional
+
+spmatrix (format: ``csr``, ``bsr``, ``dia`` or coo``)
+:xref_param_type:`spmatrix` (format: ``csr``, ``bsr``, ``dia`` or coo``)
+
+:ref:`strftime <strftime-strptime-behavior>`
+:ref:`strftime <strftime-strptime-behavior>`
+
+callable or :ref:`strftime <strftime-strptime-behavior>`
+:xref_param_type:`callable` or :ref:`strftime <strftime-strptime-behavior>`
+
+callable or :ref:`strftime behavior <strftime-strptime-behavior>`
+:xref_param_type:`callable` or :ref:`strftime behavior <strftime-strptime-behavior>`
+
+list(int)
+:xref_param_type:`list`\(:xref_param_type:`int`)
+
+list[int]
+:xref_param_type:`list`\[:xref_param_type:`int`]
+
+dict(str, int)
+:xref_param_type:`dict`\(:xref_param_type:`str`, :xref_param_type:`int`)
+
+dict[str, int]
+:xref_param_type:`dict`\[:xref_param_type:`str`, :xref_param_type:`int`]
+
+tuple(float, float)
+:xref_param_type:`tuple`\(:xref_param_type:`float`, :xref_param_type:`float`)
+
+dict[tuple(str, str), int]
+:xref_param_type:`dict`\[:xref_param_type:`tuple`\(:xref_param_type:`str`, :xref_param_type:`str`), :xref_param_type:`int`]
+""" # noqa: E501
+
+xref_ignore = {'or', 'in', 'of', 'default', 'optional'}
+
+
+def test_make_xref_param_type():
+ for s in data.strip().split('\n\n'):
+ param_type, expected_result = s.split('\n')
+ result = make_xref_param_type(
+ param_type,
+ xref_aliases,
+ xref_ignore
+ )
+ assert_equal(result, expected_result)
diff --git a/numpydoc/xref.py b/numpydoc/xref.py
new file mode 100644
index 0000000..5804a7c
--- /dev/null
+++ b/numpydoc/xref.py
@@ -0,0 +1,185 @@
+import re
+
+from docutils import nodes
+from sphinx import addnodes
+from sphinx.util.nodes import split_explicit_title
+
+# When sphinx (including the napoleon extension) parses the parameters
+# section of a docstring, it converts the information into field lists.
+# Some items in the list are for the parameter type. When the type fields
+# are processed, the text is split and some tokens are turned into
+# pending_xref nodes. These nodes are responsible for creating links.
+#
+# numpydoc does not create field lists, so the type information is
+# not placed into fields that can be processed to make links. Instead,
+# when parsing the type information we identify tokens that are link
+# worthy and wrap them around a special role (xref_param_type_role).
+# When the role is processed, we create pending_xref nodes which are
+# later turned into links.
+
+# Note: we never split on commas that are not followed by a space
+# You risk creating bad rst markup if you do so.
+
+QUALIFIED_NAME_RE = re.compile(
+ # e.g int, numpy.array, ~numpy.array, .class_in_current_module
+ r'^'
+ r'[~\.]?'
+ r'[a-zA-Z_]\w*'
+ r'(?:\.[a-zA-Z_]\w*)*'
+ r'$'
+)
+
+CONTAINER_SPLIT_RE = re.compile(
+ # splits dict(str, int) into
+ # ['dict', '[', 'str', ', ', 'int', ']', '']
+ r'(\s*[\[\]\(\)\{\}]\s*|,\s+)'
+)
+
+CONTAINER_SPLIT_REJECT_RE = re.compile(
+ # Leads to bad markup e.g.
+ # {int}qualified_name
+ r'[\]\)\}]\w'
+)
+
+DOUBLE_QUOTE_SPLIT_RE = re.compile(
+ # splits 'callable ``f(x0, *args)`` or ``f(x0, y0, *args)``' into
+ # ['callable ', '``f(x0, *args)``', ' or ', '``f(x0, y0, *args)``', '']
+ r'(``.+?``)'
+)
+
+ROLE_SPLIT_RE = re.compile(
+ # splits to preserve ReST roles
+ r'(:\w+:`.+?(?<!\\)`)'
+)
+
+SINGLE_QUOTE_SPLIT_RE = re.compile(
+ # splits to preserve quoted expressions roles
+ r'(`.+?`)'
+)
+
+TEXT_SPLIT_RE = re.compile(
+ # splits on ' or ', ' | ', ', ' and ' '
+ r'(\s+or\s+|\s+\|\s+|,\s+|\s+)'
+)
+
+CONTAINER_CHARS = set('[](){}')
+
+
+def make_xref_param_type(param_type, xref_aliases, xref_ignore):
+ """
+ Enclose str in a role that creates a cross-reference
+ The role ``xref_param_type`` *may be* added to any token
+ that looks like type information and no other. The
+ function tries to be clever and catch type information
+ in different disguises.
+
+ Parameters
+ ----------
+ param_type : str
+ text
+ xref_aliases : dict
+ Mapping used to resolve common abbreviations and aliases
+ to fully qualified names that can be cross-referenced.
+ xref_ignore : set
+ Words not to cross-reference.
+
+ Returns
+ -------
+ out : str
+ Text with parts that may be wrapped in a
+ ``xref_param_type`` role.
+ """
+ if param_type in xref_aliases:
+ link, title = xref_aliases[param_type], param_type
+ param_type = link
+ else:
+ link = title = param_type
+
+ if QUALIFIED_NAME_RE.match(link) and link not in xref_ignore:
+ if link != title:
+ return ':xref_param_type:`%s <%s>`' % (title, link)
+ else:
+ return ':xref_param_type:`%s`' % link
+
+ def _split_and_apply_re(s, pattern):
+ """
+ Split string using the regex pattern,
+ apply main function to the parts that do not match the pattern,
+ combine the results
+ """
+ results = []
+ tokens = pattern.split(s)
+ n = len(tokens)
+ if n > 1:
+ for i, tok in enumerate(tokens):
+ if pattern.match(tok):
+ results.append(tok)
+ else:
+ res = make_xref_param_type(
+ tok, xref_aliases, xref_ignore)
+ # Openning brackets immediated after a role is
+ # bad markup. Detect that and add backslash.
+ # :role:`type`( to :role:`type`\(
+ if res and res[-1] == '`' and i < n-1:
+ next_char = tokens[i+1][0]
+ if next_char in '([{':
+ res += '\\'
+ results.append(res)
+
+ return ''.join(results)
+ return s
+
+ # The cases are dealt with in an order the prevents
+ # conflict.
+ # Then the strategy is:
+ # - Identify a pattern we are not interested in
+ # - split off the pattern
+ # - re-apply the function to the other parts
+ # - join the results with the pattern
+
+ # Unsplittable literal
+ if '``' in param_type:
+ return _split_and_apply_re(param_type, DOUBLE_QUOTE_SPLIT_RE)
+
+ # Any roles
+ if ':`' in param_type:
+ return _split_and_apply_re(param_type, ROLE_SPLIT_RE)
+
+ # Any quoted expressions
+ if '`' in param_type:
+ return _split_and_apply_re(param_type, SINGLE_QUOTE_SPLIT_RE)
+
+ # Any sort of bracket '[](){}'
+ if any(c in CONTAINER_CHARS for c in param_type):
+ if CONTAINER_SPLIT_REJECT_RE.search(param_type):
+ return param_type
+ return _split_and_apply_re(param_type, CONTAINER_SPLIT_RE)
+
+ # Common splitter tokens
+ return _split_and_apply_re(param_type, TEXT_SPLIT_RE)
+
+
+def xref_param_type_role(role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ """
+ Add a pending_xref for the param_type of a field list
+ """
+ has_title, title, target = split_explicit_title(text)
+ if has_title:
+ target = target.lstrip('~')
+ else:
+ if target.startswith(('~', '.')):
+ prefix, target = target[0], target[1:]
+ if prefix == '.':
+ env = inliner.document.settings.env
+ modname = env.ref_context.get('py:module')
+ target = target[1:]
+ target = '%s.%s' % (modname, target)
+ elif prefix == '~':
+ title = target.split('.')[-1]
+
+ contnode = nodes.Text(title, title)
+ node = addnodes.pending_xref('', refdomain='py', refexplicit=False,
+ reftype='class', reftarget=target)
+ node += contnode
+ return [node], []