Last active
August 9, 2023 06:05
-
-
Save kyouko-taiga/de5ece0792d2f5fe8fb3 to your computer and use it in GitHub Desktop.
A python class wrapper to instrument/override properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This snippet was written by Dimitri Racordon ([email protected]) | |
# | |
# Copyright (c) 2015 Dimitri Racordon | |
# Licensed under the The MIT License (MIT). | |
class lazy(object): | |
# This class is heavily inspired by the werkzeug.utils.cached_property | |
# decorator. It transforms a class method to a lazy property, evaluated | |
# the first time the property is accessed. | |
_missing = object | |
def __init__(self, func, name=None): | |
self.__name__ = name or func.__name__ | |
self.__module__ = func.__module__ | |
self.__doc__ = func.__doc__ | |
self.func = func | |
def __get__(self, instance, type=None): | |
if instance is None: | |
return self | |
value = instance.__dict__.get(self.__name__, self._missing) | |
if value is self._missing: | |
value = self.func(instance) | |
instance.__dict__[self.__name__] = value | |
return value | |
class WrapperBase(type): | |
# This metaclass is heavily inspired by the Object Proxying python recipe | |
# (http://code.activestate.com/recipes/496741/). It adds special methods | |
# to the wrapper class so it can proxy the wrapped class. In addition, it | |
# adds a field __overrides__ in the wrapper class dictionary, containing | |
# all attributes decorated to be overriden. | |
_special_names = [ | |
'__abs__', '__add__', '__and__', '__call__', '__cmp__', '__coerce__', | |
'__contains__', '__delitem__', '__delslice__', '__div__', '__divmod__', | |
'__eq__', '__float__', '__floordiv__', '__ge__', '__getitem__', | |
'__getslice__', '__gt__', '__hash__', '__hex__', '__iadd__', '__iand__', | |
'__idiv__', '__idivmod__', '__ifloordiv__', '__ilshift__', '__imod__', | |
'__imul__', '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__', | |
'__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__', | |
'__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', | |
'__neg__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', | |
'__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', | |
'__repr__', '__reversed__', '__rfloorfiv__', '__rlshift__', '__rmod__', | |
'__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', | |
'__rtruediv__', '__rxor__', '__setitem__', '__setslice__', '__sub__', | |
'__truediv__', '__xor__', 'next', | |
] | |
def __new__(cls, classname, bases, attrs): | |
def make_method(name): | |
def method(self, *args, **kwargs): | |
mtd = getattr(object.__getattribute__(self, "_wrapped"), name) | |
return mtd(*args, **kwargs) | |
return method | |
for name in cls._special_names: | |
attrs[name] = make_method(name) | |
overrides = attrs.get('__overrides__', []) | |
overrides.extend(k for k,v in attrs.items() if isinstance(v, lazy)) | |
attrs['__overrides__'] = overrides | |
return type.__new__(cls, classname, bases, attrs) | |
class Wrapper(metaclass=WrapperBase): | |
# This class acts as a proxy for the wrapped instance it is passed. All | |
# access to its attributes are delegated to the wrapped class, except | |
# those contained in __overrides__. | |
__slots__ = ['_wrapped', '__weakref__'] | |
def __init__(self, wrapped): | |
object.__setattr__(self, '_wrapped', wrapped) | |
def __getattribute__(self, attr): | |
if attr in object.__getattribute__(self, '__overrides__'): | |
return object.__getattribute__(self, attr) | |
# If the requested attribute wasn't overriden, then we delegate to | |
# the wrapped class. | |
return getattr(object.__getattribute__(self, '_wrapped'), attr) | |
def __setattr__(self, attr, value): | |
setattr(object.__getattribute__(self, '_wrapped'), attr, value) | |
def __nonzero__(self): | |
return bool(object.__getattribute__(self, '_wrapped')) | |
def __str__(self): | |
return str(object.__getattribute__(self, '_wrapped')) | |
def __repr__(self): | |
return repr(object.__getattribute__(self, '_wrapped')) | |
# ============================================================================= | |
class MyClass(object): | |
def __init__(self, foo=None, bar=None): | |
self.setattr_unless_none('foo', foo) | |
self.setattr_unless_none('bar', bar) | |
def setattr_unless_none(self, name, value): | |
if value is not None: | |
setattr(self, name, value) | |
def __lt__(self, rhs): | |
return id(self) < id(rhs) | |
class MyWrapper(Wrapper): | |
def __init__(self, *args, **kwargs): | |
super().__init__(MyClass(*args, **kwargs)) | |
@lazy | |
def foo(self): | |
return 'lazy evaluated' | |
a = MyWrapper() | |
b = MyWrapper(foo='defined in kwargs') | |
print(a.foo) # prints "lazy evaluated" | |
print(b.foo) # prints "defined in kwargs" | |
print(a < b) # prints id(a) < id(b) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment