python-dependency-injector
python-dependency-injector copied to clipboard
Injection on a decorator
Hi, I really like this package and am using it in production. I was tying to mess around with decorators because I want to add side effects to a function, adding a secret_number
in the example. I was expecting decorated_function_1
to work but it didn't and I can't wrap my head around it. Is this an expected behavior? Could injection be supported like in my_decorator_1
?
I also added few examples of things I tried and only decorated_function_4
actually works.
from functools import wraps
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@inject
def my_decorator_1(func, secret_number: int = Provide[Container.config.secret_number]):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + secret_number
return wrapper
@inject
def my_decorator_2(secret_number: int = Provide[Container.config.secret_number]):
def inner_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + secret_number
return wrapper
return inner_decorator
def my_decorator_3():
@inject
def inner_decorator(func, secret_number: int = Provide[Container.config.secret_number]):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + secret_number
return wrapper
return inner_decorator
def my_decorator_4(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
secret_number = kwargs['secret_number']
return result + secret_number
return wrapper
@my_decorator_1
def decorated_function_1():
return 42
@inject
@my_decorator_1
def decorated_function_1a():
return 42
@my_decorator_2()
def decorated_function_2():
return 42
@my_decorator_3()
def decorated_function_3():
return 42
@inject
@my_decorator_3()
def decorated_function_3a():
return 42
@inject
@my_decorator_4
def decorated_function_4(secret_number: int = Provide[Container.config.secret_number]):
return 42
@my_decorator_4
@inject
def decorated_function_4a(secret_number: int = Provide[Container.config.secret_number]):
return 42
def main():
test_funcs = [
decorated_function_1,
decorated_function_1a,
decorated_function_2,
decorated_function_3,
decorated_function_3a,
decorated_function_4,
decorated_function_4a,
]
for test_f in test_funcs:
try:
result = test_f()
print(f"Function {test_f} returned {result}")
except Exception as exc:
print(f"Function {test_f} raised {exc.__class__.__name__} '{exc}'")
if __name__ == '__main__':
import sys
container = Container()
container.init_resources()
container.config.secret_number.from_env("SECRET_INT", 24)
container.wire(modules=[sys.modules[__name__]])
main()
The output is:
Function <function decorated_function_1 at 0x7f93d8a4d310> raised TypeError 'unsupported operand type(s) for +: 'int' and 'Provide''
Function <function decorated_function_1a at 0x7f93d8a4d4c0> raised TypeError 'unsupported operand type(s) for +: 'int' and 'Provide''
Function <function decorated_function_2 at 0x7f93d8a4d670> raised TypeError 'unsupported operand type(s) for +: 'int' and 'Provide''
Function <function decorated_function_3 at 0x7f93d8a4d820> raised TypeError 'unsupported operand type(s) for +: 'int' and 'Provide''
Function <function decorated_function_3a at 0x7f93d8a4daf0> raised TypeError 'unsupported operand type(s) for +: 'int' and 'Provide''
Function <function decorated_function_4 at 0x7f93d8a4dca0> returned 66
Function <function decorated_function_4a at 0x7f93d8a4de50> raised KeyError ''secret_number''
Hi, I noticed the same issue on my project. The container doesn't inject properly in the arguments of my decorator function.
Thanks for your experimentations, I'll use the workaround presented in your my_decorator_4
for now.
Are there any plans to fix this issue ? I'm wondering why this even occurs specifically on decorator among all functions..
Hey @platipo and @MatthieuMoreau0 ,
Found a solution after some debugging:
def my_decorator_1(func):
@wraps(func)
@inject
def wrapper(
*args,
secret_number: int = Provide[Container.config.secret_number],
**kwargs,
):
result = func(*args, **kwargs)
return result + secret_number
return wrapper
def my_decorator_2():
def inner_decorator(func):
@wraps(func)
@inject
def wrapper(
*args,
secret_number: int = Provide[Container.config.secret_number],
**kwargs,
):
result = func(*args, **kwargs)
return result + secret_number
return wrapper
return inner_decorator
def my_decorator_3():
def inner_decorator(func, ):
@wraps(func)
@inject
def wrapper(
*args,
secret_number: int = Provide[Container.config.secret_number],
**kwargs,
):
result = func(*args, **kwargs)
return result + secret_number
return wrapper
return inner_decorator
def my_decorator_4(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
secret_number = kwargs['secret_number']
return result + secret_number
return wrapper
This way it produces expected output for all functions (except decorated_function_4a
that seems to be not working anyway):
(venv) ➜ issue454 git:(develop) ✗ python example.py
Function <function decorated_function_1 at 0x106518d30> returned 66
Function <function decorated_function_1a at 0x106518f70> returned 66
Function <function decorated_function_2 at 0x10651e1f0> returned 66
Function <function decorated_function_3 at 0x10651e430> returned 66
Function <function decorated_function_3a at 0x10651e040> returned 66
Function <function decorated_function_4 at 0x10651e790> returned 66
Function <function decorated_function_4a at 0x10651e940> raised KeyError ''secret_number''
To make injections work with decorators you need to inject dependencies into a decorating closure, but not a decorator itself. Otherwise the actual call of the decorator happens before the config value is defined.
PS: My apologies it took that long to answer.
Closing this issue for now. Please re-open or comment if needed.
from functools import wraps
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
class Reporter:
def info(self, msg):
print(f"*** [INFO] {msg}")
class Container(containers.DeclarativeContainer):
conf = providers.Configuration()
reporter = providers.Singleton(Reporter)
def tagger(tag):
def inner(func):
# @wraps(func) # <-- using wraps break inject
@inject
def tagger_wrapper(text, sep=Provide[Container.conf.sep], **kwargs):
result = func(text, **kwargs)
return f"{result}{sep}({tag}[{func.__name__}])"
return tagger_wrapper
return inner
def logger(prefix):
def inner(func):
# @wraps(func) # <-- using wraps break inject
@inject
def logger_wrapper(text, reporter=Provide[Container.reporter], **kwargs):
result = func(text, **kwargs)
reporter.info(f"{prefix} - {func.__name__}('{text}') was called")
return result
return logger_wrapper
return inner
def helper(tag, prefix):
def inner(func):
@tagger(tag)
@wraps(func) # <-- if remove this double wraps it breaks the function name
@logger(prefix)
@inject
@wraps(func)
def helper_wrapper(text, **kwargs):
result = func(text, **kwargs)
return result
return helper_wrapper
return inner
@helper("my_tag", "|my_prefix|")
def my_example(text):
return text
if __name__ == "__main__":
container = Container()
container.wire(modules=[__name__])
container.conf.from_dict({"sep": " / "})
print(my_example("hi!"))
Running the example above we get the expected behaviour
*** [INFO] |my_prefix| - my_example('hi!') was called
hi! / (my_tag[my_example])
If you try to remove this double @wraps
call in helper
the function name breaks:
*** [INFO] |my_prefix| - my_example('hi!') was called
hi! / (my_tag[logger_wrapper])
^^^^^^^^^^^^^^^^
if I move the @wraps
to the decorator(this I think should be the correct way) it breaks @inject
Traceback (most recent call last):
File "example.py", line 69, in <module>
print(my_example("hi!"))
File "/home/fabio/.pyenv/versions/.../dependency_injector/wiring.py", line 612, in _patched
result = fn(*args, **to_inject)
File "example.py", line 22, in tagger_wrapper
result = func(text, **kwargs)
File "/home/fabio/.pyenv/versions/.../dependency_injector/wiring.py", line 612, in _patched
result = fn(*args, **to_inject)
File "example.py", line 36, in logger_wrapper
reporter.info(f"{prefix} - {func.__name__}('{text}') was called")
AttributeError: 'Provide' object has no attribute 'info'
@fabiocerqueira I faced the same issue as well, adding wraps is breaking the inject.
@rmk135 could you please have a look?
My use case is similar to what Fabio mentioned:
def tagger(tag):
def inner(func):
# @wraps(func) # <-- using wraps break inject
@inject
def tagger_wrapper(text, sep=Provide[Container.conf.sep], **kwargs):
result = func(text, **kwargs)
return f"{result}{sep}({tag}[{func.__name__}])"
return tagger_wrapper
return inner
Hey @Jitesh-Khuttan @fabiocerqueira ,
First, thanks a lot for finding the issue and providing steps to reproduce it. It's uneasy to catch. After 3 rounds of debugging, I've localized the root cause. The problem is that the second @wraps(func)
where func
is already @inject
-patched function, overrides previously parsed injections.
There is no easy fix, but I have one refactoring in mind that theoretically could help. I'll proceed with it and get back when I have any news.
Linking this with #597
I have the fix! Working on merging it to develop.
Merged the fix to develop. Added some docs on the @inject
decorator. Will be releasing to PyPI by the end of the week.
What next of this discussion? Are this issue has been fixed? I'm still faced the issue. using version 4.41.0.
File "src/dependency_injector/_cwiring.pyx", line 28, in dependency_injector._cwiring._get_sync_patched._patched
File "/Users/nizar/ProjectBima/simpeg_bima/simpeg/internal/decorators.py", line 147, in wrapped
if application_service.get_by_app_key(app_key):
AttributeError: 'Provide' object has no attribute 'get_by_app_key'
And this is my decorators:
def jwt_or_app_key_required(optional=False,
fresh=False,
refresh=False,
locations=None,):
def wrapper(f):
@wraps(f)
@inject
@wraps(f)
def wrapped(*args,
application_service: ApplicationService = Provide[
DependencyContainer.application_service
], **kwargs):
app_key = request.args.get('app_key', default=None, type=str)
if application_service.get_by_app_key(app_key):
return f(*args, **kwargs)
try:
verify_jwt_in_request(optional, fresh, refresh, locations)
except BaseException:
return create_response(403, msg="Forbidden access")
return f(*args, **kwargs)
return wrapped
return wrapper
Bumping this, also coming across this issue. Not ideal at all since the docs claim that this is supposed to work.