-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Replace dict-like interface with new state dict #5893
Conversation
Codecov Report
@@ Coverage Diff @@
## master #5893 +/- ##
==========================================
- Coverage 96.75% 96.75% -0.01%
==========================================
Files 44 44
Lines 9851 9847 -4
Branches 1591 1591
==========================================
- Hits 9531 9527 -4
Misses 182 182
Partials 138 138
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
19494fa
to
3541e90
Compare
# We cheat here slightly, to allow users to type hint their apps more easily. | ||
self._state: _T = {} # type: ignore[assignment] |
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 this is probably the right thing to do. I tried to take a look at ways this could be fixed but couldn't come up with any. I was thinking something along the lines of overloading __init__
but that doesn't seem to be the right approach?
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.
We're literally ignoring the type and assigning an empty dict. I don't think there's any way to achieve this without cheating the typing or doing something worse. I want to avoid asking the user to pass in a pre-made dict (which would be required for technically correct typing), as most of the dict values should typically be created in an app context.
@@ -171,6 +154,10 @@ def _set_loop(self, loop: Optional[asyncio.AbstractEventLoop]) -> None: | |||
stacklevel=2, | |||
) | |||
|
|||
@property | |||
def state(self) -> _T: |
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.
If I may ask, what is the thought process behind naming this state
?
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.
Mainly that I saw the old (private) variable was named state. It makes as much sense to me as anything else.
If others come up with an agreed name, we can change it.
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.
My initial thought is that it might have an adverse affect of causing users to using it for reading and writing when it seems to me that the intended purpose is to act more as global context that is set up in the beginning and then only read from? I also don't know if that is the case though. This is also just a minor nit, I think the name state
would probably be fine.
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.
While it's expected to have the top-level variables set on startup (before it freezes), you'll see plenty of examples of mutable objects being initialised for state, so I'm not sure that's an issue. For example, the web_ws.py example stores open web sockets in a list using the app object.
It's semi-read-only once frozen, so users trying to assign to the dict afterwards would get an error anyway, thus making it clear the intended behaviour.
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.
My bigger concern would be the verbosity of request.app.state[...]
, so I'd also consider a shorter variable name, such as flask's g
. Making it atleast a little bit shorter to write: request.app.g[...]
.
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 hadn't thought about verbosity. What do you think about continuing to support __getitem__
?
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 see any way to support static typing with it, and:
There should be one-- and preferably only one --obvious way to do it.
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.
Fair enough, can't argue with zen.
That should be everything ready now. |
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.
No sure if I like the proposed design.
I can imagine something like contextvars but getting the value from app/request:
appvar: AppVar[int] = AppVar('var', app)
or 'define type statically':
APP_INT_ARG:AppVarType[int] = AppVarType('var')
appvar: AppVar[int] = APP_INT_ARG(arg) # type can be inferred
What do you think?
I'm not sure I follow. Wouldn't that basically make all the variables globals? Which seems like something heavily avoided in aiohttp (in contrast to flask where globals are used all the time, which also resulted in my previous company serving Chinese translations to most users due to a library messing it up...). Taking just the relevant parts of one of the examples, this is the what the usage looks like with the current proposal:
Maybe if you can convert this example to demonstrate your proposal? |
No global variables, you are correct. My translation of your example could look like the following:
In this approach, I like that app and request are not generic objects, only I have no idea how to mix all these plugins into the same app without Please feel free to ask if you need more explanations / examples, I ma be bad in describing relative complex things. |
This feels even more verbose than simply annotating a variable every time:
It's slightly more type safe, but I think it's even less likely to actually be used. I think most users already lose typing from not wanting (or not remembering) to add these annotations every single time. My goal is really to minimise the amount of work needed by users and make it less likely that they lose type safety. As an example, taking some snippets from my own project, a user might write a handler like this:
Most users do not want to add the extra imports and annotations to every single handler. Every handler currently needs boilerplate code like:
With this proposal, none of that boilerplate would be needed, just update the request type and then everything is there:
I understand your argument, but I think it's easy enough to deal with. As it's only type checking, we can bypass it in the libraries. It might also be possible to do something with inheritance where the user would define their config something like I can put together a draft PR for aiohttp_jinja2 later to extend this proposal and see what it looks like. |
As an example from a random project using aiohttp and clearly interested in type safety: They've diligently added type annotations to all the functions and the object they are referencing is fully typed: But, they've lost all type safety on that object as they didn't add an annotation to the assignment. This is the typical use case where I want to ensure type safety is not lost, without overcomplicating things for the user. |
Wait, |
It was mentioned in the PR description. If a user has opted in to strict mode of mypy, it means they want this typing information to be available (it's basically a bug that they have lost the typing information). They'll just need to add the config dict and find/replace the handler signatures. Or possibly just change the import in views.py from I don't think a change to mypy errors counts as backwards incompatible. Otherwise, any new type annotation or change could potentially be backwards-incompatible (updating any typed library, or mypy itself, often results in new errors appearing in mypy). |
This PR has conflicts now. Needs rebasing. |
Let's agree that the change is desirable and should be merged, before I spend more time on it. This one can safely miss the 3.8 release. |
@asvetlov I've tried this out with aiohttp-jinja2, so you can take a look at how the integration works. The strict typing version is: aio-libs/aiohttp-jinja2#513 This also neatly summarises all the keys used by the library: https://github.com/aio-libs/aiohttp-jinja2/pull/513/files#diff-a36dd5434a69167cd4f6bc392daaa6205b9cb70aee122c6b573c08eee5c92b67R21-R26 The lazy version using |
@Dreamsorcerer I have no strong opinion here. Could you take a look at an alternative approach again :) pytest has a The value type information is stored in a key, different keys can interoperate without clashing. We can adopt this approach easily without making Application and Request classes generic. The backward compatibility is preserved by keeping both overloaded versions, e.g.
We can even raise a The only thing that I would like to improve in pytest's implementation is improving So far pytest's solution is the preferable approach in my eyes. I very appreciate your opinion about the proposal. Thank you for your patience! |
OK, that looks like a decent approach. I have one question regarding the implementation though, should we:
|
I like the third option: raise a warning and keep this behavior for a very long time |
Also, to include a key's name in |
Superseeded by #6498. |
What do these changes do?
Much better support for static typing by replacing the dict-like interface on
Application
with a newstate
attribute which can be typed as a generic.Are there changes in behavior for the user?
app[..]
toapp.state[...]
.app: Application[MyState] = Application()
def handler(request: Request[MyState]):
etc.
Minor points:
TypeError
rather thanRuntimeError
.Additional points
There are some points that should probably be thought about and changed in a future PR around subapps.
For example,
Request.config_dict
won't have any static typing support currently. Not sure what the best approach is to improve on that.It would also be possible to provide type-safety to
Application.add_routes()
etc. to ensure handlers are used on the correct apps. But, this would likely require makingUrlDispatcher
,AbstractRouteDef
andHandler
into generics. I'm not sure it's worth the extra work to get this working perfectly.This can probably be backported to 3.x with deprecation warnings when assigning to the old dict-like interface.
Related issue number
Fixes #5864
Checklist