htmy 0.3.6__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of htmy might be problematic. Click here for more details.
- {htmy-0.3.6 → htmy-0.4.0}/PKG-INFO +8 -8
- {htmy-0.3.6 → htmy-0.4.0}/README.md +7 -7
- {htmy-0.3.6 → htmy-0.4.0}/htmy/__init__.py +4 -1
- {htmy-0.3.6 → htmy-0.4.0}/htmy/core.py +3 -3
- {htmy-0.3.6 → htmy-0.4.0}/htmy/etree.py +7 -7
- {htmy-0.3.6 → htmy-0.4.0}/htmy/md/core.py +3 -3
- htmy-0.4.0/htmy/renderer/__init__.py +8 -0
- htmy-0.3.6/htmy/renderer.py → htmy-0.4.0/htmy/renderer/baseline.py +13 -4
- htmy-0.4.0/htmy/renderer/default.py +282 -0
- {htmy-0.3.6 → htmy-0.4.0}/htmy/typing.py +2 -2
- {htmy-0.3.6 → htmy-0.4.0}/pyproject.toml +4 -4
- {htmy-0.3.6 → htmy-0.4.0}/LICENSE +0 -0
- {htmy-0.3.6 → htmy-0.4.0}/htmy/html.py +0 -0
- {htmy-0.3.6 → htmy-0.4.0}/htmy/i18n.py +0 -0
- {htmy-0.3.6 → htmy-0.4.0}/htmy/io.py +0 -0
- {htmy-0.3.6 → htmy-0.4.0}/htmy/md/__init__.py +0 -0
- {htmy-0.3.6 → htmy-0.4.0}/htmy/md/typing.py +0 -0
- {htmy-0.3.6 → htmy-0.4.0}/htmy/py.typed +0 -0
- {htmy-0.3.6 → htmy-0.4.0}/htmy/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: htmy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Async, pure-Python rendering engine.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Peter Volf
|
|
@@ -170,16 +170,16 @@ user_table = html.table(
|
|
|
170
170
|
|
|
171
171
|
### Rendering
|
|
172
172
|
|
|
173
|
-
`htmy.
|
|
173
|
+
`htmy.Renderer` is the built-in, default renderer of the library.
|
|
174
174
|
|
|
175
|
-
If you're using the library in an async web framework like [FastAPI](https://fastapi.tiangolo.com/), then you're already in an async environment, so you can render components as simply as this: `await
|
|
175
|
+
If you're using the library in an async web framework like [FastAPI](https://fastapi.tiangolo.com/), then you're already in an async environment, so you can render components as simply as this: `await Renderer().render(my_root_component)`.
|
|
176
176
|
|
|
177
177
|
If you're trying to run the renderer in a sync environment, like a local script or CLI, then you first need to wrap the renderer in an async task and execute that task with `asyncio.run()`:
|
|
178
178
|
|
|
179
179
|
```python
|
|
180
180
|
import asyncio
|
|
181
181
|
|
|
182
|
-
from htmy import
|
|
182
|
+
from htmy import Renderer, html
|
|
183
183
|
|
|
184
184
|
async def render_page() -> None:
|
|
185
185
|
page = (
|
|
@@ -192,7 +192,7 @@ async def render_page() -> None:
|
|
|
192
192
|
)
|
|
193
193
|
)
|
|
194
194
|
|
|
195
|
-
result = await
|
|
195
|
+
result = await Renderer().render(page)
|
|
196
196
|
print(result)
|
|
197
197
|
|
|
198
198
|
|
|
@@ -213,7 +213,7 @@ Here's an example context provider and consumer implementation:
|
|
|
213
213
|
```python
|
|
214
214
|
import asyncio
|
|
215
215
|
|
|
216
|
-
from htmy import
|
|
216
|
+
from htmy import Component, ComponentType, Context, Renderer, component, html
|
|
217
217
|
|
|
218
218
|
class UserContext:
|
|
219
219
|
def __init__(self, *children: ComponentType, username: str, theme: str) -> None:
|
|
@@ -259,7 +259,7 @@ async def render_welcome_page() -> None:
|
|
|
259
259
|
theme="dark",
|
|
260
260
|
)
|
|
261
261
|
|
|
262
|
-
result = await
|
|
262
|
+
result = await Renderer().render(page)
|
|
263
263
|
print(result)
|
|
264
264
|
|
|
265
265
|
if __name__ == "__main__":
|
|
@@ -270,7 +270,7 @@ You can of course rely on the built-in context related utilities like the `Conte
|
|
|
270
270
|
|
|
271
271
|
### Formatter
|
|
272
272
|
|
|
273
|
-
As mentioned before, the built-in `Formatter` class is responsible for tag attribute name and value formatting. You can completely override or extend the built-in formatting behavior simply by extending this class or adding new rules to an instance of it, and then adding the custom instance to the context, either directly in `
|
|
273
|
+
As mentioned before, the built-in `Formatter` class is responsible for tag attribute name and value formatting. You can completely override or extend the built-in formatting behavior simply by extending this class or adding new rules to an instance of it, and then adding the custom instance to the context, either directly in `Renderer` or `Renderer.render()`, or in a context provider component.
|
|
274
274
|
|
|
275
275
|
These are default tag attribute formatting rules:
|
|
276
276
|
|
|
@@ -151,16 +151,16 @@ user_table = html.table(
|
|
|
151
151
|
|
|
152
152
|
### Rendering
|
|
153
153
|
|
|
154
|
-
`htmy.
|
|
154
|
+
`htmy.Renderer` is the built-in, default renderer of the library.
|
|
155
155
|
|
|
156
|
-
If you're using the library in an async web framework like [FastAPI](https://fastapi.tiangolo.com/), then you're already in an async environment, so you can render components as simply as this: `await
|
|
156
|
+
If you're using the library in an async web framework like [FastAPI](https://fastapi.tiangolo.com/), then you're already in an async environment, so you can render components as simply as this: `await Renderer().render(my_root_component)`.
|
|
157
157
|
|
|
158
158
|
If you're trying to run the renderer in a sync environment, like a local script or CLI, then you first need to wrap the renderer in an async task and execute that task with `asyncio.run()`:
|
|
159
159
|
|
|
160
160
|
```python
|
|
161
161
|
import asyncio
|
|
162
162
|
|
|
163
|
-
from htmy import
|
|
163
|
+
from htmy import Renderer, html
|
|
164
164
|
|
|
165
165
|
async def render_page() -> None:
|
|
166
166
|
page = (
|
|
@@ -173,7 +173,7 @@ async def render_page() -> None:
|
|
|
173
173
|
)
|
|
174
174
|
)
|
|
175
175
|
|
|
176
|
-
result = await
|
|
176
|
+
result = await Renderer().render(page)
|
|
177
177
|
print(result)
|
|
178
178
|
|
|
179
179
|
|
|
@@ -194,7 +194,7 @@ Here's an example context provider and consumer implementation:
|
|
|
194
194
|
```python
|
|
195
195
|
import asyncio
|
|
196
196
|
|
|
197
|
-
from htmy import
|
|
197
|
+
from htmy import Component, ComponentType, Context, Renderer, component, html
|
|
198
198
|
|
|
199
199
|
class UserContext:
|
|
200
200
|
def __init__(self, *children: ComponentType, username: str, theme: str) -> None:
|
|
@@ -240,7 +240,7 @@ async def render_welcome_page() -> None:
|
|
|
240
240
|
theme="dark",
|
|
241
241
|
)
|
|
242
242
|
|
|
243
|
-
result = await
|
|
243
|
+
result = await Renderer().render(page)
|
|
244
244
|
print(result)
|
|
245
245
|
|
|
246
246
|
if __name__ == "__main__":
|
|
@@ -251,7 +251,7 @@ You can of course rely on the built-in context related utilities like the `Conte
|
|
|
251
251
|
|
|
252
252
|
### Formatter
|
|
253
253
|
|
|
254
|
-
As mentioned before, the built-in `Formatter` class is responsible for tag attribute name and value formatting. You can completely override or extend the built-in formatting behavior simply by extending this class or adding new rules to an instance of it, and then adding the custom instance to the context, either directly in `
|
|
254
|
+
As mentioned before, the built-in `Formatter` class is responsible for tag attribute name and value formatting. You can completely override or extend the built-in formatting behavior simply by extending this class or adding new rules to an instance of it, and then adding the custom instance to the context, either directly in `Renderer` or `Renderer.render()`, or in a context provider component.
|
|
255
255
|
|
|
256
256
|
These are default tag attribute formatting rules:
|
|
257
257
|
|
|
@@ -15,7 +15,7 @@ from .core import WithContext as WithContext
|
|
|
15
15
|
from .core import XBool as XBool
|
|
16
16
|
from .core import component as component
|
|
17
17
|
from .core import xml_format_string as xml_format_string
|
|
18
|
-
from .renderer import
|
|
18
|
+
from .renderer import Renderer as Renderer
|
|
19
19
|
from .typing import AsyncComponent as AsyncComponent
|
|
20
20
|
from .typing import AsyncContextProvider as AsyncContextProvider
|
|
21
21
|
from .typing import AsyncFunctionComponent as AsyncFunctionComponent
|
|
@@ -36,3 +36,6 @@ from .typing import SyncContextProvider as SyncContextProvider
|
|
|
36
36
|
from .typing import SyncFunctionComponent as SyncFunctionComponent
|
|
37
37
|
from .typing import is_component_sequence as is_component_sequence
|
|
38
38
|
from .utils import join_components as join_components
|
|
39
|
+
|
|
40
|
+
HTMY = Renderer
|
|
41
|
+
"""Deprecated alias for `Renderer`."""
|
|
@@ -122,7 +122,7 @@ class WithContext(Fragment):
|
|
|
122
122
|
self._context = context
|
|
123
123
|
|
|
124
124
|
def htmy_context(self) -> Context:
|
|
125
|
-
"""Returns
|
|
125
|
+
"""Returns the context for child rendering."""
|
|
126
126
|
return self._context
|
|
127
127
|
|
|
128
128
|
|
|
@@ -174,7 +174,7 @@ class Snippet:
|
|
|
174
174
|
def _render_text(self, text: str, context: Context) -> Component:
|
|
175
175
|
"""
|
|
176
176
|
Render function that takes the text that must be rendered and the current rendering context,
|
|
177
|
-
and returns the corresponding
|
|
177
|
+
and returns the corresponding component.
|
|
178
178
|
"""
|
|
179
179
|
return SafeStr(text)
|
|
180
180
|
|
|
@@ -523,7 +523,7 @@ class BaseTag(abc.ABC):
|
|
|
523
523
|
|
|
524
524
|
@abc.abstractmethod
|
|
525
525
|
def htmy(self, context: Context) -> Component:
|
|
526
|
-
"""Abstract base
|
|
526
|
+
"""Abstract base component implementation."""
|
|
527
527
|
...
|
|
528
528
|
|
|
529
529
|
def _get_htmy_name(self) -> str:
|
|
@@ -17,7 +17,7 @@ from htmy.core import Fragment, SafeStr, WildcardTag
|
|
|
17
17
|
|
|
18
18
|
class ETreeConverter:
|
|
19
19
|
"""
|
|
20
|
-
Utility for converting XML strings to custom
|
|
20
|
+
Utility for converting XML strings to custom components.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
__slots__ = ("_rules",)
|
|
@@ -33,12 +33,12 @@ class ETreeConverter:
|
|
|
33
33
|
Initialization.
|
|
34
34
|
|
|
35
35
|
Arguments:
|
|
36
|
-
rules: Tag-name to
|
|
36
|
+
rules: Tag-name to component conversion rules.
|
|
37
37
|
"""
|
|
38
38
|
self._rules = rules
|
|
39
39
|
|
|
40
40
|
def convert(self, element: str) -> ComponentType:
|
|
41
|
-
"""Converts the given (
|
|
41
|
+
"""Converts the given (possibly multi-root) XML string to a component."""
|
|
42
42
|
if len(self._rules) == 0:
|
|
43
43
|
return SafeStr(element)
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ class ETreeConverter:
|
|
|
46
46
|
return self.convert_element(ET.fromstring(element)) # noqa: S314 # Only use from XML strings from a trusted source.
|
|
47
47
|
|
|
48
48
|
def convert_element(self, element: Element) -> ComponentType:
|
|
49
|
-
"""Converts the given `Element` to
|
|
49
|
+
"""Converts the given `Element` to a component."""
|
|
50
50
|
rules = self._rules
|
|
51
51
|
if len(rules) == 0:
|
|
52
52
|
return SafeStr(ET.tostring(element))
|
|
@@ -67,7 +67,7 @@ class ETreeConverter:
|
|
|
67
67
|
|
|
68
68
|
def _convert_properties(self, element: Element) -> Properties:
|
|
69
69
|
"""
|
|
70
|
-
Converts the attributes of the given `Element` to
|
|
70
|
+
Converts the attributes of the given `Element` to a `Properties` mapping.
|
|
71
71
|
|
|
72
72
|
This method should not alter property names in any way.
|
|
73
73
|
"""
|
|
@@ -75,8 +75,8 @@ class ETreeConverter:
|
|
|
75
75
|
|
|
76
76
|
def _convert_children(self, element: Element) -> Generator[ComponentType, None, None]:
|
|
77
77
|
"""
|
|
78
|
-
Generator that converts all (text and `Element`) children of the given `Element`
|
|
79
|
-
|
|
78
|
+
Generator that converts all (text and `Element`) children of the given `Element` to a component.
|
|
79
|
+
"""
|
|
80
80
|
if text := self._process_text(element.text):
|
|
81
81
|
yield text
|
|
82
82
|
|
|
@@ -99,9 +99,9 @@ class MD(Snippet):
|
|
|
99
99
|
Arguments:
|
|
100
100
|
path_or_text: The path where the markdown file is located or a markdown `Text`.
|
|
101
101
|
converter: Function that converts an HTML string (the parsed and processed markdown text)
|
|
102
|
-
into
|
|
103
|
-
renderer: Function that
|
|
104
|
-
and turns them into
|
|
102
|
+
into a component.
|
|
103
|
+
renderer: Function that gets the parsed and converted content and the metadata (if it exists)
|
|
104
|
+
and turns them into a component.
|
|
105
105
|
text_processor: An optional text processors that can be used to process the text
|
|
106
106
|
content before rendering. It can be used for example for token replacement or
|
|
107
107
|
string formatting.
|
|
@@ -4,12 +4,21 @@ import asyncio
|
|
|
4
4
|
from collections import ChainMap
|
|
5
5
|
from collections.abc import Awaitable, Callable, Iterable
|
|
6
6
|
|
|
7
|
-
from .core import ErrorBoundary, xml_format_string
|
|
8
|
-
from .typing import Component, ComponentType, Context
|
|
7
|
+
from htmy.core import ErrorBoundary, xml_format_string
|
|
8
|
+
from htmy.typing import Component, ComponentType, Context
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
12
|
-
"""
|
|
11
|
+
class Renderer:
|
|
12
|
+
"""
|
|
13
|
+
The baseline component renderer.
|
|
14
|
+
|
|
15
|
+
Because of the simple, recursive implementation, this renderer is the easiest to reason about.
|
|
16
|
+
Therefore it is useful for validating component correctness before bug reporting (if another
|
|
17
|
+
renderer implementation fails), testing and debugging alternative implementations, and it can
|
|
18
|
+
also serve as the baseline for benchmarking optimized renderers.
|
|
19
|
+
|
|
20
|
+
The performance of this renderer is not production quality.
|
|
21
|
+
"""
|
|
13
22
|
|
|
14
23
|
__slots__ = ("_default_context", "_string_formatter")
|
|
15
24
|
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections import ChainMap, deque
|
|
5
|
+
from collections.abc import Awaitable, Callable, Iterator
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from htmy.core import ErrorBoundary, xml_format_string
|
|
9
|
+
from htmy.typing import Component, ComponentType, Context, ContextProvider, is_component_sequence
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Node:
|
|
13
|
+
"""A single node in the linked list the renderer constructs to resolve a component tree."""
|
|
14
|
+
|
|
15
|
+
__slots__ = ("component", "next")
|
|
16
|
+
|
|
17
|
+
def __init__(self, component: ComponentType, next: _Node | None = None) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initialization.
|
|
20
|
+
|
|
21
|
+
Arguments:
|
|
22
|
+
component: The component in this node.
|
|
23
|
+
next: The next component in the list, if there is one.
|
|
24
|
+
"""
|
|
25
|
+
self.component = component
|
|
26
|
+
self.next = next
|
|
27
|
+
|
|
28
|
+
def iter_nodes(self, *, include_self: bool = True) -> Iterator[_Node]:
|
|
29
|
+
"""
|
|
30
|
+
Iterates over all following nodes.
|
|
31
|
+
|
|
32
|
+
Arguments:
|
|
33
|
+
include_self: Whether the node on which this method is called should also
|
|
34
|
+
be included in the iterator.
|
|
35
|
+
"""
|
|
36
|
+
current = self if include_self else self.next
|
|
37
|
+
while current is not None:
|
|
38
|
+
yield current
|
|
39
|
+
current = current.next
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_NodeAndChildContext: TypeAlias = tuple[_Node, Context]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _ComponentRenderer:
|
|
46
|
+
"""
|
|
47
|
+
`ComponentType` renderer that converts a component tree into a linked list of resolved (`str`) nodes.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
__slots__ = ("_async_todos", "_error_boundary_todos", "_sync_todos", "_root", "_string_formatter")
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
component: ComponentType,
|
|
55
|
+
context: Context,
|
|
56
|
+
*,
|
|
57
|
+
string_formatter: Callable[[str], str],
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Initialization.
|
|
61
|
+
|
|
62
|
+
Arguments:
|
|
63
|
+
component: The component to render.
|
|
64
|
+
context: The base context to use for rendering the component.
|
|
65
|
+
string_formatter: The string formatter to use.
|
|
66
|
+
"""
|
|
67
|
+
self._async_todos: deque[_NodeAndChildContext] = deque()
|
|
68
|
+
"""Async node - context tuples that need to be rendered."""
|
|
69
|
+
self._error_boundary_todos: deque[_NodeAndChildContext] = deque()
|
|
70
|
+
"""Node context tuples where `node.component` is an `ErrorBoundary`."""
|
|
71
|
+
self._sync_todos: deque[_NodeAndChildContext] = deque()
|
|
72
|
+
"""
|
|
73
|
+
Sync node - context tuples that need to be rendered (`node.component` is an `HTMYComponentType`).
|
|
74
|
+
"""
|
|
75
|
+
self._string_formatter = string_formatter
|
|
76
|
+
"""The string formatter to use."""
|
|
77
|
+
|
|
78
|
+
if isinstance(component, str):
|
|
79
|
+
root = _Node(string_formatter(component), None)
|
|
80
|
+
else:
|
|
81
|
+
root = _Node(component, None)
|
|
82
|
+
self._schedule_node(root, context)
|
|
83
|
+
self._root = root
|
|
84
|
+
"""The root node in the linked list the renderer constructs."""
|
|
85
|
+
|
|
86
|
+
async def _extend_context(self, component: ContextProvider, context: Context) -> Context:
|
|
87
|
+
"""
|
|
88
|
+
Returns a new context from the given component and context.
|
|
89
|
+
|
|
90
|
+
Arguments:
|
|
91
|
+
component: A `ContextProvider` component.
|
|
92
|
+
context: The current rendering context.
|
|
93
|
+
"""
|
|
94
|
+
extra_context: Context | Awaitable[Context] = component.htmy_context()
|
|
95
|
+
if isinstance(extra_context, Awaitable):
|
|
96
|
+
extra_context = await extra_context
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
# Context must not be mutated. We can ignore that ChainMap expects mutable mappings.
|
|
100
|
+
ChainMap(extra_context, context) # type: ignore[arg-type]
|
|
101
|
+
if extra_context
|
|
102
|
+
else context
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def _process_error_boundary(self, node: _Node, context: Context) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Processes a single node whose component is an `ErrorBoundary`.
|
|
108
|
+
"""
|
|
109
|
+
component: ErrorBoundary = node.component # type: ignore[assignment]
|
|
110
|
+
if hasattr(component, "htmy_context"): # isinstance() is too expensive.
|
|
111
|
+
context = await self._extend_context(component, context)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
result = await _render_component(
|
|
115
|
+
component.htmy(context),
|
|
116
|
+
context=context,
|
|
117
|
+
string_formatter=self._string_formatter,
|
|
118
|
+
)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
renderer = _ComponentRenderer(
|
|
121
|
+
component.fallback_component(e),
|
|
122
|
+
context,
|
|
123
|
+
string_formatter=self._string_formatter,
|
|
124
|
+
)
|
|
125
|
+
result = await renderer.run()
|
|
126
|
+
|
|
127
|
+
node.component = result # No string formatting.
|
|
128
|
+
|
|
129
|
+
def _process_node_result(self, parent_node: _Node, component: Component, context: Context) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Processes the result of a single node.
|
|
132
|
+
|
|
133
|
+
Arguments:
|
|
134
|
+
parent_node: The node that was resolved.
|
|
135
|
+
component: The (awaited if async) result of `parent_node.component.htmy()`.
|
|
136
|
+
context: The context that was used for rendering `parent_node.component`.
|
|
137
|
+
"""
|
|
138
|
+
schedule_node = self._schedule_node
|
|
139
|
+
string_formatter = self._string_formatter
|
|
140
|
+
if is_component_sequence(component):
|
|
141
|
+
if len(component) == 0:
|
|
142
|
+
parent_node.component = ""
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
first_comp, *rest_comps = component
|
|
146
|
+
if isinstance(first_comp, str):
|
|
147
|
+
parent_node.component = string_formatter(first_comp)
|
|
148
|
+
else:
|
|
149
|
+
parent_node.component = first_comp
|
|
150
|
+
schedule_node(parent_node, context)
|
|
151
|
+
|
|
152
|
+
old_next = parent_node.next
|
|
153
|
+
last: _Node = parent_node
|
|
154
|
+
for c in rest_comps:
|
|
155
|
+
if isinstance(c, str):
|
|
156
|
+
node = _Node(string_formatter(c), old_next)
|
|
157
|
+
else:
|
|
158
|
+
node = _Node(c, old_next)
|
|
159
|
+
schedule_node(node, context)
|
|
160
|
+
|
|
161
|
+
last.next = node
|
|
162
|
+
last = node
|
|
163
|
+
elif isinstance(component, str):
|
|
164
|
+
parent_node.component = string_formatter(component)
|
|
165
|
+
else:
|
|
166
|
+
parent_node.component = component # type: ignore[assignment]
|
|
167
|
+
schedule_node(parent_node, context)
|
|
168
|
+
|
|
169
|
+
async def _process_async_node(self, node: _Node, context: Context) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Processes the given node. `node.component` must be an async component.
|
|
172
|
+
"""
|
|
173
|
+
result = await node.component.htmy(context) # type: ignore[misc,union-attr]
|
|
174
|
+
self._process_node_result(node, result, context)
|
|
175
|
+
|
|
176
|
+
def _schedule_node(self, node: _Node, child_context: Context) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Schedules the given node for rendering with the given child context.
|
|
179
|
+
|
|
180
|
+
`node.component` must be an `HTMYComponentType` (single component and not `str`).
|
|
181
|
+
"""
|
|
182
|
+
component = node.component
|
|
183
|
+
if asyncio.iscoroutinefunction(component.htmy): # type: ignore[union-attr]
|
|
184
|
+
self._async_todos.append((node, child_context))
|
|
185
|
+
elif isinstance(component, ErrorBoundary):
|
|
186
|
+
self._error_boundary_todos.append((node, child_context))
|
|
187
|
+
else:
|
|
188
|
+
self._sync_todos.append((node, child_context))
|
|
189
|
+
|
|
190
|
+
async def run(self) -> str:
|
|
191
|
+
"""Runs the component renderer."""
|
|
192
|
+
async_todos = self._async_todos
|
|
193
|
+
sync_todos = self._sync_todos
|
|
194
|
+
process_node_result = self._process_node_result
|
|
195
|
+
process_async_node = self._process_async_node
|
|
196
|
+
|
|
197
|
+
while sync_todos or async_todos:
|
|
198
|
+
while sync_todos:
|
|
199
|
+
node, child_context = sync_todos.pop()
|
|
200
|
+
component = node.component
|
|
201
|
+
if hasattr(component, "htmy_context"): # isinstance() is too expensive.
|
|
202
|
+
child_context = await self._extend_context(component, child_context) # type: ignore[arg-type]
|
|
203
|
+
|
|
204
|
+
if asyncio.iscoroutinefunction(node.component.htmy): # type: ignore[union-attr]
|
|
205
|
+
async_todos.append((node, child_context))
|
|
206
|
+
else:
|
|
207
|
+
result: Component = node.component.htmy(child_context) # type: ignore[assignment,union-attr]
|
|
208
|
+
process_node_result(node, result, child_context)
|
|
209
|
+
|
|
210
|
+
if async_todos:
|
|
211
|
+
await asyncio.gather(*(process_async_node(n, ctx) for n, ctx in async_todos))
|
|
212
|
+
async_todos.clear()
|
|
213
|
+
|
|
214
|
+
if self._error_boundary_todos:
|
|
215
|
+
await asyncio.gather(
|
|
216
|
+
*(self._process_error_boundary(n, ctx) for n, ctx in self._error_boundary_todos)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return "".join(node.component for node in self._root.iter_nodes()) # type: ignore[misc]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def _render_component(
|
|
223
|
+
component: Component,
|
|
224
|
+
*,
|
|
225
|
+
context: Context,
|
|
226
|
+
string_formatter: Callable[[str], str],
|
|
227
|
+
) -> str:
|
|
228
|
+
"""Renders the given component with the given settings."""
|
|
229
|
+
if is_component_sequence(component):
|
|
230
|
+
if len(component) == 0:
|
|
231
|
+
return ""
|
|
232
|
+
|
|
233
|
+
renderers = (_ComponentRenderer(c, context, string_formatter=string_formatter) for c in component)
|
|
234
|
+
return "".join(await asyncio.gather(*(r.run() for r in renderers)))
|
|
235
|
+
else:
|
|
236
|
+
return await _ComponentRenderer(component, context, string_formatter=string_formatter).run() # type: ignore[arg-type]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Renderer:
|
|
240
|
+
"""
|
|
241
|
+
The default renderer.
|
|
242
|
+
|
|
243
|
+
It resolves component trees by converting them to a linked list of resolved component parts
|
|
244
|
+
before combining them to the final string.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
__slots__ = ("_default_context", "_string_formatter")
|
|
248
|
+
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
default_context: Context | None = None,
|
|
252
|
+
*,
|
|
253
|
+
string_formatter: Callable[[str], str] = xml_format_string,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Initialization.
|
|
257
|
+
|
|
258
|
+
Arguments:
|
|
259
|
+
default_context: The default context to use for rendering if `render()` doesn't
|
|
260
|
+
receive a context.
|
|
261
|
+
string_formatter: Callable that should be used to format plain strings. By default
|
|
262
|
+
an XML-safe string formatter will be used.
|
|
263
|
+
"""
|
|
264
|
+
self._default_context: Context = {} if default_context is None else default_context
|
|
265
|
+
self._string_formatter = string_formatter
|
|
266
|
+
|
|
267
|
+
async def render(self, component: Component, context: Context | None = None) -> str:
|
|
268
|
+
"""
|
|
269
|
+
Renders the given component.
|
|
270
|
+
|
|
271
|
+
Arguments:
|
|
272
|
+
component: The component to render.
|
|
273
|
+
context: An optional rendering context.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
The rendered string.
|
|
277
|
+
"""
|
|
278
|
+
# Type ignore: ChainMap expects mutable mappings, but context mutation is not allowed so don't care.
|
|
279
|
+
context = (
|
|
280
|
+
self._default_context if context is None else ChainMap(context, self._default_context) # type: ignore[arg-type]
|
|
281
|
+
)
|
|
282
|
+
return await _render_component(component, context=context, string_formatter=self._string_formatter)
|
|
@@ -88,7 +88,7 @@ class SyncContextProvider(Protocol):
|
|
|
88
88
|
"""Protocol definition for sync context providers."""
|
|
89
89
|
|
|
90
90
|
def htmy_context(self) -> Context:
|
|
91
|
-
"""Returns
|
|
91
|
+
"""Returns a context for child rendering."""
|
|
92
92
|
...
|
|
93
93
|
|
|
94
94
|
|
|
@@ -97,7 +97,7 @@ class AsyncContextProvider(Protocol):
|
|
|
97
97
|
"""Protocol definition for async context providers."""
|
|
98
98
|
|
|
99
99
|
async def htmy_context(self) -> Context:
|
|
100
|
-
"""Returns
|
|
100
|
+
"""Returns a context for child rendering."""
|
|
101
101
|
...
|
|
102
102
|
|
|
103
103
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "htmy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Async, pure-Python rendering engine."
|
|
5
5
|
authors = ["Peter Volf <do.volfp@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -47,13 +47,13 @@ exclude = [
|
|
|
47
47
|
"docs",
|
|
48
48
|
]
|
|
49
49
|
lint.select = [
|
|
50
|
+
"B", # flake8-bugbear
|
|
51
|
+
"C", # flake8-comprehensions
|
|
50
52
|
"E", # pycodestyle errors
|
|
51
|
-
"W", # pycodestyle warnings
|
|
52
53
|
"F", # pyflakes
|
|
53
54
|
"I", # isort
|
|
54
55
|
"S", # flake8-bandit - we must ignore these rules in tests
|
|
55
|
-
"
|
|
56
|
-
"B", # flake8-bugbear
|
|
56
|
+
"W", # pycodestyle warnings
|
|
57
57
|
]
|
|
58
58
|
|
|
59
59
|
[tool.ruff.lint.per-file-ignores]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|