Fastapi: [FEATURE] Dependency decorator

Created on 11 Nov 2019  路  8Comments  路  Source: tiangolo/fastapi

Is your feature request related to a problem? Please describe.
I've got dependency methods relying on other dependencies, not a routing/path level, where I don't need the return values. Look at the difference of the produce_kitten dependency before and after.

def often_true(is_true: bool):
    if not is_true:
        raise HTTPException(status_code=401)

def produce_kitten(possibly_true: NoReturn = Depends(often_true)):
    # only produce kittens if often_true has not raised
    pass

@app.get('/', dependencies=[Depends(do_something_interesting)])
def hello():
    return {'Hello': 'World'}

Describe the solution you'd like
If we'd be able to use dependencies in decorators for non-path/routing methods it would be more readable and concise IMO.

def often_true(is_true: bool):
    if not is_true:
        raise HTTPException(status_code=401)

@depends(dependencies=[Depends(often_true)])
def produce_kitten():
    # only produce kittens if often_true has not raised
    pass

@app.get('/', dependencies=[Depends(do_something_interesting)])
def hello():
    return {'Hello': 'World'}

Describe alternatives you've considered
People have suggested putting similar things in the router dependency level and just doing more routes in https://github.com/tiangolo/fastapi/issues/688, but then it seems better to just calling the lower subdependencies directly from where they're needed and avoid tainting the service contract.

enhancement

Most helpful comment

Well, I know this is an old closed thread, and I definitely don't have a compelling example, but just stopping by to give my 2c as a beginner that was a bit confused reading the documentation :) I really like something similar to what @MrMrRobat said. But also 100% agree with @dmontagu that readability is a personal preference.

I find this pretty clean and easy to use

@depends(my_dependency)
def produce_kitten(arg: None) -> None:
     # Ok, the function depends on something
     pass

This is a bit confusing to me, I find the logic harder to understand

def produce_kitten(arg: None = Depends(my_dependency)) -> None:
    # The default value of the argument of the function has a function called Depends, that takes my_dependency
    pass

This makes total sense, pretty comfortable to use since I've seen a lot of @login_required or @auth_required or @cross_origin() in Flask

@depends(admin_role)
@app.get('/')
def hello():
    # This is ok, easy to read, very common for django/flask
    pass 

This is pretty nice, similar to flask methods, very clean

@app.get('/', dependencies=[admin_role]) 
def login():
    # this is pretty nice, is cool that you don't have to stack decorators
    pass 
@app.get('/')
def login(user_data: UserFormData = Depends(admin_role)):
    # User data of schema xyz has a function as default value that depends on
    pass

My current use case is an ACL implementation. I've seen the fastapi_utils from @dmontagu which is great, and @cbv implementation that puts dependencies as class attributes is pretty nice really makes routes lighter, but still would prefer using cleaner decorators. Maybe someone comes up with a more convincing use case.

@henriklindgren and @MrMrRobat have you had the chance to implement something similar? I might be going that way to implement ACL.

All 8 comments

That's what I need right now!

But I would go with something like this.

@depends(often_true)
def produce_kitten(): ...

@app.get('/', dependencies=[do_something_interesting])
def hello(): ...

It's simpler, and can be handled this way as we passing it to special argument

Another question is, how we will return results from dependencies back to handler. I guess it could be implemented properly only this or similar way:

@depends(condition=often_true)
def produce_kitten(condition: bool): ...

@app.get('/', dependencies=dict(result=do_something_interesting))
def hello(result: InterestingResult): ...

But i would be happy to have option to pass dependencies as positional arguments, considering, that no return is needed (like in original often_true case)

Not to say that this is a bad feature necessarily, but I personally don't find this example very compelling.

If we'd be able to use dependencies in decorators for non-path/routing methods it would be more readable and concise IMO.

The version with @depends is actually longer than it would take to implement this using the existing system:

def produce_kitten(_: None = Depends(often_true)) -> None:
    # only produce kittens if often_true has not raised
    pass

(also, note: NoReturn is actually the wrong type hint here; that should only be used as a return type annotation, since no value can ever be of type NoReturn, and even then should only be used when the function always raises an exception, but in this case it can return None. And that's what that argument's value would be if the produce_kitten function were actually called by FastAPI.)

Also, in this specific case, you can easily eliminate the often_true dependency entirely:

def produce_kitten(is_true: bool):
    # only produce kittens if often_true has not raised
    if not is_true:
        raise HTTPException(status_code=401)

So I'm not (yet) convinced that an extra depends decorator would make things more readable or more concise.

For this case, I think it is at best a personal opinion that it is more readable, but I would argue that the best solution is to actually just change the pattern entirely. I worry that having this decorator would lead people to make more use of this semi-awkward pattern of having unused dependencies.


I recognize you may have a more complex example where this way of breaking apart the dependencies makes sense, but I think dependencies having other unused-argument dependencies is an anti-pattern more often than not, and is better handled through a function call inside a dependency and/or route, rather than being injected into another dependency via dependency injection.


Tweaks to the dependency injection system add a substantial amount of testing and documentation maintenance burden, and also add yet another thing a newcomer to a FastAPI codebase has to learn about. So I think this feature should meet a relatively high bar of usefulness in order to deserve implementation. I personally am not yet convinced this meets that bar, but a more compelling (set of) example(s) could definitely change my mind.

My real world example is authorization rules based on business logic where some of the rules are inherently collections of the other rules. The naming of the collection of rules into a method makes it clearer what the rules aims to capture when reading the router dependency, instead of having each lower level rule by itself in the router where I'd have to leave comment on what the group of rules entail. Also it would work as a cache solution for the lesser rules.

@dmontagu, I agree with your sentiment on the risk of adding complexity to FastAPI. I might try to write a decorator pattern which can be used in conjunction with FastAPI instead.

Thanks for clarifying NoReturn, noted, I dug it up for writing the example, now I know!

@henriklindgren Obviously it's hard to provide specific feedback without being able to see your code, but my suspicion is that you might be better served using a callable class dependency (i.e., a dependency which is a class instance, where the class implements __call__). You could then instantiate the class instance based on the settings you want (rather than injecting a subset of your nested None-returning dependencies).

This is closer to how the reusable_oauth2 dependency works from the tests/project generator (might be in the tutorial too, but I'm not sure): https://github.com/tiangolo/fastapi/blob/master/pending_tests/main.py#L31

This would also have the benefit of being marginally more performant -- each dependency injected comes with additional overhead.


It also wouldn't be that hard to write a self-contained decorator that accomplishes the original goal of this issue; my CBV gist might be useful if you want to go down that route but aren't sure where to start.

In particular, you'd want to look at this section:

        for name, hint in get_type_hints(cls).items():
            if getattr(hint, "__origin__", None) is ClassVar:
                continue
            value = getattr(cls, name, Ellipsis)
            parameter_kwargs = {}
            if value is not Ellipsis:
                parameter_kwargs["default"] = value
            dependency_names.append(name)
            new_parameters.append(
                inspect.Parameter(name=name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=hint, **parameter_kwargs)
            )
        new_signature = old_signature.replace(parameters=new_parameters)

        def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
            for dep_name in dependency_names:
                dep_value = kwargs.pop(dep_name)
                setattr(self, dep_name, dep_value)
            old_init(self, *args, **kwargs)

        setattr(new_init, "__signature__", new_signature)

replacing:

for name, hint in get_type_hints(cls).items():

with:

for dependency in dependencies:

and having new_init just be the decorated function that you return.

(Obviously there are other changes that need to happen, but that should give you a head start -- the idea is to just modify the function to take arbitrary keyword arguments, and then modify the function signature to add extra parameters for the unused dependencies; fastapi looks at the function signature to figure out what to inject.)

once merged :roll_eyes: this could be of use too (https://github.com/tiangolo/fastapi/pull/681)

Thanks for the suggestion @henriklindgren . But yeah, agreed to all @dmontagu said :point_up:

I think your use case or similar use cases are probably better handled in a different way. Calling the other function internally, or using a callable class to parameterize a dependency, if that's what you need.

Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.

Well, I know this is an old closed thread, and I definitely don't have a compelling example, but just stopping by to give my 2c as a beginner that was a bit confused reading the documentation :) I really like something similar to what @MrMrRobat said. But also 100% agree with @dmontagu that readability is a personal preference.

I find this pretty clean and easy to use

@depends(my_dependency)
def produce_kitten(arg: None) -> None:
     # Ok, the function depends on something
     pass

This is a bit confusing to me, I find the logic harder to understand

def produce_kitten(arg: None = Depends(my_dependency)) -> None:
    # The default value of the argument of the function has a function called Depends, that takes my_dependency
    pass

This makes total sense, pretty comfortable to use since I've seen a lot of @login_required or @auth_required or @cross_origin() in Flask

@depends(admin_role)
@app.get('/')
def hello():
    # This is ok, easy to read, very common for django/flask
    pass 

This is pretty nice, similar to flask methods, very clean

@app.get('/', dependencies=[admin_role]) 
def login():
    # this is pretty nice, is cool that you don't have to stack decorators
    pass 
@app.get('/')
def login(user_data: UserFormData = Depends(admin_role)):
    # User data of schema xyz has a function as default value that depends on
    pass

My current use case is an ACL implementation. I've seen the fastapi_utils from @dmontagu which is great, and @cbv implementation that puts dependencies as class attributes is pretty nice really makes routes lighter, but still would prefer using cleaner decorators. Maybe someone comes up with a more convincing use case.

@henriklindgren and @MrMrRobat have you had the chance to implement something similar? I might be going that way to implement ACL.

Was this page helpful?
0 / 5 - 0 ratings