htmy 0.4.2__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htmy/__init__.py +6 -6
- htmy/core.py +5 -157
- htmy/function_component.py +243 -0
- htmy/md/core.py +18 -4
- htmy/renderer/default.py +2 -1
- htmy/snippet.py +259 -0
- htmy/typing.py +24 -17
- htmy/utils.py +21 -2
- {htmy-0.4.2.dist-info → htmy-0.6.0.dist-info}/METADATA +22 -9
- htmy-0.6.0.dist-info/RECORD +21 -0
- {htmy-0.4.2.dist-info → htmy-0.6.0.dist-info}/WHEEL +1 -1
- htmy-0.4.2.dist-info/RECORD +0 -19
- {htmy-0.4.2.dist-info → htmy-0.6.0.dist-info}/LICENSE +0 -0
htmy/__init__.py
CHANGED
|
@@ -5,7 +5,6 @@ from .core import Formatter as Formatter
|
|
|
5
5
|
from .core import Fragment as Fragment
|
|
6
6
|
from .core import SafeStr as SafeStr
|
|
7
7
|
from .core import SkipProperty as SkipProperty
|
|
8
|
-
from .core import Snippet as Snippet
|
|
9
8
|
from .core import Tag as Tag
|
|
10
9
|
from .core import TagConfig as TagConfig
|
|
11
10
|
from .core import TagWithProps as TagWithProps
|
|
@@ -13,12 +12,13 @@ from .core import Text as Text
|
|
|
13
12
|
from .core import WildcardTag as WildcardTag
|
|
14
13
|
from .core import WithContext as WithContext
|
|
15
14
|
from .core import XBool as XBool
|
|
16
|
-
from .core import component as component
|
|
17
15
|
from .core import xml_format_string as xml_format_string
|
|
16
|
+
from .function_component import component as component
|
|
18
17
|
from .renderer import Renderer as Renderer
|
|
18
|
+
from .snippet import Slots as Slots
|
|
19
|
+
from .snippet import Snippet as Snippet
|
|
19
20
|
from .typing import AsyncComponent as AsyncComponent
|
|
20
21
|
from .typing import AsyncContextProvider as AsyncContextProvider
|
|
21
|
-
from .typing import AsyncFunctionComponent as AsyncFunctionComponent
|
|
22
22
|
from .typing import Component as Component
|
|
23
23
|
from .typing import ComponentSequence as ComponentSequence
|
|
24
24
|
from .typing import ComponentType as ComponentType
|
|
@@ -26,15 +26,15 @@ from .typing import Context as Context
|
|
|
26
26
|
from .typing import ContextKey as ContextKey
|
|
27
27
|
from .typing import ContextProvider as ContextProvider
|
|
28
28
|
from .typing import ContextValue as ContextValue
|
|
29
|
-
from .typing import FunctionComponent as FunctionComponent
|
|
30
29
|
from .typing import HTMYComponentType as HTMYComponentType
|
|
31
30
|
from .typing import MutableContext as MutableContext
|
|
32
31
|
from .typing import Properties as Properties
|
|
33
32
|
from .typing import PropertyValue as PropertyValue
|
|
34
33
|
from .typing import SyncComponent as SyncComponent
|
|
35
34
|
from .typing import SyncContextProvider as SyncContextProvider
|
|
36
|
-
from .
|
|
37
|
-
from .
|
|
35
|
+
from .utils import as_component_sequence as as_component_sequence
|
|
36
|
+
from .utils import as_component_type as as_component_type
|
|
37
|
+
from .utils import is_component_sequence as is_component_sequence
|
|
38
38
|
from .utils import join_components as join_components
|
|
39
39
|
|
|
40
40
|
HTMY = Renderer
|
htmy/core.py
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
import asyncio
|
|
5
4
|
import enum
|
|
6
|
-
from collections.abc import
|
|
7
|
-
from
|
|
8
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict, cast, overload
|
|
5
|
+
from collections.abc import Callable, Container
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
|
|
9
7
|
from xml.sax.saxutils import escape as xml_escape
|
|
10
8
|
from xml.sax.saxutils import quoteattr as xml_quoteattr
|
|
11
9
|
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
14
|
-
AsyncFunctionComponent,
|
|
15
|
-
Component,
|
|
16
|
-
ComponentType,
|
|
17
|
-
Context,
|
|
18
|
-
ContextKey,
|
|
19
|
-
ContextValue,
|
|
20
|
-
FunctionComponent,
|
|
21
|
-
PropertyValue,
|
|
22
|
-
SyncFunctionComponent,
|
|
23
|
-
T,
|
|
24
|
-
TextProcessor,
|
|
25
|
-
is_component_sequence,
|
|
26
|
-
)
|
|
27
|
-
from .utils import join_components
|
|
10
|
+
from .typing import Component, ComponentType, Context, ContextKey, ContextValue, PropertyValue, T
|
|
11
|
+
from .utils import as_component_type, join_components
|
|
28
12
|
|
|
29
13
|
if TYPE_CHECKING:
|
|
30
14
|
from typing_extensions import Never, Self
|
|
@@ -97,11 +81,7 @@ class ErrorBoundary(Fragment):
|
|
|
97
81
|
if not (self._errors is None or any(e in self._errors for e in type(error).mro())):
|
|
98
82
|
raise error
|
|
99
83
|
|
|
100
|
-
return (
|
|
101
|
-
Fragment(*self._fallback)
|
|
102
|
-
if is_component_sequence(self._fallback)
|
|
103
|
-
else cast(ComponentType, self._fallback)
|
|
104
|
-
)
|
|
84
|
+
return as_component_type(self._fallback)
|
|
105
85
|
|
|
106
86
|
|
|
107
87
|
class WithContext(Fragment):
|
|
@@ -127,59 +107,6 @@ class WithContext(Fragment):
|
|
|
127
107
|
return self._context
|
|
128
108
|
|
|
129
109
|
|
|
130
|
-
class Snippet:
|
|
131
|
-
"""
|
|
132
|
-
Base component that can load its content from a file.
|
|
133
|
-
"""
|
|
134
|
-
|
|
135
|
-
__slots__ = ("_path_or_text", "_text_processor")
|
|
136
|
-
|
|
137
|
-
def __init__(
|
|
138
|
-
self,
|
|
139
|
-
path_or_text: Text | str | Path,
|
|
140
|
-
*,
|
|
141
|
-
text_processor: TextProcessor | None = None,
|
|
142
|
-
) -> None:
|
|
143
|
-
"""
|
|
144
|
-
Initialization.
|
|
145
|
-
|
|
146
|
-
Arguments:
|
|
147
|
-
path_or_text: The path from where the content should be loaded or a `Text`
|
|
148
|
-
instance if this value should be rendered directly.
|
|
149
|
-
text_processor: An optional text processors that can be used to process the text
|
|
150
|
-
content before rendering. It can be used for example for token replacement or
|
|
151
|
-
string formatting.
|
|
152
|
-
"""
|
|
153
|
-
self._path_or_text = path_or_text
|
|
154
|
-
self._text_processor = text_processor
|
|
155
|
-
|
|
156
|
-
async def htmy(self, context: Context) -> Component:
|
|
157
|
-
"""Renders the component."""
|
|
158
|
-
text = await self._get_text_content()
|
|
159
|
-
if self._text_processor is not None:
|
|
160
|
-
processed = self._text_processor(text, context)
|
|
161
|
-
text = (await processed) if isinstance(processed, Awaitable) else processed
|
|
162
|
-
|
|
163
|
-
return self._render_text(text, context)
|
|
164
|
-
|
|
165
|
-
async def _get_text_content(self) -> str:
|
|
166
|
-
"""Returns the plain text content that should be rendered."""
|
|
167
|
-
path_or_text = self._path_or_text
|
|
168
|
-
|
|
169
|
-
if isinstance(path_or_text, Text):
|
|
170
|
-
return path_or_text
|
|
171
|
-
else:
|
|
172
|
-
async with await open_file(path_or_text, "r") as f:
|
|
173
|
-
return await f.read()
|
|
174
|
-
|
|
175
|
-
def _render_text(self, text: str, context: Context) -> Component:
|
|
176
|
-
"""
|
|
177
|
-
Render function that takes the text that must be rendered and the current rendering context,
|
|
178
|
-
and returns the corresponding component.
|
|
179
|
-
"""
|
|
180
|
-
return SafeStr(text)
|
|
181
|
-
|
|
182
|
-
|
|
183
110
|
# -- Context utilities
|
|
184
111
|
|
|
185
112
|
|
|
@@ -262,85 +189,6 @@ class ContextAware:
|
|
|
262
189
|
raise TypeError(f"Invalid context data type for {cls.__name__}.")
|
|
263
190
|
|
|
264
191
|
|
|
265
|
-
# -- Function components
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
class SyncFunctionComponentWrapper(Generic[T]):
|
|
269
|
-
"""Base class `FunctionComponent` wrappers."""
|
|
270
|
-
|
|
271
|
-
__slots__ = ("_props",)
|
|
272
|
-
|
|
273
|
-
_wrapped_function: SyncFunctionComponent[T]
|
|
274
|
-
|
|
275
|
-
def __init__(self, props: T) -> None:
|
|
276
|
-
self._props = props
|
|
277
|
-
|
|
278
|
-
def __init_subclass__(cls, *, func: SyncFunctionComponent[T]) -> None:
|
|
279
|
-
cls._wrapped_function = func
|
|
280
|
-
|
|
281
|
-
def htmy(self, context: Context) -> Component:
|
|
282
|
-
"""Renders the component."""
|
|
283
|
-
# type(self) is necessary, otherwise the wrapped function would be called
|
|
284
|
-
# with an extra self argument...
|
|
285
|
-
return type(self)._wrapped_function(self._props, context)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
class AsyncFunctionComponentWrapper(Generic[T]):
|
|
289
|
-
"""Base class `FunctionComponent` wrappers."""
|
|
290
|
-
|
|
291
|
-
__slots__ = ("_props",)
|
|
292
|
-
|
|
293
|
-
_wrapped_function: AsyncFunctionComponent[T]
|
|
294
|
-
|
|
295
|
-
def __init__(self, props: T) -> None:
|
|
296
|
-
self._props = props
|
|
297
|
-
|
|
298
|
-
def __init_subclass__(cls, *, func: AsyncFunctionComponent[T]) -> None:
|
|
299
|
-
cls._wrapped_function = func
|
|
300
|
-
|
|
301
|
-
async def htmy(self, context: Context) -> Component:
|
|
302
|
-
"""Renders the component."""
|
|
303
|
-
# type(self) is necessary, otherwise the wrapped function would be called
|
|
304
|
-
# with an extra self argument...
|
|
305
|
-
return await type(self)._wrapped_function(self._props, context)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
@overload
|
|
309
|
-
def component(func: SyncFunctionComponent[T]) -> type[SyncFunctionComponentWrapper[T]]: ...
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
@overload
|
|
313
|
-
def component(func: AsyncFunctionComponent[T]) -> type[AsyncFunctionComponentWrapper[T]]: ...
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def component(
|
|
317
|
-
func: FunctionComponent[T],
|
|
318
|
-
) -> type[SyncFunctionComponentWrapper[T]] | type[AsyncFunctionComponentWrapper[T]]:
|
|
319
|
-
"""
|
|
320
|
-
Decorator that converts the given function into a component.
|
|
321
|
-
|
|
322
|
-
Internally this is achieved by wrapping the function in a pre-configured
|
|
323
|
-
`FunctionComponentWrapper` subclass.
|
|
324
|
-
|
|
325
|
-
Arguments:
|
|
326
|
-
func: The decorated function component.
|
|
327
|
-
|
|
328
|
-
Returns:
|
|
329
|
-
A pre-configured `FunctionComponentWrapper` subclass.
|
|
330
|
-
"""
|
|
331
|
-
|
|
332
|
-
if asyncio.iscoroutinefunction(func):
|
|
333
|
-
|
|
334
|
-
class AsyncFCW(AsyncFunctionComponentWrapper[T], func=func): ...
|
|
335
|
-
|
|
336
|
-
return AsyncFCW
|
|
337
|
-
else:
|
|
338
|
-
|
|
339
|
-
class SyncFCW(SyncFunctionComponentWrapper[T], func=func): ... # type: ignore[arg-type]
|
|
340
|
-
|
|
341
|
-
return SyncFCW
|
|
342
|
-
|
|
343
|
-
|
|
344
192
|
# -- Formatting
|
|
345
193
|
|
|
346
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
|
+
"""
|
htmy/md/core.py
CHANGED
|
@@ -4,8 +4,9 @@ from typing import TYPE_CHECKING, ClassVar
|
|
|
4
4
|
|
|
5
5
|
from markdown import Markdown
|
|
6
6
|
|
|
7
|
-
from htmy.core import ContextAware, SafeStr,
|
|
8
|
-
from htmy.
|
|
7
|
+
from htmy.core import ContextAware, SafeStr, Text
|
|
8
|
+
from htmy.snippet import Snippet
|
|
9
|
+
from htmy.typing import TextProcessor, TextResolver
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from collections.abc import Callable
|
|
@@ -78,7 +79,17 @@ class MarkdownParser(ContextAware):
|
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
class MD(Snippet):
|
|
81
|
-
"""
|
|
82
|
+
"""
|
|
83
|
+
Component for reading, customizing, and rendering markdown documents.
|
|
84
|
+
|
|
85
|
+
It supports all the processing utilities of `Snippet`, including `text_resolver` and
|
|
86
|
+
`text_processor` for formatting, token replacement, and slot conversion to components.
|
|
87
|
+
|
|
88
|
+
One note regaring slot convesion (`text_resolver`): it is executed before markdown parsing,
|
|
89
|
+
and all string segments of the resulting component sequence are parsed individually by the
|
|
90
|
+
markdown parser. As a consequence, you should only use slots in places where the preceding
|
|
91
|
+
and following texts individually result in valid markdown.
|
|
92
|
+
"""
|
|
82
93
|
|
|
83
94
|
__slots__ = (
|
|
84
95
|
"_converter",
|
|
@@ -88,6 +99,7 @@ class MD(Snippet):
|
|
|
88
99
|
def __init__(
|
|
89
100
|
self,
|
|
90
101
|
path_or_text: Text | str | Path,
|
|
102
|
+
text_resolver: TextResolver | None = None,
|
|
91
103
|
*,
|
|
92
104
|
converter: Callable[[str], Component] | None = None,
|
|
93
105
|
renderer: MarkdownRenderFunction | None = None,
|
|
@@ -98,6 +110,8 @@ class MD(Snippet):
|
|
|
98
110
|
|
|
99
111
|
Arguments:
|
|
100
112
|
path_or_text: The path where the markdown file is located or a markdown `Text`.
|
|
113
|
+
text_resolver: An optional `TextResolver` (e.g. `Slots`) that converts the processed
|
|
114
|
+
text into a component.
|
|
101
115
|
converter: Function that converts an HTML string (the parsed and processed markdown text)
|
|
102
116
|
into a component.
|
|
103
117
|
renderer: Function that gets the parsed and converted content and the metadata (if it exists)
|
|
@@ -106,7 +120,7 @@ class MD(Snippet):
|
|
|
106
120
|
content before rendering. It can be used for example for token replacement or
|
|
107
121
|
string formatting.
|
|
108
122
|
"""
|
|
109
|
-
super().__init__(path_or_text, text_processor=text_processor)
|
|
123
|
+
super().__init__(path_or_text, text_resolver, text_processor=text_processor)
|
|
110
124
|
self._converter: Callable[[str], Component] = SafeStr if converter is None else converter
|
|
111
125
|
self._renderer = renderer
|
|
112
126
|
|
htmy/renderer/default.py
CHANGED
|
@@ -6,7 +6,8 @@ from collections.abc import Awaitable, Callable, Iterator
|
|
|
6
6
|
from typing import TypeAlias
|
|
7
7
|
|
|
8
8
|
from htmy.core import ErrorBoundary, xml_format_string
|
|
9
|
-
from htmy.typing import Component, ComponentType, Context, ContextProvider
|
|
9
|
+
from htmy.typing import Component, ComponentType, Context, ContextProvider
|
|
10
|
+
from htmy.utils import is_component_sequence
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class _Node:
|
htmy/snippet.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Awaitable, Iterator, Mapping
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .core import SafeStr, Text
|
|
6
|
+
from .io import open_file
|
|
7
|
+
from .typing import (
|
|
8
|
+
Component,
|
|
9
|
+
ComponentType,
|
|
10
|
+
Context,
|
|
11
|
+
TextProcessor,
|
|
12
|
+
TextResolver,
|
|
13
|
+
)
|
|
14
|
+
from .utils import as_component_sequence, as_component_type, is_component_sequence
|
|
15
|
+
|
|
16
|
+
# -- Components and utilities
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Slots:
|
|
20
|
+
"""
|
|
21
|
+
Utility that resolves slots in a string input to components.
|
|
22
|
+
|
|
23
|
+
More technically, it splits a string into slot and non-slot parts, replaces the
|
|
24
|
+
slot parts with the corresponding components (which may be component sequences)
|
|
25
|
+
from the given slot mapping, and returns the resulting component sequence.
|
|
26
|
+
|
|
27
|
+
The default slot placeholder is a standard XML/HTML comment of the following form:
|
|
28
|
+
`<!-- slot[slot-key] -->`. Any number of whitespaces (including 0) are allowed in
|
|
29
|
+
the placeholder, but the slot key must not contain any whitespaces. For details, see
|
|
30
|
+
`Slots.slot_re`.
|
|
31
|
+
|
|
32
|
+
Besides the pre-defined regular expressions in `Slots.slot_re`, any other regular
|
|
33
|
+
expression can be used to identify slots as long as it meets the requirements described
|
|
34
|
+
in `Slots.slots_re`.
|
|
35
|
+
|
|
36
|
+
Implements: `htmy.typing.TextResolver`
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ("_slot_mapping", "_slot_re", "_not_found")
|
|
40
|
+
|
|
41
|
+
class slot_re:
|
|
42
|
+
"""
|
|
43
|
+
Slot regular expressions.
|
|
44
|
+
|
|
45
|
+
Requirements:
|
|
46
|
+
|
|
47
|
+
- The regular expression must have exactly one capturing group that captures the slot key.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
square_bracket = re.compile(r"<!-- *slot *\[ *([^[ ]+) *\] *-->")
|
|
51
|
+
"""
|
|
52
|
+
Slot regular expression that matches slots defined as follows: `<!-- slot[slot-key] -->`.
|
|
53
|
+
|
|
54
|
+
The slot key must not contain any whitespaces and there must not be any additional text
|
|
55
|
+
in the XML/HTML comment. Any number of whitespaces (including 0) are allowed around the
|
|
56
|
+
parts of the slot placeholder.
|
|
57
|
+
"""
|
|
58
|
+
parentheses = re.compile(r"<!-- *slot *\( *([^( ]+) *\) *-->")
|
|
59
|
+
"""
|
|
60
|
+
Slot regular expression that matches slots defined as follows: `<!-- slot(slot-key) -->`.
|
|
61
|
+
|
|
62
|
+
The slot key must not contain any whitespaces and there must not be any additional text
|
|
63
|
+
in the XML/HTML comment. Any number of whitespaces (including 0) are allowed around the
|
|
64
|
+
parts of the slot placeholder.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# There are no defaults for angle bracket and curly braces, because
|
|
68
|
+
# they may conflict with HTML and format strings.
|
|
69
|
+
|
|
70
|
+
default = square_bracket
|
|
71
|
+
"""
|
|
72
|
+
The default slot regular expression. Same as `Slots.slot_re.square_bracket`.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
slot_mapping: Mapping[str, Component],
|
|
78
|
+
*,
|
|
79
|
+
slot_re: re.Pattern[str] = slot_re.default,
|
|
80
|
+
not_found: Component | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Initialization.
|
|
84
|
+
|
|
85
|
+
Slot regular expressions are used to find slot keys in strings, which are then replaced
|
|
86
|
+
with the corresponding component from the slot mapping. `slot_re` must have exactly one
|
|
87
|
+
capturing group that captures the slot key. `Slots.slot_re` contains some predefined slot
|
|
88
|
+
regular expressions, but any other regular expression can be used as long as it matches
|
|
89
|
+
the capturing group requirement above.
|
|
90
|
+
|
|
91
|
+
Arguments:
|
|
92
|
+
slot_mapping: Slot mapping the maps slot keys to the corresponding component.
|
|
93
|
+
slot_re: The slot regular expression that is used to find slot keys in strings.
|
|
94
|
+
not_found: The component that is used to replace slot keys that are not found in
|
|
95
|
+
`slot_mapping`. If `None` and the slot key is not found in `slot_mapping`,
|
|
96
|
+
then a `KeyError` will be raised by `resolve()`.
|
|
97
|
+
"""
|
|
98
|
+
self._slot_mapping = slot_mapping
|
|
99
|
+
self._slot_re = slot_re
|
|
100
|
+
self._not_found = not_found
|
|
101
|
+
|
|
102
|
+
def resolve_text(self, text: str) -> Component:
|
|
103
|
+
"""
|
|
104
|
+
Resolves the given string into components using the instance's slot regular expression
|
|
105
|
+
and slot mapping.
|
|
106
|
+
|
|
107
|
+
Arguments:
|
|
108
|
+
text: The text to resolve.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The component sequence the text resolves to.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
KeyError: If a slot key is not found in the slot mapping and `not_found` is `None`.
|
|
115
|
+
"""
|
|
116
|
+
return tuple(self._resolve_text(text))
|
|
117
|
+
|
|
118
|
+
def _resolve_text(self, text: str) -> Iterator[ComponentType]:
|
|
119
|
+
"""
|
|
120
|
+
Generator that yields the slot and non-slot parts of the given string in order.
|
|
121
|
+
|
|
122
|
+
Arguments:
|
|
123
|
+
text: The text to resolve.
|
|
124
|
+
|
|
125
|
+
Yields:
|
|
126
|
+
The slot and non-slot parts of the given string.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
KeyError: If a slot key is not found in the slot mapping and `not_found` is `None`.
|
|
130
|
+
"""
|
|
131
|
+
is_slot = False
|
|
132
|
+
# The implementation requires that the slot regular expression has exactly one capturing group.
|
|
133
|
+
for part in self._slot_re.split(text):
|
|
134
|
+
if is_slot:
|
|
135
|
+
resolved = self._slot_mapping.get(part, self._not_found)
|
|
136
|
+
if resolved is None:
|
|
137
|
+
raise KeyError(f"Component not found for slot: {part}")
|
|
138
|
+
|
|
139
|
+
if is_component_sequence(resolved):
|
|
140
|
+
yield from resolved
|
|
141
|
+
else:
|
|
142
|
+
# mypy complains that resolved may be a sequence, but that's not the case.
|
|
143
|
+
yield resolved # type: ignore[misc]
|
|
144
|
+
else:
|
|
145
|
+
yield part
|
|
146
|
+
|
|
147
|
+
is_slot = not is_slot
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Snippet:
|
|
151
|
+
"""
|
|
152
|
+
Component that renders text, which may be asynchronously loaded from a file.
|
|
153
|
+
|
|
154
|
+
The entire snippet processing pipeline consists of the following steps:
|
|
155
|
+
|
|
156
|
+
1. The text content is loaded from a file or passed directly as a `Text` instance.
|
|
157
|
+
2. The text content is processed by a `TextProcessor` if provided.
|
|
158
|
+
3. The processed text is converted into a component (may be component sequence)
|
|
159
|
+
by a `TextResolver`, for example `Slots`.
|
|
160
|
+
4. Every `str` children (produced by the steps above) is converted into a `SafeStr` for
|
|
161
|
+
rendering.
|
|
162
|
+
|
|
163
|
+
The pipeline above is a bit abstract, so here are some usage notes:
|
|
164
|
+
|
|
165
|
+
- The text content of a snippet can be a Python format string template, in which case the
|
|
166
|
+
`TextProcessor` can be a simple method that calls `str.format()` with the correct arguments.
|
|
167
|
+
- Alternatively, a text processor can also be used to get only a substring -- commonly referred
|
|
168
|
+
to as fragment in frameworks like Jinja -- of the original text.
|
|
169
|
+
- The text processor is applied before the text resolver, which makes it possible to insert
|
|
170
|
+
placeholders into the text (for example slots, like in this case:
|
|
171
|
+
`..."{toolbar}...".format(toolbar="<!-- slot[toolbar] -->")`) that are then replaced with any
|
|
172
|
+
`htmy.Component` by the `TextResolver` (for example `Slots`).
|
|
173
|
+
- `TextResolver` can return plain `str` values, it is not necessary for it to convert strings
|
|
174
|
+
to `SafeStr` to prevent unwanted escaping.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from datetime import date
|
|
180
|
+
from htmy import Snippet, Slots
|
|
181
|
+
|
|
182
|
+
def text_processor(text: str, context: Context) -> str:
|
|
183
|
+
return text.format(today=date.today())
|
|
184
|
+
|
|
185
|
+
snippet = Snippet(
|
|
186
|
+
"my-page.html",
|
|
187
|
+
text_processor=text_processor,
|
|
188
|
+
text_resolver=Slots(
|
|
189
|
+
{
|
|
190
|
+
"date-picker": MyDatePicker(class_="text-primary"),
|
|
191
|
+
"Toolbar": MyPageToolbar(active_page="home"),
|
|
192
|
+
...
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
In the above example, if `my-page.html` contains a `{today}` placeholder, it will be replaced
|
|
199
|
+
with the current date. If it contains a `<!-- slot[toolbar] -->}` slot, then the `MyPageToolbar`
|
|
200
|
+
`htmy` component instance will be rendered in its place, and the `<!-- slot[date-picker] -->` slot
|
|
201
|
+
will be replaced with the `MyDatePicker` component instance.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
__slots__ = ("_path_or_text", "_text_processor", "_text_resolver")
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
path_or_text: Text | str | Path,
|
|
209
|
+
text_resolver: TextResolver | None = None,
|
|
210
|
+
*,
|
|
211
|
+
text_processor: TextProcessor | None = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Initialization.
|
|
215
|
+
|
|
216
|
+
Arguments:
|
|
217
|
+
path_or_text: The path from where the content should be loaded or a `Text`
|
|
218
|
+
instance if this value should be rendered directly.
|
|
219
|
+
text_resolver: An optional `TextResolver` (e.g. `Slots`) that converts the processed
|
|
220
|
+
text into a component. If not provided, the text will be rendered as a `SafeStr`.
|
|
221
|
+
text_processor: An optional `TextProcessor` that can be used to process the text
|
|
222
|
+
content before rendering. It can be used for example for token replacement or
|
|
223
|
+
string formatting.
|
|
224
|
+
"""
|
|
225
|
+
self._path_or_text = path_or_text
|
|
226
|
+
self._text_processor = text_processor
|
|
227
|
+
self._text_resolver = text_resolver
|
|
228
|
+
|
|
229
|
+
async def htmy(self, context: Context) -> Component:
|
|
230
|
+
"""Renders the component."""
|
|
231
|
+
text = await self._get_text_content()
|
|
232
|
+
if self._text_processor is not None:
|
|
233
|
+
processed = self._text_processor(text, context)
|
|
234
|
+
text = (await processed) if isinstance(processed, Awaitable) else processed
|
|
235
|
+
|
|
236
|
+
if self._text_resolver is None:
|
|
237
|
+
return self._render_text(text, context)
|
|
238
|
+
|
|
239
|
+
comps = as_component_sequence(self._text_resolver.resolve_text(text))
|
|
240
|
+
return tuple(
|
|
241
|
+
as_component_type(self._render_text(c, context)) if isinstance(c, str) else c for c in comps
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def _get_text_content(self) -> str:
|
|
245
|
+
"""Returns the plain text content that should be rendered."""
|
|
246
|
+
path_or_text = self._path_or_text
|
|
247
|
+
|
|
248
|
+
if isinstance(path_or_text, Text):
|
|
249
|
+
return path_or_text
|
|
250
|
+
else:
|
|
251
|
+
async with await open_file(path_or_text, "r") as f:
|
|
252
|
+
return await f.read()
|
|
253
|
+
|
|
254
|
+
def _render_text(self, text: str, context: Context) -> Component:
|
|
255
|
+
"""
|
|
256
|
+
Render function that takes the text that must be rendered and the current rendering context,
|
|
257
|
+
and returns the corresponding component.
|
|
258
|
+
"""
|
|
259
|
+
return SafeStr(text)
|
htmy/typing.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections.abc import Callable, Coroutine, Mapping, MutableMapping
|
|
2
|
-
from typing import Any, Protocol, TypeAlias,
|
|
2
|
+
from typing import Any, Protocol, TypeAlias, TypeVar, runtime_checkable
|
|
3
3
|
|
|
4
4
|
T = TypeVar("T")
|
|
5
5
|
U = TypeVar("U")
|
|
@@ -65,21 +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
|
-
def is_component_sequence(obj: Any) -> TypeGuard[ComponentSequence]:
|
|
70
|
-
"""Returns whether the given object is a component sequence."""
|
|
71
|
-
return isinstance(obj, (list, tuple))
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
SyncFunctionComponent: TypeAlias = Callable[[T, Context], Component]
|
|
75
|
-
"""Protocol definition for sync function components."""
|
|
76
|
-
|
|
77
|
-
AsyncFunctionComponent: TypeAlias = Callable[[T, Context], Coroutine[Any, Any, Component]]
|
|
78
|
-
"""Protocol definition for async function components."""
|
|
79
|
-
|
|
80
|
-
FunctionComponent: TypeAlias = SyncFunctionComponent[T] | AsyncFunctionComponent[T]
|
|
81
|
-
"""Function component type."""
|
|
82
|
-
|
|
83
68
|
# -- Context providers
|
|
84
69
|
|
|
85
70
|
|
|
@@ -102,10 +87,32 @@ class AsyncContextProvider(Protocol):
|
|
|
102
87
|
|
|
103
88
|
|
|
104
89
|
ContextProvider: TypeAlias = SyncContextProvider | AsyncContextProvider
|
|
105
|
-
"""
|
|
90
|
+
"""
|
|
91
|
+
Sync or async context provider type.
|
|
106
92
|
|
|
93
|
+
Components can implement this protocol to add extra data to the rendering context
|
|
94
|
+
of their entire component subtree (including themselves).
|
|
95
|
+
"""
|
|
107
96
|
|
|
108
97
|
# -- Text processors
|
|
109
98
|
|
|
110
99
|
TextProcessor: TypeAlias = Callable[[str, Context], str | Coroutine[Any, Any, str]]
|
|
111
100
|
"""Callable type that expects a string and a context, and returns a processed string."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TextResolver(Protocol):
|
|
104
|
+
"""
|
|
105
|
+
Protocol definition for resolvers that convert a string to a component.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def resolve_text(self, text: str) -> Component:
|
|
109
|
+
"""
|
|
110
|
+
Returns the resolved component for the given text.
|
|
111
|
+
|
|
112
|
+
Arguments:
|
|
113
|
+
text: The text to resolve.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
KeyError: If the text cannot be resolved to a component.
|
|
117
|
+
"""
|
|
118
|
+
...
|
htmy/utils.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Generator
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING, TypeGuard
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
-
from .typing import ComponentSequence, ComponentType
|
|
7
|
+
from .typing import Component, ComponentSequence, ComponentType
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def join_components(
|
|
@@ -42,3 +42,22 @@ def join(*items: str | None, separator: str = " ") -> str:
|
|
|
42
42
|
Joins the given strings with the given separator, skipping `None` values.
|
|
43
43
|
"""
|
|
44
44
|
return separator.join(i for i in items if i)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_component_sequence(comp: Component) -> TypeGuard[ComponentSequence]:
|
|
48
|
+
"""Returns whether the given component is a component sequence."""
|
|
49
|
+
return isinstance(comp, (list, tuple))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def as_component_sequence(comp: Component) -> ComponentSequence:
|
|
53
|
+
"""Returns the given component as a component sequence."""
|
|
54
|
+
# mypy doesn't understand the `is_component_sequence` type guard.
|
|
55
|
+
return comp if is_component_sequence(comp) else (comp,) # type: ignore[return-value]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def as_component_type(comp: Component) -> ComponentType:
|
|
59
|
+
"""Returns the given component as a `ComponentType` (not sequence)."""
|
|
60
|
+
from .core import Fragment
|
|
61
|
+
|
|
62
|
+
# mypy doesn't understand the `is_component_sequence` type guard.
|
|
63
|
+
return comp if not is_component_sequence(comp) else Fragment(*comp) # type: ignore[return-value]
|
|
@@ -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,12 +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.
|
|
41
|
+
- Support for **native HTML/XML** documents with dynamic formatting and **slot rendering**, **without custom syntax**.
|
|
39
42
|
- **Markdown** support with tools for customization.
|
|
40
43
|
- Async, JSON based **internationalization**.
|
|
41
44
|
- Built-in, easy to use `ErrorBoundary` component for graceful error handling.
|
|
@@ -44,6 +47,10 @@ Description-Content-Type: text/markdown
|
|
|
44
47
|
- Automatic and customizable **property-name conversion** from snake case to kebab case.
|
|
45
48
|
- **Fully-typed**.
|
|
46
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
|
+
|
|
47
54
|
## Installation
|
|
48
55
|
|
|
49
56
|
The package is available on PyPI and can be installed with:
|
|
@@ -60,9 +67,9 @@ Also, the library doesn't rely on advanced Python features such as metaclasses o
|
|
|
60
67
|
|
|
61
68
|
### Components
|
|
62
69
|
|
|
63
|
-
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.
|
|
64
71
|
|
|
65
|
-
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.
|
|
66
73
|
|
|
67
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.
|
|
68
75
|
|
|
@@ -161,11 +168,11 @@ user_table = html.table(
|
|
|
161
168
|
`htmy` has a rich set of built-in utilities and components for both HTML and other use-cases:
|
|
162
169
|
|
|
163
170
|
- `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
|
|
171
|
+
- `Snippet` and `Slots`: utilities for creating dynamic, customizable document snippets in their native file format (HTML, XML, Markdown, etc.), with slot rendering support.
|
|
164
172
|
- `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
|
|
165
173
|
- `i18n`: utilities for async, JSON based internationalization.
|
|
166
174
|
- `BaseTag`, `TagWithProps`, `Tag`, `WildcardTag`: base classes for custom XML tags.
|
|
167
175
|
- `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
|
|
168
|
-
- `Snippet`: utility class for loading and customizing document snippets from the file system.
|
|
169
176
|
- `etree.ETreeConverter`: utility that converts XML to a component tree with support for custom HTMY components.
|
|
170
177
|
|
|
171
178
|
### Rendering
|
|
@@ -204,7 +211,7 @@ if __name__ == "__main__":
|
|
|
204
211
|
|
|
205
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".
|
|
206
213
|
|
|
207
|
-
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.
|
|
208
215
|
|
|
209
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.
|
|
210
217
|
|
|
@@ -305,13 +312,13 @@ FastAPI:
|
|
|
305
312
|
|
|
306
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).
|
|
307
314
|
|
|
308
|
-
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.
|
|
309
316
|
|
|
310
|
-
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.
|
|
311
318
|
|
|
312
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).
|
|
313
320
|
|
|
314
|
-
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.
|
|
315
322
|
|
|
316
323
|
## Dependencies
|
|
317
324
|
|
|
@@ -329,7 +336,13 @@ The documentation is built with `mkdocs-material` and `mkdocstrings`.
|
|
|
329
336
|
|
|
330
337
|
## Contributing
|
|
331
338
|
|
|
332
|
-
|
|
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.
|
|
333
346
|
|
|
334
347
|
## License - MIT
|
|
335
348
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
htmy/__init__.py,sha256=Us5P9Y6ZSp38poIz88bsAh2Hxuze5jE3V_uMtMyuH-E,1880
|
|
2
|
+
htmy/core.py,sha256=OoL11j2V-CfePC0dbkC2A5GbdK942b5Huszw3rLo7fc,15124
|
|
3
|
+
htmy/etree.py,sha256=yKxom__AdsJY-Q1kbU0sdTMr0ZF5dMSVBKxayntNFyQ,3062
|
|
4
|
+
htmy/function_component.py,sha256=ff9gTV577SOzxlAiRcMNNIPy70ptYIxr46cC6yu-xi0,7803
|
|
5
|
+
htmy/html.py,sha256=7UohfPRtl-3IoSbOiDxazsSHQpCZ0tyRdNayQISPM8A,21086
|
|
6
|
+
htmy/i18n.py,sha256=brNazQjObBFfbnViZCpcnxa0qgxQbJfX7xJAH-MqTW8,5124
|
|
7
|
+
htmy/io.py,sha256=iebJOZp7L0kZ9SWdqMatKtW5VGRIkEd-eD0_vTAldH8,41
|
|
8
|
+
htmy/md/__init__.py,sha256=lxBJnYplkDuxYuiese6My9KYp1DeGdzo70iUdYTvMnE,334
|
|
9
|
+
htmy/md/core.py,sha256=Xu-8xGAOGqSYLGPOib0Wn-blmyQBHl3MrAOza_w__Y8,4456
|
|
10
|
+
htmy/md/typing.py,sha256=LF-AEvo7FCW2KumyR5l55rsXizV2E4AHVLKFf6lApgM,762
|
|
11
|
+
htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
htmy/renderer/__init__.py,sha256=xnP_aaoK-pTok-69wi8O_xlsgjoKTzWd2lIIeHGcuaY,226
|
|
13
|
+
htmy/renderer/baseline.py,sha256=hHb7CoQhFFdD7Sdw0ltR1-XLGwE9pqmfL5yKFeF2rCg,4288
|
|
14
|
+
htmy/renderer/default.py,sha256=lVMGuRybpFZ0u7pMB3IGOsFxw_rY8KqFQzWcNlmKCVI,10789
|
|
15
|
+
htmy/snippet.py,sha256=dkHEOuULGsgawIMnSz99hghvNu8pLVGAQMQSlrn9ibY,10260
|
|
16
|
+
htmy/typing.py,sha256=0spTpz_JWql2yy_lSlRx0uqgXar7fxwyBqWeIzltvKU,3111
|
|
17
|
+
htmy/utils.py,sha256=Kp0j9G8CBeRiyFGmz-CoDiLtXHfpvHzlTVsWeDhIebM,1935
|
|
18
|
+
htmy-0.6.0.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
|
|
19
|
+
htmy-0.6.0.dist-info/METADATA,sha256=E0Fm8_Vg1MMGHc3bW1YZxZII5YX2R-FOTZYml1zI8PI,18306
|
|
20
|
+
htmy-0.6.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
21
|
+
htmy-0.6.0.dist-info/RECORD,,
|
htmy-0.4.2.dist-info/RECORD
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
htmy/__init__.py,sha256=yMPXQHkXQCjyx7UUVcfsMQ_5YjvNT62Kb9TI1xEcw2A,1899
|
|
2
|
-
htmy/core.py,sha256=Pg3wouHS2fhXBF8q9qYdEDld-HprnGN9ZlXnJeuuark,19601
|
|
3
|
-
htmy/etree.py,sha256=yKxom__AdsJY-Q1kbU0sdTMr0ZF5dMSVBKxayntNFyQ,3062
|
|
4
|
-
htmy/html.py,sha256=7UohfPRtl-3IoSbOiDxazsSHQpCZ0tyRdNayQISPM8A,21086
|
|
5
|
-
htmy/i18n.py,sha256=brNazQjObBFfbnViZCpcnxa0qgxQbJfX7xJAH-MqTW8,5124
|
|
6
|
-
htmy/io.py,sha256=iebJOZp7L0kZ9SWdqMatKtW5VGRIkEd-eD0_vTAldH8,41
|
|
7
|
-
htmy/md/__init__.py,sha256=lxBJnYplkDuxYuiese6My9KYp1DeGdzo70iUdYTvMnE,334
|
|
8
|
-
htmy/md/core.py,sha256=EK-QoB2BIra3o1nvTK0lGP3mQQPtRXuE6zBKF_IWR_o,3675
|
|
9
|
-
htmy/md/typing.py,sha256=LF-AEvo7FCW2KumyR5l55rsXizV2E4AHVLKFf6lApgM,762
|
|
10
|
-
htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
htmy/renderer/__init__.py,sha256=xnP_aaoK-pTok-69wi8O_xlsgjoKTzWd2lIIeHGcuaY,226
|
|
12
|
-
htmy/renderer/baseline.py,sha256=hHb7CoQhFFdD7Sdw0ltR1-XLGwE9pqmfL5yKFeF2rCg,4288
|
|
13
|
-
htmy/renderer/default.py,sha256=rdx-yFYz-cz197xfe9co8Lru2cdZxAjOO4dqY250Y1Q,10767
|
|
14
|
-
htmy/typing.py,sha256=f4QZ8vQL7JfN402yDb8Hq_DYvQS_GUgdXK8-xTBM8y8,3122
|
|
15
|
-
htmy/utils.py,sha256=7_CyA39l2m6jzDqparPKkKgRB2wiGuBZXbiPgiZOXKA,1093
|
|
16
|
-
htmy-0.4.2.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
|
|
17
|
-
htmy-0.4.2.dist-info/METADATA,sha256=5bwi8CxKUwLVHzN0XCwum21pkqfrR_l0UbhCPu2hWE8,16379
|
|
18
|
-
htmy-0.4.2.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
19
|
-
htmy-0.4.2.dist-info/RECORD,,
|
|
File without changes
|