10000 Fix `django.http` errors (#2654) · typeddjango/django-stubs@08cdfe5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 08cdfe5

Browse files
Fix django.http errors (#2654)
* HttpRequest does not inherit from BytesIo at runtime and only define a subset of the interface * Add missing`content` property * Fix `QueryDict.pop` and `QueryDict.popitem` return type * Add pyright ignore and fix import * Use `__new__` instead of `__init__` to specialize type so that pyright pass too
1 parent 68cc7ed commit 08cdfe5

File tree

6 files changed

+78
-26
lines changed

6 files changed

+78
-26
lines changed

django-stubs/http/request.pyi

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import datetime
2-
from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
3-
from io import BytesIO
2+
from collections.abc import Awaitable, Callable, Iterable, Iterator, Mapping, Sequence
43
from re import Pattern
54
from typing import Any, BinaryIO, Literal, NoReturn, TypeAlias, TypeVar, overload, type_check_only
65

@@ -36,7 +35,7 @@ class HttpHeaders(CaseInsensitiveMapping[str]):
3635
@classmethod
3736
def to_asgi_names(cls, headers: Mapping[str, Any]) -> dict[str, Any]: ...
3837

39-
class HttpRequest(BytesIO):
38+
class HttpRequest:
4039
GET: _ImmutableQueryDict
4140
POST: _ImmutableQueryDict
4241
COOKIES: dict[str, str]
@@ -104,7 +103,12 @@ class HttpRequest(BytesIO):
104103
def body(self) -> bytes: ...
105104
def _load_post_and_files(self) -> None: ...
106105
def accepts(self, media_type: str) -> bool: ...
107-
def readlines(self) -> list[bytes]: ... # type: ignore[override]
106+
def close(self) -> None: ...
107+
# File-like and iterator interface, a minimal subset of BytesIO.
108+
def read(self, n: int = -1, /) -> bytes: ...
109+
def readline(self, limit: int = -1, /) -> bytes: ...
110+
def __iter__(self) -> Iterator[bytes]: ...
111+
def readlines(self) -> list[bytes]: ...
108112

109113
@type_check_only
110114
class _MutableHttpRequest(HttpRequest):
@@ -113,36 +117,49 @@ class _MutableHttpRequest(HttpRequest):
113117

114118
_Z = TypeVar("_Z")
115119

116-
class QueryDict(MultiValueDict[str, str]):
120+
# mypy uses mro to pick between `__init__` and `__new__` for return types and will prefers `__init__` over `__new__`
121+
# in case of a tie. So to be able to specialize type via `__new__` (which is required for pyright to work),
122+
# we need to use an intermediary class for the `__init__` method.
123+
# See https://github.com/python/mypy/issues/17251
124+
# https://github.com/python/mypy/blob/c724a6a806655f94d0c705a7121e3d671eced96d/mypy/typeops.py#L148-L149
125+
class _QueryDictMixin:
126+
def __init__(
127+
self,
128+
query_string: str | bytes | None = ...,
129+
mutable: bool = ...,
130+
encoding: str | None = ...,
131+
) -> None: ...
132+
133+
class QueryDict(_QueryDictMixin, MultiValueDict[str, str]):
117134
_mutable: bool
118135
# We can make it mutable only by specifying `mutable=True`.
119136
# It can be done a) with kwarg and b) with pos. arg. `overload` has
120137
# some problems with args/kwargs + Literal, so two signatures are required.
121138
# ('querystring', True, [...])
122139
@overload
123-
def __init__(
124-
self: QueryDict,
140+
def __new__(
141+
cls,
125142
query_string: str | bytes | None,
126143
mutable: Literal[True],
127144
encoding: str | None = ...,
128-
) -> None: ...
145+
) -> QueryDict: ...
129146
# ([querystring='string',] mutable=True, [...])
130147
@overload
131-
def __init__(
132-
self: QueryDict,
148+
def __new__(
149+
cls,
133150
*,
134-
mutable: Literal[True],
135151
query_string: str | bytes | None = ...,
152+
mutable: Literal[True],
136153
encoding: str | None = ...,
137-
) -> None: ...
154+
) -> QueryDict: ...
138155
# Otherwise it's immutable
139156
@overload
140-
def __init__( # type: ignore[misc]
141-
self: _ImmutableQueryDict,
157+
def __new__(
158+
cls,
142159
query_string: str | bytes | None = ...,
143-
mutable: bool = ...,
160+
mutable: Literal[False] = ...,
144161
encoding: str | None = ...,
145-
) -> None: ...
162+
) -> _ImmutableQueryDict: ...
146163
@classmethod
147164
def fromkeys( # type: ignore[override]
148165
cls,
@@ -161,11 +178,11 @@ class QueryDict(MultiValueDict[str, str]):
161178
def setlistdefault(self, key: str | bytes, default_list: list[str] | None = ...) -> list[str]: ...
162179
def appendlist(self, key: str | bytes, value: str | bytes) -> None: ...
163180
# Fake signature (because *args is used in source, but it fails with more that 1 argument)
181+
@overload # type:ignore[override]
182+
def pop(self, key: str | bytes, /) -> list[str]: ...
164183
@overload
165-
def pop(self, key: str | bytes, /) -> str: ...
166-
@overload
167-
def pop(self, key: str | bytes, default: str | _Z = ..., /) -> str | _Z: ...
168-
def popitem(self) -> tuple[str, str]: ...
184+
def pop(self, key: str | bytes, default: str | _Z = ..., /) -> list[str] | _Z: ...
185+
def popitem(self) -> tuple[str, list[str]]: ... # type:ignore[override]
169186
def clear(self) -> None: ...
170187
def setdefault(self, key: str | bytes, default: str | bytes | None = ...) -> str: ...
171188
def copy(self) -> QueryDict: ...

django-stubs/http/response.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ class StreamingHttpResponse(HttpResponseBase, Iterable[bytes], AsyncIterable[byt
118118
def __aiter__(self) -> AsyncIterator[bytes]: ...
119119
def getvalue(self) -> bytes: ...
120120
@property
121+
def content(self) -> NoReturn: ...
122+
@property
121123
def text(self) -> NoReturn: ...
122124

123125
class FileResponse(StreamingHttpResponse):

scripts/stubtest/allowlist.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,3 +556,8 @@ django.core.mail.outbox
556556

557557
# Variable is supposed to be a set but is initialised to an empty dict
558558
django.contrib.gis.db.backends.base.features.BaseSpatialFeatures.unsupported_geojson_options
559+
560+
# We declare more strict types for this in stubs to avoid RuntimeErrors.
561+
# Django uses a `*args` parameter but crash if it contains more than 1 element.
562+
django.http.QueryDict.pop
563+
django.http.request.QueryDict.pop

scripts/stubtest/allowlist_todo.txt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,10 +1005,6 @@ django.forms.widgets.RadioSelect.use_fieldset
10051005
django.forms.widgets.SelectDateWidget.select_widget
10061006
django.forms.widgets.SelectDateWidget.use_fieldset
10071007
django.forms.widgets.Widget.use_fieldset
1008-
django.http.HttpRequest.__init__
1009-
django.http.StreamingHttpResponse.content
1010-
django.http.request.HttpRequest.__init__
1011-
django.http.response.StreamingHttpResponse.content
10121008
django.template.VariableDoesNotExist.__init__
10131009
django.template.base.FilterExpression.is_var
10141010
django.template.base.VariableDoesNotExist.__init__

scripts/stubtest/allowlist_todo_django52.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,4 @@ django.forms.BoundField.aria_describedby
122122
django.forms.boundfield.BoundField.aria_describedby
123123
django.forms.forms.BaseForm.bound_field_class
124124
django.forms.renderers.BaseRenderer.bound_field_class
125-
django.http.QueryDict.pop
126-
django.http.request.QueryDict.pop
127125
django.test.runner.ParallelTestSuite.handle_event
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from collections.abc import Iterable, Iterator
2+
3+
from django.http import QueryDict
4+
from django.http.request import _ImmutableQueryDict
5+
from typing_extensions import assert_type
6+
7+
q = QueryDict("", False)
8+
# Test constructor overloads -- Mutable
9+
assert_type(QueryDict("querystring", True), QueryDict)
10+
assert_type(QueryDict("querystring", mutable=True), QueryDict)
11+
12+
# Test constructor overloads -- Immutable
13+
assert_type(QueryDict(), _ImmutableQueryDict)
14+
assert_type(QueryDict("querystring"), _ImmutableQueryDict)
15+
assert_type(QueryDict("querystring", False), _ImmutableQueryDict)
16+
assert_type(QueryDict("querystring", mutable=False), _ImmutableQueryDict)
17+
18+
19+
# Test ImmutableQueryDict
20+
q = QueryDict()
21+
assert_type(q["a"], str)
22+
assert_type(q.get("a"), str | None)
23+
assert_type(q.items(), Iterator[tuple[str, str | list[object]]])
24+
assert_type(q.getlist("a"), list[str])
25+
assert_type(q.lists(), Iterable[tuple[str, list[str]]])
26+
27+
# Test MutableQueryDict
28+
mut_q = QueryDict(mutable=True)
29+
mut_q["a"] = "3"
30+
mut_q["a"] = ["1", "2"] # type: ignore[assignment] # pyright: ignore[reportArgumentType]
31+
32+
assert_type(mut_q.pop("a"), list[str])
33+
assert_type(mut_q.pop("a", 12), list[str] | int)
34+
assert_type(mut_q.popitem(), tuple[str, list[str]])

0 commit comments

Comments
 (0)
0