Repository: encode/starlette

Issue:
BUG: Incorrect WebSocket status code 1005 description, missing status code 1006
### Discussed in https://github.com/encode/starlette/discussions/1577

<div type='discussions-op-text'>

<sup>Originally posted by **vbsd** April  7, 2022</sup>
It seems that there is a mistake in starlette's WebSocket [status code list](https://github.com/encode/starlette/blob/0.19.0/starlette/status.py#L83).

Status code 1005 has the name `1005_ABNORMAL_CLOSURE` and 1006 is missing altogether. However, [Mozilla MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code) indicate that 1005 actually means "no status received". It is status code 1006 that means "abnormal closure".

It looks like what should have been
```
WS_1005_NO_STATUS_RECEIVED = 1005
WS_1006_ABNORMAL_CLOSURE = 1006
```
got mashed together and became just one line:
```
WS_1006_NO_STATUS_RECEIVED = 1005
```

Making starlette backwards-incompatible over this wouldn't make much sense, but it seems reasonable to at least add the two correct lines and document `WS_1005_ABNORMAL_CLOSURE` as deprecated/wrong.</div>


Discussion:
Did you format the message to create an issue based on the discussion or the "turn into issue" button is available to everyone?
But you're right. It's an issue.

It's quite sensitive tho...
@Kludex I really appreciate you taking your time to read the details and reflecting here!

Happy to provide a PR - but are PR's merged in within a few days after there are no issues with the PR?

For example, it's unclear to me what this PR needs to move forward: https://github.com/encode/starlette/pull/1535
That PR is huge, and there was no discussion about it. Some member will check it at some point, maybe I'll be the one.

About this one, we just need to see a solution before implementing it. Changing the values is a breaking change, so I wonder if a rename with the right names and a deprecation on the old variables would be a possibility... Not sure what is the best approach here. Suggestions are welcome.
> Suggestions are welcome

Understood

> I wonder if a rename with the right names and a deprecation on the old variables would be a possibility

We don't want to break existing code - do we? This would be technically correct but practically, not preferable

If not, then we continue supporting the previous ambiguity but provide new/right names while updating documentation with a note. 
Current behavior is wrong, I don't think we should support it. I would like to deprecate, and eventually remove. That if we choose to give other names to new variables...

If we choose the same name, then there is no thinking. It's straightforward breaking change.

Do we have an alternative besides the above solutions?
> Current behavior is wrong, I don't think we should support it. I would like to deprecate, and eventually remove

Agreed! This would be technically correct and reduce future headaches

> If we choose the same name, then there is no thinking. It's straightforward breaking change.

Agreed

> Do we have an alternative besides the above solutions?

Not anything that's simpler.

Is there a way to give a "heads up" to people/code who use the current (flawed/misleading) design (whose code will break once we merge this PR in)? in a way that would be less disruptive to them?

Eg: Do we make a post on a forum/chat?

Resolving patch:
diff --git a/starlette/_pep562.py b/starlette/_pep562.py
new file mode 100644
--- /dev/null
+++ b/starlette/_pep562.py
@@ -0,0 +1,62 @@
+# flake8: noqa
+"""
+Backport of PEP 562.
+https://pypi.org/search/?q=pep562
+Licensed under MIT
+Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
+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 <https://pypi.org/search/?q=pep562>.
+    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:  # pragma: no cover
+        """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]:  # 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:  # pragma: no cover
+        """
+        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:  # pragma: no cover
+    """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
--- a/starlette/status.py
+++ b/starlette/status.py
@@ -5,6 +5,93 @@
 
 And RFC 2324 - https://tools.ietf.org/html/rfc2324
 """
+import sys
+import warnings
+from typing import List
+
+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 +166,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 +177,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__() -> List[str]:
+    return sorted(list(__all__) + list(__deprecated__.keys()))  # pragma: no cover
+
+
+pep562(__name__)


Test patch:
diff --git a/tests/test_status.py b/tests/test_status.py
new file mode 100644
--- /dev/null
+++ b/tests/test_status.py
@@ -0,0 +1,25 @@
+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])