Skip to content

RFC: Use annotations for parsing arguments #353

Open

Description

@sloria

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).

Activity

  1. mbello commented on Dec 18, 2019

    @mbello

    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.

  2. mbello commented on Dec 18, 2019

    @mbello

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

    Maybe @use_type_hints or @args_from_method_signature

  3. sirosen commented on Mar 3, 2020

    @sirosen
    Collaborator

    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?

  4. sirosen commented on Sep 11, 2020

    @sirosen
    Collaborator

    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"]).

  5. lukasjuhrich commented on Mar 26, 2023

    @lukasjuhrich

    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.

  6. lukasjuhrich commented on Mar 26, 2023

    @lukasjuhrich

    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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions