Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type-hinted list arg causes "TypeError: Injecting partially applied functions is no longer supported." #50

Open
macdjord opened this issue Feb 20, 2020 · 3 comments

Comments

@macdjord
Copy link

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 argument list_arg, the injection works correctly
@jstasiak
Copy link
Collaborator

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

@macdjord
Copy link
Author

macdjord commented Feb 21, 2020

@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.

@jstasiak
Copy link
Collaborator

jstasiak commented Feb 21, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants