flask_injector icon indicating copy to clipboard operation
flask_injector copied to clipboard

Jinja/Flask `get_flashed_messages` fails

Open NoRePercussions opened this issue 1 year ago • 3 comments

Flask-Injector 0.14.0 Injector 0.20.1 Flask 2.1.3

Minimum reproducible example: https://gist.github.com/NoRePercussions/00a26a4d801a91b01a916f818dd27092

Summary

  • Flask-Injector adds injection capabilities to Jinja templates
  • get_flashed_messages from Flask gets a list of internal messages
  • While calling get_flashed_messages, F-I attempts to inject:
> Providing {'with_categories': <class 'bool'>, 'category_filter': typing.Iterable[str]} for <function get_flashed_messages at 0x103838670>
>> Injector.get(<class 'bool'>, scope=<class 'injector.NoScope'>) using <injector.ClassProvider object at 0x107b89b20>
>> Creating <class 'bool'> object with {}
>> Providing {} for <slot wrapper '__init__' of 'object' objects>
>>  -> False
[2023-07-12 17:35:53,377] ERROR in app: Exception on / [GET]
Traceback (most recent call last):
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 637, in get_binding
    return self._get_binding(interface, only_this_binder=is_scope or is_assisted_builder)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 631, in _get_binding
    raise KeyError
KeyError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/app.py", line 1519, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/app.py", line 1517, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/app.py", line 1503, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/app.py", line 8, in hello_world
    return render_template("helloworld.html")
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/templating.py", line 154, in render_template
    return _render(
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/templating.py", line 128, in _render
    rv = template.render(context)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/jinja2/environment.py", line 1301, in render
    self.environment.handle_exception()
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/jinja2/environment.py", line 936, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/templates/helloworld.html", line 1, in top-level template code
    <!DOCTYPE html>
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask_injector/__init__.py", line 89, in wrapper
    return injector.call_with_injection(callable=fun, args=args, kwargs=kwargs)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 999, in call_with_injection
    dependencies = self.args_to_inject(
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 91, in wrapper
    return function(*args, **kwargs)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 1047, in args_to_inject
    instance: Any = self.get(interface)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 932, in get
    binding, binder = self.binder.get_binding(interface)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 646, in get_binding
    binding = self.create_binding(interface)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 560, in create_binding
    provider = self.provider_for(interface, to)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 622, in provider_for
    raise UnknownProvider('couldn\'t determine provider for %r to %r' % (interface, to))
injector.UnknownProvider: couldn't determine provider for typing.Iterable[str] to None

Other concerns

  • Jinja's error trace appears to not identify the right error location?
  • F-I's injection appears to be blindly applied to all methods I call in Jinja, regardless of whether I want injection on them

NoRePercussions avatar Jul 12 '23 21:07 NoRePercussions

If I disable auto_bind:

[2023-07-12 17:54:14,660] ERROR in app: Exception on / [GET]
Traceback (most recent call last):
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 1047, in args_to_inject
    instance: Any = self.get(interface)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 932, in get
    binding, binder = self.binder.get_binding(interface)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 650, in get_binding
    raise UnsatisfiedRequirement(None, interface)
injector.UnsatisfiedRequirement: unsatisfied requirement on bool

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/app.py", line 1519, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/app.py", line 1517, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/app.py", line 1503, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/app.py", line 9, in hello_world
    return render_template("helloworld.html")
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/templating.py", line 154, in render_template
    return _render(
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask/templating.py", line 128, in _render
    rv = template.render(context)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/jinja2/environment.py", line 1301, in render
    self.environment.handle_exception()
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/jinja2/environment.py", line 936, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/templates/helloworld.html", line 1, in top-level template code
    <!DOCTYPE html>
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/flask_injector/__init__.py", line 89, in wrapper
    return injector.call_with_injection(callable=fun, args=args, kwargs=kwargs)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 999, in call_with_injection
    dependencies = self.args_to_inject(
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 91, in wrapper
    return function(*args, **kwargs)
  File "/Users/-/Projects/PycharmProjects/FlaskInjectorError/venv/lib/python3.9/site-packages/injector/__init__.py", line 1051, in args_to_inject
    raise e
injector.UnsatisfiedRequirement: flask.helpers has an unsatisfied requirement on bool

However, the app runs fine if Flask-Injector is not installed.

So far I haven't found any other methods in my actual web app with this issue.

NoRePercussions avatar Jul 12 '23 21:07 NoRePercussions

It's because: Flask-Injector lets you inject dependencies into Jinja environment globals functions (And I would even say it obliges them to be introduced into the Standard Context.)

and,

The following global variables are available within Jinja2 templates by default in the Standard Context (by the way, the "url_for()" is there, too)

So,

# Somewhere in imports
....
from injector import Module, provider
from typing import Iterable
....

# flask.get_flashed_messages(with_categories=False, category_filter=())
# Parameters:
# with_categories ([bool])– set to True to also receive categories.
# category_filter (Iterable][str]) – filter of categories to limit return values. Only categories in the list will be returned.

class GetFlashModule(Module):
    def configure(self, binder):
        binder.bind(bool, to=False)  # provide with_categories

    @provider
    def category_filter(self) -> Iterable[str]:
        return ()

FlaskInjector(app=app, modules=[GetFlashModule])
...

Or, the best solution, in my opinion:

# Somewhere in imports
...
from flask.helpers import get_flashed_messages
.
.
.
FlaskInjector(app=app, modules=[])

# after init the FlaskInjector
app.jinja_env.globals.update({'url_for': app.url_for, 'get_flashed_messages': get_flashed_messages})

# or, use 'context_processor' 
# @app.context_processor
# def noninject_ctx_processor():  
#    return dict(url_for=app.url_for, get_flashed_messages=get_flashed_messages)

...

I can't imagine a single use case where dependencies must be injected into the Standard template context. It's annoying and painful.

olehkodak avatar Aug 11 '23 11:08 olehkodak

I think the conclusion here would be the "inject in many places even if they don't declare they need injection" aspect of Flask-Injector's behavior is problematic.

It'd be a breaking change but I'm open to changing that.

jstasiak avatar Sep 05 '23 23:09 jstasiak