htmy 0.7.0__tar.gz → 0.9.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.
@@ -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
@@ -1,3 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: htmy
3
+ Version: 0.9.0
4
+ Summary: Async, pure-Python server-side rendering engine.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: Peter Volf
8
+ Author-email: do.volfp@gmail.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Provides-Extra: lxml
18
+ Requires-Dist: anyio (>=4.6.2.post1,<5.0.0)
19
+ Requires-Dist: async-lru (>=2.0.4,<3.0.0)
20
+ Requires-Dist: lxml (>=6.0.0) ; extra == "lxml"
21
+ Requires-Dist: markdown (>=3.8,<4.0)
22
+ Description-Content-Type: text/markdown
23
+
1
24
  ![Tests](https://github.com/volfpeter/htmy/actions/workflows/tests.yml/badge.svg)
2
25
  ![Linters](https://github.com/volfpeter/htmy/actions/workflows/linters.yml/badge.svg)
3
26
  ![Documentation](https://github.com/volfpeter/htmy/actions/workflows/build-docs.yml/badge.svg)
@@ -9,13 +32,13 @@
9
32
 
10
33
  # `htmy`
11
34
 
12
- **Async**, **pure-Python** rendering engine.
35
+ **Async**, **pure-Python** server-side rendering engine.
13
36
 
14
37
  Unleash your creativity with the full power and Python, without the hassle of learning a new templating language or dealing with its limitations!
15
38
 
16
39
  ## Key features
17
40
 
18
- - **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.
19
42
  - **Powerful**, React-like **context support**, so you can avoid prop-drilling.
20
43
  - Sync and async **function components** with **decorator syntax**.
21
44
  - All baseline **HTML** tags built-in.
@@ -26,8 +49,23 @@ Unleash your creativity with the full power and Python, without the hassle of le
26
49
  - **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
27
50
  - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
28
51
  - Automatic and customizable **property-name conversion** from snake case to kebab case.
52
+ - **Compatible** with any other templating library through wrappers.
29
53
  - **Fully-typed**.
30
54
 
55
+ ## Testimonials
56
+
57
+ "Thank you for your work on `fasthx`, as well as `htmy`! I've never had an easier time developing with another stack." ([ref](https://github.com/volfpeter/fasthx/discussions/77))
58
+
59
+ "One of the main parts of the `FastAPI` -> `fasthx` -> `htmy` integration I'm falling in love with is its explicitness, and not too much magic happening." ([ref](https://github.com/volfpeter/fasthx/issues/54))
60
+
61
+ "Thank you for your work on `htmy` and `fasthx`, both have been very pleasant to use, and the APIs are both intuitive and simple. Great work." ([ref](https://github.com/volfpeter/fasthx/issues/54))
62
+
63
+ "I love that the language-embedded HTML generation library approach is becoming more popular." ([ref](https://www.reddit.com/r/programming/comments/1h1a0dx/comment/lzd3phw))
64
+
65
+ "Neat approach and it naturally solves the partial templates problem 👍" ([ref](https://www.reddit.com/r/Python/comments/1gp3mww/comment/lwqj4fc))
66
+
67
+ "Great API design!" ([ref](https://www.reddit.com/r/Python/comments/1gp3mww/comment/lwpdyq9))
68
+
31
69
  ## Support
32
70
 
33
71
  Consider supporting the development and maintenance of the project through [sponsoring](https://buymeacoffee.com/volfpeter), or reach out for [consulting](https://www.volfp.com/contact?subject=Consulting%20-%20HTMY) so you can get the most out of the library.
@@ -40,6 +78,10 @@ The package is available on PyPI and can be installed with:
40
78
  $ pip install htmy
41
79
  ```
42
80
 
81
+ The package has the following optional dependencies:
82
+
83
+ - `lxml` *(recommended)*: When installed, it is prioritized over `xml.etree.ElementTree` and provides more secure, faster, and more flexible HTML and XML processing. It is used, for example, for Markdown processing. Install with: `pip install "htmy[lxml]"`.
84
+
43
85
  ## Concepts
44
86
 
45
87
  The entire library -- from the rendering engine itself to the built-in components -- is built around a few simple protocols and a handful of simple utility classes. This means that you can easily customize, extend, or replace basically everything in the library. Yes, even the rendering engine. The remaining parts will keep working as expected.
@@ -97,7 +139,7 @@ user_table = html.table(
97
139
  )
98
140
  ```
99
141
 
100
- `htmy` also provides a `@component` decorator that can be used on sync or async `my_component(props: MyProps, context: Context) -> Component` functions to convert them into components (preserving the `props` typing).
142
+ `htmy` also provides a powerful `@component` decorator that can be used on sync or async `my_component(props: MyProps, context: Context) -> Component` functions and methods to convert them into components (preserving the `props` typing). You can find out more about this feature in the [Function components](https://volfpeter.github.io/htmy/function-components/) guide.
101
143
 
102
144
  Here is the same example as above, but with function components:
103
145
 
@@ -266,6 +308,7 @@ These are default tag attribute formatting rules:
266
308
  - `bool` attribute values are converted to strings (`"true"` and `"false"`).
267
309
  - `XBool.true` attributes values are converted to an empty string, and `XBool.false` values are skipped (only the attribute name is rendered).
268
310
  - `date` and `datetime` attribute values are converted to ISO strings.
311
+ - Complex values such as lists, dictionaries, tuples, and sets are JSON serialized.
269
312
 
270
313
  ### Error boundary
271
314
 
@@ -283,11 +326,39 @@ If a component executes a potentially "long-running" synchronous call, it is str
283
326
 
284
327
  In all other cases, it's best to use sync components.
285
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
+
286
352
  ## Framework integrations
287
353
 
288
354
  FastAPI:
289
355
 
290
- - [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.
358
+
359
+ ## External examples
360
+
361
+ - [lipsum-chat](https://github.com/volfpeter/lipsum-chat): A simple chat application using `FastAPI`, `htmx`, and `fasthx`.
291
362
 
292
363
  ## Why
293
364
 
@@ -328,3 +399,4 @@ We welcome contributions from the community to help improve the project! Whether
328
399
  ## License - MIT
329
400
 
330
401
  The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
402
+
@@ -1,22 +1,3 @@
1
- Metadata-Version: 2.3
2
- Name: htmy
3
- Version: 0.7.0
4
- Summary: Async, pure-Python rendering engine.
5
- License: MIT
6
- Author: Peter Volf
7
- Author-email: do.volfp@gmail.com
8
- Requires-Python: >=3.10,<4.0
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: anyio (>=4.6.2.post1,<5.0.0)
16
- Requires-Dist: async-lru (>=2.0.4,<3.0.0)
17
- Requires-Dist: markdown (>=3.7,<4.0)
18
- Description-Content-Type: text/markdown
19
-
20
1
  ![Tests](https://github.com/volfpeter/htmy/actions/workflows/tests.yml/badge.svg)
21
2
  ![Linters](https://github.com/volfpeter/htmy/actions/workflows/linters.yml/badge.svg)
22
3
  ![Documentation](https://github.com/volfpeter/htmy/actions/workflows/build-docs.yml/badge.svg)
@@ -28,13 +9,13 @@ Description-Content-Type: text/markdown
28
9
 
29
10
  # `htmy`
30
11
 
31
- **Async**, **pure-Python** rendering engine.
12
+ **Async**, **pure-Python** server-side rendering engine.
32
13
 
33
14
  Unleash your creativity with the full power and Python, without the hassle of learning a new templating language or dealing with its limitations!
34
15
 
35
16
  ## Key features
36
17
 
37
- - **Async**-first, to let you make the best use of [modern async tools](https://github.com/timofurrer/awesome-asyncio).
18
+ - **Async**-first, to let you make the best use of modern async tools.
38
19
  - **Powerful**, React-like **context support**, so you can avoid prop-drilling.
39
20
  - Sync and async **function components** with **decorator syntax**.
40
21
  - All baseline **HTML** tags built-in.
@@ -45,8 +26,23 @@ Unleash your creativity with the full power and Python, without the hassle of le
45
26
  - **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
46
27
  - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
47
28
  - Automatic and customizable **property-name conversion** from snake case to kebab case.
29
+ - **Compatible** with any other templating library through wrappers.
48
30
  - **Fully-typed**.
49
31
 
32
+ ## Testimonials
33
+
34
+ "Thank you for your work on `fasthx`, as well as `htmy`! I've never had an easier time developing with another stack." ([ref](https://github.com/volfpeter/fasthx/discussions/77))
35
+
36
+ "One of the main parts of the `FastAPI` -> `fasthx` -> `htmy` integration I'm falling in love with is its explicitness, and not too much magic happening." ([ref](https://github.com/volfpeter/fasthx/issues/54))
37
+
38
+ "Thank you for your work on `htmy` and `fasthx`, both have been very pleasant to use, and the APIs are both intuitive and simple. Great work." ([ref](https://github.com/volfpeter/fasthx/issues/54))
39
+
40
+ "I love that the language-embedded HTML generation library approach is becoming more popular." ([ref](https://www.reddit.com/r/programming/comments/1h1a0dx/comment/lzd3phw))
41
+
42
+ "Neat approach and it naturally solves the partial templates problem 👍" ([ref](https://www.reddit.com/r/Python/comments/1gp3mww/comment/lwqj4fc))
43
+
44
+ "Great API design!" ([ref](https://www.reddit.com/r/Python/comments/1gp3mww/comment/lwpdyq9))
45
+
50
46
  ## Support
51
47
 
52
48
  Consider supporting the development and maintenance of the project through [sponsoring](https://buymeacoffee.com/volfpeter), or reach out for [consulting](https://www.volfp.com/contact?subject=Consulting%20-%20HTMY) so you can get the most out of the library.
@@ -59,6 +55,10 @@ The package is available on PyPI and can be installed with:
59
55
  $ pip install htmy
60
56
  ```
61
57
 
58
+ The package has the following optional dependencies:
59
+
60
+ - `lxml` *(recommended)*: When installed, it is prioritized over `xml.etree.ElementTree` and provides more secure, faster, and more flexible HTML and XML processing. It is used, for example, for Markdown processing. Install with: `pip install "htmy[lxml]"`.
61
+
62
62
  ## Concepts
63
63
 
64
64
  The entire library -- from the rendering engine itself to the built-in components -- is built around a few simple protocols and a handful of simple utility classes. This means that you can easily customize, extend, or replace basically everything in the library. Yes, even the rendering engine. The remaining parts will keep working as expected.
@@ -116,7 +116,7 @@ user_table = html.table(
116
116
  )
117
117
  ```
118
118
 
119
- `htmy` also provides a `@component` decorator that can be used on sync or async `my_component(props: MyProps, context: Context) -> Component` functions to convert them into components (preserving the `props` typing).
119
+ `htmy` also provides a powerful `@component` decorator that can be used on sync or async `my_component(props: MyProps, context: Context) -> Component` functions and methods to convert them into components (preserving the `props` typing). You can find out more about this feature in the [Function components](https://volfpeter.github.io/htmy/function-components/) guide.
120
120
 
121
121
  Here is the same example as above, but with function components:
122
122
 
@@ -285,6 +285,7 @@ These are default tag attribute formatting rules:
285
285
  - `bool` attribute values are converted to strings (`"true"` and `"false"`).
286
286
  - `XBool.true` attributes values are converted to an empty string, and `XBool.false` values are skipped (only the attribute name is rendered).
287
287
  - `date` and `datetime` attribute values are converted to ISO strings.
288
+ - Complex values such as lists, dictionaries, tuples, and sets are JSON serialized.
288
289
 
289
290
  ### Error boundary
290
291
 
@@ -302,11 +303,39 @@ If a component executes a potentially "long-running" synchronous call, it is str
302
303
 
303
304
  In all other cases, it's best to use sync components.
304
305
 
306
+ ## XSS prevention
307
+
308
+ `htmy` does XML/HTML escaping by default. This means user input is normally sanitized and rendered safely.
309
+
310
+ There are a couple of notable exceptions to this, where components by design allow XML/HTML inputs and assume they are safe:
311
+
312
+ - `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!
313
+ - `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!
314
+
315
+ ## AI assistance
316
+
317
+ The library is registered at [Context7](https://context7.com/volfpeter).
318
+
319
+ 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.
320
+
321
+ Because of the similarity with native HTML, JSX, and React, you can expect good results, both for vibe coding or inline completion.
322
+
323
+ ## Compatibility and performance
324
+
325
+ 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.
326
+
327
+ 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.
328
+
305
329
  ## Framework integrations
306
330
 
307
331
  FastAPI:
308
332
 
309
- - [FastHX](https://github.com/volfpeter/fasthx)
333
+ - [holm](https://github.com/volfpeter/holm): Web development framework that brings the Next.js developer experience to Python, built on FastAPI, htmy, and FastHX.
334
+ - [FastHX](https://github.com/volfpeter/fasthx): Declarative server-side rendering utility for FastAPI with built-in HTMX support.
335
+
336
+ ## External examples
337
+
338
+ - [lipsum-chat](https://github.com/volfpeter/lipsum-chat): A simple chat application using `FastAPI`, `htmx`, and `fasthx`.
310
339
 
311
340
  ## Why
312
341
 
@@ -347,4 +376,3 @@ We welcome contributions from the community to help improve the project! Whether
347
376
  ## License - MIT
348
377
 
349
378
  The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
350
-
@@ -30,12 +30,17 @@ from .typing import HTMYComponentType as HTMYComponentType
30
30
  from .typing import MutableContext as MutableContext
31
31
  from .typing import Properties as Properties
32
32
  from .typing import PropertyValue as PropertyValue
33
+ from .typing import RendererType as RendererType
34
+ from .typing import StrictComponentType as StrictComponentType
33
35
  from .typing import SyncComponent as SyncComponent
34
36
  from .typing import SyncContextProvider as SyncContextProvider
35
37
  from .utils import as_component_sequence as as_component_sequence
36
38
  from .utils import as_component_type as as_component_type
37
39
  from .utils import is_component_sequence as is_component_sequence
40
+ from .utils import join
38
41
  from .utils import join_components as join_components
39
42
 
43
+ join_classes = join
44
+
40
45
  HTMY = Renderer
41
46
  """Deprecated alias for `Renderer`."""
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import abc
4
4
  import enum
5
+ import json
5
6
  from collections.abc import Callable, Container
6
7
  from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
7
8
  from xml.sax.saxutils import escape as xml_escape
@@ -251,6 +252,9 @@ class Formatter(ContextAware):
251
252
  """
252
253
  The default, context-aware property name and value formatter.
253
254
 
255
+ The formatter supports both primitive and (many) complex values, such as lists,
256
+ dictionaries, tuples, and sets. Complex values are JSON-serialized by default.
257
+
254
258
  Important: the default implementation looks up the formatter for a given value by checking
255
259
  its type, but it doesn't do this check with the base classes of the encountered type. For
256
260
  example the formatter will know how to format `datetime` object, but it won't know how to
@@ -258,7 +262,7 @@ class Formatter(ContextAware):
258
262
 
259
263
  One reason for this is efficiency: always checking the base classes of every single value is a
260
264
  lot of unnecessary calculation. The other reason is customizability: this way you could use
261
- subclassing for fomatter selection, e.g. with `LocaleDatetime(datetime)`-like classes.
265
+ subclassing for formatter selection, e.g. with `LocaleDatetime(datetime)`-like classes.
262
266
 
263
267
  Property name and value formatters may raise a `SkipProperty` error if a property should be skipped.
264
268
  """
@@ -337,6 +341,10 @@ class Formatter(ContextAware):
337
341
  bool: lambda v: "true" if v else "false",
338
342
  date: lambda d: cast(date, d).isoformat(),
339
343
  datetime: lambda d: cast(datetime, d).isoformat(),
344
+ dict: lambda v: json.dumps(v),
345
+ list: lambda v: json.dumps(v),
346
+ tuple: lambda v: json.dumps(v),
347
+ set: lambda v: json.dumps(tuple(v)),
340
348
  XBool: lambda v: cast(XBool, v).format(),
341
349
  type(None): SkipProperty.format_property,
342
350
  }
@@ -1,23 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
- import xml.etree.ElementTree as ET
4
- from collections.abc import Callable, Generator
5
3
  from typing import TYPE_CHECKING, ClassVar
6
4
  from xml.sax.saxutils import unescape
7
5
 
8
- if TYPE_CHECKING:
9
- from collections.abc import Mapping
10
- from xml.etree.ElementTree import Element
11
-
12
- from htmy.typing import ComponentType, Properties
13
-
6
+ try:
7
+ from lxml.etree import _Element as Element
8
+ from lxml.etree import tostring as etree_to_string
9
+ from lxml.html import fragment_fromstring as etree_from_string
10
+ except ImportError:
11
+ from xml.etree.ElementTree import Element # type: ignore[assignment]
12
+ from xml.etree.ElementTree import fromstring as etree_from_string # type: ignore[assignment]
13
+ from xml.etree.ElementTree import tostring as etree_to_string # type: ignore[no-redef]
14
14
 
15
+ from htmy import ComponentType, Properties
15
16
  from htmy.core import Fragment, SafeStr, WildcardTag
16
17
 
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Callable, Generator, Mapping
20
+
17
21
 
18
22
  class ETreeConverter:
19
23
  """
20
24
  Utility for converting XML strings to custom components.
25
+
26
+ By default the converter uses the standard library's `xml.etree.ElementTree`
27
+ module for string to element tree, and element tree to string conversion,
28
+ but if `lxml` is installed, it will be used instead.
29
+
30
+ Installing `lxml` is recommended for better performance and additional features,
31
+ like performance and support for broken HTML fragments. **Important:** `lxml` is
32
+ far more lenient and flexible than the standard library, so having it installed is
33
+ not only a performance boost, but it may also slightly change the element conversion
34
+ behavior in certain edge-cases!
21
35
  """
22
36
 
23
37
  __slots__ = ("_rules",)
@@ -43,15 +57,15 @@ class ETreeConverter:
43
57
  return SafeStr(element)
44
58
 
45
59
  element = f"<{self._htmy_fragment}>{element}</{self._htmy_fragment}>"
46
- return self.convert_element(ET.fromstring(element)) # noqa: S314 # Only use from XML strings from a trusted source.
60
+ return self.convert_element(etree_from_string(element)) # noqa: S314 # Only use XML strings from a trusted source.
47
61
 
48
62
  def convert_element(self, element: Element) -> ComponentType:
49
63
  """Converts the given `Element` to a component."""
50
64
  rules = self._rules
51
65
  if len(rules) == 0:
52
- return SafeStr(ET.tostring(element))
66
+ return SafeStr(etree_to_string(element, encoding="unicode"))
53
67
 
54
- tag: str = element.tag
68
+ tag: str = element.tag # type: ignore[assignment]
55
69
  component = Fragment if tag == self._htmy_fragment else rules.get(tag)
56
70
  children = self._convert_children(element)
57
71
  properties = self._convert_properties(element)
@@ -285,7 +285,7 @@ class hr(TagWithProps):
285
285
  __slots__ = ()
286
286
 
287
287
 
288
- class iframe(TagWithProps):
288
+ class iframe(Tag):
289
289
  """
290
290
  `<iframe>` element.
291
291
 
@@ -709,6 +709,18 @@ class i(Tag):
709
709
  tag_config = _DefaultTagConfig.inline_children
710
710
 
711
711
 
712
+ class kbd(Tag):
713
+ """
714
+ `<kbd>` element.
715
+
716
+ See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd.
717
+ """
718
+
719
+ __slots__ = ()
720
+
721
+ tag_config = _DefaultTagConfig.inline_children
722
+
723
+
712
724
  class picture(Tag):
713
725
  """
714
726
  `<picture>` element.
@@ -112,7 +112,7 @@ class I18n(ContextAware):
112
112
  return result
113
113
 
114
114
 
115
- @alru_cache(8)
115
+ @alru_cache()
116
116
  async def load_translation_resource(path: Path) -> TranslationResource:
117
117
  """
118
118
  Loads the translation resource from the given path.
htmy-0.9.0/htmy/io.py ADDED
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from anyio import open_file as open_file
6
+
7
+ if TYPE_CHECKING:
8
+ from pathlib import Path
9
+
10
+
11
+ async def load_text_file(path: str | Path) -> str:
12
+ """Loads the text content from the given path."""
13
+ async with await open_file(path, "r") as f:
14
+ return await f.read()
@@ -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__ = (
@@ -44,6 +44,8 @@ class Renderer:
44
44
  """
45
45
  Renders the given component.
46
46
 
47
+ Implements `htmy.typing.RendererType`.
48
+
47
49
  Arguments:
48
50
  component: The component to render.
49
51
  context: An optional rendering context.
@@ -71,16 +73,18 @@ class Renderer:
71
73
  """
72
74
  if isinstance(component, str):
73
75
  return self._string_formatter(component)
76
+ elif component is None:
77
+ return ""
74
78
  elif isinstance(component, Iterable):
75
79
  rendered_children = await asyncio.gather(
76
- *(self._render_one(comp, context) for comp in component)
80
+ *(self._render_one(comp, context) for comp in component if comp is not None)
77
81
  )
78
82
 
79
- return "".join(rendered_children)
83
+ return "".join(child for child in rendered_children if child is not None)
80
84
  else:
81
- return await self._render_one(component, context)
85
+ return await self._render_one(component, context) or ""
82
86
 
83
- async def _render_one(self, component: ComponentType, context: Context) -> str:
87
+ async def _render_one(self, component: ComponentType, context: Context) -> str | None:
84
88
  """
85
89
  Renders a single component.
86
90
 
@@ -93,6 +97,8 @@ class Renderer:
93
97
  """
94
98
  if isinstance(component, str):
95
99
  return self._string_formatter(component)
100
+ elif component is None:
101
+ return None
96
102
  else:
97
103
  child_context: Context = context
98
104
  if hasattr(component, "htmy_context"): # isinstance() is too expensive.
@@ -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,25 +201,29 @@ 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:
212
- await asyncio.gather(*(process_async_node(n, ctx) for n, ctx in async_todos))
213
- async_todos.clear()
217
+ current_async_todos = async_todos
218
+ self._async_todos = async_todos = deque()
219
+ await asyncio.gather(*(process_async_node(n, ctx) for n, ctx in current_async_todos))
214
220
 
215
221
  if self._error_boundary_todos:
216
222
  await asyncio.gather(
217
223
  *(self._process_error_boundary(n, ctx) for n, ctx in self._error_boundary_todos)
218
224
  )
219
225
 
220
- 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]
221
227
 
222
228
 
223
229
  async def _render_component(
@@ -269,6 +275,8 @@ class Renderer:
269
275
  """
270
276
  Renders the given component.
271
277
 
278
+ Implements `htmy.typing.RendererType`.
279
+
272
280
  Arguments:
273
281
  component: The component to render.
274
282
  context: An optional rendering context.
@@ -1,9 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
  from collections.abc import Awaitable, Iterator, Mapping
3
- from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from async_lru import alru_cache
4
8
 
5
9
  from .core import SafeStr, Text
6
- from .io import open_file
10
+ from .io import load_text_file
7
11
  from .typing import (
8
12
  Component,
9
13
  ComponentType,
@@ -13,6 +17,9 @@ from .typing import (
13
17
  )
14
18
  from .utils import as_component_sequence, as_component_type, is_component_sequence
15
19
 
20
+ if TYPE_CHECKING:
21
+ from pathlib import Path
22
+
16
23
  # -- Components and utilities
17
24
 
18
25
 
@@ -151,6 +158,10 @@ class Snippet:
151
158
  """
152
159
  Component that renders text, which may be asynchronously loaded from a file.
153
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
+
154
165
  The entire snippet processing pipeline consists of the following steps:
155
166
 
156
167
  1. The text content is loaded from a file or passed directly as a `Text` instance.
@@ -248,8 +259,7 @@ class Snippet:
248
259
  if isinstance(path_or_text, Text):
249
260
  return path_or_text
250
261
  else:
251
- async with await open_file(path_or_text, "r") as f:
252
- return await f.read()
262
+ return await Snippet._load_text_file(path_or_text)
253
263
 
254
264
  def _render_text(self, text: str, context: Context) -> Component:
255
265
  """
@@ -257,3 +267,9 @@ class Snippet:
257
267
  and returns the corresponding component.
258
268
  """
259
269
  return SafeStr(text)
270
+
271
+ @staticmethod
272
+ @alru_cache()
273
+ async def _load_text_file(path: str | Path) -> str:
274
+ """Async text loader with an LRU cache."""
275
+ return await load_text_file(path)
@@ -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.
@@ -65,6 +68,27 @@ ComponentSequence: TypeAlias = list[ComponentType] | tuple[ComponentType, ...]
65
68
  Component: TypeAlias = ComponentType | ComponentSequence
66
69
  """Component type: a single component or a sequence of components."""
67
70
 
71
+
72
+ # -- Renderer
73
+
74
+
75
+ class RendererType(Protocol):
76
+ """Protocol definition for `htmy` renderers."""
77
+
78
+ async def render(self, component: Component, context: Context | None = None) -> str:
79
+ """
80
+ Renders the given component.
81
+
82
+ Arguments:
83
+ component: The component to render.
84
+ context: An optional rendering context.
85
+
86
+ Returns:
87
+ The rendered string.
88
+ """
89
+ ...
90
+
91
+
68
92
  # -- Context providers
69
93
 
70
94
 
@@ -0,0 +1,87 @@
1
+ [tool.poetry]
2
+ name = "htmy"
3
+ version = "0.9.0"
4
+ description = "Async, pure-Python server-side rendering engine."
5
+ authors = ["Peter Volf <do.volfp@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ anyio = "^4.6.2.post1"
12
+ async-lru = "^2.0.4"
13
+ markdown = "^3.8"
14
+ lxml = { version = ">=6.0.0", optional = true }
15
+
16
+ [tool.poetry.extras]
17
+ lxml = ["lxml"]
18
+
19
+ [tool.poetry.group.dev.dependencies]
20
+ fastapi = "^0.116.0"
21
+ fasthx = "^2.3.3"
22
+ mkdocs-material = "^9.6.16"
23
+ mkdocstrings = { extras = ["python"], version = "^0.30.0" }
24
+ mypy = "^1.18.2"
25
+ poethepoet = "^0.37.0"
26
+ pytest = "^8.3.3"
27
+ pytest-asyncio = "^0.24.0"
28
+ pytest-random-order = "^1.1.1"
29
+ ruff = "^0.14.4"
30
+ types-markdown = "^3.8.0.20250809"
31
+ typing-extensions = "^4.12.2"
32
+ types-lxml = "^2025.3.30"
33
+
34
+ [build-system]
35
+ requires = ["poetry-core"]
36
+ build-backend = "poetry.core.masonry.api"
37
+
38
+ [tool.mypy]
39
+ strict = true
40
+ show_error_codes = true
41
+
42
+ [tool.pyright]
43
+ venvPath = "."
44
+ venv = ".venv"
45
+
46
+ [tool.pytest.ini_options]
47
+ addopts = "--random-order"
48
+
49
+ [tool.ruff]
50
+ line-length = 108
51
+ exclude = [
52
+ ".git",
53
+ ".mypy_cache",
54
+ ".pytest_cache",
55
+ ".ruff_cache",
56
+ ".venv",
57
+ "dist",
58
+ "docs",
59
+ ]
60
+ lint.select = [
61
+ "B", # flake8-bugbear
62
+ "C", # flake8-comprehensions
63
+ "E", # pycodestyle errors
64
+ "F", # pyflakes
65
+ "I", # isort
66
+ "S", # flake8-bandit - we must ignore these rules in tests
67
+ "W", # pycodestyle warnings
68
+ ]
69
+
70
+ [tool.ruff.lint.per-file-ignores]
71
+ "tests/**/*" = ["S101"] # S101: use of assert detected
72
+
73
+ [tool.poe.tasks]
74
+ format = "ruff format --check ."
75
+ format-fix = "ruff format ."
76
+
77
+ lint = "ruff check ."
78
+ lint-fix = "ruff . --fix"
79
+
80
+ mypy = "mypy ."
81
+
82
+ check.sequence = ["format", "lint", "mypy"]
83
+ check.ignore_fail = "return_non_zero"
84
+
85
+ test = "python -m pytest tests"
86
+
87
+ serve-docs = "mkdocs serve"
htmy-0.7.0/htmy/io.py DELETED
@@ -1 +0,0 @@
1
- from anyio import open_file as open_file
htmy-0.7.0/pyproject.toml DELETED
@@ -1,73 +0,0 @@
1
- [tool.poetry]
2
- name = "htmy"
3
- version = "0.7.0"
4
- description = "Async, pure-Python rendering engine."
5
- authors = ["Peter Volf <do.volfp@gmail.com>"]
6
- license = "MIT"
7
- readme = "README.md"
8
-
9
- [tool.poetry.dependencies]
10
- python = "^3.10"
11
- anyio = "^4.6.2.post1"
12
- async-lru = "^2.0.4"
13
- markdown = "^3.7"
14
-
15
- [tool.poetry.group.dev.dependencies]
16
- mkdocs-material = "^9.5.39"
17
- mkdocstrings = {extras = ["python"], version = "^0.26.1"}
18
- mypy = "^1.15.0"
19
- poethepoet = "^0.29.0"
20
- pytest = "^8.3.3"
21
- pytest-asyncio = "^0.24.0"
22
- pytest-random-order = "^1.1.1"
23
- ruff = "^0.9.0"
24
- types-markdown = "^3.7.0.20240822"
25
- typing-extensions = "^4.12.2"
26
- fastapi = "^0.115.8"
27
- fasthx = "^2.2.1"
28
-
29
- [build-system]
30
- requires = ["poetry-core"]
31
- build-backend = "poetry.core.masonry.api"
32
-
33
- [tool.mypy]
34
- strict = true
35
- show_error_codes = true
36
-
37
- [tool.pytest.ini_options]
38
- addopts = "--random-order"
39
-
40
- [tool.ruff]
41
- line-length = 108
42
- exclude = [
43
- ".git",
44
- ".mypy_cache",
45
- ".pytest_cache",
46
- ".ruff_cache",
47
- ".venv",
48
- "dist",
49
- "docs",
50
- ]
51
- lint.select = [
52
- "B", # flake8-bugbear
53
- "C", # flake8-comprehensions
54
- "E", # pycodestyle errors
55
- "F", # pyflakes
56
- "I", # isort
57
- "S", # flake8-bandit - we must ignore these rules in tests
58
- "W", # pycodestyle warnings
59
- ]
60
-
61
- [tool.ruff.lint.per-file-ignores]
62
- "tests/**/*" = ["S101"] # S101: use of assert detected
63
-
64
- [tool.poe.tasks]
65
- check-format = "ruff format --check ."
66
- format = "ruff format ."
67
- lint = "ruff check ."
68
- lint-fix = "ruff . --fix"
69
- mypy = "mypy ."
70
- test = "python -m pytest tests"
71
- static-checks.sequence = ["lint", "check-format", "mypy"]
72
- static-checks.ignore_fail = "return_non_zero"
73
- serve-docs = "mkdocs serve"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes