Skip to content

Guiding Principles#

This page acts as a reference for concepts and principles that are mentioned throughout the documentation.

Infer As Much As Possible#

If a function or constructor only requires types that the container already knows how to build, we simply supply them. You can still register a custom implementation if you want, but you are not required to.

import os

from diy import Specification, RuntimeContainer

from app.database import DatabaseConnection, connect_to_database


class UserService:
  def __init__(
    self,
    db_connection: DatabaseConnection,
    initial_password: str = "s3cr3t",
  ):
    ...

spec = Specification()

@spec.add
def build_database_connection() -> DatabaseConnection:
  return connect_to_database(os.environ["DATABASE_URL"])

container = RuntimeContainer(spec)

# We know enough to fill all parameters of the UserService constructor.
# So even though the container was not explicitly taught how to construct a
# UserService, we can build one by constructing a DatabaseConnection, pass it
# to the UserService constructor and return the instance.
user_service = container.resolve(UserService)

Avoid Library Specific Annotations#

Some of the other python dependency injection libraries rely on annotations that need to be present on your code in order for them to work. This can be handy, but couples your code to that specific library.

from other_di_library import inject, Depends

# The following function is entangled with the DI process
@inject
def get_users(user_service: UserService): ...

# another form of this
def get_users(user_service: UserService = Depends(UserService): ...
def get_users(user_service: Annotated[UserService, Depends(UserService)]): ...

This works, but you have to adjust your existing code to fit the libraries expectations.

Using diy, we do not need such annotations, and instead call functions on a container.

from diy import RuntimeContainer

# Imagine this is constructed and filled with the necessary specs
container: RuntimeContainer

# This function can be called like every other one. It is not important or
# relevant that there exists a container somewhere, that could provide it with
# parameters.
def get_users(user_service: UserService): ...

# If we want our container to provide parameters, we do so externally and
# explicitly
result = container.call(get_users)

This way the library (diy) is made to work for your code, and not the other way around.

Fail Fast And Catch Errors Early#

Trying to resolve an instance from the container can fail. After building a container, you should be able to run some analysis, before you deploy and run it on production.

All of this has not been implemented yet!

Sketch#

TODO: Move this to the guide and reference it

CLI#

Using your terminal you can import

diy verify \                             # pass any identifier understood by importlib.
  --container "app.containers:default" \ # You may already know this from e.g. uvicorn.
  --include "app"                        # All types from this and all submodules will be
                                         # analyzed and verified, that all types are
                                         # buildable.

Test Frameworks#

Maybe also a pytest plugin or plain python functions for unittest?

Provide Helpful Feedback#

Everything can fail. However, especially when trying to infer as much as possible, you need helpful error messages.

Bad Example#

Imagine the following

DependencyInjectionError: Failed to resolve "DatabaseConnection".

accompanied with a looong stacktrace consisting of mostly framework code.

Not that helpful. Some questions are:

  • What type required did we need a DatabaseConnection in the first place?
  • What did go wrong? Were there wrong credentials or did I use the wrong port? Is the database even running?#

Good Example#

DependencyInjectionError: Failed to resolve parameter db_connection when
                          building an instance of UserService.

app.api.users.get_users_endpoint()
├─ mailer: aws.ses.Mailer
├─ logger: logging.Logger
└─ user_service: app.services.UserService
   └─ db: app.db.DatabaseConnection  <-- Here

caused by: InvalidCredentialsError: Wrong user or password

Now we know everything we need! Something tried to call a function get_users_endpoint, which required three parameters. The first two were successfully built, but something went wrong when trying to build an instance of the UserService. This needs a DatabaseConnection and something was wrong with the supplied credentials.