Coder Social home page Coder Social logo

serum's Introduction

Build Status

CircleCI codecov

Description

serum is a fresh take on Dependency Injection in Python 3.

serum is pure python, has no dependencies, and is less than 300 lines of code.

Installation

> pip install serum

Quickstart

from serum import *

class Log(Component):
    @abstractmethod
    def info(self, message: str):
        pass

class SimpleLog(Log):
    def info(self, message: str):
        print(message)

class StubLog(Log):
    def info(self, message: str):
        pass

class NeedsLog:
    log = inject(Log)

NeedsLog().log.info('Hello serum!')  # raises: NoEnvironment

with Environment():
    NeedsLog().log.info('Hello serum!')  # raises: UnregisteredDependency

with Environment(SimpleLog):
    NeedsLog().log.info('Hello serum!')  # outputs: Hello serum!

with Environment(StubLog):
    NeedsLog().log.info('Hello serum!')  # doesn't output anything

class NeedsSimpleLog:
    log = inject(SimpleLog)

with Environment():
    NeedsSimpleLog().log.info('Hello serum!')  # outputs: Hello serum!

Documentation

serum uses 3 main abstractions: Component, Environment and inject.

Components are dependencies that can be injected.

from serum import Component, Environment, inject

class Log(Component):
    def info(self, message):
        print(message)

class NeedsLog:
    log = inject(Log)

instance = NeedsLog()
with Environment():
    assert isinstance(instance.log, Log)

Environments provide implementations of Components. An Environment will always provide the most specific subtype of the requested type (in Method Resolution Order).

class MockLog(Log):
    def info(self, message):
        pass

with Environment(MockLog):
    assert isinstance(instance.log, MockLog)

It is an error to inject a type in an Environment that provides two or more equally specific subtypes of that type:

class FileLog(Log):
    _file = 'log.txt'
    def info(self, message):
        with open(self._file, 'w') as f:
            f.write(message)

with Environment(MockLog, FileLog):
    instance.log  # raises: AmbiguousDependencies: Attempt to inject type <class 'Log'> with equally specific provided subtypes: <class 'MockLog'>, <class 'FileLog'>

Environments can also be used as decorators:

test_environment = Environment(MockLog)

@test_environment
def f():
    assert isinstance(instance.log, MockLog)

You can only provide subtypes of Component with Environment.

class C:
    pass

Environment(C)  # raises: InvalidDependency: Attempt to register type that is not a Component: <class 'C'> 

Similarly, you can't inject types that are not Component subtypes.

class InvalidDependent:
    dependency = inject(C)  # raises: InvalidDependency: Attempt to inject type that is not a Component: <class 'C'>

Injected Components can't be accessed outside an Environment context:

instance.log  # raises NoEnvironment: Can't inject components outside an environment 

Injected Components are immutable

with Environment():
    instance.log = 'mutate this'  # raises AttributeError: Can't set property

You can define mutable static fields in a Component. If you want to define immutable static fields (constants), serum provides the immutable utility that also supports type inference with PEP 484 tools.

from serum import immutable

class Immutable(Component):
    value = immutable(1)

i = Immutable()
i.value = 2  # raises AttributeError: Can't set property

This is just convenience for:

class Immutable(Component):
    value = property(fget=lambda _: 1)

Components can only define an __init__ method that takes 1 parameter.

class ValidComponent(Component):  # OK!
    some_dependency = inject(SomeDependency)
    def __init__(self):
        self.value = self.some_dependency.method()

class InvalidComponent(Component):  # raises: InvalidComponent: __init__ method in Components can only take 1 parameter
    def __init__(self, a):
        self.a = a

To construct Components with dependencies, you should instead use inject

class ComponentWithDependencies(Component):
    log = inject(Log)

Note that if you access injected members in the constructor of any type, that type can only be instantiated inside an environment.

Components can be abstract. Abstract Components can't be injected in an Environment that doesn't provide a concrete implementation. For convenience you can import abstractmethod, abstractclassmethod or abstractclassmethod from serum, but they are simply references to the equivalent decorators from the abc module in the standard library.

from serum import abstractmethod

class AbstractLog(Component):
    @abstractmethod
    def info(self, message):
        pass
        
class NeedsLog:
    log = inject(AbstractLog)

instance = NeedsLog()
with Environment():
    instance.log  # raises UnregisteredDependency: No concrete implementation of <class 'AbstractLog'> found

class ConcreteLog(AbstractLog):
    def info(self, message):
        print(message)

with Environment(ConcreteLog):
    instance.log  # Ok!
 

Environments are local to each thread. This means that when using multi-threading each thread must define its own environment.

import threading

def worker_without_environment():
    NeedsLog().log  # raises NoEnvironment: Can't inject components outside an environment

def worker_with_environment():
    with Environment(ConcreteLog):
        NeedsLog().log  # OK!

with Environment(ConcreteLog):
    threading.Thread(target=worker_without_environment()).start()
    threading.Thread(target=worker_with_environment()).start()

serum has support for injecting MagicMocks from the builtin unittest.mock library in unittests using the mock utility function. mock is only usable inside an an environment context. Mocks are reset when the environment context is closed.

from serum import mock

environment = Environment(ConcreteLog)
with environment:
    log_mock = mock(AbstractLog)
    log_mock.method.return_value = 'some value'
    instance = NeedsLog()
    assert instance.log is log_mock
    assert instance.log.method() == 'some value'

with environment:
    instance = NeedsLog()
    assert instance.log is not log_mock
    assert isinstance(instance.log, ConcreteLog)

mock(AbstractLog)  # raises: NoEnvironment: Can't register mock outside environment

Note that mock will only mock requests of the exact type supplied as its argument, but not requests of more or less specific types

from unittest.mock import MagicMock
class Super(Component):
    pass

class Sub(Super):
    pass

class SubSub(Sub):
    pass

class NeedsSuper:
    injected = inject(Super)

class NeedsSub:
    injected = inject(Sub)

class NeedsSubSub:
    injected = inject(SubSub)

with Environment():
    mock(Sub)
    needs_super = NeedsSuper()
    needs_sub = NeedsSub()
    needs_subsub = NeedsSubSub()
    assert isinstance(needs_super.injected, Super)
    assert isinstance(needs_sub.injected, MagicMock)
    assert isinstance(needs_subsub.injected, SubSub)

serum is designed for type inference with PEP 484 tools (work in progress). This feature is currently only supported for the PyCharm type checker.

type inference in PyCharm

Why?

If you've been researching Dependency Injection frameworks for python, you've no doubt come across this opinion:

You dont need Dependency Injection in python. You can just use duck typing and monkey patching!

The position behind this statement is often that you only need Dependency Injection in statically typed languages.

In truth, you don't really need Dependency Injection in any language, statically typed or otherwise. When building large applications that need to run in multiple environments however, Dependency Injection can make your life a lot easier. In my experience, excessive use of monkey patching for managing environments leads to a jumbled mess of implicit initialisation steps and if value is None type code.

In addition to being a framework, I've attempted to design serum to encourage designing classes that follow the Dependency Inversion Principle:

one should “depend upon abstractions, not concretions."

This is achieved by letting inheritance being the principle way of providing dependencies and allowing dependencies to be abstract. See the example.py for a detailed tutorial (work in progress).

serum's People

Watchers

James Cloos avatar Aditya Narayan Singh avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.