bswck / proxyvars

Create type-safe, context-dependent proxy objects. Inspired by werkzeug's LocalProxy
https://bswck.github.io/proxyvars/
MIT License
3 stars 1 forks source link
contextvars werkzeug

proxyvars
skeleton Supported Python versions Package version

Tests Coverage Documentation Status Lifted?

Callback-based object proxies in Python.

Example Usage

The library might have many use cases.

Asynchronous Apps

Imagine a web app that processes many requests asynchronously. A fun design we can learn from contextvars is that we can run request handlers in proper contexts with all the request-related data held in context variables instead of requiring endpoint functions to accept parameters like request. This approach heavily used in Flask, where one can access request data using a global proxy flask.request. With bare contextvars, we could achieve this:

from contextvars import ContextVar
from proxyvars import proxy
from asgi_framework import App  # some random ASGI web framework

class Request:
    args: dict[str, str]
    headers: dict[str, str]
    data: str
    # etc...

request_context: ContextVar[Request] = ContextVar("request_context")
app = App()

# the app will manage to set the appropriate Request object in the context
# before actually triggering the index() coroutine
@app.get("/")
async def echo():
    request: Request = request_context.get()
    # do something with request...
    return request.data

With proxyvars, we can skip the assignment and simply create a dynamic-lookup proxy:

# below the Request class
request = lookup_proxy(request_context)

@app.get("/")
async def echo():
    return request.data  # request delegated attribute access to request_context.get()

Flexible "Global" State

One of the crucial caveats of using global variables is reduction of modularity and flexibility. Typically we want to test our code in an easily-parametrizable environment, which becomes unobvious in case of some global state that other parts of your program might depend on. Problems arise in multi-threaded or asynchronous applications, where some global data typically should be thread- or task-local. Python offers threading.local and contextvars (respectively) for achieving these goals.

But sometimes global state is just the most convenient solution. Imagine running an app with a core App class with a config read from a file. We don't want to populate the configuration into attributes of the App instance and rather store configuration data properly in a class named Config.

We can store Config instance as an attribute config of our main class App that manages the whole application life cycle and then, everytime we need a config value, we can request the app's config attribute. With a lot of codebase, you can find this solution more and more tedious.

Creating a global app_config variable, not bound to an app, is not a good direction either though. What if you want to 2 apps with distinct configurations? Will you modify the global app_config to the proper Config object everytime?

Get That "Global" Experience With Proxyvars!

Simply create a global context variable.

app_config_var: ContextVar[Config] = ContextVar("app_config_var")
app_config: Config = lookup_proxy(app_config_var)

Now just run your tests in properly set-up contexts with app_config_var holding objects local to every thread, task or just a custom copy of the current context.

Mutable Immutables

Ever dreamt of thread-safe, mutable integers in Python? I don't think anybody did, but it's possible with lookup proxies & beloved contextvars just in case.

from contextvars import ContextVar
from proxyvars import proxy

my_iq_var = ContextVar("my_iq_var", 100)
my_iq = lookup_proxy(my_iq_var)

print(my_iq)  # 100

# Users of proxyvars typically have 200 IQ. Patch our current variable.
my_iq_var.set(200)

print(my_iq)  # 200

# Alright, let's be real. Assuming we have 200 IQ is a non-200 IQ behavior.
# Subtract 60 IQ points. We are entitled to 140 as programmers.
my_iq -= 60

print(my_iq_var.get())  # 140
print(my_iq)  # 140

This way, we got a mutable immutable. Or, more correctly, we got a proxy object that can change its state which is represented by an immutable object.

Have fun with proxyvars!

For Enterprise

Tidelift Available as part of the Tidelift Subscription.
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. Learn more here.

To report a security vulnerability, please use the Tidelift security contact.
Tidelift will coordinate the fix and disclosure.

Installation

You might simply install it with pip:

pip install proxyvars

If you use Poetry, then you might want to run:

poetry add proxyvars

For Contributors

Poetry Ruff Pre-commit

[!Note] If you use Windows, it is highly recommended to complete the installation in the way presented below through WSL2.

  1. Fork the proxyvars repository on GitHub.
  1. Install Poetry.
    Poetry is an amazing tool for managing dependencies & virtual environments, building packages and publishing them. You might use pipx to install it globally (recommended):

    pipx install poetry

    If you encounter any problems, refer to the official documentation for the most up-to-date installation instructions.

    Be sure to have Python 3.8 installed—if you use pyenv, simply run:

    pyenv install 3.8
  2. Clone your fork locally and install dependencies.

    git clone https://github.com/your-username/proxyvars path/to/proxyvars
    cd path/to/proxyvars
    poetry env use $(cat .python-version)
    poetry install

    Next up, simply activate the virtual environment and install pre-commit hooks:

    poetry shell
    pre-commit install

For more information on how to contribute, check out CONTRIBUTING.md.
Always happy to accept contributions! ❤️

Legal Info

© Copyright by Bartosz Sławecki (@bswck).
This software is licensed under the terms of MIT License.