Skip to content

Commit 1e3f362

Browse files
added READER_HINTS (#2533)
* added _READER_HINTS allows Reader to interrogate object and return suitability * added _TOPOLOGY_HINTS Co-authored-by: Oliver Beckstein <[email protected]>
1 parent 156ca53 commit 1e3f362

File tree

11 files changed

+141
-55
lines changed

11 files changed

+141
-55
lines changed

package/MDAnalysis/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,11 @@
169169
# Registry of Readers, Parsers and Writers known to MDAnalysis
170170
# Metaclass magic fills these as classes are declared.
171171
_READERS = {}
172+
_READER_HINTS = {}
172173
_SINGLEFRAME_WRITERS = {}
173174
_MULTIFRAME_WRITERS = {}
174175
_PARSERS = {}
176+
_PARSER_HINTS = {}
175177
_SELECTION_WRITERS = {}
176178
_CONVERTERS = {}
177179
# Registry of TopologyAttributes

package/MDAnalysis/coordinates/MMTF.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ class MMTFReader(base.SingleFrameReaderBase):
6161
"""Coordinate reader for the Macromolecular Transmission Format format (MMTF_)."""
6262
format = 'MMTF'
6363

64+
@staticmethod
65+
def _format_hint(thing):
66+
"""Can this Reader read *thing*?
67+
68+
.. versionadded:: 1.0.0
69+
"""
70+
return isinstance(thing, mmtf.MMTFDecoder)
71+
6472
@due.dcite(
6573
Doi('10.1371/journal.pcbi.1005575'),
6674
description="MMTF Reader",

package/MDAnalysis/coordinates/ParmEd.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ class ParmEdReader(base.SingleFrameReaderBase):
8787
# Structure.coordinates always in Angstrom
8888
units = {'time': None, 'length': 'Angstrom'}
8989

90+
@staticmethod
91+
def _format_hint(thing):
92+
"""Can this reader read *thing*?
93+
94+
.. versionadded:: 1.0.0
95+
"""
96+
try:
97+
import parmed as pmd
98+
except ImportError:
99+
# if we can't import parmed, it's probably not parmed
100+
return False
101+
else:
102+
return isinstance(thing, pmd.Structure)
103+
90104
def _read_first_frame(self):
91105
self.n_atoms = len(self.filename.atoms)
92106

package/MDAnalysis/coordinates/__init__.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -279,30 +279,6 @@ class can choose an appropriate reader automatically.
279279
:mod:`MDAnalysis.coordinates.base`.
280280
281281
282-
History
283-
~~~~~~~
284-
285-
- 2010-04-30 Draft [orbeckst]
286-
- 2010-08-20 added single frame writers to API [orbeckst]
287-
- 2010-10-09 added write() method to Writers [orbeckst]
288-
- 2010-10-19 use close() instead of close_trajectory() [orbeckst]
289-
- 2010-10-30 clarified Writer write() methods (see also `Issue 49`_)
290-
- 2011-02-01 extended call signature of Reader class
291-
- 2011-03-30 optional Writer() method for Readers
292-
- 2011-04-18 added time and frame managed attributes to Reader
293-
- 2011-04-20 added volume to Timestep
294-
- 2012-02-11 added _velocities to Timestep
295-
- 2012-05-24 multiframe keyword to distinguish trajectory from single frame writers
296-
- 2012-06-04 missing implementations of Reader.__getitem__ should raise :exc:`TypeError`
297-
- 2013-08-02 Readers/Writers must conform to the Python `Context Manager`_ API
298-
- 2015-01-15 Timestep._init_unitcell() method added
299-
- 2015-06-11 Reworked Timestep init. Base Timestep now does Vels & Forces
300-
- 2015-07-21 Major changes to Timestep and Reader API (release 0.11.0)
301-
- 2016-04-03 Removed references to Strict Readers for PDBS [jdetle]
302-
303-
.. _Issue 49: https://github.com/MDAnalysis/mdanalysis/issues/49
304-
.. _Context Manager: http://docs.python.org/2/reference/datamodel.html#context-managers
305-
306282
Registry
307283
~~~~~~~~
308284
@@ -316,6 +292,13 @@ class can choose an appropriate reader automatically.
316292
defining the expected suffix. To assign multiple suffixes to an I/O class, a
317293
list of suffixes can be given.
318294
295+
In addition to this, a Reader may define a ``_format_hint`` staticmethod, which
296+
returns a boolean of if it can process a given object. E.g. the
297+
:class:`MDAnalysis.coordinates.memory.MemoryReader` identifies itself as
298+
capable of reading numpy arrays. This functionality is used in
299+
:func:`MDAnalysis.core._get_readers.get_reader_for` when figuring out how to
300+
read an object (which was usually supplied to mda.Universe).
301+
319302
To define that a Writer can write multiple trajectory frames, set the
320303
`multiframe` attribute to ``True``. The default is ``False``.
321304
To define that a Writer *does not* support single frame writing the
@@ -489,6 +472,8 @@ class can choose an appropriate reader automatically.
489472
``__exit__()``
490473
exit method of a `Context Manager`_, should call ``close()``.
491474
475+
.. _Context Manager: http://docs.python.org/2/reference/datamodel.html#context-managers
476+
492477
.. Note::
493478
a ``__del__()`` method should also be present to ensure that the
494479
trajectory is properly closed. However, certain types of Reader can ignore

package/MDAnalysis/coordinates/base.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@
210210
from . import core
211211
from .. import NoDataError
212212
from .. import (
213-
_READERS,
213+
_READERS, _READER_HINTS,
214214
_SINGLEFRAME_WRITERS,
215215
_MULTIFRAME_WRITERS,
216216
_CONVERTERS
@@ -1362,6 +1362,11 @@ def __exit__(self, exc_type, exc_val, exc_tb):
13621362

13631363

13641364
class _Readermeta(type):
1365+
"""Automatic Reader registration metaclass
1366+
1367+
.. versionchanged:: 1.0.0
1368+
Added _format_hint functionality
1369+
"""
13651370
# Auto register upon class creation
13661371
def __init__(cls, name, bases, classdict):
13671372
type.__init__(type, name, bases, classdict)
@@ -1370,9 +1375,13 @@ def __init__(cls, name, bases, classdict):
13701375
except KeyError:
13711376
pass
13721377
else:
1373-
for f in fmt:
1374-
f = f.upper()
1375-
_READERS[f] = cls
1378+
for fmt_name in fmt:
1379+
fmt_name = fmt_name.upper()
1380+
_READERS[fmt_name] = cls
1381+
1382+
if '_format_hint' in classdict:
1383+
# isn't bound yet, so access __func__
1384+
_READER_HINTS[fmt_name] = classdict['_format_hint'].__func__
13761385

13771386

13781387
class ProtoReader(six.with_metaclass(_Readermeta, IOBase)):

package/MDAnalysis/coordinates/chain.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252

5353
import numpy as np
5454

55+
from ..lib import util
5556
from ..lib.util import asiterable
5657
from . import base
5758
from . import core
@@ -367,6 +368,16 @@ def __init__(self, filenames, skip=1, dt=None, continuous=False, **kwargs):
367368
self.ts = None
368369
self.rewind()
369370

371+
@staticmethod
372+
def _format_hint(thing):
373+
"""Can ChainReader read the object *thing*
374+
375+
.. versionadded:: 1.0.0
376+
"""
377+
return (not isinstance(thing, np.ndarray) and
378+
util.iterable(thing) and
379+
not util.isstream(thing))
380+
370381
def _get_local_frame(self, k):
371382
"""Find trajectory index and trajectory frame for chained frame `k`.
372383

package/MDAnalysis/coordinates/memory.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,14 @@ def __init__(self, coordinate_array, order='fac',
400400
self.ts.time = -1
401401
self._read_next_timestep()
402402

403+
@staticmethod
404+
def _format_hint(thing):
405+
"""For internal use: Check if MemoryReader can operate on *thing*
406+
407+
.. versionadded:: 1.0.0
408+
"""
409+
return isinstance(thing, np.ndarray)
410+
403411
@staticmethod
404412
def parse_n_atoms(filename, order='fac', **kwargs):
405413
"""Deduce number of atoms in a given array of coordinates

package/MDAnalysis/core/_get_readers.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,10 @@
2424

2525
import copy
2626
import inspect
27-
import mmtf
28-
import parmed as pmd
29-
import numpy as np
30-
from MDAnalysis.lib.util import isstream
3127

32-
from .. import _READERS, _PARSERS, _MULTIFRAME_WRITERS, _SINGLEFRAME_WRITERS, _CONVERTERS
28+
from .. import (_READERS, _READER_HINTS,
29+
_PARSERS, _PARSER_HINTS,
30+
_MULTIFRAME_WRITERS, _SINGLEFRAME_WRITERS, _CONVERTERS)
3331
from ..lib import util
3432

3533

@@ -77,26 +75,23 @@ def get_reader_for(filename, format=None):
7775
:class:`~MDAnalysis.coordinates.chain.ChainReader` is returned and `format`
7876
passed to the :class:`~MDAnalysis.coordinates.chain.ChainReader`.
7977
78+
.. versionchanged:: 1.0.0
79+
Added format_hint functionalityx
8080
"""
8181
# check if format is actually a Reader
8282
if inspect.isclass(format):
8383
return format
8484

8585
# ChainReader gets returned even if format is specified
86-
if not isinstance(filename, np.ndarray) and util.iterable(filename) and not isstream(filename):
86+
if _READER_HINTS['CHAIN'](filename):
8787
format = 'CHAIN'
8888
# Only guess if format is not specified
89-
elif format is None:
90-
# Checks for specialised formats
91-
if isinstance(filename, np.ndarray):
92-
# memoryreader slurps numpy arrays
93-
format = 'MEMORY'
94-
elif isinstance(filename, mmtf.MMTFDecoder):
95-
# mmtf slurps mmtf object
96-
format = 'MMTF'
97-
elif isinstance(filename, pmd.Structure):
98-
format = 'PARMED'
99-
else:
89+
if format is None:
90+
for fmt_name, test in _READER_HINTS.items():
91+
if test(filename):
92+
format = fmt_name
93+
break
94+
else: # hits else if for loop completes
10095
# else let the guessing begin!
10196
format = util.guess_format(filename)
10297
format = format.upper()
@@ -231,16 +226,18 @@ def get_parser_for(filename, format=None):
231226
ValueError
232227
If no appropriate parser could be found.
233228
229+
.. versionchanged:: 1.0.0
230+
Added format_hint functionality
234231
"""
235232
if inspect.isclass(format):
236233
return format
237234

238235
# Only guess if format is not provided
239236
if format is None:
240-
if isinstance(filename, mmtf.MMTFDecoder):
241-
format = 'mmtf'
242-
elif isinstance(filename, pmd.Structure):
243-
format = 'PARMED'
237+
for fmt_name, test in _PARSER_HINTS.items():
238+
if test(filename):
239+
format = fmt_name
240+
break
244241
else:
245242
format = util.guess_format(filename)
246243
format = format.upper()
@@ -286,4 +283,4 @@ def get_converter_for(format):
286283
except KeyError:
287284
errmsg = 'No converter found for {} format'
288285
raise_from(TypeError(errmsg.format(format)), None)
289-
return writer
286+
return writer

package/MDAnalysis/topology/MMTFParser.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ def apply(self, group):
152152
class MMTFParser(base.TopologyReaderBase):
153153
format = 'MMTF'
154154

155+
@staticmethod
156+
def _format_hint(thing):
157+
"""Can parser read *thing*?
158+
159+
.. versionadded:: 1.0.0
160+
"""
161+
return isinstance(thing, mmtf.MMTFDecoder)
162+
155163
@due.dcite(
156164
Doi('10.1371/journal.pcbi.1005575'),
157165
description="MMTF Parser",

package/MDAnalysis/topology/ParmEdParser.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ class ParmEdParser(TopologyReaderBase):
138138
"""
139139
format = 'PARMED'
140140

141+
@staticmethod
142+
def _format_hint(thing):
143+
"""Can this Parser read object *thing*?
144+
145+
.. versionadded:: 1.0.0
146+
"""
147+
try:
148+
import parmed as pmd
149+
except ImportError: # if no parmed, probably not parmed
150+
return False
151+
else:
152+
return isinstance(thing, pmd.Structure)
153+
141154
def parse(self, **kwargs):
142155
"""Parse PARMED into Topology
143156

0 commit comments

Comments
 (0)