Skip to content

Touchstone is an annotations-driven Inversion of Control container for Python 3.6 and above.

License

Notifications You must be signed in to change notification settings

rhocairn/touchstone

Repository files navigation

Touchstone

What is Touchstone?

Touchstone is an annotations-driven Inversion of Control container for Python 3.6 and above.

Links:

Learn by Example

Auto Wiring

from touchstone import Container

class Child:
    pass

class Parent:
    def __init__(self, child: Child) -> None:
        self.child = child


container = Container()
parent = container.make(Parent)

assert isinstance(parent.child, Child)

Interface Binding

from touchstone import Container

class AbstractChild:
    pass

class Child(AbstractChild):
    pass

class Parent:
    def __init__(self, child: AbstractChild) -> None:
        self.child = child


container = Container()
container.bind(AbstractChild, Child)
parent = container.make(Parent)

assert isinstance(parent.child, Child)

Binding with Factory Methods

from touchstone import Container

class Child:
    def __init__(self, name: str) -> None:
        self.name = name

class Parent:
    def __init__(self, child: Child) -> None:
        self.child = child


container = Container()
container.bind(Child, lambda: Child('them'))
parent = container.make(Parent)

assert isinstance(parent.child, Child)
assert parent.child.name == 'them'

Binding Singletons

from touchstone import Container, SINGLETON

class Child:
    def __init__(self, name: str) -> None:
        self.name = name

class Parent:
    def __init__(self, child: Child) -> None:
        self.child = child


container = Container()
them_child = Child('them')
container.bind_instance(Child, them_child)
# Or...
container.bind(Child, lambda: them_child, lifetime_strategy=SINGLETON)
parent = container.make(Parent)

assert isinstance(parent.child, Child)
assert parent.child is them_child

Contextual Binding

from touchstone import Container

class Child:
    def __init__(self, name: str) -> None:
        self.name = name

class Parent:
    def __init__(self, child1: Child, child2: Child) -> None:
        self.child1 = child1
        self.child2 = child2


container = Container()
container.bind_contextual(when=Parent, wants=Child, wants_name='child1', give=lambda: Child('her'))
container.bind_contextual(when=Parent, wants=Child, wants_name='child2', give=lambda: Child('him'))
parent = container.make(Parent)

assert isinstance(parent.child1, Child)
assert isinstance(parent.child2, Child)
assert parent.child1.name == 'her'
assert parent.child2.name == 'him'

Django Support

Now featuring Django support! New in v0.3.0

  • Configure your instance of touchstone.Container however you see fit.
  • In your main settings.py, set TOUCHSTONE_CONTAINER_GETTER to the path to a callable that will return the singleton instance of touchstone.Container your app uses.
    • Note that your getter should build the Container precisely once, and then return that same Container on all subsequent calls. Build it as a singleton.

To get injected properties in your class-based views:

  • In your main settings.py, add touchstone.django.InjectViewsMiddleware to your MIDDLEWARE list.
  • Use class annotations on your class-based views. Cached Properties will be added to your view classes so that they automatically resolve using your configured touchstone container. For example:
class MyView(View):
    something: MyObject
    def get(self, request):
        # You can now access self.something!

To get injected properties in your middleware, you'll need to do a little more work because we haven't found a good way to hook into Django's middleware instantiation logic.

from touchstone.django import inject_magic_properties

@inject_magic_properties
class MyMixin:
    something: MyObject
    # define your mixin here...
    # You'll be able to use `self.something` from within every instance method.

Celery Tasks

Celery tasks called can be applied to any callable with a decorator. For example:

class MyLogger:
    def __init__(self, another_logger: AnotherLogger):
        self.another_logger = another_logger

    def log(self, msg):
        print(msg)
        self.another_logger.log(msg)

@app.task
def log_messages(msg):
    another_logger = AnotherLogger()
    my_logger = MyLogger(another_logger)

    my_logger.log(msg)

log_messages.apply_async(args=['hello world'])

However, if we want to refactor this code to make use of touchstone as a service locator:

class MyLogger:
    def __init__(self, another_logger: AnotherLogger):
        self.another_logger = another_logger

    def log(self, msg):
        print(msg)
        self.another_logger.log(msg)

@app.task
def log_messages(msg):
    container = get_container()
    my_logger = container.make(MyLogger)

    my_logger.log(msg)

log_messages.apply_async(args=['hello world'])

Each task would then have quite a bit of repeated code to create the container for each task and then make class instance we need.

class MyLogger:
    def __init__(self, another_logger: AnotherLogger):
        self.another_logger = another_logger

    def log(self, msg):
        print(msg)
        self.another_logger.log(msg)

@touchstone_task
class LogMessagesTask:
    my_logger: MyLogger

    def run(self, msg):
        self.my_logger.log(msg)

LogMessagesTask.apply_async(args=['hello world'])

About

Touchstone is an annotations-driven Inversion of Control container for Python 3.6 and above.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published