From 477ff10470784baa344229fde5880e4e2bffb545 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 29 Dec 2017 20:09:43 -0600 Subject: [PATCH 1/3] 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. --- doc/install.rst | 47 ++++++++ numpydoc/docscrape_sphinx.py | 24 +++- numpydoc/numpydoc.py | 11 +- numpydoc/tests/test_docscrape.py | 79 +++++++++++++ numpydoc/tests/test_xref.py | 128 +++++++++++++++++++++ numpydoc/xref.py | 185 +++++++++++++++++++++++++++++++ 6 files changed, 469 insertions(+), 5 deletions(-) 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 1fa0fde5..c6d62096 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 5f7843b2..942c78f0 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -17,6 +17,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 @@ -37,6 +38,9 @@ def load_config(self, config): 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 @@ def _str_returns(self, name='Returns'): 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 @@ def _str_param_list(self, name, fake_autosummary=False): 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 f6f262ce..b8e993fb 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -37,6 +37,7 @@ 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 c6f9d08a..2f5a688a 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -1423,6 +1423,85 @@ def test_autoclass(): ''') +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 ` + 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 00000000..8786d4bc --- /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 ` + +(...,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 ` or :term:`python:sequence` + +array of 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` + +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 `}, 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 `, :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 ` +: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`) + +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 00000000..5804a7c7 --- /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+:`.+?(?`' % (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], [] From 807f4e02cdb95e7e4c564b440629aeda5832299a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 14 Jan 2019 12:30:51 -0500 Subject: [PATCH 2/3] FIX: Opt-in, test, uniform styling, css --- doc/install.rst | 10 +++++++--- numpydoc/docscrape_sphinx.py | 8 +++++--- numpydoc/numpydoc.py | 2 +- numpydoc/tests/test_docscrape.py | 18 ++++++++++-------- numpydoc/tests/test_xref.py | 5 ++--- numpydoc/xref.py | 20 ++++++++++++-------- 6 files changed, 37 insertions(+), 26 deletions(-) diff --git a/doc/install.rst b/doc/install.rst index c6d62096..18e22bb8 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -51,7 +51,13 @@ 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. + ``False`` by default. + + .. note:: Depending on the link types, the CSS styles might be different. + consider overridding e.g. ``span.classifier a span.xref`` and + ``span.classifier a code.docutils.literal.notranslate`` + CSS classes to achieve a uniform appearance. + numpydoc_xref_aliases : dict Mappings to fully qualified paths (or correct ReST references) for the aliases/shortcuts used when specifying the types of parameters. @@ -84,7 +90,6 @@ 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 @@ -93,7 +98,6 @@ numpydoc_xref_ignore : set 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 942c78f0..a77390b0 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -225,14 +225,16 @@ def _str_param_list(self, name, fake_autosummary=False): parts.append(display_param) param_type = param.type if param_type: + param_type = 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)]) - +(??) out += self._str_indent(['%s : %s' % (display_param, +(??) param.type)]) +(??) else: +(??) out += self._str_indent([display_param]) if desc and self.use_blockquotes: out += [''] elif not desc: diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py index b8e993fb..fc713a9c 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -236,7 +236,7 @@ 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_param_type', False, True) app.add_config_value('numpydoc_xref_aliases', dict(), True) app.add_config_value('numpydoc_xref_ignore', set(), True) diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 2f5a688a..8bb05d7d 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -387,7 +387,9 @@ def line_by_line_compare(a, b): b = textwrap.dedent(b) a = [l.rstrip() for l in _strip_blank_lines(a).split('\n')] b = [l.rstrip() for l in _strip_blank_lines(b).split('\n')] - assert all(x == y for x, y in zip(a, b)), str([[x, y] for x, y in zip(a, b) if x != y]) + assert len(a) == len(b) + for ii, (aa, bb) in enumerate(zip(a, b)): + assert aa == bb def test_str(): @@ -1450,33 +1452,33 @@ def test_autoclass(): """ -xref_doc_txt_expected = """ +xref_doc_txt_expected = r""" Test xref in Parameters, Other Parameters and Returns :Parameters: - p1 : :xref_param_type:`int` + **p1** : :xref_param_type:`int` Integer value - p2 : :xref_param_type:`float`, optional + **p2** : :xref_param_type:`float`, optional Integer value :Returns: - out : :xref_param_type:`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` + **p4** : :class:`pandas.DataFrame` A dataframe - p5 : :term:`python:sequence` of :xref_param_type:`int` + **p5** : :term:`python:sequence` of :xref_param_type:`int` A sequence """ diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py index 8786d4bc..05950265 100644 --- a/numpydoc/tests/test_xref.py +++ b/numpydoc/tests/test_xref.py @@ -1,7 +1,6 @@ # -*- 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 = { @@ -19,7 +18,7 @@ } # Comes mainly from numpy -data = """ +data = r""" (...) array_like, float, optional (...) :term:`numpy:array_like`, :xref_param_type:`float`, optional @@ -125,4 +124,4 @@ def test_make_xref_param_type(): xref_aliases, xref_ignore ) - assert_equal(result, expected_result) + assert result == expected_result diff --git a/numpydoc/xref.py b/numpydoc/xref.py index 5804a7c7..11c3fbed 100644 --- a/numpydoc/xref.py +++ b/numpydoc/xref.py @@ -66,8 +66,8 @@ def make_xref_param_type(param_type, xref_aliases, xref_ignore): - """ - Enclose str in a role that creates a cross-reference + """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 @@ -165,21 +165,25 @@ def xref_param_type_role(role, rawtext, text, lineno, inliner, Add a pending_xref for the param_type of a field list """ has_title, title, target = split_explicit_title(text) + env = inliner.document.settings.env 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], [] + domain = 'py' + contnode = nodes.literal(title, title) + refnode = addnodes.pending_xref('', refdomain=domain, refexplicit=False, + reftype='class', reftarget=target) + refnode += contnode + # attach information about the current scope + if env: + env.get_domain(domain).process_field_xref(refnode) + return [refnode], [] From 4c9698e4fdc2bafcad441027ee97bf70131a8585 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 15 Apr 2019 12:45:40 -0400 Subject: [PATCH 3/3] ENH: Just use obj --- .gitignore | 1 + .travis.yml | 12 ++--- doc/Makefile | 2 +- doc/conf.py | 5 +- doc/example.py | 8 +-- doc/install.rst | 23 ++++---- numpydoc/docscrape_sphinx.py | 17 +++--- numpydoc/numpydoc.py | 12 ++++- numpydoc/tests/test_docscrape.py | 38 ++++++------- numpydoc/tests/test_numpydoc.py | 5 +- numpydoc/tests/test_xref.py | 58 ++++++++++---------- numpydoc/xref.py | 92 +++++++++++++++----------------- 12 files changed, 136 insertions(+), 137 deletions(-) diff --git a/.gitignore b/.gitignore index 81562fd2..2b54b226 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ *.swo build dist +doc/_build diff --git a/.travis.yml b/.travis.yml index c19e178a..8ffb2bbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,21 @@ # After changing this file, check it on: # http://lint.travis-ci.org/ language: python +dist: xenial sudo: false -env: - - SPHINX_SPEC="Sphinx==1.2.3" - - SPHINX_SPEC="Sphinx" matrix: include: - - python: 3.6 + - python: 3.7 + env: SPHINX_SPEC="==1.2.3" SPHINXOPTS="" + - python: 3.7 - python: 2.7 - env: - - SPHINXOPTS='-W' cache: directories: - $HOME/.cache/pip before_install: - sudo apt-get install texlive texlive-latex-extra latexmk - pip install --upgrade pip setuptools # Upgrade pip and setuptools to get ones with `wheel` support - - pip install pytest numpy matplotlib ${SPHINX_SPEC} + - pip install pytest numpy matplotlib sphinx${SPHINX_SPEC} script: - | python setup.py sdist diff --git a/doc/Makefile b/doc/Makefile index 508376e4..0612444f 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = -nWT --keep-going SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build diff --git a/doc/conf.py b/doc/conf.py index ef2788e2..e23939ef 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -82,6 +82,8 @@ release = numpydoc.__version__ version = re.sub(r'(\d+\.\d+)\.\d+(.*)', r'\1\2', numpydoc.__version__) version = re.sub(r'(\.dev\d+).*?$', r'\1', version) +numpydoc_xref_param_type = True +numpydoc_xref_ignore = {'optional', 'type_without_description', 'BadException'} # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -269,5 +271,6 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('http://docs.python.org/', None), - 'scikitlearn': ('http://scikit-learn.org/stable/', None), + 'numpy': ('https://www.numpy.org/devdocs', None), + 'sklearn': ('http://scikit-learn.org/stable/', None), } diff --git a/doc/example.py b/doc/example.py index 1bae2d5e..592c2b16 100644 --- a/doc/example.py +++ b/doc/example.py @@ -78,10 +78,10 @@ def foo(var1, var2, long_var_name='hi'): See Also -------- - otherfunc : relationship (optional) - newfunc : Relationship (optional), which could be fairly long, in which - case the line wraps here. - thirdfunc, fourthfunc, fifthfunc + numpy.array : relationship (optional) + numpy.ndarray : Relationship (optional), which could be fairly long, in + which case the line wraps here. + numpy.dot, numpy.linalg.norm, numpy.eye Notes ----- diff --git a/doc/install.rst b/doc/install.rst index 18e22bb8..2d2c074a 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -70,26 +70,21 @@ numpydoc_xref_aliases : dict 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:: + The default ``numpydoc_xref_aliases`` will supply some common ``Python`` + standard library and ``NumPy`` names for you. Then for your module, a useful + ``dict`` may look like the following (e.g., if you were documenting + :mod:`sklearn.model_selection`):: 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`', + 'LeaveOneOut': 'sklearn.model_selection.LeaveOneOut', + ... } - This option depends on the ``numpydoc_xref_param_type`` option - being ``True``. + 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 diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py index a77390b0..a4cc71d7 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -17,7 +17,7 @@ from sphinx.jinja2glue import BuiltinTemplateLoader from .docscrape import NumpyDocString, FunctionDoc, ClassDoc -from .xref import make_xref_param_type +from .xref import make_xref if sys.version_info[0] >= 3: sixu = lambda s: s @@ -85,7 +85,7 @@ def _str_returns(self, name='Returns'): 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 = make_xref( param_type, self.xref_aliases, self.xref_ignore) @@ -93,7 +93,7 @@ def _str_returns(self, name='Returns'): out += self._str_indent([named_fmt % (param.name.strip(), 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: @@ -168,10 +168,8 @@ def _process_param(self, param, desc, fake_autosummary): prefix = getattr(self, '_name', '') if prefix: - autosum_prefix = '~%s.' % prefix link_prefix = '%s.' % prefix else: - autosum_prefix = '' link_prefix = '' # Referenced object has a docstring @@ -227,14 +225,13 @@ def _str_param_list(self, name, fake_autosummary=False): if param_type: param_type = param.type if self.xref_param_type: - param_type = make_xref_param_type( + param_type = make_xref( param_type, self.xref_aliases, self.xref_ignore) -(??) out += self._str_indent(['%s : %s' % (display_param, -(??) param.type)]) -(??) else: -(??) out += self._str_indent([display_param]) + parts.append(param_type) + out += self._str_indent([' : '.join(parts)]) + if desc and self.use_blockquotes: out += [''] elif not desc: diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py index fc713a9c..e1b8f263 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -37,7 +37,7 @@ 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 .xref import DEFAULT_LINKS from . import __version__ if sys.version_info[0] >= 3: @@ -223,7 +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('builder-inited', update_config) app.connect('autodoc-process-docstring', mangle_docstrings) app.connect('autodoc-process-signature', mangle_signature) app.connect('doctree-read', relabel_references) @@ -248,6 +248,14 @@ def setup(app, get_doc_object_=get_doc_object): 'parallel_read_safe': True} return metadata + +def update_config(app): + """Update the configuration with default values.""" + for key, value in DEFAULT_LINKS.items(): + if key not in app.config.numpydoc_xref_aliases: + app.config.numpydoc_xref_aliases[key] = value + + # ------------------------------------------------------------------------------ # Docstring-mangling domains # ------------------------------------------------------------------------------ diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 8bb05d7d..f6844669 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -1,6 +1,7 @@ # -*- encoding:utf-8 -*- from __future__ import division, absolute_import, print_function +from collections import namedtuple import re import sys import textwrap @@ -8,6 +9,7 @@ import jinja2 +from numpydoc.numpydoc import update_config from numpydoc.docscrape import ( NumpyDocString, FunctionDoc, @@ -382,11 +384,11 @@ def _strip_blank_lines(s): return s -def line_by_line_compare(a, b): +def line_by_line_compare(a, b, n_lines=None): a = textwrap.dedent(a) b = textwrap.dedent(b) - a = [l.rstrip() for l in _strip_blank_lines(a).split('\n')] - b = [l.rstrip() for l in _strip_blank_lines(b).split('\n')] + a = [l.rstrip() for l in _strip_blank_lines(a).split('\n')][:n_lines] + b = [l.rstrip() for l in _strip_blank_lines(b).split('\n')][:n_lines] assert len(a) == len(b) for ii, (aa, bb) in enumerate(zip(a, b)): assert aa == bb @@ -518,8 +520,7 @@ def test_yield_str(): The number of bananas. int The number of unknowns. - -.. index:: """) +""") def test_receives_str(): @@ -537,8 +538,7 @@ def test_receives_str(): The number of bananas. c : int The number of oranges. - -.. index:: """) +""") def test_no_index_in_str(): @@ -1200,8 +1200,6 @@ def test_class_members_doc(): b c - .. index:: - """) @@ -1422,7 +1420,7 @@ def test_autoclass(): .. rubric:: Methods - ''') + ''', 5) xref_doc_txt = """ @@ -1442,7 +1440,7 @@ def test_autoclass(): List of integers p4 : :class:`pandas.DataFrame` A dataframe -p5 : sequence of int +p5 : sequence of `int` A sequence Returns @@ -1458,37 +1456,39 @@ def test_autoclass(): :Parameters: - **p1** : :xref_param_type:`int` + **p1** : :class:`python:int` Integer value - **p2** : :xref_param_type:`float`, optional + **p2** : :class:`python:float`, optional Integer value :Returns: - **out** : :xref_param_type:`array ` + **out** : :obj:`array ` Numerical return value :Other Parameters: - **p3** : :xref_param_type:`list`\[:xref_param_type:`int`] + **p3** : :class:`python:list`\[:class:`python:int`] List of integers **p4** : :class:`pandas.DataFrame` A dataframe - **p5** : :term:`python:sequence` of :xref_param_type:`int` + **p5** : :obj:`python:sequence` of `int` A sequence """ def test_xref(): xref_aliases = { - 'sequence': ':term:`python:sequence`', - 'iterable': ':term:`python:iterable`', - 'array': 'numpy.ndarray', + 'sequence': ':obj:`python:sequence`', } + config = namedtuple('numpydoc_xref_aliases', + 'numpydoc_xref_aliases')(xref_aliases) + app = namedtuple('config', 'config')(config) + update_config(app) xref_ignore = {'of', 'default', 'optional'} diff --git a/numpydoc/tests/test_numpydoc.py b/numpydoc/tests/test_numpydoc.py index 0fd3c37a..635a271d 100644 --- a/numpydoc/tests/test_numpydoc.py +++ b/numpydoc/tests/test_numpydoc.py @@ -10,6 +10,9 @@ class MockConfig(): numpydoc_show_class_members = True numpydoc_show_inherited_class_members = True numpydoc_class_members_toctree = True + numpydoc_xref_param_type = False + numpydoc_xref_aliases = {} + numpydoc_xref_ignore = set() templates_path = [] numpydoc_edit_link = False numpydoc_citation_re = '[a-z0-9_.-]+' @@ -41,7 +44,7 @@ def test_mangle_docstrings(): doc = mangle_docstrings(MockApp(), 'class', 'str', str, {'members': ['upper']}, lines) assert 'rpartition' not in [x.strip() for x in lines] assert 'upper' in [x.strip() for x in lines] - + lines = s.split('\n') doc = mangle_docstrings(MockApp(), 'class', 'str', str, {'exclude-members': ALL}, lines) assert 'rpartition' not in [x.strip() for x in lines] diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py index 05950265..6e3170ef 100644 --- a/numpydoc/tests/test_xref.py +++ b/numpydoc/tests/test_xref.py @@ -1,7 +1,7 @@ # -*- encoding:utf-8 -*- from __future__ import division, absolute_import, print_function -from numpydoc.xref import make_xref_param_type +from numpydoc.xref import make_xref xref_aliases = { # python @@ -20,49 +20,49 @@ # Comes mainly from numpy data = r""" (...) array_like, float, optional -(...) :term:`numpy:array_like`, :xref_param_type:`float`, optional +(...) :term:`numpy:array_like`, :obj:`float`, optional (2,) ndarray -(2,) :xref_param_type:`ndarray ` +(2,) :obj:`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` +(..., :obj:`M`, :obj:`N`) :term:`numpy:array_like` (float, float), optional -(:xref_param_type:`float`, :xref_param_type:`float`), optional +(:obj:`float`, :obj:`float`), optional 1-D array or sequence -1-D :xref_param_type:`array ` or :term:`python:sequence` +1-D :obj:`array ` or :term:`python:sequence` array of str or unicode-like -:xref_param_type:`array ` of :xref_param_type:`str` or unicode-like +:obj:`array ` of :obj:`str` or unicode-like array_like of float -:term:`numpy:array_like` of :xref_param_type:`float` +:term:`numpy:array_like` of :obj:`float` bool or callable -:xref_param_type:`bool` or :xref_param_type:`callable` +:obj:`bool` or :obj:`callable` int in [0, 255] -:xref_param_type:`int` in [0, 255] +:obj:`int` in [0, 255] int or None, optional -:xref_param_type:`int` or :xref_param_type:`None`, optional +:obj:`int` or :obj:`None`, optional list of str or array_like -:xref_param_type:`list` of :xref_param_type:`str` or :term:`numpy:array_like` +:obj:`list` of :obj:`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` +:obj:`str` or :obj:`pathlib.Path` {'', string}, optional -{'', :xref_param_type:`string `}, optional +{'', :obj:`string `}, optional {'C', 'F', 'A', or 'K'}, optional {'C', 'F', 'A', or 'K'}, optional @@ -71,55 +71,55 @@ {'linear', 'lower', 'higher', 'midpoint', 'nearest'} {False, True, 'greedy', 'optimal'} -{:xref_param_type:`False`, :xref_param_type:`True`, 'greedy', 'optimal'} +{:obj:`False`, :obj:`True`, 'greedy', 'optimal'} {{'begin', 1}, {'end', 0}}, {string, int} -{{'begin', 1}, {'end', 0}}, {:xref_param_type:`string `, :xref_param_type:`int`} +{{'begin', 1}, {'end', 0}}, {:obj:`string `, :obj:`int`} callable f'(x,*args) -:xref_param_type:`callable` f'(x,*args) +:obj:`callable` f'(x,*args) callable ``fhess(x, *args)``, optional -:xref_param_type:`callable` ``fhess(x, *args)``, optional +:obj:`callable` ``fhess(x, *args)``, optional spmatrix (format: ``csr``, ``bsr``, ``dia`` or coo``) -:xref_param_type:`spmatrix` (format: ``csr``, ``bsr``, ``dia`` or coo``) +:obj:`spmatrix` (format: ``csr``, ``bsr``, ``dia`` or coo``) :ref:`strftime ` :ref:`strftime ` callable or :ref:`strftime ` -:xref_param_type:`callable` or :ref:`strftime ` +:obj:`callable` or :ref:`strftime ` callable or :ref:`strftime behavior ` -:xref_param_type:`callable` or :ref:`strftime behavior ` +:obj:`callable` or :ref:`strftime behavior ` list(int) -:xref_param_type:`list`\(:xref_param_type:`int`) +:obj:`list`\(:obj:`int`) list[int] -:xref_param_type:`list`\[:xref_param_type:`int`] +:obj:`list`\[:obj:`int`] dict(str, int) -:xref_param_type:`dict`\(:xref_param_type:`str`, :xref_param_type:`int`) +:obj:`dict`\(:obj:`str`, :obj:`int`) dict[str, int] -:xref_param_type:`dict`\[:xref_param_type:`str`, :xref_param_type:`int`] +:obj:`dict`\[:obj:`str`, :obj:`int`] tuple(float, float) -:xref_param_type:`tuple`\(:xref_param_type:`float`, :xref_param_type:`float`) +:obj:`tuple`\(:obj:`float`, :obj:`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`] +:obj:`dict`\[:obj:`tuple`\(:obj:`str`, :obj:`str`), :obj:`int`] """ # noqa: E501 xref_ignore = {'or', 'in', 'of', 'default', 'optional'} -def test_make_xref_param_type(): +def test_make_xref(): for s in data.strip().split('\n\n'): param_type, expected_result = s.split('\n') - result = make_xref_param_type( + result = make_xref( param_type, xref_aliases, xref_ignore diff --git a/numpydoc/xref.py b/numpydoc/xref.py index 11c3fbed..65542209 100644 --- a/numpydoc/xref.py +++ b/numpydoc/xref.py @@ -1,9 +1,5 @@ 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 @@ -13,9 +9,7 @@ # 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. +# worthy and wrap them around a :obj: role. # Note: we never split on commas that are not followed by a space # You risk creating bad rst markup if you do so. @@ -64,14 +58,44 @@ 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. +# Save people some time and add some common standard aliases +DEFAULT_LINKS = { + # Python + 'None': ':data:`python:None`', + 'bool': ':ref:`bool `', + 'boolean': ':ref:`bool `', + 'True': ':data:`python:True`', + 'False': ':data:`python:False`', + 'list': ':class:`python:list`', + 'tuple': ':class:`python:tuple`', + 'str': ':class:`python:str`', + 'string': ':class:`python:str`', + 'dict': ':class:`python:dict`', + 'float': ':class:`python:float`', + 'int': ':class:`python:int`', + 'callable': ':func:`python:callable`', + 'iterable': ':term:`python:iterable`', + 'sequence': ':term:`python:sequence`', + 'contextmanager': ':func:`python:contextlib.contextmanager`', + 'namedtuple': ':func:`python:collections.namedtuple`', + 'generator': ':term:`python:generator`', + # NumPy + 'array': 'numpy.ndarray', + 'ndarray': 'numpy.ndarray', + 'np.ndarray': 'numpy.ndarray', + 'array-like': ':term:`numpy:array_like`', + 'array_like': ':term:`numpy:array_like`', + 'scalar': ':ref:`scalar `', + 'RandomState': 'numpy.random.RandomState', + 'np.random.RandomState': 'numpy.random.RandomState', + 'np.inf': ':data:`numpy.inf`', + 'np.nan': ':data:`numpy.nan`', + 'numpy': ':mod:`numpy`', +} + + +def make_xref(param_type, xref_aliases, xref_ignore): + """Enclose str in a :obj: role. Parameters ---------- @@ -87,7 +111,7 @@ def make_xref_param_type(param_type, xref_aliases, xref_ignore): ------- out : str Text with parts that may be wrapped in a - ``xref_param_type`` role. + ``:obj:`` role. """ if param_type in xref_aliases: link, title = xref_aliases[param_type], param_type @@ -97,9 +121,9 @@ def make_xref_param_type(param_type, xref_aliases, xref_ignore): if QUALIFIED_NAME_RE.match(link) and link not in xref_ignore: if link != title: - return ':xref_param_type:`%s <%s>`' % (title, link) + return ':obj:`%s <%s>`' % (title, link) else: - return ':xref_param_type:`%s`' % link + return ':obj:`%s`' % link def _split_and_apply_re(s, pattern): """ @@ -115,7 +139,7 @@ def _split_and_apply_re(s, pattern): if pattern.match(tok): results.append(tok) else: - res = make_xref_param_type( + res = make_xref( tok, xref_aliases, xref_ignore) # Openning brackets immediated after a role is # bad markup. Detect that and add backslash. @@ -157,33 +181,3 @@ def _split_and_apply_re(s, pattern): # 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) - env = inliner.document.settings.env - if has_title: - target = target.lstrip('~') - else: - if target.startswith(('~', '.')): - prefix, target = target[0], target[1:] - if prefix == '.': - modname = env.ref_context.get('py:module') - target = target[1:] - target = '%s.%s' % (modname, target) - elif prefix == '~': - title = target.split('.')[-1] - - domain = 'py' - contnode = nodes.literal(title, title) - refnode = addnodes.pending_xref('', refdomain=domain, refexplicit=False, - reftype='class', reftarget=target) - refnode += contnode - # attach information about the current scope - if env: - env.get_domain(domain).process_field_xref(refnode) - return [refnode], []