Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add LiteStar integration #9

Merged
merged 2 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ jobs:
run: just publish modern-di-fastapi
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

- if: startsWith(github.ref_name, 'litestar')
run: just publish modern-di-litestar
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
4 changes: 3 additions & 1 deletion .github/workflows/test-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ jobs:
- uses: extractions/setup-just@v2
- uses: astral-sh/setup-uv@v3
- run: uv python install ${{ matrix.python-version }}
- run: just install test
- run: |
just install-ci modern-di
just test-core
4 changes: 3 additions & 1 deletion .github/workflows/test-fastapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ jobs:
- uses: extractions/setup-just@v2
- uses: astral-sh/setup-uv@v3
- run: uv python install ${{ matrix.python-version }}
- run: just install test-fastapi
- run: |
just install-ci modern-di-fastapi
just test-fastapi
36 changes: 36 additions & 0 deletions .github/workflows/test-litestar.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: test litestar

on:
push:
branches:
- main
paths:
- 'packages/modern-di-litestar/**'
- '.github/workflows/test-litestar.yml'
pull_request:
paths:
- 'packages/modern-di-litestar/**'
- '.github/workflows/test-litestar.yml'

concurrency:
group: ${{ github.head_ref || github.run_id }} fastapi
cancel-in-progress: false

jobs:
pytest:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: astral-sh/setup-uv@v3
- run: uv python install ${{ matrix.python-version }}
- run: |
just install-ci modern-di-litestar
just test-litestar
7 changes: 7 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ install:
uv lock --upgrade
uv sync --all-extras --all-packages --frozen

install-ci package:
uv lock --upgrade
uv sync --all-extras --package {{ package }} --frozen

lint:
uv run ruff format .
uv run ruff check . --fix
Expand All @@ -23,6 +27,9 @@ test-core *args:
test-fastapi *args:
uv run --directory=packages/modern-di-fastapi pytest {{ args }}

test-litestar *args:
uv run --directory=packages/modern-di-litestar pytest {{ args }}

publish package:
rm -rf dist
uv build --package {{ package }}
Expand Down
12 changes: 11 additions & 1 deletion packages/modern-di-fastapi/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ repository = "https://github.com/modern-python/modern-di"
docs = "https://modern-di.readthedocs.io"

[dependency-groups]
dev = ["httpx", "asgi-lifespan"]
dev = [
"pytest",
"pytest-cov",
"pytest-asyncio",
"ruff",
"mypy",
"typing-extensions",
"httpx",
"asgi-lifespan",
]

[build-system]
requires = ["hatchling", "hatch-vcs"]
Expand All @@ -39,6 +48,7 @@ include = ["modern_di_fastapi"]
[tool.pytest.ini_options]
addopts = "--cov=. --cov-report term-missing"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.report]
exclude_also = ["if typing.TYPE_CHECKING:"]
6 changes: 6 additions & 0 deletions packages/modern-di-litestar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"Modern-DI-LiteStar"
==

Integration of [Modern-DI](https://github.com/modern-python/modern-di) to LiteStar

📚 [Documentation](https://modern-di.readthedocs.io)
8 changes: 8 additions & 0 deletions packages/modern-di-litestar/modern_di_litestar/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from modern_di_litestar.main import FromDI, fetch_di_container, setup_di


__all__ = [
"FromDI",
"fetch_di_container",
"setup_di",
]
55 changes: 55 additions & 0 deletions packages/modern-di-litestar/modern_di_litestar/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import dataclasses
import enum
import typing

import litestar
from litestar.di import Provide
from litestar.enums import ScopeType
from litestar.types import ASGIApp, Receive, Scope, Send
from modern_di import Container, providers
from modern_di import Scope as DIScope


T_co = typing.TypeVar("T_co", covariant=True)


def setup_di(app: litestar.Litestar, scope: enum.IntEnum = DIScope.APP) -> Container:
app.asgi_handler = make_add_request_container_middleware(
app.asgi_handler,
)
app.state.di_container = Container(scope=scope)
return app.state.di_container


def fetch_di_container(app: litestar.Litestar) -> Container:
return typing.cast(Container, app.state.di_container)


def make_add_request_container_middleware(app: ASGIApp) -> ASGIApp:
async def middleware(scope: Scope, receive: Receive, send: Send) -> None:
if scope.get("type") != ScopeType.HTTP:
await app(scope, receive, send)
return

request: litestar.Request[typing.Any, typing.Any, typing.Any] = litestar.Request(scope)
di_container = fetch_di_container(request.app)

async with di_container.build_child_container(
scope=DIScope.REQUEST, context={"request": request}
) as request_container:
request.state.di_container = request_container
await app(scope, receive, send)

return middleware


@dataclasses.dataclass(slots=True, frozen=True)
class Dependency(typing.Generic[T_co]):
dependency: providers.AbstractProvider[T_co]

async def __call__(self, request: litestar.Request[typing.Any, typing.Any, typing.Any]) -> T_co:
return await self.dependency.async_resolve(request.state.di_container)


def FromDI(dependency: providers.AbstractProvider[T_co], *, use_cache: bool = True) -> Provide: # noqa: N802
return Provide(dependency=Dependency(dependency), use_cache=use_cache)
Empty file.
53 changes: 53 additions & 0 deletions packages/modern-di-litestar/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[project]
name = "modern-di-litestar"
description = "Modern-DI integration for LiteStar"
authors = [{ name = "Artur Shiriev", email = "[email protected]" }]
requires-python = ">=3.10,<4"
license = "MIT"
readme = "README.md"
keywords = ["DI", "dependency injector", "ioc-container", "LiteStar", "python"]
classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Typing :: Typed",
"Topic :: Software Development :: Libraries",
]
dependencies = ["litestar", "modern-di"]
dynamic = ["version"]

[project.urls]
repository = "https://github.com/modern-python/modern-di"
docs = "https://modern-di.readthedocs.io"

[dependency-groups]
dev = [
"pytest",
"pytest-cov",
"pytest-asyncio",
"ruff",
"mypy",
"typing-extensions",
"httpx",
"asgi-lifespan",
]

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"
raw-options.root = "../.."
fallback-version = "0"

[tool.hatch.build]
include = ["modern_di_litestar"]

[tool.pytest.ini_options]
addopts = "--cov=. --cov-report term-missing"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.report]
exclude_also = ["if typing.TYPE_CHECKING:"]
Empty file.
28 changes: 28 additions & 0 deletions packages/modern-di-litestar/tests_litestar/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import contextlib
import typing

import litestar
import pytest
from asgi_lifespan import LifespanManager
from litestar.testing import TestClient
from modern_di_litestar import setup_di


@contextlib.asynccontextmanager
async def lifespan(app_: litestar.Litestar) -> typing.AsyncIterator[None]:
container = setup_di(app_)
async with container:
yield


@pytest.fixture
async def app() -> typing.AsyncIterator[litestar.Litestar]:
app_ = litestar.Litestar(lifespan=[lifespan], debug=True)
async with LifespanManager(app_): # type: ignore[arg-type]
yield app_


@pytest.fixture
def client(app: litestar.Litestar) -> typing.Iterator[TestClient[litestar.Litestar]]:
with TestClient(app=app) as client:
yield client
11 changes: 11 additions & 0 deletions packages/modern-di-litestar/tests_litestar/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import dataclasses


@dataclasses.dataclass(kw_only=True, slots=True)
class SimpleCreator:
dep1: str


@dataclasses.dataclass(kw_only=True, slots=True)
class DependentCreator:
dep1: SimpleCreator
63 changes: 63 additions & 0 deletions packages/modern-di-litestar/tests_litestar/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import typing

import litestar
from litestar import status_codes
from litestar.testing import TestClient
from modern_di import Scope, providers
from modern_di_litestar import FromDI

from tests_litestar.dependencies import DependentCreator, SimpleCreator


def context_adapter_function(*, request: litestar.Request[typing.Any, typing.Any, typing.Any], **_: object) -> str:
return request.method


app_factory = providers.Factory(Scope.APP, SimpleCreator, dep1="original")
request_factory = providers.Factory(Scope.REQUEST, DependentCreator, dep1=app_factory.cast)
action_factory = providers.Factory(Scope.ACTION, DependentCreator, dep1=app_factory.cast)
context_adapter = providers.ContextAdapter(Scope.REQUEST, context_adapter_function)


def test_factories(client: TestClient[litestar.Litestar], app: litestar.Litestar) -> None:
@litestar.get(
"/",
dependencies={"app_factory_instance": FromDI(app_factory), "request_factory_instance": FromDI(request_factory)},
)
async def read_root(app_factory_instance: SimpleCreator, request_factory_instance: DependentCreator) -> None:
assert isinstance(app_factory_instance, SimpleCreator)
assert isinstance(request_factory_instance, DependentCreator)
assert request_factory_instance.dep1 is not app_factory_instance

app.register(read_root)

response = client.get("/")
assert response.status_code == status_codes.HTTP_200_OK, response.text
assert response.json() is None


def test_context_adapter(client: TestClient[litestar.Litestar], app: litestar.Litestar) -> None:
@litestar.get("/", dependencies={"method": FromDI(context_adapter)})
async def read_root(method: str) -> None:
assert method == "GET"

app.register(read_root)

response = client.get("/")
assert response.status_code == status_codes.HTTP_200_OK
assert response.json() is None


def test_factories_action_scope(client: TestClient[litestar.Litestar], app: litestar.Litestar) -> None:
@litestar.get("/")
async def read_root(request: litestar.Request[typing.Any, typing.Any, typing.Any]) -> None:
request_container = request.state.di_container
with request_container.build_child_container() as action_container:
action_factory_instance = action_factory.sync_resolve(action_container)
assert isinstance(action_factory_instance, DependentCreator)

app.register(read_root)

response = client.get("/")
assert response.status_code == status_codes.HTTP_200_OK
assert response.json() is None
12 changes: 12 additions & 0 deletions packages/modern-di-litestar/tests_litestar/test_websockets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import litestar
from litestar.testing import TestClient


async def test_websocket_not_supported(client: TestClient[litestar.Litestar], app: litestar.Litestar) -> None:
async def websocket_handler(data: str) -> None:
pass

app.register(litestar.websocket_listener("/ws")(websocket_handler))

with client.websocket_connect("/ws") as websocket:
websocket.send("test")
11 changes: 11 additions & 0 deletions packages/modern-di/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ dynamic = ["version", "readme"]
repository = "https://github.com/modern-python/modern-di"
docs = "https://modern-di.readthedocs.io"

[dependency-groups]
dev = [
"pytest",
"pytest-cov",
"pytest-asyncio",
"ruff",
"mypy",
"typing-extensions",
]

[build-system]
requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"
Expand All @@ -40,6 +50,7 @@ path = "../../README.md"
[tool.pytest.ini_options]
addopts = "--cov=. --cov-report term-missing"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.report]
exclude_also = ["if typing.TYPE_CHECKING:"]
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ isort.no-lines-before = ["standard-library", "local-folder"]
[tool.pytest.ini_options]
addopts = "--cov=. --cov-report term-missing"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.report]
exclude_also = ["if typing.TYPE_CHECKING:"]