Pattern: using attrs and dataclasses

attrs and dataclasses are two libraries which make it easy to create classes without creating too much boilerplate.

One of the things it can do is creating __init__ functions based on class’ type hints.

Note

injectpy doesn’t do anything special to support attrs and dataclasses. They create __init__ methods with proper type hints, so injectpy works with them out of the box.

Let’s consider this example:

from myapp.lib import IFileSystem, IUserRepository, IEmailGateway

class RegistrationService:
    def __init__(self, fs: IFileSystem, repo: IUserRepository, email: IEmailGateway) -> None:
        self.fs = fs
        self.repo = repo
        self.email = email

    def register(self, username: str, password: str) -> None:
        ...

That’s a lot of boilerplate just to tell what dependencies we need. Instead of that we can use attrs to create __init__ for us:

import attr
from myapp.lib import IFileSystem, IUserRepository, IEmailGateway

@attr.s(auto_attribs=True)
class RegistrationService:
    fs: IFileSystem
    repo: IUserRepository
    email: IEmailGateway

    def register(self, username: str, password: str) -> None:
        ...

Here’s an example using dataclasses library:

from dataclasses import dataclass
from myapp.lib import IFileSystem, IUserRepository, IEmailGateway

@dataclass()
class RegistrationService:
    fs: IFileSystem
    repo: IUserRepository
    email: IEmailGateway

    def register(self, username: str, password: str) -> None:
        ...

That way we don’t have to create constructor but we get one that we can use directly and with injectpy:

# fine to use directly:
service = RegistrationService(
    fs=InMemoryFileSystem(),
    repo=InMemoryUserRepository(),
    email=InMemoryEmailAdapter(),
)

# you can also use it with kernel out of the box:
inst = kernel.get(RegistrationService)

Inheritance

While composition is almost always preferred to inheritance - both attrs and dataclasses handle inheritance in a way that makes it really easy to use with injectpy:

from dataclasses import dataclass

@dataclass()
class BaseHandler:
    request: HttpRequest

@dataclass()
class MyHandler(BaseHandler):
    fs: IFileSystem

# it's possible to use the constructor directly:
my_handler = MyHandler(request=some_req, fs=InMemoryFileSystem())

# it's also fine to use it with injectpy
inst = kernel.get(MyHandler)