Skip to content

Commit

Permalink
chore: make contributor's checklist pass on CPython 3.12 (#2199)
Browse files Browse the repository at this point in the history
* chore(py312): fix docs build errors/warnings on CPython 3.12

* test(ws): add missing coverage on py312

* chore(mintest): add py312 to CI

* chore: add missing setuptools dep

* chore: leave mintest.yaml only for master merge CI after demonstration
  • Loading branch information
vytas7 committed Dec 17, 2023
1 parent 2382d44 commit 0aac950
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 12 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/mintest.yaml
Expand Up @@ -18,6 +18,7 @@ jobs:
- "3.8"
- "3.10"
- "3.11"
- "3.12"

steps:
- name: Checkout repo
Expand All @@ -31,7 +32,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U tox
pip install -U setuptools tox wheel
python --version
pip --version
tox --version
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Expand Up @@ -88,7 +88,7 @@
# |version| and |release|, also used in various other places throughout the
# built documents.

cfg = configparser.SafeConfigParser()
cfg = configparser.ConfigParser()
cfg.read('../setup.cfg')
tag = cfg.get('egg_info', 'tag_build')

Expand Down
2 changes: 1 addition & 1 deletion docs/ext/rfc.py
Expand Up @@ -23,7 +23,7 @@
import re


RFC_PATTERN = re.compile('RFC (\d{4}), Section ([\d\.]+)')
RFC_PATTERN = re.compile(r'RFC (\d{4}), Section ([\d\.]+)')


def _render_section(section_number, rfc_number):
Expand Down
4 changes: 3 additions & 1 deletion falcon/asgi/ws.py
Expand Up @@ -530,12 +530,14 @@ class WebSocketOptions:

__slots__ = ['error_close_code', 'max_receive_queue', 'media_handlers']

def __init__(self):
def __init__(self) -> None:
try:
import msgpack
except ImportError:
msgpack = None

bin_handler: media.BinaryBaseHandlerWS

if msgpack:
bin_handler = media.MessagePackHandlerWS()
else:
Expand Down
3 changes: 2 additions & 1 deletion falcon/media/handlers.py
Expand Up @@ -6,6 +6,7 @@
from falcon.constants import MEDIA_MULTIPART
from falcon.constants import MEDIA_URLENCODED
from falcon.constants import PYPY
from falcon.media.base import BinaryBaseHandlerWS
from falcon.media.json import JSONHandler
from falcon.media.multipart import MultipartFormHandler
from falcon.media.multipart import MultipartParseOptions
Expand All @@ -15,7 +16,7 @@
from falcon.vendor import mimeparse


class MissingDependencyHandler:
class MissingDependencyHandler(BinaryBaseHandlerWS):
"""Placeholder handler that always raises an error.
This handler is used by the framework for media types that require an
Expand Down
17 changes: 10 additions & 7 deletions falcon/testing/helpers.py
Expand Up @@ -402,6 +402,8 @@ class ASGIWebSocketSimulator:
``None`` if the connection has not been accepted.
"""

_DEFAULT_WAIT_READY_TIMEOUT = 5

def __init__(self):
self.__msgpack = None

Expand Down Expand Up @@ -435,7 +437,7 @@ def subprotocol(self) -> str:
def headers(self) -> Iterable[Iterable[bytes]]:
return self._accepted_headers

async def wait_ready(self, timeout: Optional[int] = 5):
async def wait_ready(self, timeout: Optional[int] = None):
"""Wait until the connection has been accepted or denied.
This coroutine can be awaited in order to pause execution until the
Expand All @@ -447,16 +449,17 @@ async def wait_ready(self, timeout: Optional[int] = 5):
raising an error (default: ``5``).
"""

timeout = timeout or self._DEFAULT_WAIT_READY_TIMEOUT

try:
await asyncio.wait_for(self._event_handshake_complete.wait(), timeout)
except asyncio.TimeoutError:
msg = (
'Timed out after waiting {} seconds for '
'the WebSocket handshake to complete. Check the '
'on_websocket responder and '
'any middleware for any conditions that may be stalling the '
'request flow.'
).format(timeout)
f'Timed out after waiting {timeout} seconds for the WebSocket '
f'handshake to complete. Check the on_websocket responder and '
f'any middleware for any conditions that may be stalling the '
f'request flow.'
)
raise asyncio.TimeoutError(msg)

self._require_accepted()
Expand Down
29 changes: 29 additions & 0 deletions tests/asgi/test_ws.py
Expand Up @@ -10,6 +10,7 @@
from falcon import media, testing
from falcon.asgi import App
from falcon.asgi.ws import _WebSocketState as ServerWebSocketState
from falcon.asgi.ws import WebSocket
from falcon.asgi.ws import WebSocketOptions
from falcon.testing.helpers import _WebSocketState as ClientWebSocketState

Expand Down Expand Up @@ -1088,6 +1089,34 @@ class Resource:
event = await ws._emit()


@pytest.mark.asyncio
async def test_ws_responder_never_ready(conductor, monkeypatch):
async def noop_close(obj, code=None):
pass

class SleepyResource:
async def on_websocket(self, req, ws):
for i in range(10):
await asyncio.sleep(0.001)

conductor.app.add_route('/', SleepyResource())

# NOTE(vytas): It seems that it is hard to impossible to hit the second
# `await ready_waiter` of the _WSContextManager on CPython 3.12 due to
# different async code optimizations, so we mock away WebSocket.close.
monkeypatch.setattr(WebSocket, 'close', noop_close)

# NOTE(vytas): Shorten the timeout so that we do not wait for 5 seconds.
monkeypatch.setattr(
testing.ASGIWebSocketSimulator, '_DEFAULT_WAIT_READY_TIMEOUT', 0.5
)

async with conductor as c:
with pytest.raises(asyncio.TimeoutError):
async with c.simulate_ws():
pass


@pytest.mark.skipif(msgpack, reason='test requires msgpack lib to be missing')
def test_msgpack_missing():

Expand Down

0 comments on commit 0aac950

Please sign in to comment.