"""Some generic decorators and a decorator for making better decorators.
Decorators implemented as function closures have important flaws:
- __name__ and __doc__ are wrong
- function argspec is wrong
- function source code cannot be retrieved
- cannot be applied on top of decorators implemented as descriptors
The ``decorator`` decorator in this file solves these problems.
``@decorator`` should be applied to all decorator functions in Nengo.
This makes decorators easy to write, while avoiding the issues above.
----
Some of the code in this file is a modified subset of the wrapt package,
available at https://pypi.org/project/wrapt/.
For more details on the implementations herein, see the code comments
in the wrapt source at https://github.com/GrahamDumpleton/wrapt.
The code is included here under the terms of wrapt's BSD license:
Copyright (c) 2013, Graham Dumpleton. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""
import functools
import inspect
class ObjectProxyMethods:
"""Properly override __module__ and __doc__.
If we add these in ObjectProxy, the derived class __dict__ will
still be setup to have string variants of these attributes and the
rules of descriptors means that they appear to take precedence
over the properties in the base class. To avoid that, we copy the
properties into the derived class type itself via a meta class. In
that way the properties will always take precedence.
"""
@property
def __module__(self): # pylint: disable=function-redefined
return self.__wrapped__.__module__
@property
def __doc__(self):
return self.__wrapped__.__doc__
@property
def __dict__(self):
"""Must be explicit to ensure that vars() works as expected."""
return self.__wrapped__.__dict__
class ObjectProxy(metaclass=ObjectProxyMeta):
"""A transparent object proxy for creating decorator descriptors.
This is used in lieu of ``functools.update_wrapper``, which copies
a number of properties of the wrapped function in the wrapper.
Copying properties can be expensive though, so this is used instead
to make the wrapper act like the wrapped function in all cases
except ``__call__``.
"""
__slots__ = "__wrapped__"
def __init__(self, wrapped):
object.__setattr__(self, "__wrapped__", wrapped)
# Python 3 has the __qualname__ attribute, but it does not
# allow it to be overridden using a property and it must instead
# be an actual string object instead.
try:
object.__setattr__(self, "__qualname__", wrapped.__qualname__)
except AttributeError:
pass
@property
def __annotations__(self):
return self.__wrapped__.__anotations__
@property
def __name__(self):
return self.__wrapped__.__name__
@property
def __class__(self):
return type(self.__wrapped__)
def __dir__(self):
return dir(self.__wrapped__)
def __getattr__(self, key):
return getattr(self.__wrapped__, key)
def __hash__(self):
return hash(self.__wrapped__)
def __setattr__(self, key, value):
setattr(self.__wrapped__, key, value)
def __str__(self):
return str(self.__wrapped__)
def __repr__(self):
return "<%s at 0x%x for %s at 0x%x>" % (
type(self).__name__,
id(self),
type(self.__wrapped__).__name__,
id(self.__wrapped__),
)
class BoundFunctionWrapper(ObjectProxy):
"""A descriptor to emulate a bound function.
This is used to create bound function decorators.
It maintains all of the nice introspection that can normally
be done on bound functions.
"""
__slots__ = ("instance", "wrapper", "binding", "parent")
def __init__(self, wrapped, instance, wrapper, binding, parent):
super().__init__(wrapped)
# Using object.__setattr__ to subvert ObjectProxy.__setattr__
object.__setattr__(self, "instance", instance)
object.__setattr__(self, "wrapper", wrapper)
object.__setattr__(self, "binding", binding)
object.__setattr__(self, "parent", parent)
def __get__(self, instance, owner):
if self.instance is None and self.binding == "function":
descriptor = self.parent.__wrapped__.__get__(instance, owner)
return BoundFunctionWrapper(
descriptor, instance, self.wrapper, self.binding, self.parent
)
return self
def __call__(self, *args, **kwargs):
if self.binding == "function":
if self.instance is None:
instance, args = args[0], args[1:]
wrapped = functools.partial(self.__wrapped__, instance)
return self.wrapper(wrapped, instance, args, kwargs)
else:
return self.wrapper(self.__wrapped__, self.instance, args, kwargs)
else:
instance = getattr(self.__wrapped__, "__self__", None)
return self.wrapper(self.__wrapped__, instance, args, kwargs)
class FunctionWrapper(ObjectProxy):
"""A descriptor to emulate a function.
This is used to create function decorators.
It maintains all of the nice introspection that can normally
be done on functions.
"""
__slots__ = ("wrapper", "binding")
def __init__(self, wrapped, wrapper):
super().__init__(wrapped)
# Using object.__setattr__ to subvert ObjectProxy.__setattr__
object.__setattr__(self, "wrapper", wrapper)
if isinstance(wrapped, classmethod):
object.__setattr__(self, "binding", "classmethod")
elif isinstance(wrapped, staticmethod):
object.__setattr__(self, "binding", "staticmethod")
else:
object.__setattr__(self, "binding", "function")
def __get__(self, instance, owner):
wrapped = self.__wrapped__.__get__(instance, owner)
return BoundFunctionWrapper(wrapped, instance, self.wrapper, self.binding, self)
def __call__(self, *args, **kwargs):
return self.wrapper(self.__wrapped__, None, args, kwargs)
[docs]def decorator(wrapper):
"""Decorate decorators.
This imposes a particular style of writing descriptors.
The descriptor must accept four positional arguments:
- ``wrapped``: the function being wrapped
- ``instance``: the instance that is bound to the function in the case of
bound functions (None in the case of plain functions)
- ``args``: the positional arguments passed to the wrapped function
- ``kwargs``: the keyword arguments passed to the wrapped function
Examples
--------
Decorating a normal function (i.e., instance will always be None):
.. testcode::
from nengo.utils.magic import decorator
@decorator
def my_decorator(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@my_decorator
def f():
return 1
Decorating a bound function:
.. testcode::
@decorator
def my_decorator(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
class MyClass:
@my_decorator
def f(self):
return self.num + 1
"""
def _wrapper(wrapped, instance, args, kwargs):
def _execute(wrapped, *_args, **_kwargs):
if instance is None:
target_wrapper = wrapper
elif inspect.isclass(instance):
target_wrapper = wrapper.__get__(None, instance)
else:
target_wrapper = wrapper.__get__(instance, type(instance))
return FunctionWrapper(wrapped, target_wrapper)
return _execute(*args, **kwargs)
return FunctionWrapper(wrapper, _wrapper)