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)