Source code for schrodinger.infra.util

"""
General infrastructure level utilities.

Copyright Schrodinger, LLC. All rights reserved.

"""

import contextlib as _contextlib
import inspect as _inspect
import types as _types
import weakref as _weakref

import decorator as _decorator


[docs]class CreateWhenNeeded: """ This class can be used like property() (i.e., it is a descriptor; see section 3.3.2.2 in the python reference). It will hold off on creating the object until it is needed, but once it has been created it will return the object directly. It's best used for attributes that are expensive to calculate (as measured by profiling, of course) and not always used. """ # Currently, the descriptor section of the reference is at # http://docs.python.org/ref/descriptors.html # For more details on descriptors, see Raymond Hettinger's "How-To Guide # for Descriptors" # http://users.rcn.com/python/download/Descriptor.htm
[docs] def __init__(self, function, name=None, doc=None): """ Create the descriptor. """ # Use the name of the function unless an override is provided. if name is None: name = function.__name__ self.function = function self.name = name self.__doc__ = doc
def __get__(self, obj, cls): """ Calculate the value from the function, then set the attribute of the object to this result and return it. """ # If not bound to an instance, return the underlying descriptor. if obj is None: return self # Get the result... result = self.function(obj) # ...and overwrite this descriptor with the result for future access. setattr(obj, self.name, result) return result
[docs]class OneIndexedList(list): """ A list that starts at one instead of zero """ # For documentation on all __methods__, see the Python documentation def __getitem__(self, index): converted_index = self._convertIndex(index) return super().__getitem__(converted_index) def __setitem__(self, index, value): converted_index = self._convertIndex(index) super().__setitem__(converted_index, value) def __delitem__(self, index): converted_index = self._convertIndex(index) super().__delitem__(converted_index) # Note that the __*slice__ methods have been deprecated since Python 2.0 in # favor of the __*item__ methods, but they are still used in the CPython # implementation of list, so we have to override them. We simply redirect # things to the appropriate __*item__ method. def __getslice__(self, start, stop): return self.__getitem__(slice(start, stop)) def __setslice__(self, start, stop, value): self.__setitem__(slice(start, stop), value) def __delslice__(self, start, stop): self.__delitem__(slice(start, stop))
[docs] def index(self, x): index = super().index(x) return index + 1
[docs] def insert(self, i, x): i = self._convertScalarIndex(i) super().insert(i, x)
[docs] def pop(self, i): i = self._convertScalarIndex(i) return super().pop(i)
def _convertIndex(self, index): """ Convert a list index or slice from 0-indexed to 1-indexed :param index: The index to convert :type index: int or slice :return: The converted index :rtype: int or slice :raise ValueError: If the index is for element 0 """ if isinstance(index, slice): start = self._convertScalarIndex(index.start) stop = self._convertScalarIndex(index.stop) return slice(start, stop, index.step) elif index == 0: err = "Cannot access index zero of a OneIndexedList" raise ValueError(err) else: return self._convertScalarIndex(index) def _convertScalarIndex(self, index): """ Convert a list index from 0-indexed to 1-indexed :param index: The index to convert :type index: int :return: The converted index :rtype: int """ if index is None or index <= 0: return index else: return index - 1 def __add__(self, other): result = super().__add__(other) return self.__class__(result) def __mul__(self, other): result = super().__mul__(other) return self.__class__(result) def __iadd__(self, other): result = super().__iadd__(other) return self.__class__(result) def __imul__(self, other): result = super().__imul__(other) return self.__class__(result)
[docs] def copy(self): return self.__class__(self)
[docs]def flag_context_manager(attr_name, set_to=True): """ Create a context manager that sets the specified class attribute to the given value and then restores the original value of the attribute when leaving the context. Example: class Foo(object): _includingStructure = util.flag_context_manager("_including_structure") def __init__(self): self._including_structure = False def includeStructure(proj_row): with self._includingStructure(): proj_row.in_workspace = project.IN_WORKSPACE :see: `skip_if` :param attr_name: The attribute to set to `set_to` :type attr_name: str :param set_to: The value to set `attr_name` to :type set_to: object """ @_contextlib.contextmanager def flag_context_manager_inner(self): # storing the original value allows the context managers to nest orig_value = getattr(self, attr_name) setattr(self, attr_name, set_to) try: yield finally: setattr(self, attr_name, orig_value) return flag_context_manager_inner
[docs]def skip_if(attr_name, if_set_to=True): """ Create a decorator that converts the decorated method to a no-op if class attribute `attr_name` equals `is_set_to`. Example:: skip_if_including_structure = util.skip_if("_including_structure") class Foo(object): _includingStructure = util.flag_context_manager("_including_structure") def __init__(self): self._including_structure = False def includeStructure(proj_row): with self._includingStructure(): proj_row.in_workspace = project.IN_WORKSPACE @skip_if_including_structure def skipped_method(self): print ("This method would have been skipped if we were the process " "of including a structure") :see: `flag_context_manager` :param attr_name: The attribute name to check :type attr_name: str :param if_set_to: The decorated method will be skipped if `attr_name` equals this value. :type if_set_to: object """ @_decorator.decorator def dec(func, *args, **kwargs): self = args[0] if getattr(self, attr_name) != if_set_to: return func(*args, **kwargs) return dec
[docs]def enum_speedup(enum_cls): """ Add all enum members to a new class to speed up access. Attribute access in enum classes under Python 2.7 was *incredibly* slow (see https://bugs.python.org/issue23486). Previously, it accounted for roughly 7.5% of the runtime of scrolling in the HPT. Use of this function reduced that time to nearly zero. Enums were significantly sped up in Python 3.5, but attribute access is still measurably slower than in regular classes. :param enum_cls: The enum class to wrap. :type enum_cls: enum.Enum :return: A new class that allows for faster access of the enum members. :rtype: object :note: Declaring a Qt signal with an argument type of a sped-up enum class will lead to a TypeError when you try to emit an enum member (since Qt doesn't pay attention to `__instancecheck__`). Use `object` in the signal declaration instead. In other words, use :: mySignal = QtCore.pyqtSignal(object) instead of :: mySignal = QtCore.pyqtSignal(MySpedUpEnum) """ from schrodinger.models import json class meta(type): def __call__(self, *args, **kwargs): return enum_cls(*args, **kwargs) def __getitem__(self, key): return enum_cls[key] def __instancecheck__(self, instance): return isinstance(instance, enum_cls) def __contains__(self, item): return item in enum_cls def __iter__(self): return enum_cls.__iter__() def __len__(self): return len(enum_cls) def __getattr__(self, attr): return getattr(enum_cls, attr) def __repr__(self): return repr(enum_cls) cls_dict = {val.name: val for val in enum_cls} if issubclass(enum_cls, json.JsonableClassMixin): bases = (json.JsonableClassMixin,) cls_dict["toJsonImplementation"] = enum_cls.toJsonImplementation cls_dict["fromJsonImplementation"] = enum_cls.fromJsonImplementation else: bases = () return meta(enum_cls.__name__, bases, cls_dict)
[docs]class WeakRefAttribute: """ A descriptor for an instance attribute that should be stored as a weakref. Unlike weakref.proxy, this descriptor allows the attribute to be hashed. Note that the weakref is stored on the instance using the same name as the descriptor (which is stored on the class). Since this descriptor implements __set__, it will always take precedence over the value stored on the instance. """ def __set_name__(self, owner, name): self.refname = name def __get__(self, obj, objtype): ref = obj.__dict__.get(self.refname, None) if ref is None: return None else: return ref() def __set__(self, obj, val): if val is None: obj.__dict__.pop(self.refname, None) else: obj.__dict__[self.refname] = _weakref.ref(val)
[docs]def find_decorated_methods(cls_or_instance, method_attr_name): """ Find all class methods where the method object contains the specified attribute. This is normally used to find methods that have been decorated with a particular decorator. Note that this function will *not* cause properties to be executed so as to avoid any potential performance issues. :param cls_or_instance: A class or instance if a class to find methods on :type cls_or_instance: type or object :param method_attr_name: The attribute name to search for :type method_attr_name: str :return: A list of methods with the given attribute. The list is ordered the same as `dir`, i.e. alphabetically. This list will contain bound methods if `cls_or_instance` is an instance, and will contain functions (i.e. unbound methods) if `cls_or_instance` is a class. :rtype: list[FunctionType] or list[MethodType] Example usage:: def my_decorator(func): func.is_special = True # This can be any value return func class Foo: @my_decorator def bar(self): pass find_decorated_methods(Foo, "is_special") # returns [Foo.bar] foo = Foo() find_decorated_methods(foo, "is_special") # returns [foo.bar] """ decorated_methods = list() for attr_name in dir(cls_or_instance): static_attr = _inspect.getattr_static(cls_or_instance, attr_name) if not isinstance(static_attr, (classmethod, staticmethod, _types.FunctionType)): continue # For classes, getattr_static will return classmethod or staticmethod # wrapper objects instead of functions. For instances, it returns # functions instead of bound methods. To fix that, we use the normal # getattr now that we know the attribute isn't a property. method = getattr(cls_or_instance, attr_name) if hasattr(method, method_attr_name): decorated_methods.append(method) return decorated_methods