Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Generate doc stubs for transplanted methods #2176

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ package/doc/html/
MDAnalysis.log
# Ignore the authors.py files as they are generated files
authors.py

# Ignore generated documentation stubs
package/doc/sphinx/source/documentation_pages/core/*.txt
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ matrix:
BUILD_CMD="cd ${TRAVIS_BUILD_DIR}/package && python setup.py build_ext --inplace"
INSTALL_HOLE="false"
PIP_DEPENDENCIES="${PIP_DEPENDENCIES} sphinx-sitemap"
CONDA_DEPENDENCIES="${CONDA_DEPENDENCIES} tabulate"

- env: NAME="Lint"
PYLINTRC="${TRAVIS_BUILD_DIR}/package/.pylintrc"
Expand Down
16 changes: 16 additions & 0 deletions package/MDAnalysis/core/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ class GroupBase(_MutableBase):
| | | that are part of ``s`` or |
| | | ``t`` but not both |
+-------------------------------+------------+----------------------------+

.. include:: GroupBase.txt

"""
def __init__(self, *args):
try:
Expand Down Expand Up @@ -1781,6 +1784,8 @@ class AtomGroup(GroupBase):
new :class:`AtomGroup` for multiple matches. This makes it difficult to use
the feature consistently in scripts.

.. include:: AtomGroup.txt


See Also
--------
Expand Down Expand Up @@ -2702,6 +2707,9 @@ class ResidueGroup(GroupBase):
*Instant selectors* of Segments will be removed in the 1.0 release.
See :ref:`Instant selectors <instance-selectors>` for details and
alternatives.

.. include:: ResidueGroup.txt

"""

@property
Expand Down Expand Up @@ -2864,6 +2872,7 @@ class SegmentGroup(GroupBase):
*Instant selectors* of Segments will be removed in the 1.0 release.
See :ref:`Instant selectors <instance-selectors>` for details and
alternatives.

"""

@property
Expand Down Expand Up @@ -3102,6 +3111,9 @@ class Atom(ComponentBase):
:class:`~MDAnalysis.core.topologyattrs.TopologyAttr` components are obtained
from :class:`ComponentBase`, so this class only includes ad-hoc methods
specific to :class:`Atoms<Atom>`.

.. include:: Atom.txt

"""
def __getattr__(self, attr):
"""Try and catch known attributes and give better error message"""
Expand Down Expand Up @@ -3241,6 +3253,9 @@ class Residue(ComponentBase):
:class:`~MDAnalysis.core.topologyattrs.TopologyAttr` components are obtained
from :class:`ComponentBase`, so this class only includes ad-hoc methods
specific to :class:`Residues<Residue>`.

.. include:: Residue.txt

"""
def __repr__(self):
me = '<Residue'
Expand Down Expand Up @@ -3288,6 +3303,7 @@ class Segment(ComponentBase):
*Instant selectors* of :class:`Segments<Segment>` will be removed in the
1.0 release. See :ref:`Instant selectors <instance-selectors>` for
details and alternatives.

"""
def __repr__(self):
me = '<Segment'
Expand Down
8 changes: 8 additions & 0 deletions package/doc/sphinx/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@
import sys
import os
import platform
import subprocess

# http://alabaster.readthedocs.io/en/latest/
import alabaster

# Generate documentation stubs for transplanted methods. This is needed because
# methods transplanted from topology attributes into topology components and
# groups (e.g. Atom, AtomGroup) do not appear in the documentation of the
# component or group otherwise.
subprocess.call('./transplant_stub.py')


# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown
Expand Down
154 changes: 154 additions & 0 deletions package/doc/sphinx/source/transplant_stub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/env python

from __future__ import print_function
import sys
from pprint import pprint
import collections
import os
import textwrap
import re
import inspect
import tabulate

from sphinx.ext.napoleon import NumpyDocstring

# Make sure we use the same version of MDAnalysis as sphinx
sys.path.insert(0, os.path.abspath('../../..'))
import MDAnalysis as mda

class TransplantedMethod:
def __init__(self, method):
self.method = method
try:
# We may be dealing with a property; then we need to get the
# actual method out of it.
self.method = method.fget
except AttributeError:
# Well, it was not a property
pass

@property
def name(self):
return self.method.__name__

@property
def doc(self):
dedent_doc = textwrap.dedent(' ' + self.method.__doc__)
numpy_doc = NumpyDocstring(dedent_doc)
doc_clear = clear_citations(str(numpy_doc))
return doc_clear

@property
def signature(self):
return get_signature(self.method)

@property
def short_desc(self):
return self.doc.splitlines()[0].strip()

@property
def is_private(self):
return self.name.startswith('_') or self.name.endswith('_')

@property
def formatted(self):
text = '.. method:: {}{}\n\n{}\n\n'.format(
self.name,
self.signature,
textwrap.indent(self.doc, prefix=' ' * 8)
)
return text


def clear_citations(doc):
return doc
citation_re = re.compile(r'^ *\.\. \[[^]]+\]')

result = []
in_citation = False
for line in doc.splitlines():
match = citation_re.match(line)
if match is not None:
in_citation = True
elif in_citation and not line.strip():
in_citation = False
elif not in_citation:
result.append(line)

return '\n'.join(result)


def get_signature(method):
signature = str(inspect.signature(method))
return re.sub(r'\(self,? *', '(', signature)


# Collect the transplanted functions from the topopoly attributes
targets = collections.defaultdict(lambda : collections.defaultdict(list))
for attribute_key, attribute in mda.core.topologyattrs._TOPOLOGY_ATTRS.items():
for target, methods in attribute.transplants.items():
all_methods = []
for method in methods:
function = TransplantedMethod(method[1])
if not function.is_private:
all_methods.append(function)
if all_methods:
targets[target][attribute.attrname] = all_methods


for target_key, target_dict in targets.items():
try:
target_name = target_key.__name__
except AttributeError:
# For some reason, some target are not classes but str
target_name = target_key
if hasattr(target_key, '__mro__'):
for parent in target_key.__mro__:
for attribute_key, method_list in targets.get(parent, {}).items():
if attribute_key not in target_dict:
target_dict[attribute_key] = []
for method in method_list:
if method not in target_dict[attribute_key]:
target_dict[attribute_key].append(method)


for target_key, target_dict in targets.items():
try:
target_name = target_key.__name__
except AttributeError:
# For some reason, some target are not classes but str
target_name = target_key
table = []
for attribute_key, method_list in target_dict.items():
table.append(['**Requires {}**'.format(attribute_key), ''])
for method in method_list:
table.append([method.name, method.short_desc])
print(tabulate.tabulate(table, tablefmt='grid'))


for target_key, target_dict in targets.items():
try:
target_name = target_key.__name__
except AttributeError:
# For some reason, some target are not classes but str
target_name = target_key
file_name = os.path.join(
'documentation_pages',
'core',
'{}.txt'.format(target_name)
)
with open(file_name, 'w') as outfile:
table = []
for attribute_key, method_list in target_dict.items():
table.append(['**Requires {}**'.format(attribute_key), ''])
for method in method_list:
table.append([':meth:`{}`'.format(method.name), method.short_desc])
print(tabulate.tabulate(table, tablefmt='grid'), file=outfile)

for attribute_key, method_list in target_dict.items():
print(file=outfile)

for method in method_list:
print(method.formatted, file=outfile)