Feature request: Strict mode which enforces `Module` definitions
I played around with injector and Flask-Injector the last few days, mostly because I came to realize that bigger Flask applications can easily become a nightmare when writing tests.
I have to say I really enjoy using the library so far. However, there is one design decision I am a bit afraid of... since you are way more into the topic of DI than me, maybe you can easily convince me that this is a non-issue, or maybe there is some easy way to work around it.
It's already in the first example of the the README:
>>> from injector import Injector, inject
>>> class Inner:
... def __init__(self):
... self.forty_two = 42
...
>>> class Outer:
... @inject
... def __init__(self, inner: Inner):
... self.inner = inner
...
>>> injector = Injector()
>>> outer = injector.get(Outer)
An instance of Outer gets created even though there is no Module with any further description of what instance (e.g., inheritance) to set up or which scope to use. Maybe you can proof me wrong, but I consider this kind of a dangerous design decision. Imagine someone writing integration tests that wipe the database after every round but forgetting to define the TestDbConnection dependency in the Module, so what the integration test really uses is some production database... ouch!
Even though this example sounds a bit constructed, what I would like to see is some kind of strict mode that tells injector not to create instances out of thin air IF I already use a Module and this module lacks a definition of the according dependency (so the example would still work if someone prefers not to use modules at all).
I have some experiences with the Spring Framework in the kotlin/java world, I also had a look into the documentations of guice, dependencies and punq. I think they all enforce this kind of setup.
This is a good question. I would really advise to only provide production-related configuration in production code entry points so that you never forget to override them in tests with grave consequences because production configuration is never there in the first place.
So something like:
class CommonModule(Module):
def provide_db(cs: DBConnectionString):
# ...
class ProductionDBModule(Module):
def configure(binder):
binder.bind(DBConnectionString, to='tcp://...')
class IntegrationTestDBModule(Module):
def configure(binder):
binder.bind(DBConnectionString, to='file://...')
Then use Injector([CommonModule, ProductionDBModule]) in your application and Injector([CommonModule, IntegrationTestDBModule]) in integration tests and there's no way to leak production credentials to tests if you organize it this way. Not inheriting your integration test module(s) from production module(s) or combining them to override each other seems to avoid the issue you describe.
Now, while I don't believe this to be a huge issue there's an auto_bind Injector constructor switch which you can set to False to only ever be able to inject configured types if you need to. Flask-Injector has no way to declare that right now, so you need to do something like
injector = Injector(auto_bind=False)
FlaskInjector(app, injector=injector)
Of course you're absolutely right, no one should make production config directly accessible to the codebase. I was just looking for some drastic example of what might go wrong if dependencies automagically get injected. Weirdly behaving tests are probably a more common result...
But actually the auto_bind setting is exactly what I was looking for, seems like I should have rtfm better :). Can't wait to refactor my codebase now... Many thanks for pointing me out!
You may wanna promote this possibility a bit more in your docs, I think many developers experienced in other DI frameworks might find the initial example a bit scary.
That's fair, what you raise is a valid concern. Agreed on the documentation front, the auto_bind flag is definitely underdocumented and could be mentioned in the readme.