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

Add InjectorConfig to control explicit injection behavior #85

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions flask_injector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
except ImportError:
flask_restx = None

from injector import Binder, Injector, inject
from injector import Binder, Injector, get_bindings, inject
from flask import Config, Request
from werkzeug.local import Local, LocalManager, LocalProxy
from werkzeug.wrappers import Response
Expand All @@ -39,6 +39,13 @@
T = TypeVar('T', LocalProxy, Callable)


class InjectorConfig:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we could use a dataclass

inject_explicit_only: bool

def __init__(self, inject_explicit_only: bool = False) -> None:
self.inject_explicit_only = inject_explicit_only


def instance_method_wrapper(im: T) -> T:
@functools.wraps(im)
def wrapper(*args: Any, **kwargs: Any) -> Any:
Expand All @@ -47,7 +54,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
return wrapper # type: ignore


def wrap_fun(fun: T, injector: Injector) -> T:
def wrap_fun(fun: T, injector: Injector, config: InjectorConfig) -> T:
if isinstance(fun, LocalProxy):
return fun # type: ignore

Expand All @@ -64,7 +71,10 @@ def wrap_fun(fun: T, injector: Injector) -> T:

if hasattr(fun, '__call__') and not isinstance(fun, type):
try:
type_hints = get_type_hints(fun)
if config.inject_explicit_only:
type_hints = get_bindings(fun) # detect if we actually have anything to inject
else:
type_hints = get_type_hints(fun)
except (AttributeError, TypeError):
# Some callables aren't introspectable with get_type_hints,
# let's assume they don't have anything to inject. The exception
Expand All @@ -78,8 +88,13 @@ def wrap_fun(fun: T, injector: Injector) -> T:
type_hints.pop('return', None)
wrap_it = type_hints != {}
if wrap_it:
if config.inject_explicit_only:
return wrap_fun(fun, injector, config) # in this case we don't assume we want to inject everything by default

return wrap_fun(inject(fun), injector)



return fun


Expand Down Expand Up @@ -274,14 +289,14 @@ def get(self, key: Any, provider: Provider) -> Any:

_ModuleT = Union[Callable[[Binder], Any], Module]


class FlaskInjector:
def __init__(
self,
app: flask.Flask,
modules: Iterable[_ModuleT] = [],
injector: Injector = None,
request_scope_class: type = RequestScope,
inject_explicit_only: bool = False,
) -> None:
"""Initializes Injector for the application.

Expand All @@ -294,6 +309,7 @@ def __init__(
:param modules: Configuration for newly created :class:`injector.Injector`
:param injector: Injector to initialize app with, if not provided
a new instance will be created.
:param inject_explicit_only: If set to True, only inject parameters explicitly marked with Inject[SomeType].
:type app: :class:`flask.Flask`
:type modules: Iterable of configuration modules
:rtype: :class:`injector.Injector`
Expand All @@ -306,6 +322,7 @@ def __init__(
for module in modules:
injector.binder.install(module)

config = InjectorConfig(inject_explicit_only=inject_explicit_only)
for container in (
app.view_functions,
app.before_request_funcs,
Expand All @@ -315,7 +332,7 @@ def __init__(
app.jinja_env.globals,
app.error_handler_spec,
):
process_dict(container, injector)
process_dict(container, injector, config)

# This is to make sure that mypy sees a non-nullable variable
# in the closures below, otherwise it'd complain that injector
Expand Down Expand Up @@ -361,23 +378,23 @@ def blueprint_reset_request_scope_after(*args: Any, **kwargs: Any) -> None:
self.app = app


def process_dict(d: Dict, injector: Injector) -> None:
def process_dict(d: Dict, injector: Injector, config: InjectorConfig) -> None:
for key, value in d.items():
if isinstance(value, LocalProxy):
# We need this no-op here, because if we have a LocalProxy and try to use isinstance() on it
# we'll get a RuntimeError
pass
elif isinstance(value, list):
process_list(value, injector)
process_list(value, injector, config)
elif hasattr(value, '__call__'):
d[key] = wrap_fun(value, injector)
d[key] = wrap_fun(value, injector, config)
elif isinstance(value, dict):
process_dict(value, injector)
process_dict(value, injector, config)


def process_list(l: List, injector: Injector) -> None:
def process_list(l: List, injector: Injector, config: InjectorConfig) -> None:
# This function mutates the l parameter
l[:] = [wrap_fun(fun, injector) for fun in l]
l[:] = [wrap_fun(fun, injector, config) for fun in l]


class FlaskModule(Module):
Expand Down