Skip to content

Commit

Permalink
Merge branch 'refs/heads/master' into django-integration
Browse files Browse the repository at this point in the history
  • Loading branch information
maldoinc committed May 4, 2024
2 parents 0851dcb + 6594bcd commit c0b0b65
Show file tree
Hide file tree
Showing 26 changed files with 234 additions and 99 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ check-fmt:
.venv/bin/ruff format . --check

check-ruff:
.venv/bin/ruff check wireup $(RUFF_ARGS)
.venv/bin/ruff check wireup test $(RUFF_ARGS)

check-mypy:
.venv/bin/mypy wireup --strict
Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ nav:
- Flask: integrations/flask.md
- Misc:
- Introduce to an existing project: introduce_to_an_existing_project.md
- Multiple db connections: multiple_registrations.md
- Demo application: demo_app.md
- Versioning: versioning.md
- API Reference:
Expand Down
70 changes: 70 additions & 0 deletions docs/pages/multiple_registrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Wireup supports registering the same class multiple times under different qualifiers through the use of factories.

A common use case for this would be to have multiple services connected to resources of the same underlying
type, such as maintaining multiple database connections: a main and a readonly copy.

## Example

Assume an application which has two databases set up: A main one and a readonly replica. In these scenarios the main
connection is used for writes while the readonly connection will be used to perform reads.

### Service registration via factories

```python title="db_service.py"
from typing import Annotated
from wireup import container, Wire

# Define a class that holds the base methods for interacting with the db.
class DatabaseService:
def __init__(self, dsn: str) -> None:
self.__connection = ...

def query(self) -> ...:
return self.__connection.query(...)


# Define a factory which creates and registers the service interacting with the main db.
# Register this directly without using a qualifier, this will be injected
# when services depend on DatabaseService.
@container.register
def main_db_connection_factory(
dsn: Annotated[str, Wire(param="APP_DB_DSN")]
) -> DatabaseService:
return DatabaseService(dsn)

# This factory registers the function using the qualifier "read"
# and requests the parameter that corresponds to the read replica DSN.
@container.register(qualifier="read")
def read_db_connection_factory(
dsn: Annotated[str, Wire(param="APP_READ_DB_DSN")]
) -> DatabaseService:
return DatabaseService(dsn)
```

### Usage

```python title="thing_repository.py"
from dataclasses import dataclass
from wireup import container

@container.register
@dataclass
class ThingRepository:
# Main db connection can be injected directly as it is registered
# without a qualifier, this makes it the "default" implementation.
main_db_connection: DatabaseService

# To inject the read connection the qualifier must be specified.
read_db_connection: Annotated[DatabaseService, Wire(qualifier="read")]


def create_thing(self, ...) -> None:
return self.main_db_connection...

def find_by_id(self, pk: int) -> Thing:
return self.read_db_connection...
```




5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wireup"
version = "0.7.1"
version = "0.7.2"
description = "Python Dependency Injection Library"
authors = ["Aldo Mateli <[email protected]>"]
license = "MIT"
Expand Down Expand Up @@ -84,6 +84,9 @@ lint.ignore = [
"COM812",
"ISC001"
]
[tool.ruff.lint.per-file-ignores]
"test/*" = ["D", "ANN", "PT", "SLF001", "T201", "EM101", "TRY", "FA100", "B008", "RUF009", "F401", "SIM117"]


[build-system]
requires = ["poetry-core"]
Expand Down
3 changes: 1 addition & 2 deletions test/integration/django/factory/factories.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from django.conf import settings

from test.integration.django.service.random_service import RandomService

from django.conf import settings
from wireup import container


Expand Down
1 change: 1 addition & 0 deletions test/integration/django/service/greeter_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

@container.abstract
class GreeterService(abc.ABC):
@abc.abstractmethod
def greet(self, name: str) -> str:
raise NotImplementedError
2 changes: 1 addition & 1 deletion test/integration/django/test_django_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
DEBUG = True
ROOT_URLCONF = sys.modules[__name__]
WIREUP = {"SERVICE_MODULES": ["test.integration.django.service", "test.integration.django.factory"]}
SECRET_KEY = "secret"
SECRET_KEY = "not_actually_a_secret" # noqa: S105
START_NUM = 4

urlpatterns = [
Expand Down
29 changes: 25 additions & 4 deletions test/integration/test_fastapi_integration.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import unittest
from test.unit.services.no_annotations.random.random_service import RandomService

from fastapi import FastAPI, Depends
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from typing_extensions import Annotated

from test.unit.services.no_annotations.random.random_service import RandomService
from wireup import Wire, ParameterBag, DependencyContainer
from wireup import DependencyContainer, ParameterBag, Wire
from wireup.errors import UnknownServiceRequestedError
from wireup.integration.fastapi_integration import wireup_init_fastapi_integration

Expand All @@ -18,8 +17,18 @@ def setUp(self):

def test_injects_service(self):
self.container.register(RandomService)
is_lucky_number_invoked = False

def get_lucky_number() -> int:
nonlocal is_lucky_number_invoked

# Raise if this will be invoked more than once
# That would be the case if wireup also "unwraps" and tries
# to resolve dependencies it doesn't own.
if is_lucky_number_invoked:
raise Exception("Lucky Number was already invoked")

is_lucky_number_invoked = True
return 42

@self.app.get("/")
Expand All @@ -33,6 +42,18 @@ async def target(
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"number": 4, "lucky_number": 42})

def test_injects_parameters(self):
self.container.params.put("foo", "bar")

@self.app.get("/")
async def target(foo: Annotated[str, Wire(param="foo")], foo_foo: Annotated[str, Wire(expr="${foo}-${foo}")]):
return {"foo": foo, "foo_foo": foo_foo}

wireup_init_fastapi_integration(self.app, dependency_container=self.container, service_modules=[])
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"foo": "bar", "foo_foo": "bar-bar"})

def test_raises_on_unknown_service(self):
@self.app.get("/")
async def target(_unknown_service: Annotated[unittest.TestCase, Wire()]):
Expand Down
4 changes: 1 addition & 3 deletions test/integration/test_flask_integration.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import unittest
from dataclasses import dataclass

from test.fixtures import FooBar, FooBase
from test.unit.services.no_annotations.random.random_service import RandomService

from flask import Flask
from typing_extensions import Annotated

from test.unit.services.no_annotations.random.random_service import RandomService
from wireup import DependencyContainer, ParameterBag, Wire
from wireup.integration.flask_integration import wireup_init_flask_integration

Expand Down
9 changes: 5 additions & 4 deletions test/performance_test/test_inject_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import timeit
import unittest
from dataclasses import dataclass
from typing import Optional

from typing_extensions import Annotated
from wireup import DependencyContainer, ParameterBag, Wire
Expand Down Expand Up @@ -60,10 +61,10 @@ def autowired(
a: A,
b: B,
c: C,
_d: unittest.TestCase = None,
_e: unittest.TestCase = None,
_f: unittest.TestCase = None,
_g: unittest.TestCase = None,
_d: Optional[unittest.TestCase] = None,
_e: Optional[unittest.TestCase] = None,
_f: Optional[unittest.TestCase] = None,
_g: Optional[unittest.TestCase] = None,
):
return c.c() + b.b() + a.a()

Expand Down
1 change: 0 additions & 1 deletion test/unit/services/no_annotations/foo/foo_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from dataclasses import dataclass

from test.unit.services.no_annotations.db_service import DbService


Expand Down
53 changes: 28 additions & 25 deletions test/unit/test_container.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import datetime
import functools
import unittest
from dataclasses import dataclass
from test.fixtures import Counter, FooBar, FooBase, FooBaz
from unittest.mock import Mock, patch

from typing_extensions import Annotated

from test.unit import services
from test.unit.services.no_annotations.random.random_service import RandomService
from test.unit.services.no_annotations.random.truly_random_service import TrulyRandomService
from typing import Optional
from unittest.mock import Mock, patch

from typing_extensions import Annotated
from wireup import ServiceLifetime, Wire, register_all_in_module, wire
from wireup.errors import (
DuplicateQualifierForInterfaceError,
Expand Down Expand Up @@ -72,17 +73,6 @@ def __init__(
self.assertEqual(svc.connection_str, "sqlite://memory")
self.assertEqual(svc.cache_dir, "/var/cache/etc")

def test_inject_param(self):
result = wire(param="value")
self.assertIsInstance(result, ParameterWrapper)
self.assertEqual(result.param, "value")

def test_inject_expr(self):
result = wire(expr="some ${param}")
self.assertIsInstance(result, ParameterWrapper)
self.assertIsInstance(result.param, TemplatedString)
self.assertEqual(result.param.value, "some ${param}")

@patch("importlib.import_module")
def test_inject_fastapi_dep(self, mock_import_module):
mock_import_module.return_value = Mock(Depends=Mock())
Expand All @@ -99,7 +89,7 @@ def inner(random: Annotated[RandomService, Wire()]):

def test_inject_using_annotated_empty_wire_fails_to_inject_unknown(self):
@self.container.autowire
def inner(random: Annotated[unittest.TestCase, Wire()]): ...
def inner(_random: Annotated[unittest.TestCase, Wire()]): ...

with self.assertRaises(UnknownServiceRequestedError) as context:
inner()
Expand Down Expand Up @@ -307,7 +297,7 @@ def test_register_same_qualifier_should_raise(self):

def test_qualifier_raises_wire_called_on_unknown_type(self):
@self.container.autowire
def inner(sub1: FooBase = wire(qualifier="sub1")): ...
def inner(_sub1: FooBase = wire(qualifier="sub1")): ...

self.container.abstract(FooBase)
with self.assertRaises(UnknownQualifiedServiceRequestedError) as context:
Expand All @@ -321,7 +311,7 @@ def inner(sub1: FooBase = wire(qualifier="sub1")): ...

def test_inject_abstract_directly_raises(self):
@self.container.autowire
def inner(sub1: FooBase): ...
def inner(_sub1: FooBase): ...

self.container.abstract(FooBase)
self.container.register(FooBar, qualifier="foobar")
Expand All @@ -336,7 +326,7 @@ def inner(sub1: FooBase): ...

def test_inject_abstract_directly_with_no_impls_raises(self):
@self.container.autowire
def inner(sub1: FooBase): ...
def inner(_sub1: FooBase): ...

self.container.abstract(FooBase)
with self.assertRaises(Exception) as context:
Expand All @@ -353,14 +343,14 @@ def test_register_with_qualifier_fails_when_invoked_without(self):
class RegisterWithQualifierClass: ...

@self.container.autowire
def inner(foo: RegisterWithQualifierClass): ...
def inner(_foo: RegisterWithQualifierClass): ...

with self.assertRaises(UnknownQualifiedServiceRequestedError) as context:
inner()

self.assertIn(
f"Cannot instantiate concrete class for {RegisterWithQualifierClass} "
"as qualifier 'None' is unknown. Available qualifiers: {'test_container'}",
f"as qualifier 'None' is unknown. Available qualifiers: {{'{__name__}'}}",
str(context.exception),
)

Expand All @@ -380,13 +370,13 @@ def inner(foo: RegisterWithQualifierClass = wire(qualifier=__name__)):

def test_inject_qualifier_on_unknown_type(self):
@self.container.autowire
def inner(foo: str = wire(qualifier=__name__)): ...
def inner(_foo: str = wire(qualifier=__name__)): ...

with self.assertRaises(UsageOfQualifierOnUnknownObjectError) as context:
inner()

self.assertEqual(
"Cannot use qualifier test_container on a type that is not managed by the container.",
f"Cannot use qualifier {__name__} on a type that is not managed by the container.",
str(context.exception),
)

Expand Down Expand Up @@ -500,11 +490,24 @@ def test_get_returns_real_instance(self):
self.assertIsInstance(second, RandomService)

def test_shrinks_context_on_autowire(self):
def target(a: RandomService, _b: unittest.TestCase = None, _c: datetime.datetime = None):
class SomeClass:
pass

def provide_b(fn):
@functools.wraps(fn)
def __inner(*args, **kwargs):
return fn(*args, **kwargs, b=SomeClass())

return __inner

@provide_b
def target(a: RandomService, b: SomeClass, _c: Optional[datetime.datetime] = None):
self.assertEqual(a.get_random(), 4)
self.assertIsInstance(b, SomeClass)

autowired = self.container.autowire(target)
self.assertEqual(self.container.context.dependencies[target].keys(), {"a", "_b", "_c"})
self.assertEqual(self.container.context.dependencies[target].keys(), {"a", "b"})
# On the second call, container will drop b from dependencies as it is an unknown object.
autowired()
self.assertEqual(self.container.context.dependencies[target].keys(), {"a"})

Expand Down
3 changes: 1 addition & 2 deletions test/unit/test_container_optimized.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import unittest
from dataclasses import dataclass
from test.fixtures import FooBar, FooBase, FooBaz
from test.unit.services.no_annotations.random.random_service import RandomService

from typing_extensions import Annotated

from test.unit.services.no_annotations.random.random_service import RandomService
from wireup import DependencyContainer, ParameterBag, ServiceLifetime, Wire


Expand Down
Loading

0 comments on commit c0b0b65

Please sign in to comment.