ekiro / haps Goto Github PK
View Code? Open in Web Editor NEWPure Python dependency injection library
License: MIT License
Pure Python dependency injection library
License: MIT License
Lets assume such a situation
from abc import ABC, abstractmethod
from haps import base, egg, Container as IoC, PROFILES
from haps.config import Configuration
@base
class IMailService(ABC):
@abstractmethod
def send_mail(self, text: str, to: str, **kwargs):
pass
@egg
class MailService(IMailService):
def send_mail(self, text: str, to: str, **kwargs):
# Send mail here
pass
@egg(profile='mail_disabled')
class DummyMailService(IMailService):
def send_mail(self, text: str, to: str, **kwargs):
pass
profiles = ['mail_disabled']
Configuration().set(PROFILES, profiles)
IoC.autodiscover(['app'])
When it comes to testing I need to configure test profiles like this:
profiles = ['mail_disabled']
Configuration().set(PROFILES, profiles)
IoC.autodiscover(['app'])
Assume I have more than one service like MailService
with Dummy
implementation, then the configuration will look like this:
profiles = ['mail_disabled', 'serviceX_disabled', 'serviceY_disabled', ...]
Configuration().set(PROFILES, profiles)
IoC.autodiscover(['app'])
It's a lot of profiles and we need to update base tests configuration every time we add a new service...
We can make it a lot easier with defining multiple profiles on eggs like
...
@egg(profiles=['mail_disabled', 'test'])
class DummyMailService(IMailService):
def send_mail(self, text: str, to: str, **kwargs):
pass
Then our testing configuration will be simply:
profiles = ['test']
Configuration().set(PROFILES, profiles)
IoC.autodiscover(['app'])
I often run into the case where I don't actually want to define an interface for my implementation, but still make use of the autodiscovery and DI for instances of that class.
The way I usually go about doing this with haps is to define both the base
and egg
decorators:
@base
@egg
@scope(SINGLETON_SCOPE)
class MySimpleService:
pass
A quick search on my current project yields 12 occurences of this combination of decorators and I'm wondering if it might make sense to introduce a simplified decorator for marking a class as "injectable" (i.e. acting as both the base and egg).
I am aware that one of the purposes of dependency injection is to decouple an interface from its implementation (e.g. for testability), but especially for private projects with fewer test cases I'd still like to make use of the control inversion and let a library figure out the autowiring of my application.
I couldn't come up with a very nice API for this concept, so I'll just throw "@injectable(scope=SINGLETON_SCOPE)
" and "@base_egg
" in the room.
Now it's possible to omit some function parameters during injection, but that requires not annotating them. Every annotated parameter has to be registered as a dependency.
@inject
def f(s1: IService, s2: IService2, s3):
pass
f(some_service)
Inject should have 2 optional forms, without
and only
@inject.without('s3')
def f(s1: IService, s2: IService2, s3: IService3):
pass
@inject.only('s1', 's2')
def f2(s1: IService, s2: IService2, s3: IService3):
pass
The "too many bases" error does not tell you which bases haps has found during autodiscovery:
"More than one base found for %r" % implementation
The error message could easily tell you which base classes were found as this parameter is available.
Suppose the following case:
ClientFoo
from an external library "Ext". ClientFoo
is very complex and it is difficult to do composition over inheritance, and I don't want to extract an interface either as the third-party package might change in the future, causing unnecessary maintenance effort.@inject
ClientFoo
from "Ext" in some type initializers, so I annotate it with @base
and @egg
.ClientFoo
with some custom functionality, so I create a subclass ClientBar
. It would be valid to inject ClientFoo
into types of "Shared", but not necessarily into all the types of "App" because of the extra features required. In "Shared", I would like to rely only on ClientFoo
so that its types become reusable but still easy to instantiate, whereas I want to receive concrete ClientBars
in "App".To solve this, my first thought was to annotate ClientBar
with both @base
and @egg
aswell, but that violates the "single base" principle.
Is it possible to overcome this issue with the constraints given above? Alternatively, is there a simple way to monkeypatch haps to allow using multiple bases in particular cases?
In order to make use of the awesome python-decouple library in conjunction with haps, I wrote the following monkeypatch:
from typing import Any
import decouple
from haps.config import Configuration
from haps.exceptions import UnknownConfigVariable
def decouple_resolver(self: Configuration, var_name: str) -> Any:
"""
Monkeypatched resolver for using python-decouple together with haps.
Gives priority to explicitly-declared resolvers in the application, then falls back to retrieving the value
from decouple.
"""
if var_name in self.resolvers:
return self.resolvers[var_name]()
env_var = decouple.config(var_name)
if env_var:
return env_var
raise UnknownConfigVariable(f"No resolver registered for {var_name}")
# Override haps default resolver
Configuration._resolve_var = decouple_resolver
It occured to me that it would be nice to be able to provide custom variable resolvers for arbitrary keys (not just specified ones) to haps which it will use in a configurable lookup order (aka priority). The advantage of using decouple is that it already searches in a bunch of settings repositories (.env
, settings.ini
, env vars) and so simply adding it as a fallback is enough, but people might also want to add their own settings sources (some YAML or JSON files or whatever).
When running pytest
the configure_ioc()
fixture is called first and then while loading application create_application()
is called and that call produces haps.exceptions.ConfigurationError: Value for haps.profiles already set
.
/api/resources/api.py
from haps import Container as IoC, PROFILES
from haps.config import Configuration
def create_application() -> API:
profiles = ['profile1', 'profile2']
Configuration().set(PROFILES, profiles)
IoC.autodiscover(['app1', 'app2'])
/tests/conftest.py
import pytest
from haps import Container as IoC, PROFILES
@pytest.fixture(scope="session", autouse=True)
def configure_ioc():
# Initialize haps IoC Container
profiles = ['test']
Configuration().set(PROFILES, profiles)
IoC.autodiscover(['app1', 'app2'])
The same situation is with Alembic migrations env.py
file, which is called through alembic
command and imports the app there.
Error:
haps.exceptions.ConfigurationError: Value for haps.profiles already set
/alembic/env.py
from haps import Container as IoC, PROFILES
from haps.config import Configuration
from alembic import context
cfg = Configuration()
cfg.set(haps.PROFILES, ())
IoC.autodiscover(['app1', 'app2'])
context.config.set_main_option('sqlalchemy.url', cfg.get_var('db_url'))
...
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.