-
Notifications
You must be signed in to change notification settings - Fork 213
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
Add extensions["lifespan.state"] #354
Conversation
The types module needs changes as well, jfyk. |
While looking at this PR I thought: "Why not introducing a new field on The thing is, if we do that, the scope on the I'm going to ping some people that might be interested in this. 👋 @pgjones @tomchristie |
5e93b99
to
cfdcecd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There were some rebases here, so we lost a bit the track of the previous comments. I'm going to give a bit of context.
The first implementation here was adding the "Lifespan State" extension to the "Extensions" section under the extensions["lifespan.state"]
, but it was also adding the extensions["state"]
on the lifespan scope. But there's no precedent about using the "extensions" key outside the "Extensions" section. So... The only thing missing on that first implementation was to change the extensions["state"]
to state
- ofc being optional.
The current implementation, I believe is a breaking change, based on previous comments from Andrew. Since it adds a subsection under the "Lifespan" section.
Anyway, just expressing here what we talked outside GitHub.
if sys.version_info >= (3, 11): | ||
from typing import NotRequired | ||
else: | ||
from typing_extensions import NotRequired |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the NotRequired should be included only here. This was previously mentioned on the HTTP trailers PR, and on the PR that introduced this module (I'm on my phone, difficult to provide links).
IMO either we change what we consider to be optional on those type hints, or we don't add the NotRequired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is that making it Optional
implies that the server must set it to None, so every single server is immediately out of spec. Using NotRequired
means that no changes are necessary to servers.
Thank you for all of the clarifying comments Marcelo.
I don't think this is a particularly salient reason to make it an extension or not. My original reasoning for making it an extension was to avoid conflict with Starlette's use of
I didn't really understand this comment. That said I'm happy to move it to |
I don't think we are understanding each other. Let me try to improve my wording:
Complying with the 3rd point doesn't make this proposal a non-extension. Analogous, let's take the HTTP Trailers previously implemented... The ASGI server needs to provide a As for the fact that Starlette is using this field... I think the most correct thing would be to find a namespace that doesn't conflict with a namespace that a known ASGI framework is already using, unless we are trying to document what the ASGI framework is already doing. Because in any case, if we do this, Starlette users are going to receive a different data on that namespace, so it's a breaking change. Maybe we can find a name that has the same meaning as "state"? - Let's read Andrew's comment before... 👇 Unrelated to this PR:
|
Thanks for clarifying, that makes more sense. That sounds fine to me! Since we've gone back and forth I'll wait for Andrew to chime in though before making any changes. I do want to point out that
Why would it be a breaking change? User's code will only break if they were doing something like I think re-using the field that is already being used is a good thing, it means Starlette and things that depend on it or imitate it (so most of the ASGI landscape) can start using this feature without making any changes internally. For example, I could add support to https://github.com/adriangb/asgi-lifespan while we figure out the API in Starlette to actually set values in the lifespan and then those values would show up transparently in the request/response cycle's |
The extension needs to be provided in the HTTP scope as well, but I think it's fine using the
Oh, yeah, actually, the user needs to add the field itself on the state initially. Got it. So yeah, the "state" field is fine. 👍 |
@andrewgodwin sorry to bother, but is there anything we can do to make your life easier while checking this PR? |
Sorry, I've been very busy with other things for the past month or so - I'll hopefully get to this soonish, but it's going to take time for me to re-read the initial ticket here to be in the right headspace. |
@andrewgodwin if you are too busy to review that's okay, but would you be able to recommend or be okay with tagging another reviewer? This PR/issue have been waiting for an in-depth review for almost a year now. |
We don't exactly have a lot of other people who review things on this repo, sadly. I would more maintainers! But they don't exactly appear out of thin air. I've just got back from Australia; I'll try to sit down and get to this this week. |
Alright, I have re-read it, understand it, and it looks good. Fix the linting error and I'll land it! |
extensions: Optional[Dict[str, Dict[object, object]]] | ||
|
||
|
||
class LifespanScope(TypedDict): | ||
type: Literal["lifespan"] | ||
asgi: ASGIVersions | ||
state: NotRequired[Dict[str, Any]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@andrewgodwin gave this a last pass and noticed that this was Optional[Dict[str, Any]]
but I think it should be NotRequired[Dict[str, Any]]
for the same reasons as https://github.com/django/asgiref/pull/354/files#r1118125962 so I changed it in 579bc3d. Just an FYI since you already reviewed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree - I also missed that!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adriangb why can't this be Dict[Any, Any]
rather than Dict[str, Any]
?
Below, the docs state:
Applications can store arbitrary data in this namespace.
Why wouldn't that include arbitrary keys?
applications should be cooperative by properly naming their keys such that they
will not collide with other frameworks or middleware.
The most foul-proof way to avoid collisions would be to not use string keys at all, but objects (similar to Symbol
keys in javascript).
Also, that would help make dependency injection simpler in FastAPI/Starlette.
Example use-case:
# (roughly)
HTTP_CLIENT: ClientSession = fastapi.Depends(lambda request: request.scope['state'][HTTP_CLIENT])
DB_POOL: Pool = fastapi.Depends(lambda request: request.scope['state'][DB_POOL])
@asynccontextmanager
async def app_lifespan(app: FastAPI):
async with make_http_client() as client:
async with make_database_connection_pool() as db_pool:
yield {HTTP_CLIENT: client, DB_POOL: db_pool}
I am already doing this and it works just fine! However, it is annoying that the typing doesn't match up quite right... and I don't see a reason keys should be limited to strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Starlette already assumes string keys for its state object, so I kind of copied that here.
It's always possible to do scope["state"]["mykey"] = {object(): 123}
.
Finally, I think if FastAPI ends up wanting to use this feature and shows a compelling use case for making the keys Any
we can re-evaluate and change the typing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adriangb thanks for the response. This isn't something I'd think would be part of the fastapi codebase (although, maybe, who knows). This is more a user-land example of a use-case for non-string keys. Would it be more appropriate for me to raise a similar issue in Starlette or Uvicorn then? As I mentioned, I am already doing the above approach (nothing prevents me). It's just that the typing is a lie, since I'm storing non-string keys. Since Starlette don't have any control over what kinds of keys are returned from the app lifespan function (example above), and similarly, Uvicorn doesn't have any control over what keys its lifespan state dict is updated with, it doesn't make sense to me to pretend they are definitely strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing in particular that will break is Starlette's State
class: https://github.com/encode/starlette/blob/15d9350eed9437bcb0683e2c24cd4cfbf985ed7e/starlette/datastructures.py#L683-L708. Now, I'm not a fan of that class/functionality, but it exists. Of course, if you don't care you can continue doing what you are doing, but then solving the typing issue is also as simple as a type: ignore
.
I'm also curious why the suggestion above of something like yield {"dependencies": {...}}
or something like that wouldn't work.
All of that said, if you feel strongly please feel free to open an issue / PR so it can be discussed properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adriangb thanks for the feedback, I will raise one! I did look at that class as well prior to implementing things the way that I did, but it doesn't seem like anything about it would break. In particular, __setattr__
and __getattr__
are both already typed with key: typing.Any
. If they are called via state.my_property
, obviously that usage simply wouldn't be applicable for non-string keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right the typing is just wrong/lazy on Starlette's part there. It really should be key: str
and I expect it to be changed in Starlette soonish.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adriangb as promised, I've raised an issue here: encode/starlette#2182. Thanks again for the feedback.
Thanks for your work (and persistence) on this! |
Thank you for reviewing and pushing this across the finish line! I'm excited to explore the new APIs this will unblock. I think I should have changed the PR title since the commit message is now out of date with the implementation, sorry about that... |
* ``state`` Optional(*dict[Unicode string, Any]*) -- A copy of the | ||
namespace passed into the lifespan corresponding to this request. (See :doc:`lifespan`). | ||
Optional; if missing the server does not support this feature. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is missing in the WebSocket
section below.
@@ -56,6 +59,38 @@ lifespan protocol. If you want to log an error that occurs during lifespan | |||
startup and prevent the server from starting, then send back | |||
``lifespan.startup.failed`` instead. | |||
|
|||
The ``extensions["lifespan.state"]`` dict is an empty namespace. | |||
Applications can store arbitrary data in this namespace. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is unclear... In the next section it's specified to store in lifespan["state"]
, but here it mentions that applications can use extension["lifespan.state"]
... 🤔
Was this supposed to be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I think so
Closes #322