diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 04354988efd..e6d6b6ca1ca 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -69,33 +69,51 @@ DEFAULT_CHUNK_SIZE = 2**18 # 256 KiB COOKIE_MAX_LENGTH = 4096 _QUOTED_PAIR_SUB = re.compile(r"\\(.)") -_QUOTED_STRING = r'"(?:[^"\\]|\\.)*"' -_ESCAPED_COMMENT = r"(?:[^()\\]|\\.)*" -# Matches one element in a comma-separated header list. -# Group 1: content of a top-level quoted-string (quotes stripped). -# Group 2: an unquoted element (may contain parameter quoted-strings / comments). -_LIST_ELEMENT_RE = re.compile( - rf""" +if sys.version_info >= (3, 11): + _QUOTED_STRING_CONTENT = r'(?:[^"\\]++|\\.)*+' + _ESCAPED_COMMENT = r"(?:[^()\\]++|\\.)*+" + _LIST_ELEMENT = rf""" [ \t]* (?: - "( (?:[^"\\]|\\.)* )" # group 1: top-level quoted-string + "( {_QUOTED_STRING_CONTENT} )" # group 1: top-level quoted-string [ \t]* (?:,|\Z) | ( # group 2: unquoted element (?: - (?<=[^\s]=) {_QUOTED_STRING} # parameter quoted value + (?<=[^\s]=) "{_QUOTED_STRING_CONTENT}" # parameter quoted value + | (?<=\s) \( {_ESCAPED_COMMENT} \) # comment + | [^,"(\\]++ # run of ordinary characters + | [^,] # quote, paren or backslash the branches above rejected + )++ + ) + (?:,|\Z) + ) + """ +else: + _QUOTED_STRING_CONTENT = r'(?:[^"\\]|\\.)*' + _ESCAPED_COMMENT = r"(?:[^()\\]|\\.)*" + _LIST_ELEMENT = rf""" + [ \t]* + (?: + "( {_QUOTED_STRING_CONTENT} )" # group 1: top-level quoted-string + [ \t]* (?:,|\Z) + | ( # group 2: unquoted element + (?: + (?<=[^\s]=) "{_QUOTED_STRING_CONTENT}" # parameter quoted value | (?<=\s) \( {_ESCAPED_COMMENT} \) # comment | [^,] # any non-comma character )+? ) (?:,|\Z) ) - """, - re.VERBOSE, -) + """ +# Matches one element in a comma-separated header list. +# Group 1: content of a top-level quoted-string (quotes stripped). +# Group 2: an unquoted element (may contain parameter quoted-strings / comments). +_LIST_ELEMENT_RE = re.compile(_LIST_ELEMENT, re.VERBOSE) # Finds parameter quoted-strings and comments inside an unquoted element for unescaping. _PROTECTED_RE = re.compile( rf""" - (?<=[^\s]=) {_QUOTED_STRING} # parameter quoted-string + (?<=[^\s]=) "{_QUOTED_STRING_CONTENT}" # parameter quoted-string | (?<=\s) \( {_ESCAPED_COMMENT} \) # comment """, re.VERBOSE, diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 4e243c86ed1..05eb111844f 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -gunicorn==26.1.0 +gunicorn==26.2.0 # via -r requirements/base-ft.in idna==3.19 # via yarl diff --git a/requirements/base.txt b/requirements/base.txt index 14b31ce18ad..fe468e38bc3 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -gunicorn==26.1.0 +gunicorn==26.2.0 # via -r requirements/base.in idna==3.19 # via yarl diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 24f86982945..d228c63b9af 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -100,7 +100,7 @@ frozenlist==1.8.0 # -r requirements/runtime-deps.in # aiohttp # aiosignal -gunicorn==26.1.0 +gunicorn==26.2.0 # via -r requirements/base.in identify==2.6.19 # via pre-commit @@ -109,7 +109,7 @@ idna==3.19 # requests # trustme # yarl -imagesize==2.0.0 +imagesize==2.0.1 # via sphinx iniconfig==2.3.0 # via pytest @@ -166,7 +166,7 @@ pip-tools==7.6.1 # via -r requirements/dev.in pkgconfig==1.6.0 # via -r requirements/test-common-base.in -platformdirs==4.11.3 +platformdirs==4.11.4 # via virtualenv pluggy==1.6.0 # via @@ -235,7 +235,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.5.2 +python-discovery==1.5.3 # via virtualenv python-on-whales==0.81.0 # via @@ -331,7 +331,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.7.4 +virtualenv==21.7.5 # via pre-commit wheel==0.48.0 # via pip-tools diff --git a/requirements/dev.txt b/requirements/dev.txt index 40f1fb2ca1a..e33a94873dd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -98,7 +98,7 @@ frozenlist==1.8.0 # -r requirements/runtime-deps.in # aiohttp # aiosignal -gunicorn==26.1.0 +gunicorn==26.2.0 # via -r requirements/base.in identify==2.6.19 # via pre-commit @@ -107,7 +107,7 @@ idna==3.19 # requests # trustme # yarl -imagesize==2.0.0 +imagesize==2.0.1 # via sphinx iniconfig==2.3.0 # via pytest @@ -163,7 +163,7 @@ pip-tools==7.6.1 # via -r requirements/dev.in pkgconfig==1.6.0 # via -r requirements/test-common-base.in -platformdirs==4.11.3 +platformdirs==4.11.4 # via virtualenv pluggy==1.6.0 # via @@ -230,7 +230,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.5.2 +python-discovery==1.5.3 # via virtualenv python-on-whales==0.81.0 # via @@ -321,7 +321,7 @@ uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpytho # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.7.4 +virtualenv==21.7.5 # via pre-commit wheel==0.48.0 # via pip-tools diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index b76adf01d3a..b62377d7e16 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -22,7 +22,7 @@ docutils==0.21.2 # sphinx idna==3.19 # via requests -imagesize==2.0.0 +imagesize==2.0.1 # via sphinx jinja2==3.1.6 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index a82e5f3c76b..5fe2d0edafc 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -22,7 +22,7 @@ docutils==0.21.2 # sphinx idna==3.19 # via requests -imagesize==2.0.0 +imagesize==2.0.1 # via sphinx jinja2==3.1.6 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 9ec4e344732..041f3c5094c 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -84,7 +84,7 @@ packaging==26.3 # via pytest pathspec==1.1.1 # via mypy -platformdirs==4.11.3 +platformdirs==4.11.4 # via virtualenv pluggy==1.6.0 # via pytest @@ -125,7 +125,7 @@ pytest-mock==3.15.1 # via -r requirements/lint.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.5.2 +python-discovery==1.5.3 # via virtualenv python-on-whales==0.81.0 # via -r requirements/lint.in @@ -164,7 +164,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.7.4 +virtualenv==21.7.5 # via pre-commit yarl==1.24.5 # via aiohttp diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 071ca517aa8..304735580de 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -59,7 +59,7 @@ frozenlist==1.8.0 # -r requirements/runtime-deps.in # aiohttp # aiosignal -gunicorn==26.1.0 +gunicorn==26.2.0 # via -r requirements/base-ft.in idna==3.19 # via diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 5116d51f110..1cb597f14b4 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -47,7 +47,7 @@ frozenlist==1.8.0 # -r requirements/runtime-deps.in # aiohttp # aiosignal -gunicorn==26.1.0 +gunicorn==26.2.0 # via -r requirements/base-ft.in idna==3.19 # via yarl diff --git a/requirements/test.txt b/requirements/test.txt index ff2303ad5eb..934575f3dd6 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -59,7 +59,7 @@ frozenlist==1.8.0 # -r requirements/runtime-deps.in # aiohttp # aiosignal -gunicorn==26.1.0 +gunicorn==26.2.0 # via -r requirements/base.in idna==3.19 # via diff --git a/tests/test_benchmarks_multipart.py b/tests/test_benchmarks_multipart.py index 6c82aacbcb1..c6c4b24ae3c 100644 --- a/tests/test_benchmarks_multipart.py +++ b/tests/test_benchmarks_multipart.py @@ -48,17 +48,26 @@ def _part(body: bytes, loop: asyncio.AbstractEventLoop) -> BodyPartReader: def test_read_base64_part( event_loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture ) -> None: - """Read a line-wrapped base64 part to completion. + """Read a line-wrapped base64 part to completion chunk by chunk. Every 8 KiB chunk lands mid-quartet, so this covers the common cost of the base64 realignment in ``read_chunk`` on well-formed input. + + The chunks are deliberately dropped rather than accumulated: growing one + large buffer inside the measured region made the measurement depend on + whether realloc could extend it in place -- allocator luck, which showed + up as double-digit swings between unrelated CodSpeed runs. """ body = base64.encodebytes(b"x" * (256 * 1024)).replace(b"\n", b"\r\n") body += b"\r\n--:--" + async def read_all_chunks(part: BodyPartReader) -> None: + while not part.at_eof(): + await part.read_chunk(BodyPartReader.chunk_size) + @benchmark def _run() -> None: - event_loop.run_until_complete(_part(body, event_loop).read()) + event_loop.run_until_complete(read_all_chunks(_part(body, event_loop))) def test_read_chunk_base64_realignment(