Skip to content

Commit

Permalink
doc: add example of composition root pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
adriangb committed Apr 15, 2022
1 parent 1111b4d commit 8f65081
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 1 deletion.
34 changes: 34 additions & 0 deletions docs/advanced/dependencies/composition-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Controlling the composition root

In dependency injection technical jargon, the "composition root" is a single logical place (function, module, etc.) where all of your dependendencies are "composed" together and abstractions are bound to concrete implementations.

You can acheive this in Xpresso if your are willing to take some control of application initialization.

In many cases, this will let you cut out intermediary dependencies (e.g. a dependency to get a database connection or load a config from the environment): you can load your config, create your database connection and bind your repos/DAOs so that your application never has to know about a config or even what database backend it is using.

```python
--8<-- "docs_src/advanced/dependencies/tutorial_006.py"
```

Notice that we didn't have to add a verbose `Depends(...)` to our endpoint function since we are wiring `WordsRepo` up in our composition root.

This pattern also lends iteself natually to _depending on abstractions_: because you aren't forced to specify how `WordsRepo` should be built, it can be an abstract interface (`SupportsWordsRepo`, using `typing.Protocol` or `abc.ABC`), leaving you with a clean and testable endpoint handler that has no mention of the concrete implementation of `SupportsWordsRepo` that will be used at runtime

## Running an ASIG server programmatically

If you are running your ASGI server programmatically you have control of the event loop, allowing you to intialize arbitrarily complex dependencies (for example, a database connection that requires an async context manager).

This also has the side effect of making ASGI lifespans in redundant since you can do anything a lifespan can yourself before starting the ASGI server.

Here is an example of this pattern using Uvicorn

```python
--8<-- "docs_src/advanced/dependencies/tutorial_007.py"
```

There are many variations to this pattern, you should try different arrangements to find one that best fits your use case.
For example, you could:

- Splitting out your aggregate root into a `build_app()` function
- Mix this manual wiring in the aggregate root with use of `Depends()`
- Use Uvicorn's `--factory` CLI parameter or `uvicorn.run(..., factory=True)` if you'd like to wire up your dependencies in a composition root but don't need to take control of the event loop or need to run under Gunicorn.
49 changes: 49 additions & 0 deletions docs_src/advanced/dependencies/tutorial_006.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import sqlite3
from dataclasses import dataclass
from typing import List

from xpresso import App, FromJson, Path


class SupportsWordsRepo:
def add_word(self, word: str) -> None:
raise NotImplementedError


@dataclass
class SQLiteWordsRepo(SupportsWordsRepo):
conn: sqlite3.Connection

def add_word(self, word: str) -> None:
with self.conn:
self.conn.execute("SELECT ?", (word,))


def add_word(repo: SupportsWordsRepo, word: FromJson[str]) -> str:
repo.add_word(word)
return word


routes = [Path("/words/", post=add_word)]


def create_app() -> App:
conn = sqlite3.connect(":memory:")
repo = SQLiteWordsRepo(conn)
app = App(routes)
app.dependency_overrides[SupportsWordsRepo] = lambda: repo
return app


def test_add_word_endpoint() -> None:
# this demonstrates how easy it is to swap
# out an implementation with this pattern
words: List[str] = []

class TestWordsRepo(SupportsWordsRepo):
def add_word(self, word: str) -> None:
words.append(word)

add_word(TestWordsRepo(), "hello")

assert words == ["hello"]
12 changes: 12 additions & 0 deletions docs_src/advanced/dependencies/tutorial_007.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import asyncpg # type: ignore[import]
import uvicorn # type: ignore[import]

from xpresso import App


async def main() -> None:
app = App()
async with asyncpg.create_pool(...) as pool: # type: ignore
app.dependency_overrides[asyncpg.Pool] = lambda: pool
server = uvicorn.Server(uvicorn.Config(app))
await server.serve()
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ nav:
- Caching: "advanced/dependencies/caching.md"
- Accessing Responses: "advanced/dependencies/responses.md"
- Dependency Overrides: "advanced/dependencies/overrides.md"
- Composition Root: "advanced/dependencies/composition-root.md"
- Binders: advanced/binders.md
- Proxies and URL paths: advanced/proxies-root-path.md
- Body Unions: advanced/body-union.md
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "xpresso"
version = "0.38.1"
version = "0.38.2"
description = "A developer centric, performant Python web framework"
authors = ["Adrian Garcia Badaracco <[email protected]>"]
readme = "README.md"
Expand Down
20 changes: 20 additions & 0 deletions tests/test_docs/advanced/dependencies/test_tutorial_006.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pytest
from httpx import AsyncClient

from docs_src.advanced.dependencies.tutorial_006 import (
create_app,
test_add_word_endpoint,
)


def test_example_test() -> None:
test_add_word_endpoint()


@pytest.mark.anyio
async def test_against_sqlite() -> None:
app = create_app()
async with AsyncClient(app=app, base_url="http://example.com") as client:
resp = await client.post("/words/", json="foo") # type: ignore
assert resp.status_code == 200
assert resp.json() == "foo"

0 comments on commit 8f65081

Please sign in to comment.