Skip to content

Commit

Permalink
Always transplant a stub for transplanted methods (#2427)
Browse files Browse the repository at this point in the history
- Fixes #1845
- Adds dummy method with documentation of original method
- Dummy method gets replaced by actual method on instantiation

(cherry picked from commit 799100b)
  • Loading branch information
jbarnoud authored and orbeckst committed Oct 23, 2020
1 parent c4fa754 commit 5a259b7
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 44 deletions.
1 change: 1 addition & 0 deletions maintainer/conda/MDAnalysis/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ requirements:
- netcdf4
- mmtf-python
- scikit-learn
- funcsigs

test:
imports:
Expand Down
2 changes: 2 additions & 0 deletions package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Fixes
* Development status changed from beta to mature (Issue #2773)
* pip installation only requests Python 2.7-compatible packages (#2736)
* Testsuite does not use any more matplotlib.use('agg') (#2191)
* The methods provided by topology attributes now appear in the
documentation (Issue #1845)
* In ChainReader, read_frame does not trigger change of iterating position.
(Issue #2723, PR #2815)
* empty_atomgroup.select_atoms('name *') now returns an empty
Expand Down
2 changes: 1 addition & 1 deletion package/MDAnalysis/core/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def _mix(cls, other):
A class of parents :class:`_ImmutableBase`, *other* and this class.
Its name is the same as *other*'s.
"""
newcls = type(other.__name__, (_ImmutableBase, other, cls), {})
newcls = type(other.__name__, (_ImmutableBase, cls, other), {})
newcls._derived_class = newcls
return newcls

Expand Down
155 changes: 114 additions & 41 deletions package/MDAnalysis/core/topologyattrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@
import numbers
import numpy as np
import warnings
import textwrap

# inspect.signature was added in python 3.3, earlier versions require
# funcsigs as backport.
try:
from inspect import signature as inspect_signature
except ImportError:
from funcsigs import signature as inspect_signature

from ..lib.util import (cached, convert_aa_code, iterable, warn_if_not_unique,
unique_int_1d)
Expand Down Expand Up @@ -166,6 +173,105 @@ def _wronglevel_error(attr, group):
))


def _build_stub(method_name, method, attribute_name):
"""
Build a stub for a transplanted method.
A transplanted stub is a dummy method that gets attached to a core class
(usually from :mod:`MDAnalysis.core.groups`) and raises a
:exc:`NoDataError`.
The stub mimics the original method for everything that has traits with the
documentation (docstring, name, signature). It gets overwritten by the
actual method when the latter is transplanted at universe creation.
Parameters
----------
method_name: str
The name of the attribute in the destination class.
method: Callable
The method to be mimicked.
attribute_name: str
The name topology attribute that is required for the method to be
relevant (e.g. masses, charges, ...)
Returns
-------
The stub.
"""
def stub_method(self, *args, **kwargs):
message = (
'{class_name}.{method_name}() '
'not available; this requires {attribute_name}'
).format(
class_name=self.__class__.__name__,
method_name=method_name,
attribute_name=attribute_name,
)
raise NoDataError(message)

annotation = textwrap.dedent("""\
.. note::
This requires the underlying topology to have {}. Otherwise, a
:exc:`~MDAnalysis.exceptions.NoDataError` is raised.
""".format(attribute_name))
# The first line of the original docstring is not indented, but the
# subsequent lines are. We want to dedent the whole docstring.
first_line, other_lines = method.__doc__.split('\n', 1)
stub_method.__doc__ = (
first_line + '\n'
+ textwrap.dedent(other_lines)
+ '\n\n' + annotation
)
stub_method.__name__ = method_name
stub_method.__signature__ = inspect_signature(method)
return stub_method


def _attach_transplant_stubs(attribute_name, topology_attribute_class):
"""
Transplant a stub for every method that will be transplanted from a
topology attribute.
Parameters
----------
attribute_name: str
User-facing name of the topology attribute (e.g. masses, charges, ...)
topology_attribute_class:
Topology attribute class to inspect for transplant methods.
"""
transplants = topology_attribute_class.transplants
for dest_class, methods in transplants.items():
if dest_class == 'Universe':
# Cannot be imported at the top level, it creates issues with
# circular imports.
from .universe import Universe
dest_class = Universe
for method_name, method_callback in methods:
# Methods the name of which is prefixed by _ should not be accessed
# directly by a user, we do not transplant a stub as the stubs are
# only relevant for user-facing method and properties. Also,
# methods _-prefixed can be operator methods, and we do not want
# to overwrite these with a stub.
if method_name.startswith('_'):
continue

is_property = False
try:
method_callback = method_callback.fget
is_property = True
except AttributeError:
pass
stub = _build_stub(method_name, method_callback, attribute_name)
if is_property:
setattr(dest_class, method_name, property(stub, None, None))
else:
setattr(dest_class, method_name, stub)


class _TopologyAttrMeta(type):
# register TopologyAttrs
def __init__(cls, name, bases, classdict):
Expand All @@ -189,6 +295,14 @@ def __init__(cls, name, bases, classdict):
clean = name.lower().replace('_', '')
_TOPOLOGY_ATTRNAMES[clean] = name

for attr in ['singular', 'attrname']:
try:
attrname = classdict[attr]
except KeyError:
pass
else:
_attach_transplant_stubs(attrname, cls)


class TopologyAttr(six.with_metaclass(_TopologyAttrMeta, object)):
"""Base class for Topology attributes.
Expand Down Expand Up @@ -1347,15 +1461,6 @@ def shape_parameter(group, pbc=False, **kwargs):
calculation. [``False``]
References
----------
.. [Dima2004a] Dima, R. I., & Thirumalai, D. (2004). Asymmetry
in the shapes of folded and denatured states of
proteins. *J Phys Chem B*, 108(21),
6564-6570. doi:`10.1021/jp037128y
<https://doi.org/10.1021/jp037128y>`_
.. versionadded:: 0.7.7
.. versionchanged:: 0.8 Added *pbc* keyword
Expand Down Expand Up @@ -1401,16 +1506,6 @@ def asphericity(group, pbc=False, unwrap=None, compound='group'):
Which type of component to keep together during unwrapping.
References
----------
.. [Dima2004b] Dima, R. I., & Thirumalai, D. (2004). Asymmetry
in the shapes of folded and denatured states of
proteins. *J Phys Chem B*, 108(21),
6564-6570. doi:`10.1021/jp037128y
<https://doi.org/10.1021/jp037128y>`_
.. versionadded:: 0.7.7
.. versionchanged:: 0.8 Added *pbc* keyword
.. versionchanged:: 0.20.0 Added *unwrap* and *compound* parameter
Expand Down Expand Up @@ -2259,11 +2354,6 @@ def fragindex(self):
:class:`~MDAnalysis.core.topologyattrs.Bonds.fragment` this
:class:`~MDAnalysis.core.groups.Atom` is part of.
Note
----
This property is only accessible if the underlying topology contains
bond information.
.. versionadded:: 0.20.0
"""
Expand All @@ -2279,11 +2369,6 @@ def fragindices(self):
:attr:`~numpy.ndarray.shape`\ ``=(``\ :attr:`~AtomGroup.n_atoms`\ ``,)``
and :attr:`~numpy.ndarray.dtype`\ ``=numpy.int64``.
Note
----
This property is only accessible if the underlying topology contains
bond information.
.. versionadded:: 0.20.0
"""
Expand All @@ -2302,11 +2387,6 @@ def fragment(self):
of :class:`Atoms<MDAnalysis.core.groups.Atom>`
within a fragment. Thus, a fragment typically corresponds to a molecule.
Note
----
This property is only accessible if the underlying topology contains
bond information.
.. versionadded:: 0.9.0
"""
Expand All @@ -2330,8 +2410,6 @@ def fragments(self):
Note
----
* This property is only accessible if the underlying topology contains
bond information.
* The contents of the fragments may extend beyond the contents of this
:class:`~MDAnalysis.core.groups.AtomGroup`.
Expand All @@ -2348,11 +2426,6 @@ def n_fragments(self):
:class:`Atoms<MDAnalysis.core.groups.Atom>` of this
:class:`~MDAnalysis.core.groups.AtomGroup` are part of.
Note
----
This property is only accessible if the underlying topology contains
bond information.
.. versionadded:: 0.20.0
"""
Expand Down
26 changes: 26 additions & 0 deletions package/doc/sphinx/source/documentation_pages/references.rst
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,34 @@ If you use :meth:`~MDAnalysis.analysis.pca.PCA.cumulative_overlap` or



If you calculate shape parameters using
:meth:`~MDAnalysis.core.group.AtomGroup.shape_parameter`,
:meth:`~MDAnalysis.core.group.ResidueGroup.shape_parameter`,
:meth:`~MDAnalysis.core.group.SegmentGroup.shape_parameter`
please cite [Dima2004a]_.

.. [Dima2004a] Dima, R. I., & Thirumalai, D. (2004). Asymmetry
in the shapes of folded and denatured states of
proteins. *J Phys Chem B*, 108(21),
6564-6570. doi:`10.1021/jp037128y
<https://doi.org/10.1021/jp037128y>`_
If you calculate asphericities using
:meth:`~MDAnalysis.core.group.AtomGroup.asphericity`,
:meth:`~MDAnalysis.core.group.ResidueGroup.asphericity`,
:meth:`~MDAnalysis.core.group.SegmentGroup.asphericity`
please cite [Dima2004b]_.

.. [Dima2004b] Dima, R. I., & Thirumalai, D. (2004). Asymmetry
in the shapes of folded and denatured states of
proteins. *J Phys Chem B*, 108(21),
6564-6570. doi:`10.1021/jp037128y
<https://doi.org/10.1021/jp037128y>`_
.. _citations-using-duecredit:


Citations using Duecredit
=========================

Expand Down
3 changes: 2 additions & 1 deletion package/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,13 +579,14 @@ def long_description(readme):
'biopython>=1.71,<1.77', # to support Py 2
'networkx>=1.0',
'GridDataFormats>=0.4.0',
'six>=1.4.0',
'six>=1.4.0', # to support Py 2
'mmtf-python>=1.0.0',
'joblib>=0.12,<0.15.0', # to support Py 2
'scipy>=1.0.0',
'matplotlib>=1.5.1',
'mock',
'tqdm>=4.43.0',
'funcsigs', # to support Py 2
]
if not os.name == 'nt':
install_requires.append('gsd>=1.4.0')
Expand Down
23 changes: 23 additions & 0 deletions testsuite/MDAnalysisTests/core/test_topologyattrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,26 @@ def test_static_typing_from_empty():

assert isinstance(u._topology.masses.values, np.ndarray)
assert isinstance(u.atoms[0].mass, float)


@pytest.mark.parametrize('level, transplant_name', (
('atoms', 'center_of_mass'),
('atoms', 'total_charge'),
('residues', 'total_charge'),
))
def test_stub_transplant_methods(level, transplant_name):
u = mda.Universe.empty(n_atoms=2)
group = getattr(u, level)
with pytest.raises(NoDataError):
getattr(group, transplant_name)()


@pytest.mark.parametrize('level, transplant_name', (
('universe', 'models'),
('atoms', 'n_fragments'),
))
def test_stub_transplant_property(level, transplant_name):
u = mda.Universe.empty(n_atoms=2)
group = getattr(u, level)
with pytest.raises(NoDataError):
getattr(group, transplant_name)
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def test_center_in_box_no_masses(translate_universes):
ag = translate_universes[0].residues[0].atoms
# if the universe has no masses and `mass` is passed as the center arg
bad_center = "mass"
with pytest.raises(AttributeError):
with pytest.raises(mda.NoDataError):
center_in_box(ag, center=bad_center)(ts)


Expand Down

0 comments on commit 5a259b7

Please sign in to comment.