Skip to content

Commit

Permalink
Closes sphinx-doc#1384: Parse and interpret See Also section the way …
Browse files Browse the repository at this point in the history
…NumpyDoc does

The NumpyDoc extension that is developed by the Numpy folks does a lot
of extra work interpreting See Also sections. It assumes that the contents
of the See Also will always be references to other functions/classes and
it tries to deduce what is being referenced.

I've ported their implementation for See Also sections written in the
Numpy style. There is NO extra interpretation done for See Also sections
that are written using the Google style.
  • Loading branch information
RelentlessIdiot committed Mar 10, 2014
1 parent 18df642 commit 191e0b4
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 0 deletions.
34 changes: 34 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,37 @@ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

-------------------------------------------------------------------------------

The included implementation of NumpyDocstring._parse_see_also_section was
derived from code under the following license:

-------------------------------------------------------------------------------

Copyright (C) 2008 Stefan van der Walt <[email protected]>, Pauli Virtanen <[email protected]>

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

-------------------------------------------------------------------------------
123 changes: 123 additions & 0 deletions sphinx/ext/napoleon/docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
:license: BSD, see LICENSE for details.
"""

import collections
import inspect
import re
import sys
from sphinx.ext.napoleon.iterators import modify_iter
Expand Down Expand Up @@ -94,9 +96,21 @@ def __init__(self, docstring, config=None, app=None, what='', name='',
obj=None, options=None):
self._config = config
self._app = app

if not self._config:
from sphinx.ext.napoleon import Config
self._config = self._app and self._app.config or Config()

if not what:
if inspect.isclass(obj):
what = 'class'
elif inspect.ismodule(obj):
what = 'module'
elif isinstance(obj, collections.Callable):
what = 'function'
else:
what = 'object'

self._what = what
self._name = name
self._obj = obj
Expand Down Expand Up @@ -724,3 +738,112 @@ def _is_section_header(self):
if section.startswith(directive_section):
return True
return False

_name_rgx = re.compile(r"^\s*(:(?P<role>\w+):`(?P<name>[a-zA-Z0-9_.-]+)`|"
r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X)

def _parse_see_also_section(self, section):
"""
Derived from the NumpyDoc implementation of _parse_see_also.
func_name : Descriptive text
continued text
another_func_name : Descriptive text
func_name1, func_name2, :meth:`func_name`, func_name3
"""
content = self._consume_to_next_section()
items = []

def parse_item_name(text):
"""Match ':role:`name`' or 'name'"""
m = self._name_rgx.match(text)
if m:
g = m.groups()
if g[1] is None:
return g[3], None
else:
return g[2], g[1]
raise ValueError("%s is not a item name" % text)

def push_item(name, rest):
if not name:
return
name, role = parse_item_name(name)
items.append((name, list(rest), role))
del rest[:]

current_func = None
rest = []

for line in content:
if not line.strip(): continue

m = self._name_rgx.match(line)
if m and line[m.end():].strip().startswith(':'):
push_item(current_func, rest)
current_func, line = line[:m.end()], line[m.end():]
rest = [line.split(':', 1)[1].strip()]
if not rest[0]:
rest = []
elif not line.startswith(' '):
push_item(current_func, rest)
current_func = None
if ',' in line:
for func in line.split(','):
if func.strip():
push_item(func, [])
elif line.strip():
current_func = line
elif current_func is not None:
rest.append(line.strip())
push_item(current_func, rest)


if not items:
return []

roles = {
'method': 'meth',
'meth': 'meth',
'function': 'func',
'func': 'func',
'class': 'class',
'exception': 'exc',
'exc': 'exc',
'object': 'obj',
'obj': 'obj',
'module': 'mod',
'mod': 'mod',
'data': 'data',
'constant': 'const',
'const': 'const',
'attribute': 'attr',
'attr': 'attr'
}
if self._what is None:
func_role = 'obj'
else:
func_role = roles.get(self._what, '')
lines = []
last_had_desc = True
for func, desc, role in items:
if role:
link = ':%s:`%s`' % (role, func)
elif func_role:
link = ':%s:`%s`' % (func_role, func)
else:
link = "`%s`_" % func
if desc or last_had_desc:
lines += ['']
lines += [link]
else:
lines[-1] += ", %s" % link
if desc:
lines += self._indent([' '.join(desc)])
last_had_desc = True
else:
last_had_desc = False
lines += ['']

return self._format_admonition('seealso', lines)
53 changes: 53 additions & 0 deletions tests/test_napoleon_docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring
from unittest import TestCase

try:
# Python >=3.3
from unittest.mock import Mock
except ImportError:
from mock import Mock


class BaseDocstringTest(TestCase):
pass
Expand Down Expand Up @@ -306,3 +312,50 @@ def test_parameters_without_class_reference(self):
:type param1: MyClass instance
""")
self.assertEqual(expected, actual)

def test_see_also_refs(self):
docstring = """
numpy.multivariate_normal(mean, cov, shape=None, spam=None)
See Also
--------
some, other, funcs
otherfunc : relationship
"""

actual = str(NumpyDocstring(textwrap.dedent(docstring)))

expected = """
numpy.multivariate_normal(mean, cov, shape=None, spam=None)
.. seealso::
\n :obj:`some`, :obj:`other`, :obj:`funcs`
\n :obj:`otherfunc`
relationship
"""
self.assertEqual(expected, actual)

docstring = """
numpy.multivariate_normal(mean, cov, shape=None, spam=None)
See Also
--------
some, other, funcs
otherfunc : relationship
"""

config = Config()
app = Mock()
actual = str(NumpyDocstring(textwrap.dedent(docstring), config, app, "method"))

expected = """
numpy.multivariate_normal(mean, cov, shape=None, spam=None)
.. seealso::
\n :meth:`some`, :meth:`other`, :meth:`funcs`
\n :meth:`otherfunc`
relationship
"""
self.assertEqual(expected, actual)

0 comments on commit 191e0b4

Please sign in to comment.