Skip to content

Instantly share code, notes, and snippets.

@Strangemother
Created May 3, 2020 16:20
Show Gist options
  • Save Strangemother/4a9d243cec99d5949bb404e99a40630b to your computer and use it in GitHub Desktop.
Save Strangemother/4a9d243cec99d5949bb404e99a40630b to your computer and use it in GitHub Desktop.
"""Create complex functional or class-based decorators without effort.
+ Capture the class of a method: `owner_class`
+ Accept arguments at create, decorate or execution
+ functional based drop-in replacement for `functools.wraps`
+ Alternative work with a class-based setup
+ no dependecies
+
A class-based decorator provides methods to override the given function.
class ClassDecorator(decor.ArgSpy):
def before(self, owner, name):
return (args, kwargs,)
def execute(self, func, *func_args, **func_kw):
print('self.init_args ', self.init_args)
print('self.init_kwargs ', self.init_kwargs)
print('self.decor_args ', self.decor_args)
print('self.decor_kwargs ', self.decor_kwargs)
print('func args: ', func_args)
print('func kwargs: ', func_kw)
return func(*func_args, **func_kwargs)
def after(self, result):
return result
my_decorator = decor.wrap_class(decor.CacheContent)
class Apples():
@my_decorator
def foo(self, count, color='red'):
return (count*2, color,)
@my_decorator("cake", custom=1)
def foo(self, count, color='red'):
return (count*2, color,)
A functional decorator builds a standard _wrapped_ function but captures
attributes at all execution steps for your decorator.
def catch_all(owner_class, func,
init_args, init_kwargs,
decor_args, decor_kwargs,
func_args, func_kwargs,
):
print('owner ', owner_class, func)
print('init ', init_args, init_kwargs)
print('decor ', decor_args, decor_kwargs)
print('func ', func_args, func_kwargs)
return func(*func_args, **func_kwargs)
my_decorator = decor.wrap(catch_all, 'first-arg', 3, poppy='red')
class Apples():
@my_decorator
def foo(self, count, color='red'):
return (count*2, color,)
@my_decorator("cake", custom=1)
def foo(self, count, color='red'):
return (count*2, color,)
"""
import json
import os
import inspect
from collections import ChainMap
from functools import partial
def wrap_class(main_class, *init_args, **init_kwargs):
"""Return a read-to-execute decorator method given the setup class and the
class initial args and kwargs.
"""
return partial(decor_method, main_class, init_args, init_kwargs)
def wrap(func_caller, *init_args, **init_kwargs):
"""Return a read-to-execute decorator method given the setup function and
the function initial args and kwargs.
"""
return partial(decor_method, FunctionDecorator,
init_args, init_kwargs,
call_func=func_caller,
)
def decor_method(main_class, init_args, init_kwargs, *decor_args, **decor_kw):
"""Create and return a ready-to-execute decorator given the
`ClassDecorator` inherited class, its required init_arguments and the
decorator arguments
This function runs when using the previous setup decor.wrap(entity):
@partial(decor_method(decor.ClassDecorator, (), {}, *(), **{}))
def my_method(self, a, b):
# ...
pass
This function captures the optional decorator parenthesis set and returns
the appropriate `partial` or `main_class` callable. When using the
decorator without parenthesis, the first argument is the wrapped method
returning a class instance.
@my_decorator
def my_method(self):
pass
# return main_class instance
# > my_decorator(my_method)
When applying arguments, the initial values are captured without the target
function and returns a `partial`.
@my_decorator(1, alpha=2)
def my_method(self):
pass
# return partial
# > my_decorator(a, alpha=2)(my_method)
"""
prepared_args = (main_class, )
prepared_decorator = partial
prepared_kw = {
'init_args': init_args,
'init_kwargs': init_kwargs,
'args': decor_args,
'kwargs': decor_kw,
}
if callable(decor_args[0]):
# If the first argument is callable, the () parenthesis were omitted.
# alter the call to ensure the main_class receives the target function
# as the first argument.
prepared_kw['func'] = decor_args[0]
prepared_kw['args'] = decor_args[1:]
prepared_decorator = main_class
prepared_args = ()
return prepared_decorator(*prepared_args, **prepared_kw)
class ClassDecorator:
"""A Base class for all class-based decorators. Extend this class to
build custom decorator classes:
class MyDecor(ClassDecorator):
def execute(f, *a, **kw):
return f(*a, **kw)
my_decor = decor.wrap_class(MyDecor)
@my_decor
def foo():
return 1 + 1
"""
config = None
def __init__(self, func, args, kwargs,
init_args=None, init_kwargs=None,
**config):
"""Prepare the class as a decorator by providing the function to wrap,
the decorator arguments and initial arguments.
Any extra keywords append to `self.config`
"""
self.func = func
self.decor_args = args
self.decor_kwargs = kwargs
self.init_args = init_args or ()
self.init_kwargs = init_kwargs or {}
self.config = config
def flat_config(self, **kw):
"""Return a dictionary for reading config, init, decorator and given
keyword arguments in order.
"""
return ChainMap(self.config, self.init_kwargs, self.decor_kwargs, kw)
def before(self, owner_class, name):
"""Called before `execute` capturing the `owner_class`: the <class> or
parent of the wrapped method, and the `name` of the method to _wrap_.
return a tuple of (args, kwargs,) to extend the calling method.
If None is returned, the additional mutation arguments are ignored.
This method is the first and only method to receive a reference of the
owning class. If required, store for later:
self.owner_class = owner_class.
"""
return ((), {},)
def call_func(self, func, func_args, func_kwargs, before_args, before_kwargs):
"""Called the the _execute_ at runtime, the `call_func` runs the given
method with its given arguments.
The `before_*` arguments are optional and not applied by default to the
execution of the wrapped function.
Return the result of the wrapped function, through `after(result)`
"""
res = self.execute(func, *func_args, **func_kwargs)
return self.after(res)
def execute(self, func, *a, **kw):
"""The last stage runtime call of the wrapped function.
Given the function and its arguments return the value of the wrapped
function call.
"""
return func(*a, **kw)
def after(self, result):
"""ReturnGiven the `result` from the previous `execute()` call,
the final value from the execution of the wrapped function.
The value from this function is the late-stage output from the
decorator and is considered _final_ to be consumed by the code.
"""
return result
def __set_name__(self, owner_class, name):
"""The magic function to capture the alteration of the decorated class
or parent entity. Given the <class> (not an instance) and the `name` of
the changing function, alter the class using to call the decorated function
- or the _wrapper_.
This function calls `apply_wrapper` with the pwner class and any "before"
arguments to store within the generating decorator class instance.
Return Nothing.
"""
args, kwargs = self.before(owner_class, name) or ((), {},)
# injected = self.before(owner_class, name)
# args, kwargs = ((), {},)
# if injected is not None:
# args, kwargs = injected
self.apply_wrapper(owner_class, name, *args, **kwargs)
def __call__(self, *a, **kw):
"""A call or functional execution on this instance notes the wrapped
entity is a _class_ and _method_. Therefore the class executor should
run the same `before`, `execute`, `after` chain, but without the
`__set_name__` appliance.
Return the value from the executed wrapped entity. Notably it could
be a class instance but the instantiation and input method remains the
same.
"""
name = self.func.__name__
args, kwargs = self.before(self.func, name) or ((), {},)
return self.call_func(self.func,
func_args=a,
func_kwargs=kw,
before_args=args,
before_kwargs=kwargs,
)
def apply_wrapper(self, owner_class, name, *args, **kw):
"""Create the runtime callable as a replacement for the existing
function on the owner_class and call `write_wrapper` to apply
the new function.
As the class and collected method are unbound, the `self` argument
is dynamically applied within the executor if required.
Return the scoped method used as the wrapped. By default this method
is not stored internally.
"""
def _executor(func_self, *func_args, **func_kw):
_func_args = (func_self,) + func_args
return self.call_func(self.func,
func_args=_func_args, func_kwargs=func_kw,
before_args=args, before_kwargs=kw,
)
self.write_wrapper(owner_class, name, _executor)
return _executor
def write_wrapper(self, owner_class, name, method):
"""Write the given unbound method to the `owner_class` with the given
`name`. This is the last-stage implementation of method creation.
The method will be a newly scoped function from `apply_wrapper`,
therefore doesn't exist in the ClassDecorator MRO. This is to ensure
the `self` isn't polluted on the wrapped function if it is a method
on a class.
Return nothing.
"""
setattr(owner_class, name, method)
class ArgSpy(ClassDecorator):
"""The ArgSpy class extends ClassDecorator to serve as a simple debug and
helper tool when developing a new decorator.
This class extends few methods with `print` to present the flow of arguments
to the point of execution.
foo = decor.wrap_class(decor.ArgSpy, 1,2, charlie=3, delta=4)
@foo(5,6, echo=7)
def myfunc(count=3):
return count*2
The function will execute normally with addition print statements
displaying the input of `init` and `func` arguments
"""
def __init__(self, *a, **kw):
print('ArgSpy', *a, kw)
super().__init__(*a, **kw)
def apply_wrapper(self, owner_class, name, *args, **kw):
print('Apply Wrapper', owner_class, name, args, kw)
return super().apply_wrapper(owner_class, name, *args, **kw)
def call_func(self, func, func_args, func_kwargs, before_args, before_kwargs):
print('> Execute: ', func.__name__)
self.print_args(func_args, func_kwargs)
print('> extra args: ', before_args)
print('> extra kwargs: ', before_kwargs)
return super().call_func(func, func_args, func_kwargs,
before_args, before_kwargs)
def execute(self, func, *a, **kw):
print(' ---- Execute')
return super().execute(func, *a, **kw)
def before(self, owner, name):
print(('ClassDecorator.before - decorating '
f'"{name}" {self.func} and using {owner}'))
return super().before(owner, name)
def print_args(self, func_args, func_kw):
print('> self.init_args ', self.init_args)
print('> self.init_kwargs ', self.init_kwargs)
print('> self.decor_args ', self.decor_args)
print('> self.decor_kwargs ', self.decor_kwargs)
print('> func args: ', func_args)
print('> func kwargs: ', func_kw)
class FunctionDecorator(ClassDecorator):
"""The FunctionDecorator provides an abstraction of the class-based
decorator to a function-based methodology. Using `wrap(func)` to define a
new decorator, a FunctionDecorator with `call_func=func` to call upon a
given function.
The function decorator accepts additional variables to facilitate a
combination of all arguments to one function.
def catch_all(owner_class, func,
init_args, init_kwargs,
decor_args, decor_kwargs,
func_args, func_kwargs,
):
return func(*func_args, **func_kwargs)
my_decor = decor.wrap(catch_all, rose='blue', poppy='red')
@my_decor(True, one=1)
def my_func(*a, **kw):
return {}
"""
# def __init__(self, func, args, kwargs,
# init_args=None, init_kwargs=None,
# **config):
# super().__init__(func, args, kwargs,
# init_args=init_args, init_kwargs=init_kwargs,
# **config)
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
self.write_executers(**kw)
def write_executers(self, **config):
"""Reading the keys from the given `config` the api _execute_
functions, the optional `before`, `execute`, `after` and mandatory
`call_func` functions are applied to `self.executors`.
Calling any of these methods on this instance, calls the attached
executor.
"""
conf = self.flat_config(**config)
keys = ('before', 'execute', 'after', 'call_func',)
self.executors = {x: conf[x] for x in keys if x in conf}
def before(self, owner_class, name):
"""Call upon the given 'before' function if it exists.
If the before function does not exist, return the default tuple of
empty args and kwargs.
"""
self.owner_class = owner_class
_before = self.executors.get('before', None)
if _before is None:
return ((), {},)
return _before(owner_class, name)
def call_func(self, func, func_args, func_kwargs, before_args, before_kwargs):
func_caller = self.executors['call_func']
if func_caller is None:
return super().call_func(func,
func_args, func_kwargs,
before_args, before_kwargs)
return func_caller(owner_class=self.owner_class,
func=func,
init_args=self.init_args,
init_kwargs=self.init_kwargs,
#before_args, before_kwargs,
decor_args=self.decor_args,
decor_kwargs=self.decor_kwargs,
func_args=func_args,
func_kwargs=func_kwargs,
)
def after(self, result):
"""Call upon the given 'after' function if it exists.
If the after function does not exist, return the default result given.
"""
_after = self.executors.get('after', None)
if _after is None:
return result
return _after(result)
def execute(self, func, *a, **kw):
"""Call upon the given 'execute' function if it exists.
If the execute function does not exist, run the attached function and
return the result.
"""
_execute = self.executors.get('execute', None)
if _execute is None:
return self.func(*a, **kw)
return _execute(func, *a, **kw)
class CacheContent(ArgSpy):
"""
cache = decor.create_decor(CacheContent, root='./dir-path/')
@cache
@cache('foo')
@cache(name='foo')
"""
def before(self, owner, name):
"""Called before the 'execute' function, store the given `owner`
class name for later.
Return super
"""
self.owner_name = owner.__name__
return super().before(owner, name)
def execute(self, func, *a, **kw):
"""Called when the given function is called. Using the internal
arguments create and test for a cache file "class.method.xxx.json"
If the json file exists, return the content. If the file does not
exist, execute the given function and store the result as json for
any future calls.
Return the cache or new newly cached data from the wrapped function
call.
"""
filepath = self.build_path(a, kw)
if self.use_cache(filepath):
return self.open_json(filepath)
value = super().execute(func, *a, **kw)
self.store_json(filepath, value)
return value
def use_cache(self, filepath):
"""Return boolean to determine if the cache should be used.
By default the file existence is asserted.
"""
return os.path.exists(filepath)
def store_json(self, filename, content):
"""Store the given content with the given filename
"""
with open(filename, 'w', encoding='utf') as stream:
json.dump(content, stream, indent=4)
def open_json(self, filename):
"""Open the given filename and return the content.
"""
with open(filename, 'r', encoding='utf') as stream:
return json.load(stream)
def given_fixname(self, default=None):
name = self.decor_kwargs.get('name', default)
if len(self.decor_args) > 0:
name = self.decor_args[0]
return name
def build_path(self, a, kw):
config = self.flat_config(**kw)
root = config.get('root', os.getcwd)
root = root() if callable(root) else root
abs_root = os.path.abspath(root)
fixname = self.given_fixname(self.func.__name__)
# root, owner, fixname, .json
oname = f'{self.owner_name}.' if config.get('add_class', True) else ''
filename = f'{oname}{fixname}.json'
filepath = os.path.join(abs_root, filename)
return filepath
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment