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

RFC: Use annotations for parsing arguments #353

Open
sloria opened this issue Jan 4, 2019 · 6 comments
Open

RFC: Use annotations for parsing arguments #353

sloria opened this issue Jan 4, 2019 · 6 comments

Comments

@sloria
Copy link
Member

sloria commented Jan 4, 2019

Add a decorator that will parse request arguments from the view function's type annotations.

I've built a working proof of concept of this idea in webargs-starlette.

@app.route("/")
@use_annotations(locations=("query",))
async def index(request, name: str = "World"):
    return JSONResponse({"Hello": name})

A marshmallow Schema is generated from the annotations using Schema.TYPE_MAPPING to construct fields.

The code for use_annotations mostly isn't tied to Starlette and could be adapted for AsyncParser (and core.Parser when we drop Python 2 support).

@mbello
Copy link

mbello commented Dec 18, 2019

I was searching the web looking exactly for a solution to have method signature read by a decorator, parsed from request and fed to the method.
This is perfect for when you have a method that you suddenly want to make available on the web (e.g. as a CloudFunction or a Lambda). I would no longer have to change the method to take input from a request object or have to write a redundant declaration of the desired fields when I already have all type hints right there on the method signature.

Hoping to see this in webargs soon.

@mbello
Copy link

mbello commented Dec 18, 2019

But... I would prefer a different name. Annotations are a different thing.

Maybe @use_type_hints or @args_from_method_signature

@sirosen
Copy link
Collaborator

sirosen commented Mar 3, 2020

I keep circling back to this to think about it, but with the same concern. How would such a decorator distinguish between type annotations meant for it, vs ones which are just part of a view function?

In particular, if a view function is already decorated with user-defined decorators, it may be receiving a variety of arguments.

I think that for really simple cases like your example it's very nice. But what if instead of

def index(request, name: str = "World")

it's

@get_authenticated_username  # passes username
@use_annotations(location="query")
def index(request, username: str, name: str = "World")

or something similar?

Would we simply not support such usages? Maybe that's okay, and just has to be documented, or maybe there's a workaround?

@sirosen
Copy link
Collaborator

sirosen commented Sep 11, 2020

I was just looking at webargs-starlette for inspiration, thinking about whether or not we could do this as part of v7 (and whether there are any backwards-incompatible changes we'd like to make for it, which would make now a good time for it).

I think my above concern is basically a non-issue. First because nothing would force people to use such a feature, but also because we could easily add an optional exclude list to the annotations helper. My above case can then be solved simply with @use_annotations(location="query", exclude=["username"]).

@lukasjuhrich
Copy link

Not sure how active this undertaking is, but usability could be improved even further by using PEP593 typing.Annotated[…] objects, which have been included in python 3.9.

For instance, sqlalchemy uses them for its dataclass integration to augment to allow for more fine-grained configuration than what you can do with a type map.

@lukasjuhrich
Copy link

lukasjuhrich commented Mar 26, 2023

To elaborate, the objective is that passing e.g. fields.String as the data type breaks type checking, because clearly we get a string, not an instance of the field type, at runtime.

For instance, the following snippet with vanilla webargs has type hints correctly reflecting the types possible at runtime:

@bp.route('/accounts/<int:account_id>/json')
@use_kwargs({
    "style": fields.String(),
    "limit": fields.Int(),
    "offset": fields.Int(),
    "sort_by": fields.String(data_key="sort", missing="valid_on"),
    "sort_order": fields.String(data_key="order", missing="desc"),
    "search": fields.String(),
    "splitted": fields.Bool(missing=False),
}, location="query")
def accounts_show_json(
    account_id: int,
    *,
    style: str | None = None,
    limit: int | None = None,
    offset: int | None = None,
    sort_by: str,
    sort_order: str,
    search: str | None = None,
    splitted: bool,
): ...

However, clearly the information here is rendundant, so inferring the type information from the annotations is desirable.

Now with webargs-starlette, in my understanding the code would have to look like this:

@bp.route('/accounts/<int:account_id>/json')
@use_annotations(location="query", exclude={"account_id"})
def accounts_show_json(
    account_id: int,
    *,
    style: str | None = None,
    limit: int | None = None,
    offset: int | None = None,
    sort_by: fields.String(data_key="sort") = "valid_on",
    sort_order: fields.String(data_key="order") = "desc",
    search: str | None = None,
    splitted: bool = False,
):
    ...

However, as mentioned the above would not type check. PEP593 Annotated to the rescue:

import typing as t

@bp.route('/accounts/<int:account_id>/json')
@use_annotations(location="query", exclude={"account_id"})
def accounts_show_json(
    account_id: int,
    *,
    style: str | None = None,
    limit: int | None = None,
    offset: int | None = None,
    sort_by: t.Annotated[str, fields.String(data_key="sort")] = "valid_on",
    sort_order: t.Annotated[str, fields.String(data_key="order")] = "desc",
    search: str | None = None,
    splitted: bool = False,
):
    ...

Note that in the example, I expect default values to be incorporated in the field's missing= attribute, even if I give a field directly. I did not check whether webargs-starlette supports this.

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

No branches or pull requests

4 participants