Skip to content

Commit

Permalink
Add support for Annotated types
Browse files Browse the repository at this point in the history
  • Loading branch information
maldoinc committed Sep 13, 2023
1 parent be1602a commit 62530a5
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 54 deletions.
14 changes: 8 additions & 6 deletions .github/workflows/run_all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Create venv
run: python -m venv .venv
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
poetry install
.venv/bin/python -m pip install --upgrade pip
.venv/bin/pip install poetry
.venv/bin/poetry install --no-root
- name: Lint
run: |
poetry run ruff --format=github wireup
.venv/bin/poetry run ruff --format=github wireup
- name: Check formatting
run: |
poetry run black --check .
.venv/bin/poetry run black --check .
- name: Run tests
run: |
python -m unittest discover -s test/
.venv/bin/python -m unittest discover -s test/
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ site_name: WireUp
nav:
- Home: index.md
- Quickstart: quickstart.md
- Annotations: annotations.md
- Services: services.md
- Parameters: parameters.md
- Advanced Topics:
Expand Down
49 changes: 49 additions & 0 deletions docs/pages/annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
WireUp relies on various kind of type annotations or hints to be able to autowire dependencies.
When it is not possible to automatically locate a given dependency the argument must be annotated
with additional metadata.

## When do you need to provide annotations.

Not needed when injecting:

* Services
* Injecting an interface which has only one implementing service

Annotations required when injecting:

* Parameters
* Parameter expressions
* Injecting an interface which has multiple implementing services.

## Annotation types

Wireup supports two types of annotations. Using Python's `Annotated` or by using default values.

### Annotated

This is the preferred method for Python 3.9+ and moving forward. It is also recommended to
backport this using `typing_extensions` for Python 3.8.


```python
@container.autowire
def target(
env: Annotated[str, Wire(param="env_name")],
logs_cache_dir: Annotated[str, Wire(expr="${cache_dir}/logs")]
):
...
```

### Default values

This relies on the use of default values to inject parameters. Anything that can be passed to `Annotated` may also
be used here.

```python
@container.autowire
def target(
env: str = wire(param="env_name"),
logs_cache_dir: str = wire(expr="${cache_dir}/logs")
):
...
```
7 changes: 4 additions & 3 deletions docs/pages/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ While injecting an interface with multiple implementing dependencies, you need t
which concrete class should be resolved.

```python
def home(
engine: Engine = wire(qualifier="electric"),
combustion: Engine = wire(qualifier="combustion"),
container.autowire
def target(
engine: Annotated[Engine, Wire(qualifier="electric")],
combustion: Annotated[Engine, Wire(qualifier="combustion")],
):
...
```
4 changes: 2 additions & 2 deletions docs/pages/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ To inject a parameter by name simply call `wire(param="param_name")`.

```python
@container.autowire
def do_something_cool(cache_dir: str = wire(param="cache_dir")) -> None:
def do_something_cool(cache_dir: Annotated[str, Wire(param="cache_dir")]) -> None:
...
```

Expand All @@ -39,7 +39,7 @@ at once and concatenate their values together or simply format the value of a si

```python
@container.autowire
def do_something_cool(logs_cache_dir: str = wire(expr="${cache_dir}/${env}/logs")) -> None:
def do_something_cool(logs_cache_dir: Annotated[str, Wire(expr="${cache_dir}/${env}/logs")]) -> None:
...
```

Expand Down
23 changes: 13 additions & 10 deletions docs/pages/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,20 @@ from wireup import container
# Parameters serve as configuration for services.
# Think of a database url or environment name.
container.params.update({
"db.connection_str": os.environ.get("DATABASE_URL") # (1)!
"db.connection_str": os.environ.get("DATABASE_URL") # (1)!
"cache_dir": gettempdir(),
"env": os.environ.get("ENV", "dev")
})


# Constructor injection is supported for regular classes as well as dataclasses.
@container.register # (2)!
# Constructor injection is supported for regular classes as well as dataclasses
@container.register # (2)!
class DbService:
# Inject a parameter by name
connection_str: str = wire(param="db.connection_str"),
connection_str: Annotated[str, Wire(param="db.connection_str")],
# Or by interpolating multiple parameters into a string
cache_dir: str = wire(expr="${cache_dir}/${env}/db"),
cache_dir: Annotated[str, Wire(expr="${cache_dir}/${env}/db")],

# resulting in a more compact syntax.
@container.register
@dataclass
class UserRepository:
Expand All @@ -41,17 +40,21 @@ class UserRepository:

```python
@app.route("/greet/<str:name>")
@container.autowire # (2)!
@container.autowire # (1)!
# Classes are automatically injected based on annotated type.
# Parameters will be located based on the hint given in their default value.
# Unknown arguments will not be processed.
def greet(name: str, user_repository: UserRepository, env: str = wire(param="env")): # (1)!
def greet(
name: str,
user_repository: UserRepository,
env: Annotated[str, Wire(param="env")]
):
...
```

1. We know that this will be used in conjunction with many other libraries, so WireUp will not throw on unknown
1. Decorate all methods where the library must perform injection.
We know that this will be used in conjunction with many other libraries, so WireUp will not throw on unknown
parameters in order to let other decorators to do their job.
2. Decorate all methods where the library must perform injection.

**Installation**

Expand Down
56 changes: 38 additions & 18 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mkdocs-material = "^9.2.8"
mkdocstrings-python = "^1.6.2"
mkdocs-open-in-new-tab = "^1.0.2"
mike = "^1.1.2"
typing-extensions = "^4.7.1"

[tool.black]
line-length = 120
Expand Down
8 changes: 4 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ for services and a simple web view called "greet".
**1. Register dependencies**

```python
from wireup import container
from wireup import container, Wire

# Parameters serve as configuration for services.
# Think of a database url or environment name.
Expand All @@ -32,9 +32,9 @@ container.params.update({
@container.register
class DbService:
# Inject a parameter by name
connection_str: str = wire(param="db.connection_str"),
connection_str: Annotated[str, Wire(param="db.connection_str")],
# Or by interpolating multiple parameters into a string
cache_dir: str = wire(expr="${cache_dir}/${env}/db"),
cache_dir: Annotated[str, Wire(expr="${cache_dir}/${env}/db")],


@container.register
Expand All @@ -51,7 +51,7 @@ class UserRepository:
# Decorate all targets where the library must perform injection,such as views in a web app.
# Classes are automatically injected based on annotated type.
# Parameters will be located based on the hint given in their default value.
def greet(name: str, user_repository: UserRepository, env: str = wire(param="env")):
def greet(name: str, user_repository: UserRepository, env: Annotated[str, Wire(param="env")]):
...
```

Expand Down
31 changes: 29 additions & 2 deletions test/test_container.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import unittest
from dataclasses import dataclass
from typing_extensions import Annotated
from unittest.mock import Mock, patch

from test import services
from test.fixtures import Counter, FooBase, FooBar, FooBaz
from test.fixtures import Counter, FooBar, FooBase, FooBaz
from test.services.random_service import RandomService
from test.services.truly_random_service import TrulyRandomService
from wireup import wire
from wireup import Wire, wire
from wireup.ioc.container_util import ParameterWrapper
from wireup.ioc.dependency_container import ContainerProxy, DependencyContainer
from wireup.ioc.parameter import ParameterBag, TemplatedString
Expand Down Expand Up @@ -208,6 +209,17 @@ def inner(sub1: FooBase = wire(qualifier="sub1"), sub2: FooBase = wire(qualifier
self.container.register(FooBaz, qualifier="sub2")
inner()

def test_two_qualifiers_are_injected_annotated(self):
@self.container.autowire
def inner(sub1: Annotated[FooBase, Wire(qualifier="sub1")], sub2: Annotated[FooBase, Wire(qualifier="sub2")]):
self.assertEqual(sub1.foo, "bar")
self.assertEqual(sub2.foo, "baz")

self.container.abstract(FooBase)
self.container.register(FooBar, qualifier="sub1")
self.container.register(FooBaz, qualifier="sub2")
inner()

def test_interface_with_single_implementation_no_qualifier_gets_autowired(self):
@self.container.autowire
def inner(foo: FooBase):
Expand Down Expand Up @@ -387,6 +399,21 @@ def inner(name=wire(param="name")):
self.container.params.put("name", "foo")
inner()

def test_wire_from_annotation(self):
@self.container.autowire
def inner(
name: Annotated[str, wire(param="name")],
env: Annotated[str, Wire(param="env")],
env_name: Annotated[str, Wire(expr="${env}-${name}")],
):
self.assertEqual(name, "foo")
self.assertEqual(env, "test")
self.assertEqual(env_name, "test-foo")

self.container.params.put("name", "foo")
self.container.params.put("env", "test")
inner()

def test_injects_ctor(self):
class Dummy:
@self.container.autowire
Expand Down
Loading

0 comments on commit 62530a5

Please sign in to comment.