Created
          May 3, 2020 16:20 
        
      - 
      
- 
        Save Strangemother/4a9d243cec99d5949bb404e99a40630b to your computer and use it in GitHub Desktop. 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | """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