htmy 0.8.2__py3-none-any.whl → 0.9.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.
- htmy/__init__.py +4 -0
- htmy/md/core.py +6 -2
- htmy/renderer/baseline.py +8 -4
- htmy/renderer/default.py +9 -4
- htmy/snippet.py +4 -0
- htmy/typing.py +4 -1
- {htmy-0.8.2.dist-info → htmy-0.9.0.dist-info}/METADATA +31 -4
- {htmy-0.8.2.dist-info → htmy-0.9.0.dist-info}/RECORD +10 -10
- {htmy-0.8.2.dist-info → htmy-0.9.0.dist-info}/WHEEL +1 -1
- {htmy-0.8.2.dist-info → htmy-0.9.0.dist-info/licenses}/LICENSE +1 -1
htmy/__init__.py
CHANGED
|
@@ -31,12 +31,16 @@ from .typing import MutableContext as MutableContext
|
|
|
31
31
|
from .typing import Properties as Properties
|
|
32
32
|
from .typing import PropertyValue as PropertyValue
|
|
33
33
|
from .typing import RendererType as RendererType
|
|
34
|
+
from .typing import StrictComponentType as StrictComponentType
|
|
34
35
|
from .typing import SyncComponent as SyncComponent
|
|
35
36
|
from .typing import SyncContextProvider as SyncContextProvider
|
|
36
37
|
from .utils import as_component_sequence as as_component_sequence
|
|
37
38
|
from .utils import as_component_type as as_component_type
|
|
38
39
|
from .utils import is_component_sequence as is_component_sequence
|
|
40
|
+
from .utils import join
|
|
39
41
|
from .utils import join_components as join_components
|
|
40
42
|
|
|
43
|
+
join_classes = join
|
|
44
|
+
|
|
41
45
|
HTMY = Renderer
|
|
42
46
|
"""Deprecated alias for `Renderer`."""
|
htmy/md/core.py
CHANGED
|
@@ -22,7 +22,7 @@ class MarkdownParser(ContextAware):
|
|
|
22
22
|
Context-aware markdown parser.
|
|
23
23
|
|
|
24
24
|
By default, this class uses the `markdown` library with a sensible set of
|
|
25
|
-
[extensions](https://python-markdown.github.io/extensions/) including code
|
|
25
|
+
[extensions](https://python-markdown.github.io/extensions/) including code highlighting.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
__slots__ = ("_md",)
|
|
@@ -85,10 +85,14 @@ class MD(Snippet):
|
|
|
85
85
|
It supports all the processing utilities of `Snippet`, including `text_resolver` and
|
|
86
86
|
`text_processor` for formatting, token replacement, and slot conversion to components.
|
|
87
87
|
|
|
88
|
-
One note
|
|
88
|
+
One note regarding slot conversion (`text_resolver`): it is executed before markdown parsing,
|
|
89
89
|
and all string segments of the resulting component sequence are parsed individually by the
|
|
90
90
|
markdown parser. As a consequence, you should only use slots in places where the preceding
|
|
91
91
|
and following texts individually result in valid markdown.
|
|
92
|
+
|
|
93
|
+
**Warning:** The component treats its input as trusted. If any part of the input comes from
|
|
94
|
+
untrusted sources, ensure it is safely escaped (using for example `htmy.xml_format_string`)!
|
|
95
|
+
Passing untrusted input to this component leads to XSS vulnerabilities.
|
|
92
96
|
"""
|
|
93
97
|
|
|
94
98
|
__slots__ = (
|
htmy/renderer/baseline.py
CHANGED
|
@@ -73,16 +73,18 @@ class Renderer:
|
|
|
73
73
|
"""
|
|
74
74
|
if isinstance(component, str):
|
|
75
75
|
return self._string_formatter(component)
|
|
76
|
+
elif component is None:
|
|
77
|
+
return ""
|
|
76
78
|
elif isinstance(component, Iterable):
|
|
77
79
|
rendered_children = await asyncio.gather(
|
|
78
|
-
*(self._render_one(comp, context) for comp in component)
|
|
80
|
+
*(self._render_one(comp, context) for comp in component if comp is not None)
|
|
79
81
|
)
|
|
80
82
|
|
|
81
|
-
return "".join(rendered_children)
|
|
83
|
+
return "".join(child for child in rendered_children if child is not None)
|
|
82
84
|
else:
|
|
83
|
-
return await self._render_one(component, context)
|
|
85
|
+
return await self._render_one(component, context) or ""
|
|
84
86
|
|
|
85
|
-
async def _render_one(self, component: ComponentType, context: Context) -> str:
|
|
87
|
+
async def _render_one(self, component: ComponentType, context: Context) -> str | None:
|
|
86
88
|
"""
|
|
87
89
|
Renders a single component.
|
|
88
90
|
|
|
@@ -95,6 +97,8 @@ class Renderer:
|
|
|
95
97
|
"""
|
|
96
98
|
if isinstance(component, str):
|
|
97
99
|
return self._string_formatter(component)
|
|
100
|
+
elif component is None:
|
|
101
|
+
return None
|
|
98
102
|
else:
|
|
99
103
|
child_context: Context = context
|
|
100
104
|
if hasattr(component, "htmy_context"): # isinstance() is too expensive.
|
htmy/renderer/default.py
CHANGED
|
@@ -181,7 +181,9 @@ class _ComponentRenderer:
|
|
|
181
181
|
`node.component` must be an `HTMYComponentType` (single component and not `str`).
|
|
182
182
|
"""
|
|
183
183
|
component = node.component
|
|
184
|
-
if
|
|
184
|
+
if component is None:
|
|
185
|
+
pass # Just skip the node
|
|
186
|
+
elif asyncio.iscoroutinefunction(component.htmy): # type: ignore[union-attr]
|
|
185
187
|
self._async_todos.append((node, child_context))
|
|
186
188
|
elif isinstance(component, ErrorBoundary):
|
|
187
189
|
self._error_boundary_todos.append((node, child_context))
|
|
@@ -199,13 +201,16 @@ class _ComponentRenderer:
|
|
|
199
201
|
while sync_todos:
|
|
200
202
|
node, child_context = sync_todos.pop()
|
|
201
203
|
component = node.component
|
|
204
|
+
if component is None:
|
|
205
|
+
continue
|
|
206
|
+
|
|
202
207
|
if hasattr(component, "htmy_context"): # isinstance() is too expensive.
|
|
203
208
|
child_context = await self._extend_context(component, child_context) # type: ignore[arg-type]
|
|
204
209
|
|
|
205
|
-
if asyncio.iscoroutinefunction(
|
|
210
|
+
if asyncio.iscoroutinefunction(component.htmy): # type: ignore[union-attr]
|
|
206
211
|
async_todos.append((node, child_context))
|
|
207
212
|
else:
|
|
208
|
-
result: Component =
|
|
213
|
+
result: Component = component.htmy(child_context) # type: ignore[assignment,union-attr]
|
|
209
214
|
process_node_result(node, result, child_context)
|
|
210
215
|
|
|
211
216
|
if async_todos:
|
|
@@ -218,7 +223,7 @@ class _ComponentRenderer:
|
|
|
218
223
|
*(self._process_error_boundary(n, ctx) for n, ctx in self._error_boundary_todos)
|
|
219
224
|
)
|
|
220
225
|
|
|
221
|
-
return "".join(node.component for node in self._root.iter_nodes()) # type: ignore[misc]
|
|
226
|
+
return "".join(node.component for node in self._root.iter_nodes() if node.component is not None) # type: ignore[misc]
|
|
222
227
|
|
|
223
228
|
|
|
224
229
|
async def _render_component(
|
htmy/snippet.py
CHANGED
|
@@ -158,6 +158,10 @@ class Snippet:
|
|
|
158
158
|
"""
|
|
159
159
|
Component that renders text, which may be asynchronously loaded from a file.
|
|
160
160
|
|
|
161
|
+
**Warning:** The component treats its input as trusted. If any part of the input comes from
|
|
162
|
+
untrusted sources, ensure it is safely escaped (using for example `htmy.xml_format_string`)!
|
|
163
|
+
Passing untrusted input to this component leads to XSS vulnerabilities.
|
|
164
|
+
|
|
161
165
|
The entire snippet processing pipeline consists of the following steps:
|
|
162
166
|
|
|
163
167
|
1. The text content is loaded from a file or passed directly as a `Text` instance.
|
htmy/typing.py
CHANGED
|
@@ -55,7 +55,10 @@ class AsyncComponent(Protocol):
|
|
|
55
55
|
HTMYComponentType: TypeAlias = SyncComponent | AsyncComponent
|
|
56
56
|
"""Sync or async `htmy` component type."""
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
StrictComponentType: TypeAlias = HTMYComponentType | str
|
|
59
|
+
"""Type definition for a single component that's not `None`."""
|
|
60
|
+
|
|
61
|
+
ComponentType: TypeAlias = StrictComponentType | None
|
|
59
62
|
"""Type definition for a single component."""
|
|
60
63
|
|
|
61
64
|
# Omit strings from this type to simplify checks.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: htmy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Async, pure-Python server-side rendering engine.
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Peter Volf
|
|
7
8
|
Author-email: do.volfp@gmail.com
|
|
8
9
|
Requires-Python: >=3.10,<4.0
|
|
@@ -12,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
17
|
Provides-Extra: lxml
|
|
16
18
|
Requires-Dist: anyio (>=4.6.2.post1,<5.0.0)
|
|
17
19
|
Requires-Dist: async-lru (>=2.0.4,<3.0.0)
|
|
@@ -36,7 +38,7 @@ Unleash your creativity with the full power and Python, without the hassle of le
|
|
|
36
38
|
|
|
37
39
|
## Key features
|
|
38
40
|
|
|
39
|
-
- **Async**-first, to let you make the best use of
|
|
41
|
+
- **Async**-first, to let you make the best use of modern async tools.
|
|
40
42
|
- **Powerful**, React-like **context support**, so you can avoid prop-drilling.
|
|
41
43
|
- Sync and async **function components** with **decorator syntax**.
|
|
42
44
|
- All baseline **HTML** tags built-in.
|
|
@@ -47,6 +49,7 @@ Unleash your creativity with the full power and Python, without the hassle of le
|
|
|
47
49
|
- **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
|
|
48
50
|
- Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
|
|
49
51
|
- Automatic and customizable **property-name conversion** from snake case to kebab case.
|
|
52
|
+
- **Compatible** with any other templating library through wrappers.
|
|
50
53
|
- **Fully-typed**.
|
|
51
54
|
|
|
52
55
|
## Testimonials
|
|
@@ -323,11 +326,35 @@ If a component executes a potentially "long-running" synchronous call, it is str
|
|
|
323
326
|
|
|
324
327
|
In all other cases, it's best to use sync components.
|
|
325
328
|
|
|
329
|
+
## XSS prevention
|
|
330
|
+
|
|
331
|
+
`htmy` does XML/HTML escaping by default. This means user input is normally sanitized and rendered safely.
|
|
332
|
+
|
|
333
|
+
There are a couple of notable exceptions to this, where components by design allow XML/HTML inputs and assume they are safe:
|
|
334
|
+
|
|
335
|
+
- `Snippet`: The primary use-case is to efficiently render XML/HTML templates, filling in placeholders with dynamic content. In this case you must ensure that the input template itself is safe!
|
|
336
|
+
- `MD`: This component builds on `Snippet` to support markdown inputs and performs automatic markdown to HTML conversion. You must ensure the input text is safe!
|
|
337
|
+
|
|
338
|
+
## AI assistance
|
|
339
|
+
|
|
340
|
+
The library is registered at [Context7](https://context7.com/volfpeter).
|
|
341
|
+
|
|
342
|
+
To get good AI assistance, all you need to do is register the Context7 MCP server in your coding tool and tell the agent to use it.
|
|
343
|
+
|
|
344
|
+
Because of the similarity with native HTML, JSX, and React, you can expect good results, both for vibe coding or inline completion.
|
|
345
|
+
|
|
346
|
+
## Compatibility and performance
|
|
347
|
+
|
|
348
|
+
By design, `htmy` is compatible with any other Python templating library, for example Jinja, through wrappers. A wrapper is simply a custom `htmy` component that internally offloads rendering to another templating framework. This makes it possible to easily combine `htmy` with other libraries, to gradually adopt it, and even to enjoy the benefits of multiple frameworks.
|
|
349
|
+
|
|
350
|
+
Performance strongly depends on how you use `htmy`. The `Snippet` component for example makes it possible to reach almost Python string formatting performance, while rendering large, deep component trees is noticeably slower than Jinja for example. Wrapping another templating library for certain use-cases, or pre-rendering components and later using `Snippet` to fill in the dynamic content can be beneficial for performance.
|
|
351
|
+
|
|
326
352
|
## Framework integrations
|
|
327
353
|
|
|
328
354
|
FastAPI:
|
|
329
355
|
|
|
330
|
-
- [
|
|
356
|
+
- [holm](https://github.com/volfpeter/holm): Web development framework that brings the Next.js developer experience to Python, built on FastAPI, htmy, and FastHX.
|
|
357
|
+
- [FastHX](https://github.com/volfpeter/fasthx): Declarative server-side rendering utility for FastAPI with built-in HTMX support.
|
|
331
358
|
|
|
332
359
|
## External examples
|
|
333
360
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
htmy/__init__.py,sha256=
|
|
1
|
+
htmy/__init__.py,sha256=YPl0qVUFQpkISXYkXTCpnWnqnHUdfATmHPu9L3pW5FA,2037
|
|
2
2
|
htmy/core.py,sha256=mvNbxTRS8HNnjMSwe4NEJdL1KF19pJImF4Ciip6837I,15484
|
|
3
3
|
htmy/etree.py,sha256=3znZCYQ5xbNqyXYSYFkUh6M22QLWMYWhTNjUcgpjCLc,4051
|
|
4
4
|
htmy/function_component.py,sha256=iSp5cGrErmIsc-VfNq053_J2m-Nuu_k2xK9UxvEnlw8,12431
|
|
@@ -6,16 +6,16 @@ htmy/html.py,sha256=Pw9KCSn0X01D_fwIpcckI9nsQWNJMiYbcqQH0q2ezuM,21276
|
|
|
6
6
|
htmy/i18n.py,sha256=Kobvm9mFoNcJas52KQbheiRIzJF1Ad1azOhtfm_k0BE,5123
|
|
7
7
|
htmy/io.py,sha256=oEXXVnpdbjs2NzAGi36Pept-pyvXshEGHrbBFzcHYio,344
|
|
8
8
|
htmy/md/__init__.py,sha256=lxBJnYplkDuxYuiese6My9KYp1DeGdzo70iUdYTvMnE,334
|
|
9
|
-
htmy/md/core.py,sha256=
|
|
9
|
+
htmy/md/core.py,sha256=jkgI78otXg7VmjmgeCpyjsNi0E-CqUnKYNVWVt3Sbcs,4729
|
|
10
10
|
htmy/md/typing.py,sha256=LF-AEvo7FCW2KumyR5l55rsXizV2E4AHVLKFf6lApgM,762
|
|
11
11
|
htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
htmy/renderer/__init__.py,sha256=xnP_aaoK-pTok-69wi8O_xlsgjoKTzWd2lIIeHGcuaY,226
|
|
13
|
-
htmy/renderer/baseline.py,sha256=
|
|
14
|
-
htmy/renderer/default.py,sha256=
|
|
15
|
-
htmy/snippet.py,sha256=
|
|
16
|
-
htmy/typing.py,sha256=
|
|
13
|
+
htmy/renderer/baseline.py,sha256=WrKO5cQpvF5AFUuFoEoAS8_lUqTg51m8hkolq9-Uetw,4519
|
|
14
|
+
htmy/renderer/default.py,sha256=n1nAbU4cCtA26rng5D2T4oIjHVSPjRK5GCphJ8NFPmA,11076
|
|
15
|
+
htmy/snippet.py,sha256=Lo99zoIzU8n-qO2-10d8tX_J8SCEmEBpQWdYR0AVsZM,10808
|
|
16
|
+
htmy/typing.py,sha256=qgOj8nZmemzSEQzNRib9tT3l7LETvajOyLkxveA6nss,3671
|
|
17
17
|
htmy/utils.py,sha256=Kp0j9G8CBeRiyFGmz-CoDiLtXHfpvHzlTVsWeDhIebM,1935
|
|
18
|
-
htmy-0.
|
|
19
|
-
htmy-0.
|
|
20
|
-
htmy-0.
|
|
21
|
-
htmy-0.
|
|
18
|
+
htmy-0.9.0.dist-info/METADATA,sha256=aX7JtrXXJTD4bDExa3W08orerBX3J98hI7k3l0nJiMs,22267
|
|
19
|
+
htmy-0.9.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
20
|
+
htmy-0.9.0.dist-info/licenses/LICENSE,sha256=ulLk8GOf1aK1cTSWjx5Hw1C4r_FtcgL4NSXs48zam5M,1067
|
|
21
|
+
htmy-0.9.0.dist-info/RECORD,,
|