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

Converters API #2882

Merged
merged 41 commits into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d10357d
kwargs for converter + case insensitive package
Jul 30, 2020
515b108
fixes
Jul 30, 2020
95b768a
prototype for convert_to accessor
Jul 30, 2020
840bfd9
automatic addition of convert_to.lib() methods
Jul 30, 2020
db45e3e
doc + tests
Jul 30, 2020
a29ee62
move accessors
Jul 31, 2020
4ca5baf
pep8
Jul 31, 2020
2161177
fix unused import
cbouy Apr 24, 2021
e84273b
changed the Accessor and ConverterWrapper to cache the wrapped access…
cbouy Apr 25, 2021
9d9586f
fix pep8
cbouy Apr 25, 2021
b93076a
move tests
cbouy Apr 25, 2021
6b660c5
add changelog
cbouy May 5, 2021
9666c15
test ConverterWrapper
cbouy May 5, 2021
5867457
docs
cbouy May 5, 2021
27aa366
fix access from class
cbouy May 5, 2021
bfdbedf
fix test min-deps
cbouy May 6, 2021
8cea0e0
fix docs
cbouy May 6, 2021
9792cbc
Merge branch 'develop' into converters-api
cbouy May 6, 2021
e506380
fix min-deps tests
cbouy May 6, 2021
eb19fc6
Merge branch 'converters-api' of github.com:cbouy/mdanalysis into con…
cbouy May 6, 2021
18b58c4
show accessor and converter classes in docs
cbouy May 6, 2021
7442a4c
fixes to docs
cbouy May 7, 2021
71b0fa8
move to converters
cbouy May 7, 2021
85dbefd
ud changelog
cbouy May 7, 2021
399afb6
add deprecation warnings
cbouy May 7, 2021
8a5231e
Merge branch 'develop' into converters-api
cbouy May 7, 2021
63d953f
pep8
cbouy May 7, 2021
8456cf5
move accessors to core
cbouy May 7, 2021
18dc71e
fix tests
cbouy May 7, 2021
e5abca5
include openmm
cbouy May 7, 2021
fa7077c
fix and move openmm tests
cbouy May 7, 2021
47b798e
fix openmm docs
cbouy May 7, 2021
7f1fa38
fix imports
cbouy May 7, 2021
38ed979
fix docs
cbouy May 7, 2021
646c479
fix openmm relative imports
cbouy May 7, 2021
2f919cb
fix tests
cbouy May 7, 2021
7db6ddf
Merge branch 'develop' into converters-api
IAlibay May 9, 2021
249bc74
Fix docs
IAlibay May 9, 2021
5b25b49
Fix parmed imports, adds warning tests
IAlibay May 9, 2021
8b6c18f
Remove init.rst from converters docs
IAlibay May 10, 2021
26ebd6e
Update changelog
IAlibay May 10, 2021
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
1 change: 1 addition & 0 deletions package/MDAnalysis/converters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import accessors
187 changes: 187 additions & 0 deletions package/MDAnalysis/converters/accessors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*-
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8
#
# MDAnalysis --- https://www.mdanalysis.org
# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors
# (see the file AUTHORS for the full list of names)
#
# Released under the GNU Public Licence, v2 or any higher version
#
# Please cite your use of MDAnalysis in published work:
#
# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler,
# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein.
# MDAnalysis: A Python package for the rapid analysis of molecular dynamics
# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th
# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy.
# doi: 10.25080/majora-629e541a-00e
#
# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein.
# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
#

"""AtomGroup accessors --- :mod:`MDAnalysis.lib.accessors`
=============================================================

The aim of this module is simply to link a wrapper class to an object in order
to avoid cluttering the namespace of the object.

Example
-------

.. code-block:: python

>>> class SpeechWrapper:
... def __init__(self, person):
... self.person = person
... def __call__(self, *args):
... print(self.person.name, "says", *args)
... def whoami(self):
... print("I am %s" % self.person.name)
...
>>> class Person:
... def __init__(self, name):
... self.name = name
... say = Accessor(SpeechWrapper)
...
>>> bob = Person("Bob")
>>> bob.say("hello")
Bob says hello
>>> bob.say.whoami()
I am Bob

"""

from functools import partial, update_wrapper

from .. import _CONVERTERS


class Accessor:
"""Used to pass data between two classes

Parameters
----------
name : str
Name of the property in the parent class
accessor : class
A class that needs access to its parent's instance

Example
-------
If you want the property to be named "convert_to" in the AtomGroup class,
use:

.. code-block:: python

>>> class AtomGroup:
>>> # ...
>>> convert_to = Accessor("convert_to", ConverterWrapper)

And when calling ``ag.convert_to.rdkit()``, the "rdkit" method of the
ConverterWrapper will be able to have access to "ag"
"""

def __init__(self, name, accessor):
self._accessor = accessor
self._name = name

def __get__(self, obj, cls):
# instances the accessor class with the parent object as argument
wrapped = self._accessor(obj)
# replace the parent object's property with the wrapped instance
# so we avoid reinstantiating the accessor everytime `obj.<_name>`
# is called
object.__setattr__(obj, self._name, wrapped)
return wrapped


class ConverterWrapper:
"""Convert :class:`AtomGroup` to a structure from another Python
package.

The converters are accessible to any AtomGroup through the ``convert_to``
property. `ag.convert_to` will return this ConverterWrapper, which can be
called directly with the name of the destination package as a string
(similarly to the old API), or through custom methods named after the
package (in lowercase) that are automatically constructed thanks to
metaclass magic.

Example
-------
The code below converts a Universe to a :class:`parmed.structure.Structure`.

.. code-block:: python

>>> import MDAnalysis as mda
>>> from MDAnalysis.tests.datafiles import GRO
>>> u = mda.Universe(GRO)
>>> parmed_structure = u.atoms.convert_to('PARMED')
>>> parmed_structure
<Structure 47681 atoms; 11302 residues; 0 bonds; PBC (triclinic); NOT parametrized>

You can also directly use ``u.atoms.convert_to.parmed()``

Parameters
----------
package: str
The name of the package to convert to, e.g. ``"PARMED"``
*args:
Positional arguments passed to the converter
**kwargs:
Keyword arguments passed to the converter

Returns
-------
output:
An instance of the structure type from another package.

Raises
------
TypeError:
No converter was found for the required package


.. versionadded:: 1.0.0
.. versionchanged:: 2.0.0
Moved the ``convert_to`` method to its own class. The old API is still
available and is now case-insensitive to package names, it also accepts
positional and keyword arguments. Each converter function can also
be accessed as a method with the name of the package in lowercase, i.e.
`convert_to.parmed()`
"""
_CONVERTERS = {}

def __init__(self, ag):
"""
Parameters
----------
ag : AtomGroup
The AtomGroup to convert
"""
self._ag = ag
for lib, converter_cls in _CONVERTERS.items():
method_name = lib.lower()
# makes sure we always use the same instance of the converter
# no matter which atomgroup instance called it
try:
converter = self._CONVERTERS[method_name]
except KeyError:
converter = converter_cls().convert
# store in class attribute
self._CONVERTERS[method_name] = converter
# create partial function that passes ag to the converter
convert = partial(converter, self._ag)
# copy docstring and metadata to the partial function
# note: it won't work with help()
update_wrapper(convert, converter)
setattr(self, method_name, convert)

def __call__(self, package, *args, **kwargs):
try:
convert = getattr(self, package.lower())
except AttributeError:
raise TypeError(f"No {package!r} converter found. Available: "
cbouy marked this conversation as resolved.
Show resolved Hide resolved
f"{' '.join(self._CONVERTERS.keys())}") from None
return convert(*args, **kwargs)
42 changes: 2 additions & 40 deletions package/MDAnalysis/core/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
from ..lib import distances
from ..lib import transformations
from ..lib import mdamath
from ..converters.accessors import Accessor, ConverterWrapper
from ..selections import get_writer as get_selection_writer_for
from . import selection
from ..exceptions import NoDataError
Expand Down Expand Up @@ -3146,46 +3147,7 @@ def cmap(self):
"cmap only makes sense for a group with exactly 5 atoms")
return topologyobjects.CMap(self.ix, self.universe)

def convert_to(self, package):
"""
Convert :class:`AtomGroup` to a structure from another Python package.

Example
orbeckst marked this conversation as resolved.
Show resolved Hide resolved
-------

The code below converts a Universe to a :class:`parmed.structure.Structure`.

.. code-block:: python

>>> import MDAnalysis as mda
>>> from MDAnalysis.tests.datafiles import GRO
>>> u = mda.Universe(GRO)
>>> parmed_structure = u.atoms.convert_to('PARMED')
>>> parmed_structure
<Structure 47681 atoms; 11302 residues; 0 bonds; PBC (triclinic); NOT parametrized>


Parameters
----------
package: str
The name of the package to convert to, e.g. ``"PARMED"``


Returns
-------
output:
An instance of the structure type from another package.

Raises
------
TypeError:
No converter was found for the required package


.. versionadded:: 1.0.0
"""
converter = get_converter_for(package)
return converter().convert(self.atoms)
convert_to = Accessor("convert_to", ConverterWrapper)

def write(self, filename=None, file_format=None,
filenamefmt="{trjname}_{frame}", frames=None, **kwargs):
Expand Down
50 changes: 50 additions & 0 deletions testsuite/MDAnalysisTests/converters/test_accessors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*-
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8
#
# MDAnalysis --- https://www.mdanalysis.org
# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors
# (see the file AUTHORS for the full list of names)
#
# Released under the GNU Public Licence, v2 or any higher version
#
# Please cite your use of MDAnalysis in published work:
#
# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler,
# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein.
# MDAnalysis: A Python package for the rapid analysis of molecular dynamics
# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th
# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy.
# doi: 10.25080/majora-629e541a-00e
#
# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein.
# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
#
import pytest
import MDAnalysis as mda
from MDAnalysisTests.util import import_not_available


requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"),
reason="requires RDKit")


@requires_rdkit
class TestConvertTo:
@pytest.fixture(scope="class")
def u(self):
return mda.Universe.from_smiles("CCO")

def test_convert_to_case_insensitive(self, u):
mol = u.atoms.convert_to("rdkit")

def test_convert_to_lib_as_method(self, u):
mol = u.atoms.convert_to.rdkit()

def test_convert_to_kwargs(self, u):
mol = u.atoms.convert_to("RDKIT", NoImplicit=False)
assert mol.GetAtomWithIdx(0).GetNoImplicit() is False

def test_convert_to_lib_method_kwargs(self, u):
mol = u.atoms.convert_to.rdkit(NoImplicit=False)
assert mol.GetAtomWithIdx(0).GetNoImplicit() is False