Skip to content

Commit 799b41f

Browse files
fix typing on HttpResponse and StreamingHttpResponse (#712)
While the documentation for `HttpResponse` and `StreamingHttpResponse` *says* `content` and `streaming_content` should be bytestrings [1] or an iterable of bytestrings respectively [2], this is not what the API supports [3] [4] and there are tests which make sure the API supports more than bytestrings [5] [6] [etc]. Before assigning `content` or `streaming_content` the code paths will call `self.make_bytes` to coerce the value to bytes. [1]: https://github.com/django/django/blob/ecf87ad513fd8af6e4a6093ed918723a7d88d5ca/django/http/response.py#L324-L327 [2]: https://github.com/django/django/blob/0a28b42b1510b8093a90718bafd7627ed67fa13b/django/http/response.py#L395-L399 [3]: https://github.com/django/django/blob/ecf87ad513fd8af6e4a6093ed918723a7d88d5ca/django/http/response.py#L342-L362 [4]: https://github.com/django/django/blob/0a28b42b1510b8093a90718bafd7627ed67fa13b/django/http/response.py#L415-L427 [5]: https://github.com/django/django/blob/0a28b42b1510b8093a90718bafd7627ed67fa13b/tests/cache/tests.py#L2250 [6]: https://github.com/django/django/blob/0a28b42b1510b8093a90718bafd7627ed67fa13b/tests/i18n/urls.py#L8
1 parent fb4d204 commit 799b41f

File tree

2 files changed

+136
-10
lines changed

2 files changed

+136
-10
lines changed

django-stubs/http/response.pyi

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22
from io import BytesIO
33
from json import JSONEncoder
4-
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Type, Union, overload
4+
from typing import Any, Dict, Generic, Iterable, Iterator, List, Optional, Tuple, Type, TypeVar, Union, overload
55

66
from django.core.handlers.wsgi import WSGIRequest
77
from django.http.cookie import SimpleCookie
@@ -10,6 +10,31 @@ from django.test.client import Client
1010
from django.urls import ResolverMatch
1111
from django.utils.datastructures import CaseInsensitiveMapping
1212

13+
_T = TypeVar("_T")
14+
_U = TypeVar("_U")
15+
16+
class _PropertyDescriptor(Generic[_T, _U]):
17+
"""
18+
This helper property descriptor allows defining asynmetric getter/setters
19+
which mypy currently doesn't support with either:
20+
21+
class HttpResponse:
22+
@property
23+
def content(...): ...
24+
@property.setter
25+
def content(...): ...
26+
27+
or:
28+
29+
class HttpResponse:
30+
def _get_content(...): ...
31+
def _set_content(...): ...
32+
content = property(_get_content, _set_content)
33+
"""
34+
35+
def __get__(self, instance: Any, owner: Optional[Any]) -> _U: ...
36+
def __set__(self, instance: Any, value: _T) -> None: ...
37+
1338
class BadHeaderError(ValueError): ...
1439

1540
class ResponseHeaders(CaseInsensitiveMapping):
@@ -21,7 +46,7 @@ class ResponseHeaders(CaseInsensitiveMapping):
2146
def pop(self, key: str, default: Optional[str] = ...) -> str: ...
2247
def setdefault(self, key: str, value: str) -> None: ...
2348

24-
class HttpResponseBase(Iterable[Any]):
49+
class HttpResponseBase:
2550
status_code: int = ...
2651
streaming: bool = ...
2752
cookies: SimpleCookie = ...
@@ -72,10 +97,9 @@ class HttpResponseBase(Iterable[Any]):
7297
def seekable(self) -> bool: ...
7398
def writable(self) -> bool: ...
7499
def writelines(self, lines: Iterable[object]): ...
75-
def __iter__(self) -> Iterator[Any]: ...
76100

77-
class HttpResponse(HttpResponseBase):
78-
content: Any
101+
class HttpResponse(HttpResponseBase, Iterable[bytes]):
102+
content = _PropertyDescriptor[object, bytes]()
79103
csrf_cookie_set: bool
80104
redirect_chain: List[Tuple[str, int]]
81105
sameorigin: bool
@@ -85,6 +109,7 @@ class HttpResponse(HttpResponseBase):
85109
def __init__(self, content: object = ..., *args: Any, **kwargs: Any) -> None: ...
86110
def serialize(self) -> bytes: ...
87111
__bytes__ = serialize
112+
def __iter__(self) -> Iterator[bytes]: ...
88113
@property
89114
def url(self) -> str: ...
90115
# Attributes assigned by monkey-patching in test client ClientHandler.__call__()
@@ -96,13 +121,12 @@ class HttpResponse(HttpResponseBase):
96121
context: Context
97122
resolver_match: ResolverMatch
98123
def json(self) -> Any: ...
99-
def __iter__(self): ...
100124
def getvalue(self) -> bytes: ...
101125

102-
class StreamingHttpResponse(HttpResponseBase):
103-
content: Any
104-
streaming_content: Iterator[bytes]
105-
def __init__(self, streaming_content: Iterable[bytes] = ..., *args: Any, **kwargs: Any) -> None: ...
126+
class StreamingHttpResponse(HttpResponseBase, Iterable[bytes]):
127+
streaming_content = _PropertyDescriptor[Iterable[object], Iterator[bytes]]()
128+
def __init__(self, streaming_content: Iterable[object] = ..., *args: Any, **kwargs: Any) -> None: ...
129+
def __iter__(self) -> Iterator[bytes]: ...
106130
def getvalue(self) -> bytes: ...
107131

108132
class FileResponse(StreamingHttpResponse):
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
- case: http_response
2+
main: |
3+
from django.http.request import HttpRequest
4+
from django.http.response import HttpResponse
5+
from django.utils.translation import gettext_lazy as _
6+
7+
def empty_response(request: HttpRequest) -> HttpResponse:
8+
return HttpResponse()
9+
10+
def str_response(request: HttpRequest) -> HttpResponse:
11+
return HttpResponse('It works!')
12+
13+
def bytes_response(request: HttpRequest) -> HttpResponse:
14+
return HttpResponse(b'It works!')
15+
16+
def object_response(request: HttpRequest) -> HttpResponse:
17+
return HttpResponse(_('It works!'))
18+
19+
- case: http_response_content
20+
main: |
21+
from django.http.request import HttpRequest
22+
from django.http.response import HttpResponse
23+
from django.utils.translation import gettext_lazy as _
24+
25+
def empty_response(request: HttpRequest) -> HttpResponse:
26+
response = HttpResponse()
27+
reveal_type(response.content) # N: Revealed type is "builtins.bytes*"
28+
return response
29+
30+
def str_response(request: HttpRequest) -> HttpResponse:
31+
response = HttpResponse()
32+
response.content = 'It works!'
33+
reveal_type(response.content) # N: Revealed type is "builtins.bytes*"
34+
return response
35+
36+
def bytes_response(request: HttpRequest) -> HttpResponse:
37+
response = HttpResponse()
38+
response.content = b'It works!'
39+
reveal_type(response.content) # N: Revealed type is "builtins.bytes*"
40+
return response
41+
42+
def object_response(request: HttpRequest) -> HttpResponse:
43+
response = HttpResponse()
44+
response.content = _('It works!')
45+
reveal_type(response.content) # N: Revealed type is "builtins.bytes*"
46+
return response
47+
48+
- case: streaming_http_response
49+
main: |
50+
from django.http.request import HttpRequest
51+
from django.http.response import StreamingHttpResponse
52+
from django.utils.translation import gettext_lazy as _
53+
54+
def empty_response(request: HttpRequest) -> StreamingHttpResponse:
55+
return StreamingHttpResponse()
56+
57+
def str_response(request: HttpRequest) -> StreamingHttpResponse:
58+
return StreamingHttpResponse(['It works!'])
59+
60+
def bytes_response(request: HttpRequest) -> StreamingHttpResponse:
61+
return StreamingHttpResponse([b'It works!'])
62+
63+
def object_response(request: HttpRequest) -> StreamingHttpResponse:
64+
return StreamingHttpResponse([_('It works!')])
65+
66+
def mixed_response(request: HttpRequest) -> StreamingHttpResponse:
67+
return StreamingHttpResponse([_('Yes'), '/', _('No')])
68+
69+
- case: streaming_http_response_streaming_content
70+
main: |
71+
from django.http.request import HttpRequest
72+
from django.http.response import StreamingHttpResponse
73+
from django.utils.translation import gettext_lazy as _
74+
75+
def empty_response(request: HttpRequest) -> StreamingHttpResponse:
76+
response = StreamingHttpResponse()
77+
reveal_type(response.streaming_content) # N: Revealed type is "typing.Iterator*[builtins.bytes]"
78+
return response
79+
80+
def str_response(request: HttpRequest) -> StreamingHttpResponse:
81+
response = StreamingHttpResponse()
82+
response.streaming_content = ['It works!']
83+
reveal_type(response.streaming_content) # N: Revealed type is "typing.Iterator*[builtins.bytes]"
84+
return response
85+
86+
def bytes_response(request: HttpRequest) -> StreamingHttpResponse:
87+
response = StreamingHttpResponse()
88+
response.streaming_content = [b'It works!']
89+
reveal_type(response.streaming_content) # N: Revealed type is "typing.Iterator*[builtins.bytes]"
90+
return response
91+
92+
def object_response(request: HttpRequest) -> StreamingHttpResponse:
93+
response = StreamingHttpResponse()
94+
response.streaming_content = [_('It works!')]
95+
reveal_type(response.streaming_content) # N: Revealed type is "typing.Iterator*[builtins.bytes]"
96+
return response
97+
98+
def mixed_response(request: HttpRequest) -> StreamingHttpResponse:
99+
response = StreamingHttpResponse()
100+
response.streaming_content = [_('Yes'), '/', _('No')]
101+
reveal_type(response.streaming_content) # N: Revealed type is "typing.Iterator*[builtins.bytes]"
102+
return response

0 commit comments

Comments
 (0)