Module omnipy.util.callable_decorator
Overview
View Source
from functools import update_wrapper
from types import MethodWrapperType
from typing import Callable, cast, Type
from omnipy.api.protocols.private.util import IsCallableClass, IsCallableParamAfterSelf
from omnipy.api.typedefs import DecoratorClassT
def callable_decorator_cls( # noqa: C901
cls: Type[DecoratorClassT]) -> IsCallableClass[DecoratorClassT]:
"""
"Meta-decorator" that allows any class to function as a decorator for a callable.
The only requirements are that 1) the first argument after self of the __init__() method needs
to be annotated as a callable, and 2) the class must not already be callable (have a __call__()
method).
Arguments and keyword arguments to the class decorator are supported.
"""
if not isinstance(cls.__call__, MethodWrapperType):
cls._wrapped_call: Callable = cast(Callable, cls.__call__)
def _forward_call_to_obj_if_callable(self, *args: object,
**kwargs: object) -> Type[DecoratorClassT]:
"""
__call__ method at the class level which forward the call to instance-level call methods,
if present (hardcoded as '_obj_call()'). This is needed due to the peculiarity that Python
only looks up special methods (with double underscores) at the class level, and not at the
instance level. Used in the decoration process to forward __call__ calls to the object level
_obj_call() methods, if present.
See: https://stackoverflow.com/q/33824228
"""
if hasattr(self, '_obj_call'):
return self._obj_call(*args, **kwargs)
if hasattr(self, '_wrapped_call'):
return self._wrapped_call(*args, **kwargs)
raise TypeError("'{}' object is not callable".format(self.__class__.__name__))
setattr(cls, '__call__', _forward_call_to_obj_if_callable)
def _real_callable(arg: object) -> bool:
"""
Helper method needed to ignore the class level __call__ method when checking whether the
decorated class is callable.
"""
if callable(arg):
if hasattr(arg, '__call__'):
if hasattr(arg.__call__, '__func__'):
# due to mypy bug: https://github.com/python/mypy/issues/14123
call_func_name = getattr(arg.__call__, '__func__').__name__
if call_func_name == _forward_call_to_obj_if_callable.__name__:
return False
return True
return False
_wrapped_new: Callable = cls.__new__
def _new_wrapper(cls, *args: object, **kwargs: object) -> DecoratorClassT:
if _wrapped_new is object.__new__:
obj = _wrapped_new(cls)
else:
obj = _wrapped_new(cls, *args, **kwargs)
# setattr(cls, '__new__', _wrapped_new)
_wrapped_init: IsCallableParamAfterSelf = cast(IsCallableParamAfterSelf,
obj.__class__.__init__)
# Wrapper method that replaces the __init__ method of the decorated class
def _init_wrapper(self, *args: object, **kwargs: object) -> None:
args_list = list(args)
def _init(callable_arg: Callable) -> None:
_wrapped_init(self, callable_arg, *args_list, **kwargs)
update_wrapper(self, callable_arg, updated=[])
if len(args_list) == 1 and _real_callable(args_list[0]):
# Decorate the callable directly
_callable_arg: Callable = cast(Callable, args_list[0])
args_list.pop(0)
_init(_callable_arg)
else:
# Add an instance-level _obj_call method, which are again callable by the
# class-level __call__ method. When this method is called, the provided
# _callable_arg is decorated.
def _init_as_obj_call_method(
self, _callable_arg: Callable) -> Type[DecoratorClassT]: # noqa
_init(_callable_arg)
del self._obj_call
return self
self._obj_call = _init_as_obj_call_method.__get__(self)
setattr(self.__class__, '__init__', _wrapped_init)
setattr(obj.__class__, '__init__', _init_wrapper)
return obj
setattr(cls, '__new__', _new_wrapper)
return cast(IsCallableClass[DecoratorClassT], cls)
Functions
callable_decorator_cls
def callable_decorator_cls(
cls: Type[+DecoratorClassT]
) -> omnipy.api.protocols.private.util.IsCallableClass[+DecoratorClassT]
"Meta-decorator" that allows any class to function as a decorator for a callable.
The only requirements are that 1) the first argument after self of the init() method needs to be annotated as a callable, and 2) the class must not already be callable (have a call() method).
Arguments and keyword arguments to the class decorator are supported.
Returns:
Type | Description |
---|---|
IsCallableClass[+DecoratorClassT] |
View Source
def callable_decorator_cls( # noqa: C901
cls: Type[DecoratorClassT]) -> IsCallableClass[DecoratorClassT]:
"""
"Meta-decorator" that allows any class to function as a decorator for a callable.
The only requirements are that 1) the first argument after self of the __init__() method needs
to be annotated as a callable, and 2) the class must not already be callable (have a __call__()
method).
Arguments and keyword arguments to the class decorator are supported.
"""
if not isinstance(cls.__call__, MethodWrapperType):
cls._wrapped_call: Callable = cast(Callable, cls.__call__)
def _forward_call_to_obj_if_callable(self, *args: object,
**kwargs: object) -> Type[DecoratorClassT]:
"""
__call__ method at the class level which forward the call to instance-level call methods,
if present (hardcoded as '_obj_call()'). This is needed due to the peculiarity that Python
only looks up special methods (with double underscores) at the class level, and not at the
instance level. Used in the decoration process to forward __call__ calls to the object level
_obj_call() methods, if present.
See: https://stackoverflow.com/q/33824228
"""
if hasattr(self, '_obj_call'):
return self._obj_call(*args, **kwargs)
if hasattr(self, '_wrapped_call'):
return self._wrapped_call(*args, **kwargs)
raise TypeError("'{}' object is not callable".format(self.__class__.__name__))
setattr(cls, '__call__', _forward_call_to_obj_if_callable)
def _real_callable(arg: object) -> bool:
"""
Helper method needed to ignore the class level __call__ method when checking whether the
decorated class is callable.
"""
if callable(arg):
if hasattr(arg, '__call__'):
if hasattr(arg.__call__, '__func__'):
# due to mypy bug: https://github.com/python/mypy/issues/14123
call_func_name = getattr(arg.__call__, '__func__').__name__
if call_func_name == _forward_call_to_obj_if_callable.__name__:
return False
return True
return False
_wrapped_new: Callable = cls.__new__
def _new_wrapper(cls, *args: object, **kwargs: object) -> DecoratorClassT:
if _wrapped_new is object.__new__:
obj = _wrapped_new(cls)
else:
obj = _wrapped_new(cls, *args, **kwargs)
# setattr(cls, '__new__', _wrapped_new)
_wrapped_init: IsCallableParamAfterSelf = cast(IsCallableParamAfterSelf,
obj.__class__.__init__)
# Wrapper method that replaces the __init__ method of the decorated class
def _init_wrapper(self, *args: object, **kwargs: object) -> None:
args_list = list(args)
def _init(callable_arg: Callable) -> None:
_wrapped_init(self, callable_arg, *args_list, **kwargs)
update_wrapper(self, callable_arg, updated=[])
if len(args_list) == 1 and _real_callable(args_list[0]):
# Decorate the callable directly
_callable_arg: Callable = cast(Callable, args_list[0])
args_list.pop(0)
_init(_callable_arg)
else:
# Add an instance-level _obj_call method, which are again callable by the
# class-level __call__ method. When this method is called, the provided
# _callable_arg is decorated.
def _init_as_obj_call_method(
self, _callable_arg: Callable) -> Type[DecoratorClassT]: # noqa
_init(_callable_arg)
del self._obj_call
return self
self._obj_call = _init_as_obj_call_method.__get__(self)
setattr(self.__class__, '__init__', _wrapped_init)
setattr(obj.__class__, '__init__', _init_wrapper)
return obj
setattr(cls, '__new__', _new_wrapper)
return cast(IsCallableClass[DecoratorClassT], cls)