diff --git a/.github/workflows/test-dist.yaml b/.github/workflows/test-dist.yaml new file mode 100644 index 000000000..5f64e176a --- /dev/null +++ b/.github/workflows/test-dist.yaml @@ -0,0 +1,46 @@ +# Test source distribution and pure-Python wheel. +name: test-dist + +on: + push: + branches: + - "*" + +jobs: + test-dist: + name: test-${{ matrix.build }} + runs-on: ubuntu-latest + strategy: + matrix: + build: + - sdist + - wheel + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build dist + env: + FALCON_DISABLE_CYTHON: "Y" + run: | + pip install --upgrade pip + pip install --upgrade build + python -m build --${{ matrix.build }} + + - name: Test sdist + if: matrix.build == 'sdist' + run: | + tools/test_dist.py dist/*.tar.gz + + - name: Test pure-Python wheel + if: matrix.build == 'wheel' + run: | + tools/test_dist.py dist/*.whl diff --git a/.github/workflows/tox-sdist.yaml b/.github/workflows/tox-sdist.yaml index d890223f1..bcccd9e5e 100644 --- a/.github/workflows/tox-sdist.yaml +++ b/.github/workflows/tox-sdist.yaml @@ -9,7 +9,7 @@ on: jobs: run_tox: - name: tox (default envlist on ${{matrix.python-version}}) + name: tox (python${{ matrix.python-version }}) runs-on: "ubuntu-latest" strategy: fail-fast: false diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5d3177879..20221e8d0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,7 +13,8 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py - +formats: + - pdf # We recommend specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: diff --git a/docs/changes/4.0.0.rst b/docs/changes/4.0.0.rst index c3e9d426a..ac078fad5 100644 --- a/docs/changes/4.0.0.rst +++ b/docs/changes/4.0.0.rst @@ -4,30 +4,45 @@ Changelog for Falcon 4.0.0 Summary ------- -Falcon ``4.0.0b4`` is hopefully the final beta release before moving forward to -a release candidate. - -As Falcon 4.0 is now feature-complete, we would really be thankful if you -could test this beta release with your apps, and -:ref:`let us know if you run into any issues `! -Please also check the list of **breaking changes** below. - -If you make use of type annotations in your Falcon app, please run your type -checker of choice without any *typeshed* extensions for Falcon, and -:ref:`report back to us ` how it went! - -As always, you can grab the new release -`from PyPI `__:: - - pip install falcon==4.0.0b4 - -(Alternatively, continue reading these docs for more -:ref:`installation options `.) - -This release would have not been possible without contributions from the -fantastic group of 30 community members and maintainers. - -Thank You! +We are happy to present Falcon 4.0, a new major version of the framework that +brings a couple of commonly requested features including support for matching +multiple path segments (using :class:`~falcon.routing.PathConverter`), and +a fully typed codebase. (Please read more about typing in the notes below.) + +The timeframe for Falcon 4.0 was challenging due to the need to balance our +high standards with the CPython 3.13 timeline. We aimed to deliver the main +development branch in this release, without resorting to another compatibility +micro update (as we did with Falcon 3.1.1-3.1.3). Following community feedback, +we also want to improve our overall release schedule by shipping smaller +increments more often. +To support this goal, we have made several tooling and testing improvements: +the build process for :ref:`binary wheels ` has been simplified +using `cibuildwheel `__, and our test suite now +only requires ``pytest`` as a hard dependency. Additionally, you can run +``pytest`` against our tests from any directory. We hope that these changes +should also benefit packaging Falcon in Linux distributions. + +As with every SemVer major release, we have removed a number of previously +deprecated functions, classes, compatibility shims, as well as made other +potentially breaking changes that we could not risk in a minor version. +If you have been paying attention the deprecation warnings from the 3.x series, +the impact should be minimal, but please do take a look at the list of breaking +changes below. + +This release would not have been possible without the numerous contributions +from our community. This release alone comprises a number of pull requests +submitted by a group of 30 talented individuals. What is more, we were +particularly impressed by the high-quality discussions and code submissions +during our +`EuroPython 2024 Sprint `__. +Some notable sprint contributions include CHIPS support, and a new +:ref:`WebSocket Tutorial `, among others. +In fact, according to the +`statistics on GitHub `__, +we are thrilled to report that the total number of Falcon +contributors has now exceeded 200. We find it fascinating that our framework +has become a collaborative effort involving so many individuals, and would like +to thank everyone who has made this release possible! Changes to Supported Platforms @@ -43,14 +58,16 @@ Changes to Supported Platforms (`#2074 `__, `#2273 `__) - End-of-life Python 3.8 is no longer actively supported, but - the framework should still continue to install from source and function. + the framework should still continue to install from the pure-Python wheel or + source distribution, and function normally. - The Falcon 4.x series is guaranteed to support CPython 3.10 and PyPy3.10 (v7.3.16). This means that we may drop the support for Python 3.8 & 3.9 altogether in a later 4.x release, especially if we are faced with incompatible ecosystem changes in typing, Cython, etc. -Typing support + +Typing Support -------------- Type checking support was introduced in version 4.0. While most of the library is @@ -79,6 +96,46 @@ runtime behavior, but may surface new or different errors with type checkers. Also, make sure to :ref:`let us know ` which essential aliases are missing from the public interface! +Known typing limitations +^^^^^^^^^^^^^^^^^^^^^^^^ + +Falcon's emphasis on flexibility and performance has presented 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 `. + +For instance, the following application might unexpectedly not pass type +checking: + +.. code-block:: python + + from typing import Any + + from falcon import App, HTTPInternalServerError, Request, Response + + + class MyRequest(Request): + ... + + + def handle_os_error(req: MyRequest, resp: Response, ex: Exception, + params: dict[str, Any]) -> None: + raise HTTPInternalServerError(title='OS error!') from ex + + + app = App(request_type=MyRequest) + app.add_error_handler(OSError, handle_os_error) + +(Please also see the following GitHub issue: +`#2372 `__.) + +.. important:: + This is only a typing limitation that has no effect outside of type + checking -- the above ``app`` will run just fine! + + Breaking Changes ---------------- @@ -407,12 +464,6 @@ Misc Contributors to this Release ---------------------------- -.. note:: - If we missed you below, don’t worry! - - We will refresh the full list of contributors before the 4.0.0 final - release. - Many thanks to all of our talented and stylish contributors for this release! - `aarcex3 `__ diff --git a/docs/changes/4.0.1.rst b/docs/changes/4.0.1.rst new file mode 100644 index 000000000..2541b0c53 --- /dev/null +++ b/docs/changes/4.0.1.rst @@ -0,0 +1,17 @@ +Changelog for Falcon 4.0.1 +========================== + +Summary +------- + +This is a minor point release addressing a Python distribution issue in +Falcon 4.0.0. + + +Fixed +----- + +- Installing Falcon 4.0.0 unexpectedly copies many unintended directories from the + source tree to the venv's ``site-packages``. This issue has been rectified, and + our CI has been extended with new tests (that verify what is actually installed + from the distribution) to make sure this regression does not resurface. (`#2384 `__) diff --git a/docs/changes/4.1.0.rst b/docs/changes/4.1.0.rst new file mode 100644 index 000000000..c9a6f005a --- /dev/null +++ b/docs/changes/4.1.0.rst @@ -0,0 +1,25 @@ +Changelog for Falcon 4.1.0 +========================== + +Summary +------- + +Falcon 4.1 is in development. The progress is tracked via the +`Version 4.1 milestone `__ +on GitHub. + + +Changes to Supported Platforms +------------------------------ + +.. NOTE(vytas): No changes to the supported platforms (yet). + + +.. towncrier release notes start + +Contributors to this Release +---------------------------- + +Many thanks to all of our talented and stylish contributors for this release! + +- `vytas7 `__ diff --git a/docs/changes/index.rst b/docs/changes/index.rst index 06371e311..cec3621d2 100644 --- a/docs/changes/index.rst +++ b/docs/changes/index.rst @@ -3,6 +3,8 @@ Changelogs .. toctree:: + 4.1.0 <4.1.0> + 4.0.1 <4.0.1> 4.0.0 <4.0.0> 3.1.3 <3.1.3> 3.1.2 <3.1.2> diff --git a/docs/user/install.rst b/docs/user/install.rst index 9c5ae3e20..8de6b3690 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -127,7 +127,7 @@ Binary Wheels ^^^^^^^^^^^^^ Binary Falcon wheels are automatically built for many CPython platforms, -courtesy of `cibuildwheel `__. +courtesy of `cibuildwheel `__. .. wheels:: .github/workflows/cibuildwheel.yaml diff --git a/docs/user/recipes/request-id.rst b/docs/user/recipes/request-id.rst index 9274a5ad8..7a69b8336 100644 --- a/docs/user/recipes/request-id.rst +++ b/docs/user/recipes/request-id.rst @@ -10,14 +10,14 @@ to every log entry. If you wish to trace each request throughout your application, including from within components that are deeply nested or otherwise live outside of the -normal request context, you can use a `thread-local`_ context object to store +normal request context, you can use a `contextvars`_ object to store the request ID: .. literalinclude:: ../../../examples/recipes/request_id_context.py :language: python Then, you can create a :ref:`middleware ` class to generate a -unique ID for each request, persisting it in the thread local: +unique ID for each request, persisting it in the `contextvars` object: .. literalinclude:: ../../../examples/recipes/request_id_middleware.py :language: python @@ -48,4 +48,4 @@ In a pinch, you can also output the request ID directly: .. literalinclude:: ../../../examples/recipes/request_id_log.py :language: python -.. _thread-local: https://docs.python.org/3/library/threading.html#thread-local-data +.. _contextvars: https://docs.python.org/3/library/contextvars.html diff --git a/examples/recipes/request_id_context.py b/examples/recipes/request_id_context.py index d071c904d..a7e36ddc3 100644 --- a/examples/recipes/request_id_context.py +++ b/examples/recipes/request_id_context.py @@ -1,19 +1,19 @@ # context.py -import threading +import contextvars class _Context: def __init__(self): - self._thread_local = threading.local() + self._request_id_var = contextvars.ContextVar('request_id', default=None) @property def request_id(self): - return getattr(self._thread_local, 'request_id', None) + return self._request_id_var.get() @request_id.setter def request_id(self, value): - self._thread_local.request_id = value + self._request_id_var.set(value) ctx = _Context() diff --git a/falcon/version.py b/falcon/version.py index a10f53d7a..21a355c2c 100644 --- a/falcon/version.py +++ b/falcon/version.py @@ -14,5 +14,5 @@ """Falcon version.""" -__version__ = '4.0.0b4' +__version__ = '4.1.0.dev1' """Current version of Falcon.""" diff --git a/pyproject.toml b/pyproject.toml index 4fe30640a..ac71c10c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ zip-safe = false version = {attr = "falcon.version.__version__"} [tool.setuptools.packages.find] -exclude = ["examples", "tests"] +include = ["falcon*"] [tool.mypy] exclude = [ @@ -116,7 +116,7 @@ exclude = ["examples", "tests"] [tool.towncrier] package = "falcon" package_dir = "" - filename = "docs/changes/4.0.0.rst" + filename = "docs/changes/4.1.0.rst" directory = "docs/_newsfragments" issue_format = "`#{issue} `__" diff --git a/tools/test_dist.py b/tools/test_dist.py index a0dc967b5..6b35f6e9b 100755 --- a/tools/test_dist.py +++ b/tools/test_dist.py @@ -15,22 +15,43 @@ REQUIREMENTS = FALCON_ROOT / 'requirements' / 'cibwtest' TESTS = FALCON_ROOT / 'tests' +EXPECTED_SCRIPTS = set({'falcon-bench', 'falcon-inspect-app', 'falcon-print-routes'}) +EXPECTED_PACKAGES = set({'falcon'}) + def test_package(package): with tempfile.TemporaryDirectory() as tmpdir: venv = pathlib.Path(tmpdir) / 'venv' + venv_bin = venv / 'bin' + venv_pip = venv_bin / 'pip' subprocess.check_call((sys.executable, '-m', 'venv', venv)) logging.info(f'Created a temporary venv in {venv}.') - subprocess.check_call((venv / 'bin' / 'pip', 'install', '--upgrade', 'pip')) - subprocess.check_call((venv / 'bin' / 'pip', 'install', '-r', REQUIREMENTS)) + subprocess.check_call((venv_pip, 'install', '--upgrade', 'pip')) + subprocess.check_call((venv_pip, 'install', '-r', REQUIREMENTS)) logging.info(f'Installed test requirements in {venv}.') - subprocess.check_call( - (venv / 'bin' / 'pip', 'install', package), - ) + + (venv_site_pkg,) = venv.glob('lib/python*/site-packages') + bin_before = {path.name for path in venv_bin.iterdir()} + pkg_before = {path.name for path in venv_site_pkg.iterdir()} + + subprocess.check_call((venv_pip, 'install', package)) logging.info(f'Installed {package} into {venv}.') - subprocess.check_call((venv / 'bin' / 'pytest', TESTS), cwd=venv) + bin_after = {path.name for path in venv_bin.iterdir()} + assert bin_after - bin_before == EXPECTED_SCRIPTS, ( + f'Unexpected scripts installed in {venv_bin} from {package}: ' + f'{bin_after - bin_before - EXPECTED_SCRIPTS}' + ) + pkg_after = { + path.name for path in venv_site_pkg.iterdir() if path.suffix != '.dist-info' + } + assert pkg_after - pkg_before == EXPECTED_PACKAGES, ( + f'Unexpected packages installed in {venv_site_pkg} from {package}: ' + f'{pkg_after - pkg_before - EXPECTED_PACKAGES}' + ) + + subprocess.check_call((venv_bin / 'pytest', TESTS), cwd=venv) logging.info(f'{package} passes tests.')