-
-
Notifications
You must be signed in to change notification settings - Fork 463
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
Recover after #909 #925
Recover after #909 #925
Conversation
…itive with `Q(**{...})`
request.GET['foo'] = 'bar' | ||
|
||
def mk_request() -> HttpRequest: | ||
return HttpRequest() |
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.
Sorry, I fail to understand how this works.
None of these calls use .copy
or mutable=True
. Why is there any difference?
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 bad, probably I haven't explained that good enough.
We assume that anything annotated as HttpRequest
is referring to {W,A}SGIRequest
and thus has immutable GET
and POST
. However, this assumption doesn't hold for HttpRequest
itself that has mutable dicts. So when we instantiate HttpRequest
directly, overridden __new__
returns fake _MutableHttpRequest
class, so the first part of case is actually operating on it (see reveal_type
there). The second part works with HttpRequest
and assumes it's immutable.
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.
Do you think that having _MutableHttpRequest
is worth it?
In my experience, we almost never do such things.
I fear that it complicates our code quite a bit.
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.
@sobolevn For background, the reason of this complexity is not distinguishing between mutable/immutable QueryDict
, but about what types QueryDict
methods can return.
-
As streliakov points out in this comment, the
dict()
and__getitem__()
methods ofQueryDict
andMultiValueDict
may returnstr
as well as emptylist
(but never a list with length>0).IMO this is crazy behavior on Django's part -- it should throw KeyError -- but nothing we can do about that. This isn't mentioned in official Django documentation, but is mentioned in
MultiValueDict
docstring: https://github.com/django/django/blob/main/django/utils/datastructures.py#L78-L82 -
However, the vast majority of code that uses
QueryDict
is within the request lifecycle of Django. In these cases, the empty lists cannot appear. Sodict()
and__getitem__()
can safely be assumed to only havestr
values. -
Large amounts of code rely on the # 2 assumption, even example code from Django's official tutorial (explained here).
-
The
QueryDict
attributes of Django request are created withmutable=False
So there are three options:
- We could have one
QueryDict
class and these methods so they always returnUnion[str, List]
. This is the situation after Large update #909. But this causes false positive errors in a significant amount of idiomatic Django code, since most code assumes list return can not appear inrequest.GET["key"]
- Do the opposite, have one
QueryDict
class, and hint these methods as returningstr
. This is not strictly correct typing, although the list return is a rare fringe case. - A mix of the above, which is what this PR does. In Django request lifecycle, use the magic
_ImmutableQueryDict
type, which only holdsstr
values. In other cases, assume mutableQueryDict
may also return empty list for values.
I would prefer the second or third option out of these three.
…ment). Now it is resolved to `form_class` argument if present, but also errors if it is not subclass of _FormT
Thanks! I can confirm that this PR fixes all the regressions I saw in #909, and introduces no new regressions. (I would have tested sooner, but I was not made aware of this PR -- feel free to ping me directly next time). |
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.
Ok then, thanks a lot to both of you! 👍
@overload | ||
def pop(self, key: Union[str, bytes], /) -> str: ... | ||
@overload | ||
def pop(self, key: Union[str, bytes], default: Union[str, _Z] = ..., /) -> Union[str, _Z]: ... |
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.
django-stubs still supports Python 3.7, but the /
positional-only parameter syntax is supported only in Python 3.8+.
Opened PR #945 to fix this.
Fix revealed in #909 problems
Note on
QueryDict
supportNow
http.request.QueryDict
mutability is checked with plugin. When it is instantiated withmutable=True
or obtained fromquery_dict.copy()
, it is mutable, otherwise — immutable.mypy
will report error on the line where immutable version is modified and report any following code as unreachable."Convenient" signatures for
__getitem__: (str) -> str
anddict: () -> dict[str, str]
are guaranteed only for immutable instances.HttpRequest.GET
andHttpRequest.POST
are immutable in most cases. The only exception: when request is directly instantiated, it has mutable parts.Related issues
Refs #924
Fixes #916
Fixes #893