Skip to content

Dependencies

Dependencies are a piece of great functionality now common in a lot of the frameworks out there and allows the concept of dependency injection to take place.

Esmerald uses the Inject object to manage those dependencies in every application level

Dependencies and the application levels

In every level the dependencies parameter (among others) are available to be used and handle specific dependencies raised on each level.

The dependencies are read from top-down in a python dictionary format, which means the last one takes the priority.

How to use

Assuming we have a User model using Saffier.

from myapp.accounts.models import User
from saffier.exceptions import ObjectNotFound

from esmerald import Esmerald, Gateway, Inject, Injects, get


async def get_user_model() -> User:
    try:
        return await User.get(pk=1)
    except ObjectNotFound:
        return None


@get("/me", dependencies={"user": Inject(get_user_model)})
async def me(user: User = Injects()) -> str:
    return user.email


app = Esmerald(routes=[Gateway(handler=me)])

The example above is very simple and of course a user can be obtained in a slighly and safer way but for it serves only for example purposes.

Using dependencies is quite simple, it needs:

  1. Uses Inject object.
  2. Uses the Injects object to, well, inject the dependency into the handler.

Some complexity

Dependencies can be injected in many levels as previously referred and that also means, you can implement the levels of complexity you desire.

from esmerald import Esmerald, Gateway, Include, Inject, get


def first_dependency() -> int:
    return 20


def second_dependency(number: int) -> bool:
    return number >= 5


@get("/validate")
async def me(is_valid: bool) -> bool:
    return is_valid


app = Esmerald(
    routes=[
        Include(
            routes=[Gateway(handler=me)],
            dependencies={
                "is_valid": Inject(second_dependency),
            },
        )
    ],
    dependencies={"number": Inject(first_dependency)},
)

What is happening

The number is obtained from the first_dependency and passed to the second_dependency as a result and validates and checks if the value is bigger or equal than 5 and that result is_valid is than passed to the main handler /validate returning a bool.

Exceptions

All the levels are managed in a simple top-down approach where one takes priority over another as previously mentioned but.

Pior to version 1.0.0, a ChildEsmerald was an independent instance that is plugged into a main Esmerald application but since it is like another Esmerald instance that also means the ChildEsmerald didn't take priority over the top-level application.

In other words, a ChildEsmerald did not take priority over the main instance but the rules of prioritization of the levels inside a ChildEsmerald prevailed the same as for a normal Esmerald instance.

Some exceptions are still applied. For example, for dependencies and exception handlers, the rule of isolation and priority is still applied.

The same is applied also to exception handlers.

More real world examples

Now let us imagine that we have a web application with one of the views. Something like this:

from typing import List

from esmerald import get
from esmerald.openapi.datastructures import OpenAPIResponse


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
)
async def users(user_dao: UserDAO) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    return await user_dao.get_all()

As you can notice, the `user_dao`` is injected automatically using the appropriate level of dependency injection.

Let us see the urls.py and understand from where we got the user_dao:

from esmerald import Factory, Include, Inject

# Using lambdas
route_patterns = [
    Include(
        "/api/v1",
        routes=[
            Include("/accounts", namespace="accounts.v1.urls"),
            Include("/articles", namespace="articles.v1.urls"),
            Include("/posts", namespace="posts.v1.urls"),
        ],
        interceptors=[LoggingInterceptor],  # Custom interceptor
        dependencies={
            "user_dao": Inject(lambda: UserDAO()),
            "article_dao": Inject(lambda: ArticleDAO()),
            "post_dao": Inject(lambda: PostDAO()),
        },
    )
]

# Using the Factory
route_patterns = [
    Include(
        "/api/v1",
        routes=[
            Include("/accounts", namespace="accounts.v1.urls"),
            Include("/articles", namespace="articles.v1.urls"),
            Include("/posts", namespace="posts.v1.urls"),
        ],
        interceptors=[LoggingInterceptor],  # Custom interceptor
        dependencies={
            "user_dao": Inject(Factory(UserDAO)),
            "article_dao": Inject(Factory(ArticleDAO)),
            "post_dao": Inject(Factory(PostDAO)),
        },
    )
]

In the previous example we use lambdas to create a callable from DAO instances and we refactor it to use the Factory object instead. It is cleaner and more pleasant to work with.

The cleaner version of lambdas using Esmerald it is called Factory.

Note

You can see the Python lambdas as the equivalent of the anonymous functions in JavaScript. If you are still not sure, see more details about it.

Tip

Learn more about Esmerald DAOs and how to take advantage of those.

The Factory is a clean wrapper around any callable (classes usually are callables as well, even without instantiating the object itself).

Tip

No need to explicitly instantiate the class, just pass the class definition to the Factory and Esmerald takes care of the rest for you.

Importing using strings

Like everything is Esmerald, there are different ways of achieving the same results and the Factory is no exception.

In the previous examples we were passing the UserDAO, ArticleDAO and PostDAO classes directly into the Factory object and that also means that you will need to import the objects to then be passed.

What can happen with this process? Majority of the times nothing but you can also have the classic partially imported ... annoying error, right?

Well, the good news is that Esmerald got you covered, as usual.

The Factory also allows import via string without the need of importing directly the object to the place where it is needed.

Let us then see how it would look like and let us then assume:

  1. The UserDAO is located somewhere in the codebase like myapp.accounts.daos.
  2. The ArticleDAO is located somewhere in the codebase like myapp.articles.daos.
  3. The PostDAO is located somewhere in the codebase like myapp.posts.daos.

Ok, now that we know this, let us see how it would look like in the codebase importing it inside the Factory.

from esmerald import Factory, Include, Inject

route_patterns = [
    Include(
        "/api/v1",
        routes=[
            Include("/accounts", namespace="accounts.v1.urls"),
            Include("/articles", namespace="articles.v1.urls"),
            Include("/posts", namespace="posts.v1.urls"),
        ],
        interceptors=[LoggingInterceptor],  # Custom interceptor
        dependencies={
            "user_dao": Inject(Factory("myapp.accounts.daos.UserDAO")),
            "article_dao": Inject(Factory("myapp.articles.daos.ArticleDAO")),
            "post_dao": Inject(Factory("myapp.posts.daos.PostDAO")),
        },
    )
]

Now, this is a beauty is it not? This way, the codebase is cleaner and without all of those imported objects from the top.

Tip

Both cases work well within Esmerald, this is simply an alternative in case the complexity of the codebase increases and you would like to tidy it up a bit more.

In conclusion, if your views/routes expect dependencies, you can define them in the upper level as described and Esmerald will make sure that they will be automatically injected.