# -*- coding: utf-8 -*-
"""
Decorators and objects used for overloading attributes and interfacing them.
.. Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
.. distribution and on <https://github.com/Ulm-IQO/qudi-core/>
..
.. This file is part of qudi.
..
.. Qudi is free software: you can redistribute it and/or modify it under the terms of
.. the GNU Lesser General Public License as published by the Free Software Foundation,
.. either version 3 of the License, or (at your option) any later version.
..
.. Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
.. without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
.. See the GNU Lesser General Public License for more details.
..
.. You should have received a copy of the GNU Lesser General Public License along with qudi.
.. If not, see <https://www.gnu.org/licenses/>.
"""
__all__ = ['OverloadedAttribute', 'OverloadProxy']
import weakref
from typing import Any, Callable
class _OverloadedAttributeMapper:
def __init__(self):
self._map_dict = dict()
self._parent = lambda: None
def add_mapping(self, key: str, obj: Any) -> None:
self._map_dict[key] = obj
def remove_mapping(self, key: str) -> None:
del self._map_dict[key]
@property
def parent(self) -> Any:
return self._parent()
@parent.setter
def parent(self, obj: Any) -> None:
self._parent = weakref.ref(obj)
def get_mapped(self, key: str) -> Any:
if key not in self._map_dict:
raise KeyError(f'No attribute overload found for key "{key}"')
return self._map_dict[key]
def __getitem__(self, key):
mapped_obj = self.get_mapped(key)
if hasattr(mapped_obj, '__get__'):
return mapped_obj.__get__(self.parent)
else:
return mapped_obj
def __setitem__(self, key, value):
mapped_obj = self.get_mapped(key)
if hasattr(mapped_obj, '__set__'):
mapped_obj.__set__(self.parent, value)
else:
self._map_dict[key] = value
def __delitem__(self, key):
mapped_obj = self.get_mapped(key)
if hasattr(mapped_obj, '__delete__'):
mapped_obj.__delete__(self.parent)
else:
del self._map_dict[key]
[docs]
class OverloadedAttribute:
[docs]
def __init__(self):
self._attr_mapper = _OverloadedAttributeMapper()
[docs]
def overload(self, key: str) -> Callable[[Any], Any]:
def decorator(attr):
self._attr_mapper.add_mapping(key, attr)
return self
return decorator
def __get__(self, instance, owner=None):
if instance is None:
return self
self._attr_mapper.parent = instance
return self._attr_mapper
def __set__(self, instance, value):
raise AttributeError("can't set attribute")
def __delete__(self, instance):
raise AttributeError("can't delete attribute")
[docs]
def setter(self, key: str) -> Callable[[Any], Any]:
obj = self._attr_mapper.get_mapped(key)
def decorator(attr):
self._attr_mapper.add_mapping(key, obj.setter(attr))
return self
return decorator
[docs]
def getter(self, key: str) -> Callable[[Any], Any]:
obj = self._attr_mapper.get_mapped(key)
def decorator(attr):
self._attr_mapper.add_mapping(key, obj.getter(attr))
return self
return decorator
[docs]
def deleter(self, key: str) -> Callable[[Any], Any]:
obj = self._attr_mapper.get_mapped(key)
def decorator(attr):
self._attr_mapper.add_mapping(key, obj.deleter(attr))
return self
return decorator
[docs]
class OverloadProxy:
"""Instances of this class serve as proxies for objects containing attributes of type
OverloadedAttribute. It can be used to hide the overloading mechanism by fixing the overloaded
attribute access key in a OverloadProxy instance. This allows for interfacing an overloaded
attribute in the object represented by this proxy by normal "pythonic" means without the
additional key-mapping lookup usually required by OverloadedAttribute.
Heavily inspired by this python recipe under PSF License:
https://code.activestate.com/recipes/496741-object-proxying/
"""
__slots__ = ['_obj_ref', '_overload_key', '__weakref__']
[docs]
def __init__(self, obj: Any, overload_key: str):
object.__setattr__(self, '_obj_ref', weakref.ref(obj))
object.__setattr__(self, '_overload_key', overload_key)
# proxying (special cases)
def __getattribute__(self, name):
obj = object.__getattribute__(self, '_obj_ref')()
attr = getattr(obj, name)
if isinstance(attr, _OverloadedAttributeMapper):
return attr[object.__getattribute__(self, '_overload_key')]
return attr
def __delattr__(self, name):
obj = object.__getattribute__(self, '_obj_ref')()
attr = getattr(obj, name)
if isinstance(attr, _OverloadedAttributeMapper):
del attr[object.__getattribute__(self, '_overload_key')]
else:
delattr(obj, name)
def __setattr__(self, name, value):
obj = object.__getattribute__(self, '_obj_ref')()
attr = getattr(obj, name)
if isinstance(attr, _OverloadedAttributeMapper):
attr[object.__getattribute__(self, '_overload_key')] = value
else:
setattr(obj, name, value)
def __nonzero__(self):
return bool(object.__getattribute__(self, '_obj_ref')())
def __str__(self):
return str(object.__getattribute__(self, '_obj_ref')())
def __repr__(self):
return repr(object.__getattribute__(self, '_obj_ref')())
# factories
_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',
)
@classmethod
def _create_class_proxy(cls, theclass):
"""creates a proxy for the given class"""
def make_method(name):
def method(self, *args, **kw):
return getattr(object.__getattribute__(self, '_obj_ref')(), name)(
*args, **kw
)
return method
namespace = {}
for name in cls._special_names:
if hasattr(theclass, name) and not hasattr(cls, name):
namespace[name] = make_method(name)
return type(f'{cls.__name__}({theclass.__name__})', (cls,), namespace)
def __new__(cls, obj, overload_key, *args, **kwargs):
"""creates an proxy instance referencing `obj`. (obj, *args, **kwargs) are passed to this
class' __init__, so deriving classes can define an __init__ method of their own.
note: _class_proxy_cache is unique per class (each deriving class must hold its own cache)
"""
try:
cache = cls.__dict__['_class_proxy_cache']
except KeyError:
cls._class_proxy_cache = cache = {}
try:
theclass = cache[obj.__class__]
except KeyError:
cache[obj.__class__] = theclass = cls._create_class_proxy(obj.__class__)
ins = object.__new__(theclass)
return ins