htmy 0.6.0__py3-none-any.whl → 0.7.1__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.
@@ -4,25 +4,22 @@ import asyncio
4
4
  from collections.abc import Callable, Coroutine
5
5
  from typing import Any, Protocol, TypeAlias, overload
6
6
 
7
- from .typing import AsyncComponent, Component, Context, SyncComponent, T
7
+ from .typing import AsyncComponent, Component, Context, SyncComponent
8
+ from .typing import T as TProps
9
+ from .typing import U as TSelf
8
10
 
9
- # -- Typing for "full" function components.
11
+ # -- Typing for "full" function components and context only method components.
10
12
 
11
- _SyncFunctionComponent: TypeAlias = Callable[[T, Context], Component]
13
+ _SyncFunctionComponent: TypeAlias = Callable[[TProps, Context], Component]
12
14
  """
13
15
  Protocol definition for sync function components that have both a properties and a context argument.
14
16
  """
15
17
 
16
- _AsyncFunctionComponent: TypeAlias = Callable[[T, Context], Coroutine[Any, Any, Component]]
18
+ _AsyncFunctionComponent: TypeAlias = Callable[[TProps, Context], Coroutine[Any, Any, Component]]
17
19
  """
18
20
  Protocol definition for async function components that have both a properties and a context argument.
19
21
  """
20
22
 
21
- _FunctionComponent: TypeAlias = _SyncFunctionComponent[T] | _AsyncFunctionComponent[T]
22
- """
23
- Function component type that has both a properties and a context argument.
24
- """
25
-
26
23
  # -- Typing for context-only function components.
27
24
 
28
25
  _ContextOnlySyncFunctionComponent: TypeAlias = Callable[[Context], Component]
@@ -55,22 +52,22 @@ class _DecoratedContextOnlyAsyncFunctionComponent(SyncComponent, Protocol):
55
52
  def __call__(self) -> SyncComponent: ...
56
53
 
57
54
 
58
- _ContextOnlyFunctionComponent: TypeAlias = (
59
- _ContextOnlySyncFunctionComponent | _ContextOnlyAsyncFunctionComponent
60
- )
55
+ # -- Typing for "full" method components.
56
+
57
+ _SyncMethodComponent: TypeAlias = Callable[[TSelf, TProps, Context], Component]
61
58
  """
62
- Function component type that only accepts a context argument.
59
+ Protocol definition for sync method components that have both a properties and a context argument.
63
60
  """
64
61
 
65
- _DecoratedContextOnlyFunction: TypeAlias = (
66
- _DecoratedContextOnlySyncFunctionComponent | _DecoratedContextOnlyAsyncFunctionComponent
67
- )
62
+ _AsyncMethodComponent: TypeAlias = Callable[[TSelf, TProps, Context], Coroutine[Any, Any, Component]]
68
63
  """
69
- Protocol definition for sync or async components that are also callable, and return a sync
70
- or async component when called.
64
+ Protocol definition for async method components that have both a properties and a context argument.
71
65
  """
72
66
 
73
67
 
68
+ # -- Component decorators.
69
+
70
+
74
71
  class ComponentDecorators:
75
72
  """
76
73
  Function component decorators.
@@ -78,18 +75,18 @@ class ComponentDecorators:
78
75
 
79
76
  __slots__ = ()
80
77
 
81
- # -- FunctionComponent decorator.
78
+ # -- Function component decorator.
82
79
 
83
80
  @overload
84
- def __call__(self, func: _SyncFunctionComponent[T]) -> Callable[[T], SyncComponent]: ...
81
+ def __call__(self, func: _SyncFunctionComponent[TProps]) -> Callable[[TProps], SyncComponent]: ...
85
82
 
86
83
  @overload
87
- def __call__(self, func: _AsyncFunctionComponent[T]) -> Callable[[T], AsyncComponent]: ...
84
+ def __call__(self, func: _AsyncFunctionComponent[TProps]) -> Callable[[TProps], AsyncComponent]: ...
88
85
 
89
86
  def __call__(
90
87
  self,
91
- func: _FunctionComponent[T],
92
- ) -> Callable[[T], SyncComponent] | Callable[[T], AsyncComponent]:
88
+ func: _SyncFunctionComponent[TProps] | _AsyncFunctionComponent[TProps],
89
+ ) -> Callable[[TProps], SyncComponent] | Callable[[TProps], AsyncComponent]:
93
90
  """
94
91
  Decorator that converts the decorated function into one that must be called with
95
92
  the function component's properties and returns a component instance.
@@ -104,7 +101,7 @@ class ComponentDecorators:
104
101
  def my_component(props: int, context: Context) -> Component:
105
102
  return html.p(f"Value: {props}")
106
103
 
107
- async def render():
104
+ async def render() -> str:
108
105
  return await Renderer().render(
109
106
  my_component(42)
110
107
  )
@@ -121,7 +118,7 @@ class ComponentDecorators:
121
118
 
122
119
  if asyncio.iscoroutinefunction(func):
123
120
 
124
- def async_wrapper(props: T) -> AsyncComponent:
121
+ def async_wrapper(props: TProps) -> AsyncComponent:
125
122
  # This function must be async, in case the renderer inspects it to decide how to handle it.
126
123
  async def component(context: Context) -> Component:
127
124
  return await func(props, context) # type: ignore[no-any-return]
@@ -132,7 +129,7 @@ class ComponentDecorators:
132
129
  return async_wrapper
133
130
  else:
134
131
 
135
- def sync_wrapper(props: T) -> SyncComponent:
132
+ def sync_wrapper(props: TProps) -> SyncComponent:
136
133
  def component(context: Context) -> Component:
137
134
  return func(props, context) # type: ignore[return-value]
138
135
 
@@ -142,15 +139,15 @@ class ComponentDecorators:
142
139
  return sync_wrapper
143
140
 
144
141
  @overload
145
- def function(self, func: _SyncFunctionComponent[T]) -> Callable[[T], SyncComponent]: ...
142
+ def function(self, func: _SyncFunctionComponent[TProps]) -> Callable[[TProps], SyncComponent]: ...
146
143
 
147
144
  @overload
148
- def function(self, func: _AsyncFunctionComponent[T]) -> Callable[[T], AsyncComponent]: ...
145
+ def function(self, func: _AsyncFunctionComponent[TProps]) -> Callable[[TProps], AsyncComponent]: ...
149
146
 
150
147
  def function(
151
148
  self,
152
- func: _FunctionComponent[T],
153
- ) -> Callable[[T], SyncComponent] | Callable[[T], AsyncComponent]:
149
+ func: _SyncFunctionComponent[TProps] | _AsyncFunctionComponent[TProps],
150
+ ) -> Callable[[TProps], SyncComponent] | Callable[[TProps], AsyncComponent]:
154
151
  """
155
152
  Decorator that converts the decorated function into one that must be called with
156
153
  the function component's properties and returns a component instance.
@@ -167,7 +164,7 @@ class ComponentDecorators:
167
164
  def my_component(props: int, context: Context) -> Component:
168
165
  return html.p(f"Value: {props}")
169
166
 
170
- async def render():
167
+ async def render() -> str:
171
168
  return await Renderer().render(
172
169
  my_component(42)
173
170
  )
@@ -182,7 +179,7 @@ class ComponentDecorators:
182
179
  """
183
180
  return self(func)
184
181
 
185
- # -- ContextOnlyFunctionComponent decorator.
182
+ # -- Context-only function component decorator.
186
183
 
187
184
  @overload
188
185
  def context_only(
@@ -196,7 +193,7 @@ class ComponentDecorators:
196
193
 
197
194
  def context_only(
198
195
  self,
199
- func: _ContextOnlyFunctionComponent,
196
+ func: _ContextOnlySyncFunctionComponent | _ContextOnlyAsyncFunctionComponent,
200
197
  ) -> _DecoratedContextOnlySyncFunctionComponent | _DecoratedContextOnlyAsyncFunctionComponent:
201
198
  """
202
199
  Decorator that converts the decorated function into a component.
@@ -204,19 +201,16 @@ class ComponentDecorators:
204
201
  If used on an async function, the resulting component will also be async;
205
202
  otherwise it will be sync.
206
203
 
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:
204
+ Example:
210
205
 
211
206
  ```python
212
207
  @component.context_only
213
208
  def my_component(ctx):
214
209
  return "Context only function component."
215
210
 
216
- async def render():
211
+ async def render() -> str:
217
212
  return await Renderer().render(
218
- my_component(), # With call signature.
219
- my_component, # Without call signature.
213
+ my_component()
220
214
  )
221
215
  ```
222
216
 
@@ -231,9 +225,139 @@ class ComponentDecorators:
231
225
  func.htmy = func # type: ignore[union-attr]
232
226
  return func # type: ignore[return-value]
233
227
 
228
+ # This assignment adds support for context-only function components without call signature.
234
229
  wrapper.htmy = func # type: ignore[attr-defined]
235
230
  return wrapper # type: ignore[return-value]
236
231
 
232
+ # -- Method component decorator.
233
+
234
+ @overload
235
+ def method(
236
+ self, func: _SyncMethodComponent[TSelf, TProps]
237
+ ) -> Callable[[TSelf, TProps], SyncComponent]: ...
238
+
239
+ @overload
240
+ def method(
241
+ self, func: _AsyncMethodComponent[TSelf, TProps]
242
+ ) -> Callable[[TSelf, TProps], AsyncComponent]: ...
243
+
244
+ def method(
245
+ self,
246
+ func: _SyncMethodComponent[TSelf, TProps] | _AsyncMethodComponent[TSelf, TProps],
247
+ ) -> Callable[[TSelf, TProps], SyncComponent] | Callable[[TSelf, TProps], AsyncComponent]:
248
+ """
249
+ Decorator that converts the decorated method into one that must be called with
250
+ the method component's properties and returns a component instance.
251
+
252
+ If used on an async method, the resulting component will also be async;
253
+ otherwise it will be sync.
254
+
255
+ Example:
256
+
257
+ ```python
258
+ @dataclass
259
+ class MyBusinessObject:
260
+ message: str
261
+
262
+ @component.method
263
+ def paragraph(self, props: int, context: Context) -> Component:
264
+ return html.p(f"{self.message} {props}")
265
+
266
+
267
+ async def render() -> str:
268
+ return await Renderer().render(
269
+ MyBusinessObject("Hi!").paragraph(42)
270
+ )
271
+ ```
272
+
273
+ Arguments:
274
+ func: The decorated method.
275
+
276
+ Returns:
277
+ A method that must be called with the method component's properties and
278
+ returns a component instance. (Or loosly speaking, an `HTMYComponentType` which
279
+ can be "instantiated" with the method component's properties.)
280
+ """
281
+ if asyncio.iscoroutinefunction(func):
282
+
283
+ def async_wrapper(self: TSelf, props: TProps) -> AsyncComponent:
284
+ # This function must be async, in case the renderer inspects it to decide how to handle it.
285
+ async def component(context: Context) -> Component:
286
+ return await func(self, props, context) # type: ignore[no-any-return]
287
+
288
+ component.htmy = component # type: ignore[attr-defined]
289
+ return component # type: ignore[return-value]
290
+
291
+ return async_wrapper
292
+ else:
293
+
294
+ def sync_wrapper(self: TSelf, props: TProps) -> SyncComponent:
295
+ def component(context: Context) -> Component:
296
+ return func(self, props, context) # type: ignore[return-value]
297
+
298
+ component.htmy = component # type: ignore[attr-defined]
299
+ return component # type: ignore[return-value]
300
+
301
+ return sync_wrapper
302
+
303
+ # -- Context-only function component decorator.
304
+
305
+ @overload
306
+ def context_only_method(
307
+ self, func: _SyncFunctionComponent[TSelf]
308
+ ) -> Callable[[TSelf], SyncComponent]: ...
309
+
310
+ @overload
311
+ def context_only_method(
312
+ self, func: _AsyncFunctionComponent[TSelf]
313
+ ) -> Callable[[TSelf], AsyncComponent]: ...
314
+
315
+ def context_only_method(
316
+ self,
317
+ func: _SyncFunctionComponent[TSelf] | _AsyncFunctionComponent[TSelf],
318
+ ) -> Callable[[TSelf], SyncComponent] | Callable[[TSelf], AsyncComponent]:
319
+ """
320
+ Decorator that converts the decorated method into one that must be called
321
+ without any arguments and returns a component instance.
322
+
323
+ If used on an async method, the resulting component will also be async;
324
+ otherwise it will be sync.
325
+
326
+ Example:
327
+
328
+ ```python
329
+ @dataclass
330
+ class MyBusinessObject:
331
+ message: str
332
+
333
+ @component.context_only_method
334
+ def paragraph(self, context: Context) -> Component:
335
+ return html.p(f"{self.message} Goodbye!")
336
+
337
+
338
+ async def render() -> str:
339
+ return await Renderer().render(
340
+ MyBusinessObject("Hello!").paragraph()
341
+ )
342
+ ```
343
+
344
+ Arguments:
345
+ func: The decorated method.
346
+
347
+ Returns:
348
+ A method that must be called without any arguments and returns a component instance.
349
+ (Or loosly speaking, an `HTMYComponentType` which can be "instantiated" by calling
350
+ the method.)
351
+ """
352
+ # A context only method component must be implemented in the same way as
353
+ # a function component. The self argument replaces the props argument
354
+ # and it is added automatically by Python when the method is called.
355
+ # Even the type hint must be the same.
356
+ # This implementation doesn't make the function itself a component though,
357
+ # so the call signature is always necessary (unlike for context-only function
358
+ # components).
359
+ return self(func)
360
+
237
361
 
238
362
  component = ComponentDecorators()
239
363
  """
htmy/renderer/default.py CHANGED
@@ -209,8 +209,9 @@ class _ComponentRenderer:
209
209
  process_node_result(node, result, child_context)
210
210
 
211
211
  if async_todos:
212
- await asyncio.gather(*(process_async_node(n, ctx) for n, ctx in async_todos))
213
- async_todos.clear()
212
+ current_async_todos = async_todos
213
+ self._async_todos = async_todos = deque()
214
+ await asyncio.gather(*(process_async_node(n, ctx) for n, ctx in current_async_todos))
214
215
 
215
216
  if self._error_boundary_todos:
216
217
  await asyncio.gather(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: htmy
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Async, pure-Python rendering engine.
5
5
  License: MIT
6
6
  Author: Peter Volf
@@ -116,7 +116,7 @@ user_table = html.table(
116
116
  )
117
117
  ```
118
118
 
119
- `htmy` also provides a `@component` decorator that can be used on sync or async `my_component(props: MyProps, context: Context) -> Component` functions to convert them into components (preserving the `props` typing).
119
+ `htmy` also provides a powerful `@component` decorator that can be used on sync or async `my_component(props: MyProps, context: Context) -> Component` functions and methods to convert them into components (preserving the `props` typing). You can find out more about this feature in the [Function components](https://volfpeter.github.io/htmy/function-components/) guide.
120
120
 
121
121
  Here is the same example as above, but with function components:
122
122
 
@@ -1,7 +1,7 @@
1
1
  htmy/__init__.py,sha256=Us5P9Y6ZSp38poIz88bsAh2Hxuze5jE3V_uMtMyuH-E,1880
2
2
  htmy/core.py,sha256=OoL11j2V-CfePC0dbkC2A5GbdK942b5Huszw3rLo7fc,15124
3
3
  htmy/etree.py,sha256=yKxom__AdsJY-Q1kbU0sdTMr0ZF5dMSVBKxayntNFyQ,3062
4
- htmy/function_component.py,sha256=ff9gTV577SOzxlAiRcMNNIPy70ptYIxr46cC6yu-xi0,7803
4
+ htmy/function_component.py,sha256=iSp5cGrErmIsc-VfNq053_J2m-Nuu_k2xK9UxvEnlw8,12431
5
5
  htmy/html.py,sha256=7UohfPRtl-3IoSbOiDxazsSHQpCZ0tyRdNayQISPM8A,21086
6
6
  htmy/i18n.py,sha256=brNazQjObBFfbnViZCpcnxa0qgxQbJfX7xJAH-MqTW8,5124
7
7
  htmy/io.py,sha256=iebJOZp7L0kZ9SWdqMatKtW5VGRIkEd-eD0_vTAldH8,41
@@ -11,11 +11,11 @@ htmy/md/typing.py,sha256=LF-AEvo7FCW2KumyR5l55rsXizV2E4AHVLKFf6lApgM,762
11
11
  htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  htmy/renderer/__init__.py,sha256=xnP_aaoK-pTok-69wi8O_xlsgjoKTzWd2lIIeHGcuaY,226
13
13
  htmy/renderer/baseline.py,sha256=hHb7CoQhFFdD7Sdw0ltR1-XLGwE9pqmfL5yKFeF2rCg,4288
14
- htmy/renderer/default.py,sha256=lVMGuRybpFZ0u7pMB3IGOsFxw_rY8KqFQzWcNlmKCVI,10789
14
+ htmy/renderer/default.py,sha256=G6K5YptvH9QvEvMQZdLPtgUblO_zTv4Eo6TETHZDlX8,10869
15
15
  htmy/snippet.py,sha256=dkHEOuULGsgawIMnSz99hghvNu8pLVGAQMQSlrn9ibY,10260
16
16
  htmy/typing.py,sha256=0spTpz_JWql2yy_lSlRx0uqgXar7fxwyBqWeIzltvKU,3111
17
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,,
18
+ htmy-0.7.1.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
19
+ htmy-0.7.1.dist-info/METADATA,sha256=-1VV-vjHWSTWiqNq1bi_E8sKy3PGSLMmauuuKscc70A,18459
20
+ htmy-0.7.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
21
+ htmy-0.7.1.dist-info/RECORD,,
File without changes
File without changes