htmy 0.3.5__py3-none-any.whl → 0.4.0__py3-none-any.whl
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/__init__.py +4 -1
- htmy/core.py +20 -6
- htmy/etree.py +7 -7
- htmy/md/core.py +9 -4
- htmy/renderer/__init__.py +8 -0
- htmy/{renderer.py → renderer/baseline.py} +13 -4
- htmy/renderer/default.py +282 -0
- htmy/typing.py +8 -2
- {htmy-0.3.5.dist-info → htmy-0.4.0.dist-info}/METADATA +8 -8
- htmy-0.4.0.dist-info/RECORD +19 -0
- htmy-0.3.5.dist-info/RECORD +0 -17
- {htmy-0.3.5.dist-info → htmy-0.4.0.dist-info}/LICENSE +0 -0
- {htmy-0.3.5.dist-info → htmy-0.4.0.dist-info}/WHEEL +0 -0
htmy/__init__.py
CHANGED
|
@@ -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`."""
|
htmy/core.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import abc
|
|
4
4
|
import asyncio
|
|
5
5
|
import enum
|
|
6
|
-
from collections.abc import Callable, Container
|
|
6
|
+
from collections.abc import Awaitable, Callable, Container
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict, cast, overload
|
|
9
9
|
from xml.sax.saxutils import escape as xml_escape
|
|
@@ -21,6 +21,7 @@ from .typing import (
|
|
|
21
21
|
PropertyValue,
|
|
22
22
|
SyncFunctionComponent,
|
|
23
23
|
T,
|
|
24
|
+
TextProcessor,
|
|
24
25
|
is_component_sequence,
|
|
25
26
|
)
|
|
26
27
|
from .utils import join_components
|
|
@@ -121,7 +122,7 @@ class WithContext(Fragment):
|
|
|
121
122
|
self._context = context
|
|
122
123
|
|
|
123
124
|
def htmy_context(self) -> Context:
|
|
124
|
-
"""Returns
|
|
125
|
+
"""Returns the context for child rendering."""
|
|
125
126
|
return self._context
|
|
126
127
|
|
|
127
128
|
|
|
@@ -130,21 +131,34 @@ class Snippet:
|
|
|
130
131
|
Base component that can load its content from a file.
|
|
131
132
|
"""
|
|
132
133
|
|
|
133
|
-
__slots__ = ("_path_or_text",)
|
|
134
|
+
__slots__ = ("_path_or_text", "_text_processor")
|
|
134
135
|
|
|
135
|
-
def __init__(
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
path_or_text: Text | str | Path,
|
|
139
|
+
*,
|
|
140
|
+
text_processor: TextProcessor | None = None,
|
|
141
|
+
) -> None:
|
|
136
142
|
"""
|
|
137
143
|
Initialization.
|
|
138
144
|
|
|
139
145
|
Arguments:
|
|
140
146
|
path_or_text: The path from where the content should be loaded or a `Text`
|
|
141
147
|
instance if this value should be rendered directly.
|
|
148
|
+
text_processor: An optional text processors that can be used to process the text
|
|
149
|
+
content before rendering. It can be used for example for token replacement or
|
|
150
|
+
string formatting.
|
|
142
151
|
"""
|
|
143
152
|
self._path_or_text = path_or_text
|
|
153
|
+
self._text_processor = text_processor
|
|
144
154
|
|
|
145
155
|
async def htmy(self, context: Context) -> Component:
|
|
146
156
|
"""Renders the component."""
|
|
147
157
|
text = await self._get_text_content()
|
|
158
|
+
if self._text_processor is not None:
|
|
159
|
+
processed = self._text_processor(text, context)
|
|
160
|
+
text = (await processed) if isinstance(processed, Awaitable) else processed
|
|
161
|
+
|
|
148
162
|
return self._render_text(text, context)
|
|
149
163
|
|
|
150
164
|
async def _get_text_content(self) -> str:
|
|
@@ -160,7 +174,7 @@ class Snippet:
|
|
|
160
174
|
def _render_text(self, text: str, context: Context) -> Component:
|
|
161
175
|
"""
|
|
162
176
|
Render function that takes the text that must be rendered and the current rendering context,
|
|
163
|
-
and returns the corresponding
|
|
177
|
+
and returns the corresponding component.
|
|
164
178
|
"""
|
|
165
179
|
return SafeStr(text)
|
|
166
180
|
|
|
@@ -509,7 +523,7 @@ class BaseTag(abc.ABC):
|
|
|
509
523
|
|
|
510
524
|
@abc.abstractmethod
|
|
511
525
|
def htmy(self, context: Context) -> Component:
|
|
512
|
-
"""Abstract base
|
|
526
|
+
"""Abstract base component implementation."""
|
|
513
527
|
...
|
|
514
528
|
|
|
515
529
|
def _get_htmy_name(self) -> str:
|
htmy/etree.py
CHANGED
|
@@ -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
|
|
htmy/md/core.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, ClassVar
|
|
|
5
5
|
from markdown import Markdown
|
|
6
6
|
|
|
7
7
|
from htmy.core import ContextAware, SafeStr, Snippet, Text
|
|
8
|
+
from htmy.typing import TextProcessor
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
11
|
from collections.abc import Callable
|
|
@@ -90,6 +91,7 @@ class MD(Snippet):
|
|
|
90
91
|
*,
|
|
91
92
|
converter: Callable[[str], Component] | None = None,
|
|
92
93
|
renderer: MarkdownRenderFunction | None = None,
|
|
94
|
+
text_processor: TextProcessor | None = None,
|
|
93
95
|
) -> None:
|
|
94
96
|
"""
|
|
95
97
|
Initialization.
|
|
@@ -97,11 +99,14 @@ class MD(Snippet):
|
|
|
97
99
|
Arguments:
|
|
98
100
|
path_or_text: The path where the markdown file is located or a markdown `Text`.
|
|
99
101
|
converter: Function that converts an HTML string (the parsed and processed markdown text)
|
|
100
|
-
into
|
|
101
|
-
renderer: Function that
|
|
102
|
-
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
|
+
text_processor: An optional text processors that can be used to process the text
|
|
106
|
+
content before rendering. It can be used for example for token replacement or
|
|
107
|
+
string formatting.
|
|
103
108
|
"""
|
|
104
|
-
super().__init__(path_or_text)
|
|
109
|
+
super().__init__(path_or_text, text_processor=text_processor)
|
|
105
110
|
self._converter: Callable[[str], Component] = SafeStr if converter is None else converter
|
|
106
111
|
self._renderer = renderer
|
|
107
112
|
|
|
@@ -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
|
|
htmy/renderer/default.py
ADDED
|
@@ -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)
|
htmy/typing.py
CHANGED
|
@@ -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,9 +97,15 @@ 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
|
|
|
104
104
|
ContextProvider: TypeAlias = SyncContextProvider | AsyncContextProvider
|
|
105
105
|
"""Context provider type."""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# -- Text processors
|
|
109
|
+
|
|
110
|
+
TextProcessor: TypeAlias = Callable[[str, Context], str | Coroutine[Any, Any, str]]
|
|
111
|
+
"""Callable type that expects a string and a context, and returns a processed string."""
|
|
@@ -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
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
htmy/__init__.py,sha256=yMPXQHkXQCjyx7UUVcfsMQ_5YjvNT62Kb9TI1xEcw2A,1899
|
|
2
|
+
htmy/core.py,sha256=5V019PwIes9Eyzu0eWwsvBZNZ6x7DhQy9y99myCw9A0,19322
|
|
3
|
+
htmy/etree.py,sha256=yKxom__AdsJY-Q1kbU0sdTMr0ZF5dMSVBKxayntNFyQ,3062
|
|
4
|
+
htmy/html.py,sha256=pxmE-KU5OgwNp6MyxOfdS0Ohpzu2RNYCeGGFlHLDGUM,20940
|
|
5
|
+
htmy/i18n.py,sha256=brNazQjObBFfbnViZCpcnxa0qgxQbJfX7xJAH-MqTW8,5124
|
|
6
|
+
htmy/io.py,sha256=iebJOZp7L0kZ9SWdqMatKtW5VGRIkEd-eD0_vTAldH8,41
|
|
7
|
+
htmy/md/__init__.py,sha256=lxBJnYplkDuxYuiese6My9KYp1DeGdzo70iUdYTvMnE,334
|
|
8
|
+
htmy/md/core.py,sha256=EK-QoB2BIra3o1nvTK0lGP3mQQPtRXuE6zBKF_IWR_o,3675
|
|
9
|
+
htmy/md/typing.py,sha256=LF-AEvo7FCW2KumyR5l55rsXizV2E4AHVLKFf6lApgM,762
|
|
10
|
+
htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
htmy/renderer/__init__.py,sha256=xnP_aaoK-pTok-69wi8O_xlsgjoKTzWd2lIIeHGcuaY,226
|
|
12
|
+
htmy/renderer/baseline.py,sha256=hHb7CoQhFFdD7Sdw0ltR1-XLGwE9pqmfL5yKFeF2rCg,4288
|
|
13
|
+
htmy/renderer/default.py,sha256=rdx-yFYz-cz197xfe9co8Lru2cdZxAjOO4dqY250Y1Q,10767
|
|
14
|
+
htmy/typing.py,sha256=f4QZ8vQL7JfN402yDb8Hq_DYvQS_GUgdXK8-xTBM8y8,3122
|
|
15
|
+
htmy/utils.py,sha256=7_CyA39l2m6jzDqparPKkKgRB2wiGuBZXbiPgiZOXKA,1093
|
|
16
|
+
htmy-0.4.0.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
|
|
17
|
+
htmy-0.4.0.dist-info/METADATA,sha256=kuRCDnI6icrwlaYUOuK9FAe__t5aqjOX8_QTtqN6vNU,16379
|
|
18
|
+
htmy-0.4.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
19
|
+
htmy-0.4.0.dist-info/RECORD,,
|
htmy-0.3.5.dist-info/RECORD
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
htmy/__init__.py,sha256=My_Dmh9JNQpQekTO6CEbdbV7Ux7MD9Pz-L2M3KMZjGQ,1835
|
|
2
|
-
htmy/core.py,sha256=JZNryJU1VCAVBf1wzkaa9bD3LiepT-Eu28XydfbWpOw,18735
|
|
3
|
-
htmy/etree.py,sha256=zZkKY82t5fX85unS9oHuG6KEBsJY_iz6E7SJto8lSVQ,3097
|
|
4
|
-
htmy/html.py,sha256=pxmE-KU5OgwNp6MyxOfdS0Ohpzu2RNYCeGGFlHLDGUM,20940
|
|
5
|
-
htmy/i18n.py,sha256=brNazQjObBFfbnViZCpcnxa0qgxQbJfX7xJAH-MqTW8,5124
|
|
6
|
-
htmy/io.py,sha256=iebJOZp7L0kZ9SWdqMatKtW5VGRIkEd-eD0_vTAldH8,41
|
|
7
|
-
htmy/md/__init__.py,sha256=lxBJnYplkDuxYuiese6My9KYp1DeGdzo70iUdYTvMnE,334
|
|
8
|
-
htmy/md/core.py,sha256=-EKucDFKMUtGgs9k_q9134oXY2GXtdKX1KOJXG4YmKc,3342
|
|
9
|
-
htmy/md/typing.py,sha256=LF-AEvo7FCW2KumyR5l55rsXizV2E4AHVLKFf6lApgM,762
|
|
10
|
-
htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
htmy/renderer.py,sha256=vCYqq83RaEtlXKDGWB6LgKjoE1TSfMfB5TW1lJWHE5c,3829
|
|
12
|
-
htmy/typing.py,sha256=iUZQjMU-DHMV1Mv-ugW26eSPlrFO-KVhWfkLod7dig4,2937
|
|
13
|
-
htmy/utils.py,sha256=7_CyA39l2m6jzDqparPKkKgRB2wiGuBZXbiPgiZOXKA,1093
|
|
14
|
-
htmy-0.3.5.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
|
|
15
|
-
htmy-0.3.5.dist-info/METADATA,sha256=emcRbV2rYZ_RP4LqKGjzv3WBdadUpyQREdujGyycXt0,16347
|
|
16
|
-
htmy-0.3.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
-
htmy-0.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|