htmy 0.5.0__tar.gz → 0.6.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.5.0 → htmy-0.6.0}/PKG-INFO +21 -9
- {htmy-0.5.0 → htmy-0.6.0}/README.md +20 -8
- {htmy-0.5.0 → htmy-0.6.0}/htmy/__init__.py +1 -4
- {htmy-0.5.0 → htmy-0.6.0}/htmy/core.py +2 -93
- htmy-0.6.0/htmy/function_component.py +243 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/typing.py +6 -11
- {htmy-0.5.0 → htmy-0.6.0}/pyproject.toml +5 -3
- {htmy-0.5.0 → htmy-0.6.0}/LICENSE +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/etree.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/html.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/i18n.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/io.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/md/__init__.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/md/core.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/md/typing.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/py.typed +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/renderer/__init__.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/renderer/baseline.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/renderer/default.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/snippet.py +0 -0
- {htmy-0.5.0 → htmy-0.6.0}/htmy/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: htmy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Async, pure-Python rendering engine.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Peter Volf
|
|
@@ -30,13 +30,15 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
**Async**, **pure-Python** rendering engine.
|
|
32
32
|
|
|
33
|
+
Unleash your creativity with the full power and Python, without the hassle of learning a new templating language or dealing with its limitations!
|
|
34
|
+
|
|
33
35
|
## Key features
|
|
34
36
|
|
|
35
37
|
- **Async**-first, to let you make the best use of [modern async tools](https://github.com/timofurrer/awesome-asyncio).
|
|
36
38
|
- **Powerful**, React-like **context support**, so you can avoid prop-drilling.
|
|
37
39
|
- Sync and async **function components** with **decorator syntax**.
|
|
38
40
|
- All baseline **HTML** tags built-in.
|
|
39
|
-
- Support for **native HTML/XML** documents with dynamic formatting and **slot rendering**.
|
|
41
|
+
- Support for **native HTML/XML** documents with dynamic formatting and **slot rendering**, **without custom syntax**.
|
|
40
42
|
- **Markdown** support with tools for customization.
|
|
41
43
|
- Async, JSON based **internationalization**.
|
|
42
44
|
- Built-in, easy to use `ErrorBoundary` component for graceful error handling.
|
|
@@ -45,6 +47,10 @@ Description-Content-Type: text/markdown
|
|
|
45
47
|
- Automatic and customizable **property-name conversion** from snake case to kebab case.
|
|
46
48
|
- **Fully-typed**.
|
|
47
49
|
|
|
50
|
+
## Support
|
|
51
|
+
|
|
52
|
+
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.
|
|
53
|
+
|
|
48
54
|
## Installation
|
|
49
55
|
|
|
50
56
|
The package is available on PyPI and can be installed with:
|
|
@@ -61,9 +67,9 @@ Also, the library doesn't rely on advanced Python features such as metaclasses o
|
|
|
61
67
|
|
|
62
68
|
### Components
|
|
63
69
|
|
|
64
|
-
Every
|
|
70
|
+
Every object 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. In many cases though, you don't even need to create components, simple functions that return components will be sufficient -- you can find out more about this in the [Components guide](https://volfpeter.github.io/htmy/components-guide/) of the documentation.
|
|
65
71
|
|
|
66
|
-
Using
|
|
72
|
+
Using the `htmy()` method name enables the conversion of any of your pre-existing business objects (from `TypedDicts`s or `pydantic` models to ORM classes) into components without the fear of name collision or compatibility issues with other tools.
|
|
67
73
|
|
|
68
74
|
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.
|
|
69
75
|
|
|
@@ -205,7 +211,7 @@ if __name__ == "__main__":
|
|
|
205
211
|
|
|
206
212
|
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".
|
|
207
213
|
|
|
208
|
-
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.
|
|
214
|
+
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 (including themselves), and components can simply take what they need from the context.
|
|
209
215
|
|
|
210
216
|
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.
|
|
211
217
|
|
|
@@ -306,13 +312,13 @@ FastAPI:
|
|
|
306
312
|
|
|
307
313
|
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).
|
|
308
314
|
|
|
309
|
-
The main benefit of these frameworks is rapid application prototyping and a very convenient developer experience
|
|
315
|
+
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.
|
|
310
316
|
|
|
311
|
-
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.
|
|
317
|
+
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 custom template syntax.
|
|
312
318
|
|
|
313
319
|
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).
|
|
314
320
|
|
|
315
|
-
The primary aim of `htmy` is to be
|
|
321
|
+
The primary aim of `htmy` is to be a `Jinja` alternative that is similarly powerful and flexible, while also providing the benefits of full IDE support, static code analysis, and native Python (and HTML, XML, markdown) syntax. Additionally, `htmy` is **async-first**, so it works great with modern async Python frameworks such as [FastAPI](https://fastapi.tiangolo.com). The library was designed to be as **simple**, **maintainable**, and **customizable** as possible, while still providing all the building blocks for creating complex web applications.
|
|
316
322
|
|
|
317
323
|
## Dependencies
|
|
318
324
|
|
|
@@ -330,7 +336,13 @@ The documentation is built with `mkdocs-material` and `mkdocstrings`.
|
|
|
330
336
|
|
|
331
337
|
## Contributing
|
|
332
338
|
|
|
333
|
-
|
|
339
|
+
We welcome contributions from the community to help improve the project! Whether you're an experienced developer or just starting out, there are many ways you can contribute:
|
|
340
|
+
|
|
341
|
+
- **Discuss**: Join our [Discussion Board](https://github.com/volfpeter/htmy/discussions) to ask questions, share ideas, provide feedback, and engage with the community.
|
|
342
|
+
- **Document**: Help improve the documentation by fixing typos, adding examples, and updating guides to make it easier for others to use the project.
|
|
343
|
+
- **Develop**: Prototype requested features or pick up issues from the issue tracker.
|
|
344
|
+
- **Share**: Share your own project by adding a link to it in the documentation, helping others discover and benefit from your work.
|
|
345
|
+
- **Test**: Write tests to improve coverage and enhance reliability.
|
|
334
346
|
|
|
335
347
|
## License - MIT
|
|
336
348
|
|
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
|
|
12
12
|
**Async**, **pure-Python** rendering engine.
|
|
13
13
|
|
|
14
|
+
Unleash your creativity with the full power and Python, without the hassle of learning a new templating language or dealing with its limitations!
|
|
15
|
+
|
|
14
16
|
## Key features
|
|
15
17
|
|
|
16
18
|
- **Async**-first, to let you make the best use of [modern async tools](https://github.com/timofurrer/awesome-asyncio).
|
|
17
19
|
- **Powerful**, React-like **context support**, so you can avoid prop-drilling.
|
|
18
20
|
- Sync and async **function components** with **decorator syntax**.
|
|
19
21
|
- All baseline **HTML** tags built-in.
|
|
20
|
-
- Support for **native HTML/XML** documents with dynamic formatting and **slot rendering**.
|
|
22
|
+
- Support for **native HTML/XML** documents with dynamic formatting and **slot rendering**, **without custom syntax**.
|
|
21
23
|
- **Markdown** support with tools for customization.
|
|
22
24
|
- Async, JSON based **internationalization**.
|
|
23
25
|
- Built-in, easy to use `ErrorBoundary` component for graceful error handling.
|
|
@@ -26,6 +28,10 @@
|
|
|
26
28
|
- Automatic and customizable **property-name conversion** from snake case to kebab case.
|
|
27
29
|
- **Fully-typed**.
|
|
28
30
|
|
|
31
|
+
## Support
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
|
|
29
35
|
## Installation
|
|
30
36
|
|
|
31
37
|
The package is available on PyPI and can be installed with:
|
|
@@ -42,9 +48,9 @@ Also, the library doesn't rely on advanced Python features such as metaclasses o
|
|
|
42
48
|
|
|
43
49
|
### Components
|
|
44
50
|
|
|
45
|
-
Every
|
|
51
|
+
Every object 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. In many cases though, you don't even need to create components, simple functions that return components will be sufficient -- you can find out more about this in the [Components guide](https://volfpeter.github.io/htmy/components-guide/) of the documentation.
|
|
46
52
|
|
|
47
|
-
Using
|
|
53
|
+
Using the `htmy()` method name enables the conversion of any of your pre-existing business objects (from `TypedDicts`s or `pydantic` models to ORM classes) into components without the fear of name collision or compatibility issues with other tools.
|
|
48
54
|
|
|
49
55
|
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.
|
|
50
56
|
|
|
@@ -186,7 +192,7 @@ if __name__ == "__main__":
|
|
|
186
192
|
|
|
187
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".
|
|
188
194
|
|
|
189
|
-
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.
|
|
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 (including themselves), and components can simply take what they need from the context.
|
|
190
196
|
|
|
191
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.
|
|
192
198
|
|
|
@@ -287,13 +293,13 @@ FastAPI:
|
|
|
287
293
|
|
|
288
294
|
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
295
|
|
|
290
|
-
The main benefit of these frameworks is rapid application prototyping and a very convenient developer experience
|
|
296
|
+
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
297
|
|
|
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.
|
|
298
|
+
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 custom template syntax.
|
|
293
299
|
|
|
294
300
|
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
301
|
|
|
296
|
-
The primary aim of `htmy` is to be
|
|
302
|
+
The primary aim of `htmy` is to be a `Jinja` alternative that is similarly powerful and flexible, while also providing the benefits of full IDE support, static code analysis, and native Python (and HTML, XML, markdown) syntax. Additionally, `htmy` is **async-first**, so it works great with modern async Python frameworks such as [FastAPI](https://fastapi.tiangolo.com). The library was designed to be as **simple**, **maintainable**, and **customizable** as possible, while still providing all the building blocks for creating complex web applications.
|
|
297
303
|
|
|
298
304
|
## Dependencies
|
|
299
305
|
|
|
@@ -311,7 +317,13 @@ The documentation is built with `mkdocs-material` and `mkdocstrings`.
|
|
|
311
317
|
|
|
312
318
|
## Contributing
|
|
313
319
|
|
|
314
|
-
|
|
320
|
+
We welcome contributions from the community to help improve the project! Whether you're an experienced developer or just starting out, there are many ways you can contribute:
|
|
321
|
+
|
|
322
|
+
- **Discuss**: Join our [Discussion Board](https://github.com/volfpeter/htmy/discussions) to ask questions, share ideas, provide feedback, and engage with the community.
|
|
323
|
+
- **Document**: Help improve the documentation by fixing typos, adding examples, and updating guides to make it easier for others to use the project.
|
|
324
|
+
- **Develop**: Prototype requested features or pick up issues from the issue tracker.
|
|
325
|
+
- **Share**: Share your own project by adding a link to it in the documentation, helping others discover and benefit from your work.
|
|
326
|
+
- **Test**: Write tests to improve coverage and enhance reliability.
|
|
315
327
|
|
|
316
328
|
## License - MIT
|
|
317
329
|
|
|
@@ -12,14 +12,13 @@ from .core import Text as Text
|
|
|
12
12
|
from .core import WildcardTag as WildcardTag
|
|
13
13
|
from .core import WithContext as WithContext
|
|
14
14
|
from .core import XBool as XBool
|
|
15
|
-
from .core import component as component
|
|
16
15
|
from .core import xml_format_string as xml_format_string
|
|
16
|
+
from .function_component import component as component
|
|
17
17
|
from .renderer import Renderer as Renderer
|
|
18
18
|
from .snippet import Slots as Slots
|
|
19
19
|
from .snippet import Snippet as Snippet
|
|
20
20
|
from .typing import AsyncComponent as AsyncComponent
|
|
21
21
|
from .typing import AsyncContextProvider as AsyncContextProvider
|
|
22
|
-
from .typing import AsyncFunctionComponent as AsyncFunctionComponent
|
|
23
22
|
from .typing import Component as Component
|
|
24
23
|
from .typing import ComponentSequence as ComponentSequence
|
|
25
24
|
from .typing import ComponentType as ComponentType
|
|
@@ -27,14 +26,12 @@ from .typing import Context as Context
|
|
|
27
26
|
from .typing import ContextKey as ContextKey
|
|
28
27
|
from .typing import ContextProvider as ContextProvider
|
|
29
28
|
from .typing import ContextValue as ContextValue
|
|
30
|
-
from .typing import FunctionComponent as FunctionComponent
|
|
31
29
|
from .typing import HTMYComponentType as HTMYComponentType
|
|
32
30
|
from .typing import MutableContext as MutableContext
|
|
33
31
|
from .typing import Properties as Properties
|
|
34
32
|
from .typing import PropertyValue as PropertyValue
|
|
35
33
|
from .typing import SyncComponent as SyncComponent
|
|
36
34
|
from .typing import SyncContextProvider as SyncContextProvider
|
|
37
|
-
from .typing import SyncFunctionComponent as SyncFunctionComponent
|
|
38
35
|
from .utils import as_component_sequence as as_component_sequence
|
|
39
36
|
from .utils import as_component_type as as_component_type
|
|
40
37
|
from .utils import is_component_sequence as is_component_sequence
|
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
import asyncio
|
|
5
4
|
import enum
|
|
6
5
|
from collections.abc import Callable, Container
|
|
7
|
-
from typing import TYPE_CHECKING, Any, ClassVar,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
|
|
8
7
|
from xml.sax.saxutils import escape as xml_escape
|
|
9
8
|
from xml.sax.saxutils import quoteattr as xml_quoteattr
|
|
10
9
|
|
|
11
|
-
from .typing import
|
|
12
|
-
AsyncFunctionComponent,
|
|
13
|
-
Component,
|
|
14
|
-
ComponentType,
|
|
15
|
-
Context,
|
|
16
|
-
ContextKey,
|
|
17
|
-
ContextValue,
|
|
18
|
-
FunctionComponent,
|
|
19
|
-
PropertyValue,
|
|
20
|
-
SyncFunctionComponent,
|
|
21
|
-
T,
|
|
22
|
-
)
|
|
10
|
+
from .typing import Component, ComponentType, Context, ContextKey, ContextValue, PropertyValue, T
|
|
23
11
|
from .utils import as_component_type, join_components
|
|
24
12
|
|
|
25
13
|
if TYPE_CHECKING:
|
|
@@ -201,85 +189,6 @@ class ContextAware:
|
|
|
201
189
|
raise TypeError(f"Invalid context data type for {cls.__name__}.")
|
|
202
190
|
|
|
203
191
|
|
|
204
|
-
# -- Function components
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class SyncFunctionComponentWrapper(Generic[T]):
|
|
208
|
-
"""Base class `FunctionComponent` wrappers."""
|
|
209
|
-
|
|
210
|
-
__slots__ = ("_props",)
|
|
211
|
-
|
|
212
|
-
_wrapped_function: SyncFunctionComponent[T]
|
|
213
|
-
|
|
214
|
-
def __init__(self, props: T) -> None:
|
|
215
|
-
self._props = props
|
|
216
|
-
|
|
217
|
-
def __init_subclass__(cls, *, func: SyncFunctionComponent[T]) -> None:
|
|
218
|
-
cls._wrapped_function = func
|
|
219
|
-
|
|
220
|
-
def htmy(self, context: Context) -> Component:
|
|
221
|
-
"""Renders the component."""
|
|
222
|
-
# type(self) is necessary, otherwise the wrapped function would be called
|
|
223
|
-
# with an extra self argument...
|
|
224
|
-
return type(self)._wrapped_function(self._props, context)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
class AsyncFunctionComponentWrapper(Generic[T]):
|
|
228
|
-
"""Base class `FunctionComponent` wrappers."""
|
|
229
|
-
|
|
230
|
-
__slots__ = ("_props",)
|
|
231
|
-
|
|
232
|
-
_wrapped_function: AsyncFunctionComponent[T]
|
|
233
|
-
|
|
234
|
-
def __init__(self, props: T) -> None:
|
|
235
|
-
self._props = props
|
|
236
|
-
|
|
237
|
-
def __init_subclass__(cls, *, func: AsyncFunctionComponent[T]) -> None:
|
|
238
|
-
cls._wrapped_function = func
|
|
239
|
-
|
|
240
|
-
async def htmy(self, context: Context) -> Component:
|
|
241
|
-
"""Renders the component."""
|
|
242
|
-
# type(self) is necessary, otherwise the wrapped function would be called
|
|
243
|
-
# with an extra self argument...
|
|
244
|
-
return await type(self)._wrapped_function(self._props, context)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
@overload
|
|
248
|
-
def component(func: SyncFunctionComponent[T]) -> type[SyncFunctionComponentWrapper[T]]: ...
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
@overload
|
|
252
|
-
def component(func: AsyncFunctionComponent[T]) -> type[AsyncFunctionComponentWrapper[T]]: ...
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def component(
|
|
256
|
-
func: FunctionComponent[T],
|
|
257
|
-
) -> type[SyncFunctionComponentWrapper[T]] | type[AsyncFunctionComponentWrapper[T]]:
|
|
258
|
-
"""
|
|
259
|
-
Decorator that converts the given function into a component.
|
|
260
|
-
|
|
261
|
-
Internally this is achieved by wrapping the function in a pre-configured
|
|
262
|
-
`FunctionComponentWrapper` subclass.
|
|
263
|
-
|
|
264
|
-
Arguments:
|
|
265
|
-
func: The decorated function component.
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
A pre-configured `FunctionComponentWrapper` subclass.
|
|
269
|
-
"""
|
|
270
|
-
|
|
271
|
-
if asyncio.iscoroutinefunction(func):
|
|
272
|
-
|
|
273
|
-
class AsyncFCW(AsyncFunctionComponentWrapper[T], func=func): ...
|
|
274
|
-
|
|
275
|
-
return AsyncFCW
|
|
276
|
-
else:
|
|
277
|
-
|
|
278
|
-
class SyncFCW(SyncFunctionComponentWrapper[T], func=func): ... # type: ignore[arg-type]
|
|
279
|
-
|
|
280
|
-
return SyncFCW
|
|
281
|
-
|
|
282
|
-
|
|
283
192
|
# -- Formatting
|
|
284
193
|
|
|
285
194
|
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable, Coroutine
|
|
5
|
+
from typing import Any, Protocol, TypeAlias, overload
|
|
6
|
+
|
|
7
|
+
from .typing import AsyncComponent, Component, Context, SyncComponent, T
|
|
8
|
+
|
|
9
|
+
# -- Typing for "full" function components.
|
|
10
|
+
|
|
11
|
+
_SyncFunctionComponent: TypeAlias = Callable[[T, Context], Component]
|
|
12
|
+
"""
|
|
13
|
+
Protocol definition for sync function components that have both a properties and a context argument.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
_AsyncFunctionComponent: TypeAlias = Callable[[T, Context], Coroutine[Any, Any, Component]]
|
|
17
|
+
"""
|
|
18
|
+
Protocol definition for async function components that have both a properties and a context argument.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_FunctionComponent: TypeAlias = _SyncFunctionComponent[T] | _AsyncFunctionComponent[T]
|
|
22
|
+
"""
|
|
23
|
+
Function component type that has both a properties and a context argument.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# -- Typing for context-only function components.
|
|
27
|
+
|
|
28
|
+
_ContextOnlySyncFunctionComponent: TypeAlias = Callable[[Context], Component]
|
|
29
|
+
"""
|
|
30
|
+
Protocol definition for sync function components that only have a context argument.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _DecoratedContextOnlySyncFunctionComponent(SyncComponent, Protocol):
|
|
35
|
+
"""
|
|
36
|
+
Protocol definition for sync components that are also callable, and return a sync
|
|
37
|
+
component when called.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __call__(self) -> SyncComponent: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_ContextOnlyAsyncFunctionComponent: TypeAlias = Callable[[Context], Coroutine[Any, Any, Component]]
|
|
44
|
+
"""
|
|
45
|
+
Protocol definition for async function components that only have a context argument.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _DecoratedContextOnlyAsyncFunctionComponent(SyncComponent, Protocol):
|
|
50
|
+
"""
|
|
51
|
+
Protocol definition for async components that are also callable, and return an async
|
|
52
|
+
component when called.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __call__(self) -> SyncComponent: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_ContextOnlyFunctionComponent: TypeAlias = (
|
|
59
|
+
_ContextOnlySyncFunctionComponent | _ContextOnlyAsyncFunctionComponent
|
|
60
|
+
)
|
|
61
|
+
"""
|
|
62
|
+
Function component type that only accepts a context argument.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
_DecoratedContextOnlyFunction: TypeAlias = (
|
|
66
|
+
_DecoratedContextOnlySyncFunctionComponent | _DecoratedContextOnlyAsyncFunctionComponent
|
|
67
|
+
)
|
|
68
|
+
"""
|
|
69
|
+
Protocol definition for sync or async components that are also callable, and return a sync
|
|
70
|
+
or async component when called.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ComponentDecorators:
|
|
75
|
+
"""
|
|
76
|
+
Function component decorators.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
__slots__ = ()
|
|
80
|
+
|
|
81
|
+
# -- FunctionComponent decorator.
|
|
82
|
+
|
|
83
|
+
@overload
|
|
84
|
+
def __call__(self, func: _SyncFunctionComponent[T]) -> Callable[[T], SyncComponent]: ...
|
|
85
|
+
|
|
86
|
+
@overload
|
|
87
|
+
def __call__(self, func: _AsyncFunctionComponent[T]) -> Callable[[T], AsyncComponent]: ...
|
|
88
|
+
|
|
89
|
+
def __call__(
|
|
90
|
+
self,
|
|
91
|
+
func: _FunctionComponent[T],
|
|
92
|
+
) -> Callable[[T], SyncComponent] | Callable[[T], AsyncComponent]:
|
|
93
|
+
"""
|
|
94
|
+
Decorator that converts the decorated function into one that must be called with
|
|
95
|
+
the function component's properties and returns a component instance.
|
|
96
|
+
|
|
97
|
+
If used on an async function, the resulting component will also be async;
|
|
98
|
+
otherwise it will be sync.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
@component
|
|
104
|
+
def my_component(props: int, context: Context) -> Component:
|
|
105
|
+
return html.p(f"Value: {props}")
|
|
106
|
+
|
|
107
|
+
async def render():
|
|
108
|
+
return await Renderer().render(
|
|
109
|
+
my_component(42)
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Arguments:
|
|
114
|
+
func: The decorated function.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A function that must be called with the function component's properties and
|
|
118
|
+
returns a component instance. (Or loosly speaking, an `HTMYComponentType` which
|
|
119
|
+
can be "instantiated" with the function component's properties.)
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
if asyncio.iscoroutinefunction(func):
|
|
123
|
+
|
|
124
|
+
def async_wrapper(props: T) -> AsyncComponent:
|
|
125
|
+
# This function must be async, in case the renderer inspects it to decide how to handle it.
|
|
126
|
+
async def component(context: Context) -> Component:
|
|
127
|
+
return await func(props, context) # type: ignore[no-any-return]
|
|
128
|
+
|
|
129
|
+
component.htmy = component # type: ignore[attr-defined]
|
|
130
|
+
return component # type: ignore[return-value]
|
|
131
|
+
|
|
132
|
+
return async_wrapper
|
|
133
|
+
else:
|
|
134
|
+
|
|
135
|
+
def sync_wrapper(props: T) -> SyncComponent:
|
|
136
|
+
def component(context: Context) -> Component:
|
|
137
|
+
return func(props, context) # type: ignore[return-value]
|
|
138
|
+
|
|
139
|
+
component.htmy = component # type: ignore[attr-defined]
|
|
140
|
+
return component # type: ignore[return-value]
|
|
141
|
+
|
|
142
|
+
return sync_wrapper
|
|
143
|
+
|
|
144
|
+
@overload
|
|
145
|
+
def function(self, func: _SyncFunctionComponent[T]) -> Callable[[T], SyncComponent]: ...
|
|
146
|
+
|
|
147
|
+
@overload
|
|
148
|
+
def function(self, func: _AsyncFunctionComponent[T]) -> Callable[[T], AsyncComponent]: ...
|
|
149
|
+
|
|
150
|
+
def function(
|
|
151
|
+
self,
|
|
152
|
+
func: _FunctionComponent[T],
|
|
153
|
+
) -> Callable[[T], SyncComponent] | Callable[[T], AsyncComponent]:
|
|
154
|
+
"""
|
|
155
|
+
Decorator that converts the decorated function into one that must be called with
|
|
156
|
+
the function component's properties and returns a component instance.
|
|
157
|
+
|
|
158
|
+
If used on an async function, the resulting component will also be async;
|
|
159
|
+
otherwise it will be sync.
|
|
160
|
+
|
|
161
|
+
This function is just an alias for `__call__()`.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
@component.function
|
|
167
|
+
def my_component(props: int, context: Context) -> Component:
|
|
168
|
+
return html.p(f"Value: {props}")
|
|
169
|
+
|
|
170
|
+
async def render():
|
|
171
|
+
return await Renderer().render(
|
|
172
|
+
my_component(42)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
Arguments:
|
|
176
|
+
func: The decorated function.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
A function that must be called with the function component's properties and
|
|
180
|
+
returns a component instance. (Or loosly speaking, an `HTMYComponentType` which
|
|
181
|
+
can be "instantiated" with the function component's properties.)
|
|
182
|
+
"""
|
|
183
|
+
return self(func)
|
|
184
|
+
|
|
185
|
+
# -- ContextOnlyFunctionComponent decorator.
|
|
186
|
+
|
|
187
|
+
@overload
|
|
188
|
+
def context_only(
|
|
189
|
+
self, func: _ContextOnlySyncFunctionComponent
|
|
190
|
+
) -> _DecoratedContextOnlySyncFunctionComponent: ...
|
|
191
|
+
|
|
192
|
+
@overload
|
|
193
|
+
def context_only(
|
|
194
|
+
self, func: _ContextOnlyAsyncFunctionComponent
|
|
195
|
+
) -> _DecoratedContextOnlyAsyncFunctionComponent: ...
|
|
196
|
+
|
|
197
|
+
def context_only(
|
|
198
|
+
self,
|
|
199
|
+
func: _ContextOnlyFunctionComponent,
|
|
200
|
+
) -> _DecoratedContextOnlySyncFunctionComponent | _DecoratedContextOnlyAsyncFunctionComponent:
|
|
201
|
+
"""
|
|
202
|
+
Decorator that converts the decorated function into a component.
|
|
203
|
+
|
|
204
|
+
If used on an async function, the resulting component will also be async;
|
|
205
|
+
otherwise it will be sync.
|
|
206
|
+
|
|
207
|
+
The decorated function will be both a component object and a callable that returns a
|
|
208
|
+
component object, so it can be used in the component tree both with and without the
|
|
209
|
+
call signature:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
@component.context_only
|
|
213
|
+
def my_component(ctx):
|
|
214
|
+
return "Context only function component."
|
|
215
|
+
|
|
216
|
+
async def render():
|
|
217
|
+
return await Renderer().render(
|
|
218
|
+
my_component(), # With call signature.
|
|
219
|
+
my_component, # Without call signature.
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Arguments:
|
|
224
|
+
func: The decorated function.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
The created component.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def wrapper() -> SyncComponent | AsyncComponent:
|
|
231
|
+
func.htmy = func # type: ignore[union-attr]
|
|
232
|
+
return func # type: ignore[return-value]
|
|
233
|
+
|
|
234
|
+
wrapper.htmy = func # type: ignore[attr-defined]
|
|
235
|
+
return wrapper # type: ignore[return-value]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
component = ComponentDecorators()
|
|
239
|
+
"""
|
|
240
|
+
Decorators for converting functions into components
|
|
241
|
+
|
|
242
|
+
This is an instance of `ComponentDecorators`.
|
|
243
|
+
"""
|
|
@@ -65,16 +65,6 @@ ComponentSequence: TypeAlias = list[ComponentType] | tuple[ComponentType, ...]
|
|
|
65
65
|
Component: TypeAlias = ComponentType | ComponentSequence
|
|
66
66
|
"""Component type: a single component or a sequence of components."""
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
SyncFunctionComponent: TypeAlias = Callable[[T, Context], Component]
|
|
70
|
-
"""Protocol definition for sync function components."""
|
|
71
|
-
|
|
72
|
-
AsyncFunctionComponent: TypeAlias = Callable[[T, Context], Coroutine[Any, Any, Component]]
|
|
73
|
-
"""Protocol definition for async function components."""
|
|
74
|
-
|
|
75
|
-
FunctionComponent: TypeAlias = SyncFunctionComponent[T] | AsyncFunctionComponent[T]
|
|
76
|
-
"""Function component type."""
|
|
77
|
-
|
|
78
68
|
# -- Context providers
|
|
79
69
|
|
|
80
70
|
|
|
@@ -97,7 +87,12 @@ class AsyncContextProvider(Protocol):
|
|
|
97
87
|
|
|
98
88
|
|
|
99
89
|
ContextProvider: TypeAlias = SyncContextProvider | AsyncContextProvider
|
|
100
|
-
"""
|
|
90
|
+
"""
|
|
91
|
+
Sync or async context provider type.
|
|
92
|
+
|
|
93
|
+
Components can implement this protocol to add extra data to the rendering context
|
|
94
|
+
of their entire component subtree (including themselves).
|
|
95
|
+
"""
|
|
101
96
|
|
|
102
97
|
# -- Text processors
|
|
103
98
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "htmy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Async, pure-Python rendering engine."
|
|
5
5
|
authors = ["Peter Volf <do.volfp@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -15,14 +15,16 @@ markdown = "^3.7"
|
|
|
15
15
|
[tool.poetry.group.dev.dependencies]
|
|
16
16
|
mkdocs-material = "^9.5.39"
|
|
17
17
|
mkdocstrings = {extras = ["python"], version = "^0.26.1"}
|
|
18
|
-
mypy = "^1.
|
|
18
|
+
mypy = "^1.15.0"
|
|
19
19
|
poethepoet = "^0.29.0"
|
|
20
20
|
pytest = "^8.3.3"
|
|
21
21
|
pytest-asyncio = "^0.24.0"
|
|
22
22
|
pytest-random-order = "^1.1.1"
|
|
23
|
-
ruff = "^0.
|
|
23
|
+
ruff = "^0.9.0"
|
|
24
24
|
types-markdown = "^3.7.0.20240822"
|
|
25
25
|
typing-extensions = "^4.12.2"
|
|
26
|
+
fastapi = "^0.115.8"
|
|
27
|
+
fasthx = "^2.2.1"
|
|
26
28
|
|
|
27
29
|
[build-system]
|
|
28
30
|
requires = ["poetry-core"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|