Repository: encode/httpx
Pull request #1088 (2020-07-24 14:41:27)

Single consistent name for status codes
For consideration.

Currently we're exposing both `httpx.StatusCodes` and `httpx.codes` for working with status codes.

We're [only documenting `httpx.codes`](https://www.python-httpx.org/quickstart/#response-status-codes), and we probably only want a single canonical name here, right?

The documentation could also do with being expanded, at the very least in the API section.

Usages are like...

```python
>>> httpx.codes.OK
<codes.OK: 200>
>>> httpx.codes.OK == 200
True
>>> httpx.codes.get_reason_phrase(200)
'OK'
>>> httpx.codes.is_error(400)
True
>>> httpx.codes.is_redirect(400)
False
>>> httpx.codes.is_client_error(400)
True
>>> httpx.codes.is_server_error(400)
False
>>> httpx.codes(200).phrase
'OK'
>>> httpx.codes(200).value
200
```

jcugat: There are some usages in the wild of `StatusCode`, but I guess we are fine breaking compatibility with this before 1.0 right?
florimondmanca: I'm not absolute on either option though and pretty much bikeshedding at this point... The important bit is that we should have a single status codes enum, that I agree. :)
tomchristie: I've updated this with a more gentle deprecation of `httpx.StatusCode`.

Patch:
diff --git a/httpx/__init__.py b/httpx/__init__.py
index b88c9a8..644656f 100644
--- a/httpx/__init__.py
+++ b/httpx/__init__.py
@@ -80,6 +80,7 @@ __all__ = [
     "ResponseClosed",
     "ResponseNotRead",
     "RequestNotRead",
+    "StatusCode",
     "StreamConsumed",
     "StreamError",
     "ProxyError",
@@ -90,7 +91,6 @@ __all__ = [
     "URL",
     "URLLib3Transport",
     "URLLib3ProxyTransport",
-    "StatusCode",
     "Cookies",
     "Headers",
     "QueryParams",
diff --git a/httpx/_models.py b/httpx/_models.py
index 785bf47..c8cbfbb 100644
--- a/httpx/_models.py
+++ b/httpx/_models.py
@@ -30,7 +30,7 @@ from ._exceptions import (
     ResponseNotRead,
     StreamConsumed,
 )
-from ._status_codes import StatusCode
+from ._status_codes import codes
 from ._types import (
     CookieTypes,
     HeaderTypes,
@@ -660,7 +660,7 @@ class Response:
 
     @property
     def reason_phrase(self) -> str:
-        return StatusCode.get_reason_phrase(self.status_code)
+        return codes.get_reason_phrase(self.status_code)
 
     @property
     def url(self) -> typing.Optional[URL]:
@@ -758,11 +758,11 @@ class Response:
 
     @property
     def is_error(self) -> bool:
-        return StatusCode.is_error(self.status_code)
+        return codes.is_error(self.status_code)
 
     @property
     def is_redirect(self) -> bool:
-        return StatusCode.is_redirect(self.status_code) and "location" in self.headers
+        return codes.is_redirect(self.status_code) and "location" in self.headers
 
     def raise_for_status(self) -> None:
         """
@@ -773,10 +773,10 @@ class Response:
             "For more information check: https://httpstatuses.com/{0.status_code}"
         )
 
-        if StatusCode.is_client_error(self.status_code):
+        if codes.is_client_error(self.status_code):
             message = message.format(self, error_type="Client Error")
             raise HTTPStatusError(message, response=self)
-        elif StatusCode.is_server_error(self.status_code):
+        elif codes.is_server_error(self.status_code):
             message = message.format(self, error_type="Server Error")
             raise HTTPStatusError(message, response=self)
 
diff --git a/httpx/_status_codes.py b/httpx/_status_codes.py
index eda7c56..3b4ae6a 100644
--- a/httpx/_status_codes.py
+++ b/httpx/_status_codes.py
@@ -1,7 +1,8 @@
+import warnings
 from enum import IntEnum
 
 
-class StatusCode(IntEnum):
+class codes(IntEnum):
     """HTTP status codes and reason phrases
     Status codes from the following RFCs are all observed:
         * RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616
@@ -17,7 +18,7 @@ class StatusCode(IntEnum):
         * RFC 7725: An HTTP Status Code to Report Legal Obstacles
     """
 
-    def __new__(cls, value: int, phrase: str = "") -> "StatusCode":
+    def __new__(cls, value: int, phrase: str = "") -> "codes":
         obj = int.__new__(cls, value)  # type: ignore
         obj._value_ = value
 
@@ -30,7 +31,7 @@ class StatusCode(IntEnum):
     @classmethod
     def get_reason_phrase(cls, value: int) -> str:
         try:
-            return StatusCode(value).phrase  # type: ignore
+            return codes(value).phrase  # type: ignore
         except ValueError:
             return ""
 
@@ -38,15 +39,15 @@ class StatusCode(IntEnum):
     def is_redirect(cls, value: int) -> bool:
         return value in (
             # 301 (Cacheable redirect. Method may change to GET.)
-            StatusCode.MOVED_PERMANENTLY,
+            codes.MOVED_PERMANENTLY,
             # 302 (Uncacheable redirect. Method may change to GET.)
-            StatusCode.FOUND,
+            codes.FOUND,
             # 303 (Client should make a GET or HEAD request.)
-            StatusCode.SEE_OTHER,
+            codes.SEE_OTHER,
             # 307 (Equiv. 302, but retain method)
-            StatusCode.TEMPORARY_REDIRECT,
+            codes.TEMPORARY_REDIRECT,
             # 308 (Equiv. 301, but retain method)
-            StatusCode.PERMANENT_REDIRECT,
+            codes.PERMANENT_REDIRECT,
         )
 
     @classmethod
@@ -132,8 +133,26 @@ class StatusCode(IntEnum):
     NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required"
 
 
-codes = StatusCode
-
 #  Include lower-case styles for `requests` compatibility.
 for code in codes:
     setattr(codes, code._name_.lower(), int(code))
+
+
+class StatusCodeCompat:
+    def __call__(self, *args, **kwargs):  # type: ignore
+        message = "`httpx.StatusCode` is deprecated. Use `httpx.codes` instead."
+        warnings.warn(message, DeprecationWarning)
+        return codes(*args, **kwargs)
+
+    def __getattr__(self, attr):  # type: ignore
+        message = "`httpx.StatusCode` is deprecated. Use `httpx.codes` instead."
+        warnings.warn(message, DeprecationWarning)
+        return getattr(codes, attr)
+
+    def __getitem__(self, item):  # type: ignore
+        message = "`httpx.StatusCode` is deprecated. Use `httpx.codes` instead."
+        warnings.warn(message, DeprecationWarning)
+        return codes[item]
+
+
+StatusCode = StatusCodeCompat()


Test patch:
diff --git a/tests/test_status_codes.py b/tests/test_status_codes.py
index c53e959..722e83c 100644
--- a/tests/test_status_codes.py
+++ b/tests/test_status_codes.py
@@ -1,3 +1,5 @@
+import pytest
+
 import httpx
 
 
@@ -6,6 +8,14 @@ def test_status_code_as_int():
     assert str(httpx.codes.NOT_FOUND) == "404"
 
 
+def test_status_code_value_lookup():
+    assert httpx.codes(404) == 404
+
+
+def test_status_code_phrase_lookup():
+    assert httpx.codes["NOT_FOUND"] == 404
+
+
 def test_lowercase_status_code():
     assert httpx.codes.not_found == 404  # type: ignore
 
@@ -16,3 +26,14 @@ def test_reason_phrase_for_status_code():
 
 def test_reason_phrase_for_unknown_status_code():
     assert httpx.codes.get_reason_phrase(499) == ""
+
+
+def test_deprecated_status_code_class():
+    with pytest.warns(DeprecationWarning):
+        assert httpx.StatusCode.NOT_FOUND == 404
+
+    with pytest.warns(DeprecationWarning):
+        assert httpx.StatusCode(404) == 404
+
+    with pytest.warns(DeprecationWarning):
+        assert httpx.StatusCode["NOT_FOUND"] == 404