From d1d8a4fa0a90154c5a23cd5e73443f40a56d163b Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 7 Apr 2022 22:26:44 +0200 Subject: [PATCH 1/3] Deprecate `WS_1004_NO_STATUS_RCVD` and `WS_1005_ABNORMAL_CLOSURE` --- starlette/_pep562.py | 61 ++++++++++++++++++++++ starlette/status.py | 117 ++++++++++++++++++++++++++++++++++++++++++- tests/test_status.py | 23 +++++++++ 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 starlette/_pep562.py create mode 100644 tests/test_status.py diff --git a/starlette/_pep562.py b/starlette/_pep562.py new file mode 100644 index 000000000..3059257a2 --- /dev/null +++ b/starlette/_pep562.py @@ -0,0 +1,61 @@ +""" +Backport of PEP 562. +https://pypi.org/search/?q=pep562 +Licensed under MIT +Copyright (c) 2018 Isaac Muse +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions +of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" +import sys +from typing import Any, Callable, List, Optional + + +class Pep562: + """ + Backport of PEP 562 . + Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`. + The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed. + """ + + def __init__(self, name: str) -> None: + """Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7.""" + + self._module = sys.modules[name] + self._get_attr = getattr(self._module, "__getattr__", None) + self._get_dir: Optional[Callable[..., List[str]]] = getattr( + self._module, "__dir__", None + ) + sys.modules[name] = self # type: ignore[assignment] + + def __dir__(self) -> List[str]: + """Return the overridden `dir` if one was provided, else apply `dir` to the module.""" + + return self._get_dir() if self._get_dir else dir(self._module) + + def __getattr__(self, name: str) -> Any: + """ + Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present. + """ + + try: + return getattr(self._module, name) + except AttributeError: + if self._get_attr: + return self._get_attr(name) + raise + + +def pep562(module_name: str) -> None: + """Helper function to apply PEP 562.""" + + if sys.version_info < (3, 7): + Pep562(module_name) diff --git a/starlette/status.py b/starlette/status.py index b122ae85c..de6bdabd8 100644 --- a/starlette/status.py +++ b/starlette/status.py @@ -5,6 +5,92 @@ And RFC 2324 - https://tools.ietf.org/html/rfc2324 """ +import sys +import warnings + +from starlette._pep562 import pep562 + +__all__ = ( + "HTTP_100_CONTINUE", + "HTTP_101_SWITCHING_PROTOCOLS", + "HTTP_102_PROCESSING", + "HTTP_103_EARLY_HINTS", + "HTTP_200_OK", + "HTTP_201_CREATED", + "HTTP_202_ACCEPTED", + "HTTP_203_NON_AUTHORITATIVE_INFORMATION", + "HTTP_204_NO_CONTENT", + "HTTP_205_RESET_CONTENT", + "HTTP_206_PARTIAL_CONTENT", + "HTTP_207_MULTI_STATUS", + "HTTP_208_ALREADY_REPORTED", + "HTTP_226_IM_USED", + "HTTP_300_MULTIPLE_CHOICES", + "HTTP_301_MOVED_PERMANENTLY", + "HTTP_302_FOUND", + "HTTP_303_SEE_OTHER", + "HTTP_304_NOT_MODIFIED", + "HTTP_305_USE_PROXY", + "HTTP_306_RESERVED", + "HTTP_307_TEMPORARY_REDIRECT", + "HTTP_308_PERMANENT_REDIRECT", + "HTTP_400_BAD_REQUEST", + "HTTP_401_UNAUTHORIZED", + "HTTP_402_PAYMENT_REQUIRED", + "HTTP_403_FORBIDDEN", + "HTTP_404_NOT_FOUND", + "HTTP_405_METHOD_NOT_ALLOWED", + "HTTP_406_NOT_ACCEPTABLE", + "HTTP_407_PROXY_AUTHENTICATION_REQUIRED", + "HTTP_408_REQUEST_TIMEOUT", + "HTTP_409_CONFLICT", + "HTTP_410_GONE", + "HTTP_411_LENGTH_REQUIRED", + "HTTP_412_PRECONDITION_FAILED", + "HTTP_413_REQUEST_ENTITY_TOO_LARGE", + "HTTP_414_REQUEST_URI_TOO_LONG", + "HTTP_415_UNSUPPORTED_MEDIA_TYPE", + "HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE", + "HTTP_417_EXPECTATION_FAILED", + "HTTP_418_IM_A_TEAPOT", + "HTTP_421_MISDIRECTED_REQUEST", + "HTTP_422_UNPROCESSABLE_ENTITY", + "HTTP_423_LOCKED", + "HTTP_424_FAILED_DEPENDENCY", + "HTTP_425_TOO_EARLY", + "HTTP_426_UPGRADE_REQUIRED", + "HTTP_428_PRECONDITION_REQUIRED", + "HTTP_429_TOO_MANY_REQUESTS", + "HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE", + "HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS", + "HTTP_500_INTERNAL_SERVER_ERROR", + "HTTP_501_NOT_IMPLEMENTED", + "HTTP_502_BAD_GATEWAY", + "HTTP_503_SERVICE_UNAVAILABLE", + "HTTP_504_GATEWAY_TIMEOUT", + "HTTP_505_HTTP_VERSION_NOT_SUPPORTED", + "HTTP_506_VARIANT_ALSO_NEGOTIATES", + "HTTP_507_INSUFFICIENT_STORAGE", + "HTTP_508_LOOP_DETECTED", + "HTTP_510_NOT_EXTENDED", + "HTTP_511_NETWORK_AUTHENTICATION_REQUIRED", + "WS_1000_NORMAL_CLOSURE", + "WS_1001_GOING_AWAY", + "WS_1002_PROTOCOL_ERROR", + "WS_1003_UNSUPPORTED_DATA", + "WS_1005_NO_STATUS_RCVD", + "WS_1006_ABNORMAL_CLOSURE", + "WS_1007_INVALID_FRAME_PAYLOAD_DATA", + "WS_1008_POLICY_VIOLATION", + "WS_1009_MESSAGE_TOO_BIG", + "WS_1010_MANDATORY_EXT", + "WS_1011_INTERNAL_ERROR", + "WS_1012_SERVICE_RESTART", + "WS_1013_TRY_AGAIN_LATER", + "WS_1014_BAD_GATEWAY", + "WS_1015_TLS_HANDSHAKE", +) + HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_102_PROCESSING = 102 @@ -79,8 +165,8 @@ WS_1001_GOING_AWAY = 1001 WS_1002_PROTOCOL_ERROR = 1002 WS_1003_UNSUPPORTED_DATA = 1003 -WS_1004_NO_STATUS_RCVD = 1004 -WS_1005_ABNORMAL_CLOSURE = 1005 +WS_1005_NO_STATUS_RCVD = 1005 +WS_1006_ABNORMAL_CLOSURE = 1006 WS_1007_INVALID_FRAME_PAYLOAD_DATA = 1007 WS_1008_POLICY_VIOLATION = 1008 WS_1009_MESSAGE_TOO_BIG = 1009 @@ -90,3 +176,30 @@ WS_1013_TRY_AGAIN_LATER = 1013 WS_1014_BAD_GATEWAY = 1014 WS_1015_TLS_HANDSHAKE = 1015 + + +__deprecated__ = {"WS_1004_NO_STATUS_RCVD": 1004, "WS_1005_ABNORMAL_CLOSURE": 1005} + + +def __getattr__(name: str) -> int: + deprecation_changes = { + "WS_1004_NO_STATUS_RCVD": "WS_1005_NO_STATUS_RCVD", + "WS_1005_ABNORMAL_CLOSURE": "WS_1006_ABNORMAL_CLOSURE", + } + deprecated = __deprecated__.get(name) + if deprecated: + stacklevel = 3 if sys.version_info >= (3, 7) else 4 + warnings.warn( + f"'{name}' is deprecated. Use '{deprecation_changes[name]}' instead.", + category=DeprecationWarning, + stacklevel=stacklevel, + ) + return deprecated + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +def __dir__(): + return sorted(__all__ + __deprecated__) + + +pep562(__name__) diff --git a/tests/test_status.py b/tests/test_status.py new file mode 100644 index 000000000..4852c06ef --- /dev/null +++ b/tests/test_status.py @@ -0,0 +1,23 @@ +import importlib + +import pytest + + +@pytest.mark.parametrize( + "constant,msg", + ( + ( + "WS_1004_NO_STATUS_RCVD", + "'WS_1004_NO_STATUS_RCVD' is deprecated. Use 'WS_1005_NO_STATUS_RCVD' instead.", + ), + ( + "WS_1005_ABNORMAL_CLOSURE", + "'WS_1005_ABNORMAL_CLOSURE' is deprecated. Use 'WS_1006_ABNORMAL_CLOSURE' instead.", + ), + ), +) +def test_deprecated_types(constant: str, msg: str) -> None: + with pytest.warns(DeprecationWarning) as record: + getattr(importlib.import_module("starlette.status"), constant) + assert len(record) == 1 + assert msg in str(record.list[0]) From 26751fb4195c574d9551ee38bf2f881ca9114923 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Fri, 8 Apr 2022 10:14:59 +0200 Subject: [PATCH 2/3] Fix linter --- starlette/_pep562.py | 1 + starlette/status.py | 5 +++-- tests/test_status.py | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/starlette/_pep562.py b/starlette/_pep562.py index 3059257a2..975ba1d15 100644 --- a/starlette/_pep562.py +++ b/starlette/_pep562.py @@ -1,3 +1,4 @@ +# flake8: noqa """ Backport of PEP 562. https://pypi.org/search/?q=pep562 diff --git a/starlette/status.py b/starlette/status.py index de6bdabd8..7641496bb 100644 --- a/starlette/status.py +++ b/starlette/status.py @@ -7,6 +7,7 @@ """ import sys import warnings +from typing import List from starlette._pep562 import pep562 @@ -198,8 +199,8 @@ def __getattr__(name: str) -> int: raise AttributeError(f"module '{__name__}' has no attribute '{name}'") -def __dir__(): - return sorted(__all__ + __deprecated__) +def __dir__() -> List[str]: + return sorted(list(__all__) + list(__deprecated__.keys())) pep562(__name__) diff --git a/tests/test_status.py b/tests/test_status.py index 4852c06ef..04719e87e 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -8,11 +8,13 @@ ( ( "WS_1004_NO_STATUS_RCVD", - "'WS_1004_NO_STATUS_RCVD' is deprecated. Use 'WS_1005_NO_STATUS_RCVD' instead.", + "'WS_1004_NO_STATUS_RCVD' is deprecated. " + "Use 'WS_1005_NO_STATUS_RCVD' instead.", ), ( "WS_1005_ABNORMAL_CLOSURE", - "'WS_1005_ABNORMAL_CLOSURE' is deprecated. Use 'WS_1006_ABNORMAL_CLOSURE' instead.", + "'WS_1005_ABNORMAL_CLOSURE' is deprecated. " + "Use 'WS_1006_ABNORMAL_CLOSURE' instead.", ), ), ) From 15e14c095f78a073f1700f68a372e2a1ae031298 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Fri, 8 Apr 2022 10:18:33 +0200 Subject: [PATCH 3/3] Fix coverage --- starlette/_pep562.py | 8 ++++---- starlette/status.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/starlette/_pep562.py b/starlette/_pep562.py index 975ba1d15..a4757eac4 100644 --- a/starlette/_pep562.py +++ b/starlette/_pep562.py @@ -27,7 +27,7 @@ class Pep562: The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed. """ - def __init__(self, name: str) -> None: + def __init__(self, name: str) -> None: # pragma: no cover """Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7.""" self._module = sys.modules[name] @@ -37,12 +37,12 @@ def __init__(self, name: str) -> None: ) sys.modules[name] = self # type: ignore[assignment] - def __dir__(self) -> List[str]: + def __dir__(self) -> List[str]: # pragma: no cover """Return the overridden `dir` if one was provided, else apply `dir` to the module.""" return self._get_dir() if self._get_dir else dir(self._module) - def __getattr__(self, name: str) -> Any: + def __getattr__(self, name: str) -> Any: # pragma: no cover """ Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present. """ @@ -55,7 +55,7 @@ def __getattr__(self, name: str) -> Any: raise -def pep562(module_name: str) -> None: +def pep562(module_name: str) -> None: # pragma: no cover """Helper function to apply PEP 562.""" if sys.version_info < (3, 7): diff --git a/starlette/status.py b/starlette/status.py index 7641496bb..cc52d896c 100644 --- a/starlette/status.py +++ b/starlette/status.py @@ -200,7 +200,7 @@ def __getattr__(name: str) -> int: def __dir__() -> List[str]: - return sorted(list(__all__) + list(__deprecated__.keys())) + return sorted(list(__all__) + list(__deprecated__.keys())) # pragma: no cover pep562(__name__)