suned / serum Goto Github PK
View Code? Open in Web Editor NEWDependency injection framework for Python 3.6
License: MIT License
Dependency injection framework for Python 3.6
License: MIT License
from serum import immutable
class Environment:
__registry = immutable(tuple())
...
import os
def app(name: str, default: Environment, **kwargs):
kwargs['default'] = default
def inject(component, environment=None):
if environment is None:
environment = os.environ.get(name + '_ENVIRONMENT', 'default')
env = environments[environment]
return property(lambda _: env.get(component))
return inject
dev_env = Environment(ConsoleLog)
prod_env = dev_env.use(FileLog)
test_env = production_env.mock(AbstractLog)
inject = app(name='MyApp', default=dev_env, dev=dev_env, production=prod_env, test=test_env})
class Dependent:
log = inject(AbstractLog)
if __name__ == '__main__':
Dependent().log.info('Hello serum!')
> MYAPP_ENVIRONMENT=dev python my_app.py # ouputs 'Hello serum!'
It would make sense to support 3.5 since most of the reports show that around 40-45% of software is using it, the 3.6 show 30-40% usually.
My idea is to create the bare minimum Context class, dependency and inject decorator and use that to provide the full-featured sub-classes. The dependency and the inject could be served without any problem if you call internal Classes to do the IoC.
With this architecture, you can move the IPython into a separate context to make the base more lightweight and performant.
from serum import *
class S(Singleton):
pass
class Depenedent:
s = inject(S)
with Environment():
s1 = Dependent().s
s2 = Dependent().s
assert s1 is is2
It looks like the automatic dependency injection captures the return type as an argument well:
from serum import dependency, inject
@dependency
class Json(object):
def __str__(self):
return '{"data": [], "message": "This is an example output!"}'
@inject
@dependency
class ExternalService(object):
json: Json
def run(self):
return self.json
class Service:
@inject
def run(self, external_service: ExternalService) -> Json:
return external_service.run()
print(Service().run())
Output:
Traceback (most recent call last):
File "serum/v4.0.1/return_type_error.py", line 22, in <module>
print(Service().run())
File "serum/venv/lib/python3.6/site-packages/serum/_inject.py", line 136, in decorator
return f(*args, **dependency_args)
TypeError: run() got an unexpected keyword argument 'return'
Relatively easy to fix. Just remove the return key from the __annotations__
before you iterate trough that in the _inject.py
It would be nice to have a readme.io or readthedocs.org or any similar generated documentation. For that we need to document the code better.
With the current implementation, the empty context is the default.
This can lead to problems when using mock
without a Context because any mocked types cannot be unmocked in the default context.
By changing mock
to a context manager this is no longer a problem because mocks can be reset when the mock context manager closes, rather than when the context closes.
With the current implemenation, a dependency can only be found in the current context. This means that if you want to temporarily override a dependency, you must supply all the dependencies from the old context and replace your dependency of interest.
If contexts were nestable, such that dependencies are first looked up in the current context, then the context that preceeded it, replacing only a subset of dependencies would be very easy.
It would be nice if the library doesn't do anything until you don't explicitly say so. Having the IPython right in the __init__.py
feels wrong, I think it would make sense to extract that and use it in a separate file which can be loaded when it's actually necessary.
Singleton injection does not work, when no Context() created previously.
The following example shows the error:
broken_if_no_context_used()
shows the errorworks()
beforehand, calling broken_if_no_context_used()
works tooWe should add a unit test for this.
from serum import singleton, inject, Context
@singleton
class ExpensiveObject:
pass
@inject
class NeedsExpensiveObject:
expensive_instance: ExpensiveObject
def works():
with Context():
instance1 = NeedsExpensiveObject()
instance2 = NeedsExpensiveObject()
assert instance1.expensive_instance is instance2.expensive_instance
print('context works')
def broken_if_no_context_used():
# works if works() called previously
instance1 = NeedsExpensiveObject()
instance2 = NeedsExpensiveObject()
assert instance1.expensive_instance is instance2.expensive_instance
print('without context works')
# works()
broken_if_no_context_used()
Error:
Traceback (most recent call last):
File "dij2.py", line 33, in <module>
broken_if_no_context_used()
File "dij2.py", line 28, in broken_if_no_context_used
assert instance1.expensive_instance is instance2.expensive_instance
AssertionError
I really like to use Docker for development, it would help a lot to try out different versions and it would be a proof of concept which shows that it is working with the official environment.
The different environment usually referred as Containers in IoC frameworks, it makes more sense because it's more generic. You could have different reasons to separate something than just Environments.
production_env = Environment.from_yml('path/to/production.yml')
with production_env:
HasDependencies.dependency.method()
I know that the project uses Circle-CI right now, but it would make sense to move the code and the main development into GitLab because you can use the built-in CI/CD with docker and you can even serve your docker image for those who want to mess with this library/micro-framework ;-)
prod_env = Environment(Dependency)
@prod_env
def run():
Dependent().dependecy # ok
serum assumes that the init method of dependency decorated classes can be called without any arguments. This means that all arguments to init of dependency decorated classes must be injected using inject.
How would you go about instantiating a complex object that requires a number of initialization arguments (primitive strings, ints, etc.), while still having it be injectable? An example use case would be a random API-consuming Client
from a third-party library that expects you to pass api tokens, api hashes, session paths etc.
I guess you could instantiate such an object yourself, but then how would you make Serum aware of the instance? And I suppose this would also defeat the purpose of DI.
Maybe I have failed to understand the use of Contexts
, as they seem to be related. Any hints?
What most probably would be nice to have in separate files is:
Every case should have a version of:
Links:
from serum import *
class AbstractA(Component):
pass
class AbstractB(Component):
pass
class A(AbstractA):
b = inject(AbstractB)
def __init__(self):
self.b
class B(AbstractB):
a = inject(AbstractA)
def __init__(self):
self.a
class Dependent:
a = inject(AbstractA)
with Environment(A, B):
Dependent().a # whoops!
Something like
e1 = Environment(MyComponent)
e2 = e1 | Environment(MyComponentSubType)
with e2:
assert isinstance(Dependent().my_component, MyComponentSubType)
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.