Skip to content

Commit

Permalink
Added support for passing decorated function to callbacks (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelgregorovic authored Feb 7, 2023
2 parents b41ab29 + 0e71bbe commit a744ca1
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 250 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ name: build
on:
push:
branches:
- 'develop'
- 'main'
- '**'
jobs:
build:
runs-on: ubuntu-latest
Expand Down
177 changes: 84 additions & 93 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ You can specify callbacks for on_call, on_success, on_failure, and on_end, and c
## Features

- Support `on_call`, `on_success`, `on_failure` and `on_end` callbacks
- Pass decorated function **kwargs and function itself to callbacks
- Option to specify default return from the decorated function
- Option to pass local scope variables of the decorated function to the `on_end` callback
- Option to specify exception classes to be expected and invoke `on_failure` callback
Expand All @@ -45,17 +46,21 @@ Package is currently available under the same name at [![PyPI version](https://b
In latest version of `callpyback`, when declaring callback functions, following rules must be obeyed:

a) `on_call()` callback MUST eitheraccept no parameters or combination of the following:
- `func` - will receive reference to decorated function
- `func_kwargs` - will receive parameters passed to the function decorated with `CallPyBack`

b) `on_success()` callback MUST either accept no parameters or combination of the following:
- `func` - will receive reference to decorated function
- `func_result` - will receive return value of the function decorated with `CallPyBack`
- `func_kwargs` - will receive parameters passed to the function decorated with `CallPyBack`

c) `on_failure()` callback MUST either accept no parameters or combination of the following:
- `func` - will receive reference to decorated function
- `func_exception` - will receive exception raised by the function decorated with `CallPyBack`
- `func_kwargs` - will receive parameters passed to the function decorated with `CallPyBack`

d) `on_end()` callback MUST either accept no parameters or combination of the following:
- `func` - will receive reference to decorated function
- `func_result` - will receive return value of the function decorated with `CallPyBack`
- `func_exception` - will receive exception raised by the function decorated with `CallPyBack`
- `func_kwargs` - will receive parameters passed to the function decorated with `CallPyBack`
Expand All @@ -65,127 +70,114 @@ These rules are enforced to allow omitting parameters in the callback function.

<p align="right">(<a href="#readme-top">back to top</a>)</p>


#### 1. Decorating the function with ```CallPyBack``` class decorator with callback functions specified

#### Prerequisites
Consider following callbacks:
```python
def on_call(func, func_kwargs):
print('-----ON CALL CALLBACK-----')
func_kwargs_repr = ', '.join(f'{key}={val}' for key, val in func_kwargs.items())
print(f'Function `{func.__name__}` called with parameters: {func_kwargs_repr}.\n')

def on_success(func_result):
print(f'Done with a result: {func_result}!')

def on_failure(func_exception):
print(f'Failed with an error: {func_exception}!')

@background_callpyback
def on_success(func, func_result, func_kwargs):
print('-----ON SUCCESS CALLBACK-----')
func_kwargs_repr = ', '.join(f'{key}={val}' for key, val in func_kwargs.items())
print(f'Function `{func.__name__}` successfully done with a result: {func_result}.')
print(f'Was called with parameters: {func_kwargs_repr}\n')

@CallPyBack(on_success=on_success, on_failure=on_failure)
def method()
pass
@background_callpyback
def on_failure(func, func_exception, func_kwargs):
print('-----ON FAILURE CALLBACK-----')
func_kwargs_repr = ', '.join(f'{key}={val}' for key, val in func_kwargs.items())
print(f'Function `{func.__name__} failed with an error: {func_exception}!')
print(f'Was called with parameters: {func_kwargs_repr}\n')

method()
@background_callpyback
def on_end(func, func_result, func_exception, func_kwargs, func_scope_vars):
print('-----ON END CALLBACK-----')
func_kwargs_repr = ', '.join(f'{key}={val}' for key, val in func_kwargs.items())
func_scope_vars_repr = ', '.join(f'{key}={val}' for key, val in func_scope_vars.items())
if func_exception:
print(f'Function `{func.__name__} failed with an error: {func_exception}!')
else:
print('No exception was raised')
print(f'Function `{func.__name__}` done with a result: {func_result}.')
print(f'Was called with parameters: {func_kwargs_repr}')
print(f'Local variables of the function: {func_scope_vars_repr}')
```

#### 2. Preconfigured ```CallPyBack``` callback custom class
and initialization of a decorator:
```python

def on_success(func_result):
print(f'Done with a result: {func_result}!')

def on_failure(func_exception):
print(f'Failed with an error: {func_exception}!')

custom_callpyback = CallPyBack(
on_call=on_call,
on_success=on_success,
on_failure=on_failure
on_failure=on_failure,
on_end=on_end,
default_return='default',
exception_classes=(RuntimeError,),
pass_vars=('a',)
)

@custom_callpyback
def method():
pass

method()
```
These will be used in following examples:

#### 3. Using ```@background_callpyback``` decorator to make callback execute on the background thread
#### 1. Decorated function executes without error
```python

@background_callpyback
def on_success(func_result):
print(f'Done with a result: {func_result}!')

def on_failure(func_exception):
print(f'Failed with an error: {func_exception}!')

custom_callpyback = CallPyBack(
on_success=on_success,
on_failure=on_failure
)

@custom_callpyback
def method():
pass
def method(x, y, z=None):
a = 42
return x + y

method()
result = method(1, 2)
print(f'Result: {result}')
```
In this case, `on_success` will be executed on the background thread, while `on_failure` will be executed in a blocking way.

will result in
```bash
-----ON CALL CALLBACK-----
Function `method` called with parameters: x=1, y=2, z=None.

#### 4. Passing local variables of decorated function, specified in `pass_vars` to `on_end` callback
```python
Result: 3

def on_end(func_result, func_scope_vars):
print(f'Done with a result: {func_result}!')
print(f'Local function variables: {func_scope_vars}')
-----ON SUCCESS CALLBACK-----
Function `method` successfully done with a result: 3.
Was called with parameters: x=1, y=2, z=None

custom_callpyback = CallPyBack(
on_end=on_end,
pass_vars=('a', 'b')
)
-----ON END CALLBACK-----
No exception was raised
Function `method` done with a result: 3.
Was called with parameters: x=1, y=2, z=None
Local variables of the function: a=42

@custom_callpyback
def method():
a = 0
b = 1
return a

method()
```
`on_success` and `on_end` will be executed on the background thread, while `on_call` will be executed in a blocking way and `on_failure` will not be called.


#### 5. Specifiyng default return value by `default_return` parameter.
#### 2. Decorated function raises an error
```python


custom_callpyback = CallPyBack(
default_return=-1
)

@custom_callpyback
def method():
raise KeyError('fail')
def method(x, y, z=None):
a = 42
raise RuntimeError("some error")

result = method()
result = method(1, 2)
print(f'Result: {result}')
```
In this case, result will be equal to `-1` specified in `default_return`.


#### 6. Specifiyng exception classes to be caught by `exception_classes` parameter.
```python
will result in
```bash
-----ON CALL CALLBACK-----
Function `method` called with parameters: x=1, y=2, z=None.

def on_failure(func_exception):
print(f'Failed with an error: {func_exception}!')
-----ON FAILURE CALLBACK-----
Function `method` failed with an error: some error!
Was called with parameters: x=1, y=2, z=None

custom_callpyback = CallPyBack(
on_failure=on_failure,
exception_classes=(TypeError,)
)

@custom_callpyback
def method():
raise KeyError('fail')
-----ON END CALLBACK-----
Function `method` failed with an error: some error!
Function `method` done with a result: default.
Was called with parameters: x=1, y=2, z=None
Local variables of the function: a=42

result = method()
```
In this case, exception will be raised, which will not execute failure handler, but re-raise original exception.
`on_failure` and `on_end` will be executed on the background thread, while `on_call` will be executed in a blocking way and `on_success` will not be called.

<p align="right">(<a href="#readme-top">back to top</a>)</p>

Expand All @@ -200,9 +192,8 @@ In this case, exception will be raised, which will not execute failure handler,
- [x] Option to omit callbacks - default callback
- [x] Option to omit callback's function parameters (specify only those which you need)
- [x] Option to execute callbacks on the background (new thread) via `@background_callpyback` decorator
- [ ] Add `asyncio` support for decorated function
- [ ] Add `asyncio` support for callback function
- [ ] TBD...
- [x] Option to pass decorated function reference to all callbacks
- [ ] To be determined...

See the [open issues](https://github.com/samuelgregorovic/callpyback/issues) for a full list of proposed features (and known issues).

Expand Down
50 changes: 28 additions & 22 deletions callpyback/callpyback.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ def __init__(self, **kwargs):
"""Class constructor. Sets instance variables.
Args:
on_call (Callable, optional): Function to be called before function execution.
on_call (Callable, optional): Called before function execution.
Defaults to DEFAULT_ON_CALL_LAMBDA.
on_success (Callable, optional): Function to be called after successfull execution.
on_success (Callable, optional): Called after successfull run.
Defaults to DEFAULT_ON_SUCCESS_LAMBDA.
on_failure (Callable, optional): Function to be called after execution with errors.
on_failure (Callable, optional): Called after run with errors.
Defaults to DEFAULT_ON_FAILURE_LAMBDA.
on_end (Callable, optional): Function to be called after execution regardless of result.
on_end (Callable, optional): Called after every execution.
Defaults to DEFAULT_ON_END_LAMBDA.
default_return (Any, optional): Result to be returned in case of error or no return.
Defaults to None.
pass_vars (list|tuple|set, optional): Variable names to be passed to `on_end` callback.
Defaults to None.
default_return (Any, optional): Result to be returned in
case of error or no return. Defaults to None.
pass_vars (list|tuple|set, optional): Variable names to be passed
to `on_end` callback. Defaults to None.
exception_classes (list|tuple|set): Exception classes to be caught.
Defaults to (Exception,).
Expand Down Expand Up @@ -72,30 +72,35 @@ def validate_across_mixins(self):
Returns:
None
Raises:
RuntimeError: Raised if `pass_vars` is defined but `on_end` callback is not.
RuntimeError: Raised if `pass_vars` is defined but `on_end`
callback is not.
"""
if self.pass_vars and self.on_end is _default_callback:
raise RuntimeError("If `pass_vars` is defined, `on_end` must be defined.")
raise RuntimeError(
"If `pass_vars` is defined, `on_end` must be defined.",
)

def __call__(self, func):
"""Invoked on decorator instance call.
Holds logic of the callback process, including invoking callbacks and passed function.
Holds logic of the callback process, including invoking callbacks
and passed function.
Functions:
wrapper(*func_args, **func_kwargs):
Decorator class wrapper accepting `args` and `kwargs`
for the decorated function. Contains callback and execution logic.
for the decorated function. Contains callback logic.
Args:
func (Callable): Decorated function to be executed amongst callbacks.
func (Callable): Decorated function to be executed.
Returns:
None
Raises:
N/A
"""

def _wrapper(*func_args, **func_kwargs):
"""Decorator class wrapper accepting `args` and `kwargs` for the decorated function.
"""Decorator class wrapper accepting `args` and `kwargs` for the
decorated function.
Calling main method containing callback logic."""
return self.main(func, func_args, func_kwargs)

Expand All @@ -118,39 +123,40 @@ def main(self, func, func_args, func_kwargs):
7b. Executing `on_failure` callback (if defined).
8b. Returning `default_return` value
9. Reverting to default tracer.
10. Re-raising decorated function exception if `on_end` callback is not defined.
10. Re-raising decorated function exception if `on_end` callback
is not defined.
11. Executing `on_end` callback (if defined).
Args:
func(Callable): Decorated function to be executed amongst callbacks.
func(Callable): Decorated function to be executed.
func_args(tuple): Arguments for the decorated function.
func_kwargs(dict): Keyword arguments for the decorated function.
Returns:
Any: Decorated function result or `default_return` value.
Raises:
func_exception: Raised if error occurs during function execution, only if `on_end`
handler is not defined.
func_exception: Raised if error occurs during function execution,
only if `on_end` handler is not defined.
"""
self.validate_arguments()
self.set_tracer_profile(self.tracer)
all_func_kwargs = args_to_kwargs(func, func_args, func_kwargs)
func_exception, func_result, func_scope_vars = None, None, []
try:
self.run_on_call_func(all_func_kwargs)
self.run_on_call_func(func, all_func_kwargs)
func_result = func(**all_func_kwargs)
func_scope_vars = self.get_func_scope_vars()
self.run_on_success_func(func_result, all_func_kwargs)
self.run_on_success_func(func, func_result, all_func_kwargs)
return func_result
except self.exception_classes as ex:
func_exception = ex
func_scope_vars = self.get_func_scope_vars()
self.run_on_failure_func(func_exception, all_func_kwargs)
self.run_on_failure_func(func, func_exception, all_func_kwargs)
return self.default_return
finally:
self.set_tracer_profile(None)
if self.on_end is _default_callback and func_exception:
raise func_exception
result = func_result if not func_exception else self.default_return
self.run_on_end_func(
result, func_exception, all_func_kwargs, func_scope_vars
func, result, func_exception, all_func_kwargs, func_scope_vars
)
Loading

0 comments on commit a744ca1

Please sign in to comment.