flask_injector
flask_injector copied to clipboard
Type-hinted list arg causes "TypeError: Injecting partially applied functions is no longer supported."
How to reproduce:
test_app.py
:
import connexion
from connexion.resolver import RestyResolver
from flask_injector import FlaskInjector
app = connexion.App(__name__)
app.add_api(
{
'swagger': '2.0',
'info': {
'version': '1.0.0',
'title': 'Test Service',
'license': {'name': 'MIT'}
},
'basePath': '/api',
'schemes': ['http'],
'consumes': ['application/json'],
'produces': ['application/json'],
'paths': {
'/test': {
'get': {
'operationId': 'test_api.test_func',
'parameters': [
{
'name': 'list_arg',
'in': 'query',
'required': False,
'type': 'array',
'items': {'type': 'string'},
'collectionFormat': 'multi'
},
],
'responses': {'200': {'description': 'Test endpoint'}},
}
},
},
},
resolver=RestyResolver('api'),
)
FlaskInjector(app=app.app, modules=[])
app.run(port=5000)
test_api.py
:
import typing as _t
import logging as _logging
def test_func(
list_arg: _t.List[str],
) -> dict:
_logging.info(f'{list_arg!r}')
return {'list_arg': list_arg}
Execute query GET http://test_app:5000/api/test?list_arg=a&list_arg=b
Expected result:
No error, response {'list_arg': ['a', 'b']}
Actual result:
Error occurs:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 440, in get_binding
return self._get_binding(key, only_this_binder=is_scope)
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 435, in _get_binding
raise KeyError
KeyError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/flask/app.py", line 2446, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python3.7/site-packages/flask/app.py", line 1951, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python3.7/site-packages/flask/app.py", line 1820, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/usr/local/lib/python3.7/site-packages/flask/app.py", line 1949, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.7/site-packages/flask/app.py", line 1935, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/usr/local/lib/python3.7/site-packages/flask_injector.py", line 76, in wrapper
return injector.call_with_injection(callable=fun, args=args, kwargs=kwargs)
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 778, in call_with_injection
owner_key=self_.__class__ if self_ is not None else callable.__module__,
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 57, in wrapper
return function(*args, **kwargs)
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 819, in args_to_inject
instance = self.get(key.interface)
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 689, in get
binding, binder = self.binder.get_binding(None, key)
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 449, in get_binding
binding = self.create_binding(key.interface)
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 369, in create_binding
provider = self.provider_for(interface, to)
File "/usr/local/lib/python3.7/site-packages/injector/__init__.py", line 424, in provider_for
raise TypeError('Injecting partially applied functions is no longer supported.')
TypeError: Injecting partially applied functions is no longer supported.
Affected versions:
pip freeze
:
backcall==0.1.0
certifi==2019.3.9
chardet==3.0.4
Click==7.0
clickclick==1.2.2
colorful==0.5.4
connexion==2.6.0
coverage==4.5.3
decorator==4.4.1
elasticsearch==6.3.1
elasticsearch-dsl==6.4.0
fastavro==0.21.19
Flask==1.1.1
Flask-Injector==0.11.0
idna==2.7
inflection==0.3.1
injector==0.16.0
ipython==7.12.0
ipython-genutils==0.2.0
itsdangerous==1.1.0
jedi==0.16.0
Jinja2==2.11.1
jsonschema==2.6.0
MarkupSafe==1.1.1
openapi-spec-validator==0.2.6
parso==0.6.1
pathlib==1.0.1
pexpect==4.8.0
pickleshare==0.7.5
pika==0.13.1
prettyprinter==0.18.0
prompt-toolkit==3.0.3
ptyprocess==0.6.0
Pygments==2.5.2
python-dateutil==2.8.1
PyYAML==5.1
redis==3.2.1
requests==2.20.1
six==1.12.0
traitlets==4.3.3
typing==3.6.6
urllib3==1.24.1
wcwidth==0.1.8
Werkzeug==0.15.1
Notes:
- If the type hint
List[str]
is removed from the argumentlist_arg
, the injection works correctly
First of all thanks for a well written report, this is helpful.
There's no good solution for this right now because, if my understanding is correct, Connexion only gathers and provides the parameters (list_arg
in this case) for a view after Flask already called the view and Flask-Injector attempted to provide the parameters. Only if Flask-Injector's dependencies gathering is successful the control will actually reach Connexion and the client code.
The only short term workaround I can think of is to use https://injector.readthedocs.io/en/latest/api.html#injector.NoInject to mark the parameter(s) as noninjectable.
Long-term solutions include:
- making Flask-Injector optionally not implicitly decorate routes with
@inject
- finding a way for Flask-Injector to somehow figure out the arguments Connexion would pass, assuming they'd be passed and not trying to inject those particular dependencies
@jstasiak Note that, if I remove the type hint, Flask-Injector ignores the parameter and Connexion successfully injects the value. Similarly, if, instead of a list, I have a scalar API parameter - "type": "string"
, "type": "number"
, "type": "integer"
, or "type": "boolean"
- and the function has a corresponding argument with the appropriate type hint - str
, float
, int
, or bool
respectively - then Flask-Injector again leaves it alone and Connexion is able to handle its own injection.
Thus, I suppose the problem here is that Flask-Injector is incorrectly treating List[str]
as an injectable type.
Thank you for pointing out NoInject
, though; that does work as a workaround for now.
If auto_bind
(set in Injector constructor) is True
(and it's True
by default, Flask-Injector doesn't set it Injector will treat every type as injectable type.
It doesn't matter in principle if it's a scalar parameter or a list or a dictionary. The only difference is Injector crashes when it tries to inject a list that it has no (multi)binding for, while it provides values of other types if only it can instantiate them using parameterless constructors.
It doesn't crash by default when trying to provide some types, but it still does inject them before Connexion overrides them. So, to sum up:
if I remove the type hint, Flask-Injector ignores the parameter and Connexion successfully injects the value.
That's correct.
Similarly, if (...) the function has a corresponding argument with the appropriate type hint - str, float, int, or bool respectively - then Flask-Injector again leaves it alone and Connexion is able to handle its own injection.
That's not correct. Flask-Injector does provide an argument of that type (for str
it's a result of calling str()
, so empty string; for int
it's int()
(so: 0) etc. and when later control actually reaches Connexion, Connexion code gets the data from where it needs to get it and overrides the arguments in the keyword argument list. To demonstrate this, change your local Injector copy like this:
diff --git a/injector/__init__.py b/injector/__init__.py
index 9ef06f94c0bd..83a799d0bab9 100644
--- a/injector/__init__.py
+++ b/injector/__init__.py
@@ -1012,6 +1012,7 @@ class Injector:
dependencies.update(kwargs)
try:
+ print('calling %r with %r and %r' % (callable, full_args, dependencies))
return callable(*full_args, **dependencies)
except TypeError as e:
reraise(e, CallError(self_, callable, args, dependencies, e, self._stack))
Then change the type of list_arg
to any type Injector/Flask-Injector seem to ignore, like str
. When you run the code and make a request to that endpoint you'll see
calling <function test_func at 0x108510ee0> with () and {'list_arg': ''}
even though the response contains the data you expect:
{
"list_arg": [
"a",
"b"
]
}
Newer Injector has more helpful error message for the List[str]
case:
injector.UnknownProvider: couldn't determine provider for typing.List[str] to None
But the underlying issue remains unchanged.