Skip to main content

Python Dependency Inversion made simple so you can focus on creating amazing code.

Project description

Bevy

Bevy makes using Dependency Injection a breeze so that you can focus on creating amazing code.

Installation

pip install bevy

Documentation

Dependency Injection

Put simply, Dependency Injection is a design pattern where the objects that your class depends on are instantiated outside of the class. Those dependencies are then injected into your class when it is instantiated. This promotes loosely coupled code where your class doesn’t require direct knowledge of what classes it depends on or how to create them. Instead your class declares what class interface it expects and an outside framework handles the work of creating the class instances with the correct interface.

Interfaces

Python doesn’t have an actual interface implementation like many other languages. Class inheritance, however, can be used in a very similar way since sub classes will likely have the same fundamental interface as their base class.

Why Do I Care?

Dependency Injection and its reliance on abstract interfaces makes your code easier to maintain:

  • Changes can be made without needing to alter implementation details in unrelated code, so long as the interface isn’t modified in a substantial way.
  • Tests can provide mock implementations of dependencies without needing to jump through hoops to inject them. They can provide the mock to the context and Bevy will make sure it is used where appropriate.

How Bevy Works

Bevy allows your class to say what dependencies it has by using undefined class attribute annotations. That’s a bit vague so here is an example:

class MyClass(Injectable):
    my_dependency: MyDependency

The class MyClasshas a dependency on the MyDependency interface. It wants this dependency to be made available with the attribute name my_dependency. This will allow your class to access an instance of MyDependencyas self.my_dependency, even from __init__ since the injection happens when __new__ is called.

It is important to note that Bevy ignores any class attribute that has been assigned to. For example:

my_dependency: MyDependency = MyDependency()

This will be ignored by the dependency resolver because it’s been assigned a value.

Dependency Resolution

Dependency resolution and injection is handled when __new__ is called. Bevy keeps a repository of all dependencies that have already been created in a context. This repository is used to look for each of the dependency class interfaces when a Bevy Injectable class is created. A dependency will be used from the repository only if it is the same class as the interface requested or a sub-class of that interface. If no match is found for an interface, an instance will be created without arguments and saved to the repository.

So, in short, all dependencies are guaranteed to be either the same class as the dependency’s interface or a sub class of that interface.

How To Customize Dependencies

If you need to instantiate a dependency with arguments or provide an alternate implementation of a dependency you can create a custom context.

from bevy import Context

context = Context().add(MyDependency(foo="bar"))
app = context.create(MyApp, "some instantiation args")

It is important to note that Context.create does not add the instance returned to the context repository. If that is necessary use Context.get, or if you need to pass instantiation arguments use Context.load passing the instance returned by Context.create.

Dependency Factories

It is also possible to create a factory for any of your dependencies. Instances generated by a factory will not be added to the context repository since they will not be unique in the context. Creating a factory is as simple as annotating a class attribute with the bevy factory class and telling it what dependency type it should create.

from bevy import Injectable, Factory


class MyApp(Injectable):
    factory: Factory[My_Dependency]

    def get_instance(self, name: str) -> My_Dependency:
        return self.factory(name)

Accessing The Context

You can give a Bevy Injectable object access to the context that created it by adding a class attribute annotated with the Context type. This will cause the current context instance to inject itself into your class.

from bevy import Injectable, Context


class MyApp(Injectable):
    context: Context

Scoped Contexts

It is possible to branch a context to create a child context which has access to everything in its repository and in the repositories of its parent contexts, while the parents do not have access to the repository of the child. This might be used for a plugin system where you’d want the plugin to have access to the dependencies of the app but you wouldn’t want the plugin to pollute the app’s context.

class MyApp(Injectable):
    context: Context

    def __init__(self, plugins: List[Type[Plugin]]):
        self.plugins = self.load_plugins(plugins)

    def load_plugins(self, plugins: List[Type[Plugin]]) -> List[Plugin]:
        plugin_instances = []
        for plugin in plugins:
            instance = self.context.branch().create(plugin)
            plugin_instances.append(instance)
        return plugin_instances

Injector Dependencies

A dependency class object can implement the bevy.injector.InjectorProtocol by defining a bevy_inject classmethod. The context manager will call this method when it is injecting dependencies into a new instance of an injectable class. The bevy_inject method will be passed the context, the partial instance being built, and all arguments passed to the constructor. It should return an instance of the injector dependency class.

class MyInjector:
    def __bevy_injector__(self, context: bevy.Context, instance, *args, **kwargs):
        return MyInjector()
    ...

Driving Motivations

The motivations that drive the decisions about how Bevy is implemented are as follows.

  • It should feel like nothing has been changed from normal.
  • IDEs should be able to understand what is happening.
  • Everything should work independently.

Future

  • Add support for circular dependencies. Likely use descriptors to lazily inject dependencies.
  • Add more tests that cover cases beyond the main use cases.

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

Bevy-0.4.7.tar.gz (11.6 kB view hashes)

Uploaded Source

Built Distribution

Bevy-0.4.7-py3-none-any.whl (11.1 kB view hashes)

Uploaded Python 3

Supported by

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