Skip to content

Commit

Permalink
Replace asyncio and trio implementations with anyio
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Apr 3, 2024
1 parent 3fbd5f2 commit 46f345c
Show file tree
Hide file tree
Showing 71 changed files with 1,014 additions and 2,426 deletions.
15 changes: 2 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ on:

jobs:
tox:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -42,14 +41,9 @@ jobs:


h2spec:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- {name: 'asyncio', worker: 'asyncio'}
- {name: 'trio', worker: 'trio'}

steps:
- uses: actions/checkout@v3
Expand All @@ -67,7 +61,7 @@ jobs:

- name: Run server
working-directory: compliance/h2spec
run: nohup hypercorn --keyfile key.pem --certfile cert.pem -k ${{ matrix.worker }} server:app &
run: nohup anycorn --keyfile key.pem --certfile cert.pem server:app &

- name: Download h2spec
run: |
Expand All @@ -78,14 +72,9 @@ jobs:
run: ./h2spec -tk -h 127.0.0.1 -p 8000 -o 10

autobahn:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- {name: 'asyncio', worker: 'asyncio'}
- {name: 'trio', worker: 'trio'}

steps:
- uses: actions/checkout@v3
Expand All @@ -102,7 +91,7 @@ jobs:
- run: python3 -m pip install trio .
- name: Run server
working-directory: compliance/autobahn
run: nohup hypercorn -k ${{ matrix.worker }} server:app &
run: nohup anycorn server:app &

- name: Run Unit Tests
working-directory: compliance/autobahn
Expand Down
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.poetry]
name = "Hypercorn"
name = "anycorn"
version = "0.16.0"
description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn"
authors = ["pgjones <philip.graham.jones@googlemail.com>"]
Expand All @@ -19,10 +19,10 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules",
]
include = ["src/hypercorn/py.typed"]
include = ["src/anycorn/py.typed"]
license = "MIT"
readme = "README.rst"
repository = "https://github.com/pgjones/hypercorn/"
repository = "https://github.com/davidbrochart/anycorn/"
documentation = "https://hypercorn.readthedocs.io"

[tool.poetry.dependencies]
Expand All @@ -31,12 +31,12 @@ aioquic = { version = ">= 0.9.0, < 1.0", optional = true }
exceptiongroup = ">= 1.1.0"
h11 = "*"
h2 = ">=3.1.0"
anyio = ">=4.0, <5.0"
priority = "*"
pydata_sphinx_theme = { version = "*", optional = true }
sphinxcontrib_mermaid = { version = "*", optional = true }
taskgroup = { version = "*", python = "<3.11", allow-prereleases = true }
tomli = { version = "*", python = "<3.11" }
trio = { version = ">=0.22.0", optional = true }
uvloop = { version = "*", markers = "platform_system != 'Windows'", optional = true }
wsproto = ">=0.14.0"

Expand All @@ -49,7 +49,7 @@ pytest-trio = "*"
trio = "*"

[tool.poetry.scripts]
hypercorn = "hypercorn.__main__:main"
anycorn = "anycorn.__main__:main"

[tool.poetry.extras]
docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"]
Expand All @@ -65,7 +65,7 @@ target-version = ["py38"]
combine_as_imports = true
force_grid_wrap = 0
include_trailing_comma = true
known_first_party = "hypercorn, tests"
known_first_party = "anycorn, tests"
line_length = 100
multi_line_output = 3
no_lines_before = "LOCALFOLDER"
Expand Down
14 changes: 8 additions & 6 deletions src/hypercorn/trio/__init__.py → src/anycorn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
import warnings
from typing import Awaitable, Callable, Literal, Optional

import trio
import anyio

from .config import Config
from .run import worker_serve
from ..config import Config
from ..typing import Framework
from ..utils import wrap_app
from .typing import Framework
from .utils import wrap_app

__all__ = ("Config", "serve")


async def serve(
app: Framework,
config: Config,
*,
shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None,
task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED,
task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED,
mode: Optional[Literal["asgi", "wsgi"]] = None,
) -> None:
"""Serve an ASGI framework app given the config.
Expand All @@ -26,7 +28,7 @@ async def serve(
.. code-block:: python
trio.run(serve, app, config)
anyio.run(serve, app, config)
It is assumed that the event-loop is configured before calling
this function, therefore configuration values that relate to loop
Expand Down
11 changes: 0 additions & 11 deletions src/hypercorn/__main__.py → src/anycorn/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,6 @@ def main(sys_args: Optional[List[str]] = None) -> int:
parser.add_argument(
"-g", "--group", help="Group to own any unix sockets.", default=sentinel, type=int
)
parser.add_argument(
"-k",
"--worker-class",
dest="worker_class",
help="The type of worker to use. "
"Options include asyncio, uvloop (pip install hypercorn[uvloop]), "
"and trio (pip install hypercorn[trio]).",
default=sentinel,
)
parser.add_argument(
"--keep-alive",
help="Seconds to keep inactive connections alive for",
Expand Down Expand Up @@ -283,8 +274,6 @@ def _convert_verify_mode(value: str) -> ssl.VerifyMode:
config.umask = args.umask
if args.user is not sentinel:
config.user = args.user
if args.worker_class is not sentinel:
config.worker_class = args.worker_class
if args.verify_mode is not sentinel:
config.verify_mode = args.verify_mode
if args.websocket_ping_interval is not sentinel:
Expand Down
File renamed without changes.
5 changes: 2 additions & 3 deletions src/hypercorn/config.py → src/anycorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ class Config:
verify_mode: Optional[VerifyMode] = None
websocket_max_message_size = 16 * 1024 * 1024 * BYTES
websocket_ping_interval: Optional[float] = None
worker_class = "asyncio"
workers = 1
wsgi_max_body_size = 16 * 1024 * 1024 * BYTES

Expand Down Expand Up @@ -281,7 +280,7 @@ def response_headers(self, protocol: str) -> List[Tuple[bytes, bytes]]:
if self.include_date_header:
headers.append((b"date", format_date_time(time()).encode("ascii")))
if self.include_server_header:
headers.append((b"server", f"hypercorn-{protocol}".encode("ascii")))
headers.append((b"server", f"anycorn-{protocol}".encode("ascii")))

for alt_svc_header in self.alt_svc_headers:
headers.append((b"alt-svc", alt_svc_header.encode()))
Expand Down Expand Up @@ -338,7 +337,7 @@ def from_pyfile(cls: Type["Config"], filename: FilePath) -> "Config":
.. code-block:: python
Config.from_pyfile('hypercorn_config.py')
Config.from_pyfile('anycorn_config.py')
Arguments:
filename: The filename which gives the path to the file.
Expand Down
File renamed without changes.
28 changes: 14 additions & 14 deletions src/hypercorn/trio/lifespan.py → src/anycorn/lifespan.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from __future__ import annotations

import trio
import anyio

from ..config import Config
from ..typing import AppWrapper, ASGIReceiveEvent, ASGISendEvent, LifespanScope
from ..utils import LifespanFailureError, LifespanTimeoutError
from .config import Config
from .typing import AppWrapper, ASGIReceiveEvent, ASGISendEvent, LifespanScope
from .utils import LifespanFailureError, LifespanTimeoutError


class UnexpectedMessageError(Exception):
Expand All @@ -15,15 +15,15 @@ class Lifespan:
def __init__(self, app: AppWrapper, config: Config) -> None:
self.app = app
self.config = config
self.startup = trio.Event()
self.shutdown = trio.Event()
self.app_send_channel, self.app_receive_channel = trio.open_memory_channel(
self.startup = anyio.Event()
self.shutdown = anyio.Event()
self.app_send_channel, self.app_receive_channel = anyio.create_memory_object_stream[dict[str, str]](
config.max_app_queue_size
)
self.supported = True

async def handle_lifespan(
self, *, task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED
self, *, task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED
) -> None:
task_status.started()
scope: LifespanScope = {
Expand All @@ -35,8 +35,8 @@ async def handle_lifespan(
scope,
self.asgi_receive,
self.asgi_send,
trio.to_thread.run_sync,
trio.from_thread.run,
anyio.to_thread.run_sync,
anyio.from_thread.run,
)
except LifespanFailureError:
# Lifespan failures should crash the server
Expand Down Expand Up @@ -65,9 +65,9 @@ async def wait_for_startup(self) -> None:

await self.app_send_channel.send({"type": "lifespan.startup"})
try:
with trio.fail_after(self.config.startup_timeout):
with anyio.fail_after(self.config.startup_timeout):
await self.startup.wait()
except trio.TooSlowError as error:
except TimeoutError as error:
raise LifespanTimeoutError("startup") from error

async def wait_for_shutdown(self) -> None:
Expand All @@ -76,9 +76,9 @@ async def wait_for_shutdown(self) -> None:

await self.app_send_channel.send({"type": "lifespan.shutdown"})
try:
with trio.fail_after(self.config.shutdown_timeout):
with anyio.fail_after(self.config.shutdown_timeout):
await self.shutdown.wait()
except trio.TooSlowError as error:
except TimeoutError as error:
raise LifespanTimeoutError("startup") from error

async def asgi_receive(self) -> ASGIReceiveEvent:
Expand Down
4 changes: 2 additions & 2 deletions src/hypercorn/logging.py → src/anycorn/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ def __init__(self, config: "Config") -> None:
self.access_log_format = config.access_log_format

self.access_logger = _create_logger(
"hypercorn.access",
"anycorn.access",
config.accesslog,
config.loglevel,
sys.stdout,
propagate=False,
)
self.error_logger = _create_logger(
"hypercorn.error", config.errorlog, config.loglevel, sys.stderr
"anycorn.error", config.errorlog, config.loglevel, sys.stderr
)

if config.logconfig is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
from .dispatcher import DispatcherMiddleware
from .http_to_https import HTTPToHTTPSRedirectMiddleware
from .proxy_fix import ProxyFixMiddleware
from .wsgi import AsyncioWSGIMiddleware, TrioWSGIMiddleware
from .wsgi import WSGIMiddleware

__all__ = (
"AsyncioWSGIMiddleware",
"DispatcherMiddleware",
"HTTPToHTTPSRedirectMiddleware",
"ProxyFixMiddleware",
"TrioWSGIMiddleware",
"WSGIMiddleware",
)
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from __future__ import annotations

import asyncio
from functools import partial
from typing import Callable, Dict

from ..asyncio.task_group import TaskGroup
from ..typing import ASGIFramework, Scope

MAX_QUEUE_SIZE = 10
Expand Down Expand Up @@ -35,52 +33,17 @@ async def _handle_lifespan(self, scope: Scope, receive: Callable, send: Callable
pass


class AsyncioDispatcherMiddleware(_DispatcherMiddleware):
class DispatcherMiddleware(_DispatcherMiddleware):
async def _handle_lifespan(self, scope: Scope, receive: Callable, send: Callable) -> None:
self.app_queues: Dict[str, asyncio.Queue] = {
path: asyncio.Queue(MAX_QUEUE_SIZE) for path in self.mounts
}
self.startup_complete = {path: False for path in self.mounts}
self.shutdown_complete = {path: False for path in self.mounts}

async with TaskGroup(asyncio.get_event_loop()) as task_group:
for path, app in self.mounts.items():
task_group.spawn(
app,
scope,
self.app_queues[path].get,
partial(self.send, path, send),
)

while True:
message = await receive()
for queue in self.app_queues.values():
await queue.put(message)
if message["type"] == "lifespan.shutdown":
break

async def send(self, path: str, send: Callable, message: dict) -> None:
if message["type"] == "lifespan.startup.complete":
self.startup_complete[path] = True
if all(self.startup_complete.values()):
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown.complete":
self.shutdown_complete[path] = True
if all(self.shutdown_complete.values()):
await send({"type": "lifespan.shutdown.complete"})
import anyio


class TrioDispatcherMiddleware(_DispatcherMiddleware):
async def _handle_lifespan(self, scope: Scope, receive: Callable, send: Callable) -> None:
import trio

self.app_queues = {path: trio.open_memory_channel(MAX_QUEUE_SIZE) for path in self.mounts}
self.app_queues = {path: anyio.create_memory_object_stream(MAX_QUEUE_SIZE) for path in self.mounts}
self.startup_complete = {path: False for path in self.mounts}
self.shutdown_complete = {path: False for path in self.mounts}

async with trio.open_nursery() as nursery:
async with anyio.create_task_group() as tg:
for path, app in self.mounts.items():
nursery.start_soon(
tg.start_soon(
app,
scope,
self.app_queues[path][1].receive,
Expand All @@ -103,6 +66,3 @@ async def send(self, path: str, send: Callable, message: dict) -> None:
self.shutdown_complete[path] = True
if all(self.shutdown_complete.values()):
await send({"type": "lifespan.shutdown.complete"})


DispatcherMiddleware = AsyncioDispatcherMiddleware # Remove with version 0.11
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 46f345c

Please sign in to comment.