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

Does werkzeug have plans to support ASGI? #1322

Open
Lynskylate opened this issue Jun 7, 2018 · 21 comments
Open

Does werkzeug have plans to support ASGI? #1322

Lynskylate opened this issue Jun 7, 2018 · 21 comments
Labels

Comments

@Lynskylate
Copy link

@Lynskylate Lynskylate commented Jun 7, 2018

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

@davidism
Copy link
Member

@davidism davidism commented Jun 7, 2018

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: #1322 (comment)

@edk0
Copy link
Member

@edk0 edk0 commented Jun 18, 2018

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.

@tomchristie
Copy link

@tomchristie tomchristie commented Jul 2, 2018

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

@ThiefMaster
Copy link
Member

@ThiefMaster ThiefMaster commented Jul 2, 2018

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?

@edk0
Copy link
Member

@edk0 edk0 commented Jul 2, 2018

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

@ThiefMaster
Copy link
Member

@ThiefMaster ThiefMaster commented Jul 2, 2018

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?

@edk0
Copy link
Member

@edk0 edk0 commented Jul 2, 2018

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.

@tomchristie
Copy link

@tomchristie tomchristie commented Jul 2, 2018

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']
@davidism
Copy link
Member

@davidism davidism commented Jul 2, 2018

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.

@ThiefMaster
Copy link
Member

@ThiefMaster ThiefMaster commented Jul 2, 2018

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.

@tomchristie
Copy link

@tomchristie tomchristie commented Jul 2, 2018

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.

@edk0
Copy link
Member

@edk0 edk0 commented Jul 4, 2018

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

@tomchristie
Copy link

@tomchristie tomchristie commented Oct 15, 2018

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

@tomchristie
Copy link

@tomchristie tomchristie commented Oct 15, 2018

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.

@tony
Copy link
Contributor

@tony tony commented May 13, 2019

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)

@kutenai
Copy link

@kutenai kutenai commented Jul 2, 2019

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!

@alexted

This comment has been hidden.

@jab
Copy link
Member

@jab jab commented Aug 18, 2019

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/hip#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.

@Sytten
Copy link

@Sytten Sytten commented Jan 8, 2020

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?

@davidism
Copy link
Member

@davidism davidism commented Mar 19, 2020

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.

@pgjones
Copy link
Member

@pgjones pgjones commented Mar 19, 2020

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.

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

Successfully merging a pull request may close this issue.

None yet