roo-oliv / injectable Goto Github PK
View Code? Open in Web Editor NEWPython Dependency Injection for Humans™
Home Page: https://injectable.readthedocs.io
License: MIT License
Python Dependency Injection for Humans™
Home Page: https://injectable.readthedocs.io
License: MIT License
Injectable 3.4.0 has a bug that attempts to use positional args after named args passed by the caller. The following piece of code reproduces the issue:
@autowired
def bar(qux, foo: Autowired(Foo)):
...
bar(qux="QUX")
Running it with injectable 3.4.0 and Python 3.6.8 will raise TypeError: bar() got multiple values for argument 'qux'
.
This issue was originally reported by @craigminihan in #14
Hey there,
is there any reason for not having encoding in this line?
maybe theres a easier solution then mine which was forking it and adding an encoding to that line.
with open(file_entry, encoding='utf-8') as file:
this missing encoding caused me headaches so often now!
Injectable 3.4.1 raises a KeyError
for the namespace specified by an optional injection if there are no injectables registered in it. The expected behavior would be for it to inject None
instead of raising an error. The following piece of code reproduces the issue:
@autowired
def a(x: Autowired(Optional["anything"])):
...
Running it with injectable 3.4.1 and Python 3.6.8 will raise KeyError: 'DEFAULT_NAMESPACE'
.
Currently, when not using the parameter lazy
of @autowired
, Inejctable provides a lazy
function to pass in your dependencies' type annotations and make them lazy.
Under the hood it will just return a function that returns the dependency type and has an attribute lazy
set to True
. This won't break IDEs' (eg PyCharm) code completion.
So we have this syntax today:
@autowired()
__init__(*args, *, controller: lazy(Controller)):
...
It would be best to have a subscriptable Lazy type which doesn't break IDEs' code completion.
The intended syntax would be this:
@autowired()
__init__(*args, *, controller: Lazy[Controller]):
...
Autowiring should be able to resolve dependencies from a global scope without requiring the class specification:
This is how one would autowire today:
# application.models.model.py
class Model:
def __init__():
self.foo = 'foo'
# application.services.service.py
class Service:
def __init__():
self.bar = 'bar'
# application.qux.qux.py
from injectable import autowired
from application.models.model import Model
from application.services.service import Service
class Qux:
@autowired
def __init__(self, *, model: Model, service: Service):
self.model = model
self.service = service
This is how it should be:
# application.models.model.py
from injectable import injectable
class Model:
@injectable
def __init__():
self.foo = 'foo'
# application.services.service.py
from injectable import injectable
class Service:
@injectable
def __init__():
self.bar = 'bar'
# application.qux.qux.py
from injectable import autowired
from injectable import inject
class Qux:
@autowired
def __init__(self, *, model = inject('Model'), service = inject('Service')):
self.model = model
self.service = service
Injectable 3.4.2 raises an ImportError when a file imports its module's __init__
file and the __init__
file in turn imports that file. This is a problem with Injectable because Python import system is able to deal with these scenarios and they're not actually circular imports as someone would think. The following minimum sample project reproduces the issue:
project
│ main.py
│
└───test_module
│ │ __init__.py
│ │
│ └───foo_module
│ | │ __init__.py
│ | │ foo.py
│ │
│ └───utils
│ │ __init__.py
│ │ some_util.py
# ./main.py
from injectable import autowired, Autowired, load_injection_container
from injectable.testing import reset_injection_container
@autowired
def test(foo: Autowired("foo")):
...
if __name__ == "__main__":
reset_injection_container()
load_injection_container()
test()
# ./test_module/__init__.py
from project.test_module.foo_module import Foo
__all__ = ["Foo"]
# ./test_module/foo_module/__init__.py
from project.test_module.foo_module.foo import Foo
__all__ = ["Foo"]
# ./test_module/foo_module/foo.py
from injectable import injectable
from project.test_module.utils import some_util
@injectable(qualifier="foo")
class Foo:
def __init__(self):
some_util.some_util()
# ./test_module/utils/__init__.py
# EMPTY
# ./test_module/utils/some_util.py
def some_util():
...
Running it with injectable 3.4.2 and Python 3.6.8 will raise:
ImportError: cannot import name 'Foo' from 'project.test_module.foo_module.foo' (/project/test_module/foo_module/foo.py)
Right now it seems impossible to directly supply an injectable instance, except for providing a factory method like so:
a = A()
@injectable_factory(A, singleton=True)
def a_injector():
return a
The alternative would be to move the initialization into the factory, however, sometimes the initialization will run independent from injection discovery, so this isn't applicable. Examples for this would be flask projects with multiple blueprints (i am aware of flask.current_app, but this doesn't always work (e.g. extensions)) and i think providing a single injectable directly would be generally useful, tho i can see why it might go against design thoughts behind injections.
By submiting an issue I accept this project's
Code of Conduct. Any issue out of
these guidelines can be deleted at any moment without further explanation.
Supplying fake/mocked/stubbed injectables should be really easy.
An example feature would be to introduce a helper fixture for pytest:
@pytest.fixture()
def given_injectable():
originals: Dict[Union[type, str], Tuple[Set[Injectable], bool]] = {}
def register(constructor: callable, klass=None, qualifier=None, propagate=False):
if not klass and not qualifier:
raise ValueError("params 'klass' and 'qualifier' cannot be both None")
if propagate and not klass:
raise ValueError("param 'klass' must be set when 'propagate' is True")
for dependency in klass, qualifier:
if not dependency or dependency in originals:
continue
originals[dependency] = clear_injectables(dependency), propagate
register_injectables(
{Injectable(constructor)}, klass, qualifier, propagate=propagate
)
yield register
for dependency, (injectables, propagate) in originals.items():
clear_injectables(dependency)
if isinstance(dependency, str):
register_injectables(injectables, qualifier=dependency, propagate=propagate)
else:
register_injectables(injectables, klass=dependency, propagate=propagate)
Current situation:
T = TypeVar("T")
def inject(dependency: Union[T, str], ...) -> T
inject(A) # claims to return A not something of type A
Correct situation:
T = TypeVar("T")
def inject(dependency: Union[Type[T], str], ...) -> T
inject(A) # now claims to return an instance of A
I'm not an expert when it comes to typing, but this is what i think causes it. Also maybe this is an issue with intellijs type system, but I don't have any other type checkers rn.
By submiting an issue I accept this project's
Code of Conduct. Any issue out of
these guidelines can be deleted at any moment without further explanation.
@chsatyap opened Pull Request #23 to suggest the use of DeepSource.
As part of the new tools adoption process evaluation on the actual need for such a tool must be performed and should consider:
It's currently not ideal to use this library together with Python's async/await.
We should support asynchronous constructors and factories.
Async constructors are also a bigger problem since one cannot declare an async __init__
method without bending some snakes. This calls for ways to construct and/or setup an injectable other than relying solely on __init__
.
I am currently into an issue where i need the Type of all @injectable
objects. These objects have a common supertype, but no empty __init__
, so I can't just use Autowired(List[BaseType])
. Concrete, this is a database project, and the model classes need to be registered so they can be created in the database.
This is kinda far fetched, and i could definitely see why this would be rejected, as database models are kind of the only usecase i coud think of, and it is kind of narrow.
Allowing the Autowired(Type[BaseType])
to pick up an injectable subclass of a BaseType
, or Autowired(List[Type[BaseType]])
to get all injectable subclasses. Maybe this could be indicated by @injectable(type=True)
.
By submiting an issue I accept this project's
Code of Conduct. Any issue out of
these guidelines can be deleted at any moment without further explanation.
This script reproduces the bug:
from typing import List
from injectable import injectable, autowired, Autowired, InjectionContainer
@injectable(qualifier="foo")
class Foo:
pass
@autowired
def test(foo: Autowired(List["foo"])):
assert foo is not None
assert len(foo) == 1
if __name__ == "__main__":
InjectionContainer.load()
test()
The returned error is quite cryptic:
AttributeError: '_ForwardRef' object has no attribute '__qualname__'
On an existing codebase I am seeing ImportError
s when upgrading from 3.4.1 to latest.
The error I am seeing is: ImportError: attempted relative import with no known parent package
Prior to 3.4.3 all modules load successfully.
I've not had time to put an example together showing the exact error case yet. I'll follow up with this later.
Stack:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/craig/code/XXX.py", line 7, in <module>
injectable.load_injection_container(search_path=os.getenv("PYTHONPATH"))
File "/home/craig/.local/lib/python3.6/site-packages/injectable/container/load_injection_container.py", line 43, in load_injection_container
search_path, default_namespace or DEFAULT_NAMESPACE
File "/home/craig/.local/lib/python3.6/site-packages/injectable/container/injection_container.py", line 158, in load_dependencies_from
run_path(file.path)
File "/usr/lib/python3.6/runpy.py", line 263, in run_path
pkg_name=pkg_name, script_name=fname)
File "/usr/lib/python3.6/runpy.py", line 96, in _run_module_code
mod_name, mod_spec, pkg_name, script_name)
File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
exec(code, run_globals)
File "MYFILE", line 3, in <module>
from .MYCLASS
ImportError: attempted relative import with no known parent package
When we create a new context with load_injection_container()
and there are no injectable objects found, no namespace is being created in injection_container.py
. When we attempt to programmatically add an object into the context - it fails - because no default context (namespace) has been created.
Code to reproduce:
from injectable import Injectable, load_injection_container
from injectable.testing import register_injectables
class Sample():
pass
def fail():
sample = Sample()
mocked_injectable = Injectable(lambda: sample)
register_injectables({mocked_injectable}, Sample)
print("it's broken")
def run_example():
load_injection_container()
fail()
if __name__ == "__main__":
run_example()
Response:
File "sandbox/fail.py", line 12, in fail
register_injectables({mocked_injectable}, Sample)
File "\.virtualenv\lib\site-packages\injectable\testing\register_injectables_util.py", line 54, in register_injectables
namespace = InjectionContainer.NAMESPACES[namespace or DEFAULT_NAMESPACE]
KeyError: 'DEFAULT_NAMESPACE'
In this case - when we try to register_injectables
, we see KeyError
on attempt to get default namespace - when there is none.
How to fix:
When container fails to find any injection candidates - it should create empty default namespace anyway.
PEP-593 defines the a standard for annotation metadata other than type hinting through the use of typing.Annotated
. This framework should comply with this recommendation and give full support to typing.Annotated
.
This was originally brought to attention by @Euraxluo in #107 (comment)
Currently Autowired(T)
already works well with most type checkers and linters since it evaluates to T
but this is not in line with the recommendation from PEP-593 to leave annotations primary to type hinting and other metadata to extras.
We should decide how best to incorporate these changes in Injectable. I intend to lay down some possible paths we can go from here in this issue thread so anyone can share their thoughts if any and then eventually I'll commit to an implementation.
While typing.Annotated
was introduced in Python 3.9, this frameworks currently supports all Python versions since 3.6. So we should think in a way that will deliver the best possible experience for all possible users and use cases of Injectable.
When Python version < 3.9 then this is the only possible way to use Injectable:
@autowired
def foo(service: Autowired(Service): ...
When typing.Annotated is available (Python version >= 3.9), then this is also possible:
@autowired
def foo(service: Annotated[Autowired(Service), ...]): ...
Therefore, since Python 3.9 one can probably use other libs which rely on annotations and fully support PEP-593 without undesired interactions or further problems with the use of Injectable.
Autowired
an alias of typing.Annotated
Autowired
from the annotations to the default values of arguments@autowired
decorator, dispensing the use of Autowired
When a function decorated with @autowired
is inspected to get it's signature the original function's signature with required kwargs should be overwritten by a signature exposing injectables as optional kwargs.
import inspect
@autowired
def foo(a: Autowired("A")):
print(a)
inspect.signature(foo)
# <Signature (a: <injectable.autowiring.autowired_type._Autowired object at 0x7ff88eea53a0>)>
@autowired
not overwriting the original signature may cause checks of uses vs signature to fail.
When one doesn't want to pass in any parameters into @autowired
decorator they can't symple do:
@autowired
def foo(...)
Instead they are required to use empty parenthesis for it to work:
@autowired()
def foo(...)
It would be best to omit the parenthesis when no arguments will be used.
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.