Skip to content

Commit 5c9549a

Browse files
Use anyio (#107)
* feat: use anyio (implements the comments from #100) Co-authored-by: David Brochart <david.brochart@gmail.com> * docs: mention trio support and anyio use * chore: bump minor version, prepare for new release --------- Co-authored-by: David Brochart <david.brochart@gmail.com>
1 parent 01d572f commit 5c9549a

14 files changed

Lines changed: 39 additions & 31 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Unleash your creativity with the full power and Python, without the hassle of le
2929
- Automatic and customizable **property-name conversion** from snake case to kebab case.
3030
- **Compatible** with any other templating library through wrappers.
3131
- **Fully-typed**.
32+
- **Trio**-compatible, through `AnyIO`.
3233

3334
## Testimonials
3435

@@ -358,7 +359,7 @@ The primary aim of `htmy` is to be a `Jinja` alternative that is similarly power
358359

359360
The library aims to minimze its dependencies. Currently the following dependencies are required:
360361

361-
- `anyio`: for async file operations and networking.
362+
- `anyio`: for everything async.
362363
- `markdown`: for markdown parsing.
363364

364365
## Development

docs/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Unleash your creativity with the full power and Python, without the hassle of le
2929
- Automatic and customizable **property-name conversion** from snake case to kebab case.
3030
- **Compatible** with any other templating library through wrappers.
3131
- **Fully-typed**.
32+
- **Trio**-compatible, through `AnyIO`.
3233

3334
## Testimonials
3435

@@ -354,7 +355,7 @@ The primary aim of `htmy` is to be a `Jinja` alternative that is similarly power
354355

355356
The library aims to minimze its dependencies. Currently the following dependencies are required:
356357

357-
- `anyio`: for async file operations and networking.
358+
- `anyio`: for everything async.
358359
- `markdown`: for markdown parsing.
359360

360361
## Development

htmy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.10.3"
1+
__version__ = "0.11.0"
22

33
from .core import ContextAware as ContextAware
44
from .core import Formatter as Formatter

htmy/error_boundary.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ def _fallback_component(self, error: Exception) -> ComponentType:
6363
Raises:
6464
Exception: The received error if it's not accepted.
6565
"""
66-
if not (self._errors is None or any(e in self._errors for e in type(error).mro())):
66+
errors = error.exceptions if isinstance(error, ExceptionGroup) else (error,)
67+
if not (self._errors is None or any(e in self._errors for err in errors for e in type(err).mro())):
6768
raise error
6869

6970
return as_component_type(self._fallback)

htmy/renderer/default.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from __future__ import annotations
22

3-
from asyncio import gather as asyncio_gather
43
from collections import ChainMap, deque
54
from inspect import isawaitable, iscoroutinefunction
65
from typing import TYPE_CHECKING, TypeAlias
76

7+
from anyio import create_task_group
8+
89
from htmy.core import xml_format_string
910
from htmy.typing import Context
1011
from htmy.utils import is_component_sequence
@@ -197,7 +198,9 @@ async def run(self) -> str:
197198
if async_todos:
198199
current_async_todos = async_todos
199200
self._async_todos = async_todos = deque()
200-
await asyncio_gather(*(process_async_node(n, ctx) for n, ctx in current_async_todos))
201+
async with create_task_group() as tg:
202+
for n, ctx in current_async_todos:
203+
tg.start_soon(process_async_node, n, ctx)
201204

202205
return "".join(node.component for node in self._root.iter_nodes() if node.component is not None) # type: ignore[misc]
203206

@@ -217,8 +220,10 @@ async def _render_component(
217220
if len(component) == 0:
218221
return ""
219222

220-
renderers = (_ComponentRenderer(c, context, string_formatter=string_formatter) for c in component)
221-
return "".join(await asyncio_gather(*(r.run() for r in renderers)))
223+
result = ""
224+
for c in component:
225+
result += await _ComponentRenderer(c, context, string_formatter=string_formatter).run()
226+
return result
222227
elif component is None:
223228
return ""
224229
else:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Async, pure-Python server-side rendering engine."
55
authors = [{ name = "Peter Volf", email = "do.volfp@gmail.com" }]
66
license = "MIT"
77
readme = "README.md"
8-
requires-python = ">=3.10"
8+
requires-python = ">=3.11"
99
dependencies = [
1010
"anyio>=4.7.0,<5",
1111
"markdown>=3.8,<4",
@@ -22,11 +22,11 @@ dev = [
2222
"mypy>=1.20.1,<2",
2323
"poethepoet>=0.44.0",
2424
"pytest>=9.0.3",
25-
"pytest-asyncio>=1.3.0",
2625
"pytest-random-order>=1.2.0",
2726
"ruff>=0.15.11,<0.16",
2827
"types-markdown>=3.10.2.20260408,<4",
2928
"typing-extensions>=4.12.2,<5",
29+
"trio>=0.33.0",
3030
"types-lxml>=2026.2.16",
3131
"zensical>=0.0.33",
3232
]

tests/renderer/test_default_renderer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from htmy.renderer.typing import RendererType
77

88

9-
@pytest.mark.asyncio
9+
@pytest.mark.anyio
1010
async def test_async_children_of_async_node(
1111
baseline_renderer: RendererType,
1212
default_renderer: RendererType,

tests/renderer/test_renderer_comparison.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ async def htmy(self, context: Context) -> Component:
102102
# -- Tests
103103

104104

105-
@pytest.mark.asyncio
105+
@pytest.mark.anyio
106106
@pytest.mark.parametrize(
107107
("component",),
108108
(

tests/renderer/test_renderer_in_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from htmy.renderer.typing import RendererType
77

88

9-
@pytest.mark.asyncio
9+
@pytest.mark.anyio
1010
@pytest.mark.parametrize(
1111
"renderer",
1212
[

tests/test_function_component.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import asyncio
21
from dataclasses import dataclass
32
from datetime import date
43
from time import perf_counter
54

5+
import anyio
66
import pytest
77

88
from htmy import Component, ComponentType, Context, Renderer, component, html
@@ -25,7 +25,7 @@ def sync_function_component(msg: str, ctx: Context) -> ComponentType:
2525

2626
@component
2727
async def async_function_component(msg: str, ctx: Context) -> ComponentType:
28-
await asyncio.sleep(async_delay)
28+
await anyio.sleep(async_delay)
2929
dt: date = ctx["date"]
3030
return html.p(dt.isoformat(), ": ", msg)
3131

@@ -41,7 +41,7 @@ def sync_function_component_with_function_alias(msg: str, ctx: Context) -> Compo
4141

4242
@component.function
4343
async def async_function_component_with_function_alias(msg: str, ctx: Context) -> ComponentType:
44-
await asyncio.sleep(async_delay)
44+
await anyio.sleep(async_delay)
4545
dt: date = ctx["date"]
4646
return html.p(dt.isoformat(), ": ", msg)
4747

@@ -57,7 +57,7 @@ def sync_context_only_function_component(ctx: Context) -> ComponentType:
5757

5858
@component.context_only
5959
async def async_context_only_function_component(ctx: Context) -> ComponentType:
60-
await asyncio.sleep(async_delay)
60+
await anyio.sleep(async_delay)
6161
dt: date = ctx["date"]
6262
return html.p(dt.isoformat(), ": ", message)
6363

@@ -76,7 +76,7 @@ def sync_method_component(self, msg: str, ctx: Context) -> ComponentType:
7676

7777
@component.method
7878
async def async_method_component(self, msg: str, ctx: Context) -> ComponentType:
79-
await asyncio.sleep(async_delay)
79+
await anyio.sleep(async_delay)
8080
dt: date = ctx["date"]
8181
return html.p(dt.isoformat(), ": ", msg, " ", self.goodbye)
8282

@@ -87,7 +87,7 @@ def sync_context_only_method_component(self, ctx: Context) -> ComponentType:
8787

8888
@component.context_only_method
8989
async def async_context_only_method_component(self, ctx: Context) -> ComponentType:
90-
await asyncio.sleep(async_delay)
90+
await anyio.sleep(async_delay)
9191
dt: date = ctx["date"]
9292
return html.p(dt.isoformat(), ": ", self.goodbye)
9393

@@ -110,7 +110,7 @@ def baseline_renderer() -> BaselineRenderer:
110110
# -- Tests
111111

112112

113-
@pytest.mark.asyncio
113+
@pytest.mark.anyio
114114
@pytest.mark.parametrize(
115115
("comp", "expected", "min_duration"),
116116
(

0 commit comments

Comments
 (0)