""" Based on the implementation here by Phillip J. Eby: https://pypi.python.org/pypi/ProxyTypes """ from functools import wraps import operator import sys PY3 = sys.version_info[0] >= 3 OPERATORS = [ # Unary "pos", "neg", "abs", "invert", # Comparison "eq", "ne", "lt", "gt", "le", "ge", # Container "getitem", "setitem", "delitem", "contains", # In-place operators "iadd", "isub", "imul", "ifloordiv", "itruediv", "imod", "ipow", "ilshift", "irshift", "iand", "ior", "ixor", ] REFLECTED_OPERATORS = [ "add", "sub", "mul", "floordiv", "truediv", "mod", "pow", "and", "or", "xor", "lshift", "rshift", ] # These functions all have magic methods named after them MAGIC_FUNCS = [ divmod, round, repr, str, hash, len, abs, complex, bool, int, float, iter, ] if PY3: MAGIC_FUNCS += [bytes] iteritems = operator.methodcaller("items") else: OPERATORS += ["getslice", "setslice", "delslice", "idiv"] REFLECTED_OPERATORS += ["div"] MAGIC_FUNCS += [long, unicode, cmp, coerce, oct, hex] iteritems = operator.methodcaller("iteritems") _oga = object.__getattribute__ _osa = object.__setattr__ class ProxyMetaClass(type): def __new__(mcs, name, bases, dct): newcls = super(ProxyMetaClass, mcs).__new__(mcs, name, bases, dct) newcls.__notproxied__ = set(dct.pop("__notproxied__", ())) # Add all the non-proxied attributes from base classes for base in bases: if hasattr(base, "__notproxied__"): newcls.__notproxied__.update(base.__notproxied__) for key, val in iteritems(dct): setattr(newcls, key, val) return newcls def __setattr__(cls, attr, value): # Don't do any magic on the methods of the base Proxy class or the # __new__ static method if cls.__bases__[0].__name__ == "_ProxyBase" or attr == "__new__": pass elif callable(value): if getattr(value, "__notproxied__", False): cls.__notproxied__.add(attr) # Don't wrap staticmethods or classmethods if not isinstance(value, (staticmethod, classmethod)): value = cls._no_proxy(value) elif isinstance(value, property): if getattr(value.fget, "__notproxied__", False): cls.__notproxied__.add(attr) # Remake properties, with the underlying functions wrapped fset = cls._no_proxy(value.fset) if value.fset else value.fset fdel = cls._no_proxy(value.fdel) if value.fdel else value.fdel value = property(cls._no_proxy(value.fget), fset, fdel) super(ProxyMetaClass, cls).__setattr__(attr, value) @staticmethod def _no_proxy(method): """ Returns a wrapped version of `method`, such that proxying is turned off during the method call. """ @wraps(method) def wrapper(self, *args, **kwargs): notproxied = _oga(self, "__notproxied__") _osa(self, "__notproxied__", True) try: return method(self, *args, **kwargs) finally: _osa(self, "__notproxied__", notproxied) return wrapper # Since python 2 and 3 metaclass syntax aren't compatible, create an instance # of our metaclass which our Proxy class can inherit from _ProxyBase = ProxyMetaClass("_ProxyBase", (), {}) class Proxy(_ProxyBase): """ Proxy for any python object. Base class for other proxies. :attr:`__subject__` is the only non-proxied attribute, and contains the proxied object """ __notproxied__ = ("__subject__",) def __init__(self, subject): self.__subject__ = subject @staticmethod def _should_proxy(self, attr): """ Determines whether `attr` should be looked up on the proxied object, or the proxy itself. """ if attr in type(self).__notproxied__: return False if _oga(self, "__notproxied__") is True: return False return True def __getattribute__(self, attr): if Proxy._should_proxy(self, attr): return getattr(self.__subject__, attr) return _oga(self, attr) def __setattr__(self, attr, val): if Proxy._should_proxy(self, attr): setattr(self.__subject__, attr, val) _osa(self, attr, val) def __delattr__(self, attr): if Proxy._should_proxy(self, attr): delattr(self.__subject__, attr) object.__delattr__(self, attr) def __call__(self, *args, **kw): return self.__subject__(*args, **kw) @classmethod def add_proxy_meth(cls, name, func, arg_pos=0): """ Add a method `name` to the class, which returns the value of `func`, called with the proxied value inserted at `arg_pos` """ @wraps(func) def proxied(self, *args, **kwargs): args = list(args) args.insert(arg_pos, self.__subject__) result = func(*args, **kwargs) return result setattr(cls, name, proxied) for func in MAGIC_FUNCS: Proxy.add_proxy_meth("__%s__" % func.__name__, func) for op in OPERATORS + REFLECTED_OPERATORS: magic_meth = "__%s__" % op Proxy.add_proxy_meth(magic_meth, getattr(operator, magic_meth)) # Reflected operators for op in REFLECTED_OPERATORS: Proxy.add_proxy_meth("__r%s__" % op, getattr(operator, "__%s__" % op), arg_pos=1) # One offs # Only non-operator that needs a reflected version Proxy.add_proxy_meth("__rdivmod__", divmod, arg_pos=1) # pypy is missing __index__ in operator module Proxy.add_proxy_meth("__index__", operator.index) # For python 2.6 Proxy.__nonzero__ = Proxy.__bool__ class CallbackProxy(Proxy): """ Proxy for a callback result. Callback is called on each use. """ def __init__(self, callback): self.callback = callback @property def __subject__(self): return self.callback() class LazyProxy(CallbackProxy): """ Proxy for a callback result, that is cached on first use. """ @property def __subject__(self): try: return self.cache except AttributeError: self.cache = super(LazyProxy, self).__subject__ return self.cache @__subject__.setter def __subject__(self, value): self.cache = value def notproxied(func): """ Decorator to add methods to the __notproxied__ list """ func.__notproxied__ = True return func