Requiring dependencies without having it declared on parameters


#1

I want to require dependencies without actually having it injected in my function. It would be useful for middleware-like yield components to log function calls, success, failures, or whatever.

But declare _metrics is unwanted because has no use here:

def func(a: Component1, _metrics: Metrics) -> int:
    return 1

So this handy decorator may be useful to run some component before the actual execution or a middleware around it.

from typing import List, Type
from apistar.server.components import Component

def inject(*component_types: List[Type[Component]]):
    """Inject additional dependencies."""

    def decorator(func: FunctionType):
        @wraps(func)
        def wrapped(*args, **kwargs):
            new_kwargs = {}
            for key, value in kwargs.items():
                if key.startswith('__injected_'):
                    continue
                new_kwargs[key] = value
            return func(*args, **new_kwargs)

        for component_type in component_types:
            key = '__injected_%s' % component_type.__name__
            wrapped.__annotations__[key] = component_type
        return wrapped
    return decorator

It is nice because it continues working as an standalone function, but now can also accept the additional components.

@inject(str)
def func(a: int, b: float) -> float:
    return a + b

assert func(1) == 2
assert func(1, 1) == 2
assert func(1, b=1) == 2
assert func(1, b=1, __injected_str="c") == 2

It iterate the kwargs dict on every call, but probably doesn’t impact that much. Anyway seems impossible to do without it.

So do you has any plans to include a utility like this one in the framework? Maybe even use this one?

---- EDIT: fix for the previous code

Just noted that Injector uses inspect.signature() instead of __annotations__ so it didn’t worked. Sorry! It still works fine for when not using inspect.signature().

Then after some hours I came with this stranger one but faster on the long run, I guess.

import ast
import inspect
import textwrap
from types import FunctionType
from typing import List, Type
from apistar.server.components import Component


def inject(*component_types: List[Type[Component]]):
    """Inject additional dependencies."""

    name = lambda c: '__injected_%s' % c.__name__
    none_expr = ast.NameConstant(lineno=0, col_offset=0, value=None)
    def decorator(func: FunctionType):
        func_source = textwrap.dedent(inspect.getsource(func))
        func_ast = ast.parse(func_source)

        for component_type in component_types:
            arg = ast.arg(arg=name(component_type), lineno=1, col_offset=1)
            func_ast.body[0].args.kwonlyargs.append(arg)
            func_ast.body[0].args.kw_defaults.append(none_expr)

        func_ast.body[0].decorator_list = []
        func_ast.body[0].name = '__func__'

        exec(compile(func_ast, '<file>', 'exec'))
        func = locals()['__func__']

        for component_type in component_types:
            func.__annotations__[name(component_type)] = component_type
        return func
    return decorator

So far so good. The above decorator works as is (with the previous example) but something like the following would be cleaner (even if it requires changes on the framework):

def inject(*component_types: List[Type[Component]]):
    def decorator(func: FunctionType):
        func.__additional_components = component_types
        return func
    return decorator

Apistar-contrib: Extra batteries for API Star 0.4+ (Session and CSRF)
Idea: Optional Cython speed up for ApiStar