Skip to main content

Lagom, a type based dependency injection container

Project description

Lagom - Dependency injection container

Build Status Scrutinizer Code Quality Code Coverage PyPI

What

Lagom is a dependency injection container designed to give you "just enough" help with building your dependencies. The intention is that almost all of your code doesn't know about or rely on lagom. Lagom will only be involved at the top level to pull everything together.

An example usage can be found here: github.com/meadsteve/lagom-example-repo

Installation

pip install lagom
# or: 
# pipenv install lagom
# poetry add lagom

Usage

Everything in Lagom is based on types. To create an object you pass the type to the container:

container = Container()
some_thing = container[SomeClass]

Defining a singleton

container[SomeExpensiveToCreateClass] = SomeExpensiveToCreateClass("up", "left")

alternatively if you want to defer construction until it's needed:

container[SomeExpensiveToCreateClass] = Singleton(SomeExpensiveToCreateClass)

Defining a type that gets recreated every time

container[SomeClass] = lambda: SomeClass("down", "spiral")

if the type needs things from the container the lambda can take a single argument which is the container:

container[SomeClass] = lambda c: SomeClass(c[SomeOtherDep], "spinning")

if your construction logic is longer than would fit in a lambda a function can also be bound to the container:

@dependency_definition(container)
def my_constructor() -> MyComplexDep:
    # Really long
    # stuff goes here
    return MyComplexDep(some_number=5)

Alias a concrete instance to an ABC

container[SomeAbc] = ConcreteClass

Partially bind a function

Apply a function decorator to any function.

@bind_to_container(container)
def handle_some_request(request: typing.Dict, game: Game):
    # do something to the game
    pass

This function can now be called omitting any arguments that the container knows how to build.

# we can now call the following. the game argument will automagically
# come from the container
handle_some_request(request={"roll_dice": 5})

Invocation level caching

Suppose you have a function and you want all the dependencies to share an instance of an object then you can define invocation level shared dependencies.

class ProfileLoader:
    def __init__(self, loader: DataLoader):
        pass

class AvatarLoader:
    def __init__(self, loader: DataLoader):
        pass

@bind_to_container(container, shared=[DataLoader])
def handle_some_request(request: typing.Dict, profile: ProfileLoader, user_avatar: AvatarLoader):
    # do something to the game
    pass

now each invocation of handle_some_request will get the same instance of loader so this class can cache values for the invocation lifetime.

Alternative to decorator

The above example can also be used without a decorator if you want to keep the pure unaltered function available for testing.

def handle_some_request(request: typing.Dict, game: Game):
    pass

# This new function can be bound to a route or used wherever
# need
func_with_injection = container.partial(handle_some_request)

Loading environment variables

An experimental helper class is under beta in the experimental module. See documentation here: Env loading

Full Example

App setup

from abc import ABC
from dataclasses import dataclass

from lagom import Container

#--------------------------------------------------------------
# Here is an example of some classes your application may be built from


DiceApiUrl = NewType("DiceApiUrl", str)


class RateLimitingConfig:
    pass


class DiceClient(ABC):
    pass


class HttpDiceClient(DiceClient):

    def __init__(self, url: DiceApiUrl, limiting: RateLimitingConfig):
        pass


class Game:
    def __init__(self, dice_roller: DiceClient):
        pass

#--------------------------------------------------------------
# Next we setup some definitions

container = Container()
# We need a specific url
container[DiceApiUrl] = DiceApiUrl("https://roll.diceapi.com")
# Wherever our code wants a DiceClient we get the http one
container[DiceClient] = HttpDiceClient

#--------------------------------------------------------------
# Now the container can build the game object

game = container[Game]

Modifying the container instead of patching in tests

Taking the container from above we can now swap out the dice client to a test double/fake. When we get an instance of the Game class it will have the new fake dice client injected in.

def container_fixture():
    from my_app.prod_container import container
    return container.clone() # Cloning enables overwriting deps

def test_something(container_fixture: Container):
    container_fixture[DiceClient] = FakeDice(always_roll=6)
    game_to_test = container_fixture[Game]
    # TODO: act & assert on something

Integrations

Starlette (https://www.starlette.io/)

To make integration with starlette simpler a special container is provided that can generate starlette routes.

Starlette endpoints are defined in the normal way. Any extra arguments are then provided by the container:

async def homepage(request, db: DBConnection):
    user = db.fetch_data_for_user(request.user)
    return PlainTextResponse(f"Hello {user.name}")


container = StarletteContainer()
container[DBConnection] = DB("DSN_CONNECTION_GOES_HERE")


routes = [
    # This function takes the same arguments as starlette.routing.Route
    container.route("/", endpoint=homepage),
]

app = Starlette(routes=routes)

FastAPI (https://fastapi.tiangolo.com/)

FastAPI already provides a method for dependency injection however if you'd like to use lagom instead a special container is provided.

Calling the method .depends will provide a dependency in the format that FastAPI expects:

container = FastApiContainer()
container[DBConnection] = DB("DSN_CONNECTION_GOES_HERE")

app = FastAPI()

@app.get("/")
async def homepage(request, db = container.depends(DBConnection)):
    user = db.fetch_data_for_user(request.user)
    return PlainTextResponse(f"Hello {user.name}")

Flask API (https://www.flaskapi.org/)

A special container is provided for flask. It takes the flask app then provides a wrapped route decorator to use:

app = Flask(__name__)
container = FlaskContainer(app)
container[Database] = Singleton(lambda: Database("connection details"))


@container.route("/save_it/<string:thing_to_save>", methods=['POST'])
def save_to_db(thing_to_save, db: Database):
    db.save(thing_to_save)
    return 'saved'

(taken from https://github.com/meadsteve/lagom-flask-example/)

The decorator leaves the original function unaltered so it can be used directly in tests.

Django (https://www.djangoproject.com/)

A django integration is currently under beta in the experimental module. See documentation here: Django Integration Docs

Developing/Contributing

Contributions and PRS are welcome. For any large changes please open an issue to discuss first. All PRs should pass the tests, type checking and styling. To get development setup locally:

make install # sets up the pipenv virtualenv

then

make format # To format the code
make test # To make sure the build will pass

Design Goals

  • The API should expose sensible typing (for use in pycharm/mypy)
  • Everything should be done by type. No reliance on names.
  • All domain code should remain unmodified. No special decorators.
  • Usage of the container should encourage code to be testable without monkey patching.
  • Usage of the container should remove the need to depend on global state.
  • Embrace modern python features (3.7 at the time of creation)

Project details


Release history Release notifications | RSS feed

Download files

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

Source Distribution

lagom-0.10.0.tar.gz (38.5 kB view details)

Uploaded Source

Built Distribution

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

lagom-0.10.0-py2.py3-none-any.whl (19.6 kB view details)

Uploaded Python 2Python 3

File details

Details for the file lagom-0.10.0.tar.gz.

File metadata

  • Download URL: lagom-0.10.0.tar.gz
  • Upload date:
  • Size: 38.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.23.0

File hashes

Hashes for lagom-0.10.0.tar.gz
Algorithm Hash digest
SHA256 94356bde8f93e18c7ec1759b2bf800017adbed865c87cab71bb42962bd8d96c3
MD5 230598892b5dd5490b1ea54a9e552390
BLAKE2b-256 8dc7dc332683bf57dad82fd457bd6eba0de9d5271ca1d52dfc8a8f395be15d8f

See more details on using hashes here.

File details

Details for the file lagom-0.10.0-py2.py3-none-any.whl.

File metadata

  • Download URL: lagom-0.10.0-py2.py3-none-any.whl
  • Upload date:
  • Size: 19.6 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.23.0

File hashes

Hashes for lagom-0.10.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 2ceb40231ba844ed6f5bffc4c60dbd9c744681115187f87bf0be92ac6faab1e3
MD5 13c582b85c8f21d3f9980fbcb4836dea
BLAKE2b-256 796ff8c5812f045e067ec0e4bde88754a906332fbe4f19db3832086a77664b03

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