htmy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of htmy might be problematic. Click here for more details.

htmy-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Peter Volf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
htmy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.1
2
+ Name: htmy
3
+ Version: 0.1.0
4
+ Summary: Async, zero-dependency, pure-Python rendering engine.
5
+ License: MIT
6
+ Author: Peter Volf
7
+ Author-email: do.volfp@gmail.com
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Description-Content-Type: text/markdown
14
+
15
+ ![Tests](https://github.com/volfpeter/htmy/actions/workflows/tests.yml/badge.svg)
16
+ ![Linters](https://github.com/volfpeter/htmy/actions/workflows/linters.yml/badge.svg)
17
+ ![Documentation](https://github.com/volfpeter/htmy/actions/workflows/build-docs.yml/badge.svg)
18
+ ![PyPI package](https://img.shields.io/pypi/v/htmy?color=%2334D058&label=PyPI%20Package)
19
+
20
+ **Source code**: [https://github.com/volfpeter/htmy](https://github.com/volfpeter/htmy)
21
+
22
+ **Documentation and examples**: [https://volfpeter.github.io/htmy](https://volfpeter.github.io/htmy/)
23
+
24
+ # `htmy`
25
+
26
+ **Async**, **zero-dependency**, **pure-Python** rendering engine.
27
+
28
+ ## Key features
29
+
30
+ - **Async**-first, to let you make the best use of [modern async tools](https://github.com/timofurrer/awesome-asyncio).
31
+ - **Powerful**, React-like **context support**, so you can avoid prop-drilling.
32
+ - Sync and async **function components** with **decorator syntax**.
33
+ - All baseline **HTML** tags built-in.
34
+ - Built-in, easy to use `ErrorBoundary` component for graceful error handling.
35
+ - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
36
+ - Automatic and customizable **property-name conversion** from snake case to kebab case.
37
+ - **Fully-typed**.
38
+
39
+ ## Installation
40
+
41
+ The package is available on PyPI and can be installed with:
42
+
43
+ ```console
44
+ $ pip install htmy
45
+ ```
46
+
47
+ ## Concepts
48
+
49
+ 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.
50
+
51
+ Also, the library doesn't rely on advanced Python features such as metaclasses or descriptors. There are also no complex base classes and the like. Even a junior engineer could understand, develop, and debug an application that's built with `htmy`.
52
+
53
+ ### Components
54
+
55
+ Every class with a sync or async `htmy(context: Context) -> Component` method is an `htmy` component (technically an `HTMYComponentType`). Strings are also components, as well as lists or tuples of `HTMYComponentType` or string objects.
56
+
57
+ Using this method name enables the conversion of any of your business objects (from `TypedDicts`s or `pydantic` models to ORM classes) into components without the fear of name collision with other tools.
58
+
59
+ Async support makes it possible to load data or execute async business logic right in your components. This can reduce the amount of boilerplate you need to write in some cases, and also gives you the freedom to split the rendering and non-rendering logic in any way you see fit.
60
+
61
+ Example:
62
+
63
+ ```python
64
+ from dataclasses import dataclass
65
+
66
+ from htmy import Component, Context, html
67
+
68
+ @dataclass(frozen=True, kw_only=True, slots=True)
69
+ class User:
70
+ username: str
71
+ name: str
72
+ email: str
73
+
74
+ async def is_admin(self) -> bool:
75
+ return False
76
+
77
+ class UserRow(User):
78
+ async def htmy(self, context: Context) -> Component:
79
+ role = "admin" if await self.is_admin() else "restricted"
80
+ return html.tr(
81
+ html.td(self.username),
82
+ html.td(self.name),
83
+ html.td(html.a(self.email, href=f"mailto:{self.email}")),
84
+ html.td(role)
85
+ )
86
+
87
+ @dataclass(frozen=True, kw_only=True, slots=True)
88
+ class UserRows:
89
+ users: list[User]
90
+ def htmy(self, context: Context) -> Component:
91
+ # Note that a list is returned here. A list or tuple of `HTMYComponentType | str` objects is also a component.
92
+ return [UserRow(username=u.username, name=u.name, email=u.email) for u in self.users]
93
+
94
+ user_table = html.table(
95
+ UserRows(
96
+ users=[
97
+ User(username="Foo", name="Foo", email="foo@example.com"),
98
+ User(username="Bar", name="Bar", email="bar@example.com"),
99
+ ]
100
+ )
101
+ )
102
+ ```
103
+
104
+ `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).
105
+
106
+ Here is the same example as above, but with function components:
107
+
108
+ ```python
109
+ from dataclasses import dataclass
110
+
111
+ from htmy import Component, Context, component, html
112
+
113
+ @dataclass(frozen=True, kw_only=True, slots=True)
114
+ class User:
115
+ username: str
116
+ name: str
117
+ email: str
118
+
119
+ async def is_admin(self) -> bool:
120
+ return False
121
+
122
+ @component
123
+ async def user_row(user: User, context: Context) -> Component:
124
+ # The first argument of function components is their "props", the data they need.
125
+ # The second argument is the rendering context.
126
+ role = "admin" if await user.is_admin() else "restricted"
127
+ return html.tr(
128
+ html.td(user.username),
129
+ html.td(user.name),
130
+ html.td(html.a(user.email, href=f"mailto:{user.email}")),
131
+ html.td(role)
132
+ )
133
+
134
+ @component
135
+ def user_rows(users: list[User], context: Context) -> Component:
136
+ # Nothing to await in this component, so it's sync.
137
+ # Note that we only pass the "props" to the user_row() component (well, function component wrapper).
138
+ # The context will be passed to the wrapper during rendering.
139
+ return [user_row(user) for user in users]
140
+
141
+ user_table = html.table(
142
+ user_rows(
143
+ [
144
+ User(username="Foo", name="Foo", email="foo@example.com"),
145
+ User(username="Bar", name="Bar", email="bar@example.com"),
146
+ ]
147
+ )
148
+ )
149
+ ```
150
+
151
+ ### Built-in components
152
+
153
+ `htmy` has a rich set of built-in base and utility components for both HTML and other use-cases:
154
+
155
+ - `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
156
+ - `BaseTag`, `TagWithProps`, `StandaloneTag`, `Tag`: base classes for custom XML tags.
157
+ - `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
158
+
159
+ ### Rendering
160
+
161
+ `htmy.HTMY` is the built-in, default renderer of the library.
162
+
163
+ 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 HTMY().render(my_root_component)`.
164
+
165
+ 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()`:
166
+
167
+ ```python
168
+ import asyncio
169
+
170
+ from htmy import HTMY, html
171
+
172
+ async def render_page() -> None:
173
+ page = (
174
+ html.DOCTYPE.html,
175
+ html.html(
176
+ html.body(
177
+ html.h1("Hello World!"),
178
+ html.p("This page was rendered by ", html.code("htmy")),
179
+ ),
180
+ )
181
+ )
182
+
183
+ result = await HTMY().render(page)
184
+ print(result)
185
+
186
+
187
+ if __name__ == "__main__":
188
+ asyncio.run(render_page())
189
+ ```
190
+
191
+ ### Context
192
+
193
+ As you could see from the code examples above, every component has a `context: Context` argument, which we haven't used so far. Context is a way to share data with the entire subtree of a component without "prop drilling".
194
+
195
+ The context (technically a `Mapping`) is entirely managed by the renderer. Context provider components (any class with a sync or async `htmy_context() -> Context` method) add new data to the context to make it available to components in their subtree, and components can simply take what they need from the context.
196
+
197
+ There is no restriction on what can be in the context, it can be used for anything the application needs, for example making the current user, UI preferences, themes, or formatters available to components. In fact, built-in components get their `Formatter` from the context if it contains one, to make it possible to customize tag property name and value formatting.
198
+
199
+ Here's an example context provider and consumer implementation:
200
+
201
+ ```python
202
+ import asyncio
203
+
204
+ from htmy import HTMY, Component, ComponentType, Context, component, html
205
+
206
+ class UserContext:
207
+ def __init__(self, *children: ComponentType, username: str, theme: str) -> None:
208
+ self._children = children
209
+ self.username = username
210
+ self.theme = theme
211
+
212
+ def htmy_context(self) -> Context:
213
+ # Context provider implementation.
214
+ return {UserContext: self}
215
+
216
+ def htmy(self, context: Context) -> Component:
217
+ # Context providers must also be components, as they just
218
+ # wrap some children components in their context.
219
+ return self._children
220
+
221
+ @classmethod
222
+ def from_context(cls, context: Context) -> "UserContext":
223
+ user_context = context[cls]
224
+ if isinstance(user_context, UserContext):
225
+ return user_context
226
+
227
+ raise TypeError("Invalid user context.")
228
+
229
+ @component
230
+ def welcome_page(text: str, context: Context) -> Component:
231
+ # Get user information from the context.
232
+ user = UserContext.from_context(context)
233
+ return (
234
+ html.DOCTYPE.html,
235
+ html.html(
236
+ html.body(
237
+ html.h1(text, html.strong(user.username)),
238
+ data_theme=user.theme,
239
+ ),
240
+ ),
241
+ )
242
+
243
+ async def render_welcome_page() -> None:
244
+ page = UserContext(
245
+ welcome_page("Welcome back "),
246
+ username="John",
247
+ theme="dark",
248
+ )
249
+
250
+ result = await HTMY().render(page)
251
+ print(result)
252
+
253
+ if __name__ == "__main__":
254
+ asyncio.run(render_welcome_page())
255
+ ```
256
+
257
+ You can of course rely on the built-in context related utilities like the `ContextAware` or `WithContext` classes for convenient and typed context use with less boilerplate code.
258
+
259
+ ### Formatter
260
+
261
+ 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 `HTMY` or `HTMY.render()`, or in a context provider component.
262
+
263
+ These are default tag attribute formatting rules:
264
+
265
+ - Underscores are converted to dashes in attribute names (`_` -> `-`) unless the attribute name starts or ends with an underscore, in which case leading and trailing underscores are removed and the rest of attribute name is preserved. For example `data_theme="dark"` is converted to `data-theme="dark"`, but `_data_theme="dark"` will end up as `data_theme="dark"` in the rendered text. More importantly `class_="text-danger"`, `_class="text-danger"`, `_class__="text-danger"` are all converted to `class="text-danger"`, and `_for="my-input"` or `for_="my_input"` will become `for="my-input"`.
266
+ - `bool` attribute values are converted to strings (`"true"` and `"false"`).
267
+ - `XBool.true` attributes values are converted to an empty string, and `XBool.false` values are skipped (only the attribute name is rendered).
268
+ - `date` and `datetime` attribute values are converted to ISO strings.
269
+
270
+ ### Error boundary
271
+
272
+ The `ErrorBoundary` component is useful if you want your application to fail gracefully (e.g. display an error message) instead of raising an HTTP error.
273
+
274
+ The error boundary wraps a component component subtree. When the renderer encounters an `ErrorBoundary` component, it will try to render its wrapped content. If rendering fails with an exception at any point in the `ErrorBoundary`'s subtree, the renderer will automatically fall back to the component you assigned to the `ErrorBoundary`'s `fallback` property.
275
+
276
+ Optionally, you can define which errors an error boundary can handle, giving you fine control over error handling.
277
+
278
+ ### Sync or async?
279
+
280
+ In general, a component should be async if it must await some async call inside.
281
+
282
+ If a component executes a potentially "long-running" synchronous call, it is strongly recommended to delegate that call to a worker thread an await it (thus making the component async). This can be done for example with `anyio`'s `to_thread` [utility](https://anyio.readthedocs.io/en/stable/threads.html), `starlette`'s (or `fastapi`'s) `run_in_threadpool()`, and so on. The goal here is to avoid blocking the asyncio event loop, as that can lead to performance issues.
283
+
284
+ In all other cases, it's best to use sync components.
285
+
286
+ ## Why
287
+
288
+ At one end of the spectrum, there are the complete application frameworks that combine the server (Python) and client (JavaScript) applications with the entire state management and synchronization into a single Python (an in some cases an additional JavaScript) package. Some of the most popular examples are: [Reflex](https://github.com/reflex-dev/reflex), [NiceGUI](https://github.com/zauberzeug/nicegui/), [ReactPy](https://github.com/reactive-python/reactpy), and [FastUI](https://github.com/pydantic/FastUI).
289
+
290
+ The main benefit of these frameworks is rapid application prototyping and a very convenient developer experience (at least as long as you stay within the built-in feature set of the framework). In exchange for that, they are very opinionated (from components to frontend tooling and state management), the underlying engineering is very complex, deployment and scaling can be hard or costly, and they can be hard to migrate away from. Even with these caveats, they can be a very good choice for internal tools and application prototyping.
291
+
292
+ The other end of spectrum -- plain rendering engines -- is dominated by the [Jinja](https://jinja.palletsprojects.com) templating engine, which is a safe choice as it has been and will be around for a long time. The main drawbacks with Jinja are the lack of good IDE support, the complete lack of static code analysis support, and the (subjectively) ugly syntax.
293
+
294
+ Then there are tools that aim for the middleground, usually by providing most of the benefits and drawbacks of complete application frameworks while leaving state management, client-server communication, and dynamic UI updates for the user to solve, often with some level of [HTMX](https://htmx.org/) support. This group includes libraries like [FastHTML](https://github.com/answerdotai/fasthtml) and [Ludic](https://github.com/getludic/ludic).
295
+
296
+ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, which is as **simple**, **maintainable**, and **customizable** as possible, while still providing all the building blocks for (conveniently) creating complex and maintainable applications.
297
+
298
+ ## Dependencies
299
+
300
+ The library has **no dependencies**.
301
+
302
+ ## Development
303
+
304
+ Use `ruff` for linting and formatting, `mypy` for static code analysis, and `pytest` for testing.
305
+
306
+ The documentation is built with `mkdocs-material` and `mkdocstrings`.
307
+
308
+ ## Contributing
309
+
310
+ All contributions are welcome, including more documentation, examples, code, and tests. Even questions.
311
+
312
+ ## License - MIT
313
+
314
+ The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
315
+
htmy-0.1.0/README.md ADDED
@@ -0,0 +1,300 @@
1
+ ![Tests](https://github.com/volfpeter/htmy/actions/workflows/tests.yml/badge.svg)
2
+ ![Linters](https://github.com/volfpeter/htmy/actions/workflows/linters.yml/badge.svg)
3
+ ![Documentation](https://github.com/volfpeter/htmy/actions/workflows/build-docs.yml/badge.svg)
4
+ ![PyPI package](https://img.shields.io/pypi/v/htmy?color=%2334D058&label=PyPI%20Package)
5
+
6
+ **Source code**: [https://github.com/volfpeter/htmy](https://github.com/volfpeter/htmy)
7
+
8
+ **Documentation and examples**: [https://volfpeter.github.io/htmy](https://volfpeter.github.io/htmy/)
9
+
10
+ # `htmy`
11
+
12
+ **Async**, **zero-dependency**, **pure-Python** rendering engine.
13
+
14
+ ## Key features
15
+
16
+ - **Async**-first, to let you make the best use of [modern async tools](https://github.com/timofurrer/awesome-asyncio).
17
+ - **Powerful**, React-like **context support**, so you can avoid prop-drilling.
18
+ - Sync and async **function components** with **decorator syntax**.
19
+ - All baseline **HTML** tags built-in.
20
+ - Built-in, easy to use `ErrorBoundary` component for graceful error handling.
21
+ - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
22
+ - Automatic and customizable **property-name conversion** from snake case to kebab case.
23
+ - **Fully-typed**.
24
+
25
+ ## Installation
26
+
27
+ The package is available on PyPI and can be installed with:
28
+
29
+ ```console
30
+ $ pip install htmy
31
+ ```
32
+
33
+ ## Concepts
34
+
35
+ 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.
36
+
37
+ Also, the library doesn't rely on advanced Python features such as metaclasses or descriptors. There are also no complex base classes and the like. Even a junior engineer could understand, develop, and debug an application that's built with `htmy`.
38
+
39
+ ### Components
40
+
41
+ Every class with a sync or async `htmy(context: Context) -> Component` method is an `htmy` component (technically an `HTMYComponentType`). Strings are also components, as well as lists or tuples of `HTMYComponentType` or string objects.
42
+
43
+ Using this method name enables the conversion of any of your business objects (from `TypedDicts`s or `pydantic` models to ORM classes) into components without the fear of name collision with other tools.
44
+
45
+ Async support makes it possible to load data or execute async business logic right in your components. This can reduce the amount of boilerplate you need to write in some cases, and also gives you the freedom to split the rendering and non-rendering logic in any way you see fit.
46
+
47
+ Example:
48
+
49
+ ```python
50
+ from dataclasses import dataclass
51
+
52
+ from htmy import Component, Context, html
53
+
54
+ @dataclass(frozen=True, kw_only=True, slots=True)
55
+ class User:
56
+ username: str
57
+ name: str
58
+ email: str
59
+
60
+ async def is_admin(self) -> bool:
61
+ return False
62
+
63
+ class UserRow(User):
64
+ async def htmy(self, context: Context) -> Component:
65
+ role = "admin" if await self.is_admin() else "restricted"
66
+ return html.tr(
67
+ html.td(self.username),
68
+ html.td(self.name),
69
+ html.td(html.a(self.email, href=f"mailto:{self.email}")),
70
+ html.td(role)
71
+ )
72
+
73
+ @dataclass(frozen=True, kw_only=True, slots=True)
74
+ class UserRows:
75
+ users: list[User]
76
+ def htmy(self, context: Context) -> Component:
77
+ # Note that a list is returned here. A list or tuple of `HTMYComponentType | str` objects is also a component.
78
+ return [UserRow(username=u.username, name=u.name, email=u.email) for u in self.users]
79
+
80
+ user_table = html.table(
81
+ UserRows(
82
+ users=[
83
+ User(username="Foo", name="Foo", email="foo@example.com"),
84
+ User(username="Bar", name="Bar", email="bar@example.com"),
85
+ ]
86
+ )
87
+ )
88
+ ```
89
+
90
+ `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).
91
+
92
+ Here is the same example as above, but with function components:
93
+
94
+ ```python
95
+ from dataclasses import dataclass
96
+
97
+ from htmy import Component, Context, component, html
98
+
99
+ @dataclass(frozen=True, kw_only=True, slots=True)
100
+ class User:
101
+ username: str
102
+ name: str
103
+ email: str
104
+
105
+ async def is_admin(self) -> bool:
106
+ return False
107
+
108
+ @component
109
+ async def user_row(user: User, context: Context) -> Component:
110
+ # The first argument of function components is their "props", the data they need.
111
+ # The second argument is the rendering context.
112
+ role = "admin" if await user.is_admin() else "restricted"
113
+ return html.tr(
114
+ html.td(user.username),
115
+ html.td(user.name),
116
+ html.td(html.a(user.email, href=f"mailto:{user.email}")),
117
+ html.td(role)
118
+ )
119
+
120
+ @component
121
+ def user_rows(users: list[User], context: Context) -> Component:
122
+ # Nothing to await in this component, so it's sync.
123
+ # Note that we only pass the "props" to the user_row() component (well, function component wrapper).
124
+ # The context will be passed to the wrapper during rendering.
125
+ return [user_row(user) for user in users]
126
+
127
+ user_table = html.table(
128
+ user_rows(
129
+ [
130
+ User(username="Foo", name="Foo", email="foo@example.com"),
131
+ User(username="Bar", name="Bar", email="bar@example.com"),
132
+ ]
133
+ )
134
+ )
135
+ ```
136
+
137
+ ### Built-in components
138
+
139
+ `htmy` has a rich set of built-in base and utility components for both HTML and other use-cases:
140
+
141
+ - `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
142
+ - `BaseTag`, `TagWithProps`, `StandaloneTag`, `Tag`: base classes for custom XML tags.
143
+ - `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
144
+
145
+ ### Rendering
146
+
147
+ `htmy.HTMY` is the built-in, default renderer of the library.
148
+
149
+ 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 HTMY().render(my_root_component)`.
150
+
151
+ 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()`:
152
+
153
+ ```python
154
+ import asyncio
155
+
156
+ from htmy import HTMY, html
157
+
158
+ async def render_page() -> None:
159
+ page = (
160
+ html.DOCTYPE.html,
161
+ html.html(
162
+ html.body(
163
+ html.h1("Hello World!"),
164
+ html.p("This page was rendered by ", html.code("htmy")),
165
+ ),
166
+ )
167
+ )
168
+
169
+ result = await HTMY().render(page)
170
+ print(result)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ asyncio.run(render_page())
175
+ ```
176
+
177
+ ### Context
178
+
179
+ As you could see from the code examples above, every component has a `context: Context` argument, which we haven't used so far. Context is a way to share data with the entire subtree of a component without "prop drilling".
180
+
181
+ The context (technically a `Mapping`) is entirely managed by the renderer. Context provider components (any class with a sync or async `htmy_context() -> Context` method) add new data to the context to make it available to components in their subtree, and components can simply take what they need from the context.
182
+
183
+ There is no restriction on what can be in the context, it can be used for anything the application needs, for example making the current user, UI preferences, themes, or formatters available to components. In fact, built-in components get their `Formatter` from the context if it contains one, to make it possible to customize tag property name and value formatting.
184
+
185
+ Here's an example context provider and consumer implementation:
186
+
187
+ ```python
188
+ import asyncio
189
+
190
+ from htmy import HTMY, Component, ComponentType, Context, component, html
191
+
192
+ class UserContext:
193
+ def __init__(self, *children: ComponentType, username: str, theme: str) -> None:
194
+ self._children = children
195
+ self.username = username
196
+ self.theme = theme
197
+
198
+ def htmy_context(self) -> Context:
199
+ # Context provider implementation.
200
+ return {UserContext: self}
201
+
202
+ def htmy(self, context: Context) -> Component:
203
+ # Context providers must also be components, as they just
204
+ # wrap some children components in their context.
205
+ return self._children
206
+
207
+ @classmethod
208
+ def from_context(cls, context: Context) -> "UserContext":
209
+ user_context = context[cls]
210
+ if isinstance(user_context, UserContext):
211
+ return user_context
212
+
213
+ raise TypeError("Invalid user context.")
214
+
215
+ @component
216
+ def welcome_page(text: str, context: Context) -> Component:
217
+ # Get user information from the context.
218
+ user = UserContext.from_context(context)
219
+ return (
220
+ html.DOCTYPE.html,
221
+ html.html(
222
+ html.body(
223
+ html.h1(text, html.strong(user.username)),
224
+ data_theme=user.theme,
225
+ ),
226
+ ),
227
+ )
228
+
229
+ async def render_welcome_page() -> None:
230
+ page = UserContext(
231
+ welcome_page("Welcome back "),
232
+ username="John",
233
+ theme="dark",
234
+ )
235
+
236
+ result = await HTMY().render(page)
237
+ print(result)
238
+
239
+ if __name__ == "__main__":
240
+ asyncio.run(render_welcome_page())
241
+ ```
242
+
243
+ You can of course rely on the built-in context related utilities like the `ContextAware` or `WithContext` classes for convenient and typed context use with less boilerplate code.
244
+
245
+ ### Formatter
246
+
247
+ 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 `HTMY` or `HTMY.render()`, or in a context provider component.
248
+
249
+ These are default tag attribute formatting rules:
250
+
251
+ - Underscores are converted to dashes in attribute names (`_` -> `-`) unless the attribute name starts or ends with an underscore, in which case leading and trailing underscores are removed and the rest of attribute name is preserved. For example `data_theme="dark"` is converted to `data-theme="dark"`, but `_data_theme="dark"` will end up as `data_theme="dark"` in the rendered text. More importantly `class_="text-danger"`, `_class="text-danger"`, `_class__="text-danger"` are all converted to `class="text-danger"`, and `_for="my-input"` or `for_="my_input"` will become `for="my-input"`.
252
+ - `bool` attribute values are converted to strings (`"true"` and `"false"`).
253
+ - `XBool.true` attributes values are converted to an empty string, and `XBool.false` values are skipped (only the attribute name is rendered).
254
+ - `date` and `datetime` attribute values are converted to ISO strings.
255
+
256
+ ### Error boundary
257
+
258
+ The `ErrorBoundary` component is useful if you want your application to fail gracefully (e.g. display an error message) instead of raising an HTTP error.
259
+
260
+ The error boundary wraps a component component subtree. When the renderer encounters an `ErrorBoundary` component, it will try to render its wrapped content. If rendering fails with an exception at any point in the `ErrorBoundary`'s subtree, the renderer will automatically fall back to the component you assigned to the `ErrorBoundary`'s `fallback` property.
261
+
262
+ Optionally, you can define which errors an error boundary can handle, giving you fine control over error handling.
263
+
264
+ ### Sync or async?
265
+
266
+ In general, a component should be async if it must await some async call inside.
267
+
268
+ If a component executes a potentially "long-running" synchronous call, it is strongly recommended to delegate that call to a worker thread an await it (thus making the component async). This can be done for example with `anyio`'s `to_thread` [utility](https://anyio.readthedocs.io/en/stable/threads.html), `starlette`'s (or `fastapi`'s) `run_in_threadpool()`, and so on. The goal here is to avoid blocking the asyncio event loop, as that can lead to performance issues.
269
+
270
+ In all other cases, it's best to use sync components.
271
+
272
+ ## Why
273
+
274
+ At one end of the spectrum, there are the complete application frameworks that combine the server (Python) and client (JavaScript) applications with the entire state management and synchronization into a single Python (an in some cases an additional JavaScript) package. Some of the most popular examples are: [Reflex](https://github.com/reflex-dev/reflex), [NiceGUI](https://github.com/zauberzeug/nicegui/), [ReactPy](https://github.com/reactive-python/reactpy), and [FastUI](https://github.com/pydantic/FastUI).
275
+
276
+ The main benefit of these frameworks is rapid application prototyping and a very convenient developer experience (at least as long as you stay within the built-in feature set of the framework). In exchange for that, they are very opinionated (from components to frontend tooling and state management), the underlying engineering is very complex, deployment and scaling can be hard or costly, and they can be hard to migrate away from. Even with these caveats, they can be a very good choice for internal tools and application prototyping.
277
+
278
+ The other end of spectrum -- plain rendering engines -- is dominated by the [Jinja](https://jinja.palletsprojects.com) templating engine, which is a safe choice as it has been and will be around for a long time. The main drawbacks with Jinja are the lack of good IDE support, the complete lack of static code analysis support, and the (subjectively) ugly syntax.
279
+
280
+ Then there are tools that aim for the middleground, usually by providing most of the benefits and drawbacks of complete application frameworks while leaving state management, client-server communication, and dynamic UI updates for the user to solve, often with some level of [HTMX](https://htmx.org/) support. This group includes libraries like [FastHTML](https://github.com/answerdotai/fasthtml) and [Ludic](https://github.com/getludic/ludic).
281
+
282
+ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, which is as **simple**, **maintainable**, and **customizable** as possible, while still providing all the building blocks for (conveniently) creating complex and maintainable applications.
283
+
284
+ ## Dependencies
285
+
286
+ The library has **no dependencies**.
287
+
288
+ ## Development
289
+
290
+ Use `ruff` for linting and formatting, `mypy` for static code analysis, and `pytest` for testing.
291
+
292
+ The documentation is built with `mkdocs-material` and `mkdocstrings`.
293
+
294
+ ## Contributing
295
+
296
+ All contributions are welcome, including more documentation, examples, code, and tests. Even questions.
297
+
298
+ ## License - MIT
299
+
300
+ The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
@@ -0,0 +1,35 @@
1
+ from .core import BaseTag as BaseTag
2
+ from .core import ContextAware as ContextAware
3
+ from .core import ErrorBoundary as ErrorBoundary
4
+ from .core import Formatter as Formatter
5
+ from .core import Fragment as Fragment
6
+ from .core import SafeStr as SafeStr
7
+ from .core import SkipProperty as SkipProperty
8
+ from .core import StandaloneTag as StandaloneTag
9
+ from .core import Tag as Tag
10
+ from .core import TagConfig as TagConfig
11
+ from .core import TagWithProps as TagWithProps
12
+ from .core import WithContext as WithContext
13
+ from .core import XBool as XBool
14
+ from .core import component as component
15
+ from .core import xml_format_string as xml_format_string
16
+ from .renderer import HTMY as HTMY
17
+ from .typing import AsyncComponent as AsyncComponent
18
+ from .typing import AsyncContextProvider as AsyncContextProvider
19
+ from .typing import AsyncFunctionComponent as AsyncFunctionComponent
20
+ from .typing import Component as Component
21
+ from .typing import ComponentSequence as ComponentSequence
22
+ from .typing import ComponentType as ComponentType
23
+ from .typing import Context as Context
24
+ from .typing import ContextKey as ContextKey
25
+ from .typing import ContextProvider as ContextProvider
26
+ from .typing import ContextValue as ContextValue
27
+ from .typing import FunctionComponent as FunctionComponent
28
+ from .typing import HTMYComponentType as HTMYComponentType
29
+ from .typing import Properties as Properties
30
+ from .typing import PropertyValue as PropertyValue
31
+ from .typing import SyncComponent as SyncComponent
32
+ from .typing import SyncContextProvider as SyncContextProvider
33
+ from .typing import SyncFunctionComponent as SyncFunctionComponent
34
+ from .typing import is_component_sequence as is_component_sequence
35
+ from .utils import join_components as join_components