Skip to content

Commit

Permalink
Merge branch 'master' into typing-prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
vytas7 authored Jun 8, 2023
2 parents abe7214 + 1e34bf4 commit c2768e6
Show file tree
Hide file tree
Showing 24 changed files with 351 additions and 41 deletions.
22 changes: 22 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# top-most EditorConfig file
root = true

# Unix-style newlines with a final newline; trim trailing whitespace
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

# Set default charset for sources
[*.{html,js,py,pyx}]
charset = utf-8

# 4 space indentation
[*.{py,pyx}]
indent_style = space
indent_size = 4

# 2 space indentation
[*.{yaml,yml}]
indent_style = space
indent_size = 2
6 changes: 6 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ listed below by date of first contribution:
* Christian Clauss (cclauss)
* meetshah133
* Kai Chan (kaichan1201)
* Patryk Krawaczyński (nfsec)
* Jarek Kapica (jkapica)
* TigreModerata
* John G G (john-g-g)
* Aryan Iyappan (aryaniyaps)
* Eujin Ong (euj1n0ng)

(et al.)

Expand Down
4 changes: 3 additions & 1 deletion docs/_newsfragments/2022.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
:class:`FloatConverter`:Modified existing IntConverter class and added FloatConverter class to convert string to float at runtime.
Similar to the existing :class:`~falcon.routing.IntConverter`, a new
:class:`~falcon.routing.FloatConverter` has been added, allowing to convert
path segments to ``float``.
6 changes: 6 additions & 0 deletions docs/_newsfragments/2110.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Following the recommendation from
`RFC 9239 <https://www.rfc-editor.org/rfc/rfc9239>`__, the
:ref:`MEDIA_JS <media_type_constants>` constant has been updated to
``text/javascript``. Furthermore, this and other media type constants are now
preferred to the stdlib's :mod:`mimetypes` for the initialization of
:attr:`~falcon.ResponseOptions.static_media_types`.
6 changes: 6 additions & 0 deletions docs/_newsfragments/2146.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

:ref:`WebSocket <ws>` implementation has been fixed to properly handle
:class:`~falcon.HTTPError` and :class:`~falcon.HTTPStatus` exceptions raised by
custom :func:`error handlers <falcon.asgi.App.add_error_handler>`.
The WebSocket connection is now correctly closed with an appropriate code
instead of bubbling up an unhandled error to the application server.
4 changes: 4 additions & 0 deletions docs/_newsfragments/2147.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Previously, importing :class:`~falcon.testing.TestCase` as a top-level
attribute in a test module could make ``pytest`` erroneously attempt to collect
its methods as test cases. This has now been prevented by adding a ``__test__``
attribute (set to ``False``) to the :class:`~falcon.testing.TestCase` class.
2 changes: 1 addition & 1 deletion docs/_newsfragments/648.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
A new ``path`` :class:`converter <~falcon.routing.PathConverter>`
A new ``path`` :class:`converter <falcon.routing.PathConverter>`
capable of matching segments that include ``/`` was added.
3 changes: 3 additions & 0 deletions docs/api/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ Built-in Converters
.. autoclass:: falcon.routing.IntConverter
:members:

.. autoclass:: falcon.routing.FloatConverter
:members:

.. autoclass:: falcon.routing.UUIDConverter
:members:

Expand Down
9 changes: 9 additions & 0 deletions docs/changes/4.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,20 @@ Contributors to this Release

Many thanks to all of our talented and stylish contributors for this release!

- `aryaniyaps <https://github.com/aryaniyaps>`__
- `CaselIT <https://github.com/CaselIT>`__
- `cclauss <https://github.com/cclauss>`__
- `euj1n0ng <https://github.com/euj1n0ng>`__
- `jkapica <https://github.com/jkapica>`__
- `jkklapp <https://github.com/jkklapp>`__
- `john-g-g <https://github.com/john-g-g>`__
- `kaichan1201 <https://github.com/kaichan1201>`__
- `kgriffs <https://github.com/kgriffs>`__
- `meetshah133 <https://github.com/meetshah133>`__
- `mgorny <https://github.com/mgorny>`__
- `mihaitodor <https://github.com/mihaitodor>`__
- `nfsec <https://github.com/nfsec>`__
- `RioAtHome <https://github.com/RioAtHome>`__
- `TigreModerata <https://github.com/TigreModerata>`__
- `vgerak <https://github.com/vgerak>`__
- `vytas7 <https://github.com/vytas7>`__
38 changes: 27 additions & 11 deletions falcon/asgi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from .request import Request
from .response import Response
from .structures import SSEvent
from .ws import http_status_to_ws_code
from .ws import WebSocket
from .ws import WebSocketOptions

Expand Down Expand Up @@ -1032,30 +1033,45 @@ def _prepare_middleware(self, middleware=None, independent_middleware=False):
asgi=True,
)

async def _http_status_handler(self, req, resp, status, params):
self._compose_status_response(req, resp, status)
async def _http_status_handler(self, req, resp, status, params, ws=None):
if resp:
self._compose_status_response(req, resp, status)
elif ws:
code = http_status_to_ws_code(status.status)
falcon._logger.error(
'[FALCON] HTTPStatus %s raised while handling WebSocket. '
'Closing with code %s',
status,
code,
)
await ws.close(code)
else:
raise NotImplementedError('resp or ws expected')

async def _http_error_handler(self, req, resp, error, params, ws=None):
if resp:
self._compose_error_response(req, resp, error)

if ws:
elif ws:
code = http_status_to_ws_code(error.status_code)
falcon._logger.error(
'[FALCON] WebSocket handshake rejected due to raised HTTP error: %s',
'[FALCON] HTTPError %s raised while handling WebSocket. '
'Closing with code %s',
error,
code,
)

code = 3000 + error.status_code
await ws.close(code)
else:
raise NotImplementedError('resp or ws expected')

async def _python_error_handler(self, req, resp, error, params, ws=None):
falcon._logger.error('[FALCON] Unhandled exception in ASGI app', exc_info=error)

if resp:
self._compose_error_response(req, resp, falcon.HTTPInternalServerError())

if ws:
elif ws:
await self._ws_cleanup_on_error(ws)
else:
raise NotImplementedError('resp or ws expected')

async def _ws_disconnected_error_handler(self, req, resp, error, params, ws):
falcon._logger.debug(
Expand Down Expand Up @@ -1100,9 +1116,9 @@ async def _handle_exception(self, req, resp, ex, params, ws=None):
await err_handler(req, resp, ex, params, **kwargs)

except HTTPStatus as status:
self._compose_status_response(req, resp, status)
await self._http_status_handler(req, resp, status, params, ws=ws)
except HTTPError as error:
self._compose_error_response(req, resp, error)
await self._http_error_handler(req, resp, error, params, ws=ws)

return True

Expand Down
5 changes: 5 additions & 0 deletions falcon/asgi/ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,3 +701,8 @@ async def _pump(self):
if self._pop_message_waiter is not None:
self._pop_message_waiter.set_result(None)
self._pop_message_waiter = None


def http_status_to_ws_code(http_status: int) -> int:
"""Convert the provided http status to a websocket close code by adding 3000."""
return http_status + 3000
36 changes: 28 additions & 8 deletions falcon/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,10 @@
# contrary to the RFCs.
MEDIA_XML = 'application/xml'

# NOTE(kgriffs): RFC 4329 recommends application/* over text/.
# furthermore, parsers are required to respect the Unicode
# encoding signature, if present in the document, and to default
# to UTF-8 when not present. Note, however, that implementations
# are not required to support anything besides UTF-8, so it is
# unclear how much utility an encoding signature (or the charset
# parameter for that matter) has in practice.
MEDIA_JS = 'application/javascript'
# NOTE(euj1n0ng): According to RFC 9239, Changed the intended usage of the
# media type "text/javascript" from OBSOLETE to COMMON. Changed
# the intended usage for all other script media types to obsolete.
MEDIA_JS = 'text/javascript'

# NOTE(kgriffs): According to RFC 6838, most text media types should
# include the charset parameter.
Expand Down Expand Up @@ -141,6 +137,30 @@
]
)

# NOTE(vytas): We strip the preferred charsets from the default static file
# type mapping as it is hard to make any assumptions without knowing which
# files are going to be served. Moreover, the popular web servers (like
# Nginx) do not try to guess either.
_DEFAULT_STATIC_MEDIA_TYPES = tuple(
(ext, media_type.split(';', 1)[0])
for ext, media_type in (
('.bmp', MEDIA_BMP),
('.gif', MEDIA_GIF),
('.htm', MEDIA_HTML),
('.html', MEDIA_HTML),
('.jpeg', MEDIA_JPEG),
('.jpg', MEDIA_JPEG),
('.js', MEDIA_JS),
('.json', MEDIA_JSON),
('.mjs', MEDIA_JS),
('.png', MEDIA_PNG),
('.txt', MEDIA_TEXT),
('.xml', MEDIA_XML),
('.yaml', MEDIA_YAML),
('.yml', MEDIA_YAML),
)
)

# NOTE(kgriffs): Special singleton to be used internally whenever using
# None would be ambiguous.
_UNSET = object()
Expand Down
2 changes: 1 addition & 1 deletion falcon/forwarded.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class Forwarded:
the proxy.
"""

# NOTE(kgriffs): Use "client" since "for" is a keyword, and
# NOTE(kgriffs): Use "src" since "for" is a keyword, and
# "scheme" instead of "proto" to be consistent with the
# falcon.Request interface.
__slots__ = ('src', 'dest', 'host', 'scheme')
Expand Down
4 changes: 3 additions & 1 deletion falcon/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import mimetypes
from typing import Optional

from falcon.constants import _DEFAULT_STATIC_MEDIA_TYPES
from falcon.constants import _UNSET
from falcon.constants import DEFAULT_MEDIA_TYPE
from falcon.errors import HeaderNotSupported
Expand Down Expand Up @@ -1252,4 +1253,5 @@ def __init__(self):

if not mimetypes.inited:
mimetypes.init()
self.static_media_types = mimetypes.types_map
self.static_media_types = mimetypes.types_map.copy()
self.static_media_types.update(_DEFAULT_STATIC_MEDIA_TYPES)
1 change: 1 addition & 0 deletions falcon/routing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from falcon.routing.compiled import CompiledRouterOptions
from falcon.routing.converters import BaseConverter
from falcon.routing.converters import DateTimeConverter
from falcon.routing.converters import FloatConverter
from falcon.routing.converters import IntConverter
from falcon.routing.converters import PathConverter
from falcon.routing.converters import UUIDConverter
Expand Down
5 changes: 3 additions & 2 deletions falcon/routing/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@

__all__ = (
'BaseConverter',
'IntConverter',
'DateTimeConverter',
'UUIDConverter',
'FloatConverter',
'IntConverter',
'UUIDConverter',
)


Expand Down Expand Up @@ -115,6 +115,7 @@ class FloatConverter(IntConverter):
"""Converts a field value to an float.
Identifier: `float`
Keyword Args:
min (float): Reject the value if it is less than this number.
max (float): Reject the value if it is greater than this number.
Expand Down
3 changes: 3 additions & 0 deletions falcon/testing/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1970,6 +1970,9 @@ class TestClient:
"""

# NOTE(aryaniyaps): Prevent pytest from collecting tests on the class.
__test__ = False

def __init__(self, app, headers=None):
self.app = app
self._default_headers = headers
Expand Down
3 changes: 3 additions & 0 deletions falcon/testing/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def test_get_message(self):
self.assertEqual(result.json, doc)
"""

# NOTE(vytas): Here we have to restore __test__ to allow collecting tests!
__test__ = True

def setUp(self):
super(TestCase, self).setUp()

Expand Down
4 changes: 2 additions & 2 deletions falcon/util/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,10 +460,10 @@ def code_to_http_status(status):

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

try:
# NOTE(kgriffs): We do this instead of using http.HTTPStatus since
Expand Down
32 changes: 32 additions & 0 deletions tests/asgi/test_misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# misc test for 100% coverage

from unittest.mock import MagicMock

import pytest

from falcon.asgi import App
from falcon.http_error import HTTPError
from falcon.http_status import HTTPStatus


@pytest.mark.asyncio
async def test_http_status_not_impl():
app = App()
with pytest.raises(NotImplementedError):
await app._http_status_handler(MagicMock(), None, HTTPStatus(200), {}, None)


@pytest.mark.asyncio
async def test_http_error_not_impl():
app = App()
with pytest.raises(NotImplementedError):
await app._http_error_handler(MagicMock(), None, HTTPError(400), {}, None)


@pytest.mark.asyncio
async def test_python_error_not_impl():
app = App()
with pytest.raises(NotImplementedError):
await app._python_error_handler(
MagicMock(), None, ValueError('error'), {}, None
)
Loading

0 comments on commit c2768e6

Please sign in to comment.