Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions django/core/handlers/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import traceback
from collections import defaultdict
from contextlib import aclosing, closing
from io import BytesIO

from asgiref.sync import ThreadSensitiveContext, sync_to_async

Expand Down Expand Up @@ -146,6 +147,21 @@ def close(self):
super().close()
self._stream.close()

def __getstate__(self):
state = self.__dict__.copy()
state["stream"] = self._stream.read()
del state["_stream"]
return state

def __setstate__(self, state):
stream = state.pop("stream")
self.__dict__.update(state)
try:
content_length = int(self.scope.get("CONTENT_LENGTH"))
except (ValueError, TypeError):
content_length = 0
self._stream = base.LimitedStream(BytesIO(stream), content_length)


class ASGIHandler(base.BaseHandler):
"""Handler for ASGI requests."""
Expand Down Expand Up @@ -365,3 +381,7 @@ def chunk_bytes(cls, data):
(position + cls.chunk_size) >= len(data),
)
position += cls.chunk_size

def __setstate__(self, state):
self.__dict__.update(state)
self.load_middleware(is_async=True)
50 changes: 50 additions & 0 deletions django/core/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import types
from inspect import iscoroutinefunction
from io import IOBase

from asgiref.sync import async_to_sync, sync_to_async

Expand All @@ -18,6 +19,47 @@
logger = logging.getLogger("django.request")


class LimitedStream(IOBase):
"""
Wrap another stream to disallow reading it past a number of bytes.

Based on the implementation from werkzeug.wsgi.LimitedStream. See:
https://github.com/pallets/werkzeug/blob/dbf78f67/src/werkzeug/wsgi.py#L828
"""

def __init__(self, stream, limit):
self._read = stream.read
self._readline = stream.readline
self._pos = 0
self.limit = limit

def read(self, size=-1, /):
_pos = self._pos
limit = self.limit
if _pos >= limit:
return b""
if size == -1 or size is None:
size = limit - _pos
else:
size = min(size, limit - _pos)
data = self._read(size)
self._pos += len(data)
return data

def readline(self, size=-1, /):
_pos = self._pos
limit = self.limit
if _pos >= limit:
return b""
if size == -1 or size is None:
size = limit - _pos
else:
size = min(size, limit - _pos)
line = self._readline(size)
self._pos += len(line)
return line


class BaseHandler:
_view_middleware = None
_template_response_middleware = None
Expand Down Expand Up @@ -366,6 +408,14 @@ def process_exception_by_middleware(self, exception, request):
return response
return None

def __getstate__(self):
state = self.__dict__.copy()
del state["_view_middleware"]
del state["_template_response_middleware"]
del state["_exception_middleware"]
del state["_middleware_chain"]
return state


def reset_urlconf(sender, **kwargs):
"""Reset the URLconf after each request is finished."""
Expand Down
71 changes: 28 additions & 43 deletions django/core/handlers/wsgi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from io import IOBase
from io import BytesIO

from django.conf import settings
from django.core import signals
Expand All @@ -12,47 +12,6 @@
_slashes_re = _lazy_re_compile(rb"/+")


class LimitedStream(IOBase):
"""
Wrap another stream to disallow reading it past a number of bytes.

Based on the implementation from werkzeug.wsgi.LimitedStream. See:
https://github.com/pallets/werkzeug/blob/dbf78f67/src/werkzeug/wsgi.py#L828
"""

def __init__(self, stream, limit):
self._read = stream.read
self._readline = stream.readline
self._pos = 0
self.limit = limit

def read(self, size=-1, /):
_pos = self._pos
limit = self.limit
if _pos >= limit:
return b""
if size == -1 or size is None:
size = limit - _pos
else:
size = min(size, limit - _pos)
data = self._read(size)
self._pos += len(data)
return data

def readline(self, size=-1, /):
_pos = self._pos
limit = self.limit
if _pos >= limit:
return b""
if size == -1 or size is None:
size = limit - _pos
else:
size = min(size, limit - _pos)
line = self._readline(size)
self._pos += len(line)
return line


class WSGIRequest(HttpRequest):
def __init__(self, environ):
script_name = get_script_name(environ)
Expand All @@ -75,7 +34,7 @@ def __init__(self, environ):
content_length = int(environ.get("CONTENT_LENGTH"))
except (ValueError, TypeError):
content_length = 0
self._stream = LimitedStream(self.environ["wsgi.input"], content_length)
self._stream = base.LimitedStream(self.environ["wsgi.input"], content_length)
self._read_started = False
self.resolver_match = None

Expand Down Expand Up @@ -109,6 +68,28 @@ def FILES(self):

POST = property(_get_post, _set_post)

def __getstate__(self):
state = self.__dict__.copy()
errb = b""
if self.environ.get("wsgi.errors"):
errb = self.environ["wsgi.errors"].getvalue()
state["environ"]["wsgi.input"] = self._stream.read()
state["environ"]["wsgi.errors"] = errb
del state["META"]
del state["_stream"]
return state

def __setstate__(self, state):
self.__dict__.update(state)
self.environ["wsgi.input"] = BytesIO(self.environ["wsgi.input"])
self.environ["wsgi.errors"] = BytesIO(self.environ["wsgi.errors"])
self.META = self.environ
try:
content_length = int(self.environ.get("CONTENT_LENGTH"))
except (ValueError, TypeError):
content_length = 0
self._stream = base.LimitedStream(self.environ["wsgi.input"], content_length)


class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
Expand Down Expand Up @@ -143,6 +124,10 @@ def __call__(self, environ, start_response):
)
return response

def __setstate__(self, state):
self.__dict__.update(state)
self.load_middleware(is_async=False)


def get_path_info(environ):
"""Return the HTTP request's PATH_INFO as a string."""
Expand Down
2 changes: 1 addition & 1 deletion django/core/servers/basehttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from wsgiref import simple_server

from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import LimitedStream
from django.core.handlers.base import LimitedStream
from django.core.wsgi import get_wsgi_application
from django.db import connections
from django.utils.log import log_message
Expand Down
18 changes: 18 additions & 0 deletions django/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,24 @@ def get_exception_info(self, exception, token):
"end": end,
}

def __getstate__(self):
state = self.__dict__.copy()
state["engine"] = str(self.engine)
state["origin"] = self.origin.name
state["loader"] = str(self.origin.loader)
del state["nodelist"]
return state

def __setstate(self, state):
from .engine import _engine_list

self.__dict__.update(state)
self.engine = _engine_list(using=state["engine"])
self.origin = Origin(
state["origin"], template_name=state["name"], loader=state["loader"]
)
self.nodelist = self.compile_nodelist()


class PartialTemplate:
"""
Expand Down
12 changes: 10 additions & 2 deletions django/test/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

from django.conf import settings
from django.core.handlers.asgi import ASGIRequest
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import LimitedStream, WSGIRequest
from django.core.handlers.base import BaseHandler, LimitedStream
from django.core.handlers.wsgi import WSGIRequest
from django.core.serializers.json import DjangoJSONEncoder
from django.core.signals import got_request_exception, request_finished, request_started
from django.db import close_old_connections
Expand Down Expand Up @@ -209,6 +209,10 @@ def __call__(self, environ):

return response

def __setstate__(self, state):
self.__dict__.update(state)
self.load_middleware()


class AsyncClientHandler(BaseHandler):
"""An async version of ClientHandler."""
Expand Down Expand Up @@ -261,6 +265,10 @@ async def __call__(self, scope):
request_finished.connect(close_old_connections)
return response

def __setstate__(self, state):
self.__dict__.update(state)
self.load_middleware(is_async=True)


def store_rendered_templates(store, signal, sender, template, context, **kwargs):
"""
Expand Down
21 changes: 18 additions & 3 deletions django/urls/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import re
import string
from importlib import import_module
from pickle import PicklingError
from urllib.parse import quote

from asgiref.local import Local
Expand Down Expand Up @@ -101,8 +100,14 @@ def __repr__(self):
)
)

def __reduce_ex__(self, protocol):
raise PicklingError(f"Cannot pickle {self.__class__.__qualname__}.")
def __getstate__(self):
state = self.__dict__.copy()
del state["func"]
return state

def __setstate__(self, state):
self.__dict__.update(state)
self.func = get_callable(state["_func_path"])


def get_resolver(urlconf=None):
Expand Down Expand Up @@ -499,6 +504,16 @@ def lookup_str(self):
return callback.__module__ + "." + callback.__class__.__name__
return callback.__module__ + "." + callback.__qualname__

def __getstate__(self):
state = self.__dict__.copy()
state["lookup_str"] = self.lookup_str
del state["callback"]
return state

def __setstate__(self, state):
self.__dict__.update(state)
self.callback = get_callable(state["lookup_str"])


class URLResolver:
def __init__(
Expand Down
3 changes: 2 additions & 1 deletion tests/requests_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from django.core.exceptions import BadRequest, DisallowedHost
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.files.uploadhandler import MemoryFileUploadHandler
from django.core.handlers.wsgi import LimitedStream, WSGIRequest
from django.core.handlers.base import LimitedStream
from django.core.handlers.wsgi import WSGIRequest
from django.http import (
HttpHeaders,
HttpRequest,
Expand Down
Loading