Skip to main content

Dependency management for python

Project description

Seamful is a dependency injection library for Python.

It aims to facilitate writing integration tests that replace the minimum amount of dependencies with test doubles. It incentivizes using working test doubles as opposed inspectable mocks.

Seamful intervenes by instantiating the objects and their dependencies. While typically one would manually instantiate these:

my_service = MyService(MyRepository(...))
my_service.do_something()

If instantiation happens "close" to the actual working code, it is hard to replace the dependencies with test_doubles.

Seamful introduces a way of declaring which are these classes that are meant to be instantiated, and recipes for how to build them. Moreover it provides a way of configuring those recipes so that, for example, a test environment can depart from the production configuration just for a few of those classes.

For declaring which classes are meant to be instantiated, Seamful uses a concept called "Module". A module is a class which contains a list of resources, which are the classes that are meant to be instantiated. For example:

from seamful import Module, Resource


class PaymentsModule(Module):
    service = Resource(PaymentsService)
    repository = Resource(PaymentsRepository)

This states that the PaymentsModule has two resources: service and repository. It also states that they ought to be instances of PaymentsService and PaymentsRepository respectively, but it doesn't say how to build them. Since we want to be able to build them in more than one way, we want to put those recipes in a different declaration, which we call a Provider.

A provider is a class that's bound to a specific Module and specifiies builder methods for all the resources in that Module. For example:

class PaymentsProvider(Provider, module=PaymentsModule):

    def get_repository(self) -> PaymentsRepository:
        return PaymentsRepository(...)

    def get_service(self, repository: PaymentsModule.repository) -> PaymentsService:
        return PaymentsService(repository)

While the first method seems straightforward, the second is more surprising. Instead of get_service calling self.get_repository(), it just "asks" for the resource it wants as a function parameter. This are pretty much the main rules for defining a provider method:

  • must be called get_<name_of_the_resource>
  • doesn't use the self parameter
  • if it needs an instance of another resource, from it's module or another module, it requests it as a parameter.
  • annotates its parameter types and return type.
  • returns an instance of the resource it is meant to build.

Also, declaring a provider also need to follow a few rules:

  • it must inherit from Provider
  • it must state which module it provides for via the module= parameter
  • it must have a method for each resource in the module, with the name get_<name_of_the_resource>

Once you have at least one Module and one Provider, you can wire them together in a Application. A application is the entry point for both registering modules and providers and for finally requesting module resources.

They're built as follows:

from seamful import Application

application = Application.empty()
application.install_module(PaymentsModule, PaymentsProvider)
application.ready()

Once a application registered some modules and providers, it can become ready for providing resources. application.ready() checks that the module resources can be built by following the dependency graphs of the providers, which usually become more complex than the example above.

The application can be built and registered as a pubilc, global variable in your application. Then, in the entry points of your application you can import that application and ask for any of the registered modules resources. For example:

from .application import application

class Main:
    def __init__(self):
        self.service = application.provide(PaymentsModule.service)

    def run(self):
        service.do_something()

You can actually use the application anywhere in your application, but it's recommended to use it only in the entry points. Every class that explicitly uses a application will be harder to use in a different use case.

This application can run as-is in production, but it can also be used in integration tests, where the application can be configured to provide a different implementation of some of those resources. Let's imagine that the PaymentsRepository uses a database that we wish not to use in our integration tests. We can create a test_double:

class InMemoryPaymentsRepository(PaymentsRepository):
    def __init__(self):
        self.payments = []

    def save_payment(self, payment: Payment) -> int:
        self.payments.append(payment)
        return len(self.payments) - 1

    def get_payment(self, id: int) -> Payment:
        return self.payments[id]

    def list_payments(self) -> Iterable[Payment]:
        return self._payments[:]

And then we can configure the application to provide that implementation instead of the real one:

import unittest
from .application import application


class PaymentProviderForTests(PaymentsProvider):
    def get_repository(self) -> PaymentsRepository:
        return InMemoryPaymentsRepository()


class TestPayments(unittest.TestCase):
    def setUp(self):
        application.tamper()
        application.install_provider(PaymentProviderForTests)
        application.ready()

    def tearDown():
        application.restore()

    def test_payment_processes_successfully(self):
        service = application.provide(PaymentsModule.service)
        service.process_new_payment(...)
        self.assertEqual(1, len(service.repository.list_payments()))

So far this seems like a lot of boilerplate for something that could be done with a few lines of code and Mocks. The expectation is that as your application grows bigger, the impact of this boilerplate will be less than manually instantiating the object graph manually.

Also, the ability to override only those dependencies that ought to be different in tests ensures that the code being tested is as close as possible as the real application.

Finally, even the outermost part of the application could be tested in this setup. For example:

class TestPayments(unittest.TestCase):
    ...

    def test_command_line_application():
        main = Main()
        repository = application.provide(PaymentsModule.repository)
        main.run(...)
        self.assertEqual(repository.get_payment(0), Payment(...))

This example is rather basic but it should help get you started.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

seamful-0.1.0.tar.gz (42.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

seamful-0.1.0-py3-none-any.whl (83.2 kB view details)

Uploaded Python 3

File details

Details for the file seamful-0.1.0.tar.gz.

File metadata

  • Download URL: seamful-0.1.0.tar.gz
  • Upload date:
  • Size: 42.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.5.1 CPython/3.10.6 Linux/5.19.0-46-generic

File hashes

Hashes for seamful-0.1.0.tar.gz
Algorithm Hash digest
SHA256 325e2fa558a8b08a030f7f20f1404aaf47ca65ea46d7058a3815a54444f08592
MD5 325611e34289836bad120173d47ad508
BLAKE2b-256 7602a053b4b68431d519601ba18270d562f624aa011ceb212aaf58c00d704253

See more details on using hashes here.

File details

Details for the file seamful-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: seamful-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 83.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.5.1 CPython/3.10.6 Linux/5.19.0-46-generic

File hashes

Hashes for seamful-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 69539a78dce5362aa6fbc6c9f1e66b5b0eb88052e46b3f07d5cc2dcb4d96e49b
MD5 661e37b674edf61bb2c6db4da356fedb
BLAKE2b-256 624906e94658fc0fe30925062445c29db7032cc5bb97cfe5c893ee7785ca0b2b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page