Source code for schrodinger.application.msv.utils
import functools
import inspect
import schrodinger
from schrodinger.Qt import QtCore
DEBUG_MODE = schrodinger.in_dev_env()
[docs]def get_rolling_average(data, window_padding=4):
    """
    Returns the running average of data in a window
    :param data: Data to average.  Must not contain any None values.
    :type data: list
    :param window_padding: The number of elements on either side of a given
        element to include in the average.  Must be greater than 0.
    :type window_padding: int
    :return: A list of rolling averages.  This list does not contain any values
        for the first or last `window_padding` elements of `data`, as those
        positions do not have a fully populated window.  (Note that this list
        does not contain `None`s in those positions.  It is simply
        2 * window_padding elements shorter than `data`.)
    :rtype: list
    """
    window_size = window_padding * 2 + 1
    # Formatting this as a list comprehension gives us a slight speed boost over
    # the equivalent for loop
    return [
        sum(data[i - window_padding:i + window_padding + 1]) / window_size
        for i in range(window_padding,
                       len(data) - window_padding)
    ]
[docs]def const(method):
    """
    A decorator that adds a 'const' attribute to a method.  This decorator is
    used in conjunction with `WrapperMetaClass` below and must only be applied
    to methods in a wrapped class that do not modify the state of the class
    (similar to the `const` method declaration in C++).
    """
    method.const = True
    return method
[docs]class DocstringWrapperMetaClass(type):
    """
    A metaclass for a class that wraps another class.  This metaclass copies
    docstrings from wrapped methods to reimplemented wrapper methods of the same
    name.
    """
    def __new__(cls, clsname, bases, dct, *, wraps=None):
        """
        See Python documentation for positional argument documentation.
        :param wraps: The class to wrap.  If None, no wrapping will be done.
            (This allows for subclassing of the wrapper class.)
        :type wraps: type or None
        """
        if wraps is not None:
            cls._copyDocstrings(dct, wraps)
        return super().__new__(cls, clsname, bases, dct)
    @classmethod
    def _copyDocstrings(cls, dct, wrapped_class):
        """
        Copy all docstrings from the wrapped class to the wrapper class.
        :param dct: The class dictionary of the wrapper class.  The contents of
            this dictionary will be updated by this method.
        :type dct: dict
        :param wrapped_class: The class being wrapped.
        :type wrapped_class: type
        """
        for name, wrapper_func in dct.items():
            wrapped_func = getattr(wrapped_class, name, None)
            if (inspect.isfunction(wrapped_func) and
                    inspect.isfunction(wrapper_func) and
                    wrapped_func.__doc__ is not None and
                    wrapper_func.__doc__ is None):
                wrapper_func.__doc__ = wrapped_func.__doc__
[docs]class QtDocstringWrapperMetaClass(DocstringWrapperMetaClass,
                                  type(QtCore.QObject)):
    """
    A metaclass for QObject subclasses that wraps another class.  This metaclass
    only copies docstrings from wrapped methods to reimplemented wrapper methods
    of the same name.
    """
[docs]class WrapperMetaClass(QtDocstringWrapperMetaClass):
    """
    This metaclass makes it easy to create a class that wraps another one. In
    particular, this is intended to help in creating an undoable version of the
    wrapped class.  Methods that don't modify the state of the wrapped class
    (which must be indicated using the `@const` decorator) may be called
    directly on the wrapper.  Custom undoable wrappings will need to be written
    for any methods that do modify the state of the wrapped class.
    See the tests for this class for a simple example.
    In general, if class A wraps class B, then:
    * Every static method, class method, public class attribute, and property
      on B is accessible from A, unless A already has attributes with the same
      name defined
    * Every method on B decorated by @const will be available from A, unless A
      already has a method with the same name defined. Note that @const methods
      from B will take precedence over methods that A inherits. (This ensures
      that common magic methods like `__str__` and `__repr__` will get wrapped
      properly.)
    * Every instance attribute on B named in instance_attrs will be available
      from A.
    This may sound like a clumsy reinvention of classical inheritance. But
    it can be useful to use a wrapper when we want the wrapped class to be
    able to call any of its own methods without any danger of the wrapper
    methods being called, while at the same time ensuring that we can
    conveniently provide the same interface from the wrapped class. For
    example, we have a ProteinAlignment class that encapsulates operations
    on structured collections of protein sequences. We use this metaclass
    to create an undoable ProteinAlignment that presents an identical
    interface but that can safely perform redo and undo operations by
    manipulating the protein alignment it wraps.
    """
    def __new__(cls,
                clsname,
                bases,
                dct,
                *,
                wraps=None,
                wrapped_name=None,
                instance_attrs=None):
        """
        See Python documentation for positional argument documentation.
        :param wraps: The class to wrap.  If None, no wrapping will be done.
            (This allows for subclassing of the wrapper class.)
        :type wraps: type or None
        :param wrapped_name: The name of the wrapped component on the
            wrapping instance
        :type wrapped_name: str
        :param instance_attrs: The names of instance attributes to be wrapped
        :type instance_attrs: tuple of str
        :return: A MetaClass suitable for creating wrapper objects
        :rtype: type
        """
        if wraps is not None:
            cls._wrapClassAttrs(bases, dct, wraps, wrapped_name)
            cls._wrapInstanceAttrs(dct, wrapped_name, instance_attrs)
        return super().__new__(cls, clsname, bases, dct, wraps=wraps)
    @classmethod
    def _wrapClassAttrs(cls, bases, dct, wrapped_class, wrapped_name):
        """
        Wrap all appropriate class attributes.
        :param bases: The base classes of the wrapper class.
        :type bases: tuple(type)
        :param dct: The class dictionary of the wrapper class.  The contents of
            this dictionary will be updated by this method.
        :type dct: dict
        :param wrapped_class: The class to wrap.
        :type wrapped_class: type
        :param wrapped_name: The name of the wrapped component on the
            wrapping instance.
        :type wrapped_name: str
        """
        for attr in inspect.classify_class_attrs(wrapped_class):
            name = attr.name
            if name in dct:
                # never override anything defined in the wrapper class
                pass
            elif (not hasattr(attr.object, 'const') and
                  any(hasattr(base, name) for base in bases)):
                # never override anything that the wrapper class has inherited
                # from a superclass unless the wrapped class has a const method
                # with the same name
                pass
            elif attr.kind == 'data' and not name.startswith("_"):
                dct[name] = attr.object
            elif attr.kind == 'class method':
                dct[name] = cls.makeClassMethodDelegate(name, wrapped_class)
            elif attr.kind == 'method' and hasattr(attr.object, 'const'):
                dct[name] = cls.makeMethodDelegate(name, wrapped_class,
                                                   wrapped_name)
            elif attr.kind == 'static method':
                dct[name] = attr.object
            elif attr.kind == 'property':
                getter = attr.object.fget is not None
                setter = attr.object.fset is not None
                dct[name] = cls.makeProperty(name, getter, setter, wrapped_name)
    @classmethod
    def _wrapInstanceAttrs(cls, dct, wrapped_name, instance_attrs):
        """
        Wrap all specified instance attributes.
        :param dct: The class dictionary of the wrapper class.  The contents of
            this dictionary will be updated by this method.
        :type dct: dict
        :param wrapped_name: The name of the wrapped component on the
            wrapping instance.
        :type wrapped_name: str
        :param instance_attrs: The names of instance attributes to be wrapped.
        :type instance_attrs: tuple of str
        """
        for attr in instance_attrs:
            dct[attr] = cls.makeProperty(attr, True, True, wrapped_name)
[docs]    @classmethod
    def makeMethodDelegate(cls, meth_name, wrapped_class, wrapped_name):
        """
        Returns a method that delegates method calls to the wrapped instance
        :param meth_name: The name of the method to wrap
        :type meth_name: str
        """
        @functools.wraps(getattr(wrapped_class, meth_name))
        def const_meth(self, *args, **kwargs):
            wrapped_obj = getattr(self, wrapped_name)
            aln_method = getattr(wrapped_obj, meth_name)
            return aln_method(*args, **kwargs)
        return const_meth
[docs]    @classmethod
    def makeClassMethodDelegate(cls, meth_name, wrapped_class):
        """
        Returns a class method that delegates class method calls to the wrapped
        class
        :param meth_name: The name of the method to wrap
        :type meth_name: str
        """
        @functools.wraps(getattr(wrapped_class, meth_name))
        def const_meth(cls, *args, **kwargs):
            aln_method = getattr(wrapped_class, meth_name)
            return aln_method.__func__(cls, *args, **kwargs)
        return classmethod(const_meth)
[docs]    @classmethod
    def makeProperty(cls, prop_name, getter, setter, wrapped_name):
        """
        Returns a property that delegates property access to the wrapped
        instance
        :param prop_name: The name of the property
        :type prop_name: str
        :param getter: Whether to add a getter
        :type getter: bool
        :param setter: Whether to add a setter
        :type setter: bool
        :return: A property which delegates to the wrapped object
        :rtype: property
        """
        def fget(self):
            wrapped_obj = getattr(self, wrapped_name)
            return getattr(wrapped_obj, prop_name)
        def fset(self, value):
            wrapped_obj = getattr(self, wrapped_name)
            setattr(wrapped_obj, prop_name, value)
        fset = fset if setter else None
        fget = fget if getter else None
        return property(fget, fset)