Skip to content

Commit

Permalink
chore: prepare Falcon 4.0.2 (in-tree) (#2392)
Browse files Browse the repository at this point in the history
* chore: prepare 4.0.2 in-tree (WiP)

* chore: relabel #2387 as bugfix

* chore: aggregate 4.0.2 contributors

* chore: bump up version to `4.0.2`

* docs: slightly tweak newsfragment for #2387

* chore: aggregate contributors, +docs tweaks

* docs(ASGI): clean up docs on logging setup

* chore: render out newsfragments

* docs(typing); describe another limitation/inconsistency
  • Loading branch information
vytas7 authored Nov 6, 2024
1 parent ab2ce4c commit 4ac5ec6
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 117 deletions.
3 changes: 3 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ listed below by date of first contribution:
* Agustin Arce (aarcex3)
* Christian Grossmüller (chgad)
* Sai Prathik R (prathik2401)
* Akshay Awate (AkshayAwate)
* Jasper Spaans (jap)
* Alessandro Chitarrini (chitvs)

(et al.)

Expand Down
1 change: 0 additions & 1 deletion docs/_newsfragments/2365.misc.rst

This file was deleted.

7 changes: 0 additions & 7 deletions docs/_newsfragments/2387.misc.rst

This file was deleted.

16 changes: 12 additions & 4 deletions docs/api/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ Known Limitations

Falcon's emphasis on flexibility and performance presents certain
challenges when it comes to adding type annotations to the existing code base.

One notable limitation involves using custom :class:`~falcon.Request` and/or
:class:`~falcon.Response` types in callbacks that are passed back
to the framework, such as when adding an
:meth:`error handler <falcon.App.add_error_handler>`.

For instance, the following application might unexpectedly not pass type
checking:

Expand All @@ -60,12 +60,20 @@ checking:
app = App(request_type=MyRequest)
app.add_error_handler(OSError, handle_os_error)
(Please also see the following GitHub issue:
(We are working on addressing this limitation at the time of writing --
please see the following GitHub issue for the progress, and possible solutions:
`#2372 <https://github.com/falconry/falcon/issues/2372>`__.)

Another known inconsistency is the typing of the
:class:`converter interface <falcon.routing.BaseConverter>`, where certain
subclasses (such as :class:`~falcon.routing.PathConverter`) declare a different
input type than the base ``convert()`` method.
(See also the discussions and possible solutions on the GitHub issue
`#2396 <https://github.com/falconry/falcon/issues/2396>`__.)

.. important::
This is only a typing limitation that has no effect outside of type
checking -- the above ``app`` will run just fine!
The above issues are only typing limitations that have no effect outside of
type checking -- applications will work just fine at runtime!


Public Type Aliases
Expand Down
38 changes: 38 additions & 0 deletions docs/changes/4.0.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Changelog for Falcon 4.0.2
==========================

Summary
-------

This is a minor point release to fix some missed re-exports for type checkers.
In addition, we have also included a couple of documentation improvements.


Fixed
-----

- Running Mypy on code that uses parts of ``falcon.testing``
would previously lead to errors like::

Name "falcon.testing.TestClient" is not defined

This has been fixed by explicitly exporting the names that are
imported into the ``falcon.testing`` namespace. (`#2387 <https://github.com/falconry/falcon/issues/2387>`__)


Misc
----

- The printable PDF version of our documentation was enabled on Read the Docs. (`#2365 <https://github.com/falconry/falcon/issues/2365>`__)


Contributors to this Release
----------------------------

Many thanks to those who contributed to this bugfix release:

- `AkshayAwate <https://github.com/AkshayAwate>`__
- `CaselIT <https://github.com/CaselIT>`__
- `chitvs <https://github.com/chitvs>`__
- `jap <https://github.com/jap>`__
- `vytas7 <https://github.com/vytas7>`__
25 changes: 0 additions & 25 deletions docs/changes/4.1.0.rst

This file was deleted.

2 changes: 1 addition & 1 deletion docs/changes/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Changelogs

.. toctree::

4.1.0 <4.1.0>
4.0.2 <4.0.2>
4.0.1 <4.0.1>
4.0.0 <4.0.0>
3.1.3 <3.1.3>
Expand Down
49 changes: 23 additions & 26 deletions docs/user/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1348,47 +1348,44 @@ To include multiple values, simply use ``"; "`` to separate each name-value
pair. For example, if you were to pass ``{'Cookie': 'xxx=yyy; hello=world'}``,
you would get ``{'cookies': {'xxx': 'yyy', 'hello': 'world'}}``.

Why do I not see error tracebacks in ASGI applications?
-------------------------------------------------------
Why do I see no error tracebacks in my ASGI application?
--------------------------------------------------------

When using Falcon with ASGI servers like Uvicorn,
you might notice that server errors do not display a traceback by default.
This behavior differs from WSGI applications, where errors are logged to `stderr`,
providing detailed tracebacks.
When using Falcon with an ASGI server like Uvicorn,
you might notice that server errors do not include any traceback by default.
This behavior differs from WSGI, where the PEP-3333 specification defines the
`wsgi.errors <https://peps.python.org/pep-3333/#environ-variables>`__ stream
(which Falcon utilizes to log unhandled
:class:`internal server errors <falcon.HTTPInternalServerError>`).

The reason for this is that ASGI does not define a standardized way to log errors back to the application server,
unlike WSGI. Therefore, you need to configure logging manually to see these tracebacks.
Since there is no standardized way to log errors back to the ASGI server,
the framework simply opts to log them using the ``falcon``
:class:`logger <logging.Logger>`.

Here’s how to set up logging in your ASGI Falcon application to capture error tracebacks:
The easiest way to get started is configuring the root logger via
:func:`logging.basicConfig`:

.. code:: python
import logging
import falcon
import falcon.asgi
logging.basicConfig(
format="%(asctime)s [%(levelname)s] %(message)s",
level=logging.INFO
)
format="%(asctime)s [%(levelname)s] %(message)s", level=logging.INFO)
class ThingsResource:
class FaultyResource:
async def on_get(self, req, resp):
raise ValueError('foo')
app = falcon.asgi.App()
things = ThingsResource()
app.add_route('/things', things)
By adding the above logging configuration, you will see tracebacks like this in your console:
.. code-block:: none
app = falcon.asgi.App()
app.add_route('/things', FaultyResource())
[ERROR] [FALCON] Unhandled exception in ASGI app
Traceback (most recent call last):
File "<...>", line 12, in on_get
raise ValueError('foo')
ValueError: foo
By adding the above logging configuration, you should now see tracebacks logged
to :any:`stderr <sys.stderr>` when accessing ``/things``.

For additional details on this topic,
please refer to :ref:`debugging-asgi-applications`.
For additional details on this topic,
please refer to :ref:`debugging_asgi_applications`.
110 changes: 62 additions & 48 deletions docs/user/tutorial-asgi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,68 @@ Woohoo, it works!!!

Well, sort of. Onwards to adding some real functionality!

.. _debugging_asgi_applications:

Debugging ASGI Applications
---------------------------

While developing and testing a Falcon ASGI application along the lines of this
tutorial, you may encounter unexpected issues or behaviors, be it a copy-paste
mistake, an idea that didn't work out, or unusual input where validation falls
outside of the scope of this tutorial.

Unlike WSGI, the ASGI specification has no standard mechanism for logging
errors back to the application server, so Falcon falls back to the stdlib's
:mod:`logging` (using the ``falcon`` :class:`logger <logging.Logger>`).

As a well-behaved library, Falcon does not configure any loggers since that
might interfere with the user's logging setup.
Here's how you can set up basic logging in your ASGI Falcon application via
:func:`logging.basicConfig`:

.. code:: python
import logging
import falcon
logging.basicConfig(level=logging.INFO)
class ErrorResource:
def on_get(self, req, resp):
raise Exception('Something went wrong!')
app = falcon.App()
app.add_route('/error', ErrorResource())
When the above route is accessed, Falcon will catch the unhandled exception and
automatically log an error message. Below is an example of what the log output
might look like:

.. code-block:: none
ERROR:falcon.asgi.app:Unhandled exception in ASGI application
Traceback (most recent call last):
File "/path/to/your/app.py", line 123, in __call__
resp = resource.on_get(req, resp)
File "/path/to/your/app.py", line 7, in on_get
raise Exception("Something went wrong!")
Exception: Something went wrong!
.. note::
While logging is helpful for development and debugging, be mindful of
logging sensitive information. Ensure that log files are stored securely
and are not accessible to unauthorized users.

.. note::
Unhandled errors are only logged automatically by Falcon's default error
handler for :class:`Exception`. If you
:meth:`replace this handler <falcon.asgi.App.add_error_handler>` with your
own generic :class:`Exception` handler, you are responsible for logging or
reporting these errors yourself.

.. _asgi_tutorial_config:

Configuration
Expand Down Expand Up @@ -963,54 +1025,6 @@ adding ``--cov-fail-under=100`` (or any other percent threshold) to our
tests in multiple environments would most probably involve running
``coverage`` directly, and combining results.

.. _debugging-asgi-applications:

Debugging ASGI Applications
---------------------------
(This section also applies to WSGI applications)

While developing and testing ASGI applications, understanding how to configure
and utilize logging can be helpful, especially when you encounter unexpected
issues or behaviors.

By default, Falcon does not set up logging for you,
but Python's built-in :mod:`logging` module provides a flexible framework for
emitting and capturing log messages. Here's how you can set up basic logging in
your ASGI Falcon application:

.. code:: python
import logging
import falcon
logging.basicConfig(level=logging.INFO)
class ErrorResource:
def on_get(self, req, resp):
raise Exception('Something went wrong!')
app = falcon.App()
app.add_route('/error', ErrorResource())
When the above route is accessed, Falcon will catch the unhandled exception and
automatically log an error message. Below is an example of what the log output
might look like:

.. code-block:: none
ERROR:falcon.asgi.app:Unhandled exception in ASGI application
Traceback (most recent call last):
File "path/to/falcon/app.py", line 123, in __call__
resp = resource.on_get(req, resp)
File "/path/to/your/app.py", line 7, in on_get
raise Exception("Something went wrong!")
Exception: Something went wrong!
.. note::
While logging is helpful for development and debugging, be mindful of logging
sensitive information. Ensure that log files are stored securely and are not
accessible to unauthorized users.

What Now?
---------

Expand Down
17 changes: 14 additions & 3 deletions falcon/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,28 @@
from falcon.asgi import SSEvent

Headers = Dict[str, str]
"""Headers dictionary returned by the framework."""
"""Headers dictionary returned by the framework.
.. versionadded:: 4.0
"""


# WSGI
class ReadableIO(Protocol):
"""File-like protocol that defines only a read method."""
"""File-like protocol that defines only a read method.
.. versionadded:: 4.0
"""

def read(self, n: Optional[int] = ..., /) -> bytes: ...


# ASGI
class AsyncReadableIO(Protocol):
"""Async file-like protocol that defines only a read method, and is iterable."""
"""Async file-like protocol that defines only a read method, and is iterable.
.. versionadded:: 4.0
"""

async def read(self, n: Optional[int] = ..., /) -> bytes: ...
def __aiter__(self) -> AsyncIterator[bytes]: ...
Expand All @@ -42,4 +51,6 @@ def __aiter__(self) -> AsyncIterator[bytes]: ...
SSEEmitter = AsyncIterator[Optional['SSEvent']]
"""Async generator or iterator over Server-Sent Events
(instances of :class:`falcon.asgi.SSEvent`).
.. versionadded:: 4.0
"""
2 changes: 1 addition & 1 deletion falcon/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@

"""Falcon version."""

__version__ = '4.1.0.dev1'
__version__ = '4.0.2'
"""Current version of Falcon."""
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ include = ["falcon*"]
[tool.towncrier]
package = "falcon"
package_dir = ""
filename = "docs/changes/4.1.0.rst"
filename = "docs/changes/4.0.2.rst"
directory = "docs/_newsfragments"
issue_format = "`#{issue} <https://github.com/falconry/falcon/issues/{issue}>`__"

Expand Down

0 comments on commit 4ac5ec6

Please sign in to comment.