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 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 highlighing.
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 regaring slot convesion (`text_resolver`): it is executed before markdown parsing,
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 asyncio.iscoroutinefunction(component.htmy): # type: ignore[union-attr]
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(node.component.htmy): # type: ignore[union-attr]
210
+ if asyncio.iscoroutinefunction(component.htmy): # type: ignore[union-attr]
206
211
  async_todos.append((node, child_context))
207
212
  else:
208
- result: Component = node.component.htmy(child_context) # type: ignore[assignment,union-attr]
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
- ComponentType: TypeAlias = HTMYComponentType | str
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.3
1
+ Metadata-Version: 2.4
2
2
  Name: htmy
3
- Version: 0.8.2
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 [modern async tools](https://github.com/timofurrer/awesome-asyncio).
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
- - [FastHX](https://github.com/volfpeter/fasthx)
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=h1UOBOdcmwzkelpu2uGZXkXO_XLtXYiU6pW1-3kCj7k,1929
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=Xu-8xGAOGqSYLGPOib0Wn-blmyQBHl3MrAOza_w__Y8,4456
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=1cE06kEqU_fNXWIo_3Hea88BOwL37NmfY5nnQnYEYqY,4336
14
- htmy/renderer/default.py,sha256=kFfgWPF7I3h7EmIDZamVeDNvI6ngnxfLIAnMUsWiMas,10917
15
- htmy/snippet.py,sha256=9-ltfs4rugv6kFNzAY0PYNP3YI3xvJG8lkxEnYcCYWw,10538
16
- htmy/typing.py,sha256=kRi_rkQQ5CNL1TyUhEVHjTh96zHpBZlNxqr2TTTfyzI,3546
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.8.2.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
19
- htmy-0.8.2.dist-info/METADATA,sha256=sro_ZXPlLpQbnZmDfQ-RhRwce2Gg77nrZQhNAJTUUc4,20123
20
- htmy-0.8.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
21
- htmy-0.8.2.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Peter Volf
3
+ Copyright (c) 2025 Peter Volf
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal