Skip to main content

Brain-dead simple dependency injection

Project description

OOZE - Brain-dead simple dependency injection

Ooze is an attempt to do depdency injection in Python in the simplest way possible. It embraces Python decorators to leverage what classes, functions, and even static values are included in the dependency injection graph. You can get started in three easy steps:

  • decorate your functions, classes and/or variable items
  • assign a startup function
  • call ooze's run() function

That's it! Here's a quick example:

import ooze

@ooze.provide                       # Inject as 'upper_case' since a name wasn't specified
def upper_case(string):
    return string.upper()


ooze.provide_static('address', {    # Inject a static dictionary, naming it 'address'
    "name": "Steve",
    "gender": "male"
})


@ooze.provide('greeter')            # Inject as 'greeter'
class WelcomeWagon:
    def __init__(self, upper_case, address):
        self.address = address
        self.upper = upper_case

    def greet(self):
        return self.upper(f"Hello {self.address['name']}")


@ooze.startup               # Define where ooze should start running your program
def main(greeter):
    print(greeter.greet())


if __name__ == '__main__':
    ooze.run()

Installing Ooze

Installing Ooze is as simple as using pip:

$ pip install ooze

Usage Notes

The notes below go into more detail about the individual compoenents of the ooze package.

@ooze.provide

The ooze.provide decorator is used to add functions, classes and other static items to the DI graph. Every item added to the graph needs to have a name. The Ooze.provide decorator can take the name as an argument. In the example below the name provided to Ooze is file_loader.

@ooze.provide('file_loader')   # name given as 'file_loader'
def load_files(http_client):
    pass

If the item being added to the graph is a class or function, the name provided is optional. In this case, if a name is not given to ooze.provide, it will default to using the class or function's name. This time, in the example below, Ooze will determine the name to be load_files.

@ooze.provide                  # no name given, Ooze assumes 'load_files'
def load_files(http_client):
    pass

Ooze uses argument names to determine what dependencies are needed to fulfill a request. In both of the above examples, Ooze will look for an item in the dependency graph named, http_client to inject into the load_files function when executing it. If it can't find an item named http_client it will throw a InjectionError exception.

If the item being decorated with ooze.provide is a function or a static item (string, dict, etc.) it is directly added to the ooze dependency graph.

If, however, the item being decorated with ooze.provie is a class, Ooze will try to instantiate the class and will add the resulting object instance to the dependency grap. Ooze will attempt to inject any needed dependencies into the class constructor (__init__) when instantiating the class.

@ooze.provide           # no name given, ooze assumes 'databaseclient'
class DatabaseClient:
    def __init__(connection_string):
        self.connection_string = connection_string

In the above example, Ooze will try to find a connection_string in the graph and will provide it when instantiating the DatabaseClient. It will place the newly instantiated DatabaseClient instance into the dependency graph.

Note: When adding a class to the graph, you can still name it yourself, but if you don't, Ooze will lower-case the name of you class when adding it. This is because, by convention, argument names to functions are always lower-case. Since the name name of item in the depdency graph will be used as function/method arguments, this is reasonable.

Additional node: Ooze is aggressive and will instantiate all class instances at application startup, not when they are first used.

Providing static values

Adding static items to the dependency graph is similarly easy by using the ooze.provide_static() method.

ooze.provide_static('version', '1.2.0')
ooze.provide_static('config', {
    'env': 'dev',
    'url': 'http://contacts.dev.api.org/'
})

In the above example, two static items are being added to the dependency graph:

  • A string: '1.2.0' is added, named as 'version'
  • A dictionary with keys: 'env' and 'url' added, named as 'config'

You can add any static item to the DI graph that you want. You just have to name it yourself.

@ooze.startup

While using the @ooze.provide decorator is how you add items to the dependency graph, it doesn't specify when/how to use them. Ooze doesn't do anything with DI graph until told to do so. That's where the ooze.run() function comes in. When you call ooze.run() that sets Ooze into motion. Ooze will look for a starting point and then will run it. The starting point has to be a function, often called main or startup.

Ooze can be told where the starting point is in one of 2 ways. It can be told directly by an argument passed to the ooze.run() function:

def main(http_client):
    pass

if __name__ == '__main__':
    result = ooze.run(main)         # Tell it directly
    print(f"Result: {result}")

Or if no startup function is passed to the ooze.run() function, it will look for a function decorated with @ooze.startup:

@ooze.startup
def main(http_client):
    pass

if __name__ == '__main__':
    result = ooze.run()             # Infer startup from the @ooze.startup decorator
    print(f"Result: {result}")

In both cases, ooze.run() will return the results of calling the startup function so you can use them in any further processing.

The ooze.run() function will try to resolve any dependencies that the startup function has using what it finds in the dependency graph.

Factories

Sometimes the dependency isn't available at the time your script starts up and needs to be loaded at runtime: reading a file, result of a web service call, etc. Ooze makes this easy by providing factories. When you mark a function (or any callable) as a factory, instead of injecting that function as the dependency, it will call the function and inject it's return value as the dependency.

@ooze.factory('config')
def lookup_config():
    with open('config.json') as infile:
        return json.load(infile)

@ooze.provide('db')
class DatabaseManager:
    def __init__(self, config: dict):
        self.config = config

    def get_connection(self):
        pass

In the above example, when the db dependency is resolved, a new DatabaseManager object will be instantiated. It will have a config injected into it. Ooze will not inject the lookup_config function, but rather it will CALL the lookup_config function and inject it's return value as config into the DatabaseManager.

Just like @ooze.provide, @ooze.factory optionally takes a name to use when adding the factory to the dependency graph.

Manually retrieving items

Normally dependency injector items are only accessed from within other running dependency items. From time to time, however, there is a need to gain access to a dependency injector item from outside ooze. This can be accomplished using the ooze.resolve() function:

@app.get('/api/<customer_id>')
def get_customer(customer_id):
    repo = ooze.resolve('database_repository')
    serializer = ooze.resolve('json_serializer')
    return serializer(repo.get_customer(customer_id))

Magic environment variable lookups

When ooze is injecting dependencies into your function/class, it will first search the DI graph of instances. If it can't find an instance it will then look through the registered factories (see @ooze.factory above). After that, it will search through your environment variables to try and find a value to inject. This makes injecting settings from your OS (often populated from container secrets - i.e. Docker and/or Kubernetes secrets) into your script.

#!/usr/bin/env python
"""main.py"""

import ooze

@ooze.provide
def report_version(version):
    return f"Current version: {version}"

def main(report_version):
    print(report_version())

ooze.run(main)

Then

$ export version="999.99"
$ python main.py
Current version: 999.99
$

Ooze will try looking in the environment for your dependency as-is with the case you specify in your function arguments. If it can't find it, it will try upper-case and lower-case before failing.

Other modules and packages

Ooze decorators can be used across modules and packages in the event you want/need to write a non-trvial Python application. There is only one stipulation: Any modules or packages that you want to participate with Ooze must be imported into the Python process.

For example, you may have module file_reader.py that has the following code:

import ooze

@ooze.provide
class FileReader(datafile):

    def read(self):
        with open(datafile) as infile:
            return infile.read()

You may also have a module named main.py that has the following code:

import ooze
import file_reader          # You gotta import this or Ooze won't see it.

ooze.provide('datafile')('/tmp/stuff.txt')

def main_func(filereader):
    print(filereader.read())

if __name__ == '__main__':
    ooze.run(main_func)

When you run main.py, main_func will have the FileReader injected into it as you'd expect, but only if the import file_reader line is present. If you were to fail to import the file_reader module, Ooze wouldn't have an opportunity to add the FileReader instance to the dependency graph.

Integration with bottle

Ooze can be easily integrated with bottle using the OozeBottlePlugin. Simply .install() the plugin and Ooze dependencies will automatically be injected into your bottle functions:

import ooze
from bottle import Bottle

@ooze.provide
def add_numbers(x, y):
    return x + y

app = Bottle()
app.install(ooze.OozeBottlePlugin())

@app.get('/add/<x>/<y>')
def web_add(x, y, add_numbers):
    return {
        'sum': add_numbers(x, y)
    }

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

ooze-0.0.7.tar.gz (46.1 kB view hashes)

Uploaded Source

Built Distribution

ooze-0.0.7-py3-none-any.whl (31.2 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