|
13 | 13 |
|
14 | 14 | [`latest`]: https://github.com/bocadilloproject/aiodine/tree/latest |
15 | 15 |
|
16 | | -aiodine provides a simple but powerful async-first [dependency injection][di] mechanism for Python 3.6+ programs. |
| 16 | +aiodine provides a simple but powerful [dependency injection][di] mechanism for Python 3.6+ asynchronous programs. |
| 17 | + |
| 18 | +**Features** |
| 19 | + |
| 20 | +- Simple and elegant API. |
| 21 | +- Setup/teardown logic via async context managers. |
| 22 | +- Dependency caching (_coming soon_). |
| 23 | +- Great typing support. |
| 24 | +- Compatible with asyncio, trio and curio. |
| 25 | + |
| 26 | +**Contents** |
17 | 27 |
|
18 | 28 | - [Quickstart](#quickstart) |
19 | | -- [Features](#features) |
20 | 29 | - [Installation](#installation) |
| 30 | +- [User guide](#user-guide) |
21 | 31 | - [FAQ](#faq) |
22 | 32 | - [Changelog](#changelog) |
23 | 33 |
|
24 | 34 | ## Quickstart |
25 | 35 |
|
| 36 | +```python |
| 37 | +import aiodine |
| 38 | + |
| 39 | +async def moo() -> str: |
| 40 | + print("What does the cow say?") |
| 41 | + return "moo!" |
| 42 | + |
| 43 | +async def cowsay(what: str = aiodine.depends(moo)): |
| 44 | + print(f"Going to say {what!r}...") |
| 45 | + print(f"Cow says {what}") |
| 46 | + |
| 47 | +import trio |
| 48 | +trio.run(aiodine.call_resolved, cowsay) |
| 49 | +``` |
| 50 | + |
| 51 | +Output: |
| 52 | + |
| 53 | +```console |
| 54 | +What does the cow say? |
| 55 | +Going to say 'moo!'... |
| 56 | +Cow says moo! |
| 57 | +``` |
| 58 | + |
| 59 | +Running with asyncio or curio instead: |
| 60 | + |
26 | 61 | ```python |
27 | 62 | import asyncio |
| 63 | +# Python 3.7+ |
| 64 | +asyncio.run(aiodine.call_resolved(main)) |
| 65 | +# Python 3.6 |
| 66 | +loop = asyncio.get_event_loop() |
| 67 | +loop.run_until_complete(aiodine.call_resolved(main)) |
| 68 | + |
| 69 | +import curio |
| 70 | +curio.run(aiodine.call_resolved, (main,)) |
| 71 | +``` |
| 72 | + |
| 73 | +## Installation |
| 74 | + |
| 75 | +``` |
| 76 | +pip install aiodine |
| 77 | +``` |
| 78 | + |
| 79 | +## User guide |
| 80 | + |
| 81 | +This section will be using [trio](https://github.com/python-trio/trio) as a concurrency library. Feel free to adapt the code for asyncio or curio. |
| 82 | + |
| 83 | +Let's start with some imports... |
| 84 | + |
| 85 | +```python |
28 | 86 | import typing |
| 87 | +import trio |
| 88 | +import aiodine |
| 89 | +``` |
29 | 90 |
|
30 | | -from aiodine import call_resolved, depends |
| 91 | +### Core ideas |
31 | 92 |
|
32 | | -# On 3.7+, you can use 'from contextlib import asynccontextmanager' directly. |
33 | | -from aiodine.compat import asynccontextmanager |
| 93 | +The core concept in aiodine is that of a **dependable**. |
34 | 94 |
|
| 95 | +A dependable is created by calling `aiodine.depends(...)`: |
35 | 96 |
|
36 | | -class APIResult(typing.NamedTuple): |
37 | | - message: str |
| 97 | +```python |
| 98 | +async def cowsay(what: str) -> str: |
| 99 | + return f"Cow says {what}" |
38 | 100 |
|
| 101 | +dependable = aiodine.depends(cowsay) |
| 102 | +``` |
39 | 103 |
|
40 | | -# Simple function-based dependable that returns a value. |
41 | | -async def make_api_call() -> APIResult: |
42 | | - await asyncio.sleep(0.1) # Simulate an HTTP request… |
43 | | - return APIResult(message="Hello, world!") |
| 104 | +Let's inspect what the dependable refers to: |
44 | 105 |
|
| 106 | +```python |
| 107 | +print(dependable) # Dependable(func=<function cowsay at ...>) |
| 108 | +``` |
45 | 109 |
|
46 | | -class Database: |
47 | | - def __init__(self, url: str) -> None: |
48 | | - self.url = url |
| 110 | +Yup, looks good. |
49 | 111 |
|
| 112 | +A dependable can't do much on its own — we need to use it along with `call_resolved()`, the main entry point in aiodine. |
50 | 113 |
|
51 | | -# Context manager-based dependables are supported too. |
52 | | -@asynccontextmanager |
53 | | -async def get_db() -> typing.AsyncIterator[Database]: |
54 | | - db = Database(url="sqlite://:memory:") |
55 | | - print("Connecting to database") |
56 | | - try: |
57 | | - yield db |
58 | | - finally: |
59 | | - print("Releasing database connection") |
| 114 | +By default, `call_resolved()` acts as a proxy, i.e. it passes any positional and keyword arguments along to the given function: |
60 | 115 |
|
| 116 | +```python |
| 117 | +async def main() -> str: |
| 118 | + return await aiodine.call_resolved(cowsay, what="moo") |
| 119 | + |
| 120 | +assert trio.run(main) == "Cow says moo" |
| 121 | +``` |
| 122 | + |
| 123 | +But `call_resolved()` can also _inject_ dependencies into the function it is given. Put differently, `call_resolved()` does all the heavy lifting to provide the function with the arguments it needs. |
| 124 | + |
| 125 | +```python |
| 126 | +async def moo() -> str: |
| 127 | + print("Evaluating 'moo()'...") |
| 128 | + await trio.sleep(0.1) # Simulate some I/O... |
| 129 | + print("Ready!") |
| 130 | + return "moo" |
61 | 131 |
|
62 | | -async def main( |
63 | | - data: APIResult = depends(make_api_call), db: Database = depends(get_db) |
64 | | -) -> None: |
65 | | - print("Fetched:", data) |
66 | | - print("Ready to fetch rows in:", db.url) |
67 | | - # ... |
| 132 | +async def cowsay(what: str = aiodine.depends(moo)) -> str: |
| 133 | + print(f"cowsay got what={what!r}") |
| 134 | + return f"Cow says {what}" |
68 | 135 |
|
| 136 | +async def main() -> str: |
| 137 | + # Note that we're leaving out the 'what' argument here. |
| 138 | + return await aiodine.call_resolved(cowsay) |
69 | 139 |
|
70 | | -loop = asyncio.new_event_loop() |
71 | | -loop.run_until_complete(call_resolved(main)) |
| 140 | +print(trio.run(main) |
72 | 141 | ``` |
73 | 142 |
|
74 | | -Output: |
| 143 | +This code will output the following: |
75 | 144 |
|
76 | 145 | ```console |
77 | | -Connecting to database |
78 | | -Fetched: APIResult(message='Hello, world!') |
79 | | -Ready to fetch rows in: sqlite://:memory: |
80 | | -Releasing database connection |
| 146 | +Evaluating 'moo()'... |
| 147 | +Done! |
| 148 | +cowsay got what='moo' |
| 149 | +Cow says moo |
81 | 150 | ``` |
82 | 151 |
|
83 | | -**Tip**: aiodine does not rely on asyncio directly — it can run on curio or trio too: |
| 152 | +We can still pass arguments from the outside, in which case aiodine won't need to resolve anything. |
| 153 | + |
| 154 | +For example, replace the content of `main()` with: |
84 | 155 |
|
85 | 156 | ```python |
86 | | -import curio |
87 | | -import trio |
| 157 | +await aiodine.call_resolved(cowsay, "MOO!!") |
| 158 | +``` |
| 159 | + |
| 160 | +It should output the following: |
88 | 161 |
|
89 | | -curio.run(call_resolved, (main,)) |
90 | | -trio.run(call_resolved, main) |
| 162 | +```console |
| 163 | +cowsay got what='MOO!!' |
| 164 | +Cow says MOO!! |
91 | 165 | ``` |
92 | 166 |
|
93 | | -## Features |
| 167 | +### Typing support |
94 | 168 |
|
95 | | -aiodine is: |
| 169 | +You may have noticed that we used type annotations in the code snippets above. If you run the snippets through a static type checker such as [mypy](http://mypy-lang.org/), you shouldn't get any errors. |
96 | 170 |
|
97 | | -- **Editor-friendly**: |
| 171 | +On the other hand, if you change the type hint of `what` to, for example, `int`, then mypy will complain because types don't match anymore: |
98 | 172 |
|
99 | | -In the above example, the `data: Result` annotation allows your editor to provide auto-completion. |
| 173 | +```python |
| 174 | +async def cowsay(what: int = aiodine.depends(moo)) -> str: |
| 175 | + return f"Cow says {what}" |
| 176 | +``` |
100 | 177 |
|
101 | | -- **Type checker-friendly**: |
| 178 | +```console |
| 179 | +Incompatible default for argument "what" (default has type "str", argument has type "int") |
| 180 | +``` |
102 | 181 |
|
103 | | -Thanks to the `-> Result` annotation on `make_api_call()`, static type checkers can enforce the consistency of types between the `data` parameter and what `make_api_call()` returns. For example, if we change `data: Result` to `data: dict`, `mypy` will be able to tell that something's wrong. |
| 182 | +All of this is by design: aiodine tries to be as type checker-friendly as it can. It even has a test for the above situation! |
104 | 183 |
|
105 | | -- **Simple, transparent**: |
| 184 | +### Usage with context managers |
106 | 185 |
|
107 | | -No complicated concepts, no funky decorators. It just works. |
| 186 | +Sometimes, the dependable has some setup and/or teardown logic associated with it. This is typically the case for most I/O resources such as sockets, files, or database connections. |
108 | 187 |
|
109 | | -## Installation |
| 188 | +This is why `aiodine.depends()` also accepts asynchronous context managers: |
| 189 | + |
| 190 | +```python |
| 191 | +import typing |
| 192 | +import aiodine |
| 193 | + |
| 194 | +# On 3.7+, use `from contextlib import asynccontextmanager`. |
| 195 | +from aiodine.compat import asynccontextmanager |
110 | 196 |
|
| 197 | + |
| 198 | +class Database: |
| 199 | + def __init__(self, url: str) -> None: |
| 200 | + self.url = url |
| 201 | + |
| 202 | + async def connect(self) -> None: |
| 203 | + print(f"Connecting to {self.url!r}") |
| 204 | + |
| 205 | + async def fetchall(self) -> typing.List[dict]: |
| 206 | + print("Fetching data...") |
| 207 | + return [{"id": 1}] |
| 208 | + |
| 209 | + async def disconnect(self) -> None: |
| 210 | + print(f"Releasing connection to {self.url!r}") |
| 211 | + |
| 212 | + |
| 213 | +@asynccontextmanager |
| 214 | +async def get_db() -> typing.AsyncIterator[Database]: |
| 215 | + db = Database(url="sqlite://:memory:") |
| 216 | + await db.connect() |
| 217 | + try: |
| 218 | + yield db |
| 219 | + finally: |
| 220 | + await db.disconnect() |
| 221 | + |
| 222 | + |
| 223 | +async def main(db: Database = aiodine.depends(get_db)) -> None: |
| 224 | + rows = await db.fetchall() |
| 225 | + print("Rows:", rows) |
| 226 | + |
| 227 | + |
| 228 | +trio.run(aiodine.call_resolved, main) |
111 | 229 | ``` |
112 | | -pip install aiodine |
| 230 | + |
| 231 | +This code will output the following: |
| 232 | + |
| 233 | +```console |
| 234 | +Connecting to 'sqlite://:memory:' |
| 235 | +Fetching data... |
| 236 | +Rows: [{'id': 1}] |
| 237 | +Releasing connection to 'sqlite://:memory:' |
113 | 238 | ``` |
114 | 239 |
|
115 | 240 | ## FAQ |
|
0 commit comments