diff --git a/maintainer/conda/MDAnalysis/meta.yaml b/maintainer/conda/MDAnalysis/meta.yaml index ae7ce259cde..01dd6a5bb5e 100644 --- a/maintainer/conda/MDAnalysis/meta.yaml +++ b/maintainer/conda/MDAnalysis/meta.yaml @@ -36,6 +36,7 @@ requirements: - netcdf4 - mmtf-python - scikit-learn + - funcsigs test: imports: diff --git a/package/CHANGELOG b/package/CHANGELOG index ca65ebf063f..4589c6a5fec 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -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 diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index 8b0531f9052..74edf05ab74 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -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 diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 7eb766466bd..6e0ad095cc3 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -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) @@ -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): @@ -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. @@ -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 - `_ - - .. versionadded:: 0.7.7 .. versionchanged:: 0.8 Added *pbc* keyword @@ -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 - `_ - - .. versionadded:: 0.7.7 .. versionchanged:: 0.8 Added *pbc* keyword .. versionchanged:: 0.20.0 Added *unwrap* and *compound* parameter @@ -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 """ @@ -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 """ @@ -2302,11 +2387,6 @@ def fragment(self): of :class:`Atoms` 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 """ @@ -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`. @@ -2348,11 +2426,6 @@ def n_fragments(self): :class:`Atoms` 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 """ diff --git a/package/doc/sphinx/source/documentation_pages/references.rst b/package/doc/sphinx/source/documentation_pages/references.rst index 594f0719748..e1d2790877a 100644 --- a/package/doc/sphinx/source/documentation_pages/references.rst +++ b/package/doc/sphinx/source/documentation_pages/references.rst @@ -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 + `_ + +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 + `_ + + .. _citations-using-duecredit: + Citations using Duecredit ========================= diff --git a/package/setup.py b/package/setup.py index be52ed13d39..e48e12c52e2 100755 --- a/package/setup.py +++ b/package/setup.py @@ -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') diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index 6e7146941a1..6fb3e8d8e17 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -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) diff --git a/testsuite/MDAnalysisTests/transformations/test_translate.py b/testsuite/MDAnalysisTests/transformations/test_translate.py index 413c3338bce..3c66e65b6cf 100644 --- a/testsuite/MDAnalysisTests/transformations/test_translate.py +++ b/testsuite/MDAnalysisTests/transformations/test_translate.py @@ -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)