Skip to content

Factory functions

Typically getting the necessary dependencies is enough to construct an object. However, there are scenarios where you need to delegate service creation to a special function called a factory.

Use cases

  • 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) or configure them differently.
  • Inject a model/dto which represents the result of an action, such as the current authenticated user.
  • Inject a class from another library where it's not possible to add annotations.
  • Inject strings, ints and other built-in types.

Usage

In order for the container to inject these dependencies, you must register the factory function by using the @service decorator.

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

Good to know

Return type annotation of the factory is required as it denotes what will be built.

Examples

Inject a model

Assume in the context of an application a class User exists and represents a user of the system. We can use a factory to inject a user model that represents the current authenticated user.

from wireup import service, ServiceLifetime

# You may want to create a new type to make a distinction on the type of user this is.
AuthenticatedUser = NewType("AuthenticatedUser", User)


@service(lifetime=ServiceLifetime.TRANSIENT)
def get_current_user(auth_service: AuthService) -> AuthenticatedUser:
    return AuthenticatedUser(auth_service.get_current_user())


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

Implement strategy pattern

Assume a base class Notifier with implementations that define how the notification is sent (IMAP, POP, WebHooks, etc.) Given a user it is possible to instantiate the correct type of notifier based on user preferences.

from wireup import service, ServiceLifetime


@service(lifetime=ServiceLifetime.TRANSIENT)
def get_user_notifier(
    user: AuthenticatedUser, 
    slack_notifier: SlackNotifier, 
    email_mailer: EmailNotifier
) -> Notifier:
    notifier = ...  # get notifier type from preferences.

    return notifier

When injecting Notifier the correct type will be injected based on the authenticated user's preferences.

Inject a third-party class

You can use factory functions to inject a class which you have not declared yourself and therefore cannot annotate. Let's take redis client as an example.

from wireup import service


@service
def redis_factory(redis_url: Annotated[str, Inject(param="redis_url")]) -> Redis:
    return redis.from_url(redis_url)
from wireup import service


@service
def redis_factory(settings: Settings) -> Redis:
    return redis.from_url(settings.redis_url)

Inject built-in types

If you want to inject resources which are just strings, ints, or other built-in types then you can use a factory in combination with NewType.

factories.py
AuthenticatedUsername = NewType("AuthenticatedUsername", str)

@service
def authenticated_username_factory(auth: SomeAuthService) -> AuthenticatedUsername:
    return AuthenticatedUsername(...)

This can now be injected as usual by annotating the dependency with the new type.