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

chore: support CPython 3.12, update CI jobs #2177

Merged
merged 10 commits into from
Nov 5, 2023
5 changes: 4 additions & 1 deletion .github/workflows/create-wheels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11.0-rc - 3.11"
- "3.11"
- "3.12"
architecture:
- x64

Expand Down Expand Up @@ -99,6 +100,7 @@ jobs:
- cp39-cp39
- cp310-cp310
- cp311-cp311
- cp312-cp312
architecture:
- x64

Expand Down Expand Up @@ -230,6 +232,7 @@ jobs:
- cp39-cp39
- cp310-cp310
- cp311-cp311
- cp312-cp312
architecture:
- aarch64
- s390x
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/tests-mailman.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ name: Run tests (GNU Mailman 3)
on:
# Trigger the workflow on master but also allow it to run manually.
workflow_dispatch:
push:
branches:
- master

# NOTE(vytas): Disabled as it is failing as of 2023-09.
# Maybe @maxking just needs to update the Docker image (?)
# push:
# branches:
# - master

jobs:
run_tox:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ jobs:
- python-version: "3.11"
os: ubuntu-latest
toxenv: py311_cython
- python-version: "3.12"
os: ubuntu-latest
toxenv: py312
- python-version: "3.12"
os: ubuntu-latest
toxenv: py312_cython
- python-version: "3.10"
os: macos-latest
toxenv: py310_nocover
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@

# General information about the project.
project = 'Falcon'
copyright = '{year} Falcon Contributors'.format(year=datetime.utcnow().year)
copyright = '{year} Falcon Contributors'.format(year=datetime.now().year)

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
Expand Down
6 changes: 3 additions & 3 deletions falcon/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
"""Inspect utilities for falcon applications."""
from functools import partial
import inspect
from typing import Callable
from typing import Dict
from typing import Callable # NOQA: F401
from typing import Dict # NOQA: F401
from typing import List
from typing import Optional
from typing import Type
from typing import Type # NOQA: F401

from falcon import app_helpers
from falcon.app import App
Expand Down
2 changes: 1 addition & 1 deletion falcon/routing/compiled.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from falcon.util.sync import wrap_sync_to_async

if TYPE_CHECKING:
from typing import Any
from typing import Any # NOQA: F401

_TAB_STR = ' ' * 4
_FIELD_PATTERN = re.compile(
Expand Down
22 changes: 17 additions & 5 deletions falcon/util/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,20 @@
_UNSAFE_CHARS = re.compile(r'[^a-zA-Z0-9.-]')

# PERF(kgriffs): Avoid superfluous namespace lookups
strptime: Callable[[str, str], datetime.datetime] = datetime.datetime.strptime
utcnow: Callable[[], datetime.datetime] = datetime.datetime.utcnow
_strptime: Callable[[str, str], datetime.datetime] = datetime.datetime.strptime
_utcnow: Callable[[], datetime.datetime] = functools.partial(
datetime.datetime.now, datetime.timezone.utc
)

# The above aliases were not underscored prior to Falcon 3.1.2.
strptime: Callable[[str, str], datetime.datetime] = deprecated(
'This was a private alias local to this module; '
'please reference datetime.strptime() directly.'
)(datetime.datetime.strptime)
utcnow: Callable[[], datetime.datetime] = deprecated(
'This was a private alias local to this module; '
'please reference datetime.utcnow() directly.'
)(datetime.datetime.utcnow)


# NOTE(kgriffs,vytas): This is tested in the PyPy gate but we do not want devs
Expand Down Expand Up @@ -132,7 +144,7 @@
e.g., 'Tue, 15 Nov 1994 12:45:26 GMT'.
"""

return dt_to_http(utcnow())
return dt_to_http(_utcnow())

Check warning on line 147 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L147

Added line #L147 was not covered by tests


def dt_to_http(dt: datetime.datetime) -> str:
Expand Down Expand Up @@ -176,7 +188,7 @@
# over it, and setting up exception handling blocks each
# time around the loop, in the case that we don't actually
# need to check for multiple formats.
return strptime(http_date, '%a, %d %b %Y %H:%M:%S %Z')
return _strptime(http_date, '%a, %d %b %Y %H:%M:%S %Z')

Check warning on line 191 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L191

Added line #L191 was not covered by tests

time_formats = (
'%a, %d %b %Y %H:%M:%S %Z',
Expand All @@ -188,7 +200,7 @@
# Loop through the formats and return the first that matches
for time_format in time_formats:
try:
return strptime(http_date, time_format)
return _strptime(http_date, time_format)

Check warning on line 203 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L203

Added line #L203 was not covered by tests
except ValueError:
continue

Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ filterwarnings = [
"ignore:inspect.getargspec\\(\\) is deprecated:DeprecationWarning",
"ignore:.cgi. is deprecated and slated for removal:DeprecationWarning",
"ignore:path is deprecated\\. Use files\\(\\) instead:DeprecationWarning",
"ignore:This process \\(.+\\) is multi-threaded",
"ignore:There is no current event loop",
]
testpaths = [
"tests"
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ classifiers =
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Cython
keywords =
asgi
Expand Down
13 changes: 13 additions & 0 deletions tests/asgi/test_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,13 @@ async def on_websocket(self, req, ws):
await resource.data_received.wait()
assert resource.data == sample_data

# NOTE(vytas): When testing the case where the server
# explicitly closes the connection, try to receive some data
# before closing from the client side (and potentially
# winning the async race of which side closes first).
if explicit_close_server:
await ws.receive_data()

if explicit_close_client:
await ws.close(4042)

Expand Down Expand Up @@ -1111,6 +1118,9 @@ async def on_websocket(self, req, ws):
async with conductor as c:
if accept:
async with c.simulate_ws() as ws:
# Make sure the responder has a chance to reach the raise point
for _ in range(3):
await asyncio.sleep(0)
vytas7 marked this conversation as resolved.
Show resolved Hide resolved
assert ws.closed
assert ws.close_code == exp_code
else:
Expand Down Expand Up @@ -1208,6 +1218,9 @@ async def handle_foobar(req, resp, ex, param): # type: ignore
async with conductor as c:
if place == 'ws_after_accept':
async with c.simulate_ws() as ws:
# Make sure the responder has a chance to reach the raise point
for _ in range(3):
await asyncio.sleep(0)
assert ws.closed
assert ws.close_code == exp_code
else:
Expand Down
14 changes: 9 additions & 5 deletions tests/test_cookies.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, tzinfo
from datetime import datetime, timedelta, timezone, tzinfo
from http import cookies as http_cookies
import re

Expand Down Expand Up @@ -28,6 +28,10 @@ def dst(self, dt):
GMT_PLUS_ONE = TimezoneGMTPlus1()


def utcnow_naive():
return datetime.now(timezone.utc).replace(tzinfo=None)


class CookieResource:
def on_get(self, req, resp):
resp.set_cookie('foo', 'bar', domain='example.com', path='/')
Expand Down Expand Up @@ -171,7 +175,7 @@ def test_response_complex_case(client):
assert cookie.domain is None
assert cookie.same_site == 'Lax'

assert cookie.expires < datetime.utcnow()
assert cookie.expires < utcnow_naive()

# NOTE(kgriffs): I know accessing a private attr like this is
# naughty of me, but we just need to sanity-check that the
Expand All @@ -193,7 +197,7 @@ def test(cookie, path, domain, samesite='Lax'):
assert cookie.domain == domain
assert cookie.path == path
assert cookie.same_site == samesite
assert cookie.expires < datetime.utcnow()
assert cookie.expires < utcnow_naive()

test(result.cookies['foo'], path=None, domain=None)
test(result.cookies['bar'], path='/bar', domain=None)
Expand Down Expand Up @@ -231,7 +235,7 @@ def test_set(cookie, value, samesite=None):
def test_unset(cookie, samesite='Lax'):
assert cookie.value == '' # An unset cookie has an empty value
assert cookie.same_site == samesite
assert cookie.expires < datetime.utcnow()
assert cookie.expires < utcnow_naive()

test_unset(result_unset.cookies['foo'], samesite='Strict')
# default: bar is unset with no samesite param, so should go to Lax
Expand Down Expand Up @@ -325,7 +329,7 @@ def test_response_unset_cookie(client):
assert match

expiration = http_date_to_dt(match.group(1), obs_date=True)
assert expiration < datetime.utcnow()
assert expiration < utcnow_naive()


def test_cookie_timezone(client):
Expand Down
4 changes: 3 additions & 1 deletion tests/test_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import falcon
from falcon import testing
from falcon.util.deprecation import DeprecatedWarning
from falcon.util.misc import _utcnow

from _util import create_app # NOQA


Expand All @@ -31,7 +33,7 @@ def __init__(self, last_modified=None):
if last_modified is not None:
self.last_modified = last_modified
else:
self.last_modified = datetime.utcnow()
self.last_modified = _utcnow()

def _overwrite_headers(self, req, resp):
resp.content_type = 'x-falcon/peregrine'
Expand Down
8 changes: 4 additions & 4 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from datetime import datetime
import json

try:
Expand All @@ -10,6 +9,7 @@
import falcon
import falcon.errors
import falcon.testing as testing
from falcon.util.misc import _utcnow

from _util import create_app # NOQA

Expand All @@ -36,15 +36,15 @@ def process_request(self, req, resp):
class RequestTimeMiddleware:
def process_request(self, req, resp):
global context
context['start_time'] = datetime.utcnow()
context['start_time'] = _utcnow()

def process_resource(self, req, resp, resource, params):
global context
context['mid_time'] = datetime.utcnow()
context['mid_time'] = _utcnow()

def process_response(self, req, resp, resource, req_succeeded):
global context
context['end_time'] = datetime.utcnow()
context['end_time'] = _utcnow()
context['req_succeeded'] = req_succeeded

async def process_request_async(self, req, resp):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_request_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def test_subdomain(self, asgi):
# NOTE(kgriffs): Behavior for IP addresses is undefined,
# so just make sure it doesn't blow up.
req = create_req(asgi, host='127.0.0.1', path='/hello', headers=self.headers)
assert type(req.subdomain) == str
assert type(req.subdomain) is str

# NOTE(kgriffs): Test fallback to SERVER_NAME by using
# HTTP 1.0, which will cause .create_environ to not set
Expand Down
4 changes: 2 additions & 2 deletions tests/test_request_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def test_invalid_json(asgi):
try:
json.loads(expected_body)
except Exception as e:
assert type(client.resource.captured_error.value.__cause__) == type(e)
assert type(client.resource.captured_error.value.__cause__) is type(e)
assert str(client.resource.captured_error.value.__cause__) == str(e)


Expand All @@ -210,7 +210,7 @@ def test_invalid_msgpack(asgi):
try:
msgpack.unpackb(expected_body.encode('utf-8'))
except Exception as e:
assert type(client.resource.captured_error.value.__cause__) == type(e)
assert type(client.resource.captured_error.value.__cause__) is type(e)
assert str(client.resource.captured_error.value.__cause__) == str(e)


Expand Down
9 changes: 4 additions & 5 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-

from datetime import datetime
from datetime import datetime, timezone
import functools
import http
import itertools
Expand Down Expand Up @@ -109,13 +109,12 @@ def old_thing():
assert msg in str(warn.message)

def test_http_now(self):
expected = datetime.utcnow()
expected = datetime.now(timezone.utc)
actual = falcon.http_date_to_dt(falcon.http_now())

delta = actual - expected
delta_sec = abs(delta.days * 86400 + delta.seconds)
delta = actual.replace(tzinfo=timezone.utc) - expected

assert delta_sec <= 1
assert delta.total_seconds() <= 1

def test_dt_to_http(self):
assert (
Expand Down
2 changes: 1 addition & 1 deletion tests/test_validators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import typing
import typing # NOQA: F401

try:
import jsonschema as _jsonschema # NOQA
Expand Down
18 changes: 14 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ commands = python "{toxinidir}/tools/clean.py" "{toxinidir}/falcon"
[with-cython]
deps = -r{toxinidir}/requirements/tests
Cython
# NOTE(vytas): By using --no-build-isolation, we need to manage build
vytas7 marked this conversation as resolved.
Show resolved Hide resolved
# deps ourselves, and on CPython 3.12, it seems even setuptools
# (our PEP 517 backend of choice) is not guaranteed to be there.
setuptools
wheel
setenv =
PIP_CONFIG_FILE={toxinidir}/pip.conf
FALCON_DISABLE_CYTHON=
Expand Down Expand Up @@ -216,6 +221,13 @@ deps = {[with-cython]deps}
setenv = {[with-cython]setenv}
commands = {[with-cython]commands}

[testenv:py312_cython]
basepython = python3.12
install_command = {[with-cython]install_command}
deps = {[with-cython]deps}
setenv = {[with-cython]setenv}
commands = {[with-cython]commands}

# --------------------------------------------------------------------
# WSGI servers (Cythonized Falcon)
# --------------------------------------------------------------------
Expand Down Expand Up @@ -261,8 +273,7 @@ commands = {[smoke-test]commands}
# --------------------------------------------------------------------

[testenv:pep8]
# TODO(vytas): Unpin flake8 when the below plugins have caught up.
deps = flake8<6.0
deps = flake8
flake8-quotes
flake8-import-order
commands = flake8 []
Expand All @@ -286,8 +297,7 @@ commands = flake8 \
[]

[testenv:pep8-examples]
# TODO(vytas): Unpin flake8 when the below plugins have caught up.
deps = flake8<6.0
deps = flake8
flake8-quotes
flake8-import-order

Expand Down
Loading