diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3eb26124..58ffd8de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,15 +14,16 @@ jobs: fail-fast: false matrix: include: + - {name: '3.14', python: '3.14', tox: py314} + - {name: '3.13', python: '3.13', tox: py313} - {name: '3.12', python: '3.12', tox: py312} - {name: '3.11', python: '3.11', tox: py311} - {name: '3.10', python: '3.10', tox: py310} - {name: '3.9', python: '3.9', tox: py39} - - {name: '3.8', python: '3.8', tox: py38} - - {name: 'format', python: '3.12', tox: format} - - {name: 'mypy', python: '3.12', tox: mypy} - - {name: 'pep8', python: '3.12', tox: pep8} - - {name: 'package', python: '3.12', tox: package} + - {name: 'format', python: '3.13', tox: format} + - {name: 'mypy', python: '3.13', tox: mypy} + - {name: 'pep8', python: '3.13', tox: pep8} + - {name: 'package', python: '3.13', tox: package} steps: - uses: actions/checkout@v4 @@ -30,6 +31,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} + allow-prereleases: true - name: update pip run: | @@ -56,7 +58,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.13" - name: update pip run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5e011a7c..0f4486df 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/setup-python@v3 with: - python-version: 3.12 + python-version: 3.13 - run: | pip install poetry diff --git a/pyproject.toml b/pyproject.toml index 7a6b6a84..ab0d1035 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,11 +11,12 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -26,7 +27,7 @@ repository = "https://github.com/pgjones/hypercorn/" documentation = "https://hypercorn.readthedocs.io" [tool.poetry.dependencies] -python = ">=3.8" +python = ">=3.9" aioquic = { version = ">= 0.9.0, < 1.0", optional = true } exceptiongroup = { version = ">= 1.1.0", python = "<3.11" } h11 = "*" @@ -41,7 +42,7 @@ typing_extensions = { version = "*", python = "<3.11" } uvloop = { version = ">=0.18", markers = "platform_system != 'Windows'", optional = true } wsproto = ">=0.14.0" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] httpx = "*" hypothesis = "*" mock = "*" @@ -61,7 +62,7 @@ uvloop = ["uvloop"] [tool.black] line-length = 100 -target-version = ["py38"] +target-version = ["py39"] [tool.isort] combine_as_imports = true @@ -104,6 +105,7 @@ warn_return_any = true [tool.pytest.ini_options] addopts = "--no-cov-on-fail --showlocals --strict-markers" +asyncio_default_fixture_loop_scope = "function" # silence deprecationwarning asyncio_mode = "strict" testpaths = ["tests"] diff --git a/setup.cfg b/setup.cfg index 3423d8f8..bb54d51f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [flake8] ignore = E203, E252, FI58, W503, W504 max_line_length = 100 -min_version = 3.8 +min_version = 3.9 require_code = True diff --git a/src/hypercorn/app_wrappers.py b/src/hypercorn/app_wrappers.py index 56c1bfa7..fbd828ba 100644 --- a/src/hypercorn/app_wrappers.py +++ b/src/hypercorn/app_wrappers.py @@ -3,7 +3,7 @@ import sys from functools import partial from io import BytesIO -from typing import Callable, List, Optional, Tuple +from typing import Callable, List, Optional, Tuple, TYPE_CHECKING from .typing import ( ASGIFramework, @@ -14,6 +14,9 @@ WSGIFramework, ) +if TYPE_CHECKING: + from typing_extensions import Buffer # for py<3.12 + class InvalidPathError(Exception): pass @@ -117,7 +120,7 @@ def start_response( response_body.close() -def _build_environ(scope: HTTPScope, body: bytes) -> dict: +def _build_environ(scope: HTTPScope, body: Buffer) -> dict: server = scope.get("server") or ("localhost", 80) path = scope["path"] script_name = scope.get("root_path", "") diff --git a/src/hypercorn/asyncio/run.py b/src/hypercorn/asyncio/run.py index 93bd7fc5..fd718e8f 100644 --- a/src/hypercorn/asyncio/run.py +++ b/src/hypercorn/asyncio/run.py @@ -102,8 +102,6 @@ def _signal_handler(*_: Any) -> None: # noqa: N803 server_tasks: Set[asyncio.Task] = set() async def _server_callback(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: - nonlocal server_tasks - task = asyncio.current_task(loop) server_tasks.add(task) task.add_done_callback(server_tasks.discard) diff --git a/src/hypercorn/events.py b/src/hypercorn/events.py index e829616a..dfef3314 100644 --- a/src/hypercorn/events.py +++ b/src/hypercorn/events.py @@ -11,7 +11,7 @@ class Event(ABC): @dataclass(frozen=True) class RawData(Event): - data: bytes + data: bytes | bytearray | memoryview[int] # this can likely be collections.abc.Buffer address: Optional[Tuple[str, int]] = None diff --git a/src/hypercorn/logging.py b/src/hypercorn/logging.py index d9b8901a..8e02a150 100644 --- a/src/hypercorn/logging.py +++ b/src/hypercorn/logging.py @@ -34,7 +34,7 @@ def _create_logger( if target: logger = logging.getLogger(name) logger.handlers = [ - logging.StreamHandler(sys_default) if target == "-" else logging.FileHandler(target) # type: ignore # noqa: E501 + logging.StreamHandler(sys_default) if target == "-" else logging.FileHandler(target) ] logger.propagate = propagate formatter = logging.Formatter( diff --git a/src/hypercorn/protocol/__init__.py b/src/hypercorn/protocol/__init__.py index 4e8feae8..fa95969c 100644 --- a/src/hypercorn/protocol/__init__.py +++ b/src/hypercorn/protocol/__init__.py @@ -91,6 +91,10 @@ async def handle(self, event: Event) -> None: self.server, self.send, ) - await self.protocol.initiate(error.headers, error.settings) + # H2Connection only accepts bytes, not str, but it passes the value to + # base64.urlsafe_b64encode that also handles ASCII strings. + # But H2CProtocolRequiredError intentionally decodes bytes in __init__, + # which should maybe be remedied. + await self.protocol.initiate(error.headers, error.settings) # type: ignore[arg-type] if error.data != b"": return await self.protocol.handle(RawData(data=error.data)) diff --git a/src/hypercorn/protocol/h11.py b/src/hypercorn/protocol/h11.py index c3c6e0f3..311e8f98 100644 --- a/src/hypercorn/protocol/h11.py +++ b/src/hypercorn/protocol/h11.py @@ -1,7 +1,7 @@ from __future__ import annotations from itertools import chain -from typing import Awaitable, Callable, cast, Optional, Tuple, Type, Union +from typing import Awaitable, Callable, cast, Iterable, Optional, SupportsIndex, Tuple, Type, Union import h11 @@ -59,7 +59,7 @@ def __init__(self, h11_connection: h11.Connection) -> None: self.buffer = bytearray(h11_connection.trailing_data[0]) self.h11_connection = h11_connection - def receive_data(self, data: bytes) -> None: + def receive_data(self, data: Iterable[SupportsIndex]) -> None: self.buffer.extend(data) def next_event(self) -> Union[Data, Type[h11.NEED_DATA]]: @@ -111,7 +111,9 @@ async def initiate(self) -> None: async def handle(self, event: Event) -> None: if isinstance(event, RawData): - self.connection.receive_data(event.data) + # `h11.Connection.receive_data` should accept `Buffer`, but is overly narrow. + # See https://github.com/python-hyper/h11/issues/186 + self.connection.receive_data(event.data) # type: ignore[arg-type] await self._handle_events() elif isinstance(event, Closed): if self.stream is not None: diff --git a/src/hypercorn/protocol/h2.py b/src/hypercorn/protocol/h2.py index b19a2bcc..9c255fb2 100644 --- a/src/hypercorn/protocol/h2.py +++ b/src/hypercorn/protocol/h2.py @@ -1,6 +1,17 @@ from __future__ import annotations -from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union +from typing import ( + Awaitable, + Callable, + cast, + Dict, + List, + Optional, + Tuple, + Type, + TYPE_CHECKING, + Union, +) import h2 import h2.connection @@ -27,6 +38,10 @@ from ..typing import AppWrapper, ConnectionState, Event as IOEvent, TaskGroup, WorkerContext from ..utils import filter_pseudo_headers +if TYPE_CHECKING: + # fancy alias for tuple[bytes, bytes] + from hpack import HeaderTuple + BUFFER_HIGH_WATER = 2 * 2**14 # Twice the default max frame size (two frames worth) BUFFER_LOW_WATER = BUFFER_HIGH_WATER / 2 @@ -127,7 +142,7 @@ def idle(self) -> bool: return len(self.streams) == 0 or all(stream.idle for stream in self.streams.values()) async def initiate( - self, headers: Optional[List[Tuple[bytes, bytes]]] = None, settings: Optional[str] = None + self, headers: Optional[List[Tuple[bytes, bytes]]] = None, settings: Optional[bytes] = None ) -> None: if settings is not None: self.connection.initiate_upgrade_connection(settings) @@ -137,7 +152,7 @@ async def initiate( if headers is not None: event = h2.events.RequestReceived() event.stream_id = 1 - event.headers = headers + event.headers = cast("list[HeaderTuple]", headers) await self._create_stream(event) await self.streams[event.stream_id].handle(EndBody(stream_id=event.stream_id)) self.task_group.spawn(self.send_task) @@ -184,7 +199,9 @@ async def _send_data(self, stream_id: int) -> None: async def handle(self, event: Event) -> None: if isinstance(event, RawData): try: - events = self.connection.receive_data(event.data) + # H2 relies on legacy typing behavior of `bytes` accepting `bytearray` + # See https://github.com/python-hyper/h2/issues/1305 + events = self.connection.receive_data(event.data) # type: ignore[arg-type] except h2.exceptions.ProtocolError: await self._flush() await self.send(Closed()) @@ -389,7 +406,7 @@ async def _create_server_push( else: event = h2.events.RequestReceived() event.stream_id = push_stream_id - event.headers = request_headers + event.headers = cast("list[HeaderTuple]", request_headers) await self._create_stream(event) await self.streams[event.stream_id].handle(EndBody(stream_id=event.stream_id)) self.keep_alive_requests += 1 diff --git a/src/hypercorn/run.py b/src/hypercorn/run.py index cfe801aa..181b0487 100644 --- a/src/hypercorn/run.py +++ b/src/hypercorn/run.py @@ -56,7 +56,7 @@ def run(config: Config) -> int: shutdown_event = ctx.Event() def shutdown(*args: Any) -> None: - nonlocal active, shutdown_event + nonlocal active shutdown_event.set() active = False diff --git a/src/hypercorn/trio/__init__.py b/src/hypercorn/trio/__init__.py index 07957062..23d83db3 100644 --- a/src/hypercorn/trio/__init__.py +++ b/src/hypercorn/trio/__init__.py @@ -16,7 +16,7 @@ async def serve( config: Config, *, shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None, - task_status: trio.TaskStatus = trio.TASK_STATUS_IGNORED, + task_status: trio.TaskStatus[list[str]] = trio.TASK_STATUS_IGNORED, mode: Optional[Literal["asgi", "wsgi"]] = None, ) -> None: """Serve an ASGI framework app given the config. diff --git a/src/hypercorn/trio/run.py b/src/hypercorn/trio/run.py index 7c55df10..b974035a 100644 --- a/src/hypercorn/trio/run.py +++ b/src/hypercorn/trio/run.py @@ -33,7 +33,7 @@ async def worker_serve( *, sockets: Optional[Sockets] = None, shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None, - task_status: trio.TaskStatus = trio.TASK_STATUS_IGNORED, + task_status: trio.TaskStatus[list[str]] = trio.TASK_STATUS_IGNORED, ) -> None: config.set_statsd_logger_class(StatsdLogger) diff --git a/src/hypercorn/trio/worker_context.py b/src/hypercorn/trio/worker_context.py index 1cac17e3..9aa7b362 100644 --- a/src/hypercorn/trio/worker_context.py +++ b/src/hypercorn/trio/worker_context.py @@ -11,7 +11,7 @@ def _cancel_wrapper(func: Callable[[], Awaitable[None]]) -> Callable[[], Awaitable[None]]: @wraps(func) async def wrapper( - task_status: trio.TaskStatus = trio.TASK_STATUS_IGNORED, + task_status: trio.TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED, ) -> None: cancel_scope = trio.CancelScope() task_status.started(cancel_scope) diff --git a/src/hypercorn/utils.py b/src/hypercorn/utils.py index 39249c53..bb7c0e91 100644 --- a/src/hypercorn/utils.py +++ b/src/hypercorn/utils.py @@ -18,6 +18,7 @@ List, Literal, Optional, + Sequence, Tuple, TYPE_CHECKING, ) @@ -74,7 +75,7 @@ def build_and_validate_headers(headers: Iterable[Tuple[bytes, bytes]]) -> List[T return validated_headers -def filter_pseudo_headers(headers: List[Tuple[bytes, bytes]]) -> List[Tuple[bytes, bytes]]: +def filter_pseudo_headers(headers: Sequence[Tuple[bytes, bytes]]) -> List[Tuple[bytes, bytes]]: filtered_headers: List[Tuple[bytes, bytes]] = [(b"host", b"")] # Placeholder authority = None host = b"" diff --git a/tests/asyncio/test_sanity.py b/tests/asyncio/test_sanity.py index c4c87f9a..0d1d0f90 100644 --- a/tests/asyncio/test_sanity.py +++ b/tests/asyncio/test_sanity.py @@ -224,11 +224,13 @@ async def test_http2_websocket() -> None: h2_client.send_data(stream_id, client.send(wsproto.events.BytesMessage(data=SANITY_BODY))) await server.reader.send(h2_client.data_to_send()) # type: ignore events = h2_client.receive_data(await server.writer.receive()) # type: ignore + assert isinstance(events[0], h2.events.DataReceived) client.receive_data(events[0].data) assert list(client.events()) == [wsproto.events.TextMessage(data="Hello & Goodbye")] h2_client.send_data(stream_id, client.send(wsproto.events.CloseConnection(code=1000))) await server.reader.send(h2_client.data_to_send()) # type: ignore events = h2_client.receive_data(await server.writer.receive()) # type: ignore + assert isinstance(events[0], h2.events.DataReceived) client.receive_data(events[0].data) assert list(client.events()) == [wsproto.events.CloseConnection(code=1000, reason="")] h2_client.close_connection() diff --git a/tests/middleware/test_dispatcher.py b/tests/middleware/test_dispatcher.py index 2c6d8a1e..927d3212 100644 --- a/tests/middleware/test_dispatcher.py +++ b/tests/middleware/test_dispatcher.py @@ -33,7 +33,6 @@ async def __call__(self, scope: Scope, receive: Callable, send: Callable) -> Non sent_events = [] async def send(message: dict) -> None: - nonlocal sent_events sent_events.append(message) await app({**http_scope, **{"path": "/api/x/b"}}, None, send) # type: ignore @@ -66,7 +65,6 @@ async def test_asyncio_dispatcher_lifespan() -> None: sent_events = [] async def send(message: dict) -> None: - nonlocal sent_events sent_events.append(message) async def receive() -> dict: @@ -83,7 +81,6 @@ async def test_trio_dispatcher_lifespan() -> None: sent_events = [] async def send(message: dict) -> None: - nonlocal sent_events sent_events.append(message) async def receive() -> dict: diff --git a/tests/middleware/test_http_to_https.py b/tests/middleware/test_http_to_https.py index 01583e26..4d7518a0 100644 --- a/tests/middleware/test_http_to_https.py +++ b/tests/middleware/test_http_to_https.py @@ -14,7 +14,6 @@ async def test_http_to_https_redirect_middleware_http(raw_path: bytes) -> None: sent_events = [] async def send(message: dict) -> None: - nonlocal sent_events sent_events.append(message) scope: HTTPScope = { @@ -53,7 +52,6 @@ async def test_http_to_https_redirect_middleware_websocket(raw_path: bytes) -> N sent_events = [] async def send(message: dict) -> None: - nonlocal sent_events sent_events.append(message) scope: WebsocketScope = { @@ -90,7 +88,6 @@ async def test_http_to_https_redirect_middleware_websocket_http2() -> None: sent_events = [] async def send(message: dict) -> None: - nonlocal sent_events sent_events.append(message) scope: WebsocketScope = { @@ -127,7 +124,6 @@ async def test_http_to_https_redirect_middleware_websocket_no_rejection() -> Non sent_events = [] async def send(message: dict) -> None: - nonlocal sent_events sent_events.append(message) scope: WebsocketScope = { diff --git a/tests/protocol/test_h11.py b/tests/protocol/test_h11.py index aa3b0bd5..819a28dc 100755 --- a/tests/protocol/test_h11.py +++ b/tests/protocol/test_h11.py @@ -2,7 +2,7 @@ import asyncio from typing import Any -from unittest.mock import call, Mock +from unittest.mock import AsyncMock, call, Mock import h11 import pytest @@ -18,13 +18,6 @@ from hypercorn.protocol.http_stream import HTTPStream from hypercorn.typing import ConnectionState, Event as IOEvent -try: - from unittest.mock import AsyncMock -except ImportError: - # Python < 3.8 - from mock import AsyncMock # type: ignore - - BASIC_HEADERS = [("Host", "hypercorn"), ("Connection", "close")] diff --git a/tests/protocol/test_h2.py b/tests/protocol/test_h2.py index a13c494a..b549496d 100644 --- a/tests/protocol/test_h2.py +++ b/tests/protocol/test_h2.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -from unittest.mock import call, Mock +from unittest.mock import AsyncMock, call, Mock import pytest from h2.connection import H2Connection @@ -13,12 +13,6 @@ from hypercorn.protocol.h2 import BUFFER_HIGH_WATER, BufferCompleteError, H2Protocol, StreamBuffer from hypercorn.typing import ConnectionState -try: - from unittest.mock import AsyncMock -except ImportError: - # Python < 3.8 - from mock import AsyncMock # type: ignore - @pytest.mark.asyncio async def test_stream_buffer_push_and_pop() -> None: diff --git a/tests/protocol/test_http_stream.py b/tests/protocol/test_http_stream.py index b25cb2f3..3f82a02b 100644 --- a/tests/protocol/test_http_stream.py +++ b/tests/protocol/test_http_stream.py @@ -1,7 +1,7 @@ from __future__ import annotations from typing import Any, cast -from unittest.mock import call +from unittest.mock import AsyncMock, call import pytest import pytest_asyncio @@ -28,12 +28,6 @@ ) from hypercorn.utils import UnexpectedMessageError -try: - from unittest.mock import AsyncMock -except ImportError: - # Python < 3.8 - from mock import AsyncMock # type: ignore - @pytest_asyncio.fixture(name="stream") # type: ignore[misc] async def _stream() -> HTTPStream: diff --git a/tests/protocol/test_ws_stream.py b/tests/protocol/test_ws_stream.py index 7b5ee98b..a8956fb8 100644 --- a/tests/protocol/test_ws_stream.py +++ b/tests/protocol/test_ws_stream.py @@ -2,7 +2,7 @@ import asyncio from typing import Any, cast, List, Tuple -from unittest.mock import call, Mock +from unittest.mock import AsyncMock, call, Mock import pytest import pytest_asyncio @@ -30,12 +30,6 @@ ) from hypercorn.utils import UnexpectedMessageError -try: - from unittest.mock import AsyncMock -except ImportError: - # Python < 3.8 - from mock import AsyncMock # type: ignore - def test_buffer() -> None: buffer_ = WebsocketBuffer(10) diff --git a/tests/test_app_wrappers.py b/tests/test_app_wrappers.py index 0640350e..5112f124 100644 --- a/tests/test_app_wrappers.py +++ b/tests/test_app_wrappers.py @@ -47,7 +47,6 @@ async def test_wsgi_trio() -> None: messages = [] async def _send(message: ASGISendEvent) -> None: - nonlocal messages messages.append(message) await app(scope, receive_channel.receive, _send, trio.to_thread.run_sync, trio.from_thread.run) @@ -69,7 +68,6 @@ async def _run_app(app: WSGIWrapper, scope: HTTPScope, body: bytes = b"") -> Lis messages = [] async def _send(message: ASGISendEvent) -> None: - nonlocal messages messages.append(message) event_loop = asyncio.get_running_loop() diff --git a/tests/trio/test_keep_alive.py b/tests/trio/test_keep_alive.py index c570a2a0..43d5793e 100644 --- a/tests/trio/test_keep_alive.py +++ b/tests/trio/test_keep_alive.py @@ -81,13 +81,10 @@ async def test_http1_keep_alive_during( client_stream: ClientStream, ) -> None: client = h11.Connection(h11.CLIENT) - # client.send(h11.Request) and client.send(h11.EndOfMessage) only returns bytes. - # Fixed on master/ in the h11 repo, once released the ignore's can be removed. - # See https://github.com/python-hyper/h11/issues/175 - await client_stream.send_all(client.send(REQUEST)) # type: ignore[arg-type] + await client_stream.send_all(client.send(REQUEST)) await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) # Key is that this doesn't error - await client_stream.send_all(client.send(h11.EndOfMessage())) # type: ignore[arg-type] + await client_stream.send_all(client.send(h11.EndOfMessage())) @pytest.mark.trio @@ -95,9 +92,9 @@ async def test_http1_keep_alive( client_stream: ClientStream, ) -> None: client = h11.Connection(h11.CLIENT) - await client_stream.send_all(client.send(REQUEST)) # type: ignore[arg-type] + await client_stream.send_all(client.send(REQUEST)) await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) - await client_stream.send_all(client.send(h11.EndOfMessage())) # type: ignore[arg-type] + await client_stream.send_all(client.send(h11.EndOfMessage())) while True: event = client.next_event() if event == h11.NEED_DATA: @@ -106,10 +103,10 @@ async def test_http1_keep_alive( elif isinstance(event, h11.EndOfMessage): break client.start_next_cycle() - await client_stream.send_all(client.send(REQUEST)) # type: ignore[arg-type] + await client_stream.send_all(client.send(REQUEST)) await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) # Key is that this doesn't error - await client_stream.send_all(client.send(h11.EndOfMessage())) # type: ignore[arg-type] + await client_stream.send_all(client.send(h11.EndOfMessage())) @pytest.mark.trio diff --git a/tests/trio/test_sanity.py b/tests/trio/test_sanity.py index bea93f13..f055c8be 100644 --- a/tests/trio/test_sanity.py +++ b/tests/trio/test_sanity.py @@ -1,7 +1,7 @@ from __future__ import annotations from typing import cast -from unittest.mock import Mock, PropertyMock +from unittest.mock import AsyncMock, Mock, PropertyMock import h2 import h11 @@ -15,12 +15,6 @@ from hypercorn.trio.worker_context import WorkerContext from ..helpers import MockSocket, SANITY_BODY, sanity_framework -try: - from unittest.mock import AsyncMock -except ImportError: - # Python < 3.8 - from mock import AsyncMock # type: ignore - @pytest.mark.trio async def test_http1_request(nursery: trio._core._run.Nursery) -> None: @@ -33,7 +27,7 @@ async def test_http1_request(nursery: trio._core._run.Nursery) -> None: nursery.start_soon(server.run) client = h11.Connection(h11.CLIENT) await client_stream.send_all( - client.send( # type: ignore[arg-type] + client.send( h11.Request( method="POST", target="/", @@ -45,8 +39,8 @@ async def test_http1_request(nursery: trio._core._run.Nursery) -> None: ) ) ) - await client_stream.send_all(client.send(h11.Data(data=SANITY_BODY))) # type: ignore[arg-type] - await client_stream.send_all(client.send(h11.EndOfMessage())) # type: ignore[arg-type] + await client_stream.send_all(client.send(h11.Data(data=SANITY_BODY))) + await client_stream.send_all(client.send(h11.EndOfMessage())) events = [] while True: event = client.next_event() @@ -143,6 +137,8 @@ async def test_http2_request(nursery: trio._core._run.Nursery) -> None: h2_events = client.receive_data(data) for event in h2_events: if isinstance(event, h2.events.DataReceived): + assert event.flow_controlled_length is not None + assert event.stream_id is not None client.acknowledge_received_data(event.flow_controlled_length, event.stream_id) elif isinstance( event, @@ -193,6 +189,7 @@ async def test_http2_websocket(nursery: trio._core._run.Nursery) -> None: events = h2_client.receive_data(await client_stream.receive_some(1024)) if not isinstance(events[-1], h2.events.ResponseReceived): events = h2_client.receive_data(await client_stream.receive_some(1024)) + assert isinstance(events[-1], h2.events.ResponseReceived) assert events[-1].headers == [ (b":status", b"200"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), @@ -202,11 +199,14 @@ async def test_http2_websocket(nursery: trio._core._run.Nursery) -> None: h2_client.send_data(stream_id, client.send(wsproto.events.BytesMessage(data=SANITY_BODY))) await client_stream.send_all(h2_client.data_to_send()) events = h2_client.receive_data(await client_stream.receive_some(1024)) + assert isinstance(events[0], h2.events.DataReceived) client.receive_data(events[0].data) assert list(client.events()) == [wsproto.events.TextMessage(data="Hello & Goodbye")] + h2_client.send_data(stream_id, client.send(wsproto.events.CloseConnection(code=1000))) await client_stream.send_all(h2_client.data_to_send()) events = h2_client.receive_data(await client_stream.receive_some(1024)) + assert isinstance(events[0], h2.events.DataReceived) client.receive_data(events[0].data) assert list(client.events()) == [wsproto.events.CloseConnection(code=1000, reason="")] await client_stream.send_all(b"") diff --git a/tox.ini b/tox.ini index d931bfab..053e3092 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,10 @@ [tox] -envlist = docs,format,mypy,py38,py39,py310,py311,py312,package,pep8 +envlist = docs,format,mypy,py39,py310,py311,py312,py313,py314,package,pep8 minversion = 3.3 isolated_build = true [testenv] deps = - py37: mock httpx hypothesis pytest @@ -16,7 +15,7 @@ deps = commands = pytest --cov=hypercorn {posargs} [testenv:docs] -basepython = python3.12 +basepython = python3.13 deps = pydata-sphinx-theme sphinx @@ -27,7 +26,7 @@ commands = sphinx-build -W --keep-going -b html -d {envtmpdir}/doctrees docs/ docs/_build/html/ [testenv:format] -basepython = python3.12 +basepython = python3.13 deps = black isort @@ -36,7 +35,7 @@ commands = isort --check --diff src/hypercorn tests [testenv:pep8] -basepython = python3.12 +basepython = python3.13 deps = flake8 pep8-naming @@ -45,7 +44,7 @@ deps = commands = flake8 src/hypercorn/ tests/ [testenv:mypy] -basepython = python3.12 +basepython = python3.13 deps = mypy pytest @@ -54,7 +53,7 @@ commands = mypy src/hypercorn/ tests/ [testenv:package] -basepython = python3.12 +basepython = python3.13 deps = poetry twine