Werkzeug: Does werkzeug have plans to support ASGI?

Created on 7 Jun 2018  ·  21Comments  ·  Source: pallets/werkzeug

Werkzeug offers many useful methods, it would be much easier if it supported ASGI than if we started from scratch.

ASGI

Most helpful comment

Yes, Werkzeug and Flask will eventually support ASGI. I do not have a timeline for this, although I would be happy to help review a PR if someone started one.

However, I'm not going to be the one that implements it, I need help from the community. See the latest update below: https://github.com/pallets/werkzeug/issues/1322#issuecomment-600926145

All 21 comments

Yes, Werkzeug and Flask will eventually support ASGI. I do not have a timeline for this, although I would be happy to help review a PR if someone started one.

However, I'm not going to be the one that implements it, I need help from the community. See the latest update below: https://github.com/pallets/werkzeug/issues/1322#issuecomment-600926145

I'm interested in working on this.

I have some working but hacky ASGI support going on: werkzeug, flask. I'd like to understand more about any plans you may have before going any further.

Things that occur to me already, beyond the fact that everything I've done there could be generally nicer:

  • I'm supporting the form parser by just running its synchronous code in a thread (which I block while I read the data asynchronously). I don't think this will do. My preferred approach would be to factor out the IO.
  • Context-local support is pretty fragile. There seems to be a right way to do this, but it involves interfering with global state, and especially with ASGI we can't assume we own the world.
  • There's no obvious way, when running under ASGI, to parse form data on-demand for a synchronous function. It might be worth considering eagerly parsing form data unless we can tell the view function is async. Whatever the default is, there could obviously be a decorator to override it.

Please let me know your thoughts.

I'm supporting the form parser by just running its synchronous code in a thread (which I block while I read the data asynchronously). I don't think this will do. My preferred approach would be to factor out the IO.

I guess the light touch approach will be just to reimplement the existing parser, but with async IO. It'd be cleaner if the two parser classes could share a common sans-IO implementation under the hood, but it might be more practical just to duplicate & slightly modify the existing implementation.

Context-local support is pretty fragile.

Context locals (for asyncio) exist in the 3.7 stdlib. https://docs.python.org/3.7/library/contextvars.html
I'd guess use those, with an optional compat library to support earlier versions of python. (Or else just assume that ASGI on Flask would end up being a 3.7+ thing)

Does werkzeug use thread-locals? (I'm aware that Flask does)

There's no obvious way, when running under ASGI, to parse form data on-demand for a synchronous function

I'd suggest not offering a synchronous interface for parsing form data. (Or for accessing the request body in any way), and instead just offer async APIs onto it. See Starlette's API for some examples here... https://github.com/encode/starlette#body

Since Python 3.7 is out I wouldn't be opposed to having async features requiring Python 3.7 as long as no other code is affected by it. But if there's a backport that's as good as the native 3.7 solution - even better!

Regarding async form data parsing... I guess something like await request.parse() in an async function would be sufficient, and then raise an exception when trying to access unparsed form data?

Sure, or just make values and friends asynchronous themselves: values = await request.values or values = await request.values(), depending mostly on which looks nicer.

wouldn't that require rather ugly things like (await request.form)['foo'] to do an async call while getting a dict element directly without assigning in between?

yes :(

I'm not sure that's particularly avoidable, though. I don't think

form = await request.form
form['foo']

is really any more or less ugly than

await request.parse()
request.form['foo']

though that's obviously subject to taste.

I guess we could also invent an "async dict" whose _members_ are all async-ified instead, but without having seen one I'd imagine it would end up rather confusing.

Sure, or just make values and friends asynchronous themselves: values = await request.values or values = await request.values(), depending mostly on which looks nicer.

I'd suggest using function calls for I/O-performing operations, rather than properties.

wouldn't that require rather ugly things like (await request.form)['foo'] to do an async call while getting a dict element directly without assigning in between?

Shrug - Don't do that.

asyncio is necessarily more explicit about which parts of the codebase perform I/O, so I'd tend to split those out into separate lines.

form = await request.form()
form['foo']

While I'm all for adding async support, we still can't break Python 2 and sync versions. One approach I heard about from @njsmith is to write everything as async, then use a tool similar to 2to3 to generate the sync version. Apparently it's being tried in urllib3, but I don't know enough about it.

I wonder if we could add magic to the objects behind request.form etc. so calling them will do async stuff while the usual dict-like methods will be sync (and would fail in async mode). Or we could just fail any access to request.form etc. in async mode and use a separate name for the async versions, e.g. request.parse_form().

Or... request_class = AsyncRequest if someone wants async; this could actually be a default in an AsyncFlask class.

Or... request_class = AsyncRequest if someone wants async; this could actually be a default in an AsyncFlask class.

That's the sort of approach that'd make sense to me, yeah.

Regarding the form parser, I've made an attempt at sansio-ing it in #1330.

There's also a streaming form parser implementation at https://github.com/andrew-d/python-multipart with 100% coverage. (I found one other but it wasn't obvious that it could be easily adapted into a "feed data, handle events" flow.)

python-multipart is the library I'm now using for Starlette. You can take a look at the integration with the async stream here: https://github.com/encode/starlette/blob/master/starlette/formparsers.py#L207

I've also been thinking about best ways to present a sync and async compatible interface, since I also want that for Starlette (although going in the other direction to Werkzeug, for me it's about "I have an existing async interface, how do I now also present a sync one")

For Starlette I think I'll probably push the actual parsing into an async parse(self) method, and typically call that sometime during request dispatch, but expose regular plain properties form, files etc... for accessing the results from user code.

Off topic, but related to a popular ASGI and werkzeug case (I don't want to derail this issue, but don't want to create a duplicate issue without substance to add):

If you're running https://github.com/django-extensions/django-extensions with ./manage.py runserver_plus and https://github.com/django/channels (which uses ASGI) is giving Opcode -1 (opcode minus 1) in inspector, try running ./manage.py runserver for now until ASGI is supported.

(Werkzeug is used underneath the hood on Django with django-extensions and I spent a few hours figuring out why since I'm so used to developing with runserver_plus for debugging)

I use runserver_plus so that I can use https in development.
I am now trying to add Websockets support using Django channels, and have run into the issue that the channels uses runserver and does nto support runserver_plus. So, I can use channels OR I can use https, not both!

@davidism please tell me if there are any changes in the implementation of ASGI support for Flask since your last answer in this topic and whether your plans have changed in connection with this task to end support for Python 2 ?

On Jul 2, 2018 @davidism writes:

Apparently it's being tried in urllib3, but I don't know enough about it.

Just saw this from a while back – if anyone's interested in learning more, it looks like python-trio/urllib3#1 has a bunch of the details. Note the link to urllib3/urllib3#1323, which contains:

Solution: we maintain one copy of the code – the version with async/await annotations – and then a little script maintains the synchronous copy by automatically stripping them out again. It's not beautiful, but as far as I can tell all the alternatives are worse...

(Keep reading there if interested.)

Nice to see this has apparently been continuing to work well, based on the steady progress being made at https://github.com/python-trio/urllib3/commits/bleach-spike.

Small bump to make this issue back on the radar. With 3.5 reaching EOL this year, I think it's a good time to start thinking about async support?

In the year and a half since this was posted, there hasn't been much activity to implement it. I don't personally have any experience or need for asyncio, and although I do like ASGI, it was never something that I was going to take on myself.

In the mean time, @pgjones, author of Quart, has become more involved in Werkzeug. Quart now uses Werkzeug behind the scenes where possible, and we're continuing to develop that. There was some pushback from a Flask maintainer about going with ASGI, so Phil also created pallets/flask#3412 that at least allowed routing to async def functions, but that has been sitting for a while now. At this point I'd rather go ASGI than settle for that. @edk0 created #1330 to make form parsing sans-io, but it's also been sitting, and should probably go through some more design and review first.

You might ask, "Why can't Flask do what Django did?" I am not an expert on the internals of Django, but @andrewgodwin explained to me a while ago that Django has an "easier" (read: still very complicated) time of it due to how it originally adapted to WSGI, as opposed to the very WSGI-centric API that Werkzeug and Flask started with. Also, Django just gets a ton more full time attention and resources than Pallets does.

So where does that leave this issue? If you want a Flask-compatible framework that uses Werkzeug, use Quart. Contribute to Quart (or Flask) to make them more API compatible where that's missing. If you want Werkzeug and Flask to support ASGI, you are going to need to step up. Start learning about ASGI. Start identifying the WSGI-specific and blocking parts of Werkzeug's API. Start thinking of abstractions we can make to enable implementations for both WSGI and ASGI. Then bring that research back to this discussion so we can start designing and writing PRs.

Thanks for the Quart suggestion, I'd be very happy to accept contributions to it.

I've tried to answer why I think Flask can't do what Django has done in this article. Ultimately I think pallets/flask#3412 is the best solution for Flask.

In terms of Werkzeug I think that ASGI is possible, with some pain. A notable example of the pain is that many things in Werkzeug are WSGI callables (e.g. exceptions). With ASGI it isn't clear how this functionality could/should be used, so I'd prefer to remove it.

My plan is to keep integrating Werkzeug into Quart adjusting Werkzeug towards ASGI (sans-io) as I go (as much as can be accepted) - my only obstacle is a lack of time.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

SimonSapin picture SimonSapin  ·  12Comments

androiddrew picture androiddrew  ·  14Comments

sorenh picture sorenh  ·  4Comments

Nessphoro picture Nessphoro  ·  6Comments

taion picture taion  ·  7Comments