From 5f295ab55572f601ae3c73769caca9cb6f9999b8 Mon Sep 17 00:00:00 2001 From: Vytautas Liuolia Date: Wed, 27 Nov 2024 21:49:38 +0100 Subject: [PATCH 1/2] chore: implement initial support for CPython 3.14 (#2412) This patch implements rudimentary support for CPython 3.14, adapting our monkey-patching of Morsel attributes to a new optimization in 3.14 stdlib. In addition, the monkey patching of the SameSite attribute was removed, since it is no longer needed on 3.8+. --- .github/workflows/tests.yaml | 2 ++ docs/api/cookies.rst | 17 +++++------------ falcon/util/__init__.py | 22 ++++++++++++++-------- requirements/tests | 3 ++- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0a5106a6c..bfa969e8e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -59,6 +59,8 @@ jobs: python-version: "3.13" - env: py313_cython python-version: "3.13" + - env: py314 + python-version: "3.14.0-alpha.2 - 3.14.0" - env: py312_nocover os: macos-latest platform-label: ' (macos)' diff --git a/docs/api/cookies.rst b/docs/api/cookies.rst index 5089957cc..1b0a3e50e 100644 --- a/docs/api/cookies.rst +++ b/docs/api/cookies.rst @@ -155,13 +155,6 @@ at least set this attribute to ``'Lax'`` in order to mitigate Currently, :meth:`~falcon.Response.set_cookie` does not set `SameSite` by default, although this may change in a future release. -.. note:: - - The standard ``http.cookies`` module does not support the `SameSite` - attribute in versions prior to Python 3.8. Therefore, Falcon performs a - simple monkey-patch on the standard library module to backport this - feature for apps running on older Python versions. - .. _RFC 6265, Section 4.1.2.5: https://tools.ietf.org/html/rfc6265#section-4.1.2.5 @@ -172,7 +165,7 @@ by setting the 'samesite' kwarg. The Partitioned Attribute ~~~~~~~~~~~~~~~~~~~~~~~~~ -Starting from Q1 2024, Google Chrome will start to +Starting from Q1 2024, Google Chrome started to `phase out support for third-party cookies `__. If your site is relying on cross-site cookies, it might be necessary to set the @@ -187,7 +180,7 @@ automatically depending on other attributes (like ``SameSite``), although this may change in a future release. .. note:: - Similar to ``SameSite`` on older Python versions, the standard - :mod:`http.cookies` module does not support the ``Partitioned`` attribute - yet, and Falcon performs the same monkey-patching as it did for - ``SameSite``. + The standard :mod:`http.cookies` module does not support the `Partitioned` + attribute in versions prior to Python 3.14. Therefore, Falcon performs a + simple monkey-patch on the standard library module to backport this + feature for apps running on older Python versions. diff --git a/falcon/util/__init__.py b/falcon/util/__init__.py index a52a197cd..d2af7c58e 100644 --- a/falcon/util/__init__.py +++ b/falcon/util/__init__.py @@ -52,17 +52,23 @@ from falcon.util.sync import wrap_sync_to_async_unsafe from falcon.util.time import TimezoneGMT -# NOTE(kgriffs): Backport support for the new 'SameSite' attribute -# for Python versions prior to 3.8. We do it this way because -# SimpleCookie does not give us a simple way to specify our own -# subclass of Morsel. +# NOTE(kgriffs, m-mueller): Monkey-patch support for the new 'Partitioned' +# attribute that will probably be added in Python 3.14. +# We do it this way because SimpleCookie does not give us a simple way to +# specify our own subclass of Morsel. _reserved_cookie_attrs = http_cookies.Morsel._reserved # type: ignore -if 'samesite' not in _reserved_cookie_attrs: # pragma: no cover - _reserved_cookie_attrs['samesite'] = 'SameSite' -# NOTE(m-mueller): Same for the 'partitioned' attribute that will -# probably be added in Python 3.13 or 3.14. if 'partitioned' not in _reserved_cookie_attrs: # pragma: no cover _reserved_cookie_attrs['partitioned'] = 'Partitioned' +# NOTE(vytas): Morsel._reserved_defaults is a new optimization in Python 3.14+ +# for faster initialization of Morsel instances. +# TODO(vytas): Remove this part of monkey-patching in the case CPython 3.14 +# adds the Partitioned attribute to Morsel. +_reserved_cookie_defaults = getattr(http_cookies.Morsel, '_reserved_defaults', None) +if ( + _reserved_cookie_defaults is not None + and 'partitioned' not in _reserved_cookie_defaults +): # pragma: no cover + _reserved_cookie_defaults['partitioned'] = '' IS_64_BITS = sys.maxsize > 2**32 diff --git a/requirements/tests b/requirements/tests index 36825fd23..6f6788592 100644 --- a/requirements/tests +++ b/requirements/tests @@ -26,4 +26,5 @@ ujson python-rapidjson; platform_python_implementation != 'PyPy' and platform_machine != 's390x' and platform_machine != 'aarch64' # wheels are missing some EoL interpreters and non-x86 platforms; build would fail unless rust is available -orjson; platform_python_implementation != 'PyPy' and platform_machine != 's390x' and platform_machine != 'aarch64' +# (not available for CPython 3.14 either yet) +orjson; python_version < '3.14' and platform_python_implementation != 'PyPy' and platform_machine != 's390x' and platform_machine != 'aarch64' From 11076b8fc78bb61707c271953783198f4b6a04f1 Mon Sep 17 00:00:00 2001 From: Agustin Arce <59893355+aarcex3@users.noreply.github.com> Date: Sun, 8 Dec 2024 18:12:29 -0300 Subject: [PATCH 2/2] feat(testing): add `raw_path` to ASGI scope (#2331) * style (testing): fix errors in gates * docs (newsfragment): add newsfragment * refactor (testing): refactor asgi test * test (test_recipes): update test for asgi scope * docs(testing): clean up the docs, tests and notes before merging * docs(testing): add a versionadded note, improve impl --------- Co-authored-by: Vytautas Liuolia --- docs/_newsfragments/2262.newandimproved.rst | 5 +++++ falcon/testing/helpers.py | 9 ++++++++- tests/asgi/test_testing_asgi.py | 18 +++++++++++++++++ tests/test_recipes.py | 22 ++++++++------------- 4 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 docs/_newsfragments/2262.newandimproved.rst diff --git a/docs/_newsfragments/2262.newandimproved.rst b/docs/_newsfragments/2262.newandimproved.rst new file mode 100644 index 000000000..374c5b642 --- /dev/null +++ b/docs/_newsfragments/2262.newandimproved.rst @@ -0,0 +1,5 @@ +Similar to :func:`~falcon.testing.create_environ`, +the :func:`~falcon.testing.create_scope` testing helper now preserves the raw URI path, +and propagates it to the created ASGI connection scope as the ``raw_path`` byte string +(according to the `ASGI specification +`__). diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index 97392d57a..0fc95bc8f 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -974,10 +974,16 @@ def create_scope( iterable yielding a series of two-member (*name*, *value*) iterables. Each pair of items provides the name and value for the 'Set-Cookie' header. + + .. versionadded:: 4.1 + The raw (i.e., not URL-decoded) version of the provided `path` is now + preserved in the returned scope as the ``raw_path`` byte string. + According to the ASGI specification, ``raw_path`` **does not include** + any query string. """ http_version = _fixup_http_version(http_version) - + raw_path, _, _ = path.partition('?') path = uri.decode(path, unquote_plus=False) # NOTE(kgriffs): Handles both None and '' @@ -995,6 +1001,7 @@ def create_scope( 'http_version': http_version, 'method': method.upper(), 'path': path, + 'raw_path': raw_path.encode(), 'query_string': query_string_bytes, } diff --git a/tests/asgi/test_testing_asgi.py b/tests/asgi/test_testing_asgi.py index 8ec041361..67db94cc2 100644 --- a/tests/asgi/test_testing_asgi.py +++ b/tests/asgi/test_testing_asgi.py @@ -153,3 +153,21 @@ def test_immediate_disconnect(): with pytest.raises(ConnectionError): client.simulate_get('/', asgi_disconnect_ttl=0) + + +@pytest.mark.parametrize( + 'path, expected', + [ + ('/cache/http%3A%2F%2Ffalconframework.org/status', True), + ( + '/cache/http%3A%2F%2Ffalconframework.org/status?param1=value1¶m2=value2', + False, + ), + ], +) +def test_create_scope_preserve_raw_path(path, expected): + scope = testing.create_scope(path=path) + if expected: + assert scope['raw_path'] == path.encode() + else: + assert scope['raw_path'] != path.encode() diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 9b2160087..be2e9af9e 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -115,29 +115,23 @@ def test_optional_indent(self, util): class TestRawURLPath: - def path_extras(self, asgi, url): - if asgi: - return {'raw_path': url.encode()} - return None - def test_raw_path(self, asgi, app_kind, util): recipe = util.load_module( 'raw_url_path', parent_dir='examples/recipes', suffix=app_kind ) - # TODO(vytas): Improve TestClient to automatically add ASGI raw_path - # (as it does for WSGI): GH #2262. - url1 = '/cache/http%3A%2F%2Ffalconframework.org' - result1 = falcon.testing.simulate_get( - recipe.app, url1, extras=self.path_extras(asgi, url1) - ) + result1 = falcon.testing.simulate_get(recipe.app, url1) assert result1.status_code == 200 assert result1.json == {'url': 'http://falconframework.org'} + scope1 = falcon.testing.create_scope(url1) + assert scope1['raw_path'] == url1.encode() + url2 = '/cache/http%3A%2F%2Ffalconframework.org/status' - result2 = falcon.testing.simulate_get( - recipe.app, url2, extras=self.path_extras(asgi, url2) - ) + result2 = falcon.testing.simulate_get(recipe.app, url2) assert result2.status_code == 200 assert result2.json == {'cached': True} + + scope2 = falcon.testing.create_scope(url2) + assert scope2['raw_path'] == url2.encode()