Metadata-Version: 2.4
Name: nodrill
Version: 0.1.0
Summary: Scoped context for Python call trees. Provide a value once, use it anywhere below
Project-URL: Homepage, https://github.com/paqstd-dev/nodrill
Project-URL: Documentation, https://nodrill.readthedocs.io/
Project-URL: Source, https://github.com/paqstd-dev/nodrill
Project-URL: Issues, https://github.com/paqstd-dev/nodrill/issues
Project-URL: Changelog, https://github.com/paqstd-dev/nodrill/releases
Author: Pavel Kutsenko
License-Expression: MIT
License-File: LICENSE
Keywords: context,contextvars,dependency-injection,di,provider,scope
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# nodrill

[![CI](https://github.com/paqstd-dev/nodrill/actions/workflows/ci.yml/badge.svg)](https://github.com/paqstd-dev/nodrill/actions/workflows/ci.yml)
[![Coverage](https://codecov.io/gh/paqstd-dev/nodrill/graph/badge.svg)](https://codecov.io/gh/paqstd-dev/nodrill)
[![PyPI](https://img.shields.io/pypi/v/nodrill)](https://pypi.org/project/nodrill/)
[![Python](https://img.shields.io/pypi/pyversions/nodrill.svg)](https://pypi.org/project/nodrill/)

nodrill gives a call tree a shared, scoped context. Values set in a `provider` block are visible to any function below it through `use()`, without being passed through the signatures in between.
It is built on `contextvars`, so lookups are thread-safe and asyncio-task-safe, and it has no dependencies.

```python
from dataclasses import dataclass

from nodrill import provider, use


@dataclass
class RequestScope:
    user_id: int
    db: str


def handle_request():
    with provider(RequestScope(user_id=42, db="postgres://...")):
        render_page()


def render_page():
    return render_sidebar()  # knows nothing about RequestScope


def render_sidebar():
    scope = use(RequestScope)  # inferred as RequestScope
    return f"{scope.user_id} @ {scope.db}"
```

String names work too, when a typed key is more than the case needs.

```python
with provider("app", db=engine) as ctx:
    ctx.user_id = 42
    handle()  # any callee reads use("app").db
```

## What you get

- Typed keys: `use(Config)` is inferred as `Config`, under both mypy and pyright.
- String namespaces for the values that do not deserve a class, as above.
- `@inject` to declare the dependency in the signature and still pass it explicitly in a test.
- `set_default(Config, factory)` for code that has to run outside any provider.
- Threads and asyncio: tasks inherit the context, `wrap` and `Executor` carry it into threads.
- `frozen=True` hands consumers a read-only view while the block keeps a writable object.
- `isolate()` to give a test fresh context state and roll everything back after it.
- No dependencies, Python 3.10 and up, and a public API of fifteen names.

## Cost

A lookup is one dict read on a single `ContextVar`, and nothing is constructed, resolved or cached along the way.
The first four rows are one function doing one read, reached four ways, so they can be read against each other and against the parameter they replace.

<!-- benchmarks generated by benchmarks/bench.py, do not edit by hand -->

| operation                                              |  ns |   × |
| ------------------------------------------------------ | --: | --: |
| one read in a function, value passed in as a parameter |  25 | 1.0 |
| the same read through `use()`                          |  64 | 2.6 |
| the same read through `@inject`                        |  78 | 3.2 |
| the same read through a `frozen=True` provider         | 124 | 5.0 |
| `use(Config)` on its own, without the call frame       |  50 | 2.0 |
| bare `ContextVar.get()`, for reference                 |  18 | 0.7 |
| `with provider(...)`, enter and exit                   | 576 |  23 |
| the same with 8 providers already open                 | 762 |  31 |
| `wrap(fn)()`, per call into a thread                   | 654 |  26 |

CPython 3.14.5, arm64.

<!-- /benchmarks -->

The `×` column is against handing the value in as a parameter, which is the alternative nodrill removes from the signatures in between.
Reading through `use()` costs a little over the parameter it replaces; `@inject` costs more, because it fills the argument before the body runs; `frozen=True` adds a proxy hop to every attribute the consumer touches.
A request that reads a provided value a hundred times spends microseconds in nodrill, against hundreds of microseconds for one round trip to a database.

Entering a provider is the expensive end, because it copies the registry so that sibling tasks stay isolated.
That copy is proportional to how many providers are open, which the last two rows price at one and at eight, and it happens once per scope rather than once per lookup.

The absolute numbers move with the machine, and the ratios are the part worth reading.
Regenerate with `make bench ARGS=--write`, which measures on your machine and rewrites the block above.
A rerun lands within a few percent, so read the digits as approximate; nothing here runs in CI, because timing on a shared runner measures the runner.

## Install

```bash
pip install nodrill
```

Python 3.10 or newer.

## Documentation

Full documentation is at <https://nodrill.readthedocs.io/>.

Start with the [tutorial](https://nodrill.readthedocs.io/en/latest/content/intro/quickstart.html), which covers the whole library in about ten minutes, over typed class keys, fallbacks for a miss, `@inject`, threads and asyncio, frozen providers, and testing.

## Contributing

Bug reports and small focused pull requests are welcome. See [CONTRIBUTING.md](https://github.com/paqstd-dev/nodrill/blob/main/.github/CONTRIBUTING.md).
`make install` sets up the environment, and `make` runs the same gate CI does.

Security issues go through a [private advisory](https://github.com/paqstd-dev/nodrill/security/advisories/new) rather than the issue tracker.

## License

MIT
