From 616c4b9c6fa976f3294486fd03d5a79ddbef339f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 29 Dec 2017 20:09:43 -0600 Subject: [PATCH 1/7] 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. Added two options: 1. numpydoc_xref_param_type 2. numpydoc_xref_aliases --- doc/install.rst | 38 +++++++ numpydoc/docscrape_sphinx.py | 11 ++ numpydoc/numpydoc.py | 9 +- numpydoc/tests/test_docscrape.py | 76 ++++++++++++++ numpydoc/tests/test_xref.py | 110 ++++++++++++++++++++ numpydoc/xref.py | 169 +++++++++++++++++++++++++++++++ 6 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 numpydoc/tests/test_xref.py create mode 100644 numpydoc/xref.py diff --git a/doc/install.rst b/doc/install.rst index 5658dbba..4b37cf9e 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -42,6 +42,44 @@ numpydoc_use_blockqutoes : bool Until version 0.8, parameter definitions were shown as blockquotes, rather than in a definition list. If your styling requires blockquotes, switch this config option to True. This option will be removed in version 0.10. +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. + 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.array', + '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_edit_link : bool .. deprecated:: edit your HTML template instead diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py index 087ddafb..e3f522e4 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -14,6 +14,7 @@ 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 @@ -33,6 +34,8 @@ def load_config(self, config): self.use_plots = config.get('use_plots', False) self.use_blockquotes = config.get('use_blockquotes', False) self.class_members_toctree = config.get('class_members_toctree', True) + self.xref_param_type = config.get('xref_param_type', False) + self.xref_aliases = config.get('xref_aliases', dict()) self.template = config.get('template', None) if self.template is None: template_dirs = [os.path.join(os.path.dirname(__file__), 'templates')] @@ -80,6 +83,10 @@ def _str_returns(self, name='Returns'): out += [''] for param, param_type, desc in self[name]: if param_type: + if self.xref_param_type: + param_type = make_xref_param_type( + param_type, + self.xref_aliases) out += self._str_indent([typed_fmt % (param.strip(), param_type)]) else: @@ -197,6 +204,10 @@ def _str_param_list(self, name, fake_autosummary=False): fake_autosummary) if param_type: + if self.xref_param_type: + param_type = make_xref_param_type( + param_type, + self.xref_aliases) out += self._str_indent(['%s : %s' % (display_param, param_type)]) else: diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py index 0a6cc79c..438952e9 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -29,6 +29,7 @@ raise RuntimeError("Sphinx 1.0.1 or newer is required") from .docscrape_sphinx import get_doc_object, SphinxDocString +from .xref import xref_param_type_role from . import __version__ if sys.version_info[0] >= 3: @@ -76,7 +77,10 @@ def mangle_docstrings(app, what, name, obj, options, lines): 'show_class_members': app.config.numpydoc_show_class_members, 'show_inherited_class_members': app.config.numpydoc_show_inherited_class_members, - 'class_members_toctree': app.config.numpydoc_class_members_toctree} + 'class_members_toctree': app.config.numpydoc_class_members_toctree, + 'xref_param_type': app.config.numpydoc_xref_param_type, + 'xref_aliases': app.config.numpydoc_xref_aliases, + } u_NL = sixu('\n') if what == 'module': @@ -137,6 +141,7 @@ def setup(app, get_doc_object_=get_doc_object): global get_doc_object get_doc_object = get_doc_object_ + 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.add_config_value('numpydoc_edit_link', None, False) @@ -146,6 +151,8 @@ def setup(app, get_doc_object_=get_doc_object): app.add_config_value('numpydoc_show_inherited_class_members', True, True) 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_xref_param_type', True, True) + app.add_config_value('numpydoc_xref_aliases', dict(), True) # Extra mangling domains app.add_domain(NumpyPythonDomain) diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 21bbe28b..24a829f2 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -1199,6 +1199,82 @@ def test_templated_sections(): """) +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:`~numpy.array` + 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.array', + } + + doc = SphinxDocString( + xref_doc_txt, + config=dict( + xref_param_type=True, + xref_aliases=xref_aliases + ) + ) + + line_by_line_compare(str(doc), xref_doc_txt_expected) + + if __name__ == "__main__": import nose nose.run() diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py new file mode 100644 index 00000000..1b9942d7 --- /dev/null +++ b/numpydoc/tests/test_xref.py @@ -0,0 +1,110 @@ +# -*- 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.array', + '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:`~numpy.ndarray` + +(...,M,N) array_like +(...,M,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:`~numpy.array` or :term:`python:sequence` + +array of str or unicode-like +:xref_param_type:`~numpy.array` 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:`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}}, {string, :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``) + +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 + + +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) + assert_equal(result, expected_result) diff --git a/numpydoc/xref.py b/numpydoc/xref.py new file mode 100644 index 00000000..f3a045bc --- /dev/null +++ b/numpydoc/xref.py @@ -0,0 +1,169 @@ +import re +from sphinx import addnodes + + +QUALIFIED_NAME_RE = re.compile( + # e.g int, numpy.array, ~numpy.array + r'^' + r'[~\.]?' + r'[a-zA-Z_]\w*' + r'(?:\.[a-zA-Z_]\w*)*' + r'$' +) + +CONTAINER_TYPE_RE = re.compile( + # e.g. + # - list[int] + # - dict(str, int) + # - dict[str, int]' + # - tuple(float, float) + # - dict[tuple(str, str), int]' + r'^' + r'(dict|list|tuple)' + r'[\[\(]' + r'(.+?(?:,\s*)?)+' + r'[\]\)]' + r'$' +) + +CONTAINER_SPLIT_RE = re.compile( + # splits dict(str, int) into + # ['dict', '[', 'str', ', ', 'int', ']', ''] + r'(\s*[\[\]\(\),]\s*)' +) + +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'(``.+?``)' +) + +IGNORE = {'of', ' of ', 'either', 'or', 'with', 'in', 'default'} +CONTAINER_CHARS = set('[](){}') + + +def make_xref_param_type(param_type, xref_aliases): + """ + 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. + + Returns + ------- + out : str + Text with parts that may be wrapped in a + ``xref_param_type`` role. + """ + if param_type in xref_aliases: + param_type = xref_aliases[param_type] + + if (QUALIFIED_NAME_RE.match(param_type) and + param_type not in IGNORE): + return ':xref_param_type:`%s`' % param_type + + # Clever stuff below (except the last return) + # can be removed without affecting the basic functionality. + + 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) + if len(tokens) > 1: + for tok in tokens: + if pattern.match(tok): + results.append(tok) + else: + results.append( + make_xref_param_type(tok, xref_aliases)) + + return ''.join(results) + return s + + def _split_and_apply_str(s, on): + """ + Split string s, at the substring on, + apply main function to the splits, + combine the results + """ + return on.join( + make_xref_param_type(s, xref_aliases) + for s in s.split(on)) + + # 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 + + # endswith ', optional' + if param_type.endswith(', optional'): + return '%s, optional' % make_xref_param_type( + param_type[:-10], + xref_aliases) + + # Any sort of bracket '[](){}' + has_container = any(c in CONTAINER_CHARS for c in param_type) + if has_container: + # of the form 'dict[int, float]' + if CONTAINER_TYPE_RE.match(param_type): + return _split_and_apply_re(param_type, CONTAINER_SPLIT_RE) + else: + # of the form '[int, float]' + for start, end in ['[]', '()', '{}']: + if param_type.startswith(start) and param_type.endswith(end): + return '%s%s%s' % ( + start, + make_xref_param_type(param_type[1:-1], xref_aliases), + end) + + # May have an unsplittable literal + if '``' in param_type: + return _split_and_apply_re(param_type, DOUBLE_QUOTE_SPLIT_RE) + + # Is splittable + for splitter in [' or ', ', ', ' ']: + if splitter in param_type: + return _split_and_apply_str(param_type, splitter) + + return param_type + + +def xref_param_type_role(role, rawtext, text, lineno, inliner, + options={}, content=[]): + """ + Add a pending_xref for the param_type of a field list + """ + if text.startswith(('~', '.')): + prefix, target = text[0], text[1:] + if prefix == '.': + env = inliner.document.settings.env + modname = env.ref_context.get('py:module') + text = text[1:] + target = '%s.%s' % (modname, text) + elif prefix == '~': + text = text.split('.')[-1] + else: + target = text + + contnode = addnodes.literal_emphasis(text, text) + node = addnodes.pending_xref('', refdomain='py', refexplicit=False, + reftype='obj', reftarget=target) + node += contnode + return [node], [] From 0f5c33c8f6500b08594e504de9a2de6fbf2ff5de Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 29 Dec 2017 20:09:43 -0600 Subject: [PATCH 2/7] Changes after review - Also changed the role used to create links from `obj` to `class`. --- numpydoc/tests/test_xref.py | 15 ++++-- numpydoc/xref.py | 99 ++++++++++++++----------------------- 2 files changed, 49 insertions(+), 65 deletions(-) diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py index 1b9942d7..56dedf68 100644 --- a/numpydoc/tests/test_xref.py +++ b/numpydoc/tests/test_xref.py @@ -27,7 +27,7 @@ (2,) :xref_param_type:`~numpy.ndarray` (...,M,N) array_like -(...,M,N) :term:`numpy: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 @@ -72,10 +72,10 @@ {:xref_param_type:`False`, :xref_param_type:`True`, 'greedy', 'optimal'} {{'begin', 1}, {'end', 0}}, {string, int} -{{'begin', 1}, {'end', 0}}, {string, :xref_param_type:`int`} +{{'begin', 1}, {'end', 0}}, {:xref_param_type:`str`, :xref_param_type:`int`} callable f'(x,*args) -:xref_param_type:`callable` f'(x,*args) +:xref_param_type:`callable` f'(:xref_param_type:`x`,*args) callable ``fhess(x, *args)``, optional :xref_param_type:`callable` ``fhess(x, *args)``, optional @@ -83,6 +83,15 @@ spmatrix (format: ``csr``, ``bsr``, ``dia`` or coo``) :xref_param_type:`spmatrix` (format: ``csr``, ``bsr``, ``dia`` or coo``) +:ref:`strftime ` +:ref:`strftime ` + +callable or :ref:`strftime ` +:xref_param_type:`callable` or :ref:`strftime ` + +callable or :ref:`strftime behavior ` +:xref_param_type:`callable` or :ref:`strftime behavior ` + list(int) :xref_param_type:`list`(:xref_param_type:`int`) diff --git a/numpydoc/xref.py b/numpydoc/xref.py index f3a045bc..2313cc6f 100644 --- a/numpydoc/xref.py +++ b/numpydoc/xref.py @@ -1,9 +1,22 @@ import re from sphinx import addnodes +# 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. + QUALIFIED_NAME_RE = re.compile( - # e.g int, numpy.array, ~numpy.array + # e.g int, numpy.array, ~numpy.array, .class_in_current_module r'^' r'[~\.]?' r'[a-zA-Z_]\w*' @@ -11,25 +24,10 @@ r'$' ) -CONTAINER_TYPE_RE = re.compile( - # e.g. - # - list[int] - # - dict(str, int) - # - dict[str, int]' - # - tuple(float, float) - # - dict[tuple(str, str), int]' - r'^' - r'(dict|list|tuple)' - r'[\[\(]' - r'(.+?(?:,\s*)?)+' - r'[\]\)]' - r'$' -) - CONTAINER_SPLIT_RE = re.compile( # splits dict(str, int) into # ['dict', '[', 'str', ', ', 'int', ']', ''] - r'(\s*[\[\]\(\),]\s*)' + r'(\s*[\[\]\(\)\{\},]\s*)' ) DOUBLE_QUOTE_SPLIT_RE = re.compile( @@ -38,19 +36,27 @@ r'(``.+?``)' ) -IGNORE = {'of', ' of ', 'either', 'or', 'with', 'in', 'default'} +ROLE_SPLIT_RE = re.compile( + # splits to preserve ReST roles + r'(:\w+:`.+?(? Date: Mon, 8 Jan 2018 10:09:46 -0600 Subject: [PATCH 3/7] Fix issues with generated markup for param types - No tildes in aliases, the keys are the titles. - Do not emphasize text - Fix bug, open brackets cannot be to the left of the quote that ends a role. --- doc/install.rst | 6 ++-- numpydoc/tests/test_docscrape.py | 6 ++-- numpydoc/tests/test_xref.py | 26 +++++++------- numpydoc/xref.py | 60 +++++++++++++++++++++----------- 4 files changed, 59 insertions(+), 39 deletions(-) diff --git a/doc/install.rst b/doc/install.rst index 4b37cf9e..2096ae53 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -69,9 +69,9 @@ numpydoc_xref_aliases : dict 'iterable': ':term:`python:iterable`', 'string': 'str', # numpy - 'array': '~numpy.array', - 'dtype': '~numpy.dtype', - 'ndarray': '~numpy.ndarray', + 'array': 'numpy.ndarray', + 'dtype': 'numpy.dtype', + 'ndarray': 'numpy.ndarray', 'matrix': 'numpy.matrix', 'array-like': ':term:`numpy:array_like`', 'array_like': ':term:`numpy:array_like`', diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 24a829f2..8eb6ec11 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -1240,13 +1240,13 @@ def test_templated_sections(): :Returns: - out : :xref_param_type:`~numpy.array` + out : :xref_param_type:`array ` Numerical return value :Other Parameters: - p3 : :xref_param_type:`list`[:xref_param_type:`int`] + p3 : :xref_param_type:`list`\[:xref_param_type:`int`] List of integers p4 : :class:`pandas.DataFrame` @@ -1261,7 +1261,7 @@ def test_xref(): xref_aliases = { 'sequence': ':term:`python:sequence`', 'iterable': ':term:`python:iterable`', - 'array': '~numpy.array', + 'array': 'numpy.ndarray', } doc = SphinxDocString( diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py index 56dedf68..23f88f80 100644 --- a/numpydoc/tests/test_xref.py +++ b/numpydoc/tests/test_xref.py @@ -10,9 +10,9 @@ 'iterable': ':term:`python:iterable`', 'string': 'str', # numpy - 'array': '~numpy.array', + 'array': 'numpy.ndarray', 'dtype': 'numpy.dtype', - 'ndarray': '~numpy.ndarray', + 'ndarray': 'numpy.ndarray', 'matrix': 'numpy.matrix', 'array-like': ':term:`numpy:array_like`', 'array_like': ':term:`numpy:array_like`', @@ -24,7 +24,7 @@ (...) :term:`numpy:array_like`, :xref_param_type:`float`, optional (2,) ndarray -(2,) :xref_param_type:`~numpy.ndarray` +(2,) :xref_param_type:`ndarray ` (...,M,N) array_like (...,:xref_param_type:`M`,:xref_param_type:`N`) :term:`numpy:array_like` @@ -33,10 +33,10 @@ (:xref_param_type:`float`, :xref_param_type:`float`), optional 1-D array or sequence -1-D :xref_param_type:`~numpy.array` or :term:`python:sequence` +1-D :xref_param_type:`array ` or :term:`python:sequence` array of str or unicode-like -:xref_param_type:`~numpy.array` of :xref_param_type:`str` or unicode-like +:xref_param_type:`array ` of :xref_param_type:`str` or unicode-like array_like of float :term:`numpy:array_like` of :xref_param_type:`float` @@ -60,7 +60,7 @@ :xref_param_type:`str` or :xref_param_type:`pathlib.Path` {'', string}, optional -{'', :xref_param_type:`str`}, optional +{'', :xref_param_type:`string `}, optional {'C', 'F', 'A', or 'K'}, optional {'C', 'F', 'A', or 'K'}, optional @@ -72,7 +72,7 @@ {:xref_param_type:`False`, :xref_param_type:`True`, 'greedy', 'optimal'} {{'begin', 1}, {'end', 0}}, {string, int} -{{'begin', 1}, {'end', 0}}, {:xref_param_type:`str`, :xref_param_type:`int`} +{{'begin', 1}, {'end', 0}}, {:xref_param_type:`string `, :xref_param_type:`int`} callable f'(x,*args) :xref_param_type:`callable` f'(:xref_param_type:`x`,*args) @@ -93,22 +93,22 @@ :xref_param_type:`callable` or :ref:`strftime behavior ` list(int) -:xref_param_type:`list`(:xref_param_type:`int`) +:xref_param_type:`list`\(:xref_param_type:`int`) list[int] -:xref_param_type:`list`[:xref_param_type:`int`] +:xref_param_type:`list`\[:xref_param_type:`int`] dict(str, int) -:xref_param_type:`dict`(:xref_param_type:`str`, :xref_param_type:`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`] +: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`) +: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`] +:xref_param_type:`dict`\[:xref_param_type:`tuple`\(:xref_param_type:`str`, :xref_param_type:`str`), :xref_param_type:`int`] """ # noqa: E501 diff --git a/numpydoc/xref.py b/numpydoc/xref.py index 2313cc6f..de1d2e99 100644 --- a/numpydoc/xref.py +++ b/numpydoc/xref.py @@ -1,5 +1,8 @@ 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. @@ -57,6 +60,7 @@ def make_xref_param_type(param_type, xref_aliases): 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 @@ -64,6 +68,7 @@ def make_xref_param_type(param_type, xref_aliases): xref_aliases : dict Mapping used to resolve common abbreviations and aliases to fully qualified names that can be cross-referenced. + Returns ------- out : str @@ -71,11 +76,16 @@ def make_xref_param_type(param_type, xref_aliases): ``xref_param_type`` role. """ if param_type in xref_aliases: - param_type = xref_aliases[param_type] + link, title = xref_aliases[param_type], param_type + param_type = link + else: + link = title = param_type - if (QUALIFIED_NAME_RE.match(param_type) and - param_type not in IGNORE): - return ':xref_param_type:`%s`' % param_type + if QUALIFIED_NAME_RE.match(link) and link not in 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): """ @@ -85,13 +95,21 @@ def _split_and_apply_re(s, pattern): """ results = [] tokens = pattern.split(s) - if len(tokens) > 1: - for tok in tokens: + n = len(tokens) + if n > 1: + for i, tok in enumerate(tokens): if pattern.match(tok): results.append(tok) else: - results.append( - make_xref_param_type(tok, xref_aliases)) + res = make_xref_param_type(tok, xref_aliases) + # 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 @@ -125,19 +143,21 @@ def xref_param_type_role(role, rawtext, text, lineno, inliner, """ Add a pending_xref for the param_type of a field list """ - if text.startswith(('~', '.')): - prefix, target = text[0], text[1:] - if prefix == '.': - env = inliner.document.settings.env - modname = env.ref_context.get('py:module') - text = text[1:] - target = '%s.%s' % (modname, text) - elif prefix == '~': - text = text.split('.')[-1] + has_title, title, target = split_explicit_title(text) + if has_title: + target = target.lstrip('~') else: - target = text - - contnode = addnodes.literal_emphasis(text, text) + 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.inline(title, title) node = addnodes.pending_xref('', refdomain='py', refexplicit=False, reftype='class', reftarget=target) node += contnode From a6ddef606e5c13350091e53c28b74b0cb3cdb845 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Mon, 8 Jan 2018 11:24:15 -0600 Subject: [PATCH 4/7] Use nodes.Text instead of nodes.inline `nodes.inline` adds a `span` tag. `nodes.Text` adds no tag. --- numpydoc/xref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/xref.py b/numpydoc/xref.py index de1d2e99..052feb77 100644 --- a/numpydoc/xref.py +++ b/numpydoc/xref.py @@ -157,7 +157,7 @@ def xref_param_type_role(role, rawtext, text, lineno, inliner, elif prefix == '~': title = target.split('.')[-1] - contnode = nodes.inline(title, title) + contnode = nodes.Text(title, title) node = addnodes.pending_xref('', refdomain='py', refexplicit=False, reftype='class', reftarget=target) node += contnode From aaa48d70ea0fd5acdeb59a295029ef09d2b640da Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 9 Jan 2018 08:57:46 -0600 Subject: [PATCH 5/7] Split on comma-space, care with brackets & quotes - Do not split singly quoted expressions The avoid edgecases that lead to bad rst markup. - Split only when there is a space after a comma. - Do not split on close brackets if they are followed by a linkable token. --- numpydoc/tests/test_xref.py | 7 +++++-- numpydoc/xref.py | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py index 23f88f80..b6fb5250 100644 --- a/numpydoc/tests/test_xref.py +++ b/numpydoc/tests/test_xref.py @@ -27,7 +27,10 @@ (2,) :xref_param_type:`ndarray ` (...,M,N) array_like -(...,:xref_param_type:`M`,:xref_param_type:`N`) :term:`numpy: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 @@ -75,7 +78,7 @@ {{'begin', 1}, {'end', 0}}, {:xref_param_type:`string `, :xref_param_type:`int`} callable f'(x,*args) -:xref_param_type:`callable` f'(:xref_param_type:`x`,*args) +:xref_param_type:`callable` f'(x,*args) callable ``fhess(x, *args)``, optional :xref_param_type:`callable` ``fhess(x, *args)``, optional diff --git a/numpydoc/xref.py b/numpydoc/xref.py index 052feb77..36529fd5 100644 --- a/numpydoc/xref.py +++ b/numpydoc/xref.py @@ -17,6 +17,8 @@ # 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 @@ -30,7 +32,13 @@ CONTAINER_SPLIT_RE = re.compile( # splits dict(str, int) into # ['dict', '[', 'str', ', ', 'int', ']', ''] - r'(\s*[\[\]\(\)\{\},]\s*)' + 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( @@ -44,11 +52,17 @@ r'(:\w+:`.+?(? Date: Thu, 11 Jan 2018 22:49:19 -0600 Subject: [PATCH 6/7] Make the ignore set for xrefs an option --- doc/install.rst | 9 +++++++++ numpydoc/docscrape_sphinx.py | 7 +++++-- numpydoc/numpydoc.py | 2 ++ numpydoc/tests/test_docscrape.py | 5 ++++- numpydoc/tests/test_xref.py | 8 +++++++- numpydoc/xref.py | 11 ++++++----- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/doc/install.rst b/doc/install.rst index 2096ae53..5eab1bab 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -80,6 +80,15 @@ numpydoc_xref_aliases : dict 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 e3f522e4..3a8a3148 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -36,6 +36,7 @@ def load_config(self, config): self.class_members_toctree = config.get('class_members_toctree', 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')] @@ -86,7 +87,8 @@ def _str_returns(self, name='Returns'): if self.xref_param_type: param_type = make_xref_param_type( param_type, - self.xref_aliases) + self.xref_aliases, + self.xref_ignore) out += self._str_indent([typed_fmt % (param.strip(), param_type)]) else: @@ -207,7 +209,8 @@ def _str_param_list(self, name, fake_autosummary=False): if self.xref_param_type: param_type = make_xref_param_type( param_type, - self.xref_aliases) + self.xref_aliases, + self.xref_ignore) out += self._str_indent(['%s : %s' % (display_param, param_type)]) else: diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py index 438952e9..282a5721 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -80,6 +80,7 @@ def mangle_docstrings(app, what, name, obj, options, lines): 'class_members_toctree': app.config.numpydoc_class_members_toctree, 'xref_param_type': app.config.numpydoc_xref_param_type, 'xref_aliases': app.config.numpydoc_xref_aliases, + 'xref_ignore': app.config.numpydoc_xref_ignore, } u_NL = sixu('\n') @@ -153,6 +154,7 @@ def setup(app, get_doc_object_=get_doc_object): app.add_config_value('numpydoc_citation_re', '[a-z0-9_.-]+', 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 8eb6ec11..17ba1cf9 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -1264,11 +1264,14 @@ def test_xref(): 'array': 'numpy.ndarray', } + xref_ignore = {'of', 'default', 'optional'} + doc = SphinxDocString( xref_doc_txt, config=dict( xref_param_type=True, - xref_aliases=xref_aliases + xref_aliases=xref_aliases, + xref_ignore=xref_ignore ) ) diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py index b6fb5250..8786d4bc 100644 --- a/numpydoc/tests/test_xref.py +++ b/numpydoc/tests/test_xref.py @@ -114,9 +114,15 @@ :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) + 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 index 36529fd5..5804a7c7 100644 --- a/numpydoc/xref.py +++ b/numpydoc/xref.py @@ -62,12 +62,10 @@ r'(\s+or\s+|\s+\|\s+|,\s+|\s+)' ) - -IGNORE = {'of', 'either', 'or', 'with', 'in', 'default', 'optional'} CONTAINER_CHARS = set('[](){}') -def make_xref_param_type(param_type, xref_aliases): +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 @@ -82,6 +80,8 @@ def make_xref_param_type(param_type, xref_aliases): 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 ------- @@ -95,7 +95,7 @@ def make_xref_param_type(param_type, xref_aliases): else: link = title = param_type - if QUALIFIED_NAME_RE.match(link) and link not in IGNORE: + if QUALIFIED_NAME_RE.match(link) and link not in xref_ignore: if link != title: return ':xref_param_type:`%s <%s>`' % (title, link) else: @@ -115,7 +115,8 @@ def _split_and_apply_re(s, pattern): if pattern.match(tok): results.append(tok) else: - res = make_xref_param_type(tok, xref_aliases) + 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`\( From 4dd66579dc874c53d7419b23be8c6fc80c08f997 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 16 Jan 2018 03:32:12 -0600 Subject: [PATCH 7/7] DOC: No spaces allowed in xref_aliases keys --- doc/install.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/install.rst b/doc/install.rst index 5eab1bab..e5f6d4da 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -50,8 +50,8 @@ numpydoc_xref_param_type : bool numpydoc_xref_aliases : dict Mappings to fully qualified paths (or correct ReST references) for the aliases/shortcuts used when specifying the types of parameters. - Together with the ``intersphinx`` extension, you can map to links - in any documentation. + 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::