diff --git a/CHANGES.rst b/CHANGES.rst index 6d9818a5fe1..86f491dec63 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -99,7 +99,7 @@ CHANGES - Don't flap `tcp_cork` in client code, use TCP_NODELAY mode by default. -- +- Implement `web.Request.clone()` #1361 - diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py index fa81bff24e8..d5c6a3756a9 100644 --- a/aiohttp/web_reqrep.py +++ b/aiohttp/web_reqrep.py @@ -52,7 +52,6 @@ class Request(collections.MutableMapping, HeadersMixin): def __init__(self, message, payload, transport, reader, writer, time_service, *, secure_proxy_ssl_header=None): - self._app = None self._message = message self._transport = transport self._reader = reader @@ -74,6 +73,41 @@ def __init__(self, message, payload, transport, reader, writer, self._state = {} self._cache = {} + def clone(self, *, method=sentinel, rel_url=sentinel, + headers=sentinel): + """Clone itself with replacement some attributes. + + Creates and returns a new instance of Request object. If no parameters + are given, an exact copy is returned. If a parameter is not passed, it + will reuse the one from the current request object. + + """ + + if self._read_bytes: + raise RuntimeError("Cannot clone request " + "after reading it's content") + + dct = {} + if method is not sentinel: + dct['method'] = method + if rel_url is not sentinel: + dct['path'] = str(URL(rel_url)) + if headers is not sentinel: + dct['headers'] = CIMultiDict(headers) + dct['raw_headers'] = [(k.encode('utf-8'), v.encode('utf-8')) + for k, v in headers.items()] + + message = self._message._replace(**dct) + + return Request( + message, + self._payload, + self._transport, + self._reader, + self._writer, + self._time_service, + secure_proxy_ssl_header=self._secure_proxy_ssl_header) + # MutableMapping API def __getitem__(self, key): diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 90b1fc1b998..f5283d4e52b 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -277,6 +277,23 @@ like one using :meth:`Request.copy`. *If-Modified-Since* header is absent or is not a valid HTTP date. + .. method:: clone(*, method=..., rel_url=..., headers=...) + + Clone itself with replacement some attributes. + + Creates and returns a new instance of Request object. If no parameters + are given, an exact copy is returned. If a parameter is not passed, it + will reuse the one from the current request object. + + :param str method: http method + + :param rel_url: url to use, :class:`str` or :class:`~yarl.URL` + + :param headers: :class:`~multidict.CIMultidict` or compatible + headers container. + + :return: a cloned :class:`Request` instance. + .. coroutinemethod:: read() Read request body, returns :class:`bytes` object with body content. diff --git a/tests/test_web_request.py b/tests/test_web_request.py index f33bd4626ef..6c405c3bc99 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -7,6 +7,7 @@ from yarl import URL from aiohttp.protocol import HttpVersion +from aiohttp.streams import StreamReader from aiohttp.test_utils import make_mocked_request @@ -266,3 +267,54 @@ def test_rel_url(make_request): def test_url_url(make_request): req = make_request('GET', '/path', headers={'HOST': 'example.com'}) assert URL('http://example.com/path') == req.url + + +def test_clone(): + req = make_mocked_request('GET', '/path') + req2 = req.clone() + assert req2.method == 'GET' + assert req2.rel_url == URL('/path') + + +def test_clone_method(): + req = make_mocked_request('GET', '/path') + req2 = req.clone(method='POST') + assert req2.method == 'POST' + assert req2.rel_url == URL('/path') + + +def test_clone_rel_url(): + req = make_mocked_request('GET', '/path') + req2 = req.clone(rel_url=URL('/path2')) + assert req2.rel_url == URL('/path2') + + +def test_clone_rel_url_str(): + req = make_mocked_request('GET', '/path') + req2 = req.clone(rel_url='/path2') + assert req2.rel_url == URL('/path2') + + +def test_clone_headers(): + req = make_mocked_request('GET', '/path', headers={'A': 'B'}) + req2 = req.clone(headers=CIMultiDict({'B': 'C'})) + assert req2.headers == CIMultiDict({'B': 'C'}) + assert req2.raw_headers == ((b'B', b'C'),) + + +def test_clone_headers_dict(): + req = make_mocked_request('GET', '/path', headers={'A': 'B'}) + req2 = req.clone(headers={'B': 'C'}) + assert req2.headers == CIMultiDict({'B': 'C'}) + assert req2.raw_headers == ((b'B', b'C'),) + + +@asyncio.coroutine +def test_cannot_clone_after_read(loop): + payload = StreamReader(loop=loop) + payload.feed_data(b'data') + payload.feed_eof() + req = make_mocked_request('GET', '/path', payload=payload) + yield from req.read() + with pytest.raises(RuntimeError): + req.clone()