Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Response): support setting Response.status to http.HTTPStatus #1735

Merged
merged 4 commits into from
Jul 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/_newsfragments/1135.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The :attr:`falcon.Response.status` attribute can now be also set to an
``http.HTTPStatus`` instance, an integer status code, as well as anything
supported by the :func:`falcon.code_to_http_status` utility method.
3 changes: 2 additions & 1 deletion falcon/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from falcon.response import Response, ResponseOptions
import falcon.status_codes as status
from falcon.util import misc
from falcon.util.misc import code_to_http_status


# PERF(vytas): On Python 3.5+ (including cythonized modules),
Expand Down Expand Up @@ -344,7 +345,7 @@ def __call__(self, env, start_response): # noqa: C901

req_succeeded = False

resp_status = resp.status
resp_status = code_to_http_status(resp.status)
default_media_type = self.resp_options.default_media_type

if req.method == 'HEAD' or resp_status in _BODILESS_STATUS_CODES:
Expand Down
10 changes: 3 additions & 7 deletions falcon/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,9 @@ class Response:
options (dict): Set of global options passed from the App handler.

Attributes:
status (str): HTTP status line (e.g., ``'200 OK'``). Falcon requires
the full status line, not just the code (e.g., 200). This design
makes the framework more efficient because it does not have to
do any kind of conversion or lookup when composing the WSGI
response.

If not set explicitly, the status defaults to ``'200 OK'``.
status: HTTP status code or line (e.g., ``'200 OK'``). This may be set
to a member of :class:`http.HTTPStatus`, an HTTP status line string
or byte string (e.g., ``'200 OK'``), or an ``int``.

Note:
The Falcon framework itself provides a number of constants for
Expand Down
40 changes: 30 additions & 10 deletions falcon/util/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
'secure_filename',
)

_DEFAULT_HTTP_REASON = 'Unknown'

_UNSAFE_CHARS = re.compile(r'[^a-zA-Z0-9.-]')

# PERF(kgriffs): Avoid superfluous namespace lookups
Expand Down Expand Up @@ -342,7 +344,7 @@ def get_argnames(func):


@deprecated('Please use falcon.code_to_http_status() instead.')
def get_http_status(status_code, default_reason='Unknown'):
def get_http_status(status_code, default_reason=_DEFAULT_HTTP_REASON):
"""Gets both the http status code and description from just a code.

Warning:
Expand Down Expand Up @@ -430,7 +432,7 @@ def http_status_to_code(status):
An LRU is used to minimize lookup time.

Args:
status: The status code or enum to normalize
status: The status code or enum to normalize.

Returns:
int: Integer code for the HTTP status (e.g., 200)
Expand Down Expand Up @@ -458,32 +460,50 @@ def http_status_to_code(status):


@_lru_cache_safe(maxsize=64)
def code_to_http_status(code):
"""Convert an HTTP status code integer to a status line string.
def code_to_http_status(status):
"""Normalize an HTTP status to an HTTP status line string.

This function takes a member of :class:`http.HTTPStatus`, an ``int`` status
code, an HTTP status line string or byte string (e.g., ``'200 OK'``) and
returns the corresponding HTTP status line string.

An LRU is used to minimize lookup time.

Note:
Unlike the deprecated :func:`get_http_status`, this function will not
attempt to coerce a string status to an integer code, assuming the
string already denotes an HTTP status line.

Args:
code (int): The integer status code to convert to a status line.
status: The status code or enum to normalize.

Returns:
str: HTTP status line corresponding to the given code. A newline
is not included at the end of the string.
"""

if isinstance(status, http.HTTPStatus):
return '{} {}'.format(status.value, status.phrase)

if isinstance(status, str):
return status

if isinstance(status, bytes):
return status.decode()

try:
code = int(code)
if code < 100:
raise ValueError()
code = int(status)
if not 100 <= code <= 999:
raise ValueError('{} is not a valid status code'.format(code))
except (ValueError, TypeError):
raise ValueError('"{}" is not a valid status code'.format(code))
raise ValueError('{!r} is not a valid status code'.format(code))

try:
# NOTE(kgriffs): We do this instead of using http.HTTPStatus since
# the Falcon module defines a larger number of codes.
return getattr(status_codes, 'HTTP_' + str(code))
except AttributeError:
return str(code)
return '{} {}'.format(code, _DEFAULT_HTTP_REASON)


def deprecated_args(*, allowed_positional, is_method=True):
Expand Down
41 changes: 41 additions & 0 deletions tests/test_httpstatus.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8

import http

import pytest

import falcon
Expand Down Expand Up @@ -230,3 +232,42 @@ def test_body_is_set(self, body_client):
res = body_client.simulate_put('/status')
assert res.status == falcon.HTTP_719
assert res.content == b''


@pytest.fixture()
def custom_status_client(asgi):
def client(status):
class Resource:
def on_get(self, req, resp):
resp.content_type = falcon.MEDIA_TEXT
resp.data = b'Hello, World!'
resp.status = status

app = create_app(asgi=asgi)
app.add_route('/status', Resource())
return testing.TestClient(app)

return client


@pytest.mark.parametrize('status,expected_code', [
(http.HTTPStatus(200), 200),
(http.HTTPStatus(202), 202),
(http.HTTPStatus(403), 403),
(http.HTTPStatus(500), 500),
(http.HTTPStatus.OK, 200),
(http.HTTPStatus.USE_PROXY, 305),
(http.HTTPStatus.NOT_FOUND, 404),
(http.HTTPStatus.NOT_IMPLEMENTED, 501),
(200, 200),
(307, 307),
(500, 500),
(702, 702),
(b'200 OK', 200),
(b'702 Emacs', 702),
])
def test_non_string_status(custom_status_client, status, expected_code):
client = custom_status_client(status)
resp = client.simulate_get('/status')
assert resp.text == 'Hello, World!'
assert resp.status_code == expected_code
19 changes: 15 additions & 4 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,18 +415,29 @@ def test_get_http_status(self):
(703, falcon.HTTP_703),
(404, falcon.HTTP_404),
(404.9, falcon.HTTP_404),
('404', falcon.HTTP_404),
(123, '123'),
(falcon.HTTP_200, falcon.HTTP_200),
(falcon.HTTP_307, falcon.HTTP_307),
(falcon.HTTP_404, falcon.HTTP_404),
(123, '123 Unknown'),
('123 Wow Such Status', '123 Wow Such Status'),
(b'123 Wow Such Status', '123 Wow Such Status'),
(b'200 OK', falcon.HTTP_OK),
(http.HTTPStatus(200), falcon.HTTP_200),
(http.HTTPStatus(307), falcon.HTTP_307),
(http.HTTPStatus(401), falcon.HTTP_401),
(http.HTTPStatus(410), falcon.HTTP_410),
(http.HTTPStatus(429), falcon.HTTP_429),
(http.HTTPStatus(500), falcon.HTTP_500),
]
)
def test_code_to_http_status(self, v_in, v_out):
assert falcon.code_to_http_status(v_in) == v_out

@pytest.mark.parametrize(
'v',
['not_a_number', 0, '0', 99, '99', '404.3', -404.3, '-404', '-404.3']
[0, 13, 99, 1000, 1337.01, -99, -404.3, -404, -404.3]
)
def test_code_to_http_status_neg(self, v):
def test_code_to_http_status_value_error(self, v):
with pytest.raises(ValueError):
falcon.code_to_http_status(v)

Expand Down