Skip to content

Commit

Permalink
Merge branch 'master' into jsonschema_rs
Browse files Browse the repository at this point in the history
  • Loading branch information
vytas7 committed Apr 10, 2024
2 parents ce2019f + 5242217 commit 656a532
Show file tree
Hide file tree
Showing 26 changed files with 255 additions and 71 deletions.
22 changes: 11 additions & 11 deletions .github/workflows/tests.yaml
Expand Up @@ -18,7 +18,7 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.12"
os:
- "ubuntu-latest"
toxenv:
Expand All @@ -28,9 +28,9 @@ jobs:
- "pep8-docstrings"
- "mypy"
- "mypy_tests"
- "py310"
- "py310_sans_msgpack"
- "py310_cython"
- "py312"
- "py312_sans_msgpack"
- "py312_cython"
- "docs"
- "towncrier"
- "look"
Expand Down Expand Up @@ -82,12 +82,12 @@ jobs:
- python-version: "3.12"
os: ubuntu-latest
toxenv: py312_cython
- python-version: "3.10"
- python-version: "3.12"
os: macos-latest
toxenv: py310_nocover
- python-version: "3.10"
toxenv: py312_nocover
- python-version: "3.12"
os: windows-latest
toxenv: py310_nocover
toxenv: py312_nocover
# These env require 3.8 and 20.04, see tox.ini
- python-version: "3.8"
os: ubuntu-20.04
Expand Down Expand Up @@ -133,14 +133,14 @@ jobs:
run: tox -e ${{ matrix.toxenv }}

- name: Combine coverage
if: ${{ matrix.toxenv == 'py310' || matrix.toxenv == 'py310_sans_msgpack' }}
if: ${{ matrix.toxenv == 'py312' || matrix.toxenv == 'py312_sans_msgpack' }}
run: |
coverage --version
coverage combine
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
if: ${{ matrix.toxenv == 'py310' || matrix.toxenv == 'py310_sans_msgpack' }}
uses: codecov/codecov-action@v4
if: ${{ matrix.toxenv == 'py312' || matrix.toxenv == 'py312_sans_msgpack' }}
with:
env_vars: PYTHON
fail_ci_if_error: true
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Expand Up @@ -37,7 +37,7 @@ $ pip install -U blue
$ blue .
```

You can check all this by running ``tox`` from within the Falcon project directory. Your environment must be based on CPython 3.8, 3.10 or 3.11:
You can check all this by running ``tox`` from within the Falcon project directory. Your environment must be based on CPython 3.8, 3.10, 3.11 or 3.12:

```bash
$ pip install -U tox
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -1027,7 +1027,7 @@ See also: `CONTRIBUTING.md <https://github.com/falconry/falcon/blob/master/CONTR
Legal
-----

Copyright 2013-2023 by Individual and corporate contributors as
Copyright 2013-2024 by Individual and corporate contributors as
noted in the individual source files.

Licensed under the Apache License, Version 2.0 (the "License"); you may
Expand Down
4 changes: 4 additions & 0 deletions docs/_newsfragments/2066.newandimproved.rst
@@ -0,0 +1,4 @@
In Python 3.13, the ``cgi`` module is removed entirely from the stdlib,
including its ``parse_header()`` method. Falcon addresses the issue by shipping
an own implementation; :func:`falcon.parse_header` can also be used in your projects
affected by the removal.
5 changes: 5 additions & 0 deletions docs/api/util.rst
Expand Up @@ -34,6 +34,11 @@ HTTP Status
.. autofunction:: falcon.code_to_http_status
.. autofunction:: falcon.get_http_status

Media types
-----------

.. autofunction:: falcon.parse_header

Async
-----

Expand Down
2 changes: 1 addition & 1 deletion docs/changes/4.0.0.rst
Expand Up @@ -13,7 +13,7 @@ Changes to Supported Platforms
------------------------------

- CPython 3.11 is now fully supported. (`#2072 <https://github.com/falconry/falcon/issues/2072>`__)
- CPython 3.12 will be fully supported. (`#2196 <https://github.com/falconry/falcon/issues/2196>`__)
- CPython 3.12 is now fully supported. (`#2196 <https://github.com/falconry/falcon/issues/2196>`__)
- End-of-life Python 3.5 & 3.6 are no longer supported. (`#2074 <https://github.com/falconry/falcon/pull/2074>`__)
- Python 3.7 is no longer actively supported, but the framework should still
continue to install from source. We may remove the support for 3.7 altogether
Expand Down
3 changes: 1 addition & 2 deletions docs/user/recipes/pretty-json.rst
Expand Up @@ -52,7 +52,6 @@ implemented with a :ref:`custom media handler <custom-media-handler-type>`:

.. code:: python
import cgi
import json
import falcon
Expand All @@ -66,7 +65,7 @@ implemented with a :ref:`custom media handler <custom-media-handler-type>`:
return json.loads(data.decode())
def serialize(self, media, content_type):
_, params = cgi.parse_header(content_type)
_, params = falcon.parse_header(content_type)
indent = params.get('indent')
if indent is not None:
try:
Expand Down
13 changes: 6 additions & 7 deletions docs/user/tutorial-asgi.rst
Expand Up @@ -685,10 +685,10 @@ small files littering our storage, it consumes CPU resources, and we would
soon find our application crumbling under load.

Let's mitigate this problem with response caching. We'll use Redis, taking
advantage of `aioredis <https://github.com/aio-libs/aioredis>`_ for async
advantage of `redis <https://redis.readthedocs.io/en/stable/examples/asyncio_examples.html>`_ for async
support::

pip install aioredis
pip install redis

We will also need to serialize response data (the ``Content-Type`` header and
the body in the first version); ``msgpack`` should do::
Expand All @@ -700,7 +700,7 @@ installing Redis server on your machine, one could also:

* Spin up Redis in Docker, eg::

docker run -p 6379:6379 redis
docker run -p 6379:6379 redis/redis-stack:latest

* Assuming Redis is installed on the machine, one could also try
`pifpaf <https://github.com/jd/pifpaf>`_ for spinning up Redis just
Expand Down Expand Up @@ -747,9 +747,8 @@ implementations for production and testing.
``self.redis_host``. Such a design might prove helpful for apps that
need to create client connections in more than one place.

Assuming we call our new :ref:`configuration <asgi_tutorial_config>` items
``redis_host`` and ``redis_from_url()``, respectively, the final version of
``config.py`` now reads:
Assuming we call our new :ref:`configuration <asgi_tutorial_config>` item
``redis_host`` the final version of ``config.py`` now reads:

.. literalinclude:: ../../examples/asgilook/asgilook/config.py
:language: python
Expand Down Expand Up @@ -860,7 +859,7 @@ any problems with importing local utility modules or checking code coverage::
$ mkdir -p tests
$ touch tests/__init__.py

Next, let's implement fixtures to replace ``uuid`` and ``aioredis``, and inject them
Next, let's implement fixtures to replace ``uuid`` and ``redis``, and inject them
into our tests via ``conftest.py`` (place your code in the newly created ``tests``
directory):

Expand Down
3 changes: 2 additions & 1 deletion examples/asgilook/asgilook/cache.py
@@ -1,4 +1,5 @@
import msgpack
import redis.asyncio as redis


class RedisCache:
Expand All @@ -24,7 +25,7 @@ async def process_startup(self, scope, event):
await self._redis.ping()

async def process_shutdown(self, scope, event):
await self._redis.close()
await self._redis.aclose()

async def process_request(self, req, resp):
resp.context.cached = False
Expand Down
7 changes: 3 additions & 4 deletions examples/asgilook/asgilook/config.py
@@ -1,15 +1,14 @@
import os
import pathlib
import redis.asyncio
import uuid

import aioredis


class Config:
DEFAULT_CONFIG_PATH = '/tmp/asgilook'
DEFAULT_MIN_THUMB_SIZE = 64
DEFAULT_REDIS_FROM_URL = redis.asyncio.from_url
DEFAULT_REDIS_HOST = 'redis://localhost'
DEFAULT_REDIS_FROM_URL = aioredis.from_url
DEFAULT_UUID_GENERATOR = uuid.uuid4

def __init__(self):
Expand All @@ -18,7 +17,7 @@ def __init__(self):
)
self.storage_path.mkdir(parents=True, exist_ok=True)

self.redis_from_url = Config.DEFAULT_REDIS_FROM_URL
self.min_thumb_size = self.DEFAULT_MIN_THUMB_SIZE
self.redis_from_url = Config.DEFAULT_REDIS_FROM_URL
self.redis_host = self.DEFAULT_REDIS_HOST
self.uuid_generator = Config.DEFAULT_UUID_GENERATOR
2 changes: 1 addition & 1 deletion examples/asgilook/requirements/asgilook
@@ -1,4 +1,4 @@
aiofiles>=0.4.0
aioredis>=2.0
redis>=5.0
msgpack
Pillow>=6.0.0
1 change: 1 addition & 0 deletions falcon/__init__.py
Expand Up @@ -77,6 +77,7 @@
from falcon.util import IS_64_BITS
from falcon.util import is_python_func
from falcon.util import misc
from falcon.util import parse_header
from falcon.util import reader
from falcon.util import runs_sync
from falcon.util import secure_filename
Expand Down
5 changes: 2 additions & 3 deletions falcon/asgi/multipart.py
Expand Up @@ -14,11 +14,10 @@

"""ASGI multipart form media handler components."""

import cgi

from falcon.asgi.reader import BufferedReader
from falcon.errors import DelimiterError
from falcon.media import multipart
from falcon.util.mediatypes import parse_header

_ALLOWED_CONTENT_HEADERS = multipart._ALLOWED_CONTENT_HEADERS
_CRLF = multipart._CRLF
Expand Down Expand Up @@ -54,7 +53,7 @@ async def get_media(self):
return self._media

async def get_text(self):
content_type, options = cgi.parse_header(self.content_type)
content_type, options = parse_header(self.content_type)
if content_type != 'text/plain':
return None

Expand Down
10 changes: 5 additions & 5 deletions falcon/media/multipart.py
Expand Up @@ -14,7 +14,6 @@

"""Multipart form media handler."""

import cgi
import re
from urllib.parse import unquote_to_bytes

Expand All @@ -24,6 +23,7 @@
from falcon.stream import BoundedStream
from falcon.util import BufferedReader
from falcon.util import misc
from falcon.util.mediatypes import parse_header


# TODO(vytas):
Expand Down Expand Up @@ -249,7 +249,7 @@ def get_text(self):
str: The part decoded as a text string provided the part is
encoded as ``text/plain``, ``None`` otherwise.
"""
content_type, options = cgi.parse_header(self.content_type)
content_type, options = parse_header(self.content_type)
if content_type != 'text/plain':
return None

Expand All @@ -275,7 +275,7 @@ def filename(self):

if self._content_disposition is None:
value = self._headers.get(b'content-disposition', b'')
self._content_disposition = cgi.parse_header(value.decode())
self._content_disposition = parse_header(value.decode())

_, params = self._content_disposition

Expand Down Expand Up @@ -311,7 +311,7 @@ def name(self):

if self._content_disposition is None:
value = self._headers.get(b'content-disposition', b'')
self._content_disposition = cgi.parse_header(value.decode())
self._content_disposition = parse_header(value.decode())

_, params = self._content_disposition
self._name = params.get('name')
Expand Down Expand Up @@ -493,7 +493,7 @@ def __init__(self, parse_options=None):
def _deserialize_form(
self, stream, content_type, content_length, form_cls=MultipartForm
):
_, options = cgi.parse_header(content_type)
_, options = parse_header(content_type)
try:
boundary = options['boundary']
except KeyError:
Expand Down
4 changes: 2 additions & 2 deletions falcon/testing/helpers.py
Expand Up @@ -23,7 +23,6 @@
"""

import asyncio
import cgi
from collections import defaultdict
from collections import deque
import contextlib
Expand Down Expand Up @@ -51,6 +50,7 @@
from falcon.constants import SINGLETON_HEADERS
import falcon.request
from falcon.util import uri
from falcon.util.mediatypes import parse_header

# NOTE(kgriffs): Changed in 3.0 from 'curl/7.24.0 (x86_64-apple-darwin12.0)'
DEFAULT_UA = 'falcon-client/' + falcon.__version__
Expand Down Expand Up @@ -802,7 +802,7 @@ def get_encoding_from_headers(headers):
if not content_type:
return None

content_type, params = cgi.parse_header(content_type)
content_type, params = parse_header(content_type)

if 'charset' in params:
return params['charset'].strip('\'"')
Expand Down
1 change: 1 addition & 0 deletions falcon/util/__init__.py
Expand Up @@ -29,6 +29,7 @@
from falcon.util.deprecation import deprecated
from falcon.util.deprecation import deprecated_args
from falcon.util.deprecation import DeprecatedWarning
from falcon.util.mediatypes import parse_header
from falcon.util.misc import code_to_http_status
from falcon.util.misc import dt_to_http
from falcon.util.misc import get_argnames
Expand Down

0 comments on commit 656a532

Please sign in to comment.