Name | onionizer JSON |
Version |
0.4.2
JSON |
| download |
home_page | None |
Summary | A Python package to add middlewares to any function |
upload_time | 2023-04-02 21:08:33 |
maintainer | None |
docs_url | None |
author | None |
requires_python | None |
license | MIT |
keywords |
decorator
middleware
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# onionizer
[](https://pypi.org/project/onionizer)
[](http://pitest.org/)
[](https://circleci.com/gh/brumar/onionizer)
[](https://codecov.io/gh/brumar/onionizer)

-----
**Table of Contents**
- [Introduction](#Introduction)
- [Motivation](#Motivation)
- [Installation](#Installation)
- [Usage](#Usage)
- [Features](#Features)
- [More on decorators vs middlewares: Flexibilty is good, until it's not](#More-on-decorators-vs-middlewares--Flexibilty-is-good-until-its-not)
- [Advanced Usage](#Advanced-Usage)
## Introduction
Onionizer is a library that allows you to wrap a function with a list of middlewares.
Think of it of a more yumy-yumy way to create and use decorators.
```python
import onionizer
@onionizer.as_decorator
def ensure_that_total_discount_is_acceptable(original_price, context):
# you can do yummy stuff here (before the wrapped function is called)
result = yield onionizer.UNCHANGED
# and here (after the wrapped function is called)
if original_price/result < 0.5:
raise ValueError("Total discount is too high")
return result
@ensure_that_total_discount_is_acceptable
def discount_function(original_price: int, context: dict) -> int:
...
```
Yummy!
The equivalent behavior without onionizer would be:
```python
import functools
def ensure_that_total_discount_is_acceptable(func):
@functools.wraps(func)
def wrapper(original_price, context):
result = func(original_price, context)
if result < original_price/2:
raise ValueError("Total discount is too high")
return result
return wrapper
@ensure_that_total_discount_is_acceptable
def discount_function(original_price, context):
...
```
Less Yummy!
The onionizer example is a bit more concise (and more flat) as there is no need to define and return a wrapper function (while keeping in mind to use `functools.wraps` to preserve the docstring and the signature of the wrapped function).
Yielding `onionizer.UNCHANGED` ensure the reader that the arguments are not modified by the middleware.
Of course, you can yield other values if you want to mutate the arguments (more on that later).
If there is an incompatibility of signatures, the middleware will raise an error at wrapping time, whereas the decorator syntax will fail at runtime one day you did not expect.
## Motivation
Onionizer is inspired by the onion model of middlewares in web frameworks such as Django, Flask and FastAPI.
If you are into web developpement, you certainly found this pattern very convenient as you plug middlewares to your application to add features such as authentication, logging, etc.
**Why not generalize this pattern to any function ? That's what Onionizer does.**
Hopefully, it could nudge communities share code more easily when they are using extensively the same specific API. Yes, I am looking at you `openai.ChatCompletion.create`.
# Installation
```bash
pip install onionizer
```
Onionizer has no sub-dependencies
## Usage
We saw the usage of onionizer.as_decorator in the introductive example.
Another way to use onionizer is to wrap a function with a list of middlewares using `onionizer.wrap_around` :
```python
import onionizer
def func(x, y):
return x + y
def middleware1(x, y):
result = yield (x+1, y+1), {} # yield the new arguments and keyword arguments ; obtain the result
return result # Do nothing with the result
def middleware2(x, y):
result = yield (x, y), {} # arguments are not preprocessed by this middleware
return result*2 # double the result
wrapped_func = onionizer.wrap_around(func, [middleware1, middleware2])
result = wrapped_func(0, 0)
print(result) # 2
```
Tracing the execution layers by layers :
- `middleware1` is called with arguments `(0, 0)` ; it yields the new arguments `(1, 1)` and keyword arguments `{}`
- `middleware2` is called with arguments `(1, 1)` ; it yields the new arguments `(1, 1)` and keyword arguments `{}` (unchanged)
- `wrapped_func` calls `func` with arguments `(1, 1)` which returns `2`
- `middleware2` returns `4`
- `middleware1` returns `4` (unchanged)
Alternatively, you can use the decorator syntax :
```python
@onionizer.decorate([middleware1, middleware2])
def func(x, y):
return x + y
```
## Features
- support for normal function if you only want to preprocess arguments or postprocess results
- support for context managers out of the box. Use this to handle resources or exceptions (try/except around the yield statement wont work for the middlewares)
- simplified preprocessing of arguments using `PositionalArgs` and `KeywordArgs` to match your preferred style or onionizer.UNCHANGED (see below)
## More on decorators vs middlewares : Flexibilty is good, until it's not
Chances are, if asked to add behavior before and after a function, you would use decorators.
And that's fine! Decorators are awesome and super flexible. But in the programming world, flexibility can also be a weakness.
Onionizer middlewares are more constrained to ensure composability : a middleware that do not share the exact same signature as the wrapped function will raise an error at wrapping time.
Using the yield statement to separate the setup from the teardown is now a classic pattern in python development.
You might already be familiar with it if you are using context managers using contextlib.contextmanager or if you are testing your code with pytest fixtures.
It's flat, explicit and easy to read, it's pythonic then. So let's eat more of these yummy-yummy yield statements!
## Advanced usage
### PositionalArgs and KeywordArgs
The default way of using the yield statement is to pass a tuple of positional arguments and a dict of keyword arguments.
But you can also pass `onionizer.PositionalArgs` and `onionizer.KeywordArgs` to simplify the preprocessing of arguments.
Onionizer provides two classes to simplify the preprocessing of arguments : `PositionalArgs`, `KeywordArgs`.
```python
import onionizer
def func(x, y):
return x + y
def middleware1(x: int, y: int):
result = yield onionizer.PositionalArgs(x + 1, y) # pass any number of positional arguments
return result
def middleware2(x: int, y: int):
result = yield onionizer.KeywordArgs({'x': x, 'y': y + 1}) # pass a dict with any number of keyword arguments
return result
wrapped_func = onionizer.wrap_around(func, [middleware1, middleware2])
```
And if you want to keep the arguments unchanged, you can use `onionizer.UNCHANGED` :
```python
def wont_do_anything(x: int, y: int):
result = yield onionizer.UNCHANGED
return result
```
### Support for context managers
context managers are de facto supported by onionizer.
```python
def func(x, y):
with exception_catcher():
return x/y
@contextlib.contextmanager
def exception_catcher():
try:
yield
except Exception as e:
raise RuntimeError("Exception caught") from e
wrapped_func = onionizer.wrap_around(func, [exception_catcher()])
wrapped_func(x=1, y=0) # raises RuntimeError("Exception caught")
```
### Support for simple functions
You can use simple functions if you only want to preprocess arguments or postprocess results.
```python
def test_preprocessor(func_that_adds):
@onionizer.preprocessor
def midd1(x: int, y: int):
return onionizer.PositionalArgs(x + 1, y + 1)
wrapped_func = onionizer.wrap_around(func_that_adds, [midd1])
result = wrapped_func(x=0, y=0)
assert result == 2
def test_postprocessor(func_that_adds):
@onionizer.postprocessor
def midd1(val: int):
return val**2
wrapped_func = onionizer.wrap_around(func_that_adds, [midd1])
result = wrapped_func(x=1, y=1)
assert result == 4
```
### Remove signature checks
By default, onionizer will check that the signature of the middlewares matches the signature of the wrapped function. This is to ensure that the middlewares are composable. If you want to disable this check, you can use `onionizer.wrap_around_no_check` instead of `onionizer.wrap_around`.
```python
def test_uncompatible_signature_but_disable_sigcheck(func_that_adds):
def middleware1(*args):
result = yield onionizer.UNCHANGED
return result
onionizer.wrap_around(func_that_adds, middlewares=[middleware1], sigcheck=False)
assert True
```
## License
`onionizer` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
## Gotchas
- `Try: yield except:` won't work in a middleware. Use a context manager instead.
- only sync functions are supported at the moment, no methods, no classes, no generators, no coroutines, no async functions.
- middlewares must have the same signature as the wrapped function. Use sigcheck=False to disable this check.
Authorize the use of `*args` and `**kwargs` in middlewares is under consideration
Raw data
{
"_id": null,
"home_page": null,
"name": "onionizer",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": "decorator,middleware",
"author": null,
"author_email": "bruno <martin.bruno.mail@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/8f/13/f0b7d3717c276763e84ee8e9d05a9d1083754b41e2161aceea985b330e89/onionizer-0.4.2.tar.gz",
"platform": null,
"description": "\n# onionizer\n\n[](https://pypi.org/project/onionizer)\n[](http://pitest.org/)\n[](https://circleci.com/gh/brumar/onionizer)\n[](https://codecov.io/gh/brumar/onionizer)\n\n\n-----\n\n**Table of Contents**\n\n- [Introduction](#Introduction)\n- [Motivation](#Motivation)\n- [Installation](#Installation)\n- [Usage](#Usage)\n- [Features](#Features)\n- [More on decorators vs middlewares: Flexibilty is good, until it's not](#More-on-decorators-vs-middlewares--Flexibilty-is-good-until-its-not)\n- [Advanced Usage](#Advanced-Usage)\n\n\n## Introduction\n\nOnionizer is a library that allows you to wrap a function with a list of middlewares.\nThink of it of a more yumy-yumy way to create and use decorators.\n\n```python\nimport onionizer\n\n@onionizer.as_decorator\ndef ensure_that_total_discount_is_acceptable(original_price, context):\n # you can do yummy stuff here (before the wrapped function is called)\n result = yield onionizer.UNCHANGED\n # and here (after the wrapped function is called)\n if original_price/result < 0.5:\n raise ValueError(\"Total discount is too high\")\n return result\n\n@ensure_that_total_discount_is_acceptable\ndef discount_function(original_price: int, context: dict) -> int:\n ...\n```\nYummy!\n\nThe equivalent behavior without onionizer would be:\n```python\nimport functools\ndef ensure_that_total_discount_is_acceptable(func):\n @functools.wraps(func)\n def wrapper(original_price, context):\n result = func(original_price, context)\n if result < original_price/2:\n raise ValueError(\"Total discount is too high\")\n return result\n return wrapper\n\n@ensure_that_total_discount_is_acceptable\ndef discount_function(original_price, context):\n ...\n```\nLess Yummy!\n\nThe onionizer example is a bit more concise (and more flat) as there is no need to define and return a wrapper function (while keeping in mind to use `functools.wraps` to preserve the docstring and the signature of the wrapped function).\n\nYielding `onionizer.UNCHANGED` ensure the reader that the arguments are not modified by the middleware.\nOf course, you can yield other values if you want to mutate the arguments (more on that later).\n\nIf there is an incompatibility of signatures, the middleware will raise an error at wrapping time, whereas the decorator syntax will fail at runtime one day you did not expect.\n\n## Motivation\n\nOnionizer is inspired by the onion model of middlewares in web frameworks such as Django, Flask and FastAPI.\n\nIf you are into web developpement, you certainly found this pattern very convenient as you plug middlewares to your application to add features such as authentication, logging, etc.\n\n**Why not generalize this pattern to any function ? That's what Onionizer does.**\n\nHopefully, it could nudge communities share code more easily when they are using extensively the same specific API. Yes, I am looking at you `openai.ChatCompletion.create`.\n\n# Installation\n\n```bash\npip install onionizer\n```\nOnionizer has no sub-dependencies\n\n## Usage\n\nWe saw the usage of onionizer.as_decorator in the introductive example.\nAnother way to use onionizer is to wrap a function with a list of middlewares using `onionizer.wrap_around` :\n\n```python\nimport onionizer\ndef func(x, y):\n return x + y\n\ndef middleware1(x, y):\n result = yield (x+1, y+1), {} # yield the new arguments and keyword arguments ; obtain the result\n return result # Do nothing with the result\n\ndef middleware2(x, y):\n result = yield (x, y), {} # arguments are not preprocessed by this middleware\n return result*2 # double the result\n\nwrapped_func = onionizer.wrap_around(func, [middleware1, middleware2])\nresult = wrapped_func(0, 0)\nprint(result) # 2\n```\n\nTracing the execution layers by layers :\n- `middleware1` is called with arguments `(0, 0)` ; it yields the new arguments `(1, 1)` and keyword arguments `{}` \n- `middleware2` is called with arguments `(1, 1)` ; it yields the new arguments `(1, 1)` and keyword arguments `{}` (unchanged)\n- `wrapped_func` calls `func` with arguments `(1, 1)` which returns `2`\n- `middleware2` returns `4`\n- `middleware1` returns `4` (unchanged)\n\nAlternatively, you can use the decorator syntax :\n```python\n@onionizer.decorate([middleware1, middleware2])\ndef func(x, y):\n return x + y\n```\n\n## Features\n\n- support for normal function if you only want to preprocess arguments or postprocess results\n- support for context managers out of the box. Use this to handle resources or exceptions (try/except around the yield statement wont work for the middlewares)\n- simplified preprocessing of arguments using `PositionalArgs` and `KeywordArgs` to match your preferred style or onionizer.UNCHANGED (see below)\n\n\n## More on decorators vs middlewares : Flexibilty is good, until it's not\n\nChances are, if asked to add behavior before and after a function, you would use decorators. \nAnd that's fine! Decorators are awesome and super flexible. But in the programming world, flexibility can also be a weakness. \n\nOnionizer middlewares are more constrained to ensure composability : a middleware that do not share the exact same signature as the wrapped function will raise an error at wrapping time.\nUsing the yield statement to separate the setup from the teardown is now a classic pattern in python development. \nYou might already be familiar with it if you are using context managers using contextlib.contextmanager or if you are testing your code with pytest fixtures.\nIt's flat, explicit and easy to read, it's pythonic then. So let's eat more of these yummy-yummy yield statements!\n\n## Advanced usage\n\n### PositionalArgs and KeywordArgs\n\nThe default way of using the yield statement is to pass a tuple of positional arguments and a dict of keyword arguments.\nBut you can also pass `onionizer.PositionalArgs` and `onionizer.KeywordArgs` to simplify the preprocessing of arguments.\nOnionizer provides two classes to simplify the preprocessing of arguments : `PositionalArgs`, `KeywordArgs`.\n\n```python\nimport onionizer\ndef func(x, y):\n return x + y\n\ndef middleware1(x: int, y: int):\n result = yield onionizer.PositionalArgs(x + 1, y) # pass any number of positional arguments\n return result\n\ndef middleware2(x: int, y: int):\n result = yield onionizer.KeywordArgs({'x': x, 'y': y + 1}) # pass a dict with any number of keyword arguments\n return result\nwrapped_func = onionizer.wrap_around(func, [middleware1, middleware2])\n```\nAnd if you want to keep the arguments unchanged, you can use `onionizer.UNCHANGED` :\n```python\ndef wont_do_anything(x: int, y: int):\n result = yield onionizer.UNCHANGED\n return result\n```\n\n### Support for context managers\n\ncontext managers are de facto supported by onionizer.\n\n```python\ndef func(x, y):\n with exception_catcher():\n return x/y\n\n@contextlib.contextmanager\ndef exception_catcher():\n try:\n yield\n except Exception as e:\n raise RuntimeError(\"Exception caught\") from e\n\nwrapped_func = onionizer.wrap_around(func, [exception_catcher()])\nwrapped_func(x=1, y=0) # raises RuntimeError(\"Exception caught\")\n```\n\n### Support for simple functions\n\nYou can use simple functions if you only want to preprocess arguments or postprocess results.\n\n```python\ndef test_preprocessor(func_that_adds):\n @onionizer.preprocessor\n def midd1(x: int, y: int):\n return onionizer.PositionalArgs(x + 1, y + 1)\n\n wrapped_func = onionizer.wrap_around(func_that_adds, [midd1])\n result = wrapped_func(x=0, y=0)\n assert result == 2\n\n\ndef test_postprocessor(func_that_adds):\n @onionizer.postprocessor\n def midd1(val: int):\n return val**2\n\n wrapped_func = onionizer.wrap_around(func_that_adds, [midd1])\n result = wrapped_func(x=1, y=1)\n assert result == 4\n```\n\n### Remove signature checks\n\nBy default, onionizer will check that the signature of the middlewares matches the signature of the wrapped function. This is to ensure that the middlewares are composable. If you want to disable this check, you can use `onionizer.wrap_around_no_check` instead of `onionizer.wrap_around`.\n\n```python\ndef test_uncompatible_signature_but_disable_sigcheck(func_that_adds):\n def middleware1(*args):\n result = yield onionizer.UNCHANGED\n return result\n\n onionizer.wrap_around(func_that_adds, middlewares=[middleware1], sigcheck=False)\n assert True\n```\n\n## License\n\n`onionizer` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.\n\n## Gotchas\n\n- `Try: yield except:` won't work in a middleware. Use a context manager instead.\n- only sync functions are supported at the moment, no methods, no classes, no generators, no coroutines, no async functions.\n- middlewares must have the same signature as the wrapped function. Use sigcheck=False to disable this check. \nAuthorize the use of `*args` and `**kwargs` in middlewares is under consideration",
"bugtrack_url": null,
"license": "MIT",
"summary": "A Python package to add middlewares to any function",
"version": "0.4.2",
"split_keywords": [
"decorator",
"middleware"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "d76dae34a238df6751e116a163367ed9e22b90fd10cf890161617264f31ef0bf",
"md5": "8948c38f50eacb4a753e753364776684",
"sha256": "937410290f033e06d42d5c0ac4345bfe544456f277e6d22020e20705ff071514"
},
"downloads": -1,
"filename": "onionizer-0.4.2-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "8948c38f50eacb4a753e753364776684",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 7901,
"upload_time": "2023-04-02T21:08:28",
"upload_time_iso_8601": "2023-04-02T21:08:28.843384Z",
"url": "https://files.pythonhosted.org/packages/d7/6d/ae34a238df6751e116a163367ed9e22b90fd10cf890161617264f31ef0bf/onionizer-0.4.2-py2.py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "8f13f0b7d3717c276763e84ee8e9d05a9d1083754b41e2161aceea985b330e89",
"md5": "5437d02ce97609e468cc6cd3e01947bb",
"sha256": "72ab28d8d7fe4f555fc49bd0aae41af11ff2b594811073aad81eeb3868b51c29"
},
"downloads": -1,
"filename": "onionizer-0.4.2.tar.gz",
"has_sig": false,
"md5_digest": "5437d02ce97609e468cc6cd3e01947bb",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 18925524,
"upload_time": "2023-04-02T21:08:33",
"upload_time_iso_8601": "2023-04-02T21:08:33.584705Z",
"url": "https://files.pythonhosted.org/packages/8f/13/f0b7d3717c276763e84ee8e9d05a9d1083754b41e2161aceea985b330e89/onionizer-0.4.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-04-02 21:08:33",
"github": false,
"gitlab": false,
"bitbucket": false,
"lcname": "onionizer"
}