Dependency Injection and Services

Tet ships with a lightweight, request-scoped dependency injection (DI) container built on pyramid_di. The DI primitives live in the tet.services module, which re-exports the pyramid_di building blocks from a single, stable location so application code never has to reach into pyramid_di directly.

This page explains what services solve, how to declare and register them, how dependencies are wired together with autowired, how the request-scoped lifecycle works, and finishes with a realistic end-to-end example.

Why dependency injection?

A typical web request needs to talk to a database session, look up the current user, send email, call third-party APIs, and so on. If every view constructs those collaborators itself, you end up with:

  • views that know how to build their dependencies (and their dependencies’ dependencies),

  • duplicated wiring scattered across the codebase,

  • objects with unclear lifetimes (created too often, or accidentally shared across requests),

  • code that is hard to test because collaborators cannot be swapped out.

Dependency injection inverts that: a service declares what it needs, and the container supplies it. Views and other services ask the container for a service by type and receive a fully wired instance. The container also owns the lifecycle of each service – in Tet the common case is a service that is instantiated once per request and discarded when the request ends.

Service base classes

tet.services re-exports three base classes. Pick the one that matches the lifetime your service needs:

RequestScopedBaseService

The workhorse. A fresh instance is created the first time it is requested within a given request, and the same instance is reused for the remainder of that request. Instances receive the active request as self.request. Use this for anything that depends on per-request state (the database session, the authenticated user, the current locale, and so on).

ApplicationScopedBaseService

A single instance shared for the lifetime of the application (the Pyramid registry). Use this for stateless helpers or expensive, immutable resources – but never store per-request state on it.

BaseService

The common base of the two above. You normally subclass one of the scoped classes rather than this directly.

All of these are imported from tet.services:

from tet.services import (
    service,
    autowired,
    RequestScopedBaseService,
    ApplicationScopedBaseService,
    BaseService,
)

Declaring a service

Decorate a class with service() and subclass the appropriate base. A request-scoped service automatically gets self.request:

from tet.services import service, RequestScopedBaseService
from myapp.models import User


@service()
class UserService(RequestScopedBaseService):
    def get_user(self, user_id):
        return self.request.dbsession.query(User).get(user_id)

The @service() decorator marks the class for registration. The actual registration happens when you scan the module that contains it (see Registering services below).

Declaring dependencies with autowired

A service rarely lives alone. To depend on another service, declare it as a class attribute using autowired(). The attribute becomes a descriptor that resolves the dependency lazily from the container the first time it is accessed:

from tet.services import service, autowired, RequestScopedBaseService


@service()
class OrderService(RequestScopedBaseService):
    user_service = autowired(UserService)

    def get_user_orders(self, user_id):
        user = self.user_service.get_user(user_id)
        return user.orders

Here OrderService never constructs UserService itself. When self.user_service is first read, the container looks up (or instantiates) the request-scoped UserService for the current request and hands it back. Because both are request-scoped, every collaborator in a given request sees the same UserService instance.

autowired works the same way on any class the container instantiates – including view classes (see Using services in views).

Registering services

Services must be enabled and then discovered. Enabling DI means including pyramid_di; in a Tet application this is done by activating the services feature, whose includeme simply calls config.include("pyramid_di"):

from tet.config import application_factory


@application_factory(included_features=["services"])
def main(config):
    config.scan_services("myapp.services")
    config.scan()

included_features=["services"] pulls in tet.services, which wires up pyramid_di and adds the scan_services directive (among others) to the configurator. scan_services walks the given package and registers every class decorated with @service(). The regular config.scan() call still handles your views and other Venusian decorators.

If you are configuring Pyramid manually rather than through Tet’s feature list, the equivalent explicit include is:

config.include("pyramid_di")  # or: config.include("tet.services")
config.scan_services("myapp.services")

You can also register services imperatively, without the @service() decorator. This is handy for binding a concrete object or factory to an interface or third-party type:

# Register an already-constructed instance under a type/name.
config.register_service(my_instance, SomeType)


# Register a factory called with (context, request) to build the service.
def make_session_service(context, request):
    return build_session(request)


config.register_service_factory(make_session_service, Session)

Tet itself uses register_service_factory internally – for example tet.sqlalchemy.simple registers the SQLAlchemy Session as a request-scoped service factory so views can simply do request.find_service(Session).

Retrieving a service

There are two ways to get a service instance, depending on where you are.

Inside another service or any class the container instantiates, prefer autowired (shown above) – it is declarative and resolves lazily.

Anywhere you hold a request – most commonly a function-based view – call request.find_service:

from pyramid.view import view_config
from myapp.services import UserService


@view_config(route_name="user", renderer="json")
def get_user(request):
    user_service = request.find_service(UserService)
    return user_service.get_user(request.matchdict["id"])

find_service looks the service up by type (and optional name= for named registrations). For a request-scoped service it returns the cached instance for the current request, creating it on first use.

The request-scoped lifecycle

Request scoping is the heart of Tet’s DI model, so it is worth being precise about what “once per request” means:

  • The first time a request-scoped service is needed during a request – whether via find_service or via an autowired attribute – the container instantiates it and caches it on the request.

  • Every subsequent lookup within the same request returns that cached instance. Two collaborators that both depend on UserService share one instance.

  • When the request ends, the cache is discarded. The next request gets brand-new instances.

This makes request-scoped services the right place for per-request state (the database session, the current user, request-specific caches) without any risk of leaking that state across requests. Conversely, never stash per-request data on an ApplicationScopedBaseService, which is shared by every request and every thread.

Using services in views

Class-based views can themselves declare dependencies with autowired, because the view class is instantiated through the container:

from pyramid.view import view_config
from tet.services import autowired
from myapp.services import UserService


class UserViews:
    user_service = autowired(UserService)

    def __init__(self, request):
        self.request = request

    @view_config(route_name="user", renderer="json")
    def get_user(self):
        user_id = self.request.matchdict["id"]
        return self.user_service.get_user(user_id)

Tet also provides tet.view.ServiceViews, a RequestScopedBaseService subclass meant to be used as a base for view classes. It gives you self.request and self.context for free, so you can drop the boilerplate __init__:

from pyramid.view import view_config
from tet.services import autowired
from tet.view import ServiceViews
from myapp.services import UserService


class UserViews(ServiceViews):
    user_service = autowired(UserService)

    @view_config(route_name="user", renderer="json")
    def get_user(self):
        return self.user_service.get_user(self.request.matchdict["id"])

End-to-end example

Putting it all together: a UserService and an OrderService that depends on it, both scanned at startup and consumed from a view. Assume the SQLAlchemy Session is registered as a service (see SQLAlchemy Integration).

myapp/services.py:

from sqlalchemy.orm import Session

from tet.services import service, autowired, RequestScopedBaseService
from myapp.models import User


@service()
class UserService(RequestScopedBaseService):
    # The SQLAlchemy session is itself a request-scoped service.
    dbsession = autowired(Session)

    def get_user(self, user_id):
        return self.dbsession.query(User).get(user_id)


@service()
class OrderService(RequestScopedBaseService):
    user_service = autowired(UserService)

    def get_user_orders(self, user_id):
        user = self.user_service.get_user(user_id)
        if user is None:
            return []
        return list(user.orders)

myapp/views.py:

from pyramid.view import view_config

from tet.services import autowired
from tet.view import ServiceViews
from myapp.services import UserService, OrderService


class UserViews(ServiceViews):
    user_service = autowired(UserService)
    order_service = autowired(OrderService)

    @view_config(route_name="user", renderer="json")
    def get_user(self):
        user = self.user_service.get_user(self.request.matchdict["id"])
        return {"id": user.id, "name": user.name}

    @view_config(route_name="user_orders", renderer="json")
    def get_orders(self):
        user_id = self.request.matchdict["id"]
        orders = self.order_service.get_user_orders(user_id)
        return [{"id": o.id, "total": str(o.total)} for o in orders]

myapp/__init__.py:

from tet.config import application_factory


@application_factory(included_features=["services"])
def main(config):
    config.include("tet.sqlalchemy.simple")
    config.setup_sqlalchemy()

    config.add_route("user", "/users/{id}")
    config.add_route("user_orders", "/users/{id}/orders")

    config.scan_services("myapp.services")
    config.scan()

During a single request to /users/42/orders, the container creates one OrderService, one UserService, and one Session – each reused wherever it is needed – and then discards them when the response is sent.

See also