blu-react 0.0.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.
- blu/__init__.py +74 -0
- blu/_app/__init__.py +463 -0
- blu/_app/client_main.py +159 -0
- blu/_app/render.py +107 -0
- blu/_app/router.py +267 -0
- blu/_cli.py +25 -0
- blu/_exceptions.py +5 -0
- blu/_hooks.py +323 -0
- blu/_http.py +219 -0
- blu/_nodes.py +771 -0
- blu/_server_functions.py +423 -0
- blu/_settings.py +30 -0
- blu/_utils/__init__.py +47 -0
- blu/_utils/asgi.py +189 -0
- blu/_utils/asyncio.py +60 -0
- blu/_utils/client.py +3 -0
- blu/_utils/json.py +30 -0
- blu/_utils/typing.py +33 -0
- blu/html.py +91 -0
- blu/util.js +334 -0
- blu_react-0.0.1.dist-info/METADATA +63 -0
- blu_react-0.0.1.dist-info/RECORD +25 -0
- blu_react-0.0.1.dist-info/WHEEL +4 -0
- blu_react-0.0.1.dist-info/entry_points.txt +2 -0
- blu_react-0.0.1.dist-info/licenses/LICENSE +21 -0
blu/_nodes.py
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
from collections.abc import Callable, Generator
|
|
2
|
+
import importlib
|
|
3
|
+
import inspect
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from numbers import Number
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from blu._exceptions import WrongEnvironmentError
|
|
9
|
+
from blu._utils.typing import Any, Mapping, Protocol, Sequence, cast
|
|
10
|
+
from blu._utils.client import is_client
|
|
11
|
+
|
|
12
|
+
log = getLogger("blu")
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
JSON-serializable primitive value.
|
|
16
|
+
"""
|
|
17
|
+
type JsonPrimitive = bool | Number | str | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
"""JSON-serializable value."""
|
|
21
|
+
type Jsonable = JsonPrimitive | Sequence[Jsonable] | Mapping[str, Jsonable]
|
|
22
|
+
|
|
23
|
+
"""Basic item that can serialized and sent from server to client."""
|
|
24
|
+
type SerializableItem = JsonPrimitive | Node
|
|
25
|
+
|
|
26
|
+
"""Can be serialized and sent from server to client."""
|
|
27
|
+
type Serializable = (
|
|
28
|
+
SerializableItem | Sequence[Serializable] | Mapping[str, Serializable]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
type Node = """
|
|
32
|
+
ClientElement |
|
|
33
|
+
HTMLElement |
|
|
34
|
+
Key |
|
|
35
|
+
Iterable[Node] |
|
|
36
|
+
str |
|
|
37
|
+
int |
|
|
38
|
+
float |
|
|
39
|
+
None
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
"""Valid value for a prop of a `ReactElement`."""
|
|
43
|
+
type PropValue = Jsonable | Node
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_jsonable(item: Any) -> bool:
|
|
47
|
+
if item is None:
|
|
48
|
+
return True
|
|
49
|
+
elif isinstance(item, (bool, Number, str)):
|
|
50
|
+
return True
|
|
51
|
+
elif isinstance(item, Sequence):
|
|
52
|
+
items = cast(Sequence[Any], item)
|
|
53
|
+
return all(is_jsonable(x) for x in items)
|
|
54
|
+
elif isinstance(item, Mapping):
|
|
55
|
+
props = cast(Mapping[str, Any], item)
|
|
56
|
+
return all(is_jsonable(v) for v in props.values())
|
|
57
|
+
else:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_node(item: Any) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
:return: :py:data:`True` if ``item`` is a valid :type:`Node`,
|
|
64
|
+
otherwise :py:data:`False`.
|
|
65
|
+
"""
|
|
66
|
+
node_types = (bool, Number, str, HTMLElement, CustomElement)
|
|
67
|
+
if item is None:
|
|
68
|
+
return True
|
|
69
|
+
elif isinstance(item, node_types):
|
|
70
|
+
return True
|
|
71
|
+
elif isinstance(item, Sequence):
|
|
72
|
+
seq = cast(Sequence[Any], item)
|
|
73
|
+
return all(is_node(x) for x in seq)
|
|
74
|
+
else:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
"""Valid props for a `ReactElement`."""
|
|
79
|
+
type Props = Mapping[str, PropValue]
|
|
80
|
+
|
|
81
|
+
"""Valid children of a `ReactElement`."""
|
|
82
|
+
type Children = list[Node]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HTMLElement:
|
|
86
|
+
"""
|
|
87
|
+
A `React DOM component instance
|
|
88
|
+
<https://react.dev/reference/react-dom/components>`_. Created using
|
|
89
|
+
the :mod:`blu.html` module.
|
|
90
|
+
|
|
91
|
+
.. code-block:: python
|
|
92
|
+
|
|
93
|
+
from blu.html import div
|
|
94
|
+
|
|
95
|
+
div(id='my-id')[
|
|
96
|
+
'Hello World!',
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
.. code-block:: html
|
|
100
|
+
|
|
101
|
+
<div id="my-id">Hello World!</div>
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
_tagname: str
|
|
105
|
+
"""
|
|
106
|
+
The tag name of the element.
|
|
107
|
+
|
|
108
|
+
.. code-block:: python
|
|
109
|
+
|
|
110
|
+
from blu.html import span
|
|
111
|
+
|
|
112
|
+
span.tagname == 'span'
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
_attrs: Props
|
|
116
|
+
"""
|
|
117
|
+
The element's props, minus children.
|
|
118
|
+
|
|
119
|
+
.. code-block:: python
|
|
120
|
+
|
|
121
|
+
from blu.html import div
|
|
122
|
+
|
|
123
|
+
div(id='my-id')['Hello!'].props == {'id': 'my-id'}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
_children: list[Node]
|
|
127
|
+
"""
|
|
128
|
+
The element's children.
|
|
129
|
+
|
|
130
|
+
.. code-block:: python
|
|
131
|
+
|
|
132
|
+
from blu.html import div, span
|
|
133
|
+
|
|
134
|
+
div[span['Hello'], 'Hi.'].children == [span['Hello'], 'Hi.']
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, tagname: str, props: Props, children: list[Node]):
|
|
138
|
+
self._tagname = tagname
|
|
139
|
+
self._attrs = props
|
|
140
|
+
self._children = children
|
|
141
|
+
|
|
142
|
+
def __call__(self, props: Props = {}, /, **kwargs: PropValue) -> "HTMLElement":
|
|
143
|
+
"""
|
|
144
|
+
Create a copy of ``self`` with React props set based on the
|
|
145
|
+
given keyword arguments.
|
|
146
|
+
|
|
147
|
+
.. code-block:: python
|
|
148
|
+
|
|
149
|
+
from blu.html import div
|
|
150
|
+
|
|
151
|
+
div(id='my-div')
|
|
152
|
+
|
|
153
|
+
.. code-block:: html
|
|
154
|
+
|
|
155
|
+
<div id="my-div"></div>
|
|
156
|
+
|
|
157
|
+
:param props: A mapping of prop names to prop values. This is an
|
|
158
|
+
escape hatch for cases when the prop name can't be
|
|
159
|
+
represented as a keyword argument name.
|
|
160
|
+
|
|
161
|
+
:param kwargs: The new props, where the argument name is the
|
|
162
|
+
prop's key and the argument value is the prop's value. This
|
|
163
|
+
is the usual way to specify props.
|
|
164
|
+
|
|
165
|
+
:return: A copy of ``self`` with the same children, but with
|
|
166
|
+
props set as follows:
|
|
167
|
+
|
|
168
|
+
1. For any key-value pair in the ``props`` argument, the
|
|
169
|
+
prop named by the key will be set to the value.
|
|
170
|
+
|
|
171
|
+
For example, ``div(props={'my_prop_': 45})`` results in
|
|
172
|
+
the React element ``<div my_prop_={45} />``
|
|
173
|
+
|
|
174
|
+
2. For any keyword argument, the value will be set to a key
|
|
175
|
+
derived from:
|
|
176
|
+
|
|
177
|
+
- Removing the last trailing underscore (if any).
|
|
178
|
+
- Replacing all other underscores with dashes.
|
|
179
|
+
|
|
180
|
+
For example, ``div(my_prop_=45)`` results in the React
|
|
181
|
+
element ``<div my-prop={45} />``.
|
|
182
|
+
|
|
183
|
+
3. If there is any conflict between the ``props`` argument
|
|
184
|
+
and the keyword arguments, the prop that has the conflict
|
|
185
|
+
will be set based on the ``props`` argument as described
|
|
186
|
+
in (1). Any props that do not have conflicts will be set
|
|
187
|
+
the same way they otherwise would, irrespective of the
|
|
188
|
+
props that do have conflicts.
|
|
189
|
+
|
|
190
|
+
For example,
|
|
191
|
+
``div(props={'props-only': 'props', 'shared': 'props'}, shared='kw', kw_only='kw')``
|
|
192
|
+
results in the React element
|
|
193
|
+
``<div props-only="props" shared="props" kw-only="kw" />``
|
|
194
|
+
"""
|
|
195
|
+
all_props = {
|
|
196
|
+
**props,
|
|
197
|
+
**self._rename_props(kwargs),
|
|
198
|
+
}
|
|
199
|
+
for prop_name in all_props:
|
|
200
|
+
if not is_client and prop_name.startswith("on"):
|
|
201
|
+
raise WrongEnvironmentError(
|
|
202
|
+
f'Could not add attribute "{prop_name}" to {self._tagname} '
|
|
203
|
+
f'element. Event-handling attributes like "{prop_name}" '
|
|
204
|
+
"can only be set in client-side rendering; this code was "
|
|
205
|
+
"run server-side."
|
|
206
|
+
)
|
|
207
|
+
return HTMLElement(
|
|
208
|
+
self._tagname,
|
|
209
|
+
props=all_props,
|
|
210
|
+
children=self._children,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def __getitem__(self, children: Node | tuple[Node, ...]) -> "HTMLElement":
|
|
214
|
+
"""
|
|
215
|
+
Create a copy of ``self`` whose child nodes are set to the items
|
|
216
|
+
passed in.
|
|
217
|
+
|
|
218
|
+
.. code-block:: python
|
|
219
|
+
|
|
220
|
+
from blu.html import div
|
|
221
|
+
|
|
222
|
+
div['Hello World!']
|
|
223
|
+
|
|
224
|
+
.. code-block:: html
|
|
225
|
+
|
|
226
|
+
<div>Hello World!</div>
|
|
227
|
+
|
|
228
|
+
:param children: A :type:`blu.Node` or a :py:class:`tuple` of
|
|
229
|
+
:type:`blu.Node`\\ s.
|
|
230
|
+
:return:
|
|
231
|
+
- If ``children`` is a :type:`blu.Node`: A copy of
|
|
232
|
+
:data:`self` whose children are set to ``[children]``.
|
|
233
|
+
- If ``children`` is a :py:class:`tuple` of
|
|
234
|
+
:type:`blu.Node`\\ s: A copy of :data:`self` whose
|
|
235
|
+
children are set to ``list(children)``.
|
|
236
|
+
"""
|
|
237
|
+
return HTMLElement(
|
|
238
|
+
self._tagname,
|
|
239
|
+
props=self._attrs,
|
|
240
|
+
children=_get_children(children),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def _rename_props(self, props: Props) -> Props:
|
|
244
|
+
return {py_to_html_name(k): v for k, v in props.items()}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _get_children(index: Node | tuple[Node, ...]) -> list[Node]:
|
|
248
|
+
if is_client:
|
|
249
|
+
from pyodide.ffi import JsProxy
|
|
250
|
+
|
|
251
|
+
if isinstance(index, JsProxy):
|
|
252
|
+
index = cast(Node, index.unwrap())
|
|
253
|
+
if isinstance(index, tuple):
|
|
254
|
+
children = list(cast(tuple[Node], index))
|
|
255
|
+
else:
|
|
256
|
+
children = [index]
|
|
257
|
+
return [_convert_to_string(x) for x in children]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _convert_to_string(item: Node) -> Node:
|
|
261
|
+
str_types = (float, bool)
|
|
262
|
+
if is_client:
|
|
263
|
+
from pyodide.ffi import jsnull
|
|
264
|
+
|
|
265
|
+
if item == jsnull:
|
|
266
|
+
return str(item)
|
|
267
|
+
if isinstance(item, str_types):
|
|
268
|
+
return str(item)
|
|
269
|
+
else:
|
|
270
|
+
return item
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class Key:
|
|
274
|
+
"""
|
|
275
|
+
A keyed fragment of React nodes.
|
|
276
|
+
|
|
277
|
+
.. code-block:: python
|
|
278
|
+
|
|
279
|
+
from blu import Key
|
|
280
|
+
from blu.html import b
|
|
281
|
+
|
|
282
|
+
Key('my-id')[
|
|
283
|
+
'Hello, ',
|
|
284
|
+
b['World'],
|
|
285
|
+
'!',
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
.. code-block:: html
|
|
289
|
+
|
|
290
|
+
Hello, <b>World</b>!
|
|
291
|
+
|
|
292
|
+
:param key: A key to be used by React to uniquely identify the
|
|
293
|
+
fragment.
|
|
294
|
+
:return: An empty fragment identified by ``key``.
|
|
295
|
+
|
|
296
|
+
In most cases, you won't need to use :class:`blu.Key`, but it is
|
|
297
|
+
required when rendering an item in an
|
|
298
|
+
:py:class:`Iterable <collections.abc.Iterable>` (unless that
|
|
299
|
+
:py:class:`Iterable <collections.abc.Iterable>` is a :py:class:`str`
|
|
300
|
+
or :py:class:`tuple`):
|
|
301
|
+
|
|
302
|
+
.. code-block:: python
|
|
303
|
+
|
|
304
|
+
from blu import Key
|
|
305
|
+
from blu.html import b
|
|
306
|
+
|
|
307
|
+
PEOPLE = [
|
|
308
|
+
{'id': 1, name: 'Andy'},
|
|
309
|
+
{'id': 2, name: 'Brittany'},
|
|
310
|
+
{'id': 3, name: 'Calvin'},
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
Key(person['id'])[
|
|
314
|
+
'Hello, ',
|
|
315
|
+
b[person['name']],
|
|
316
|
+
'!',
|
|
317
|
+
]
|
|
318
|
+
for person in PEOPLE
|
|
319
|
+
|
|
320
|
+
.. code-block:: html
|
|
321
|
+
|
|
322
|
+
Hello, <b>Andy</b>!
|
|
323
|
+
Hello, <b>Brittany</b>!
|
|
324
|
+
Hello, <b>Calvin</b>!
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
_key: Any
|
|
328
|
+
_children: list[Node]
|
|
329
|
+
|
|
330
|
+
def __init__(self, key: Any):
|
|
331
|
+
self._key = key
|
|
332
|
+
self._children = []
|
|
333
|
+
|
|
334
|
+
def __getitem__(self, children: Node | tuple[Node, ...]) -> "Key":
|
|
335
|
+
"""
|
|
336
|
+
Create a copy of ``self`` with the given children.
|
|
337
|
+
|
|
338
|
+
.. code-block:: python
|
|
339
|
+
|
|
340
|
+
from blu import Key
|
|
341
|
+
from blu.html import b
|
|
342
|
+
|
|
343
|
+
Key('my-id')[
|
|
344
|
+
'Hello, ',
|
|
345
|
+
b['World'],
|
|
346
|
+
'!',
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
.. code-block:: html
|
|
350
|
+
|
|
351
|
+
Hello, <b>World</b>!
|
|
352
|
+
|
|
353
|
+
:param children: A :type:`blu.Node` or :py:class:`tuple` of
|
|
354
|
+
:type:`blu.Node`\\s.
|
|
355
|
+
:return: If ``children`` is a :py:class:`tuple`, a frament whose
|
|
356
|
+
children are ``list(children)``. Otherwise, a fragment whose
|
|
357
|
+
children are ``[children]``.
|
|
358
|
+
"""
|
|
359
|
+
copy = Key(self._key)
|
|
360
|
+
copy._children = _get_children(children)
|
|
361
|
+
return copy
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
type ClientRenderer = Callable[..., Node | Generator[None, Node, Node]]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class Element[**P](Protocol):
|
|
368
|
+
|
|
369
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> "Element[P]": ...
|
|
370
|
+
|
|
371
|
+
def __getitem__(self, *children: "Node") -> "Element[P]": ...
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
UNSET = object()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def noop():
|
|
378
|
+
pass
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class ClientElement:
|
|
382
|
+
"""
|
|
383
|
+
A custom element to be rendered client-side. Created using the
|
|
384
|
+
:func:`blu.client` decorator.
|
|
385
|
+
|
|
386
|
+
.. code-block:: python
|
|
387
|
+
|
|
388
|
+
from blu import client
|
|
389
|
+
from blu.html import b, span
|
|
390
|
+
|
|
391
|
+
__client__ = True
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@client
|
|
395
|
+
def ColorfulText(color, bold):
|
|
396
|
+
colorful_span = span(style={'color': color})[
|
|
397
|
+
(yield)
|
|
398
|
+
]
|
|
399
|
+
if bold:
|
|
400
|
+
return b[colorful_span]
|
|
401
|
+
else:
|
|
402
|
+
return colorful_span
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
ColorfulText('red', bold=True)[
|
|
406
|
+
'Danger! The world said hello back.',
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
.. code-block:: html
|
|
410
|
+
|
|
411
|
+
<b>
|
|
412
|
+
<span style="color: red">
|
|
413
|
+
Danger! The world said hello back.
|
|
414
|
+
</span>
|
|
415
|
+
</b>
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
_args: tuple[Any, ...]
|
|
419
|
+
_kwargs: dict[str, Any]
|
|
420
|
+
_children: list[Node]
|
|
421
|
+
_renderer: ClientRenderer
|
|
422
|
+
_key: Any
|
|
423
|
+
_has_key: bool
|
|
424
|
+
|
|
425
|
+
def __init__(
|
|
426
|
+
self,
|
|
427
|
+
renderer: ClientRenderer,
|
|
428
|
+
args: tuple[Any, ...],
|
|
429
|
+
kwargs: dict[str, Any],
|
|
430
|
+
children: list[Node],
|
|
431
|
+
key: Any,
|
|
432
|
+
has_key: bool,
|
|
433
|
+
):
|
|
434
|
+
self._renderer = renderer
|
|
435
|
+
self._args = args
|
|
436
|
+
self._kwargs = kwargs
|
|
437
|
+
self._children = children
|
|
438
|
+
self._key = key
|
|
439
|
+
self._has_key = has_key
|
|
440
|
+
|
|
441
|
+
def __call__(
|
|
442
|
+
self,
|
|
443
|
+
*args: Any,
|
|
444
|
+
**kwargs: Any,
|
|
445
|
+
) -> "ClientElement":
|
|
446
|
+
"""
|
|
447
|
+
Create a copy of ``self`` that will be rendered with the given
|
|
448
|
+
arguments.
|
|
449
|
+
|
|
450
|
+
.. code-block:: python
|
|
451
|
+
|
|
452
|
+
from blu import client
|
|
453
|
+
from blu.html import div
|
|
454
|
+
|
|
455
|
+
__client__ = True
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@client
|
|
459
|
+
def Greeting(name='World'):
|
|
460
|
+
return div[f'Hello, {name}!']
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
div[
|
|
464
|
+
Greeting('Arnold'),
|
|
465
|
+
Greeting(name='Hailey'),
|
|
466
|
+
Greeting,
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
.. code-block:: html
|
|
470
|
+
|
|
471
|
+
<div>Hello, Arnold!</div>
|
|
472
|
+
<div>Hello, Hailey!</div>
|
|
473
|
+
<div>Hello, World!</div>
|
|
474
|
+
|
|
475
|
+
:param args: The positional arguments to pass into the
|
|
476
|
+
element's render function when it is rendered.
|
|
477
|
+
:param kwargs: The keyword arguments to pass into the
|
|
478
|
+
element's render function when it is rendered, as well as an
|
|
479
|
+
optional keyword argument ``key``, which will be used by
|
|
480
|
+
React to uniquely identify the element when it is rendered.
|
|
481
|
+
Usually, you won't need to pass in a ``key``, but it's
|
|
482
|
+
required for any :py:class:`ClientElement <blu.ClientElement>` that is
|
|
483
|
+
rendered as an item in an
|
|
484
|
+
:py:class:`Iterable <collections.abc.Iterable>` (unless that
|
|
485
|
+
:py:class:`Iterable <collections.abc.Iterable>` is a
|
|
486
|
+
:py:class:`str` or a :py:class:`tuple`).
|
|
487
|
+
|
|
488
|
+
:return: A copy of ``self`` that will be rendered by calling its
|
|
489
|
+
render function with ``*args`` and ``**kwargs``, with the
|
|
490
|
+
exception that the keyword argument ``key`` will not be
|
|
491
|
+
passed on to the render function and will instead be used by
|
|
492
|
+
React to identify the new element. If the original had a
|
|
493
|
+
key, it will not be retained in the copy, and the copy will
|
|
494
|
+
have a key if and only if ``**kwargs`` contains the ``key``
|
|
495
|
+
argument.
|
|
496
|
+
|
|
497
|
+
Below is an example of using the ``key`` argument:
|
|
498
|
+
|
|
499
|
+
.. code-block:: python
|
|
500
|
+
|
|
501
|
+
from blu import client
|
|
502
|
+
from blu.html import div
|
|
503
|
+
|
|
504
|
+
PEOPLE = [
|
|
505
|
+
{'id': 0, 'name': 'Allison'},
|
|
506
|
+
{'id': 1, 'name': 'Bill'},
|
|
507
|
+
{'id': 2, 'name': 'Carrie'},
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@client
|
|
512
|
+
def Greeting(name):
|
|
513
|
+
return div[f'Hello, {name}!']
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
[Greeting(x['name'], key=x['id']) for x in PEOPLE]
|
|
517
|
+
|
|
518
|
+
.. code-block:: html
|
|
519
|
+
|
|
520
|
+
<div>Hello, Allison!</div>
|
|
521
|
+
<div>Hello, Bill!</div>
|
|
522
|
+
<div>Hello, Carrie!</div>
|
|
523
|
+
"""
|
|
524
|
+
kwargs_copy = {**kwargs}
|
|
525
|
+
try:
|
|
526
|
+
del kwargs_copy["key"]
|
|
527
|
+
except KeyError:
|
|
528
|
+
pass
|
|
529
|
+
return ClientElement(
|
|
530
|
+
self._renderer,
|
|
531
|
+
args,
|
|
532
|
+
kwargs,
|
|
533
|
+
self._children,
|
|
534
|
+
key=kwargs.get("key", None),
|
|
535
|
+
has_key="key" in kwargs,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def __getitem__(self, children: Node) -> "ClientElement":
|
|
539
|
+
"""
|
|
540
|
+
Create a copy of ``self`` that will render with the given
|
|
541
|
+
children displayed where the render function uses the ``yield``
|
|
542
|
+
keyword.
|
|
543
|
+
|
|
544
|
+
.. code-block:: python
|
|
545
|
+
|
|
546
|
+
from blu import client
|
|
547
|
+
from blu.html import span
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@client
|
|
551
|
+
def RedText(text):
|
|
552
|
+
return span(style={'color': 'red'})[(yield)]
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
RedText['Danger! This text is red.']
|
|
556
|
+
|
|
557
|
+
.. code-block:: html
|
|
558
|
+
|
|
559
|
+
<span style="color: red">Danger! This text is red.</span>
|
|
560
|
+
|
|
561
|
+
:param children: Any valid :type:`blu.Node` or a
|
|
562
|
+
:py:class:`tuple` of :type:`blu.Node`\\s.
|
|
563
|
+
:return: A copy of ``self`` that will render with ``children``
|
|
564
|
+
displayed where the element's render function uses the
|
|
565
|
+
``yield`` keyword. If no children were specified, nothing
|
|
566
|
+
will be rendered where the ``yield`` keyword is used.
|
|
567
|
+
:raises: :py:class:`TypeError` if element's rendering function
|
|
568
|
+
is not a :py:class:`Generator <collections.abc.Generator>`,
|
|
569
|
+
i.e. doesn't contain a ``yield`` statement.
|
|
570
|
+
"""
|
|
571
|
+
if not inspect.isgeneratorfunction(self._renderer):
|
|
572
|
+
log.info("INFO")
|
|
573
|
+
log.debug(f"Non-generator function: {self._renderer}")
|
|
574
|
+
raise TypeError("This element does not accept any children.")
|
|
575
|
+
log.error(f"ClientElement children: {children}")
|
|
576
|
+
return ClientElement(
|
|
577
|
+
self._renderer,
|
|
578
|
+
self._args,
|
|
579
|
+
self._kwargs,
|
|
580
|
+
_get_children(children),
|
|
581
|
+
key=self._key,
|
|
582
|
+
has_key=self._has_key,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
def _get_key(self):
|
|
586
|
+
if not self._has_key:
|
|
587
|
+
raise LookupError("This element has no key.")
|
|
588
|
+
return self._key
|
|
589
|
+
|
|
590
|
+
def __reduce__(self):
|
|
591
|
+
module = self._renderer.__module__
|
|
592
|
+
name = self._renderer.__name__
|
|
593
|
+
return (
|
|
594
|
+
_import_client_module,
|
|
595
|
+
(
|
|
596
|
+
module,
|
|
597
|
+
name,
|
|
598
|
+
self._args,
|
|
599
|
+
self._kwargs,
|
|
600
|
+
self._children,
|
|
601
|
+
self._key,
|
|
602
|
+
self._has_key,
|
|
603
|
+
),
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _import_client_module(
|
|
608
|
+
module: str,
|
|
609
|
+
name: str,
|
|
610
|
+
args: tuple[Any],
|
|
611
|
+
kwargs: dict[str, Any],
|
|
612
|
+
children: list[Any],
|
|
613
|
+
key: Any,
|
|
614
|
+
has_key: bool,
|
|
615
|
+
):
|
|
616
|
+
base = getattr(importlib.import_module(module), name)
|
|
617
|
+
if has_key:
|
|
618
|
+
all_kwargs = {**kwargs, "key": key}
|
|
619
|
+
else:
|
|
620
|
+
all_kwargs = kwargs
|
|
621
|
+
with_args = base(*args, **all_kwargs)
|
|
622
|
+
if children:
|
|
623
|
+
return with_args[children]
|
|
624
|
+
else:
|
|
625
|
+
return with_args
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def py_to_html_name(py_name: str) -> str:
|
|
629
|
+
return re.sub("_$", "", py_name).replace("_", "-")
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def create_html_element(tagname: str) -> HTMLElement:
|
|
633
|
+
"""
|
|
634
|
+
Create an HTML element. Usually, you will use the :mod:`blu.html`
|
|
635
|
+
module to create HTML elements, but in rare cases where you cannot
|
|
636
|
+
import the tag name you need from :mod:`blu.html`, you can use this
|
|
637
|
+
function to create an HTML element from any valid tag name.
|
|
638
|
+
|
|
639
|
+
.. code-block:: python
|
|
640
|
+
|
|
641
|
+
from blu import create_rare_html_element
|
|
642
|
+
|
|
643
|
+
my_element = create_rare_html_element('my_element-')
|
|
644
|
+
|
|
645
|
+
my_element(id='my-id')['Hello, World!']
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
.. code-block:: html
|
|
649
|
+
|
|
650
|
+
<my_element->Hello, World!</my_element->
|
|
651
|
+
|
|
652
|
+
:param tagname: A valid HTML tag name.
|
|
653
|
+
:return: A :class:`blu.HTMLElement` whose tag name is ``tagname``,
|
|
654
|
+
with no props set.
|
|
655
|
+
"""
|
|
656
|
+
return HTMLElement(tagname, {}, [])
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
if is_client:
|
|
660
|
+
from pyscript.ffi import create_proxy # type: ignore
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def client(renderer: ClientRenderer) -> ClientElement:
|
|
664
|
+
"""
|
|
665
|
+
Decorator that converts a rendering function into a
|
|
666
|
+
:class:`blu.ClientElement`.
|
|
667
|
+
|
|
668
|
+
.. code-block:: python
|
|
669
|
+
|
|
670
|
+
from blu import client
|
|
671
|
+
from blu.html import span
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@client
|
|
675
|
+
def ColoredText(color):
|
|
676
|
+
return span(style={'color': color})[(yield)]
|
|
677
|
+
|
|
678
|
+
:param renderer: The function that should be used to render the
|
|
679
|
+
:class:`ClientElement <blu.ClientElement>`.
|
|
680
|
+
|
|
681
|
+
:return: An element that will be rendered client-side using
|
|
682
|
+
``renderer``.
|
|
683
|
+
|
|
684
|
+
When a :class:`ClientElement <blu.ClientElement>` is rendered, the
|
|
685
|
+
rendering function it was created from is called with whatever
|
|
686
|
+
arguments were passed in using :meth:`ClientElement.__call__()
|
|
687
|
+
<blu.ClientElement.__call__>`:
|
|
688
|
+
|
|
689
|
+
.. code-block:: python
|
|
690
|
+
|
|
691
|
+
from blu import client
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@client
|
|
695
|
+
def Greeting(name='World'):
|
|
696
|
+
return f'Hello, {name}!'
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
Greeting('Gary')
|
|
700
|
+
|
|
701
|
+
.. code-block:: html
|
|
702
|
+
|
|
703
|
+
Hello, Gary!
|
|
704
|
+
|
|
705
|
+
If arguments were never set using :meth:`ClientElement.__call__()
|
|
706
|
+
<blu.ClientElement.__call__>`, then the render function will be
|
|
707
|
+
called with no arguments:
|
|
708
|
+
|
|
709
|
+
.. code-block:: python
|
|
710
|
+
|
|
711
|
+
from blu import client
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
@client
|
|
715
|
+
def Greeting(name='World'):
|
|
716
|
+
return f'Hello, {name}!'
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
Greeting
|
|
720
|
+
|
|
721
|
+
.. code-block:: html
|
|
722
|
+
|
|
723
|
+
Hello, World!
|
|
724
|
+
|
|
725
|
+
A render function may use the yield keyword (only once) to get the
|
|
726
|
+
children set using :meth:`ClientElement.__getitem__()
|
|
727
|
+
<blu.ClientElement.__getitem__>`:
|
|
728
|
+
|
|
729
|
+
.. code-block:: python
|
|
730
|
+
|
|
731
|
+
from blu import client
|
|
732
|
+
from blu.html import span
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
@client
|
|
736
|
+
def RedText():
|
|
737
|
+
return span(style={'color': 'red'})[(yield)]
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
RedText['Danger!']
|
|
741
|
+
|
|
742
|
+
.. code-block:: html
|
|
743
|
+
|
|
744
|
+
<span style='color: red'>Danger!</span>
|
|
745
|
+
|
|
746
|
+
If the element's children were never set using
|
|
747
|
+
:meth:`ClientElement.__getitem__() <blu.ClientElement.__getitem__>`,
|
|
748
|
+
it won't have any children:
|
|
749
|
+
|
|
750
|
+
.. code-block:: python
|
|
751
|
+
|
|
752
|
+
from blu import client
|
|
753
|
+
from blu.html import span
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@client
|
|
757
|
+
def RedText():
|
|
758
|
+
return span(style={'color': 'red'})[(yield)]
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
RedText
|
|
762
|
+
|
|
763
|
+
.. code-block:: html
|
|
764
|
+
|
|
765
|
+
<span style='color: red'></span>
|
|
766
|
+
|
|
767
|
+
"""
|
|
768
|
+
element = ClientElement(renderer, (), {}, [], None, False)
|
|
769
|
+
if is_client:
|
|
770
|
+
create_proxy(element) # type: ignore
|
|
771
|
+
return element
|