Skip to content

Commit

Permalink
Add static factory functions
Browse files Browse the repository at this point in the history
  • Loading branch information
maldoinc committed Sep 8, 2023
1 parent 4960236 commit 4359594
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 10 deletions.
5 changes: 5 additions & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ nav:
- Home: index.md
- Introduction: introduction.md
- Advanced Topics:
- Factory functions: factory_functions.md
- Working with Interfaces: interfaces.md
- Manual configuration: manual_configuration.md
- Multiple containers: multiple_containers.md
- Misc:
- Introduce to an existing project: introduce_to_an_existing_project.md
- Examples:
- Fastapi: examples/fastapi.md
theme: readthedocs
markdown_extensions:
- admonition
docs_dir: pages
65 changes: 65 additions & 0 deletions docs/pages/factory_functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Factory functions

Factory functions allow the container to wire dependencies that require additional logic to create
or be able to inject objects it doesn't own.

Typically getting the necessary dependencies is enough to construct an object. However, there are scenarios
where you need to delegate the creation of an object to a special function called a
[factory](https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)).

## Use cases

Some of the use cases for factories are as follows:

* Object construction needs additional logic or configuration.
* Depending on the runtime environment or configuration, you may need to create different objects
inheriting from the same base (See [Strategy Pattern](https://en.wikipedia.org/wiki/Strategy_pattern)) or configure them differently.
* Gradually introduce DI into an existing project where the container should be able to inject dependencies created elsewhere.
Such as injecting the same databaser connection as the rest of the application.
* Eliminate services which have only one method that returns the same object and instead inject the object directly.
* A service that returns a db connection
* A service which returns the current authenticated user
* Register the result of a service's method as its own service. Instead of calling `auth_service.get_current_user()` every time, inject the authenticated user directly.

## Usage

In order for the container to be able to inject these dependencies you must register the factory function.
You can do this by using the `@container.register` decorator or by calling `container.register(fn)` directly.

When the container needs to inject a dependency it checks known factories to see if any of them can create it.


!!! info
The return type of the function tells the container what type of dependency it can create.

!!! warning
Factories can only depend on objects known by the container!

## Examples

Assume in the context of a web application a class `User` exists and represents a user of the system.

```python
# Instead of doing the following over and over again
def get_user_logs(auth_service: AuthService):
current_user = auth_service.get_current_user()
...



# You can create a factory and inject the authenticated user directly.
# You may want to create a new type to make a disctinction on the type of user this is.
AuthenticatedUser = User

container.register
def get_current_user(auth_service: AuthService) -> AuthenticatedUser:
return auth_service.get_current_user()

# Now it is possible to inject the authenticated user directly wherever it is necessary.
def get_user_logs(user: AuthenticatedUser):
...
```

## Links

* [Introduce to an existing project](introduce_to_an_existing_project.md)
2 changes: 1 addition & 1 deletion docs/pages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Dependency and configuration injection library in Python.
**1. Set application configuration parameters**
```python
container.params.update({
"db.connection_str": "sqlite://memory",
"db.connection_str": "sqlite://",
"auth.user": os.environ.get("USER"),
"cache_dir": "/var/cache/",
"env": os.environ.get("ENV", "dev")
Expand Down
51 changes: 51 additions & 0 deletions docs/pages/introduce_to_an_existing_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Introduce to an existing project

It can be challenging to add DI to an existing project which doesn't yet use it. One of the issues you will run into
sooner or later is being able to share resources between code which uses DI and the rest of the application
which does not.

This is especially useful to allow the container to inject dependencies created elsewhere.

Think of a database connection. Your application probably already has one. Instead of opening a second connection
using a new database service, you can instruct the container how to get the connection either via a service or
by using factory functions.

Another case might be an existing service that is already constructed, and you wish to be able to inject.

## Using a Service

A typical way to solve this would involve create a service with a single method
that uses existing functionality to get the desired object and simply returns it.

```python
# Example of a service acting as a factory
@container.register
@dataclass
class DbConnectionService:
self.conn = get_db_connection_from_somewhere()
```

Here, it is possible inject `DbConnectionService` and call `.conn` to get the connection. While this works, it's not the best way to go.

## Using Factory functions

To handle this more elegantly, WireUp lets you register functions as factories.
You can do this by using the `@container.register` decorator or by calling `container.register(fn)` directly.


```python
@container.register
def get_db_connection_from_somewhere() -> Connection:
return ...

# Alternatively

container.register(get_db_connection_from_somewhere)
```

Now it is possible to inject `Connection` just like any other dependency.


## Links

* [Factory functions](factory_functions.md)
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Effortless dependency injection in Python.
**1. Set application parameters**
```python
container.params.update({
"db.connection_str": "sqlite://memory",
"db.connection_str": "sqlite://",
"auth.user": os.environ.get("USER"),
"cache_dir": "/var/cache/",
"env": os.environ.get("ENV", "dev")
Expand Down
121 changes: 121 additions & 0 deletions test/test_container_static_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from dataclasses import dataclass
from unittest import TestCase

from test.fixtures import Counter, FooBar
from test.services.random_service import RandomService
from wireup import DependencyContainer, ParameterBag, wire


class ThingToBeCreated:
def __init__(self, val: str):
self.val = val


class TestContainerStaticFactory(TestCase):
def setUp(self) -> None:
self.container = DependencyContainer(ParameterBag())

def test_injects_using_factory_with_dependencies(self):
self.container.register(RandomService)
self.container.params.put("dummy_val", "foo")

@self.container.register
def create_thing(val=wire(param="dummy_val")) -> ThingToBeCreated:
return ThingToBeCreated(val=val)

@self.container.autowire
def inner(thing: ThingToBeCreated):
self.assertEqual(thing.val, "foo")

inner()

self.assertEqual(create_thing("new").val, "new", msg="Assert fn is not modified")

def test_injects_using_factory_returns_singletons(self):
self.container.params.put("start", 5)

@self.container.register
def create_thing(start=wire(param="start")) -> Counter:
return Counter(count=start)

@self.container.autowire
def inner(c1: Counter, c2: Counter):
c1.inc()

self.assertEqual(c1.count, 6)
self.assertEqual(c1.count, c2.count)

inner()

def test_injects_on_instance_methods(self):
this = self

class Dummy:
@self.container.autowire
def inner(self, c1: Counter):
c1.inc()
this.assertEqual(c1.count, 1)

@self.container.register
def create_thing() -> Counter:
return Counter()

Dummy().inner()

def test_register_known_container_type(self):
self.container.register(RandomService)

with self.assertRaises(ValueError) as context:

@self.container.register
def create_random_service() -> RandomService:
return RandomService()

self.assertEqual(
f"Cannot register factory function as type {RandomService} is already known by the container.",
str(context.exception),
)

def test_register_factory_known_type_from_other_factory(self):
with self.assertRaises(ValueError) as context:

@self.container.register
def create_random_service() -> RandomService:
return RandomService()

@self.container.register
def create_random_service_too() -> RandomService:
return RandomService()

self.assertEqual(
f"A function is already registered as a factory for dependency type {RandomService}.",
str(context.exception),
)

def test_register_factory_no_return_type(self):
with self.assertRaises(ValueError) as context:

@self.container.register
def create_random_service():
return RandomService()

self.assertEqual(
"Factory functions must specify a return type denoting the type of dependency it can create.",
str(context.exception),
)

def test_factory_as_property_accessor(self):
@self.container.register
class FooGenerator:
def get_foo(self) -> FooBar:
return FooBar()

@self.container.autowire
def inner(foobar: FooBar):
self.assertEqual(foobar.foo, "bar")

@self.container.register
def foo_factory(foo_gen: FooGenerator) -> FooBar:
return foo_gen.get_foo()

inner()
Loading

0 comments on commit 4359594

Please sign in to comment.