Skip to content
This repository was archived by the owner on May 24, 2022. It is now read-only.

Commit 763f7a5

Browse files
Add user guide with more examples (#40)
1 parent 3907470 commit 763f7a5

3 files changed

Lines changed: 253 additions & 104 deletions

File tree

README.md

Lines changed: 176 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,103 +13,228 @@
1313
1414
[`latest`]: https://github.com/bocadilloproject/aiodine/tree/latest
1515

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**
1727

1828
- [Quickstart](#quickstart)
19-
- [Features](#features)
2029
- [Installation](#installation)
30+
- [User guide](#user-guide)
2131
- [FAQ](#faq)
2232
- [Changelog](#changelog)
2333

2434
## Quickstart
2535

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+
2661
```python
2762
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
2886
import typing
87+
import trio
88+
import aiodine
89+
```
2990

30-
from aiodine import call_resolved, depends
91+
### Core ideas
3192

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**.
3494

95+
A dependable is created by calling `aiodine.depends(...)`:
3596

36-
class APIResult(typing.NamedTuple):
37-
message: str
97+
```python
98+
async def cowsay(what: str) -> str:
99+
return f"Cow says {what}"
38100

101+
dependable = aiodine.depends(cowsay)
102+
```
39103

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:
44105

106+
```python
107+
print(dependable) # Dependable(func=<function cowsay at ...>)
108+
```
45109

46-
class Database:
47-
def __init__(self, url: str) -> None:
48-
self.url = url
110+
Yup, looks good.
49111

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.
50113

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:
60115

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"
61131

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}"
68135

136+
async def main() -> str:
137+
# Note that we're leaving out the 'what' argument here.
138+
return await aiodine.call_resolved(cowsay)
69139

70-
loop = asyncio.new_event_loop()
71-
loop.run_until_complete(call_resolved(main))
140+
print(trio.run(main)
72141
```
73142

74-
Output:
143+
This code will output the following:
75144

76145
```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
81150
```
82151

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:
84155

85156
```python
86-
import curio
87-
import trio
157+
await aiodine.call_resolved(cowsay, "MOO!!")
158+
```
159+
160+
It should output the following:
88161

89-
curio.run(call_resolved, (main,))
90-
trio.run(call_resolved, main)
162+
```console
163+
cowsay got what='MOO!!'
164+
Cow says MOO!!
91165
```
92166

93-
## Features
167+
### Typing support
94168

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.
96170

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:
98172

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+
```
100177

101-
- **Type checker-friendly**:
178+
```console
179+
Incompatible default for argument "what" (default has type "str", argument has type "int")
180+
```
102181

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!
104183

105-
- **Simple, transparent**:
184+
### Usage with context managers
106185

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.
108187

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
110196

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)
111229
```
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:'
113238
```
114239

115240
## FAQ

tests/test_example.py

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)