Skip to content

Call backend toolkits through decorators. #530

Open
@jthorton

Description

@jthorton

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions