maldoinc / wireup Goto Github PK
View Code? Open in Web Editor NEWPerformant, concise, and easy-to-use dependency injection container for Python 3.8+.
Home Page: https://maldoinc.github.io/wireup/
License: MIT License
Performant, concise, and easy-to-use dependency injection container for Python 3.8+.
Home Page: https://maldoinc.github.io/wireup/
License: MIT License
Hiya! Greatly enjoying this lib; we are exploring DI in one of our services and I've got a PR to add wireup and just use the singleton; the only issue the team has run into is we're not able to inject a mock into the flask route handler. Normally, like with services, we just directly use the target object and instantiate it in the unit test with mocks, however this won't work for flask endpoints. Consider the following code
@app.route("/fake/route/<route_param_one>")
@container.autowire
def import_setup(
route_param_one: str, greeter: GreeterService
) -> Response:
name = request.args.get("name")
greeter.greet(name, route_param_one)
return make_response("OK")
And this unit test with flask's test app mechanism
class TestImportSetup(TestCase):
def setUp(self) -> None:
self.app = app.test_client()
def test_missing_vendor(self) -> None:
result = self.app.get("/fake/route/fakedata?name=Cody")
# How to provide a fake GreeterService if app entry point has already wired it up?
self.assertEqual(200, result.status_code)
self.assertEqual("Ok", result.text)
I've tried setting up multiple containers to have an empty test one and a normal prod one but it has proven... quite difficult to use.
Could we perhaps have a context manager style solution?
class TestImportSetup(TestCase):
def setUp(self) -> None:
self.app = app.test_client()
self.mock_greeter = Mock()
def test_missing_vendor(self) -> None:
with container.override_inject(GreeterService, self.mock_greeter):
result = self.app.get("/fake/route/fakedata?name=Cody")
self.mock_greeter.assert_called_once_with("Cody", "fakedata")
self.assertEqual(200, result.status_code)
self.assertEqual("Ok", result.text)
Will have to detect if the factory is async or not and then use an async proxy
if (proxy := getattr(self, "_AsyncContainerProxy__proxy_object")) is None:
async with getattr(self, "_AsyncContainerProxy__lock"):
if (proxy := getattr(self, "_AsyncContainerProxy__proxy_object")) is None:
proxy = await getattr(self, "_AsyncContainerProxy__supplier")()
super().__setattr__("_AsyncContainerProxy__proxy_object", proxy)
The @container.autowire
decorator will simply return a new function that will bind container objects to the function on call. Since this will happen on every call, even though cached it still has a small performance penalty.
On registration, if the container has been warmed up we should bind any known singleton services and parameters as the container is considered final.
A deprecation warning is being raised when calling the FastApi integration.
PATH/lib/python3.12/site-packages/wireup/integration/fastapi_integration.py:34: UserWarning: Using warmup_container is deprecated. Use 'initialize_container' instead
warmup_container(dependency_container, service_modules or [])
Allow injection of types based on protocols. Services can declare themselves what protocol they support and the container can then perform autowiring based on that.
class SupportsFoo(Protocol):
def foo(self) -> str:
pass
@container.register(supports=SupportsFoo)
class FooBar:
def foo(self):
return "bar"
Requesting SupportsFoo
will have FooBar
injected.
Add test coverage and include a badge in the readme.
When there are multiple implementations for an interface and one of them has None as a qualifier, then that one gets injected when asking for the interface without any additional info.
Automatically injecting impl from interface is supported only when there is one impl. In this case user should use a qualifier. Although not technically a bug because qualifier None does exist.
def test_two_qualifiers_are_injected(self):
@self.container.autowire
# This should error
def inner(sub1: FooBase, sub2: FooBase = wire(qualifier="sub2")):
self.assertEqual(sub1.foo, "bar")
self.assertEqual(sub2.foo, "baz")
self.container.abstract(FooBase)
self.container.register(FooBar)
self.container.register(FooBaz, qualifier="sub2")
inner()
Something like
if (proxy := getattr(self, "_ContainerProxy__proxy_object")) is None:
with getattr(self, "_ContainerProxy__lock"):
if (proxy := getattr(self, "_ContainerProxy__proxy_object")) is None:
proxy = getattr(self, "_ContainerProxy__supplier")()
super().__setattr__("_ContainerProxy__proxy_object", proxy)
Looks like __create_instance
will also need updated with locking.
Given that their api is identical swap to using graphlib whenever possible. Installation can also be made optional for Py >=39.
Currently all integrations are deployed on release. We can use support for extras so that user installs only those relevant.
When using in a web app it can be beneficial to have request-scoped objects to make sure they are initialized only once per request.
Even if a real object has been instantiated, container.get(SomeInterface) will return a proxy object.
Tbh, you should not release this unless you compare against other established di frameworks. I should not have to figure it out.
example https://lagom-di.readthedocs.io/en/latest/comparison/
The following registration fails due to service FooBar being registered twice since qualifier is ignored for factories. Factories should also be able to specify qualifiers.
@container.register(qualifier="1")
def foo_factory() -> FooBar:
return FooBar()
@container.register(qualifier="2")
def foo_factory2() -> FooBar:
return FooBar()
If an arg has already been provided, the container can skip it. This should enable calling a function with an argument provided by the caller that the container would have otherwise passed.
A use case might be calling flask functions with new dependencies during tests when testing the endpoints as functions rather than e2e.
Should probably emit a log.info
for this.
# Flask example
@app.get("/")
@container.autowire
def home(greeter: GreeterService):
...
# After: This should be possible
home(greeter=DummyGreeter())
If a module only contains init and services are declared in there register_all_in_module won't trigger any registrations.
I want to do something like the following:
@service
def create_transaction() -> Iterator[Transaction]:
transaction = Transaction()
try:
yield transaction
finally:
transaction.commit()
and also an async version:
@service
async def create_transaction() -> AsyncIterator[Transaction]:
transaction = Transaction()
try:
yield transaction
finally:
await transaction.commit()
Both fastapi and wireup currently supply a value for the parameter leading to a runtime error when the function is called
They can ask for dependencies as usual and return a function that will perform the task.
@container.functional
def greeter_factory(translator_service: TranslatorService, other_service ...) -> Callable...
def greeter(name: str, locale: str = 'en_GB') -> str:
return ...
return greeter
@container.autowire
def target(greeter: Annotated[Callable..., Wire(fn=greeter_factory))
...
Currently from my experience, when we are using Annotated[AbstractClass, Wire(qualifier=Any)]
- we still need to import redundant Concrete Impl to scope, where @autowire is performed. If such import is not performed, user will experience
wireup.errors.UnknownQualifiedServiceRequestedError
It would be nice for this library to have such feature, or at least, have an docs for this cause, to not import redundant concrete classes, but have a workaround.
Transient dependencies should return a real instance just like singletons when possible and avoid the proxy overhead.
When using fastapi, annotating services with Wire()
while required -- is the equivalent of having no annotations when used in other frameworks.
This needs to change so that the behavior is consistent and that since the type is annotated, might as well get the full benefits of the annotation such as throwing when requesting things that don't exist.
Sometimes when the target method is already autowired it can be a bit difficult to substitute a service for a different implementation.
Add a way for users to override dependencies, preferrably through a ctx manager if they wish to do so.
See #7
Users should have the option to inject everything that implements some interface or protocol(once they get added).
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.