From 42ea93dc7a2ccc51a4571acdea43a703ad895128 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Thu, 17 Jul 2025 21:58:56 -0500 Subject: [PATCH 01/18] Transform with the new Multidict-CAPI --- aiohttp/_http_parser.pyx | 67 ++++++++++++++++++++++++++-------------- aiohttp/_http_writer.pyx | 42 ++++++++++++++++++------- 2 files changed, 74 insertions(+), 35 deletions(-) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index f5015b297b0..04ace8df33e 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -1,4 +1,4 @@ -#cython: language_level=3 +#cython: language_level=3, freethreading_compatible = True # # Based on https://github.com/MagicStack/httptools # @@ -12,10 +12,20 @@ from cpython cimport ( PyObject_GetBuffer, ) from cpython.mem cimport PyMem_Free, PyMem_Malloc +from cpython.object cimport PyObject from libc.limits cimport ULLONG_MAX from libc.string cimport memcpy -from multidict import CIMultiDict as _CIMultiDict, CIMultiDictProxy as _CIMultiDictProxy +from multidict cimport ( + CIMultiDict, + CIMultiDictProxy, + CIMultiDictProxy_New, + CIMultiDict_Contains, + multidict_import, + CIMultiDict_New, + CIMultiDict_Add, + CIMultiDictProxy_GetOne +) from yarl import URL as _URL from aiohttp import hdrs @@ -47,6 +57,8 @@ from aiohttp cimport _cparser as cparser include "_headers.pxi" from aiohttp cimport _find_header +# import multidict capsule... +multidict_import() ALLOWED_UPGRADES = frozenset({"websocket"}) DEF DEFAULT_FREELIST_SIZE = 250 @@ -61,8 +73,6 @@ __all__ = ('HttpRequestParser', 'HttpResponseParser', cdef object URL = _URL cdef object URL_build = URL.build -cdef object CIMultiDict = _CIMultiDict -cdef object CIMultiDictProxy = _CIMultiDictProxy cdef object HttpVersion = _HttpVersion cdef object HttpVersion10 = _HttpVersion10 cdef object HttpVersion11 = _HttpVersion11 @@ -113,8 +123,8 @@ cdef class RawRequestMessage: cdef readonly str method cdef readonly str path cdef readonly object version # HttpVersion - cdef readonly object headers # CIMultiDict - cdef readonly object raw_headers # tuple + cdef readonly CIMultiDictProxy headers # CIMultiDictProxy + cdef readonly CIMultiDict raw_headers # CIMultiDict cdef readonly object should_close cdef readonly object compression cdef readonly object upgrade @@ -186,8 +196,8 @@ cdef class RawRequestMessage: cdef _new_request_message(str method, str path, object version, - object headers, - object raw_headers, + CIMultiDictProxy headers, + CIMultiDict raw_headers, bint should_close, object compression, bint upgrade, @@ -213,8 +223,9 @@ cdef class RawResponseMessage: cdef readonly object version # HttpVersion cdef readonly int code cdef readonly str reason - cdef readonly object headers # CIMultiDict - cdef readonly object raw_headers # tuple + # Temporary Note: We can now expose the real types thanks to the new cython-api + cdef readonly CIMultiDictProxy headers + cdef readonly CIMultiDict raw_headers cdef readonly object should_close cdef readonly object compression cdef readonly object upgrade @@ -250,8 +261,8 @@ cdef class RawResponseMessage: cdef _new_response_message(object version, int code, str reason, - object headers, - object raw_headers, + CIMultiDictProxy headers, + CIMultiDict raw_headers, bint should_close, object compression, bint upgrade, @@ -297,8 +308,8 @@ cdef class HttpParser: bytearray _buf str _path str _reason - list _headers - list _raw_headers + CIMultiDict _headers + CIMultiDict _raw_headers bint _upgraded list _messages object _payload @@ -384,13 +395,13 @@ cdef class HttpParser: name = find_header(self._raw_name) value = self._raw_value.decode('utf-8', 'surrogateescape') - self._headers.append((name, value)) + CIMultiDict_Add(self._headers, name, value) if name is CONTENT_ENCODING: self._content_encoding = value self._has_value = False - self._raw_headers.append((self._raw_name, self._raw_value)) + CIMultiDict_Add(self._raw_headers, self._raw_name, self._raw_value) self._raw_name = EMPTY_BYTES self._raw_value = EMPTY_BYTES @@ -411,25 +422,34 @@ cdef class HttpParser: self._has_value = True cdef _on_headers_complete(self): + cdef CIMultiDictProxy headers + cdef CIMultiDict raw_headers + cdef PyObject* upgrade_value + cdef bint allowed = 0 + self._process_header() should_close = not cparser.llhttp_should_keep_alive(self._cparser) upgrade = self._cparser.upgrade chunked = self._cparser.flags & cparser.F_CHUNKED - raw_headers = tuple(self._raw_headers) - headers = CIMultiDictProxy(CIMultiDict(self._headers)) + raw_headers = self._raw_headers + headers = CIMultiDictProxy_New(self._headers) if self._cparser.type == cparser.HTTP_REQUEST: - allowed = upgrade and headers.get("upgrade", "").lower() in ALLOWED_UPGRADES - if allowed or self._cparser.method == cparser.HTTP_CONNECT: + if upgrade and CIMultiDictProxy_GetOne(headers, "upgrade", &upgrade_value): + self._upgraded = ( + (upgrade_value).lower() in ALLOWED_UPGRADES + or self._cparser.method == cparser.HTTP_CONNECT + ) + elif self._cparser.method == cparser.HTTP_CONNECT: self._upgraded = True else: if upgrade and self._cparser.status_code == 101: self._upgraded = True # do not support old websocket spec - if SEC_WEBSOCKET_KEY1 in headers: + if CIMultiDict_Contains(headers, SEC_WEBSOCKET_KEY1): raise InvalidHeader(SEC_WEBSOCKET_KEY1) encoding = None @@ -666,8 +686,9 @@ cdef int cb_on_message_begin(cparser.llhttp_t* parser) except -1: cdef HttpParser pyparser = parser.data pyparser._started = True - pyparser._headers = [] - pyparser._raw_headers = [] + # Not Certain yet if 5 is a good solid number of headers to preallocate yet... + pyparser._headers = CIMultiDict_New(5) + pyparser._raw_headers = CIMultiDict_New(5) PyByteArray_Resize(pyparser._buf, 0) pyparser._path = None pyparser._reason = None diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx index 4a3ae1f9e68..806d3ef1456 100644 --- a/aiohttp/_http_writer.pyx +++ b/aiohttp/_http_writer.pyx @@ -1,17 +1,29 @@ +# cython: freethreading_compatible = True + from cpython.bytes cimport PyBytes_FromStringAndSize from cpython.exc cimport PyErr_NoMemory from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc -from cpython.object cimport PyObject_Str +from cpython.object cimport PyObject_Str, PyObject from libc.stdint cimport uint8_t, uint64_t from libc.string cimport memcpy +from cpython.unicode cimport PyUnicode_CheckExact + +# NOTE: Cython API is Experimental and is held subject to change +# Depending on different circumstances. +# Remove this comment when draft is officially over +# or when 6.7 is released with the offical names. +# This may or may not be what the other authors had in mind. +# My todos are held subject to removal when Draft is transformed +# into a real pull request. + +from multidict cimport IStr_CheckExact, MultiDictIter_New, MultiDictIter_Next, multidict_import -from multidict import istr +# Run first thing so that Capsule imports... +multidict_import() DEF BUF_SIZE = 16 * 1024 # 16KiB cdef char BUFFER[BUF_SIZE] -cdef object _istr = istr - # ----------------- writer --------------------------- @@ -100,9 +112,9 @@ cdef inline int _write_str(Writer* writer, str s): cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s): cdef Py_UCS4 ch cdef str out_str - if type(s) is str: + if PyUnicode_CheckExact(s): out_str = s - elif type(s) is _istr: + elif IStr_CheckExact(s): out_str = PyObject_Str(s) elif not isinstance(s, str): raise TypeError("Cannot serialize non-str key {!r}".format(s)) @@ -121,29 +133,35 @@ cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s): # --------------- _serialize_headers ---------------------- +# TODO: Change headers parameter into CIMultiDict or MultiDict or fused +# cython type or am I insane for wanting to do so? + def _serialize_headers(str status_line, headers): cdef Writer writer - cdef object key - cdef object val - + cdef PyObject* key + cdef PyObject* val + cdef object multidict_iter _init_writer(&writer) try: + multidict_iter = MultiDictIter_New(headers) + if _write_str(&writer, status_line) < 0: raise if _write_byte(&writer, b'\r') < 0: raise if _write_byte(&writer, b'\n') < 0: raise + + while MultiDictIter_Next(multidict_iter, &key, &val): - for key, val in headers.items(): - if _write_str_raise_on_nlcr(&writer, key) < 0: + if _write_str_raise_on_nlcr(&writer, key) < 0: raise if _write_byte(&writer, b':') < 0: raise if _write_byte(&writer, b' ') < 0: raise - if _write_str_raise_on_nlcr(&writer, val) < 0: + if _write_str_raise_on_nlcr(&writer, val) < 0: raise if _write_byte(&writer, b'\r') < 0: raise From cbc5aa9cb8408cc9de8d5598646d5261bb07f56f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 03:33:56 +0000 Subject: [PATCH 02/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- aiohttp/_http_parser.pyx | 17 +++++++++-------- aiohttp/_http_writer.pyx | 27 ++++++++++++++++----------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 04ace8df33e..4908fa88d70 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -15,17 +15,17 @@ from cpython.mem cimport PyMem_Free, PyMem_Malloc from cpython.object cimport PyObject from libc.limits cimport ULLONG_MAX from libc.string cimport memcpy - from multidict cimport ( - CIMultiDict, + CIMultiDict, + CIMultiDict_Add, + CIMultiDict_Contains, + CIMultiDict_New, CIMultiDictProxy, + CIMultiDictProxy_GetOne, CIMultiDictProxy_New, - CIMultiDict_Contains, multidict_import, - CIMultiDict_New, - CIMultiDict_Add, - CIMultiDictProxy_GetOne ) + from yarl import URL as _URL from aiohttp import hdrs @@ -57,6 +57,7 @@ from aiohttp cimport _cparser as cparser include "_headers.pxi" from aiohttp cimport _find_header + # import multidict capsule... multidict_import() @@ -224,7 +225,7 @@ cdef class RawResponseMessage: cdef readonly int code cdef readonly str reason # Temporary Note: We can now expose the real types thanks to the new cython-api - cdef readonly CIMultiDictProxy headers + cdef readonly CIMultiDictProxy headers cdef readonly CIMultiDict raw_headers cdef readonly object should_close cdef readonly object compression @@ -439,7 +440,7 @@ cdef class HttpParser: if self._cparser.type == cparser.HTTP_REQUEST: if upgrade and CIMultiDictProxy_GetOne(headers, "upgrade", &upgrade_value): self._upgraded = ( - (upgrade_value).lower() in ALLOWED_UPGRADES + (upgrade_value).lower() in ALLOWED_UPGRADES or self._cparser.method == cparser.HTTP_CONNECT ) elif self._cparser.method == cparser.HTTP_CONNECT: diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx index 806d3ef1456..06bf831e2db 100644 --- a/aiohttp/_http_writer.pyx +++ b/aiohttp/_http_writer.pyx @@ -3,20 +3,25 @@ from cpython.bytes cimport PyBytes_FromStringAndSize from cpython.exc cimport PyErr_NoMemory from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc -from cpython.object cimport PyObject_Str, PyObject +from cpython.object cimport PyObject, PyObject_Str +from cpython.unicode cimport PyUnicode_CheckExact from libc.stdint cimport uint8_t, uint64_t from libc.string cimport memcpy -from cpython.unicode cimport PyUnicode_CheckExact - -# NOTE: Cython API is Experimental and is held subject to change -# Depending on different circumstances. -# Remove this comment when draft is officially over +from multidict cimport ( + IStr_CheckExact, + MultiDictIter_New, + MultiDictIter_Next, + multidict_import, +) + +# NOTE: Cython API is Experimental and is held subject to change +# Depending on different circumstances. +# Remove this comment when draft is officially over # or when 6.7 is released with the offical names. # This may or may not be what the other authors had in mind. -# My todos are held subject to removal when Draft is transformed +# My todos are held subject to removal when Draft is transformed # into a real pull request. -from multidict cimport IStr_CheckExact, MultiDictIter_New, MultiDictIter_Next, multidict_import # Run first thing so that Capsule imports... multidict_import() @@ -133,7 +138,7 @@ cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s): # --------------- _serialize_headers ---------------------- -# TODO: Change headers parameter into CIMultiDict or MultiDict or fused +# TODO: Change headers parameter into CIMultiDict or MultiDict or fused # cython type or am I insane for wanting to do so? def _serialize_headers(str status_line, headers): @@ -145,14 +150,14 @@ def _serialize_headers(str status_line, headers): try: multidict_iter = MultiDictIter_New(headers) - + if _write_str(&writer, status_line) < 0: raise if _write_byte(&writer, b'\r') < 0: raise if _write_byte(&writer, b'\n') < 0: raise - + while MultiDictIter_Next(multidict_iter, &key, &val): if _write_str_raise_on_nlcr(&writer, key) < 0: From d3fabd9cd9530653193e03f967a0e5fe0a54048f Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 14:55:11 -0500 Subject: [PATCH 03/18] simulate installation of capi --- CHANGES/11320.feature.rst | 1 + aiohttp/_http_parser.pyx | 21 +++++++++++---------- requirements/multidict.txt | 3 ++- setup.cfg | 6 +++++- setup.py | 3 ++- 5 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 CHANGES/11320.feature.rst diff --git a/CHANGES/11320.feature.rst b/CHANGES/11320.feature.rst new file mode 100644 index 00000000000..193c86aa14d --- /dev/null +++ b/CHANGES/11320.feature.rst @@ -0,0 +1 @@ +Introduced Multidict C-API to cython http parser and http writer -- by :user:`Vizonex`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 04ace8df33e..391e36691f3 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -15,17 +15,17 @@ from cpython.mem cimport PyMem_Free, PyMem_Malloc from cpython.object cimport PyObject from libc.limits cimport ULLONG_MAX from libc.string cimport memcpy - from multidict cimport ( - CIMultiDict, + CIMultiDict, + CIMultiDict_Add, + CIMultiDict_Contains, + CIMultiDict_New, CIMultiDictProxy, + CIMultiDictProxy_GetOne, CIMultiDictProxy_New, - CIMultiDict_Contains, multidict_import, - CIMultiDict_New, - CIMultiDict_Add, - CIMultiDictProxy_GetOne ) + from yarl import URL as _URL from aiohttp import hdrs @@ -57,6 +57,7 @@ from aiohttp cimport _cparser as cparser include "_headers.pxi" from aiohttp cimport _find_header + # import multidict capsule... multidict_import() @@ -224,7 +225,7 @@ cdef class RawResponseMessage: cdef readonly int code cdef readonly str reason # Temporary Note: We can now expose the real types thanks to the new cython-api - cdef readonly CIMultiDictProxy headers + cdef readonly CIMultiDictProxy headers cdef readonly CIMultiDict raw_headers cdef readonly object should_close cdef readonly object compression @@ -439,7 +440,7 @@ cdef class HttpParser: if self._cparser.type == cparser.HTTP_REQUEST: if upgrade and CIMultiDictProxy_GetOne(headers, "upgrade", &upgrade_value): self._upgraded = ( - (upgrade_value).lower() in ALLOWED_UPGRADES + (upgrade_value).lower() in ALLOWED_UPGRADES or self._cparser.method == cparser.HTTP_CONNECT ) elif self._cparser.method == cparser.HTTP_CONNECT: @@ -616,7 +617,7 @@ cdef class HttpRequestParser(HttpParser): if self._cparser.method == cparser.HTTP_CONNECT: # authority-form, # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 - self._url = URL.build(authority=self._path, encoded=True) + self._url = URL_build(authority=self._path, encoded=True) elif idx3 > 1 and self._path[0] == '/': # origin-form, # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 @@ -642,7 +643,7 @@ cdef class HttpRequestParser(HttpParser): query = self._path[idx1: idx2] fragment = self._path[idx2+1:] - self._url = URL.build( + self._url = URL_build( path=path, query_string=query, fragment=fragment, diff --git a/requirements/multidict.txt b/requirements/multidict.txt index 6f90d5c4c34..a15c877a383 100644 --- a/requirements/multidict.txt +++ b/requirements/multidict.txt @@ -4,7 +4,8 @@ # # pip-compile --allow-unsafe --output-file=requirements/multidict.txt --resolver=backtracking --strip-extras requirements/multidict.in # -multidict==6.6.3 +# Temporarily redirect it to me, will revert when 6.7 gets released +multidict=='git+https://github.com/Vizonex/multidict.git@capi' # via -r requirements/multidict.in typing-extensions==4.14.1 # via multidict diff --git a/setup.cfg b/setup.cfg index cf8326f1a4c..67017203baa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,7 +53,11 @@ install_requires = aiosignal >= 1.4.0 async-timeout >= 4.0, < 6.0 ; python_version < "3.11" frozenlist >= 1.1.1 - multidict >=4.5, < 7.0 + # To test for right now, lets use the capi repo to ensure compiling works correctly before moving + # to 6.7 when it comes out... + multidict = "git+https://github.com/Vizonex/multidict.git@capi" + + # multidict >=4.5, < 7.0 propcache >= 0.2.0 yarl >= 1.17.0, < 2.0 diff --git a/setup.py b/setup.py index fded89876f2..24d67445efe 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ import pathlib import sys +import multidict from setuptools import Extension, setup if sys.version_info < (3, 9): @@ -51,7 +52,7 @@ ] llhttp_kwargs = { "define_macros": [("LLHTTP_STRICT_MODE", 0)], - "include_dirs": ["vendor/llhttp/build"], + "include_dirs": ["vendor/llhttp/build", multidict.__path__], } extensions = [ From 5874434500190dfe63e85ce1b4eb4ecd9db0567d Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 14:57:47 -0500 Subject: [PATCH 04/18] it should install based on what I've hacked into requirements.txt for right now --- setup.cfg | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 67017203baa..cf8326f1a4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,11 +53,7 @@ install_requires = aiosignal >= 1.4.0 async-timeout >= 4.0, < 6.0 ; python_version < "3.11" frozenlist >= 1.1.1 - # To test for right now, lets use the capi repo to ensure compiling works correctly before moving - # to 6.7 when it comes out... - multidict = "git+https://github.com/Vizonex/multidict.git@capi" - - # multidict >=4.5, < 7.0 + multidict >=4.5, < 7.0 propcache >= 0.2.0 yarl >= 1.17.0, < 2.0 From 3039928a7696fcdd04c098073765ab2a57beb4cb Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 16:22:17 -0500 Subject: [PATCH 05/18] redirect multidict library to my capi fork while I wait on multidict-6.7 to release --- pyproject.toml | 2 ++ requirements/multidict.txt | 2 +- requirements/test.txt | 3 ++- setup.py | 8 ++++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a9b4200a06c..ea25255ad66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,8 @@ requires = [ "pkgconfig", "setuptools >= 46.4.0", + # Temporarily redirect it to me, will revert when 6.7 gets released + "multidict @ git+https://github.com/Vizonex/multidict@capi" ] build-backend = "setuptools.build_meta" diff --git a/requirements/multidict.txt b/requirements/multidict.txt index a15c877a383..f186799b958 100644 --- a/requirements/multidict.txt +++ b/requirements/multidict.txt @@ -5,7 +5,7 @@ # pip-compile --allow-unsafe --output-file=requirements/multidict.txt --resolver=backtracking --strip-extras requirements/multidict.in # # Temporarily redirect it to me, will revert when 6.7 gets released -multidict=='git+https://github.com/Vizonex/multidict.git@capi' +multidict @ git+https://github.com/Vizonex/multidict@capi # via -r requirements/multidict.in typing-extensions==4.14.1 # via multidict diff --git a/requirements/test.txt b/requirements/test.txt index 8cc46c25d24..019334b5d52 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -57,7 +57,8 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.6.3 +# Temporarily redirect it to me, I will revert when 6.7 gets released +multidict @ git+https://github.com/Vizonex/multidict@capi # via # -r /home/dependabot/dependabot-updater/tmp/20250715-1384-v0sr9j/dependabot_20250715-1384-mw23m4/requirements/runtime-deps.in # yarl diff --git a/setup.py b/setup.py index 24d67445efe..c401327e5a8 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ ] llhttp_kwargs = { "define_macros": [("LLHTTP_STRICT_MODE", 0)], - "include_dirs": ["vendor/llhttp/build", multidict.__path__], + "include_dirs": ["vendor/llhttp/build"] + multidict.__path__, } extensions = [ @@ -66,7 +66,11 @@ ], **llhttp_kwargs, ), - Extension("aiohttp._http_writer", ["aiohttp/_http_writer.c"]), + Extension( + "aiohttp._http_writer", + ["aiohttp/_http_writer.c"], + include_dirs=multidict.__path__, + ), Extension("aiohttp._websocket.reader_c", ["aiohttp/_websocket/reader_c.c"]), ] From 4002b65b4709d3f88845052dcf7ad913cca1dd0a Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 16:24:43 -0500 Subject: [PATCH 06/18] lets try and get multidict and the cython files to compile --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea25255ad66..9754ec4288d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "pkgconfig", "setuptools >= 46.4.0", # Temporarily redirect it to me, will revert when 6.7 gets released - "multidict @ git+https://github.com/Vizonex/multidict@capi" + # "multidict @ git+https://github.com/Vizonex/multidict@capi" ] build-backend = "setuptools.build_meta" From c095783db7146b513b1a0f36947d2eabc2c1c13a Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 16:26:31 -0500 Subject: [PATCH 07/18] trying to compile the sources --- requirements/test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test.txt b/requirements/test.txt index 019334b5d52..c1b1b2387a0 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -58,7 +58,7 @@ markdown-it-py==3.0.0 mdurl==0.1.2 # via markdown-it-py # Temporarily redirect it to me, I will revert when 6.7 gets released -multidict @ git+https://github.com/Vizonex/multidict@capi +# multidict @ git+https://github.com/Vizonex/multidict@capi # via # -r /home/dependabot/dependabot-updater/tmp/20250715-1384-v0sr9j/dependabot_20250715-1384-mw23m4/requirements/runtime-deps.in # yarl From 437eda7a2f14b4cec7229934b678f7ae0fb8de66 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 16:28:58 -0500 Subject: [PATCH 08/18] another unchecked requirements file --- requirements/runtime-deps.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 1bf23ed8f21..d954e23cb91 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -22,7 +22,10 @@ frozenlist==1.7.0 # aiosignal idna==3.6 # via yarl -multidict==6.6.3 + +# Temporarily redirect it to me, will revert when 6.7 gets released +multidict @ git+https://github.com/Vizonex/multidict@capi + # via # -r requirements/runtime-deps.in # yarl From b0d79c5a832e5c191e9ff662b649f84da868ead6 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 16:33:38 -0500 Subject: [PATCH 09/18] more multidict replacements, please compile --- pyproject.toml | 2 +- requirements/base.txt | 5 ++++- requirements/constraints.txt | 5 ++++- requirements/cython.txt | 6 +++++- requirements/dev.txt | 5 ++++- requirements/test.txt | 2 +- 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9754ec4288d..ea25255ad66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "pkgconfig", "setuptools >= 46.4.0", # Temporarily redirect it to me, will revert when 6.7 gets released - # "multidict @ git+https://github.com/Vizonex/multidict@capi" + "multidict @ git+https://github.com/Vizonex/multidict@capi" ] build-backend = "setuptools.build_meta" diff --git a/requirements/base.txt b/requirements/base.txt index 6f23cc4a568..ef0fadacc0a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,7 +24,10 @@ gunicorn==23.0.0 # via -r requirements/base.in idna==3.6 # via yarl -multidict==6.6.3 +# Temporarily redirect it to me, will revert when 6.7 gets released +multidict @ git+https://github.com/Vizonex/multidict@capi + +# multidict==6.6.3 # via # -r /home/dependabot/dependabot-updater/tmp/20250715-1382-lngh7e/dependabot_20250715-1382-a7k872/requirements/runtime-deps.in # yarl diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 0c6854dcdcb..df67d36db04 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -113,7 +113,10 @@ markupsafe==3.0.2 # via jinja2 mdurl==0.1.2 # via markdown-it-py -multidict==6.6.3 +# Temporarily redirect it to me, will revert when 6.7 gets released +multidict @ git+https://github.com/Vizonex/multidict@capi + +# multidict==6.6.3 # via # -r /home/dependabot/dependabot-updater/tmp/20250715-1384-v0sr9j/dependabot_20250715-1384-mw23m4/requirements/multidict.in # -r /home/dependabot/dependabot-updater/tmp/20250715-1384-v0sr9j/dependabot_20250715-1384-mw23m4/requirements/runtime-deps.in diff --git a/requirements/cython.txt b/requirements/cython.txt index 39257b77599..c144bb248c0 100644 --- a/requirements/cython.txt +++ b/requirements/cython.txt @@ -6,7 +6,11 @@ # cython==3.1.2 # via -r requirements/cython.in -multidict==6.6.3 + +# Temporarily redirect it to me, will revert when 6.7 gets released +multidict @ git+https://github.com/Vizonex/multidict@capi + +# multidict==6.6.3 # via -r /home/dependabot/dependabot-updater/tmp/20250715-1382-lngh7e/dependabot_20250715-1382-a7k872/requirements/multidict.in typing-extensions==4.14.1 # via multidict diff --git a/requirements/dev.txt b/requirements/dev.txt index d74d80b1f2b..2c68abaa4f6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -111,7 +111,10 @@ markupsafe==3.0.2 # via jinja2 mdurl==0.1.2 # via markdown-it-py -multidict==6.6.3 +# Temporarily redirect it to me, will revert when 6.7 gets released +multidict @ git+https://github.com/Vizonex/multidict@capi + +# multidict==6.6.3 # via # -r /home/dependabot/dependabot-updater/tmp/20250715-1384-v0sr9j/dependabot_20250715-1384-mw23m4/requirements/runtime-deps.in # yarl diff --git a/requirements/test.txt b/requirements/test.txt index c1b1b2387a0..019334b5d52 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -58,7 +58,7 @@ markdown-it-py==3.0.0 mdurl==0.1.2 # via markdown-it-py # Temporarily redirect it to me, I will revert when 6.7 gets released -# multidict @ git+https://github.com/Vizonex/multidict@capi +multidict @ git+https://github.com/Vizonex/multidict@capi # via # -r /home/dependabot/dependabot-updater/tmp/20250715-1384-v0sr9j/dependabot_20250715-1384-mw23m4/requirements/runtime-deps.in # yarl From 2c9a78fa723d4985377e67773879bfc93d2baa2a Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 16:35:40 -0500 Subject: [PATCH 10/18] spelling mistake fix --- aiohttp/_http_writer.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx index 06bf831e2db..767e5a7c51a 100644 --- a/aiohttp/_http_writer.pyx +++ b/aiohttp/_http_writer.pyx @@ -17,7 +17,7 @@ from multidict cimport ( # NOTE: Cython API is Experimental and is held subject to change # Depending on different circumstances. # Remove this comment when draft is officially over -# or when 6.7 is released with the offical names. +# or when 6.7 is released with the official names. # This may or may not be what the other authors had in mind. # My todos are held subject to removal when Draft is transformed # into a real pull request. From 4103a7a2d291f238fbbbcbbb246e2679fda53c8c Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 17:41:40 -0500 Subject: [PATCH 11/18] revert for now lets focus on the http_writer first --- aiohttp/_http_parser.pyx | 72 ++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 391e36691f3..f5015b297b0 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -1,4 +1,4 @@ -#cython: language_level=3, freethreading_compatible = True +#cython: language_level=3 # # Based on https://github.com/MagicStack/httptools # @@ -12,20 +12,10 @@ from cpython cimport ( PyObject_GetBuffer, ) from cpython.mem cimport PyMem_Free, PyMem_Malloc -from cpython.object cimport PyObject from libc.limits cimport ULLONG_MAX from libc.string cimport memcpy -from multidict cimport ( - CIMultiDict, - CIMultiDict_Add, - CIMultiDict_Contains, - CIMultiDict_New, - CIMultiDictProxy, - CIMultiDictProxy_GetOne, - CIMultiDictProxy_New, - multidict_import, -) +from multidict import CIMultiDict as _CIMultiDict, CIMultiDictProxy as _CIMultiDictProxy from yarl import URL as _URL from aiohttp import hdrs @@ -58,9 +48,6 @@ include "_headers.pxi" from aiohttp cimport _find_header -# import multidict capsule... -multidict_import() - ALLOWED_UPGRADES = frozenset({"websocket"}) DEF DEFAULT_FREELIST_SIZE = 250 @@ -74,6 +61,8 @@ __all__ = ('HttpRequestParser', 'HttpResponseParser', cdef object URL = _URL cdef object URL_build = URL.build +cdef object CIMultiDict = _CIMultiDict +cdef object CIMultiDictProxy = _CIMultiDictProxy cdef object HttpVersion = _HttpVersion cdef object HttpVersion10 = _HttpVersion10 cdef object HttpVersion11 = _HttpVersion11 @@ -124,8 +113,8 @@ cdef class RawRequestMessage: cdef readonly str method cdef readonly str path cdef readonly object version # HttpVersion - cdef readonly CIMultiDictProxy headers # CIMultiDictProxy - cdef readonly CIMultiDict raw_headers # CIMultiDict + cdef readonly object headers # CIMultiDict + cdef readonly object raw_headers # tuple cdef readonly object should_close cdef readonly object compression cdef readonly object upgrade @@ -197,8 +186,8 @@ cdef class RawRequestMessage: cdef _new_request_message(str method, str path, object version, - CIMultiDictProxy headers, - CIMultiDict raw_headers, + object headers, + object raw_headers, bint should_close, object compression, bint upgrade, @@ -224,9 +213,8 @@ cdef class RawResponseMessage: cdef readonly object version # HttpVersion cdef readonly int code cdef readonly str reason - # Temporary Note: We can now expose the real types thanks to the new cython-api - cdef readonly CIMultiDictProxy headers - cdef readonly CIMultiDict raw_headers + cdef readonly object headers # CIMultiDict + cdef readonly object raw_headers # tuple cdef readonly object should_close cdef readonly object compression cdef readonly object upgrade @@ -262,8 +250,8 @@ cdef class RawResponseMessage: cdef _new_response_message(object version, int code, str reason, - CIMultiDictProxy headers, - CIMultiDict raw_headers, + object headers, + object raw_headers, bint should_close, object compression, bint upgrade, @@ -309,8 +297,8 @@ cdef class HttpParser: bytearray _buf str _path str _reason - CIMultiDict _headers - CIMultiDict _raw_headers + list _headers + list _raw_headers bint _upgraded list _messages object _payload @@ -396,13 +384,13 @@ cdef class HttpParser: name = find_header(self._raw_name) value = self._raw_value.decode('utf-8', 'surrogateescape') - CIMultiDict_Add(self._headers, name, value) + self._headers.append((name, value)) if name is CONTENT_ENCODING: self._content_encoding = value self._has_value = False - CIMultiDict_Add(self._raw_headers, self._raw_name, self._raw_value) + self._raw_headers.append((self._raw_name, self._raw_value)) self._raw_name = EMPTY_BYTES self._raw_value = EMPTY_BYTES @@ -423,34 +411,25 @@ cdef class HttpParser: self._has_value = True cdef _on_headers_complete(self): - cdef CIMultiDictProxy headers - cdef CIMultiDict raw_headers - cdef PyObject* upgrade_value - cdef bint allowed = 0 - self._process_header() should_close = not cparser.llhttp_should_keep_alive(self._cparser) upgrade = self._cparser.upgrade chunked = self._cparser.flags & cparser.F_CHUNKED - raw_headers = self._raw_headers - headers = CIMultiDictProxy_New(self._headers) + raw_headers = tuple(self._raw_headers) + headers = CIMultiDictProxy(CIMultiDict(self._headers)) if self._cparser.type == cparser.HTTP_REQUEST: - if upgrade and CIMultiDictProxy_GetOne(headers, "upgrade", &upgrade_value): - self._upgraded = ( - (upgrade_value).lower() in ALLOWED_UPGRADES - or self._cparser.method == cparser.HTTP_CONNECT - ) - elif self._cparser.method == cparser.HTTP_CONNECT: + allowed = upgrade and headers.get("upgrade", "").lower() in ALLOWED_UPGRADES + if allowed or self._cparser.method == cparser.HTTP_CONNECT: self._upgraded = True else: if upgrade and self._cparser.status_code == 101: self._upgraded = True # do not support old websocket spec - if CIMultiDict_Contains(headers, SEC_WEBSOCKET_KEY1): + if SEC_WEBSOCKET_KEY1 in headers: raise InvalidHeader(SEC_WEBSOCKET_KEY1) encoding = None @@ -617,7 +596,7 @@ cdef class HttpRequestParser(HttpParser): if self._cparser.method == cparser.HTTP_CONNECT: # authority-form, # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 - self._url = URL_build(authority=self._path, encoded=True) + self._url = URL.build(authority=self._path, encoded=True) elif idx3 > 1 and self._path[0] == '/': # origin-form, # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 @@ -643,7 +622,7 @@ cdef class HttpRequestParser(HttpParser): query = self._path[idx1: idx2] fragment = self._path[idx2+1:] - self._url = URL_build( + self._url = URL.build( path=path, query_string=query, fragment=fragment, @@ -687,9 +666,8 @@ cdef int cb_on_message_begin(cparser.llhttp_t* parser) except -1: cdef HttpParser pyparser = parser.data pyparser._started = True - # Not Certain yet if 5 is a good solid number of headers to preallocate yet... - pyparser._headers = CIMultiDict_New(5) - pyparser._raw_headers = CIMultiDict_New(5) + pyparser._headers = [] + pyparser._raw_headers = [] PyByteArray_Resize(pyparser._buf, 0) pyparser._path = None pyparser._reason = None From d4736bddbbc02f766d1ff399c2c7bac71728c945 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 18:45:33 -0500 Subject: [PATCH 12/18] http parser is now ready --- aiohttp/_http_parser.pyx | 72 ++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index f5015b297b0..9b6fe0fdc08 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -12,10 +12,27 @@ from cpython cimport ( PyObject_GetBuffer, ) from cpython.mem cimport PyMem_Free, PyMem_Malloc +from cpython.object cimport PyObject from libc.limits cimport ULLONG_MAX from libc.string cimport memcpy +from multidict cimport ( + CIMultiDict, + CIMultiDict_Add, + CIMultiDict_Check, + CIMultiDict_New, + CIMultiDict_UpdateFromDict, + CIMultiDict_UpdateFromMultiDict, + CIMultiDictProxy, + CIMultiDictProxy_Check, + CIMultiDictProxy_Contains, + CIMultiDictProxy_GetOne, + CIMultiDictProxy_New, + MultiDict_Check, + MultiDictProxy_Check, + UpdateOp, + multidict_import, +) -from multidict import CIMultiDict as _CIMultiDict, CIMultiDictProxy as _CIMultiDictProxy from yarl import URL as _URL from aiohttp import hdrs @@ -48,6 +65,8 @@ include "_headers.pxi" from aiohttp cimport _find_header +multidict_import() + ALLOWED_UPGRADES = frozenset({"websocket"}) DEF DEFAULT_FREELIST_SIZE = 250 @@ -61,8 +80,6 @@ __all__ = ('HttpRequestParser', 'HttpResponseParser', cdef object URL = _URL cdef object URL_build = URL.build -cdef object CIMultiDict = _CIMultiDict -cdef object CIMultiDictProxy = _CIMultiDictProxy cdef object HttpVersion = _HttpVersion cdef object HttpVersion10 = _HttpVersion10 cdef object HttpVersion11 = _HttpVersion11 @@ -113,7 +130,7 @@ cdef class RawRequestMessage: cdef readonly str method cdef readonly str path cdef readonly object version # HttpVersion - cdef readonly object headers # CIMultiDict + cdef readonly CIMultiDict headers # CIMultiDict cdef readonly object raw_headers # tuple cdef readonly object should_close cdef readonly object compression @@ -126,7 +143,20 @@ cdef class RawRequestMessage: self.method = method self.path = path self.version = version - self.headers = headers + + + if CIMultiDict_Check(headers): + self.headers = headers + + elif MultiDict_Check(headers) or MultiDictProxy_Check(headers) or CIMultiDictProxy_Check(headers): + self.headers = CIMultiDict_New(len(headers)) + CIMultiDict_UpdateFromMultiDict(self.headers, headers, UpdateOp.Extend) + + elif isinstance(headers, dict): + self.headers = CIMultiDict_New(len(headers)) + CIMultiDict_UpdateFromDict(self.headers, headers, UpdateOp.Extend) + + self.raw_headers = raw_headers self.should_close = should_close self.compression = compression @@ -186,7 +216,7 @@ cdef class RawRequestMessage: cdef _new_request_message(str method, str path, object version, - object headers, + CIMultiDict headers, object raw_headers, bint should_close, object compression, @@ -213,7 +243,7 @@ cdef class RawResponseMessage: cdef readonly object version # HttpVersion cdef readonly int code cdef readonly str reason - cdef readonly object headers # CIMultiDict + cdef readonly CIMultiDict headers # CIMultiDict cdef readonly object raw_headers # tuple cdef readonly object should_close cdef readonly object compression @@ -250,7 +280,7 @@ cdef class RawResponseMessage: cdef _new_response_message(object version, int code, str reason, - object headers, + CIMultiDict headers, object raw_headers, bint should_close, object compression, @@ -297,7 +327,7 @@ cdef class HttpParser: bytearray _buf str _path str _reason - list _headers + CIMultiDict _headers list _raw_headers bint _upgraded list _messages @@ -384,7 +414,7 @@ cdef class HttpParser: name = find_header(self._raw_name) value = self._raw_value.decode('utf-8', 'surrogateescape') - self._headers.append((name, value)) + CIMultiDict_Add(self._headers, name, value) if name is CONTENT_ENCODING: self._content_encoding = value @@ -398,6 +428,8 @@ cdef class HttpParser: if self._has_value: self._process_header() + # TODO: I would like to use the CAPI for Python bytes + # instead, python slices can be a bottlekneck if self._raw_name is EMPTY_BYTES: self._raw_name = at[:length] else: @@ -411,6 +443,11 @@ cdef class HttpParser: self._has_value = True cdef _on_headers_complete(self): + cdef CIMultiDictProxy headers + cdef PyObject* upgrade_value + cdef unsigned char upgrade + cdef int chunked + self._process_header() should_close = not cparser.llhttp_should_keep_alive(self._cparser) @@ -418,18 +455,18 @@ cdef class HttpParser: chunked = self._cparser.flags & cparser.F_CHUNKED raw_headers = tuple(self._raw_headers) - headers = CIMultiDictProxy(CIMultiDict(self._headers)) + headers = CIMultiDictProxy_New(self._headers) if self._cparser.type == cparser.HTTP_REQUEST: - allowed = upgrade and headers.get("upgrade", "").lower() in ALLOWED_UPGRADES - if allowed or self._cparser.method == cparser.HTTP_CONNECT: - self._upgraded = True + if CIMultiDictProxy_GetOne(headers, "upgrade", &upgrade_value): + self._upgraded = (upgrade_value).lower() in ALLOWED_UPGRADES + self._upgraded = self._upgraded or self._cparser.method == cparser.HTTP_CONNECT else: if upgrade and self._cparser.status_code == 101: self._upgraded = True # do not support old websocket spec - if SEC_WEBSOCKET_KEY1 in headers: + if CIMultiDictProxy_Contains(headers, SEC_WEBSOCKET_KEY1): raise InvalidHeader(SEC_WEBSOCKET_KEY1) encoding = None @@ -569,7 +606,7 @@ cdef class HttpParser: return messages, False, b"" def set_upgraded(self, val): - self._upgraded = val + self._upgraded = val cdef class HttpRequestParser(HttpParser): @@ -666,7 +703,8 @@ cdef int cb_on_message_begin(cparser.llhttp_t* parser) except -1: cdef HttpParser pyparser = parser.data pyparser._started = True - pyparser._headers = [] + # I would assume 5 is a good starting number let me know if it should be higher... + pyparser._headers = CIMultiDict_New(5) pyparser._raw_headers = [] PyByteArray_Resize(pyparser._buf, 0) pyparser._path = None From 083b46550b7e2bc0b3fd451c56df01f806a5fbda Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 19:56:52 -0500 Subject: [PATCH 13/18] fix http-parser with updated Multidict-CAPI Fixes --- aiohttp/_http_parser.pyx | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 9b6fe0fdc08..8554b0879ef 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -216,7 +216,7 @@ cdef class RawRequestMessage: cdef _new_request_message(str method, str path, object version, - CIMultiDict headers, + CIMultiDictProxy headers, object raw_headers, bint should_close, object compression, @@ -228,7 +228,8 @@ cdef _new_request_message(str method, ret.method = method ret.path = path ret.version = version - ret.headers = headers + ret.headers = CIMultiDict_New(len(headers)) + CIMultiDict_UpdateFromMultiDict(ret.headers, headers, UpdateOp.Extend) ret.raw_headers = raw_headers ret.should_close = should_close ret.compression = compression @@ -255,7 +256,18 @@ cdef class RawResponseMessage: self.version = version self.code = code self.reason = reason - self.headers = headers + + if CIMultiDict_Check(headers): + self.headers = headers + + elif MultiDict_Check(headers) or MultiDictProxy_Check(headers) or CIMultiDictProxy_Check(headers): + self.headers = CIMultiDict_New(len(headers)) + CIMultiDict_UpdateFromMultiDict(self.headers, headers, UpdateOp.Extend) + + elif isinstance(headers, dict): + self.headers = CIMultiDict_New(len(headers)) + CIMultiDict_UpdateFromDict(self.headers, headers, UpdateOp.Extend) + self.raw_headers = raw_headers self.should_close = should_close self.compression = compression @@ -280,7 +292,7 @@ cdef class RawResponseMessage: cdef _new_response_message(object version, int code, str reason, - CIMultiDict headers, + CIMultiDictProxy headers, object raw_headers, bint should_close, object compression, @@ -291,7 +303,8 @@ cdef _new_response_message(object version, ret.version = version ret.code = code ret.reason = reason - ret.headers = headers + ret.headers = CIMultiDict_New(len(headers)) + CIMultiDict_UpdateFromMultiDict(ret.headers, headers, UpdateOp.Extend) ret.raw_headers = raw_headers ret.should_close = should_close ret.compression = compression From 35061362d4d324a6fbda632fccdadb284f1dcb9e Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 18 Jul 2025 20:33:05 -0500 Subject: [PATCH 14/18] revert req/resp objects we can deal with fixing these later... --- aiohttp/_http_parser.pyx | 45 +++++++++------------------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 8554b0879ef..d57f3eda795 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -18,7 +18,7 @@ from libc.string cimport memcpy from multidict cimport ( CIMultiDict, CIMultiDict_Add, - CIMultiDict_Check, + CIMultiDict_Clear, CIMultiDict_New, CIMultiDict_UpdateFromDict, CIMultiDict_UpdateFromMultiDict, @@ -125,12 +125,13 @@ cdef inline object find_header(bytes raw_header): return headers[idx] + @cython.freelist(DEFAULT_FREELIST_SIZE) cdef class RawRequestMessage: cdef readonly str method cdef readonly str path cdef readonly object version # HttpVersion - cdef readonly CIMultiDict headers # CIMultiDict + cdef readonly object headers # CIMultiDict cdef readonly object raw_headers # tuple cdef readonly object should_close cdef readonly object compression @@ -143,20 +144,7 @@ cdef class RawRequestMessage: self.method = method self.path = path self.version = version - - - if CIMultiDict_Check(headers): - self.headers = headers - - elif MultiDict_Check(headers) or MultiDictProxy_Check(headers) or CIMultiDictProxy_Check(headers): - self.headers = CIMultiDict_New(len(headers)) - CIMultiDict_UpdateFromMultiDict(self.headers, headers, UpdateOp.Extend) - - elif isinstance(headers, dict): - self.headers = CIMultiDict_New(len(headers)) - CIMultiDict_UpdateFromDict(self.headers, headers, UpdateOp.Extend) - - + self.headers = headers self.raw_headers = raw_headers self.should_close = should_close self.compression = compression @@ -216,7 +204,7 @@ cdef class RawRequestMessage: cdef _new_request_message(str method, str path, object version, - CIMultiDictProxy headers, + object headers, object raw_headers, bint should_close, object compression, @@ -228,8 +216,7 @@ cdef _new_request_message(str method, ret.method = method ret.path = path ret.version = version - ret.headers = CIMultiDict_New(len(headers)) - CIMultiDict_UpdateFromMultiDict(ret.headers, headers, UpdateOp.Extend) + ret.headers = headers ret.raw_headers = raw_headers ret.should_close = should_close ret.compression = compression @@ -244,7 +231,7 @@ cdef class RawResponseMessage: cdef readonly object version # HttpVersion cdef readonly int code cdef readonly str reason - cdef readonly CIMultiDict headers # CIMultiDict + cdef readonly object headers # CIMultiDict cdef readonly object raw_headers # tuple cdef readonly object should_close cdef readonly object compression @@ -256,18 +243,7 @@ cdef class RawResponseMessage: self.version = version self.code = code self.reason = reason - - if CIMultiDict_Check(headers): - self.headers = headers - - elif MultiDict_Check(headers) or MultiDictProxy_Check(headers) or CIMultiDictProxy_Check(headers): - self.headers = CIMultiDict_New(len(headers)) - CIMultiDict_UpdateFromMultiDict(self.headers, headers, UpdateOp.Extend) - - elif isinstance(headers, dict): - self.headers = CIMultiDict_New(len(headers)) - CIMultiDict_UpdateFromDict(self.headers, headers, UpdateOp.Extend) - + self.headers = headers self.raw_headers = raw_headers self.should_close = should_close self.compression = compression @@ -292,7 +268,7 @@ cdef class RawResponseMessage: cdef _new_response_message(object version, int code, str reason, - CIMultiDictProxy headers, + object headers, object raw_headers, bint should_close, object compression, @@ -303,8 +279,7 @@ cdef _new_response_message(object version, ret.version = version ret.code = code ret.reason = reason - ret.headers = CIMultiDict_New(len(headers)) - CIMultiDict_UpdateFromMultiDict(ret.headers, headers, UpdateOp.Extend) + ret.headers = headers ret.raw_headers = raw_headers ret.should_close = should_close ret.compression = compression From 04b12be38aabeaa1b1f43a28a2d0102971dddd25 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 19 Jul 2025 18:10:59 -0500 Subject: [PATCH 15/18] Add CIMultiDictProxy to Cython Types, Sharpen _http_writer, Replace test Error Protocol Message since Cython is Now Strict about it which is a good thing. --- aiohttp/_http_parser.pyx | 34 +++++++----- aiohttp/_http_writer.pyx | 51 +++++++++++------- aiohttp/web_protocol.py | 5 +- ...payload.cpython-310-pytest-8.4.1.pyc.25876 | Bin 0 -> 77214 bytes ...t_proxy.cpython-310-pytest-8.4.1.pyc.25940 | Bin 0 -> 26816 bytes ...endfile.cpython-310-pytest-8.4.1.pyc.25496 | Bin 0 -> 6083 bytes 6 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 tests/__pycache__/test_payload.cpython-310-pytest-8.4.1.pyc.25876 create mode 100644 tests/__pycache__/test_proxy.cpython-310-pytest-8.4.1.pyc.25940 create mode 100644 tests/__pycache__/test_web_sendfile.cpython-310-pytest-8.4.1.pyc.25496 diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index d57f3eda795..1aa00310943 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -3,11 +3,16 @@ # Based on https://github.com/MagicStack/httptools # +# NOTE: I have scattered notes around this file +# Temporarily as I hunt for things to improve, Please know +# that my notes are all temporary and I plan to remove them +# when I convert the pull request from a draft to a real pull +# request. - Vizonex + from cpython cimport ( Py_buffer, PyBUF_SIMPLE, PyBuffer_Release, - PyBytes_AsString, PyBytes_AsStringAndSize, PyObject_GetBuffer, ) @@ -23,13 +28,10 @@ from multidict cimport ( CIMultiDict_UpdateFromDict, CIMultiDict_UpdateFromMultiDict, CIMultiDictProxy, - CIMultiDictProxy_Check, CIMultiDictProxy_Contains, CIMultiDictProxy_GetOne, CIMultiDictProxy_New, - MultiDict_Check, - MultiDictProxy_Check, - UpdateOp, + istr, multidict_import, ) @@ -71,6 +73,7 @@ ALLOWED_UPGRADES = frozenset({"websocket"}) DEF DEFAULT_FREELIST_SIZE = 250 cdef extern from "Python.h": + bytearray PyByteArray_FromStringAndSize(const char *string, Py_ssize_t len) int PyByteArray_Resize(object, Py_ssize_t) except -1 Py_ssize_t PyByteArray_Size(object) except -1 char* PyByteArray_AsString(object) @@ -83,8 +86,8 @@ cdef object URL_build = URL.build cdef object HttpVersion = _HttpVersion cdef object HttpVersion10 = _HttpVersion10 cdef object HttpVersion11 = _HttpVersion11 -cdef object SEC_WEBSOCKET_KEY1 = hdrs.SEC_WEBSOCKET_KEY1 -cdef object CONTENT_ENCODING = hdrs.CONTENT_ENCODING +cdef istr SEC_WEBSOCKET_KEY1 = hdrs.SEC_WEBSOCKET_KEY1 +cdef istr CONTENT_ENCODING = hdrs.CONTENT_ENCODING cdef object EMPTY_PAYLOAD = _EMPTY_PAYLOAD cdef object StreamReader = _StreamReader cdef object DeflateBuffer = _DeflateBuffer @@ -100,7 +103,6 @@ cdef inline object extend(object buf, const char* at, size_t length): DEF METHODS_COUNT = 46; - cdef list _http_method = [] for i in range(METHODS_COUNT): @@ -131,7 +133,7 @@ cdef class RawRequestMessage: cdef readonly str method cdef readonly str path cdef readonly object version # HttpVersion - cdef readonly object headers # CIMultiDict + cdef readonly CIMultiDictProxy headers # CIMultiDictProxy[str] cdef readonly object raw_headers # tuple cdef readonly object should_close cdef readonly object compression @@ -153,7 +155,8 @@ cdef class RawRequestMessage: self.url = url def __repr__(self): - info = [] + # NOTE: This is Experimental, I might revert this later... + cdef list info = [] info.append(("method", self.method)) info.append(("path", self.path)) info.append(("version", self.version)) @@ -226,12 +229,17 @@ cdef _new_request_message(str method, return ret +# TODO: headers can sometimes come in as a different objects other than +# CIMultiDictProxy, this might be a problem if we wish to optimize these +# class datatypes further since some tests like to throw in a few +# curve balls int the headers argument. + @cython.freelist(DEFAULT_FREELIST_SIZE) cdef class RawResponseMessage: cdef readonly object version # HttpVersion cdef readonly int code cdef readonly str reason - cdef readonly object headers # CIMultiDict + cdef readonly CIMultiDictProxy headers # CIMultiDictProxy[str] cdef readonly object raw_headers # tuple cdef readonly object should_close cdef readonly object compression @@ -251,7 +259,7 @@ cdef class RawResponseMessage: self.chunked = chunked def __repr__(self): - info = [] + cdef list info = [] info.append(("version", self.version)) info.append(("code", self.code)) info.append(("reason", self.reason)) @@ -363,7 +371,7 @@ cdef class HttpParser: self._loop = loop self._timer = timer - self._buf = bytearray() + self._buf = PyByteArray_FromStringAndSize(NULL, 0) self._payload = None self._payload_error = 0 self._payload_exception = payload_exception diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx index 767e5a7c51a..204eea7ff6b 100644 --- a/aiohttp/_http_writer.pyx +++ b/aiohttp/_http_writer.pyx @@ -1,10 +1,10 @@ # cython: freethreading_compatible = True - +cimport cython from cpython.bytes cimport PyBytes_FromStringAndSize -from cpython.exc cimport PyErr_NoMemory +from cpython.exc cimport PyErr_NoMemory, PyErr_SetObject from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc -from cpython.object cimport PyObject, PyObject_Str -from cpython.unicode cimport PyUnicode_CheckExact +from cpython.object cimport PyObject +from cpython.unicode cimport PyUnicode_Check, PyUnicode_CheckExact from libc.stdint cimport uint8_t, uint64_t from libc.string cimport memcpy from multidict cimport ( @@ -14,6 +14,15 @@ from multidict cimport ( multidict_import, ) + +# Cython version should be a return type of str, +# Redoing the function signature should help eliminate +# a costly string check Otherwise A new function for the +# Multidict-CAPI should be looked into +cdef extern from "Python.h": + str PyObject_Str(object obj) + + # NOTE: Cython API is Experimental and is held subject to change # Depending on different circumstances. # Remove this comment when draft is officially over @@ -38,18 +47,18 @@ cdef struct Writer: Py_ssize_t pos -cdef inline void _init_writer(Writer* writer): +cdef inline void _init_writer(Writer* writer) noexcept: writer.buf = &BUFFER[0] writer.size = BUF_SIZE writer.pos = 0 -cdef inline void _release_writer(Writer* writer): +cdef inline void _release_writer(Writer* writer) noexcept: if writer.buf != BUFFER: PyMem_Free(writer.buf) - -cdef inline int _write_byte(Writer* writer, uint8_t ch): +# set to noexcept since we wish to handle the exceptions ourselves... +cdef inline int _write_byte(Writer* writer, uint8_t ch) except -1: cdef char * buf cdef Py_ssize_t size @@ -74,7 +83,7 @@ cdef inline int _write_byte(Writer* writer, uint8_t ch): return 0 -cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol): +cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol) except -1: cdef uint64_t utf = symbol if utf < 0x80: @@ -107,14 +116,18 @@ cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol): return _write_byte(writer, (0x80 | (utf & 0x3f))) -cdef inline int _write_str(Writer* writer, str s): +cdef inline int _write_str(Writer* writer, str s) except -1: cdef Py_UCS4 ch + if not PyUnicode_Check(s): + PyErr_SetObject(ValueError, "Invalid status-line: {!r}") + return -1 for ch in s: if _write_utf8(writer, ch) < 0: return -1 + return 0 - -cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s): +@cython.nonecheck(False) +cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s) except -1: cdef Py_UCS4 ch cdef str out_str if PyUnicode_CheckExact(s): @@ -122,26 +135,28 @@ cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s): elif IStr_CheckExact(s): out_str = PyObject_Str(s) elif not isinstance(s, str): - raise TypeError("Cannot serialize non-str key {!r}".format(s)) + PyErr_SetObject(TypeError, "Cannot serialize non-str key {!r}".format(s)) + return -1 else: out_str = str(s) for ch in out_str: if ch == 0x0D or ch == 0x0A: - raise ValueError( + PyErr_SetObject(ValueError, "Newline or carriage return detected in headers. " "Potential header injection attack." ) - if _write_utf8(writer, ch) < 0: return -1 + if _write_utf8(writer, ch) < 0: + return -1 + return 0 # --------------- _serialize_headers ---------------------- -# TODO: Change headers parameter into CIMultiDict or MultiDict or fused -# cython type or am I insane for wanting to do so? -def _serialize_headers(str status_line, headers): + +def _serialize_headers(str status_line, object headers): cdef Writer writer cdef PyObject* key cdef PyObject* val diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index cdeb423554f..5d63db24b87 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -24,6 +24,7 @@ ) import yarl +from multidict import CIMultiDict, CIMultiDictProxy from propcache import under_cached_property from .abc import AbstractAccessLogger, AbstractAsyncAccessLogger, AbstractStreamWriter @@ -75,8 +76,8 @@ "UNKNOWN", "/", HttpVersion10, - {}, # type: ignore[arg-type] - {}, # type: ignore[arg-type] + CIMultiDictProxy(CIMultiDict()), # type: ignore[arg-type] + CIMultiDictProxy(CIMultiDict()), # type: ignore[arg-type] True, None, False, diff --git a/tests/__pycache__/test_payload.cpython-310-pytest-8.4.1.pyc.25876 b/tests/__pycache__/test_payload.cpython-310-pytest-8.4.1.pyc.25876 new file mode 100644 index 0000000000000000000000000000000000000000..204ec3430d43e85e0b499212f7ffaf6606efe1ac GIT binary patch literal 77214 zcmeFa37lLp`ybLz&*4&oVKY`|dFuoyCO0z`zEov;RhArQinI0TZ;%JLGNm+IEb?ctm&N+3qs`d0_Lil&xd-m*(eLfWWF)zITVz{^xf9Ch2p^y3}*qLENIG!+|*hxL=hSVEp8$CC1!8cWGBa^AA+*nTDiB9%R<;U`p zk4^SY6~+ovePex7{bT)817ibIgJXkJ%f^;j^$(2=;r;mJ@~IVLE972ca^=*ju~m{! zPM$EedTh1iQ8q@oICW)cXT<0;`sK-KsAs?!L_LFeb~Byaj+!@B1$gM%{EaPnCMvxme&Oz>cI}A$Of|J#v>Lx7D}-xhs%+v2i1ES0ZrzBUW(lP#skRRhTI;bf!yuL{hHB4?hfSk8V@3O zCvvYe9zt#mxqZf~kb4<&`;CW@y9>Ej8?Qm`ZscBTJc8WIk$cp59dae)UT?esxqFa% z%y=VmW#ryuJdWHB?8Uxn5JPl#`3yST*ad)Xg*X zYHhka*-C7$JW$_yb1VMRY1}{!*)3adnw_jyuda^QahaSc@0qNX4P*ze+F{mJdAyGA zRmxMht17C;{q(KZTy=G6+t#sb#tD-5UlJEr;?I01l8PXqv4{~Ki(*m5j5rz-GZK|} zB~eKlNh7s0wi2kuh+=UP0mOicR^xfXx|cJ09*zSooq*2k#nVhWiY3`s$zloTDm~V_ z30d^a(h1%FTLZy0~Lc9aq)qV!60OUL9#AR0U!O<&&(mIhxUh> zp%Sq1w$Q=w=y0T!W?f~a%6nP~UDrX3qgk2Uc`&4w;guuXw_I}9OU;Th@4Btps7>E> zb!~ihsxnJ&iwe2+6*1Gc*yJ3)dsf@c!_DF!BDjkSgHOa8&iVyQ&{z> zPW=N;PTH)SYp60N9G{$yq^+s)u$43_J15KaN-I^F9XL?>hYvJX z2f}9+Yh}%Pxjt)_CacqxR&sZxY+#)RcJ6WQwfvf$l4Wh1H94pdYG;UtFFkm;J;G)8(m3snp7pN>eps z7DHER$O?4(mHK?vQnO&ov|@rX$HszbEUMi$|*TnGb^U_#Ddf8x@)FQ(12ojaaAlM^{nVvM z#_{L;fALCmXTnGrNznJ0k*dUvxRC}IkTJ5L^9dsj zD&5mcS}-1o&(?RY+0;q^P;e=VmI0A89M;t^eT|(y-|{<;n?W4jeOYz1+Ev*ID2N#x?!^wDgte?%>5^Fn6H%E_t^8F|8nfXVLe&~5fuu~8>!r#AGn3_MnJ2?>58k{6aJ90i ztfarQR$u71*0&Ep!l(edSFT`-I2G208+og5M)adrLRPyP?MnDei}2a>SH5AB+fh{7 z_EA~Zn{i*rMdYDaxY1*OQ@(|F*~RK=B*RH{0rM9!;h3vUOm1RAE=c2upk>c5T#i2h zFBMu59*Qd7^DR#fC#RGQ=~s}!4k~O!u!D;1k8t{H4&y*1%ta9euMs*B-W#dMnvpt7 zaQN>wF!k#3xx|6ceOO2JiYnFj z%v44$ove+QC(X-8wqr)#qH6b5L5xO0J=(@1px;|L?>pD=qvQ&Rchz=e8W(!z{{_Ql z=GV9okSSg!OJeP?dA7AQ&XXJKht1*4-d3h+R;NKCr^hR;@QjQkRt*- z$=Z(cq|h{VJszvAOm0Ae6-aC)4OF)y=Usstyesg+w#Cru@9ciI$4nu~hjY3Dad7scm@O(+w`A=KNq|)JvK0 z3IsSVbqyz535_7JJt|j0Yg?Jy%9FDdL5nit>ScUuGm=)Kwqv4#U1!8IEbd$#Vv&;< z0RCnal6@8jAr?-BQ_r9JyxNVszJ4L@a}db4Ff4UJAVdj|MfZYgf}Ubeuov5uX5?Ot zFs`jf=0K^M(VbxNW<|L#rH!K89);8LGpT(wjs#|Kr)Hv8j@GYSC1DYSwIrf zkjw%)AkjY-NOICAJpqt>%z@<7#}$$sAYtz~qw{#!91>Xc_~Zj%`Go_^FOMrMJ%FXR z4VIUCU@3IM0%)(Wp?N&4u0DS72omTUZN<1wgdQJBF*{O!fa~fkuAvmURx;q- znx^h$zvjGOJ(%bqQtP}F>jHHT-w`fSGmaN|FE)9Qx8Tw~!KD_NEsaA?$g#Ar=j+1! z|Az3}!s;R1?T$U)ho?67tb%2FEaKo#-wHLa^Q?&={CT#$BGj7WydJWnstPC@c5kjB zx6~+OVn#l~4S0EYA$Y`quZ;;zi_cNJyaMiD| zNE;F<5d>>}G>lKU!z-xyY}LZ*A~Q?5#O*8qi)eh2&8MlG(cN9SS&ur=sb%)v!2 zV|xak3$;?`AeXY}R$TI=Tf9fQm9Xd*T7_YAVya6cK5-oW(#whxy`cn8^_-HXG!OagwYWUKy+4y;2UBpccJgHha$tFcwPzLHz6(p_ncC333=lL2w;ShV) zrmRvyk0%7oksW122mmpGNRD&id>9PoW4QLUB#9gR+k!vy!$|6(dYH>}7xt)6h7Uyc zQ(yx&j>~N?gf$bfy_hv%sv^xO7|vKTW{%deJHy|nh6iX83Dr?vjiTQ(N_5S;pns6fIbWE zIP`3k3Atuju8SkhZ45&@BJF8qT!Md}S|_ zdzd`PS*sNGA|=w|`|!HRBL*Sh!n`HY^^LY~J8>WS>iBsK*NlSoPH&Sc3ZKS2iqQFcl(^9Y-Aj%i zbY1%D6CQX1`U>#8%>&Q7jvqY2aP(?MPs<@mYk;Wc+Y{I;`)w&TeJ5GFDj1j zE-SNAzf7di!YMWq_tvVL)+o@?v+&u*s-WV!hosn?yZwvm+&wvPM{(dkdL@PW8 zsf0zez~Q$Mts5v02_RaDxug)SbMZVSL~AW_LcWA(F;Ak!-)=-JgQwXbq6PAq10)$A z`Fs;f+mK{QRxR>rLo$Qs8j@N0s!cwR6%wEiBjzRxcR9TFdNm|ki!O-m1su1SI^k3Hy{0+C1nT24r@7r zS}egH@=oh&&7Rq0B1H$4199e_6$E+?_qxw$vSS961og0(i(v*o9J84*Q54wh7}%i> zb}TOJSi%{_Q*s!YEXZz;-a8<$CsmT4(eqAibT=`Dm&3fd02qG8md9%eMvv#R1eG5JzEMI>(7sHJD0Iq%g@57NF#nYe} zH39Y>;(Mr(K*ED()h(DG(0t0QN;L_ee4lPcS3%>GJt{IdDJdpHPpmCX&bbXO;R*7I{AtKRB&*O9tRQ zsKpxR0>}A-xb}@RuAMEZdOa>Www3A)yvy0Gev8R2CN5x-lMUs_e9*zx2C850N|egp z$jW{jNe57zfLG$r{16gXISA9b)r&N(I}xUJ=}N*exdT5A^?L^IgXe_WJqz=?lZ-+o z2NS!r$GooJ7(lr^$_*OJP_EY)LYd_-ugf{+bt{aOc+zJ?DhXKRt};%*RlgY9o!lzi zR2#n+WW=!{Y>e-of4HgY3ES#htnIqbA#iv;#DkPEG!XC*W*+nZmk98SL zLa3#N2sg<5N9l=L0ovf~J@ZJBhg6r-`4Yx*T3(&8EzD|wCMZ0jTy1kh=y@>8t#bR>;c!N4qR z5(jnVSyzkMn`4Kz05D*4I(p{}-203JOlQxC?VLK3CC*}UHj?ou68j(X?5HjHv#rj~ zxJInn#Ms9+nCsd_=kP@rC&-wci+i8v{qvACPV49*+aS%gn(LVH?J%8c!( zn`o4E$N{L>6(Fl}HNvtqR0`~?r}^|0BpAcu?X;yWTJ~w(XVO}})7DMva8K%znRHSI zRC>PJhIi}{`ycZhIgXlWHH`3_sCEzNHMss(G1wJ@F4p4O^_IrqtG-VOZXK)VUY zMm}H8B+cYRCVfnZ3B=mYwZPN1Hy85(=bUOYzWZI?b30vfGC>dG0slaseqrDP zQK>CTrKmMcSkyLos>NKeM-!8sgosWPB05c4qSK@$I;Ge&Dx%Z4K$~i&0ByS7(@bNx zoWY(n+srhxfIQdCncqb1ggghx<-S0kbKrOj;i&fjj$bDn%mMNoV7DRtO_tzJS^YLk zFz2)_WwkAB{bsD+EZW-hL;^x!5O$-t*&{8qL||)FXl&dL z4RUd_M*|~tij?{OYJE580QuEpa+rj8?Yi|FF1YZbjhikWwPnY* zP|X@(MAStC-LX0{d(Vb#k*$!v2nuQ1tF>ZOG#!k&YH2mb+t5v4#?|6hpv&^>ksBVGIFl8R-eXz9Ep1Wu-T2c;C^t_$q&$9)UT*3S!z8nakbOx`vYH4(s~?k2+) zz@nz-GNK0Agy&F7fQ`EhxtznsG0!E&U(KT0Z@fg_Ad7mNJe`k4?LmorGnhq9?6q0c z-ar=h+jy6dx3j1=9B(Ha^V>!U=FMf(mMSlfM-?)#C<;JQ(RCplENJyej}HWxG1gj1Axv9umVJ@>eb%yM z{w$Ed=oL|gb-P)ywIQE5ad1LW(+9gr6zWE25Jrvf=2qA`%Xy!7Dgu+ouhM0!P*^jw2e8s8#68yz#Q45eF>gx z>~@9BO}zmp+u@t1a@~zN(zCHUbChtHquxN~=&PumFt#&CHjHn;bB&|lOgJ1I?XI2o zvV`F1Ct1S6(Yh7wxS7lm%PbB@cV~_+UT8)-#5<5XQs2TP7**fqe3)eN9VVkpzRTo~ znfwWpuDrU;%4b-ljk!2~akJ6^5RPT-xKy}{zeU|$xeN7Yc#7?c?gxu1SSnEe((EQs zInKl??}tJCIeX?Wm{72AJV}-AUt!){&HX&Ist5h6SN@LyN%tQ8D?CMyUL-Ti-x`09 zzM#nNhwQB&WOtrStA(2_&>=XE)#jhf+vdN~ZN6l|HaoNRg<4K*_pp{6wR&y40jsro zVTykK9u#Za2&g~hy{DP%WwHqVU#svJ?n|fDr?Ef(jf(YH6*>}e*^l6*67{v7YJC3&=@pZBFDE}YNcxrXxx0)rRMoCOzz!oJ8*(>lBEcgl@_;P@&2Y-3=EI01U8`lP|CJMK0^L0zIZBD_jr-JYxyUWvX|5p%< zLu-xwJhfsYrhysdO1xjVoWi69F{CL~aP$J|ng;M^7LhyxeaptsqhbEu8&SDO=qL>( z*dtJf@pm{nsvbucM`UbuiZ!JIG( z!Xvb?2L(Vpyuw56vTXH_maiNGnEoTJf23eSEjJ^>DXz zA|G6&+hk8eZwW4KzV~wBYKhgh z2*;|ih30A@O_6)TlRG=J+vN>ldUcmPMGklno)Usbl44F}i!}XQdkpx4e{n!YXl-$( z=GY++1jY+BbIXBgn%QQLh3|ffudoHgci^a-Jr=&RTkso%M=Ode+`VEU#pp_8tdy_%WU(Fb z4T2eV8OIXGv1E^4N!o4RfFTq6aw3de*cLB67fyuKf8uLCM3KV{;#WZwlX!C<-9aKi z5aKw~RqPt|AP9K49{^)kPr^DN3~%}<5QVGZn69Vh(gBSDtJyRV+sFxCqdYdt!um zG~!g>3*Gz>DzbGF)Qan+^Ks@JH5|0{{nXZ@DqH=J4Qt)_Yw4_} z@m+VhOQPPn_Qgr?^+G(voNQ~{70RXS)t6tO9n;#x5FQ3-L8BatHp|kB zVd)|Ms?cW|=1{S?*P6&$v54tz(Pw*6UCD_o%kCOZ(B`?d( zS)e_EkHs$VIkfhplhYrbNv{485R$oMI_F}~ME#;`Y7@w5y z62cG4ciSP%TZ_@%Atmej`#>_XL(+>8t|?fIeU0mcYPQK~JsS_(qz2KW#&^@?MuY{E zdy2JbFXxRn~fqB`--X8)r!hB^tJ@YTR|nW#xLstN-HVFPZ#` ziFeWcfH#iMqRV>7;_a+fSY~(S$)bCZ4}>2UdifkL+t!&@&9@1mrC&8U6Ed!iNOo|I z^`j0iVM~P%ah1^~)VIodP%eeP{rEHg+*)ODo&ssp9Pp!XI+=(}MC-8$jQ?XWf&LJ`JJyXn;R0+Sit(@^NL?L zGFZ2(Es24D-Ex0wYCaeC+?q2g!jjqvkb9m4d}IVc1eL8JDAtIqSiSEiC6&;xGnH}K z-*?&Gb88>NAGi01r;$7ord{g+_{G<;#mAUqrpQKr02_TgA&%P?!aamCQYSwGzjMT2 z4J~)?!{c0?&-@2Si*hGwIFh*)^91(AjY{#z0Py*ErGjbWOp@Y}LgsRh@gMdEV#zqXTCpnik4*kM zl2(kC#4c`S)St=8R!#>Z*5POnim(n(&>gdN9r>V@kU%&!1TIB%xKagSj}bxfVpd5( zhZ5n32t)*Li$FvK95T3zlx?*7?0AQ|8B^x2gFRzfNW{;}K)15C$QZ`e8k2KFpshvP z*e3);(o9DkYTV$Z8n@FgFK$m6Rd z5p1es3r!Ls0ulym7g!WQ`U%S- zF)4@??3f_ZWHV_tgrz|ah&09ff=JU2D|MZ)G!XZy8-=Ao4rmgrlvCmkmJmdGH%nNE z6uxGHNXee%Nm77F@ytS`c_32kf&`JK>%AybAhH%jn%J9s#W~H`1ckv#v95Q=4paAnF%`!r>VFjN|tPyDjcbp+i6laZNkE8()9pSag70M*xHHr=fKar0 ztRWZ&HL-_502Po0%l5X-jrS4q1YS;shQg=E)OuW_t~ux^5V{AxDNF>53ke(GuKy9{gteC$_c2~NB7ot3O$FT@MG&t_I*}jU%DQi0^1Ddj=`LR+ z^sOFjpk(?>sAzREixa5UV9=`3jguWmktNHl^ByLLn0%SZSC~jgI>X-5p_AHJnxGH7 z3$>c$AtDduBK^@+nnL+lv_G7Q9uHde@wnQEFZd7EH(iWs7R<>R<TG+Enly3_a+;1ahmG!y3j$-6?S&OUTyd*I2^Z+7y_Vtqp&< zwdu3AHgJV?wl*iA)M{sIL-Ba`t<689><~6J+}bR2wl+f!9G}DUm9n+@l6+flZLo21 zO8gN^2u1#5mar%?j<2z{Hp}KtG*AO5GM-r!`2%r z^uZvueTs0|OWWGC!VkC`8wFYrQuwo?PzVa_#ih@wk)*`Nxhl#xVoY#WQAj0_Kp$!C zA{byrqgZDx30L`b7R@5DHVZ80lciyYwusGw!cj*dg)@$HZ_m^Vd0n|7y+gnki3`2 z9&~u@L-GcB?5{~F&0`Nr9$E?H_r67)j&B#}1go5DULJKSQj}<(JQ6vWCeBlQsZ}s(!}%=ObxccN_pD z5mIYWTPHIs$T5JKRqF_W%XGOcY{6z>?SWyqL$!g)WlSzWvPAB63kQt0sSvq8MoG|d zb8@8vsCGhS&T=`0BdflLjCBk0su7L(;Ur54=x_>co90~8oVcRDMHC(Kk`nP(bKat8 z`y}suj>#98{2P;>BY^|D@a-P;|o5*M(!w$ zKOt-%!c$Gyx`(Y2TSWv#hc!*a@LJPEV4q^S(Tmu~Rg~|5^+yU!I$37u^BirLZEZpu zcWozrHFCC{IKvZ(LRx-79OSo~I$Yb3c-7&sezcimxB|A@&cqQu&Fc#Ihp3$}(%KzL zcgq#fwQHe`ySABm2jQ@6X7bo<*sY+AyKcn?S;De**R23+E%$6sB(y@VX*<(vInC1| z#&(+5t+73lM3;xwkVx6CfZ7`7itTio4+?kXw@mJ8>n+SNhTr!Ovp|RZT7e8ge#s#} zMubQG7*R4$_~1SQnfOGFs@-%Ip~O&5C>fPX1SET<%R(Zk&G;Xog3FCk~yB52zm znP?HtMlSh7q_9!#YdgQu{-^NOu9o|7uj01A4J4*RExD+NyZ1%&x@3^ywD=_7xt)o~ z8U(rA!^^KSImE;@tK%YEw5_t%Q!KgDV(fhih$9d6M{{vHtczE{*7-2Q;iJS1aZ6n6 zVg?$hv}1;(U}hWbb9t?2tviFT*0 zxQ_dHX&u9&@m>JHr#qu}(_%(%+ai}z_tE3wt~A{*?tqrsD62g>x}0y`pxbEbA%u6a zqqxx-#hZ?H6meXfo>$JfUZADVk`9ilYXHx2AHF5x0(baqNH1_k@8h`)f0+ekr=|)LnKztoF^J3)e z(D*t66m+O^k3@>kK%{WC1&?Nf6A$&Hq6rL&^JK?*ibMO#$Vlmio`0ervjn9h65`|R z;o=S`5AMg2kNP-BM6KIVovhaP6phMcWfwv~n8kWcI6EIs({I_@Uv{qPZYHDQRgtDlrvxctWS$u=~KlH+H+P#?+9O zMING(y%2#1!7c5Rgu5MQl01V8|0H2JRqJdG1d7JA@b7gN;%G|XNmAJ5aIX`@(ePY9 zy-Tb<# z$|F%6+1xO=axbLm4oGpThh=~J5!B*Biu(d-)`9d5cn+^Y2x;Fe&}g+%R`SOQ(uo+t z)kPl*Tm72pfEGcoHMBSjfPLh`izfnaj|1;d1YYdS)w2R4a_HHf;|H&hAnfJ$X6vxZ zY{F((PVRxPqGxXO;CQzvd=q&t3nV3g`)&{11wIkD`)s&TVzWRFK{w8XxK7|j9 zup9lNsLS)P`et58PH$b$0z_1C?1Zs96Bw5P#)3FTKs>&;ui58;uisck>5gaa_IvvH z^B!0eO*|1;hdi*pQ(%RiCt!WAz^83Khiq8o_n2m|v|%|QACP_?Fjh!E4>kueUsf8c zocXfMI00>eKdRN&5pw`vx93QKPXzwe9{B$QB{EI)5a9onz=}NjdbQ*+uNU9fVv~KM zwL|k^=#jFiBY765D)rqpLt|>A3i~;QW8sQ6o-#*%PAql3py;1`<{kg=_BXgVTGMh^ z3|>RgtjCEVz;!7&>Yyv!g-{n~YF12mQQ2M-pC!7lF?j_Ohev27b-DFNk~kc`I=yC{ zeP_dwtcLV<5|hIe>Uyrls@pDlL7RE^ECm%p$$q^5wLjkfg&)7_OLox@HMua*d<8g&Lcd*D9 zl2)8fP3v2cnf2;!-W03x2LbVzJlTLJ8`R73L|jy20v|eWxB?$KKr-+X*&{|<)rcbQ2z)Mf*dw}G+UyZ|?qE@{=BbmyQ`?Rheu0n8 zPg`~p7Ek?BnN*g|*>U5l53}1iHe$oD5L0cjMt^~KG<*Fc=0x0hGIL8|jnu6H(Jz9ub20s*^h9~yXT!4-v^lc#9U`_`U06E+P|l}wxBFiq`S62``?gmAxbce{TSS*RGu1M4{+m#O3p}=Ixxwj_L;% zo!H_l31*KmvWhrHSWSiuZs> z>cYa-=OxeW2%&&A64bOj;4&eB3$*kuZN;C(2G`k25t0?oWe-eRp2o1Ix zgyt_$ANzvP4Dfy%G@rwB4b7M2%f}0vUi8bLw_mV*JVjv9J+rJ67Lu=@^uc1e7V!#q$A;<_U{!+HZ-{TR}UhA~^TgfK^ z;VK8hhE5wkM zht-$g+ar({1oDD$lEB;wS)GK%I@-g2f^VLeQE0z}1bPsz#_16NRb@RRoVfpM-1xIgqfs(!c z%kbq^(m+%TqhfTGj4SS{1zvqoZ=^egU;e?vu$MYp$%i!)r**?KlH}R{o z#}sQivLG;vdy;phM`!sl5om}Q!x2$vvy^*SPDC5h(g#?0l=npNafrD;WwM74{LENO z`%HFWR;&s1esU;hq6Tf8l8-(hXDvPs3C4psMKpyp0dJHiCH+8H(T~?Bw;*>z9R*up z9>UXwIF~xk@2tb~<4ri*6Br|T07_Vd&I2~_gw8wtemEBJ|DE{L+|5Laf%vT86gVa3 zKJW$zPO*=+%8scUPme*A72Ff)1wB*d@*>WE3GVgQDI~v*PniQIfkT~BVh2l*byvIO zeVd7AUBr_7)y#Vwd`XfA9Xnnk1w8HZ3Hr&r)7F_W`hn4Hl>`C@h9{DCSSh2&Ii~@X z*!(_f$9 z6jSe-=fI63TVA~3v2LpWNF5##ASkCz*E|r21Py+?!sHE1h{jrRX`4QI z>N?)NpUL%1?q>3GCM70YnTQ7-ktxK%F2Z||ga2YaKg8rpUJCyE3NOFTq|Ing@SmVL z|4eDkOCog__~*tqiSGLG2|;(!9K^D#pFb4Ib5r&z0OCV!w8?TdWnYOd2DNo)uWiYK zYmY|?WnJ}O?A|mL)5WK|6r2>{UE;jpumNAg&3_N zx&_H-Ew@GdC^0T@=}pRRflF^v2rsmv7%A;w*h8(IWfP6VSMW4Xg5r=0L_ZV@jXX#i zq8%FEO-kNljlRq~$MO9ipr%fH^bx7C5rk&>IZd0elD8%Vu=fDL=}W9e}!z z&;@bzOJw5RJ;HW%KkF>w(>ivPXzPB-%U>}OEhbr2c%F2XP&;h$l2~xn;Fo`miBKG` zA^3jYZKElN2#-rxBr?f^yyq$yg%uBCthJh_3uCPhgK~%JuSjxMqh=vFF>=#S$cg5f z$L2No>W2ZMkN8j`n8qI|qxnXQ`22tGbVacdOaYxlnRa$pa-F$N`CDBu^y5dq5(dGa@N-(q}||TApeT$0eQ-DXjnjLo3ooR^%UA zFPnB-0rwYtyO{ynhOHxz;;HzaecmNQc4aIgYq#Zjfed66C=p5vK(6;JTGr)hjXVRI z2DzoZBfLZ5*kXse(sCQ1ctPS`-E182gy(u>X98#;o$+1~#kmo{i)8U*?F zlzNqv(uCo)Qc4pB1_ZNu3U$3*Nrl}7w8gfS(rFgccj5SLAu9UJzd%0yP ziv8d`mZBsk1Of|DrC5TTOPAyN+;-EU|^_lV>*#rK@#V6-FPCbky( z9x`?iPKagnJ!I^A*z2?sVjH^7`dx4RZmJJ4k6r- zLtH2;S_W2_p(Et#BW}C~VUR3f4N?j+QKvaoOsqU4w6@@q1Qd?inDG)k7u}A!0yzmZ z4ZN6;Jeexnh|x8F+$L{O)^LYB)hs?Nsb!_iXXa?LTnNS~t-xu0h;fznwIb~?Yja!i zZG5S-6|SKp&DgY|~*S-@`}us7!xwyJ<`tl1|i zsn*v3O;R*zeUP<9Y32y*j?#kPmwDPZi;_43LfY4mHn^DE?S)6Al*ZhTNhuq1FZa~- z9w|jlKv>O5DXj@u4uf2`uBW6FMjNF*A*JkDyJFF^whw1k^6-e2)$rEHx9K$<1YO-s1r%xOM8wZrk zJuPxr{Mzu2i{IYoy64Sp!#y48{|dctY(>W2xtsdLF(6_^pG%SYpcQ1uwa~n?6`#g4-HOjksiSWNQF0t8 z*~@Te+*Ww}&25zIwk5#hTu?G$vr#f(*C<&_wmhREkP{_qmOCdbw~dmu6v9TyWVxm9 z9hBVbu-rt+?iEq8dliI|IVwRYneFPw7DA)s0ShJDEVtz^kM_+<-`gme?b9fkEVonJl+k>OE4*M#(ItQ8HO>x71To3S%rNnWgOU)-3m8W-U=N9kW(6uG~(3 z*=4!6-n>MdYz_ib2wD~nuUfO2?{=)LEsneK-N%HA3vOuFb}Q7Si#~)i?iXlVDb2M) zNBsihWbM~ue=P_rj{qyHHA-4fqOJ3u|0h z7KTQxktqN>oPhtH>^QYsKgBOr-USft|477n*Z4t-uxnt$+&D!wzTNR)8Hw;Misk_zL3z z%(KSRmOP)x2Bp_-3rb`JHZ7wjn~d{yEOxl9J@Sk)Yt0Vh>@nazJC29$dtfzx-=qJN zfGA2cAB!FC^<5QwS2!t0*x)!A+dj0RADC5dwZHrLpQ;q?C8n;3ST~SMAn5nta46{{@2(v5d{{Z9MCya9|zuDIy zeY7z}%&leqCstFBP1K<`0z`o?G*_*I*6pwwC&zA9R1iX+AVRlA9tEQFHYv0Zoxigk zom0&1FyOa!@il-!h(6L0LEnkevEZ~@J;E{2>_FHdnu^>7J&NmVfcwJO(K$?OcJzQc>Y;j?Cj!Vg^UAqPy&pl#op1aKO!Coam&&^ zEH?kt{Uu5*BetVkO4&WDoGl1xDaHXUvpriW)a(XGSAEPiVp=CTu@1!2j8DEq< zQ`&%W#k=Q9E9^0*d5O#dl;Y^ycJh}?Db3y^wza3!jZzBCJ?eUiltLbi?Fx6kpstdX z(u}Pkr8HxkHdcz9H*c)MVL!I}z29=(9EPO#8y3Oq=p zf8d&r@BR(G#kt;x(L_veYp$Q@fuA<*X|Y%jD{7WAmQdtG$=f3et+cN_UzfLZd%h(l z?e?6s==P-c_DOsCr9J(=_Vfp~Ct_ZUZ*{ikWT!n5b5I~-5P^tVDUj(P0w+s8xIGc` ze0huQi72jrcTF$7Jp`!wdj~NRgP6k{qlkF`-?Q6vDxU~GI?dB2 zz4{R)9?iFkE~Ah!&M?ljj>leR;Q@ps@Z$mCg1)_j5)$StE8@Bk z;UKS8m=uYon9}SDLJ!dBNL!;8@2i%JwtM+P7Drov;Nfr}?x|vvQZ$L)#SL%=4q`QF z!#GpkGg(8JUDpK3&5iSRm(1%sQ1-hW=(pzcZ17k}xr!B6 zb4;CzMt)C4=;!6kKIw+8dK(;k5o8HPx!$7H<6`O!bh+gwRP&e1Yx>{-3n-!-`3u5qpEfh;W(D5 z+PJArg{=rsH>*^yZ7FdNoT!2j=TYjl1$)wZbU3GRW{?WytAy_m6W653H5?U$=%=c! zc`;ycw|$rxkmR%7UevM_eWD%PWl+|^b{FC4K3@z*J8}$Gh295f%ft|DSFo$g$!^Yk zkrBn&bg=9Y^~jkJKWLjs3W9J}o6Po@Ma;MMQ+El zOvKmEzf3UkMRx5EW7CRDJ*|YU&f|}B9Vcvwi8$(PWllVE+Lp@wcrct1D8>ry$~}+y zw3tN;TtVxhZ2_zi7NEU0oCSwAm)M&Km;k>mxvbXcWMdtDoq?!2A03Nw*26i6$Ha{T zK<8gCa?Y%Ox6pW;vWUY_amr$(9-0W_ltrBFX7C(&99lOQ5ycOv0fGYhqb1zLGZ-=Z z=3e@jr9RlqSy&jzVchB$%YQ+E?QQ?1qZr)c%BrC=hp~C z+Tyt&d9DdUj9D+gkXl$ifm!hhv~7enw6=|~Kx?gyG;M?@a@KCR$B5z@qF$`qd^2zU zD{3Wd^xz)keOb559N4~#=ZMUJp8KABA33bsWs)bgBy14=$}eFXl6}@$rotl}fOd%K@jvy8fX9Sb=tpqL!t_1!ww6mK=ln zr4l1<-x-s`CF4c{hfBs`u9C+644+kEI}?<#Jscl{%&d~srFk^;!?Cf{YkAaU8rZR~ zakVUE4W}cPbed$vg+-(nZN}zO{~Hk)cYKf4u*S7l*-f?fgW8;58yo*gAd|Ow{i~)C=H~i z=U?L^hk;Z>iHLOu(Xlp9!z8}z9RtSJ$o}yZWQHS+3vQ>oQyct{k5tEZ)Azp&in^vY zBg>|~8#g#HM@Dr!ZblXDEjk(Zeux&S(~+^x7(LeZh?|dk2`W68X+MH9=ytcb0OsozFVHG0I;>%5})dYv+A_Mm3pEjI#$ zc4qn|KkfKm&7OoyK}Yo4E}F1Ct(GSdF1@VGOCW#R`Sd|Iw556}zSzp@K$E4}`p!-2 zcHD7;P^yP`c{{%#T#A89J9zbG-d&O)Lu^#pU9eYWTXn^*N0vw~g#Qpx1wW<^G7&EP ztNfnJncu_9LremBlEqGdKjk=Z#gHFQ_2y#_ASwcVxSohY_V6KmeH;&+e?&=6JcRr_ zdojPcoT*o^G0$a4TE$Z6c%~{i#MS^ljcn>5DG@# z#hc`y4GHE=bc_P>CUEBzh(Hl7Zix`3jJ(a8Kqmz2`(_3_4ESnGj}S9|7hkhsB!`j} zF2Lnb$d_mi<@EyB0vw8zp~ATD3C3CQM=#Kvimfmf*lkV)zdX;`f!4!GMajRIzY;j1 zg+X5~3*e+$4xE3x7&t9%B@d&U6_&06Q6=!E#MhA>kq^GT89C!_W#dL!e0ush^j_AY z9c{K~R_aCUrx}F6te2<9D^4IDDO-NHvlg;iyHtJAlyK# zgBxIJxPEtV1Nx-qwnfBNl{E z>Ww{#MZIr1jxBI$xcZ>K>txu;4Yl(Ax%!_Re6on7%BwV0{vGygT2{4ijd!vx2Jasg zMU4AEq+g886hxPQ&bce(7ut*)DKGFulD^muHG+&4Y6L!cI{Jt>=ZP)@fl1s*(yRdy zKNwKD$D-d7CVCy>kPtVhwz$%Nf_sf#kD6s2qM)e5xO*^sBy!4Sfa)3>s#7kL?;Txy z>oyJo0ypswJUVUxi~_rVF^t0RvYg8(`~mxbT&F%+5PPtxa(&%;%|Q4UktN1k60-Yu zP86Drh#0Szft7MlVLU})#>W83I379wxP9Z_)A%!Qz|(z;5#fa?!o!wa7zQhH$4e;0 zW1wdcIh+@0F$}IF;CkCd3c?K6Ax=92f_ad8WE_%W`VbeN5}R?*u4cCJg)KspFpxgc zVh~((-J1`t2OYTfA3wN6te0urBr8yM)LU=fTsCcK`iO{~Ak_mNsv4E4nHs`9)a7`N z@fr@C#l`~iJtvYESmet1y(1#X9^I}Lgc9yRxZi0EohLAw7606o(fxqW-^(dTDf41g zd&9|AdS`XAQsOpPPjcabWg6eZDuodirnha*o-T;8?Ff2H(B?FD8SttVpi~RoUI(Gu z8{s|IK0`T^$<}VcpE(V-79v~_Sb9m#Md-Zhw*ZAP425iTV#Qn(Zz5RfoA<~pXm{U_ zLvX9zRM*NZAUk`Z%mU5M(os@eR^=CB8CZJSZ)}w}AfQ&G^3=AO)0UnInZG)CC=RMe zi5`l3ggN8cj^Zv!6C)Uduq`RH1!6F?g^V1WR*{2?CffoLog7sJ=9m6aDTu(J5;C>R zDokUr-e@VA66TlIdAddK68Iq8QtuP^kb{6JFL{dR*fI*5_zST%oImY1wd*RFWy^I{ zv&}fxUfANK7$s?MubkB~TY%@Z3@wb~3=Fi)fQbW)eGm-IfI&kzB&`D*O);K3Qiv?m zi_xMZDB1`C*&4tFfe;)$8CeETq;+1$OggUGPM+FvRoh7}mln0R5Y~~1G{YbT1&K6M z5VNj63x3$J`$tCL?=>=z)SS@RAgkEFfNg;ov|&2b@pw%s_)-?NLG;M%0H&+8H`b>% zoKpOk-+04gdV@=HY*V#ki>w8wc)d8q1l(`6KB_*<1y82HChP%$!(jXn6oyeZ5MjJX zc>yD+x$TILz+{RW;8s?X^%7k&4_dVSPBg9k0N_^=3^@gO7jvUX8gcEHDx3HU@5(0L zIbs6T)m1gf=3Gd%;4+bFT*vR+&*W|FCSPXq z6(*lVk+F<}_|{89b)>MbS zk`o&;;SWUArvZzPSf^2%#9C0{H}F&z7?B3_4HMYW8xck}Y(}t*fC9~{x!ACX7@i3R z^38}WBA!~N_wbZFC#hpo=OCA|v~6+8qgMF^BE)HCjE5Yf_j)rX3kS}mc!~pdtcAl! zeqo|DnBa5K1KH3mN?^+RV0t6o>x3!iz*NI?9H|AE>hgW$fVOgyKYEz51{OHnponUL zhH0xA&|iNQ2wRT>+ZP2kBF3)>JQ^|f92;x~W!!ly@}cPQtj+0&S#4B`GpaTNRo9vsqrOLSWu?k{ zY<^hcW$a2q1b8Bu60o zb+|W(q$@0vF0SN1^vq0H!dj9pcV;$aW@2WmeKH%hHp!Wp5~W(N`8Ts<9RypwgC&_G zTLAKJV}&QBF0Q~2N?m#d_DUWzUVc4#J-MdTtnfriq-Ne25gJR_QZolj08^y!C-FbY zCQQIeiU(IgDoM70m@Ev2pcg$;KrlSUgy=($^rL6`@jHMX7|=KltimWDurbOqJ`pU( z_~6oFlpyvZ>FR%nYn^h& zIAMX__%nQ`vo}_IdgHAqk!%i0``#%qBab<-TJr6^@gbJBdgCK3@xQk>1UsJCxI|d6 zZ4%HT7(I&IF6j6y^)jNH-(%vzjbm+cK$Yqez4R`u(9dcH~ z0|MHDAN4nU9Yjx7%TsK5edoH1I$Qa1w(XNlgsc1lb3aGoZzKaovF#VRd@%eYXG)eZ z$~x%u3BJ4}YA5XI@8PxK*}4ttKiccrsQ!rM+o2Pk<2wGwM|+U0+A|Z7`#h55alHk`Hl*txdm% z@f?l~AUh+-iCYekQe5(2Lgkn8A|~mIoRaTg$vTLjdJms6hnDq7NfJ`N0ft$AK}h8nsc)kH?6&u+QVuS0;d-Dp1zg!<4}^z4ZvXcg*zKo zdHUjYY+VyF4z%t~0x|L!=T(w#?~4O0ZS}gMgpmS%&gXe=|AKo`xZ|DgNU3%0YS(t(l6Q+ zA&(1DK#TI_2%nGYt1F1r|BY0DbZ#szq9ye;-le`r{Q+{Vth}d{1&Z-H?cp#R+f$>5xMuX@OL8W`>4M=tz7}} zk1P8I{vw^Fz$XMO26H=CN?eC40Wcj-nR5)CUs=a?2-KH`&ZNHD(AhoUem$ORSJ;in z*=}>r0r!x5TMm`xbu3wji9eB`;}?;|bU&cgA+%ca98odv{u%1#fnaPk z&c<_i4!bQ3#j0EVae)HdhwvPPkQm!-^;cL@T1}V9pjQ8>)Wuf+xzyFxYT#`em1KmP z>xlGO%aso_-`_f#vP)Y)$zjY6UVswx#f~+oRTB_&j&`3tLMTCZ*)&aHL9NybyqYWN zlf19(D7~4b*?N@b_y@EfrC!QX6e6P3nAFqO2XgQDj=c0r=g0$^f5q22$KKlqdh~T4 z=<%Pg55CIQS$*(_Y)R+L$5(&AQdS@Q6-!w{RcH1uaUMVz00Sw_!D-Qmz25uhTkoLf zbT;jr18x}P2y`rj6}%+eQMvv_n}YdI*W0%FkNU1=X=#5%ZDomo_D9UySxVX;QFPGn zXusS$zH@*ABZ$3`B02|jNV4_;F%LzvXTUN(5st>&15XLGx(7ZXkZHk?+XD`UM15bA z(!deuuWw3eFTS9LNt=-u#4yX&Flh|d*RjTXAsh--Dbxz#XdUpc;7kiR%7?kZ=76+` z{OJOi{qd;DV+}h;+C7LZDgnRKP4K*rN7Y#ZIr74}b&hmYJIu>SN0m6tJ0s0A5wu%< z)Fk#74iI%}hk4Bno*?zVAb1|4G4vDdg}ZKmJ;DxRQ;3G zqlIs$O8y1uHQg(~kwbp!_;}2G7+;_J2+$y^Pv9%~qPFi}{HDbhXabm^sNeWI(s8VKX z@QxDw3JzNmt;2cZETu&xwpwMDc33XcT-1u(Ha)KTeR72&2ck@y1Mc|N0q8sWp}GU(GpnwYZuMx2PlmJhHSLJ=KFQdtt2AM zo8?`VR%BwPm6|YX(-c&Wcoh(o7Fv^#|gX}i98~IRUlZ*$ZgdMBQnhmDt zmrB(RuMxWveB^KNk!#goB5frnL>jrN)nm6wjMAZ67PXrzMRcTti`JgWzKC0*R`v$8 z@fIs!!e6t6gaBg3A29FA8o!Fd$}de+_we4gSnk_QT$S{n@lv)+u2AvIEX%qTZ!4{LhWAZ~LZGyxz7uT5&u>aqN>LF5uFfkK)M)Y>ig;#|B4rV_5{fyknM3|37 zQ{lw(`hF@L2@gIWiR7P)L}JgyA}Rbv@RyL^81Cuopl1oY{PU5plz%=Jw#%=u-Wz;Q z{R9p3ZL>Inj$ZwzcuF0e0pbp!CWA18jg`$-tV5_tMu`8&n7Hbl^tWV_1Gf6t3%h}|}+#}FYgh^)6KC^_ncJDu0e|J2$CTcF_W}@J;Uc(!>+!?yKhATT|4j2)J%v& zT>BQmAU|TErN~5DF|$&+cQ~v5nQ#6#!q`M$h1FWs~4qqSedDGWIVgImRUUpRXw`ZG9KhE3qIyBs-*I}2`?yLkzk09l0< zCmPCQl;okHI9wV}nL|k!x;Q0DEJ102GUR>aP=aBod~wF*8gI)lmL!~WDa}AIF*xVi zA-w$On&m7h236s7eYIls(m@w}B-V2b_W?GLoxOykNar%* zUi>XQM_?_q>`w%0YrGnAkl?6tg#%K zCc9PYpZV&~kc?&Jz9mflAH4l9Oxlp_kZ@AMe0c@MA18u63H?Dqu~x_0xg750<=*O$ z`k$y0Ta76YlX88xkG;-YNOwPe&3B;OK4Gp;2tA7U1c0@r`SZl-BajIgs={}e}8DW(70Nb;dWThwy_=( zkc~#Djssc|E+X&(Fzw){&V!LgkIW=`$d1%;Gz!<-s9v(Ym;pX|jBD9>(Kz{V4oN-q zh4_{JN@JZ)XH*TkJJ`$buXi zoyr7Rti{u%AqzdVod6Fhd*VZCnRL(yG&&E{he4we9UP0tLd6^3ug5162-X7^P{{9S zFr1{`Mm7eptuP;RVU;^osV0?cG_ocoo5aX*T zF(`R(YVr#%%hSdm%NuOtPvxoI#$}>;>NSRp<;`BvuNUgNjo>$&Hj*3d*_(IJSbSp8 z9!(z_^W6s;ThX|<8yeGcHWL9fEZ+zkG>f60HaZ(=m)6}5H;-oo6} z4z`@GkA&tz2Ou@t7E%RVQal8A<$5d0ko5S9jkYpkK-Px0)ZgMO|H!q%Iy0fhy6)>t z-gj}7EgMqvqE>YnAK+2E4m+d%jf+}XfG9rd;!wd8KXb6Bj@lK7J=-90v8Cw$Y1#<$ zz4I0p>RqO-5Fx=6+|=rIyGXhYQ9ABGWy-NBJmQUj30a_`^&P$?o}vN1pfGn38$ib} z(zT0(5#)#tGcCrAwnGVzhbNAg7#+4F$+f)kStbE8&EaM02z&IunEaB-ub2d3^u^k+ zlWo#rGU#!p;dw7A1%v3|cJ&;;66XjKzpLk&xETH(zF21RWhSFco@V0ue!5~gcQOdB zKY4*U_&1!dr=l_ow?|e)V$obU^L&4t7(O3uuh)iV4pFO0%fATKwi%GvI9!%He%=L zU~Mv?Il!)&aAyp#2i=I;<{1T)NXwVe``D#w2)m91N*8T7QThxYq)Qu(f{X?zk24yD z;L*T0KWeq1%Xpx~y@ZP60H`?c+%bV=pr-vO^&m@GJMH}}Wd+CXmwe#3;H`JbTe@zZ zq;QmDbGw#y#U?=G?KX2;Agj)I7u0m190yJjf*Rm9EE=m0k3&+`@=nC-EXtN)ciBXk zPDFI(*}o&1OcDRKGguKEi>o`U*h5IoQrCESa(tHd(6wpH(+F|D%H!j;Sw{V0bTB$1 zSEs9WMCy^6}+R2#hMdB29*v#!2?9%q&k; zq$XP{=R_I-h&s0Dh&)#l#8#&v5fz*rc1X7cWr;AsTF9ngWV97gJM_jOM)Z6;Uh{-= z7M>u%2T3MkKb~UFAv-F~OIK;O2g5KtC{(7EuGdQS-R#a*R_`A~II3U%IST4s#IG~A z6bY_sqd6XOgKzS99ocYkSD|tQPv{Msrgs$gFjr>sWhV2}8#YMu9YT48=xB9Lo2NGW zswDGCPJ+N*DvSUTHtk8p!h_*dco6%G3<436<~$Srfc*X_d`?6a0OvTMasF|eN#%;r zo`>*sU)T+oxIeNNTVyC#5E~8DMw|yJYlYAu3SB-z#%ReMVhn(in76_K1z}<2ydH=j z;2q3xowTh<$bMi0omHr^P@Cwg!W^(~Qu36i@g8s-EY0Z9=s!wWJLo_pAh^{IMVP?% z86GhP)rth~wIKO*)MY~gIdBIg58%0mWUqYnxIj|RftsapUKs3m1()a$!)YG>02u5e znRReP&{9w_9zvlH#&8|H{y< zojwbO53@7c00OPT@*WYng=}d$LmFbiYDGQwS@5n4l^`%e8n(k20Gr9~f6YPKRe zl96C7ztB^H(w#?S!v0DCfmop(kFJ?+)FL8XUu-|HxUPx8dAMMtLi_=8hGbkGdUa$a zWuPcDfdGwUbvHsbQi6}Py`_x)uTswKC5k8v;Lf`4<+!do-bu+6jV%y~Od^pI3WCx= z>A{!qnhU$R9%rGX2azBUgrEyTMm=@;GkT0(gJ2+fD1v(Ey_f3yeY4Yg2`S_-znz&q zGv}Lg`Q|%w&iR5|sh12;MIU--QU!l?uQIh?C7FI6E+#w8j_X97m9qyZ#Xep*JIr{p z`cHY#*O4U++T6sQ7*<&KF1it5R5fh+(% zNjd}I6c_{@paq-;D>;6U$Eqi`?XtA1|E?X4E`FXzl&$2s#5mevJ$zd3vL~Xg>RmDAa2|&hSVsJ8jVj ze$2a75QP`GZpcHw40QK8gtBan&Yz7l|oWt_Q;*ma@=EFb4WtF hgihIdO|z*t-R7|T+baJznl^LPG`LM}i`$*<`3+_*vUvai literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_proxy.cpython-310-pytest-8.4.1.pyc.25940 b/tests/__pycache__/test_proxy.cpython-310-pytest-8.4.1.pyc.25940 new file mode 100644 index 0000000000000000000000000000000000000000..275dbe09b175803fea2cd1fd0d32bd30a89219fa GIT binary patch literal 26816 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 literal 0 HcmV?d00001 From 425b921eacbdae0e2b830d076da2f09e9f3d3718 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 19 Jul 2025 18:13:37 -0500 Subject: [PATCH 16/18] Add __pycache__ to gitignore due to accidental __pycache__ leakage in tests folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0081b62ae7f..64b03ed8e88 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ sources var/* venv virtualenv.py +__pycache__ From a7190b0367f59148c72627738680c9592f2019ea Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 19 Jul 2025 18:19:07 -0500 Subject: [PATCH 17/18] mypy changes --- aiohttp/web_protocol.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index 5d63db24b87..fa1c381b84e 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -76,8 +76,8 @@ "UNKNOWN", "/", HttpVersion10, - CIMultiDictProxy(CIMultiDict()), # type: ignore[arg-type] - CIMultiDictProxy(CIMultiDict()), # type: ignore[arg-type] + CIMultiDictProxy(CIMultiDict()), + CIMultiDictProxy(CIMultiDict()), True, None, False, @@ -616,7 +616,7 @@ async def start(self) -> None: payload, self, writer, - self._task_handler or asyncio.current_task(loop), # type: ignore[arg-type] + self._task_handler or asyncio.current_task(loop), ) try: # a new task is used for copy context vars (#3406) From 610eec71e09a410d1b6fab65f0007f648cef6db9 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 22 Jul 2025 16:50:34 -0500 Subject: [PATCH 18/18] Remove 1 comment and optimize extend() function and add CIMultidictProxy to more arguments instead of object type --- aiohttp/_http_parser.pyx | 15 ++++++++------- aiohttp/_http_writer.pyx | 8 ++++---- aiohttp/web_protocol.py | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 1aa00310943..1bdc496ee60 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -93,14 +93,15 @@ cdef object StreamReader = _StreamReader cdef object DeflateBuffer = _DeflateBuffer cdef bytes EMPTY_BYTES = b"" -cdef inline object extend(object buf, const char* at, size_t length): +cdef inline int extend(object buf, const char* at, size_t length) except -1: cdef Py_ssize_t s cdef char* ptr s = PyByteArray_Size(buf) - PyByteArray_Resize(buf, s + length) + if PyByteArray_Resize(buf, s + length) < 0: + return -1 ptr = PyByteArray_AsString(buf) memcpy(ptr + s, at, length) - + return 0 DEF METHODS_COUNT = 46; cdef list _http_method = [] @@ -207,7 +208,7 @@ cdef class RawRequestMessage: cdef _new_request_message(str method, str path, object version, - object headers, + CIMultiDictProxy headers, object raw_headers, bint should_close, object compression, @@ -276,7 +277,7 @@ cdef class RawResponseMessage: cdef _new_response_message(object version, int code, str reason, - object headers, + CIMultiDictProxy headers, object raw_headers, bint should_close, object compression, @@ -629,7 +630,7 @@ cdef class HttpRequestParser(HttpParser): if self._cparser.method == cparser.HTTP_CONNECT: # authority-form, # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 - self._url = URL.build(authority=self._path, encoded=True) + self._url = URL_build(authority=self._path, encoded=True) elif idx3 > 1 and self._path[0] == '/': # origin-form, # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 @@ -655,7 +656,7 @@ cdef class HttpRequestParser(HttpParser): query = self._path[idx1: idx2] fragment = self._path[idx2+1:] - self._url = URL.build( + self._url = URL_build( path=path, query_string=query, fragment=fragment, diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx index 204eea7ff6b..c1e802c0126 100644 --- a/aiohttp/_http_writer.pyx +++ b/aiohttp/_http_writer.pyx @@ -57,7 +57,7 @@ cdef inline void _release_writer(Writer* writer) noexcept: if writer.buf != BUFFER: PyMem_Free(writer.buf) -# set to noexcept since we wish to handle the exceptions ourselves... + cdef inline int _write_byte(Writer* writer, uint8_t ch) except -1: cdef char * buf cdef Py_ssize_t size @@ -119,22 +119,22 @@ cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol) except -1: cdef inline int _write_str(Writer* writer, str s) except -1: cdef Py_UCS4 ch if not PyUnicode_Check(s): - PyErr_SetObject(ValueError, "Invalid status-line: {!r}") + PyErr_SetObject(ValueError, "Invalid status-line: {!r}".format(s)) return -1 for ch in s: if _write_utf8(writer, ch) < 0: return -1 return 0 -@cython.nonecheck(False) cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s) except -1: cdef Py_UCS4 ch cdef str out_str + if PyUnicode_CheckExact(s): out_str = s elif IStr_CheckExact(s): out_str = PyObject_Str(s) - elif not isinstance(s, str): + elif not PyUnicode_Check(s): PyErr_SetObject(TypeError, "Cannot serialize non-str key {!r}".format(s)) return -1 else: diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index fa1c381b84e..3d5e17bf5b3 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -77,7 +77,7 @@ "/", HttpVersion10, CIMultiDictProxy(CIMultiDict()), - CIMultiDictProxy(CIMultiDict()), + tuple(), True, None, False,