From b701f498802bea3ef5d61fe2939a800350baddc3 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Sun, 1 Sep 2024 22:17:22 +0000 Subject: [PATCH 01/16] refactor (response): Remove deprecated attributes --- falcon/response.py | 16 ---------------- tests/test_response.py | 31 +++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/falcon/response.py b/falcon/response.py index bc676c31a..6211e569c 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -41,10 +41,6 @@ from falcon.util.uri import encode_check_escaped as uri_encode from falcon.util.uri import encode_value_check_escaped as uri_encode_value -_STREAM_LEN_REMOVED_MSG = ( - 'The deprecated stream_len property was removed in Falcon 3.0. ' - 'Please use Response.set_stream() or Response.content_length instead.' -) _RESERVED_CROSSORIGIN_VALUES = frozenset({'anonymous', 'use-credentials'}) @@ -243,18 +239,6 @@ def media(self, value): self._media = value self._media_rendered = _UNSET - @property - def stream_len(self): - # NOTE(kgriffs): Provide some additional information by raising the - # error explicitly. - raise AttributeError(_STREAM_LEN_REMOVED_MSG) - - @stream_len.setter - def stream_len(self, value): - # NOTE(kgriffs): We explicitly disallow setting the deprecated attribute - # so that apps relying on it do not fail silently. - raise AttributeError(_STREAM_LEN_REMOVED_MSG) - def render_body(self): """Get the raw bytestring content for the response body. diff --git a/tests/test_response.py b/tests/test_response.py index aa349ee2c..0fafa383c 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,3 +1,4 @@ +from io import BytesIO from unittest.mock import MagicMock import pytest @@ -53,14 +54,6 @@ def test_response_attempt_to_set_read_only_headers(resp): assert headers['x-things3'] == 'thing-3a, thing-3b' -def test_response_removed_stream_len(resp): - with pytest.raises(AttributeError): - resp.stream_len = 128 - - with pytest.raises(AttributeError): - resp.stream_len - - def test_response_option_mimetype_init(monkeypatch): mock = MagicMock() mock.inited = False @@ -81,3 +74,25 @@ def test_response_option_mimetype_init(monkeypatch): assert ro.static_media_types['.js'] == 'text/javascript' assert ro.static_media_types['.json'] == 'application/json' assert ro.static_media_types['.mjs'] == 'text/javascript' + + +def test_response_set_stream(resp): + stream = BytesIO(b'dummy content') + content_length = 12 + + resp.set_stream(stream, content_length) + + assert resp.stream == stream + + assert resp._headers['content-length'] == str(content_length) + + +def test_response_set_stream_with_zero_content_length(resp): + stream = BytesIO(b'') + content_length = 0 + + resp.set_stream(stream, content_length) + + assert resp.stream == stream + + assert resp._headers['content-length'] == str(content_length) From e2f4e98f202bfa228a7e6bcbe6edbf62ed15e9c7 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Mon, 2 Sep 2024 14:44:09 +0000 Subject: [PATCH 02/16] refactor (response): Organize imports with ruff check --fix --- falcon/response.py | 1 - 1 file changed, 1 deletion(-) diff --git a/falcon/response.py b/falcon/response.py index 6211e569c..9446e662d 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -41,7 +41,6 @@ from falcon.util.uri import encode_check_escaped as uri_encode from falcon.util.uri import encode_value_check_escaped as uri_encode_value - _RESERVED_CROSSORIGIN_VALUES = frozenset({'anonymous', 'use-credentials'}) _RESERVED_SAMESITE_VALUES = frozenset({'lax', 'strict', 'none'}) From da19b965cf1baa4dde776c7d968ca4fde74c79a3 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Mon, 2 Sep 2024 14:50:03 +0000 Subject: [PATCH 03/16] test (response): Refactor test_response_set_stream test --- tests/test_response.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/tests/test_response.py b/tests/test_response.py index 0fafa383c..4816a7db4 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -76,23 +76,12 @@ def test_response_option_mimetype_init(monkeypatch): assert ro.static_media_types['.mjs'] == 'text/javascript' -def test_response_set_stream(resp): - stream = BytesIO(b'dummy content') - content_length = 12 +@pytest.mark.parametrize('content', [b'', b'dummy content']) +def test_response_set_stream(resp, content): + stream = BytesIO(content) + content_length = len(content) resp.set_stream(stream, content_length) assert resp.stream == stream - - assert resp._headers['content-length'] == str(content_length) - - -def test_response_set_stream_with_zero_content_length(resp): - stream = BytesIO(b'') - content_length = 0 - - resp.set_stream(stream, content_length) - - assert resp.stream == stream - assert resp._headers['content-length'] == str(content_length) From 3793c82a2d1e9ffed6d2ef81fc7bd36701b13001 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Mon, 2 Sep 2024 15:25:25 +0000 Subject: [PATCH 04/16] test (response): Fix set stream test --- tests/test_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_response.py b/tests/test_response.py index 4816a7db4..7a68d1ed5 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -84,4 +84,4 @@ def test_response_set_stream(resp, content): resp.set_stream(stream, content_length) assert resp.stream == stream - assert resp._headers['content-length'] == str(content_length) + assert resp.headers['content-length'] == str(content_length) From 754cf1f92d78645b43aa77befb54cc6565aaf841 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Mon, 2 Sep 2024 15:41:49 +0000 Subject: [PATCH 05/16] test (response): Improve assertion --- tests/test_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_response.py b/tests/test_response.py index 7a68d1ed5..7fac6b006 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -83,5 +83,5 @@ def test_response_set_stream(resp, content): resp.set_stream(stream, content_length) - assert resp.stream == stream + assert resp.stream is stream assert resp.headers['content-length'] == str(content_length) From c533f043c1361c56822c297f418088a1dff89daa Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Thu, 12 Sep 2024 23:40:07 +0000 Subject: [PATCH 06/16] feat (status_codes): add new status codes --- falcon/__init__.py | 20 ++++++++++++++++++++ falcon/status_codes.py | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/falcon/__init__.py b/falcon/__init__.py index b9976b643..1159ed5a5 100644 --- a/falcon/__init__.py +++ b/falcon/__init__.py @@ -154,6 +154,7 @@ 'HTTP_100', 'HTTP_101', 'HTTP_102', + 'HTTP_103', 'HTTP_200', 'HTTP_201', 'HTTP_202', @@ -191,9 +192,11 @@ 'HTTP_416', 'HTTP_417', 'HTTP_418', + 'HTTP_421' 'HTTP_422', 'HTTP_423', 'HTTP_424', + 'HTTP_425' 'HTTP_426', 'HTTP_428', 'HTTP_429', @@ -205,8 +208,10 @@ 'HTTP_503', 'HTTP_504', 'HTTP_505', + 'HTTP_506' 'HTTP_507', 'HTTP_508', + 'HTTP_510' 'HTTP_511', 'HTTP_701', 'HTTP_702', @@ -262,6 +267,7 @@ 'HTTP_CONFLICT', 'HTTP_CONTINUE', 'HTTP_CREATED', + 'HTTP_EARLY_HINTS' 'HTTP_EXPECTATION_FAILED', 'HTTP_FAILED_DEPENDENCY', 'HTTP_FORBIDDEN', @@ -277,12 +283,14 @@ 'HTTP_LOCKED', 'HTTP_LOOP_DETECTED', 'HTTP_METHOD_NOT_ALLOWED', + 'HTTP_MISDIRECTED_REQUEST' 'HTTP_MOVED_PERMANENTLY', 'HTTP_MULTIPLE_CHOICES', 'HTTP_MULTI_STATUS', 'HTTP_NETWORK_AUTHENTICATION_REQUIRED', 'HTTP_NON_AUTHORITATIVE_INFORMATION', 'HTTP_NOT_ACCEPTABLE', + 'HTTP_NOT_EXTENDED' 'HTTP_NOT_FOUND', 'HTTP_NOT_IMPLEMENTED', 'HTTP_NOT_MODIFIED', @@ -305,6 +313,7 @@ 'HTTP_SERVICE_UNAVAILABLE', 'HTTP_SWITCHING_PROTOCOLS', 'HTTP_TEMPORARY_REDIRECT', + 'HTTP_TOO_EARLY' 'HTTP_TOO_MANY_REQUESTS', 'HTTP_UNAUTHORIZED', 'HTTP_UNAVAILABLE_FOR_LEGAL_REASONS', @@ -312,6 +321,7 @@ 'HTTP_UNSUPPORTED_MEDIA_TYPE', 'HTTP_UPGRADE_REQUIRED', 'HTTP_USE_PROXY', + 'HTTP_VARIANT_ALSO_NEGOTIATE' ) # NOTE(kgriffs,vytas): Hoist classes and functions into the falcon namespace. @@ -409,6 +419,7 @@ from falcon.status_codes import HTTP_100 from falcon.status_codes import HTTP_101 from falcon.status_codes import HTTP_102 +from falcon.status_codes import HTTP_103 from falcon.status_codes import HTTP_200 from falcon.status_codes import HTTP_201 from falcon.status_codes import HTTP_202 @@ -446,9 +457,11 @@ from falcon.status_codes import HTTP_416 from falcon.status_codes import HTTP_417 from falcon.status_codes import HTTP_418 +from falcon.status_codes import HTTP_421 from falcon.status_codes import HTTP_422 from falcon.status_codes import HTTP_423 from falcon.status_codes import HTTP_424 +from falcon.status_codes import HTTP_425 from falcon.status_codes import HTTP_426 from falcon.status_codes import HTTP_428 from falcon.status_codes import HTTP_429 @@ -460,8 +473,10 @@ from falcon.status_codes import HTTP_503 from falcon.status_codes import HTTP_504 from falcon.status_codes import HTTP_505 +from falcon.status_codes import HTTP_506 from falcon.status_codes import HTTP_507 from falcon.status_codes import HTTP_508 +from falcon.status_codes import HTTP_510 from falcon.status_codes import HTTP_511 from falcon.status_codes import HTTP_701 from falcon.status_codes import HTTP_702 @@ -517,6 +532,7 @@ from falcon.status_codes import HTTP_CONFLICT from falcon.status_codes import HTTP_CONTINUE from falcon.status_codes import HTTP_CREATED +from falcon.status_codes import HTTP_EARLY_HINTS from falcon.status_codes import HTTP_EXPECTATION_FAILED from falcon.status_codes import HTTP_FAILED_DEPENDENCY from falcon.status_codes import HTTP_FORBIDDEN @@ -532,6 +548,7 @@ from falcon.status_codes import HTTP_LOCKED from falcon.status_codes import HTTP_LOOP_DETECTED from falcon.status_codes import HTTP_METHOD_NOT_ALLOWED +from falcon.status_codes import HTTP_MISDIRECTED_REQUEST from falcon.status_codes import HTTP_MOVED_PERMANENTLY from falcon.status_codes import HTTP_MULTI_STATUS from falcon.status_codes import HTTP_MULTIPLE_CHOICES @@ -539,6 +556,7 @@ from falcon.status_codes import HTTP_NO_CONTENT from falcon.status_codes import HTTP_NON_AUTHORITATIVE_INFORMATION from falcon.status_codes import HTTP_NOT_ACCEPTABLE +from falcon.status_codes import HTTP_NOT_EXTENDED from falcon.status_codes import HTTP_NOT_FOUND from falcon.status_codes import HTTP_NOT_IMPLEMENTED from falcon.status_codes import HTTP_NOT_MODIFIED @@ -560,6 +578,7 @@ from falcon.status_codes import HTTP_SERVICE_UNAVAILABLE from falcon.status_codes import HTTP_SWITCHING_PROTOCOLS from falcon.status_codes import HTTP_TEMPORARY_REDIRECT +from falcon.status_codes import HTTP_TOO_EARLY from falcon.status_codes import HTTP_TOO_MANY_REQUESTS from falcon.status_codes import HTTP_UNAUTHORIZED from falcon.status_codes import HTTP_UNAVAILABLE_FOR_LEGAL_REASONS @@ -567,6 +586,7 @@ from falcon.status_codes import HTTP_UNSUPPORTED_MEDIA_TYPE from falcon.status_codes import HTTP_UPGRADE_REQUIRED from falcon.status_codes import HTTP_USE_PROXY +from falcon.status_codes import HTTP_VARIANT_ALSO_NEGOTIATE from falcon.stream import BoundedStream # NOTE(kgriffs): Ensure that "from falcon import uri" will import diff --git a/falcon/status_codes.py b/falcon/status_codes.py index 80c0b5f82..1ca7e1a12 100644 --- a/falcon/status_codes.py +++ b/falcon/status_codes.py @@ -23,6 +23,8 @@ HTTP_SWITCHING_PROTOCOLS: Final[str] = HTTP_101 HTTP_102: Final[str] = '102 Processing' HTTP_PROCESSING: Final[str] = HTTP_102 +HTTP_103: Final[str] = "103 Early Hints" +HTTP_EARLY_HINTS: Final[str] = HTTP_103 # 2xx - Success HTTP_200: Final[str] = '200 OK' @@ -103,12 +105,16 @@ HTTP_EXPECTATION_FAILED: Final[str] = HTTP_417 HTTP_418: Final[str] = "418 I'm a teapot" HTTP_IM_A_TEAPOT: Final[str] = HTTP_418 +HTTP_421: Final[str] = '421 Misdirected Request' +HTTP_MISDIRECTED_REQUEST: Final[str] = HTTP_421 HTTP_422: Final[str] = '422 Unprocessable Entity' HTTP_UNPROCESSABLE_ENTITY: Final[str] = HTTP_422 HTTP_423: Final[str] = '423 Locked' HTTP_LOCKED: Final[str] = HTTP_423 HTTP_424: Final[str] = '424 Failed Dependency' HTTP_FAILED_DEPENDENCY: Final[str] = HTTP_424 +HTTP_425: Final[str] = "425 Too Early" +HTTP_TOO_EARLY: Final[str] = HTTP_425 HTTP_426: Final[str] = '426 Upgrade Required' HTTP_UPGRADE_REQUIRED: Final[str] = HTTP_426 HTTP_428: Final[str] = '428 Precondition Required' @@ -133,10 +139,14 @@ HTTP_GATEWAY_TIMEOUT: Final[str] = HTTP_504 HTTP_505: Final[str] = '505 HTTP Version Not Supported' HTTP_HTTP_VERSION_NOT_SUPPORTED: Final[str] = HTTP_505 +HTTP_506: Final[str] = "506 Variant Also Negotiate" +HTTP_VARIANT_ALSO_NEGOTIATE: Final[str] = HTTP_506 HTTP_507: Final[str] = '507 Insufficient Storage' HTTP_INSUFFICIENT_STORAGE: Final[str] = HTTP_507 HTTP_508: Final[str] = '508 Loop Detected' HTTP_LOOP_DETECTED: Final[str] = HTTP_508 +HTTP_510: Final[str] = "510 Not Extended" +HTTP_NOT_EXTENDED: Final[str] = HTTP_510 HTTP_511: Final[str] = '511 Network Authentication Required' HTTP_NETWORK_AUTHENTICATION_REQUIRED: Final[str] = HTTP_511 @@ -209,6 +219,7 @@ 'HTTP_100', 'HTTP_101', 'HTTP_102', + 'HTTP_103', 'HTTP_200', 'HTTP_201', 'HTTP_202', @@ -246,9 +257,11 @@ 'HTTP_416', 'HTTP_417', 'HTTP_418', + 'HTTP_421' 'HTTP_422', 'HTTP_423', 'HTTP_424', + 'HTTP_425' 'HTTP_426', 'HTTP_428', 'HTTP_429', @@ -260,8 +273,10 @@ 'HTTP_503', 'HTTP_504', 'HTTP_505', + 'HTTP_506' 'HTTP_507', 'HTTP_508', + 'HTTP_510' 'HTTP_511', 'HTTP_701', 'HTTP_702', @@ -317,6 +332,7 @@ 'HTTP_CONFLICT', 'HTTP_CONTINUE', 'HTTP_CREATED', + 'HTTP_EARLY_HINTS' 'HTTP_EXPECTATION_FAILED', 'HTTP_FAILED_DEPENDENCY', 'HTTP_FORBIDDEN', @@ -332,12 +348,14 @@ 'HTTP_LOCKED', 'HTTP_LOOP_DETECTED', 'HTTP_METHOD_NOT_ALLOWED', + 'HTTP_MISDIRECTED_REQUEST' 'HTTP_MOVED_PERMANENTLY', 'HTTP_MULTIPLE_CHOICES', 'HTTP_MULTI_STATUS', 'HTTP_NETWORK_AUTHENTICATION_REQUIRED', 'HTTP_NON_AUTHORITATIVE_INFORMATION', 'HTTP_NOT_ACCEPTABLE', + 'HTTP_NOT_EXTENDED' 'HTTP_NOT_FOUND', 'HTTP_NOT_IMPLEMENTED', 'HTTP_NOT_MODIFIED', @@ -360,6 +378,7 @@ 'HTTP_SERVICE_UNAVAILABLE', 'HTTP_SWITCHING_PROTOCOLS', 'HTTP_TEMPORARY_REDIRECT', + 'HTTP_TOO_EARLY' 'HTTP_TOO_MANY_REQUESTS', 'HTTP_UNAUTHORIZED', 'HTTP_UNAVAILABLE_FOR_LEGAL_REASONS', @@ -367,4 +386,5 @@ 'HTTP_UNSUPPORTED_MEDIA_TYPE', 'HTTP_UPGRADE_REQUIRED', 'HTTP_USE_PROXY', + 'HTTP_VARIANT_ALSO_NEGOTIATE' ) From d85f6168ab8f7fe948a5ccac633dc6a29d16efb6 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Thu, 12 Sep 2024 23:53:26 +0000 Subject: [PATCH 07/16] style: run ruff format --- falcon/__init__.py | 18 +++++++++--------- falcon/status_codes.py | 26 +++++++++++++------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/falcon/__init__.py b/falcon/__init__.py index 1159ed5a5..4c8101e8e 100644 --- a/falcon/__init__.py +++ b/falcon/__init__.py @@ -192,11 +192,11 @@ 'HTTP_416', 'HTTP_417', 'HTTP_418', - 'HTTP_421' + 'HTTP_421', 'HTTP_422', 'HTTP_423', 'HTTP_424', - 'HTTP_425' + 'HTTP_425', 'HTTP_426', 'HTTP_428', 'HTTP_429', @@ -208,10 +208,10 @@ 'HTTP_503', 'HTTP_504', 'HTTP_505', - 'HTTP_506' + 'HTTP_506', 'HTTP_507', 'HTTP_508', - 'HTTP_510' + 'HTTP_510', 'HTTP_511', 'HTTP_701', 'HTTP_702', @@ -267,7 +267,7 @@ 'HTTP_CONFLICT', 'HTTP_CONTINUE', 'HTTP_CREATED', - 'HTTP_EARLY_HINTS' + 'HTTP_EARLY_HINTS', 'HTTP_EXPECTATION_FAILED', 'HTTP_FAILED_DEPENDENCY', 'HTTP_FORBIDDEN', @@ -283,14 +283,14 @@ 'HTTP_LOCKED', 'HTTP_LOOP_DETECTED', 'HTTP_METHOD_NOT_ALLOWED', - 'HTTP_MISDIRECTED_REQUEST' + 'HTTP_MISDIRECTED_REQUEST', 'HTTP_MOVED_PERMANENTLY', 'HTTP_MULTIPLE_CHOICES', 'HTTP_MULTI_STATUS', 'HTTP_NETWORK_AUTHENTICATION_REQUIRED', 'HTTP_NON_AUTHORITATIVE_INFORMATION', 'HTTP_NOT_ACCEPTABLE', - 'HTTP_NOT_EXTENDED' + 'HTTP_NOT_EXTENDED', 'HTTP_NOT_FOUND', 'HTTP_NOT_IMPLEMENTED', 'HTTP_NOT_MODIFIED', @@ -313,7 +313,7 @@ 'HTTP_SERVICE_UNAVAILABLE', 'HTTP_SWITCHING_PROTOCOLS', 'HTTP_TEMPORARY_REDIRECT', - 'HTTP_TOO_EARLY' + 'HTTP_TOO_EARLY', 'HTTP_TOO_MANY_REQUESTS', 'HTTP_UNAUTHORIZED', 'HTTP_UNAVAILABLE_FOR_LEGAL_REASONS', @@ -321,7 +321,7 @@ 'HTTP_UNSUPPORTED_MEDIA_TYPE', 'HTTP_UPGRADE_REQUIRED', 'HTTP_USE_PROXY', - 'HTTP_VARIANT_ALSO_NEGOTIATE' + 'HTTP_VARIANT_ALSO_NEGOTIATE', ) # NOTE(kgriffs,vytas): Hoist classes and functions into the falcon namespace. diff --git a/falcon/status_codes.py b/falcon/status_codes.py index 1ca7e1a12..e89b3d9ea 100644 --- a/falcon/status_codes.py +++ b/falcon/status_codes.py @@ -23,7 +23,7 @@ HTTP_SWITCHING_PROTOCOLS: Final[str] = HTTP_101 HTTP_102: Final[str] = '102 Processing' HTTP_PROCESSING: Final[str] = HTTP_102 -HTTP_103: Final[str] = "103 Early Hints" +HTTP_103: Final[str] = '103 Early Hints' HTTP_EARLY_HINTS: Final[str] = HTTP_103 # 2xx - Success @@ -113,7 +113,7 @@ HTTP_LOCKED: Final[str] = HTTP_423 HTTP_424: Final[str] = '424 Failed Dependency' HTTP_FAILED_DEPENDENCY: Final[str] = HTTP_424 -HTTP_425: Final[str] = "425 Too Early" +HTTP_425: Final[str] = '425 Too Early' HTTP_TOO_EARLY: Final[str] = HTTP_425 HTTP_426: Final[str] = '426 Upgrade Required' HTTP_UPGRADE_REQUIRED: Final[str] = HTTP_426 @@ -139,13 +139,13 @@ HTTP_GATEWAY_TIMEOUT: Final[str] = HTTP_504 HTTP_505: Final[str] = '505 HTTP Version Not Supported' HTTP_HTTP_VERSION_NOT_SUPPORTED: Final[str] = HTTP_505 -HTTP_506: Final[str] = "506 Variant Also Negotiate" +HTTP_506: Final[str] = '506 Variant Also Negotiate' HTTP_VARIANT_ALSO_NEGOTIATE: Final[str] = HTTP_506 HTTP_507: Final[str] = '507 Insufficient Storage' HTTP_INSUFFICIENT_STORAGE: Final[str] = HTTP_507 HTTP_508: Final[str] = '508 Loop Detected' HTTP_LOOP_DETECTED: Final[str] = HTTP_508 -HTTP_510: Final[str] = "510 Not Extended" +HTTP_510: Final[str] = '510 Not Extended' HTTP_NOT_EXTENDED: Final[str] = HTTP_510 HTTP_511: Final[str] = '511 Network Authentication Required' HTTP_NETWORK_AUTHENTICATION_REQUIRED: Final[str] = HTTP_511 @@ -257,11 +257,11 @@ 'HTTP_416', 'HTTP_417', 'HTTP_418', - 'HTTP_421' + 'HTTP_421', 'HTTP_422', 'HTTP_423', 'HTTP_424', - 'HTTP_425' + 'HTTP_425', 'HTTP_426', 'HTTP_428', 'HTTP_429', @@ -273,10 +273,10 @@ 'HTTP_503', 'HTTP_504', 'HTTP_505', - 'HTTP_506' + 'HTTP_506', 'HTTP_507', 'HTTP_508', - 'HTTP_510' + 'HTTP_510', 'HTTP_511', 'HTTP_701', 'HTTP_702', @@ -332,7 +332,7 @@ 'HTTP_CONFLICT', 'HTTP_CONTINUE', 'HTTP_CREATED', - 'HTTP_EARLY_HINTS' + 'HTTP_EARLY_HINTS', 'HTTP_EXPECTATION_FAILED', 'HTTP_FAILED_DEPENDENCY', 'HTTP_FORBIDDEN', @@ -348,14 +348,14 @@ 'HTTP_LOCKED', 'HTTP_LOOP_DETECTED', 'HTTP_METHOD_NOT_ALLOWED', - 'HTTP_MISDIRECTED_REQUEST' + 'HTTP_MISDIRECTED_REQUEST', 'HTTP_MOVED_PERMANENTLY', 'HTTP_MULTIPLE_CHOICES', 'HTTP_MULTI_STATUS', 'HTTP_NETWORK_AUTHENTICATION_REQUIRED', 'HTTP_NON_AUTHORITATIVE_INFORMATION', 'HTTP_NOT_ACCEPTABLE', - 'HTTP_NOT_EXTENDED' + 'HTTP_NOT_EXTENDED', 'HTTP_NOT_FOUND', 'HTTP_NOT_IMPLEMENTED', 'HTTP_NOT_MODIFIED', @@ -378,7 +378,7 @@ 'HTTP_SERVICE_UNAVAILABLE', 'HTTP_SWITCHING_PROTOCOLS', 'HTTP_TEMPORARY_REDIRECT', - 'HTTP_TOO_EARLY' + 'HTTP_TOO_EARLY', 'HTTP_TOO_MANY_REQUESTS', 'HTTP_UNAUTHORIZED', 'HTTP_UNAVAILABLE_FOR_LEGAL_REASONS', @@ -386,5 +386,5 @@ 'HTTP_UNSUPPORTED_MEDIA_TYPE', 'HTTP_UPGRADE_REQUIRED', 'HTTP_USE_PROXY', - 'HTTP_VARIANT_ALSO_NEGOTIATE' + 'HTTP_VARIANT_ALSO_NEGOTIATE', ) From 1b9ede9620848bf5e72cf084c3567d5c3ea0dd36 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Fri, 13 Sep 2024 00:09:29 +0000 Subject: [PATCH 08/16] docs (status): update docs --- docs/api/status.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api/status.rst b/docs/api/status.rst index c48d7c918..7d8b7f044 100644 --- a/docs/api/status.rst +++ b/docs/api/status.rst @@ -66,10 +66,12 @@ HTTPStatus HTTP_CONTINUE = HTTP_100 HTTP_SWITCHING_PROTOCOLS = HTTP_101 HTTP_PROCESSING = HTTP_102 + HTTP_EARLY_HINTS = HTTP_103 HTTP_100 = '100 Continue' HTTP_101 = '101 Switching Protocols' HTTP_102 = '102 Processing' + HTTP_103 = '103 Early Hints' 2xx Success ----------- @@ -145,6 +147,7 @@ HTTPStatus HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = HTTP_416 HTTP_EXPECTATION_FAILED = HTTP_417 HTTP_IM_A_TEAPOT = HTTP_418 + HTTP_MISDIRECTED_REQUEST = HTTP_421 HTTP_UNPROCESSABLE_ENTITY = HTTP_422 HTTP_LOCKED = HTTP_423 HTTP_FAILED_DEPENDENCY = HTTP_424 @@ -173,6 +176,7 @@ HTTPStatus HTTP_416 = '416 Range Not Satisfiable' HTTP_417 = '417 Expectation Failed' HTTP_418 = "418 I'm a teapot" + HTTP_421 = '421 Misdirected Request' HTTP_422 = "422 Unprocessable Entity" HTTP_423 = '423 Locked' HTTP_424 = '424 Failed Dependency' @@ -193,8 +197,10 @@ HTTPStatus HTTP_SERVICE_UNAVAILABLE = HTTP_503 HTTP_GATEWAY_TIMEOUT = HTTP_504 HTTP_HTTP_VERSION_NOT_SUPPORTED = HTTP_505 + HTTP_VARIANT_ALSO_NEGOTIATE = HTTP_506 HTTP_INSUFFICIENT_STORAGE = HTTP_507 HTTP_LOOP_DETECTED = HTTP_508 + HTTP_NOT_EXTENDED = HTTP_510 HTTP_NETWORK_AUTHENTICATION_REQUIRED = HTTP_511 HTTP_500 = '500 Internal Server Error' @@ -203,6 +209,8 @@ HTTPStatus HTTP_503 = '503 Service Unavailable' HTTP_504 = '504 Gateway Timeout' HTTP_505 = '505 HTTP Version Not Supported' + HTTP_506 = '506 Variant Also Negotiate' HTTP_507 = '507 Insufficient Storage' HTTP_508 = '508 Loop Detected' + HTTP_510 = '510 Not Extended' HTTP_511 = '511 Network Authentication Required' From 246e132c88dec4b7e5bfb81a1ede2b3e578492ff Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Fri, 13 Sep 2024 00:28:21 +0000 Subject: [PATCH 09/16] style (status_codes): fix typo --- falcon/__init__.py | 4 ++-- falcon/status_codes.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/falcon/__init__.py b/falcon/__init__.py index 4c8101e8e..d6bcc1a06 100644 --- a/falcon/__init__.py +++ b/falcon/__init__.py @@ -321,7 +321,7 @@ 'HTTP_UNSUPPORTED_MEDIA_TYPE', 'HTTP_UPGRADE_REQUIRED', 'HTTP_USE_PROXY', - 'HTTP_VARIANT_ALSO_NEGOTIATE', + 'HTTP_VARIANT_ALSO_NEGOTIATES', ) # NOTE(kgriffs,vytas): Hoist classes and functions into the falcon namespace. @@ -586,7 +586,7 @@ from falcon.status_codes import HTTP_UNSUPPORTED_MEDIA_TYPE from falcon.status_codes import HTTP_UPGRADE_REQUIRED from falcon.status_codes import HTTP_USE_PROXY -from falcon.status_codes import HTTP_VARIANT_ALSO_NEGOTIATE +from falcon.status_codes import HTTP_VARIANT_ALSO_NEGOTIATES from falcon.stream import BoundedStream # NOTE(kgriffs): Ensure that "from falcon import uri" will import diff --git a/falcon/status_codes.py b/falcon/status_codes.py index e89b3d9ea..8c49f2655 100644 --- a/falcon/status_codes.py +++ b/falcon/status_codes.py @@ -139,8 +139,8 @@ HTTP_GATEWAY_TIMEOUT: Final[str] = HTTP_504 HTTP_505: Final[str] = '505 HTTP Version Not Supported' HTTP_HTTP_VERSION_NOT_SUPPORTED: Final[str] = HTTP_505 -HTTP_506: Final[str] = '506 Variant Also Negotiate' -HTTP_VARIANT_ALSO_NEGOTIATE: Final[str] = HTTP_506 +HTTP_506: Final[str] = '506 Variant Also Negotiates' +HTTP_VARIANT_ALSO_NEGOTIATES: Final[str] = HTTP_506 HTTP_507: Final[str] = '507 Insufficient Storage' HTTP_INSUFFICIENT_STORAGE: Final[str] = HTTP_507 HTTP_508: Final[str] = '508 Loop Detected' @@ -386,5 +386,5 @@ 'HTTP_UNSUPPORTED_MEDIA_TYPE', 'HTTP_UPGRADE_REQUIRED', 'HTTP_USE_PROXY', - 'HTTP_VARIANT_ALSO_NEGOTIATE', + 'HTTP_VARIANT_ALSO_NEGOTIATES', ) From d17737a6c744b511b0404c39649c3f4b78bf950e Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Fri, 13 Sep 2024 00:28:48 +0000 Subject: [PATCH 10/16] docs (status): fix typo --- docs/api/status.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/status.rst b/docs/api/status.rst index 7d8b7f044..449b2a071 100644 --- a/docs/api/status.rst +++ b/docs/api/status.rst @@ -197,7 +197,7 @@ HTTPStatus HTTP_SERVICE_UNAVAILABLE = HTTP_503 HTTP_GATEWAY_TIMEOUT = HTTP_504 HTTP_HTTP_VERSION_NOT_SUPPORTED = HTTP_505 - HTTP_VARIANT_ALSO_NEGOTIATE = HTTP_506 + HTTP_VARIANT_ALSO_NEGOTIATES = HTTP_506 HTTP_INSUFFICIENT_STORAGE = HTTP_507 HTTP_LOOP_DETECTED = HTTP_508 HTTP_NOT_EXTENDED = HTTP_510 @@ -209,7 +209,7 @@ HTTPStatus HTTP_503 = '503 Service Unavailable' HTTP_504 = '504 Gateway Timeout' HTTP_505 = '505 HTTP Version Not Supported' - HTTP_506 = '506 Variant Also Negotiate' + HTTP_506 = '506 Variant Also Negotiates' HTTP_507 = '507 Insufficient Storage' HTTP_508 = '508 Loop Detected' HTTP_510 = '510 Not Extended' From 9c5604db0cc180fe8eb1d0d7bbdcbf580d3206f5 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Tue, 17 Sep 2024 12:52:34 +0000 Subject: [PATCH 11/16] refactor (helpers): add raw_uri to the asgi scope --- falcon/testing/helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index e21961125..763256565 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -677,7 +677,8 @@ async def _send(self, data: Optional[bytes] = None, text: Optional[str] = None): # NOTE(kgriffs): From the client's perspective, it was a send, # but the server will be expecting websocket.receive - event = {'type': EventType.WS_RECEIVE} # type: Dict[str, Union[bytes, str]] + # type: Dict[str, Union[bytes, str]] + event = {'type': EventType.WS_RECEIVE} if data is not None: event['bytes'] = data @@ -942,7 +943,7 @@ def create_scope( """ http_version = _fixup_http_version(http_version) - + raw_path = path path = uri.decode(path, unquote_plus=False) # NOTE(kgriffs): Handles both None and '' @@ -960,6 +961,7 @@ def create_scope( 'http_version': http_version, 'method': method.upper(), 'path': path, + 'raw_uri': raw_path, 'query_string': query_string, } From 9ed2f26616b334578d838662136e7f24477650b4 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Tue, 17 Sep 2024 13:38:32 +0000 Subject: [PATCH 12/16] test (asgi): add test for create_scope --- tests/asgi/test_testing_asgi.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/asgi/test_testing_asgi.py b/tests/asgi/test_testing_asgi.py index 8ec041361..e34bee06c 100644 --- a/tests/asgi/test_testing_asgi.py +++ b/tests/asgi/test_testing_asgi.py @@ -153,3 +153,9 @@ def test_immediate_disconnect(): with pytest.raises(ConnectionError): client.simulate_get('/', asgi_disconnect_ttl=0) + + +def test_create_scope_preserve_raw_uri(): + uri = '/cache/http%3A%2F%2Ffalconframework.org/status' + scope = testing.create_scope(path=uri) + assert scope['raw_uri'] == uri From 15bba295cbb0fbc9a02d65653a5228075898063a Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Tue, 17 Sep 2024 14:42:06 +0000 Subject: [PATCH 13/16] refactor (helpers): update raw_path implementation --- falcon/testing/helpers.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index 763256565..39b092882 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -240,7 +240,7 @@ async def emit(self) -> Dict[str, Any]: return event chunk = self._body[: self._chunk_size] - self._body = self._body[self._chunk_size :] or None + self._body = self._body[self._chunk_size:] or None if chunk: event['body'] = bytes(chunk) @@ -659,13 +659,15 @@ def _require_accepted(self): assert self._state == _WebSocketState.DENIED if self._close_code == WSCloseCode.PATH_NOT_FOUND: - raise falcon_errors.WebSocketPathNotFound(WSCloseCode.PATH_NOT_FOUND) + raise falcon_errors.WebSocketPathNotFound( + WSCloseCode.PATH_NOT_FOUND) if self._close_code == WSCloseCode.SERVER_ERROR: raise falcon_errors.WebSocketServerError(WSCloseCode.SERVER_ERROR) if self._close_code == WSCloseCode.HANDLER_NOT_FOUND: - raise falcon_errors.WebSocketHandlerNotFound(WSCloseCode.HANDLER_NOT_FOUND) + raise falcon_errors.WebSocketHandlerNotFound( + WSCloseCode.HANDLER_NOT_FOUND) raise falcon_errors.WebSocketDisconnected(self._close_code) @@ -943,7 +945,7 @@ def create_scope( """ http_version = _fixup_http_version(http_version) - raw_path = path + raw_path = path.encode('utf-8') path = uri.decode(path, unquote_plus=False) # NOTE(kgriffs): Handles both None and '' @@ -961,7 +963,7 @@ def create_scope( 'http_version': http_version, 'method': method.upper(), 'path': path, - 'raw_uri': raw_path, + 'raw_path': raw_path, 'query_string': query_string, } @@ -980,7 +982,8 @@ def create_scope( if scheme: if scheme not in {'http', 'https', 'ws', 'wss'}: - raise ValueError("scheme must be either 'http', 'https', 'ws', or 'wss'") + raise ValueError( + "scheme must be either 'http', 'https', 'ws', or 'wss'") scope['scheme'] = scheme @@ -1319,7 +1322,8 @@ def create_asgi_req( body = body or b'' disconnect_at = time.time() + 300 - req_event_emitter = ASGIRequestEventEmitter(body, disconnect_at=disconnect_at) + req_event_emitter = ASGIRequestEventEmitter( + body, disconnect_at=disconnect_at) req_type = req_type or falcon.asgi.Request return req_type(scope, req_event_emitter, options=options) @@ -1456,7 +1460,8 @@ def _add_headers_to_scope( prepared_headers.append([b'host', host_header.encode()]) if cookies is not None: - prepared_headers.append([b'cookie', _make_cookie_values(cookies).encode()]) + prepared_headers.append( + [b'cookie', _make_cookie_values(cookies).encode()]) # NOTE(kgriffs): Make it an iterator to ensure the app is not expecting # a specific type (ASGI only specified that it is an iterable). @@ -1480,7 +1485,8 @@ def _fixup_http_version(http_version) -> str: def _make_cookie_values(cookies: Dict) -> str: return '; '.join( [ - '{}={}'.format(key, cookie.value if hasattr(cookie, 'value') else cookie) + '{}={}'.format(key, cookie.value if hasattr( + cookie, 'value') else cookie) for key, cookie in cookies.items() ] ) From 652792683c9db1061a008be61b58cb1c3bcc8c03 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Tue, 17 Sep 2024 14:55:21 +0000 Subject: [PATCH 14/16] refactor (asgi): refactor test scope raw_path --- falcon/testing/helpers.py | 24 +++++++++--------------- tests/asgi/test_testing_asgi.py | 6 +++--- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index 39b092882..9b71c23b8 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -240,7 +240,7 @@ async def emit(self) -> Dict[str, Any]: return event chunk = self._body[: self._chunk_size] - self._body = self._body[self._chunk_size:] or None + self._body = self._body[self._chunk_size :] or None if chunk: event['body'] = bytes(chunk) @@ -659,15 +659,13 @@ def _require_accepted(self): assert self._state == _WebSocketState.DENIED if self._close_code == WSCloseCode.PATH_NOT_FOUND: - raise falcon_errors.WebSocketPathNotFound( - WSCloseCode.PATH_NOT_FOUND) + raise falcon_errors.WebSocketPathNotFound(WSCloseCode.PATH_NOT_FOUND) if self._close_code == WSCloseCode.SERVER_ERROR: raise falcon_errors.WebSocketServerError(WSCloseCode.SERVER_ERROR) if self._close_code == WSCloseCode.HANDLER_NOT_FOUND: - raise falcon_errors.WebSocketHandlerNotFound( - WSCloseCode.HANDLER_NOT_FOUND) + raise falcon_errors.WebSocketHandlerNotFound(WSCloseCode.HANDLER_NOT_FOUND) raise falcon_errors.WebSocketDisconnected(self._close_code) @@ -945,7 +943,7 @@ def create_scope( """ http_version = _fixup_http_version(http_version) - raw_path = path.encode('utf-8') + raw_path = path path = uri.decode(path, unquote_plus=False) # NOTE(kgriffs): Handles both None and '' @@ -963,7 +961,7 @@ def create_scope( 'http_version': http_version, 'method': method.upper(), 'path': path, - 'raw_path': raw_path, + 'raw_path': raw_path.encode(), 'query_string': query_string, } @@ -982,8 +980,7 @@ def create_scope( if scheme: if scheme not in {'http', 'https', 'ws', 'wss'}: - raise ValueError( - "scheme must be either 'http', 'https', 'ws', or 'wss'") + raise ValueError("scheme must be either 'http', 'https', 'ws', or 'wss'") scope['scheme'] = scheme @@ -1322,8 +1319,7 @@ def create_asgi_req( body = body or b'' disconnect_at = time.time() + 300 - req_event_emitter = ASGIRequestEventEmitter( - body, disconnect_at=disconnect_at) + req_event_emitter = ASGIRequestEventEmitter(body, disconnect_at=disconnect_at) req_type = req_type or falcon.asgi.Request return req_type(scope, req_event_emitter, options=options) @@ -1460,8 +1456,7 @@ def _add_headers_to_scope( prepared_headers.append([b'host', host_header.encode()]) if cookies is not None: - prepared_headers.append( - [b'cookie', _make_cookie_values(cookies).encode()]) + prepared_headers.append([b'cookie', _make_cookie_values(cookies).encode()]) # NOTE(kgriffs): Make it an iterator to ensure the app is not expecting # a specific type (ASGI only specified that it is an iterable). @@ -1485,8 +1480,7 @@ def _fixup_http_version(http_version) -> str: def _make_cookie_values(cookies: Dict) -> str: return '; '.join( [ - '{}={}'.format(key, cookie.value if hasattr( - cookie, 'value') else cookie) + '{}={}'.format(key, cookie.value if hasattr(cookie, 'value') else cookie) for key, cookie in cookies.items() ] ) diff --git a/tests/asgi/test_testing_asgi.py b/tests/asgi/test_testing_asgi.py index e34bee06c..2e30d887b 100644 --- a/tests/asgi/test_testing_asgi.py +++ b/tests/asgi/test_testing_asgi.py @@ -156,6 +156,6 @@ def test_immediate_disconnect(): def test_create_scope_preserve_raw_uri(): - uri = '/cache/http%3A%2F%2Ffalconframework.org/status' - scope = testing.create_scope(path=uri) - assert scope['raw_uri'] == uri + path = '/cache/http%3A%2F%2Ffalconframework.org/status' + scope = testing.create_scope(path=path) + assert scope['raw_path'] == path.encode() From 86f63cf75bb421098fdff6f6c6a7b7b46d4a5d77 Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Tue, 17 Sep 2024 16:31:43 +0000 Subject: [PATCH 15/16] refactor (helpers): remove queries from raw_path --- falcon/testing/helpers.py | 2 +- tests/asgi/test_testing_asgi.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index 9b71c23b8..c2e3b14ad 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -943,7 +943,7 @@ def create_scope( """ http_version = _fixup_http_version(http_version) - raw_path = path + raw_path = path.split('?')[0] path = uri.decode(path, unquote_plus=False) # NOTE(kgriffs): Handles both None and '' diff --git a/tests/asgi/test_testing_asgi.py b/tests/asgi/test_testing_asgi.py index 2e30d887b..1cb182061 100644 --- a/tests/asgi/test_testing_asgi.py +++ b/tests/asgi/test_testing_asgi.py @@ -155,7 +155,12 @@ def test_immediate_disconnect(): client.simulate_get('/', asgi_disconnect_ttl=0) -def test_create_scope_preserve_raw_uri(): - path = '/cache/http%3A%2F%2Ffalconframework.org/status' - scope = testing.create_scope(path=path) - assert scope['raw_path'] == path.encode() +def test_create_scope_preserve_raw_path(): + path_no_queries = '/cache/http%3A%2F%2Ffalconframework.org/status' + scope = testing.create_scope(path=path_no_queries) + assert scope['raw_path'] == path_no_queries.encode() + path_with_queries = ( + '/cache/http%3A%2F%2Ffalconframework.org/status?param1=value1¶m2=value2' + ) + scope = testing.create_scope(path=path_with_queries) + assert scope['raw_path'] != path_with_queries.encode() From f87ec9eb1f7fa02d44d223e7f1e15b8115a6025f Mon Sep 17 00:00:00 2001 From: Agustin Arce Date: Fri, 20 Sep 2024 01:56:37 +0000 Subject: [PATCH 16/16] docs (_newsfragment): add docs about changes --- docs/_newsfragments/2262.misc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/_newsfragments/2262.misc.rst diff --git a/docs/_newsfragments/2262.misc.rst b/docs/_newsfragments/2262.misc.rst new file mode 100644 index 000000000..202d08915 --- /dev/null +++ b/docs/_newsfragments/2262.misc.rst @@ -0,0 +1,2 @@ +:class:`~falcon.testing.TestClient` preserves the raw path in the ASGI flavour in :func:`create_scope` +to conform with (`#2159 `__). \ No newline at end of file