diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py index d8a495e0..a555ed06 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -79,21 +79,136 @@ def _str_returns(self, name='Returns'): out += [''] return out - def _str_param_list(self, name): + def _process_param(self, param, desc, autosum): + """Determine how to display a parameter + + Emulates autosummary behavior if autosum is not None. + + Parameters + ---------- + param : str + The name of the parameter + desc : list of str + The parameter description as given in the docstring. This is + ignored when autosummary logic applies. + autosum : list or None + If a list, autosummary-style behaviour will apply for params + that are attributes of the class and have a docstring. + Names for autosummary generation will be appended to this list. + + If None, autosummary is disabled. + + Returns + ------- + display_param : str + The marked up parameter name for display. This may include a link + to the corresponding attribute's own documentation. + desc : list of str + A list of description lines. This may be identical to the input + ``desc``, if ``autosum is None`` or ``param`` is not a class + attribute, or it will be a summary of the class attribute's + docstring. + + Notes + ----- + This does not have the autosummary functionality to display a method's + signature, and hence is not used to format methods. It may be + complicated to incorporate autosummary's signature mangling, as it + relies on Sphinx's plugin mechanism. + """ + param = param.strip() + display_param = '**%s**' % param + + if autosum is None: + return display_param, desc + + param_obj = getattr(self._obj, param, None) + if not (callable(param_obj) + or isinstance(param_obj, property) + or inspect.isgetsetdescriptor(param_obj)): + param_obj = None + obj_doc = pydoc.getdoc(param_obj) + + if not (param_obj and obj_doc): + return display_param, desc + + prefix = getattr(self, '_name', '') + if prefix: + autosum_prefix = '~%s.' % prefix + link_prefix = '%s.' % prefix + else: + autosum_prefix = '' + link_prefix = '' + + # Referenced object has a docstring + autosum.append(" %s%s" % (autosum_prefix, param)) + display_param = ':obj:`%s <%s%s>`' % (param, + link_prefix, + param) + if obj_doc: + # Overwrite desc. Take summary logic of autosummary + desc = re.split('\n\s*\n', obj_doc.strip(), 1)[0] + # XXX: Should this have DOTALL? + # It does not in autosummary + m = re.search(r"^([A-Z].*?\.)(?:\s|$)", + ' '.join(desc.split())) + if m: + desc = m.group(1).strip() + else: + desc = desc.partition('\n')[0] + desc = desc.split('\n') + return display_param, desc + + def _str_param_list(self, name, fake_autosummary=False): + """Generate RST for a listing of parameters or similar + + Parameter names are displayed as bold text, and descriptions + are in blockquotes. Descriptions may therefore contain block + markup as well. + + Parameters + ---------- + name : str + Section name (e.g. Parameters) + fake_autosummary : bool + When True, the parameter names may correspond to attributes of the + object beign documented, usually ``property`` instances on a class. + In this case, names will be linked to fuller descriptions. + + Returns + ------- + rst : list of str + """ out = [] if self[name]: + if fake_autosummary: + autosum = [] + else: + autosum = None + out += self._str_field_list(name) out += [''] for param, param_type, desc in self[name]: + display_param, desc = self._process_param(param, desc, autosum) + if param_type: - out += self._str_indent(['**%s** : %s' % (param.strip(), - param_type)]) + out += self._str_indent(['%s : %s' % (display_param, + param_type)]) else: - out += self._str_indent(['**%s**' % param.strip()]) + out += self._str_indent([display_param]) if desc: - out += [''] + out += [''] # produces a blockquote, rather than a dt/dd out += self._str_indent(desc, 8) out += [''] + + if fake_autosummary and autosum: + if self.class_members_toctree: + autosum.insert(0, ' :toctree:') + autosum.insert(0, '.. autosummary::') + out += ['..', ' HACK to make autogen generate docs:'] + out += self._str_indent(autosum, 4) + out += [''] + return out @property @@ -130,7 +245,7 @@ def _str_member_list(self, name): or inspect.isgetsetdescriptor(param_obj)): param_obj = None - if param_obj and (pydoc.getdoc(param_obj) or not desc): + if param_obj and pydoc.getdoc(param_obj): # Referenced object has a docstring autosum += [" %s%s" % (prefix, param)] else: @@ -250,7 +365,8 @@ def __str__(self, indent=0, func_role="obj"): 'notes': self._str_section('Notes'), 'references': self._str_references(), 'examples': self._str_examples(), - 'attributes': self._str_member_list('Attributes'), + 'attributes': self._str_param_list('Attributes', + fake_autosummary=True), 'methods': self._str_member_list('Methods'), } ns = dict((k, '\n'.join(v)) for k, v in ns.items()) diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 297a0acb..2ae51c66 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -17,6 +17,8 @@ from nose.tools import (assert_equal, assert_raises, assert_list_equal, assert_true) +assert_list_equal.__self__.maxDiff = None + if sys.version_info[0] >= 3: sixu = lambda s: s else: @@ -897,8 +899,17 @@ def test_duplicate_signature(): Current time. y : ndarray Current variable values. - x : float - Some parameter + + * hello + * world + an_attribute : float + The docstring is printed instead + no_docstring : str + But a description + no_docstring2 : str + multiline_sentence + midword_period + no_period Methods ------- @@ -934,8 +945,17 @@ def test_class_members_doc(): Current time. y : ndarray Current variable values. - x : float - Some parameter + + * hello + * world + an_attribute : float + The docstring is printed instead + no_docstring : str + But a description + no_docstring2 : str + multiline_sentence + midword_period + no_period Methods ------- @@ -952,10 +972,38 @@ def test_class_members_doc(): def test_class_members_doc_sphinx(): class Foo: @property - def x(self): + def an_attribute(self): """Test attribute""" return None + @property + def no_docstring(self): + return None + + @property + def no_docstring2(self): + return None + + @property + def multiline_sentence(self): + """This is a + sentence. It spans multiple lines.""" + return None + + @property + def midword_period(self): + """The sentence for numpy.org.""" + return None + + @property + def no_period(self): + """This does not have a period + so we truncate its summary to the first linebreak + + Apparently. + """ + return None + doc = SphinxClassDoc(Foo, class_doc_txt) non_blank_line_by_line_compare(str(doc), """ @@ -975,17 +1023,36 @@ def x(self): For usage examples, see `ode`. - .. rubric:: Attributes - - .. autosummary:: - :toctree: - - x - - ===== ========== - **t** (float) Current time. - **y** (ndarray) Current variable values. - ===== ========== + :Attributes: + + **t** : float + Current time. + **y** : ndarray + Current variable values. + + * hello + * world + :obj:`an_attribute ` : float + Test attribute + **no_docstring** : str + But a description + **no_docstring2** : str + :obj:`multiline_sentence ` + This is a sentence. + :obj:`midword_period ` + The sentence for numpy.org. + :obj:`no_period ` + This does not have a period + + .. + HACK to make autogen generate docs: + .. autosummary:: + :toctree: + + an_attribute + multiline_sentence + midword_period + no_period .. rubric:: Methods