Description
Is your feature request related to a problem? Please describe.
In a recent PR #521 it was noted that there are a lot of repeated code patterns concerning calls to back end toolkits which are normally copied and pasted when implementing new features. It might be nice to remove these patterns using decorators which can automatically fill in the new method for us. For context here is a typical back end call pattern:
@staticmethod
def from_inchi(inchi, allow_undefined_stereo=False, toolkit_registry=GLOBAL_TOOLKIT_REGISTRY):
"""
Construct a Molecule from a InChI representation
Parameters
----------
inchi : str
The InChI representation of the molecule.
allow_undefined_stereo : bool, default=False
Whether to accept InChI with undefined stereochemistry. If False,
an exception will be raised if a InChI with undefined stereochemistry
is passed into this function.
toolkit_registry : openforcefield.utils.toolkits.ToolRegistry or openforcefield.utils.toolkits.ToolkitWrapper, optional, default=None
:class:`ToolkitRegistry` or :class:`ToolkitWrapper` to use for SMILES-to-molecule conversion
Returns
-------
molecule : openforcefield.topology.Molecule
Examples
--------
make cis-1,2-Dichloroethene
>>> molecule = Molecule.from_inchi('InChI=1S/C2H2Cl2/c3-1-2-4/h1-2H/b2-1-')
"""
if isinstance(toolkit_registry, ToolkitRegistry):
molecule = toolkit_registry.call('from_inchi',
inchi,
allow_undefined_stereo=allow_undefined_stereo)
elif isinstance(toolkit_registry, ToolkitWrapper):
toolkit = toolkit_registry
molecule = toolkit.from_inchi(inchi,
allow_undefined_stereo=allow_undefined_stereo)
else:
raise Exception(
'Invalid toolkit_registry passed to from_inchi. Expected ToolkitRegistry or ToolkitWrapper. Got {}'
.format(type(toolkit_registry)))
return molecule
def to_inchi(self, fixed_hydrogens=False, toolkit_registry=GLOBAL_TOOLKIT_REGISTRY):
"""
Create an InChI string for the molecule using the requested toolkit backend.
InChI is a standardised representation that does not capture tautomers unless specified using the fixed hydrogen
layer.
For information on InChi see here https://iupac.org/who-we-are/divisions/division-details/inchi/
Parameters
----------
fixed_hydrogens: bool, default=False
If a fixed hydrogen layer should be added to the InChI, if `True` this will produce a non standard specific
InChI string of the molecule.
toolkit_registry : openforcefield.utils.toolkits.ToolRegistry or openforcefield.utils.toolkits.ToolkitWrapper, optional, default=None
:class:`ToolkitRegistry` or :class:`ToolkitWrapper` to use for SMILES-to-molecule conversion
Returns
--------
inchi: str
The InChI string of the molecule.
Raises
-------
InvalidToolkitError
If an invalid object is passed as the toolkit_registry parameter
"""
if isinstance(toolkit_registry, ToolkitRegistry):
inchi = toolkit_registry.call('to_inchi',
self,
fixed_hydrogens=fixed_hydrogens)
elif isinstance(toolkit_registry, ToolkitWrapper):
toolkit = toolkit_registry
inchi = toolkit.to_inchi(self,
fixed_hydrogens=fixed_hydrogens)
else:
raise InvalidToolkitError(
'Invalid toolkit_registry passed to to_inchi. Expected ToolkitRegistry or ToolkitWrapper. Got {}'
.format(type(toolkit_registry)))
return inchi
Here is my interpretation of what the decorator may look like to help achieve this:
# test wrapper function
def handle_via_toolkit(orig_func):
"""Check if the function can be performed by the toolkit or not"""
@wraps(orig_func)
def wrapper(*args, **kwargs):
toolkit_functions = ['to_smiles', 'to_inchi']
if orig_func.__name__ in toolkit_functions:
toolkit_registry = kwargs.get('toolkit_registry', GLOBAL_TOOLKIT_REGISTRY)
if isinstance(toolkit_registry, ToolkitRegistry):
data = toolkit_registry.call(orig_func.__name__, *args, **kwargs)
elif isinstance(toolkit_registry, ToolkitWrapper):
function = getattr(toolkit_registry, orig_func.__name__)
data = function(*args, **kwargs)
else:
raise Exception(
f'Invalid toolkit_registry passed to {orig_func.__name__}. Expected ToolkitRegistry or '
f'ToolkitWrapper. Got {type(toolkit_registry)}')
return data
else:
return orig_func(*args, **kwargs)
return wrapper
This decorator would then be placed on all functions that are handled via the toolkit back ends and this would allow us to keep the functions in the Molecule
class and the docstrings but the function could just pass as the logic will be handled by the decorator. We could also have the toolkit_functions
list automatically populate so we don't have to remember to update the list every time we add a function as well. The nice part of doing it this way is I still get autocomplete to work with the functions in pycharm, the only downside is we lose hints on the function arguments which I am not sure how to get around.