Source code for sigmaepsilon.core.wrapping
# -*- coding: utf-8 -*-
from typing import Any
__all__ = ["Wrapper", "wrapper", "customwrapper", "wrap"]
NoneType = type(None)
[docs]class Wrapper:
"""
Wrapper base class that makes it easy (and safe) to extend other objects.
Based on the provided arguments at initialization, the wrapper either
(a) wraps an existing object at object creation provided as a keyword
argument with `Wrapper.wrapkey`
(b) wraps an existing object at object creation if it is a positional
argument and an instance of `Wrapper.wraptype`
(b) wraps the object Wrapper.wraptype(*args, **kwargs) if
`Wrapper.wraptype` is not `None`
The attributes and methods of the wrapped instance are all accessible
through the wrapper.
Using a wrapper is a good idea if you want to easily extend the functionality
provided by a class of an external library without having to worry about shadowing
an important method and thus risking to break the behaviour of the wrapped object.
Examples
--------
>>> import numpy as np
>>>
>>> arr = np.eye(3)
>>> wrapper = Wrapper(wrap=arr)
Now the wrapped NumPy array is accessible as `wrapper.wrapped`.
A wrapper class can be used to extend the behaviour:
>>> MyWrapper(Wrapper):
>>> wraptype = np.ndarray
>>>
>>> def invert() -> None:
>>> self.wrapped = np.linalg.inv(self.wrapped)
With this solution, you don't have to worry about shadowing an existing
implementation of NumPy arrays. Not that NumPy arrays might not have a method
called `invert` at the time of implementing the class `MyWrapper`, but it might
change in the future. You can even check that internally and get notified:
>>> import warnings
>>>
>>> MyWrapper(Wrapper):
>>> wraptype = np.ndarray
>>>
>>> def invert() -> None:
>>> if hasattr(self.wrapped, "invert"):
>>> warnings.warn("'invert' already exists in the object")
>>> self.wrapped = np.linalg.inv(self.wrapped)
Then, to wrap a NumPy array, you can do this (since the `wraptype` attribute is set,
the MyWrapper class is going to catch the object to wrap as a positional argument):
>>> wrapper = MyWrapper(arr)
"""
wrapkey: str = "wrap"
wraptype: Any = NoneType
def __init__(self, *args, **kwargs):
super().__init__()
self._wrapped = None
self.wrap(kwargs.get(self.wrapkey, None))
if self._wrapped is None and self.wraptype is not NoneType:
for arg in args:
if isinstance(arg, self.wraptype):
self._wrapped = arg
break
if self._wrapped is None:
try:
if self.wraptype is not NoneType:
self._wrapped = self.wraptype(*args, **kwargs)
except Exception:
raise ValueError(
"Wrapped class '{}' cannot be "
+ "initiated with these "
+ "arguments".format(self.wraptype.__name__)
)
else:
if self.wraptype is not NoneType:
assert isinstance(
self._wrapped, self.wraptype
), "Wrong type, unable to wrap object : {}".format(self._wrapped)
@property
def wrapped(self):
"""Retruns the wrapped object."""
return self._wrapped
[docs] def wrap(self, obj: Any=None) -> Any:
"""Wraps the provided object and returns the wrapper instance."""
if self.wraptype is not NoneType:
if isinstance(obj, self.wraptype):
self._wrapped = obj
else:
self._wrapped = obj
return self
[docs] def wraps(self):
"""Returns `True` if the instance wraps something or `False` if it doesn't."""
return self._wrapped is not None
def wrapped_obj(self):
return self._wrapped
def __hasattr__(self, attr):
return any([attr in self.__dict__, attr in self._wrapped.__dict__])
def __getattr__(self, attr):
if attr in self.__dict__:
return getattr(self, attr)
try:
return getattr(self._wrapped, attr)
except Exception:
raise AttributeError(
"'{}' object has no attribute \
called {}".format(
self.__class__.__name__, attr
)
)
def __getitem__(self, index):
try:
return super().__getitem__(index)
except Exception:
try:
return self._wrapped.__getitem__(index)
except Exception:
raise TypeError(
"'{}' object is not "
"subscriptable".format(self.__class__.__name__)
)
def __setitem__(self, index, value):
try:
return super().__setitem__(index, value)
except Exception:
try:
return self._wrapped.__setitem__(index, value)
except Exception:
raise TypeError(
"'{}' object does not support "
"item assignment".format(self.__class__.__name__)
)
[docs]def customwrapper(*, wrapkey:str="wrap", wraptype: Any=NoneType) -> Wrapper:
"""
A factory function that returns a class decorator turning a class type
into a wrapper type, that either
(a) wraps an existing object at object creation provided as a keyword
argument with wrapkey
(b) wraps an existing object at object creation if it is a positional
argument and an instance of wraptype
(b) wraps the object wraptype(*args, **kwargs)
See also
--------
:class:`~sigmaepsilon.core.wrapping.Wrapper`
Examples
--------
Take this example from the :class:`~sigmaepsilon.core.wrapping.Wrapper` class:
>>> MyWrapper(Wrapper):
>>> wraptype = np.ndarray
>>>
>>> def invert() -> None:
>>> self.wrapped = np.linalg.inv(self.wrapped)
An equivalent implementation of this is the following:
>>> @customwrapper(wraptype=np.ndarray)
>>> MyWrapper:
>>> def invert() -> None:
>>> self.wrapped = np.linalg.inv(self.wrapped)
Notice how the class `MyWrapper` is not inherited from the `Wrapper` class.
"""
class BaseWrapperType(Wrapper):
...
BaseWrapperType.wrapkey = wrapkey
BaseWrapperType.wraptype = wraptype
def wrapper(BaseType):
class WrapperType(BaseWrapperType, BaseType):
basetype = BaseType
return WrapperType
return wrapper
[docs]def wrapper(BaseType: Any) -> Wrapper:
"""
Simple class decorator that turns a type into a wrapper with default
behaviour.
Notes
-----
This is the same as using the :func:`~sigmaepsilon.core.wrapping.customwrapper`
with the default values.
See also
--------
:class:`~sigmaepsilon.core.wrapping.Wrapper`
:func:`~sigmaepsilon.core.wrapping.customwrapper`
Example
-------
>>> @wrapper
>>> MyWrapper:
>>> def invert() -> None:
>>> self.wrapped = np.linalg.inv(self.wrapped)
"""
class WrapperType(Wrapper, BaseType):
basetype = BaseType
return WrapperType
[docs]def wrap(obj: object) -> Wrapper:
"""
Wraps an object and returns the wrapper.
See also
--------
:class:`~sigmaepsilon.core.wrapping.Wrapper`
Example
-------
>>> import numpy as np
>>> wrapper = wrap(np.eye(3))
"""
return Wrapper(wrap=obj)