Skip to content

Commit

Permalink
Add django integration (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
maldoinc authored May 4, 2024
1 parent 6594bcd commit 83650bb
Show file tree
Hide file tree
Showing 17 changed files with 235 additions and 13 deletions.
4 changes: 3 additions & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ nav:
- Multiple containers: multiple_containers.md
- Testing: testing.md
- Integrations:
- Flask: integrations/flask.md
- Django: integrations/django.md
- FastAPI: integrations/fastapi.md
- Flask: integrations/flask.md
- Misc:
- Introduce to an existing project: introduce_to_an_existing_project.md
- Multiple db connections: multiple_registrations.md
Expand All @@ -28,6 +29,7 @@ nav:
- ParameterBag: class/parameter_bag.md
- ParameterEnum: class/parameter_enum.md
- InitializationContext: class/initialization_context.md
- django_integration: class/django_integration.md
- flask_integration: class/flask_integration.md
- fastapi_integration: class/fastapi_integration.md
repo_url: https://github.com/maldoinc/wireup
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/class/django_integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
::: wireup.integration.django.apps

12 changes: 6 additions & 6 deletions docs/pages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

Dependency injection container with a focus on developer experience, type safety and ease of use.

!!! note "New: Dependency injection for Flask"
Simplify usage in Flask applications by using the new [Flask integration](integrations/flask.md)!

* Automatically inject dependencies on views without having to manually decorate.
* Expose Flask application configuration in the container.
!!! note "New: First-party integrations"
Simplify Dependency injection using the first-party integrations

* [Django Integration](integrations/django.md)
* [Flask Integration](integrations/flask.md)
* [FastAPI Integration](integrations/fastapi.md)
## Key features

<div class="card-container">
Expand Down Expand Up @@ -67,7 +67,7 @@ Dependency injection container with a focus on developer experience, type safety
<img src="https://cdn.jsdelivr.net/gh/jdecked/[email protected]/assets/72x72/2753.png" />
Framework Agnostic
</div>
Seamlessly integrate with popular web frameworks like Django, Flask and FastAPI
Seamlessly integrate with popular web frameworks such as Django, Flask and FastAPI
to simplify dependency management.
</div>

Expand Down
67 changes: 67 additions & 0 deletions docs/pages/integrations/django.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Dependency injection for Django is available via the first-party integration wireup provides, available in
`wireup.integration.django`.

## Installation

To install the integration, add `wireup.integration.django` to `INSTALLED_APPS` and define a new `WIREUP` setting.

```python title="settings.py"
INSTALLED_APPS = [
...,
"wireup.integration.django"
]

WIREUP = {
# This is a list of top-level modules containing service registrations.
# It can be either a list of strings or module types.
"SERVICE_MODULES": ["mysite.polls.services"]
},
```


## Usage

### Define some services

```python title="mysite/polls/services/s3_manager.py"
@container.register
class S3Manager:
# Reference configuration by name.
# This is the same name this appears in settings.
def __init__(self, token: Annotated[str, Wire(parameter="S3_BUCKET_ACCESS_TOKEN")]):
self.access_token = token

def upload(self, file: File) -> None: ...
```

It is also possible to use django settings in factories.

```python title="mysite/polls/services/github_client.py"
@dataclass
class GithubClient:
api_key: str
```


```python title="mysite/polls/services/factories.py"
@container.register
def github_client_factory() -> GithubClient:
return GithubClient(settings.GH_API_KEY)
```

### Use in views
```python title="app/views.py"
@container.autowire
def upload_file(request: HttpRequest, s3_manager: S3Manager) -> HttpResponse:
return HttpResponse(...)
```

Class-based views are also supported. You autowire both `__init__` and the handler method as necessary.


For more examples see the [Wireup Django integration tests](https://github.com/maldoinc/wireup/tree/master/test/integration/django).


## Api Reference

* [django_integration](../class/django_integration.md)
12 changes: 6 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
</div>

> [!TIP]
> Simplify Dependency injection for Flask using the new
[Flask integration](https://maldoinc.github.io/wireup/latest/integrations/flask)!
>
> * Automatically inject dependencies without having to manually call autowire.
> * Expose flask application configuration in the container.
> Simplify Dependency injection using the first-party integrations
> * [Django integration](https://maldoinc.github.io/wireup/latest/integrations/django)
> * [Flask integration](https://maldoinc.github.io/wireup/latest/integrations/flask)
> * [FastAPI integration](https://maldoinc.github.io/wireup/latest/integrations/fastapi)
---

Expand All @@ -27,8 +26,9 @@
* Singleton/Transient dependencies
* Framework Agnostic
* Simplified usage in
[Django](https://maldoinc.github.io/wireup/latest/integrations/django/),
[Flask](https://maldoinc.github.io/wireup/latest/integrations/flask/)
and [FastApi](https://maldoinc.github.io/wireup/latest/integrations/fastapi/) using the first-party integrations.
and [FastAPI](https://maldoinc.github.io/wireup/latest/integrations/fastapi/) using the first-party integrations.

## 📋 Quickstart

Expand Down
Empty file.
Empty file.
9 changes: 9 additions & 0 deletions test/integration/django/factory/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from test.integration.django.service.random_service import RandomService

from django.conf import settings
from wireup import container


@container.register
def _make_random_service() -> RandomService:
return RandomService(settings.START_NUM)
Empty file.
9 changes: 9 additions & 0 deletions test/integration/django/service/greeter_impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from test.integration.django.service.greeter_interface import GreeterService

from wireup import container


@container.register
class GreeterServiceImpl(GreeterService):
def greet(self, name: str) -> str:
return f"Hello {name}"
10 changes: 10 additions & 0 deletions test/integration/django/service/greeter_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import abc

from wireup import container


@container.abstract
class GreeterService(abc.ABC):
@abc.abstractmethod
def greet(self, name: str) -> str:
raise NotImplementedError
6 changes: 6 additions & 0 deletions test/integration/django/service/random_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class RandomService:
def __init__(self, num: int) -> None:
self.num = num

def get_random(self) -> int:
return self.num
40 changes: 40 additions & 0 deletions test/integration/django/test_django_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import os
import sys
import unittest
from test.integration.django import view

import django
from django.test import Client
from django.urls import path

INSTALLED_APPS = ["wireup.integration.django"]
DEBUG = True
ROOT_URLCONF = sys.modules[__name__]
WIREUP = {"SERVICE_MODULES": ["test.integration.django.service", "test.integration.django.factory"]}
SECRET_KEY = "not_actually_a_secret" # noqa: S105
START_NUM = 4

urlpatterns = [
path(r"", view.index),
path(r"classbased", view.RandomNumberView.as_view()),
]


class TestDjango(unittest.TestCase):
def setUp(self):
os.environ["DJANGO_SETTINGS_MODULE"] = "test.integration.django.test_django_integration"
django.setup()

def test_django_thing(self):
c = Client()
res = c.get("/?name=World")

self.assertEqual(res.status_code, 200)
self.assertEqual(res.content.decode("utf8"), "Hello World! Debug = True. Your lucky number is 4")

def test_get_random(self):
c = Client()
res = c.get("/classbased?name=Test")

self.assertEqual(res.status_code, 200)
self.assertEqual(res.content.decode("utf8"), "Hello Test! Debug = True. Your lucky number is 4")
44 changes: 44 additions & 0 deletions test/integration/django/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from test.integration.django.service.greeter_interface import GreeterService
from test.integration.django.service.random_service import RandomService

from django.http import HttpRequest, HttpResponse
from django.views import View
from typing_extensions import Annotated
from wireup import Wire, container


@container.autowire
def index(
request: HttpRequest,
greeter: GreeterService,
is_debug: Annotated[bool, Wire(param="DEBUG")],
random_service: RandomService,
) -> HttpResponse:
name = request.GET.get("name")
greeting = greeter.greet(name)

return HttpResponse(f"{greeting}! Debug = {is_debug}. Your lucky number is {random_service.get_random()}")


class RandomNumberView(View):
@container.autowire
def __init__(
self,
greeter: GreeterService,
is_debug: Annotated[bool, Wire(param="DEBUG")],
random_service: RandomService,
) -> None:
self.random_service = random_service
self.is_debug = is_debug
self.greeter = greeter

def get(
self,
request: HttpRequest,
) -> HttpResponse:
name = request.GET.get("name")
greeting = self.greeter.greet(name)

return HttpResponse(
f"{greeting}! Debug = {self.is_debug}. Your lucky number is {self.random_service.get_random()}"
)
10 changes: 10 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ requires =
env_list =
py38-flask3
py38-fastapi
py38-django

[testenv:py38-flask3]
description = Test flask integration
Expand All @@ -24,3 +25,12 @@ commands =
make install
python -m unittest test/integration/test_fastapi_integration.py

[testenv:py38-django]
description = Test django integration
allowlist_externals = make
deps =
django
commands =
make install
python -m unittest test/integration/django/test_django_integration.py

Empty file.
23 changes: 23 additions & 0 deletions wireup/integration/django/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

import importlib

from django.apps import AppConfig
from django.conf import settings

from wireup import container, warmup_container


class WireupConfig(AppConfig):
"""Integrate wireup with Django."""

name = "wireup.integration.django"

def ready(self) -> None: # noqa: D102
service_modules = settings.WIREUP.get("SERVICE_MODULES", [])

for entry in dir(settings):
if not entry.startswith("__") and hasattr(settings, entry):
container.params.put(entry, getattr(settings, entry))

warmup_container(container, [importlib.import_module(m) if isinstance(m, str) else m for m in service_modules])

0 comments on commit 83650bb

Please sign in to comment.