From 22c0fedf58a2ddb3591cbd15c6590940bc79b3b7 Mon Sep 17 00:00:00 2001 From: David Poirier Date: Tue, 8 Aug 2017 04:29:46 +1000 Subject: [PATCH] Deprecated BaseRequest.has_body, replaced with 2 new attributes (#2005) (#2169) * Deprecated BaseRequest.has_body, replaced with 2 new attributes * Test obsolete BaseRequest.has_body attr --- aiohttp/web_request.py | 16 +++++- changes/2005.feature | 3 + docs/web_reference.rst | 24 +++++++- tests/test_web_functional.py | 4 ++ tools/bench-asyncio-write.py | 108 +++++++++++++++++++++++++++++++++++ 5 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 changes/2005.feature create mode 100644 tools/bench-asyncio-write.py diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index d59c3a61c5a..9a6d9ebf0f0 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -16,6 +16,7 @@ from . import hdrs, multipart from .helpers import HeadersMixin, SimpleCookie, reify, sentinel +from .streams import EmptyStreamReader from .web_exceptions import HTTPRequestEntityTooLarge @@ -457,9 +458,22 @@ def content(self): @property def has_body(self): - """Return True if request has HTTP BODY, False otherwise.""" + """Return True if request's HTTP BODY can be read, False otherwise.""" + warnings.warn( + "Deprecated, use .can_read_body #2005", + DeprecationWarning, stacklevel=2) + return not self._payload.at_eof() + + @property + def can_read_body(self): + """Return True if request's HTTP BODY can be read, False otherwise.""" return not self._payload.at_eof() + @property + def body_exists(self): + """Return True if request has HTTP BODY, False otherwise.""" + return type(self._payload) is not EmptyStreamReader + @asyncio.coroutine def release(self): """Release request. diff --git a/changes/2005.feature b/changes/2005.feature new file mode 100644 index 00000000000..77946cf4db6 --- /dev/null +++ b/changes/2005.feature @@ -0,0 +1,3 @@ +- Deprecated BaseRequest.has_body in favour of BaseRequest.can_read_body +- Added BaseRequest.body_exists attribute that stays static for the lifetime +of the request diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 6fb18997e29..a98929b7199 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -230,15 +230,33 @@ and :ref:`aiohttp-web-signals` handlers. Read-only property. - .. attribute:: has_body + .. attribute:: body_exists Return ``True`` if request has *HTTP BODY*, ``False`` otherwise. Read-only :class:`bool` property. - .. versionadded:: 0.16 + .. versionadded:: 2.3 + + .. attribute:: can_read_body - .. attribute:: content_type + Return ``True`` if request's *HTTP BODY* can be read, ``False`` otherwise. + + Read-only :class:`bool` property. + + .. versionadded:: 2.3 + + .. attribute:: has_body + + Return ``True`` if request's *HTTP BODY* can be read, ``False`` otherwise. + + Read-only :class:`bool` property. + + .. deprecated:: 2.3 + + Use :meth:`can_read_body` instead. + + .. attribute:: content_type Read-only property with *content* part of *Content-Type* header. diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 3f673c2ce9b..9bb26589119 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -694,6 +694,8 @@ def test_empty_content_for_query_without_body(loop, test_client): @asyncio.coroutine def handler(request): + assert not request.body_exists + assert not request.can_read_body assert not request.has_body return web.Response() @@ -710,6 +712,8 @@ def test_empty_content_for_query_with_body(loop, test_client): @asyncio.coroutine def handler(request): + assert request.body_exists + assert request.can_read_body assert request.has_body body = yield from request.read() return web.Response(body=body) diff --git a/tools/bench-asyncio-write.py b/tools/bench-asyncio-write.py new file mode 100644 index 00000000000..fd169df45dc --- /dev/null +++ b/tools/bench-asyncio-write.py @@ -0,0 +1,108 @@ +import asyncio +import atexit +import csv +import math +import os +import signal + +PORT = 8888 + +server = os.fork() +if server == 0: + loop = asyncio.get_event_loop() + coro = asyncio.start_server(lambda *_: None, port=PORT) + loop.run_until_complete(coro) + loop.run_forever() +else: + atexit.register(os.kill, server, signal.SIGTERM) + + +async def write_joined_bytearray(writer, chunks): + body = bytearray(chunks[0]) + for c in chunks[1:]: + body += c + writer.write(body) + +async def write_joined_list(writer, chunks): + body = b''.join(chunks) + writer.write(body) + +async def write_separately(writer, chunks): + for c in chunks: + writer.write(c) + + +def fm_size(s, _fms=('', 'K', 'M', 'G')): + i = 0 + while s > 1024: + s /= 1024 + i += 1 + return '{:.1f}{}B'.format(s, _fms[i]) + +writes = { + 'Join the chunks in a bytearray then write': write_joined_bytearray, + 'Join the chunks with a list then write': write_joined_list, + 'Write the chunks separately': write_separately, +} + +bodies = ( + [], + [2 ** 7], + [2 ** 17], + [2 ** 27], + [2 ** 30], + [1000 * 2 ** 0 for _ in range(1)], + [ 100 * 2 ** 0 for _ in range(10)], + [ 10 * 2 ** 0 for _ in range(100)], + [ 2 ** 0 for _ in range(1000)], + [1000 * 2 ** 10 for _ in range(1)], + [ 100 * 2 ** 10 for _ in range(10)], + [ 10 * 2 ** 10 for _ in range(100)], + [ 2 ** 10 for _ in range(1000)], + [1000 * 2 ** 20 for _ in range(1)], + [ 100 * 2 ** 20 for _ in range(10)], + [ 10 * 2 ** 20 for _ in range(100)], + [ 2 ** 20 for _ in range(1000)], +) + +jobs = [( + # always start with a 256B headers chunk + '{} in {} chunks'.format(fm_size(sum(j) if j else 0), len(j)), + [b'0' * s for s in [256] + list(j)], +) for j in bodies] + +async def time(loop, fn, *args): + spent = [] + while len(spent) < 10000 and (not spent or sum(spent) < .2): + s = loop.time() + await fn(*args) + e = loop.time() + spent.append(e - s) + mean = sum(spent) / len(spent) + sd = sum((x - mean) ** 2 for x in spent) / len(spent) + return len(spent), mean, math.sqrt(sd) + +async def main(loop): + _, writer = await asyncio.open_connection(port=PORT) + res = [] + for t, c in jobs: + print(t) + for k, v in writes.items(): + print('{:<42}: '.format(k), end='') + coro = time(loop, v, writer, c) + try: + it, mean, sd = await asyncio.wait_for(coro, 2) + except asyncio.TimeoutError: + print('timed out') + else: + print('{:.6f}ms per loop (stdev: {:.3f}ms, {} iterations)'.format(mean * 1000, sd * 1000, it)) + res.append([t, k, mean, sd, it]) + print('--') + with open('bench.csv', 'w', newline='') as f: + w = csv.writer(f) + w.writerow(['Job', 'Writer', 'Mean', 'St dev', 'Iterations']) + for r in res: + w.writerow(r) + +loop = asyncio.get_event_loop() +loop.run_until_complete(main(loop))