fastlifeweb 0.9.7__py3-none-any.whl → 0.11.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.
Files changed (90) hide show
  1. fastlife/__init__.py +2 -2
  2. fastlife/config/__init__.py +13 -0
  3. fastlife/{configurator → config}/configurator.py +64 -41
  4. fastlife/{configurator → config}/registry.py +2 -10
  5. fastlife/{configurator → config}/settings.py +7 -3
  6. fastlife/middlewares/__init__.py +7 -0
  7. fastlife/middlewares/base.py +24 -0
  8. fastlife/middlewares/reverse_proxy/__init__.py +16 -0
  9. fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -14
  10. fastlife/middlewares/session/__init__.py +16 -0
  11. fastlife/middlewares/session/middleware.py +6 -1
  12. fastlife/middlewares/session/serializer.py +21 -0
  13. fastlife/request/__init__.py +5 -0
  14. fastlife/request/{model_result.py → form.py} +21 -9
  15. fastlife/request/form_data.py +28 -3
  16. fastlife/request/request.py +18 -0
  17. fastlife/routing/__init__.py +7 -0
  18. fastlife/routing/route.py +45 -0
  19. fastlife/routing/router.py +12 -4
  20. fastlife/security/__init__.py +1 -0
  21. fastlife/security/csrf.py +29 -11
  22. fastlife/security/policy.py +6 -2
  23. fastlife/shared_utils/__init__.py +1 -0
  24. fastlife/shared_utils/infer.py +7 -0
  25. fastlife/shared_utils/resolver.py +10 -2
  26. fastlife/templates/A.jinja +33 -9
  27. fastlife/templates/Button.jinja +55 -32
  28. fastlife/templates/Checkbox.jinja +20 -6
  29. fastlife/templates/CsrfToken.jinja +4 -0
  30. fastlife/templates/Details.jinja +31 -3
  31. fastlife/templates/Form.jinja +45 -7
  32. fastlife/templates/H1.jinja +14 -1
  33. fastlife/templates/H2.jinja +14 -1
  34. fastlife/templates/H3.jinja +14 -1
  35. fastlife/templates/H4.jinja +14 -1
  36. fastlife/templates/H5.jinja +14 -1
  37. fastlife/templates/H6.jinja +14 -1
  38. fastlife/templates/Hidden.jinja +3 -3
  39. fastlife/templates/Input.jinja +21 -8
  40. fastlife/templates/Label.jinja +18 -2
  41. fastlife/templates/Option.jinja +14 -2
  42. fastlife/templates/P.jinja +14 -2
  43. fastlife/templates/Radio.jinja +34 -12
  44. fastlife/templates/Select.jinja +15 -4
  45. fastlife/templates/Summary.jinja +13 -2
  46. fastlife/templates/Table.jinja +12 -1
  47. fastlife/templates/Tbody.jinja +11 -1
  48. fastlife/templates/Td.jinja +12 -1
  49. fastlife/templates/Textarea.jinja +18 -0
  50. fastlife/templates/Tfoot.jinja +11 -1
  51. fastlife/templates/Th.jinja +12 -1
  52. fastlife/templates/Thead.jinja +11 -1
  53. fastlife/templates/Tr.jinja +11 -1
  54. fastlife/templates/pydantic_form/Boolean.jinja +3 -2
  55. fastlife/templates/pydantic_form/Checklist.jinja +3 -5
  56. fastlife/templates/pydantic_form/Dropdown.jinja +3 -2
  57. fastlife/templates/pydantic_form/Error.jinja +4 -3
  58. fastlife/templates/pydantic_form/Hidden.jinja +2 -1
  59. fastlife/templates/pydantic_form/Hint.jinja +2 -1
  60. fastlife/templates/pydantic_form/Model.jinja +16 -3
  61. fastlife/templates/pydantic_form/Sequence.jinja +15 -6
  62. fastlife/templates/pydantic_form/Text.jinja +2 -2
  63. fastlife/templates/pydantic_form/Textarea.jinja +32 -0
  64. fastlife/templates/pydantic_form/Union.jinja +7 -1
  65. fastlife/templates/pydantic_form/Widget.jinja +6 -3
  66. fastlife/templating/binding.py +18 -4
  67. fastlife/templating/renderer/__init__.py +3 -1
  68. fastlife/templating/renderer/abstract.py +21 -8
  69. fastlife/templating/renderer/constants.py +82 -0
  70. fastlife/templating/renderer/jinjax.py +269 -6
  71. fastlife/templating/renderer/widgets/base.py +43 -7
  72. fastlife/templating/renderer/widgets/boolean.py +21 -0
  73. fastlife/templating/renderer/widgets/checklist.py +23 -0
  74. fastlife/templating/renderer/widgets/dropdown.py +22 -2
  75. fastlife/templating/renderer/widgets/factory.py +100 -29
  76. fastlife/templating/renderer/widgets/hidden.py +16 -0
  77. fastlife/templating/renderer/widgets/model.py +7 -1
  78. fastlife/templating/renderer/widgets/sequence.py +8 -6
  79. fastlife/templating/renderer/widgets/text.py +80 -4
  80. fastlife/templating/renderer/widgets/union.py +25 -2
  81. fastlife/testing/testclient.py +3 -3
  82. fastlife/views/pydantic_form.py +2 -2
  83. {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/METADATA +4 -9
  84. {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/RECORD +86 -84
  85. fastlife/configurator/__init__.py +0 -4
  86. fastlife/configurator/base.py +0 -9
  87. fastlife/configurator/route_handler.py +0 -29
  88. fastlife/templates/__init__.py +0 -0
  89. {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/LICENSE +0 -0
  90. {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/WHEEL +0 -0
@@ -1,3 +1,7 @@
1
+ """
2
+ Widget for field of type Set.
3
+ """
4
+
1
5
  from typing import Sequence
2
6
 
3
7
  from pydantic import BaseModel, Field
@@ -6,6 +10,8 @@ from .base import Widget
6
10
 
7
11
 
8
12
  class Checkable(BaseModel):
13
+ """A checkable field from a checklist."""
14
+
9
15
  label: str
10
16
  name: str
11
17
  value: str
@@ -24,11 +30,26 @@ class Checkable(BaseModel):
24
30
 
25
31
 
26
32
  class ChecklistWidget(Widget[Sequence[Checkable]]):
33
+ """
34
+ Widget for field of type Set.
35
+
36
+ :param name: field name.
37
+ :param title: title for the widget.
38
+ :param hint: hint for human.
39
+ :param aria_label: html input aria-label value.
40
+ :param value: current value.
41
+ :param error: error of the value if any.
42
+ :param removable: display a button to remove the widget for optional fields.
43
+ :param token: token used to get unique id on the form.
44
+ """
45
+
27
46
  def __init__(
28
47
  self,
29
48
  name: str,
30
49
  *,
31
50
  title: str | None,
51
+ hint: str | None = None,
52
+ aria_label: str | None = None,
32
53
  value: Sequence[Checkable],
33
54
  error: str | None = None,
34
55
  token: str,
@@ -40,6 +61,8 @@ class ChecklistWidget(Widget[Sequence[Checkable]]):
40
61
  error=error,
41
62
  token=token,
42
63
  title=title,
64
+ hint=hint,
65
+ aria_label=aria_label,
43
66
  removable=removable,
44
67
  )
45
68
 
@@ -1,20 +1,39 @@
1
+ """
2
+ Widget for field of type Enum or Literal.
3
+ """
4
+
1
5
  from typing import Optional, Sequence, Tuple
2
6
 
3
7
  from .base import Widget
4
8
 
5
9
 
6
10
  class DropDownWidget(Widget[str]):
11
+ """
12
+ Widget for field of type Enum or Literal.
13
+
14
+ :param name: field name.
15
+ :param title: title for the widget.
16
+ :param hint: hint for human.
17
+ :param aria_label: html input aria-label value.
18
+ :param value: current value.
19
+ :param error: error of the value if any.
20
+ :param options: List of possible values.
21
+ :param removable: display a button to remove the widget for optional fields.
22
+ :param token: token used to get unique id on the form.
23
+ """
24
+
7
25
  def __init__(
8
26
  self,
9
27
  name: str,
10
28
  *,
11
29
  title: Optional[str],
30
+ hint: Optional[str] = None,
31
+ aria_label: Optional[str] = None,
12
32
  value: Optional[str] = None,
13
33
  error: str | None = None,
14
34
  options: Sequence[Tuple[str, str]] | Sequence[str],
15
35
  removable: bool = False,
16
36
  token: Optional[str] = None,
17
- hint: Optional[str] = None,
18
37
  ) -> None:
19
38
  super().__init__(
20
39
  name,
@@ -23,6 +42,8 @@ class DropDownWidget(Widget[str]):
23
42
  title=title,
24
43
  token=token,
25
44
  removable=removable,
45
+ hint=hint,
46
+ aria_label=aria_label,
26
47
  )
27
48
  self.options: list[dict[str, str]] = []
28
49
  for opt in options:
@@ -30,7 +51,6 @@ class DropDownWidget(Widget[str]):
30
51
  self.options.append({"value": opt[0], "text": opt[1]})
31
52
  else:
32
53
  self.options.append({"value": opt, "text": opt})
33
- self.hint = hint
34
54
 
35
55
  def get_template(self) -> str:
36
56
  return "pydantic_form.Dropdown"
@@ -1,39 +1,49 @@
1
+ """
2
+ Transform.
3
+ """
4
+
1
5
  import secrets
2
6
  from collections.abc import MutableSequence, Sequence
3
7
  from decimal import Decimal
4
8
  from enum import Enum
5
9
  from inspect import isclass
6
10
  from types import NoneType
7
- from typing import Any, Literal, Mapping, Optional, Type, cast, get_origin
11
+ from typing import Any, Literal, Mapping, Type, cast, get_origin
8
12
  from uuid import UUID
9
13
 
10
14
  from markupsafe import Markup
11
15
  from pydantic import BaseModel, EmailStr, SecretStr, ValidationError
12
16
  from pydantic.fields import FieldInfo
13
17
 
14
- from fastlife.request.model_result import ModelResult
18
+ from fastlife.request.form import FormModel
15
19
  from fastlife.shared_utils.infer import is_complex_type, is_union
16
20
  from fastlife.templating.renderer.abstract import AbstractTemplateRenderer
21
+ from fastlife.templating.renderer.widgets.base import Widget
17
22
  from fastlife.templating.renderer.widgets.boolean import BooleanWidget
18
23
  from fastlife.templating.renderer.widgets.checklist import Checkable, ChecklistWidget
19
24
  from fastlife.templating.renderer.widgets.dropdown import DropDownWidget
20
25
  from fastlife.templating.renderer.widgets.hidden import HiddenWidget
26
+ from fastlife.templating.renderer.widgets.model import ModelWidget
21
27
  from fastlife.templating.renderer.widgets.sequence import SequenceWidget
22
-
23
- from .base import Widget, get_title
24
- from .model import ModelWidget
25
- from .text import TextWidget
26
- from .union import UnionWidget
28
+ from fastlife.templating.renderer.widgets.text import TextWidget
29
+ from fastlife.templating.renderer.widgets.union import UnionWidget
27
30
 
28
31
 
29
32
  class WidgetFactory:
30
- def __init__(self, renderer: AbstractTemplateRenderer, token: Optional[str] = None):
33
+ """
34
+ Form builder for pydantic model.
35
+
36
+ :param renderer: template engine to render widget.
37
+ :param token: reuse a token.
38
+ """
39
+
40
+ def __init__(self, renderer: AbstractTemplateRenderer, token: str | None = None):
31
41
  self.renderer = renderer
32
42
  self.token = token or secrets.token_urlsafe(4).replace("_", "-")
33
43
 
34
44
  def get_markup(
35
45
  self,
36
- model: ModelResult[Any],
46
+ model: FormModel[Any],
37
47
  *,
38
48
  removable: bool = False,
39
49
  field: FieldInfo | None = None,
@@ -86,7 +96,12 @@ class WidgetFactory:
86
96
  value=value,
87
97
  removable=removable,
88
98
  title=field.title if field else "",
89
- aria_label=field.description if field else None,
99
+ hint=field.description if field else None,
100
+ aria_label=(
101
+ field.json_schema_extra.get("aria_label") # type:ignore
102
+ if field and field.json_schema_extra
103
+ else None
104
+ ),
90
105
  token=self.token,
91
106
  error=form_errors.get(name),
92
107
  ),
@@ -148,24 +163,24 @@ class WidgetFactory:
148
163
  self,
149
164
  field_name: str,
150
165
  typ: Type[BaseModel],
151
- field: Optional[FieldInfo],
166
+ field: FieldInfo | None,
152
167
  value: Mapping[str, Any],
153
168
  form_errors: Mapping[str, Any],
154
169
  removable: bool,
155
170
  ) -> Widget[Any]:
156
171
  ret: dict[str, Any] = {}
157
- for key, field in typ.model_fields.items():
172
+ for key, child_field in typ.model_fields.items():
158
173
  child_key = f"{field_name}.{key}" if field_name else key
159
- if field.exclude:
174
+ if child_field.exclude:
160
175
  continue
161
- if field.annotation is None:
176
+ if child_field.annotation is None:
162
177
  raise ValueError( # coverage: ignore
163
- f"Missing annotation for {field} in {child_key}"
178
+ f"Missing annotation for {child_field} in {child_key}"
164
179
  )
165
180
  ret[key] = self.build(
166
- field.annotation,
181
+ child_field.annotation,
167
182
  name=child_key,
168
- field=field,
183
+ field=child_field,
169
184
  value=value.get(key),
170
185
  form_errors=form_errors,
171
186
  removable=False,
@@ -174,16 +189,23 @@ class WidgetFactory:
174
189
  field_name,
175
190
  value=list(ret.values()),
176
191
  removable=removable,
177
- title=get_title(typ),
192
+ title=field.title if field and field.title else "",
193
+ hint=field.description if field else None,
194
+ aria_label=(
195
+ field.json_schema_extra.get("aria_label") # type:ignore
196
+ if field and field.json_schema_extra
197
+ else None
198
+ ),
178
199
  token=self.token,
179
200
  error=form_errors.get(field_name),
201
+ nested=field is not None,
180
202
  )
181
203
 
182
204
  def build_union(
183
205
  self,
184
206
  field_name: str,
185
207
  field_type: Type[Any],
186
- field: Optional[FieldInfo],
208
+ field: FieldInfo | None,
187
209
  value: Any,
188
210
  form_errors: Mapping[str, Any],
189
211
  removable: bool,
@@ -233,6 +255,12 @@ class WidgetFactory:
233
255
  value=child,
234
256
  children_types=types, # type: ignore
235
257
  title=field.title if field else "",
258
+ hint=field.description if field else None,
259
+ aria_label=(
260
+ field.json_schema_extra.get("aria_label") # type:ignore
261
+ if field and field.json_schema_extra
262
+ else None
263
+ ),
236
264
  token=self.token,
237
265
  removable=removable,
238
266
  error=form_errors.get(field_name),
@@ -244,8 +272,8 @@ class WidgetFactory:
244
272
  self,
245
273
  field_name: str,
246
274
  field_type: Type[Any],
247
- field: Optional[FieldInfo],
248
- value: Optional[Sequence[Any]],
275
+ field: FieldInfo | None,
276
+ value: Sequence[Any] | None,
249
277
  form_errors: Mapping[str, Any],
250
278
  removable: bool,
251
279
  ) -> Widget[Any]:
@@ -264,8 +292,13 @@ class WidgetFactory:
264
292
  ]
265
293
  return SequenceWidget(
266
294
  field_name,
267
- hint=field.description if field else "",
268
295
  title=field.title if field else "",
296
+ hint=field.description if field else None,
297
+ aria_label=(
298
+ field.json_schema_extra.get("aria_label") # type:ignore
299
+ if field and field.json_schema_extra
300
+ else None
301
+ ),
269
302
  value=items,
270
303
  item_type=typ, # type: ignore
271
304
  token=self.token,
@@ -277,8 +310,8 @@ class WidgetFactory:
277
310
  self,
278
311
  field_name: str,
279
312
  field_type: Type[Any],
280
- field: Optional[FieldInfo],
281
- value: Optional[Sequence[Any]],
313
+ field: FieldInfo | None,
314
+ value: Sequence[Any] | None,
282
315
  form_errors: Mapping[str, Any],
283
316
  removable: bool,
284
317
  ) -> Widget[Any]:
@@ -320,6 +353,12 @@ class WidgetFactory:
320
353
  return ChecklistWidget(
321
354
  field_name,
322
355
  title=field.title if field else "",
356
+ hint=field.description if field else None,
357
+ aria_label=(
358
+ field.json_schema_extra.get("aria_label") # type:ignore
359
+ if field and field.json_schema_extra
360
+ else None
361
+ ),
323
362
  token=self.token,
324
363
  value=choices,
325
364
  removable=removable,
@@ -339,6 +378,12 @@ class WidgetFactory:
339
378
  field_name,
340
379
  removable=removable,
341
380
  title=field.title if field else "",
381
+ hint=field.description if field else None,
382
+ aria_label=(
383
+ field.json_schema_extra.get("aria_label") # type:ignore
384
+ if field and field.json_schema_extra
385
+ else None
386
+ ),
342
387
  token=self.token,
343
388
  value=value,
344
389
  error=form_errors.get(field_name),
@@ -355,11 +400,16 @@ class WidgetFactory:
355
400
  ) -> Widget[Any]:
356
401
  return TextWidget(
357
402
  field_name,
358
- hint=field.description if field else "",
359
403
  input_type="email",
360
404
  placeholder=str(field.examples[0]) if field and field.examples else None,
361
405
  removable=removable,
362
406
  title=field.title if field else "",
407
+ hint=field.description if field else None,
408
+ aria_label=(
409
+ field.json_schema_extra.get("aria_label") # type:ignore
410
+ if field and field.json_schema_extra
411
+ else None
412
+ ),
363
413
  token=self.token,
364
414
  value=str(value),
365
415
  error=form_errors.get(field_name),
@@ -376,11 +426,16 @@ class WidgetFactory:
376
426
  ) -> Widget[Any]:
377
427
  return TextWidget(
378
428
  field_name,
379
- hint=field.description if field else "",
380
429
  input_type="password",
381
430
  placeholder=str(field.examples[0]) if field and field.examples else None,
382
431
  removable=removable,
383
432
  title=field.title if field else "",
433
+ hint=field.description if field else None,
434
+ aria_label=(
435
+ field.json_schema_extra.get("aria_label") # type:ignore
436
+ if field and field.json_schema_extra
437
+ else None
438
+ ),
384
439
  token=self.token,
385
440
  value=value.get_secret_value() if isinstance(value, SecretStr) else value,
386
441
  error=form_errors.get(field_name),
@@ -407,6 +462,12 @@ class WidgetFactory:
407
462
  options=choices,
408
463
  removable=removable,
409
464
  title=field.title if field else "",
465
+ hint=field.description if field else None,
466
+ aria_label=(
467
+ field.json_schema_extra.get("aria_label") # type:ignore
468
+ if field and field.json_schema_extra
469
+ else None
470
+ ),
410
471
  token=self.token,
411
472
  value=str(value),
412
473
  error=form_errors.get(field_name),
@@ -427,6 +488,12 @@ class WidgetFactory:
427
488
  options=options, # type: ignore
428
489
  removable=removable,
429
490
  title=field.title if field else "",
491
+ hint=field.description if field else None,
492
+ aria_label=(
493
+ field.json_schema_extra.get("aria_label") # type:ignore
494
+ if field and field.json_schema_extra
495
+ else None
496
+ ),
430
497
  token=self.token,
431
498
  value=str(value),
432
499
  error=form_errors.get(field_name),
@@ -443,11 +510,15 @@ class WidgetFactory:
443
510
  ) -> Widget[Any]:
444
511
  return TextWidget(
445
512
  field_name,
446
- hint=field.description if field else None,
447
513
  placeholder=str(field.examples[0]) if field and field.examples else None,
448
- aria_label=field.description if field else None,
449
- removable=removable,
450
514
  title=field.title if field else "",
515
+ hint=field.description if field else None,
516
+ aria_label=(
517
+ field.json_schema_extra.get("aria_label") # type:ignore
518
+ if field and field.json_schema_extra
519
+ else None
520
+ ),
521
+ removable=removable,
451
522
  token=self.token,
452
523
  value=str(value),
453
524
  error=form_errors.get(field_name),
@@ -1,13 +1,29 @@
1
+ from typing import Any
2
+
1
3
  from .base import Widget
2
4
 
3
5
 
4
6
  class HiddenWidget(Widget[str]):
7
+ '''
8
+ Widget to annotate to display a field as an hidden field.
9
+
10
+ ::
11
+ from pydantic import BaseModel
12
+ from fastlife.templating.renderer.widgets.hidden import HiddenWidget
13
+
14
+ class MyForm(BaseModel):
15
+ id: Annotated[str, HiddenWidget] = Field(...)
16
+ """Identifier in the database."""
17
+
18
+ '''
19
+
5
20
  def __init__(
6
21
  self,
7
22
  name: str,
8
23
  *,
9
24
  value: str,
10
25
  token: str,
26
+ **kwargs: Any,
11
27
  ) -> None:
12
28
  super().__init__(name, value=value, token=token)
13
29
 
@@ -16,22 +16,28 @@ class ModelWidget(Widget[Sequence[Widget[Any]]]):
16
16
  error: str | None = None,
17
17
  removable: bool,
18
18
  title: str,
19
+ hint: str | None = None,
20
+ aria_label: str | None = None,
19
21
  token: str,
22
+ nested: bool,
20
23
  ):
21
24
  super().__init__(
22
25
  name,
23
26
  title=title,
27
+ hint=hint,
28
+ aria_label=aria_label,
24
29
  value=value,
25
30
  error=error,
26
31
  removable=removable,
27
32
  token=token,
28
33
  )
34
+ self.nested = nested
29
35
 
30
36
  def get_template(self) -> str:
31
37
  return "pydantic_form.Model"
32
38
 
33
39
  def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
34
- """Return the html version"""
40
+ """Return the html version."""
35
41
  children_widget = [child.to_html(renderer) for child in self.value or []]
36
42
  kwargs = {
37
43
  "widget": self,
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional, Sequence, Type
1
+ from typing import Any, Sequence, Type
2
2
 
3
3
  from markupsafe import Markup
4
4
 
@@ -12,9 +12,10 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
12
12
  self,
13
13
  name: str,
14
14
  *,
15
- title: Optional[str],
16
- hint: Optional[str],
17
- value: Optional[Sequence[Widget[Any]]],
15
+ title: str | None,
16
+ hint: str | None = None,
17
+ aria_label: str | None = None,
18
+ value: Sequence[Widget[Any]] | None,
18
19
  error: str | None = None,
19
20
  item_type: Type[Any],
20
21
  token: str,
@@ -25,11 +26,12 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
25
26
  value=value,
26
27
  error=error,
27
28
  title=title,
29
+ hint=hint,
30
+ aria_label=aria_label,
28
31
  token=token,
29
32
  removable=removable,
30
33
  )
31
34
  self.item_type = item_type
32
- self.hint = hint
33
35
 
34
36
  def get_template(self) -> str:
35
37
  return "pydantic_form/Sequence"
@@ -38,7 +40,7 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
38
40
  return TypeWrapper(self.item_type, route_prefix, self.name, self.token)
39
41
 
40
42
  def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
41
- """Return the html version"""
43
+ """Return the html version."""
42
44
  children = [Markup(item.to_html(renderer)) for item in self.value or []]
43
45
  return Markup(
44
46
  renderer.render_template(
@@ -4,32 +4,108 @@ from .base import Widget
4
4
 
5
5
 
6
6
  class TextWidget(Widget[str]):
7
+ """
8
+ Widget for text like field (email, ...).
9
+
10
+ :param name: input name.
11
+ :param title: title for the widget.
12
+ :param hint: hint for human.
13
+ :param aria_label: html input aria-label value.
14
+ :param placeholder: html input placeholder value.
15
+ :param error: error of the value if any.
16
+ :param value: current value.
17
+ :param removable: display a button to remove the widget for optional fields.
18
+ :param token: token used to get unique id on the form.
19
+ """
20
+
7
21
  def __init__(
8
22
  self,
9
23
  name: str,
10
24
  *,
11
25
  title: Optional[str],
26
+ hint: Optional[str] = None,
12
27
  aria_label: Optional[str] = None,
13
28
  placeholder: Optional[str] = None,
14
29
  error: str | None = None,
15
- removable: bool = False,
16
30
  value: str = "",
17
- token: Optional[str] = None,
18
- hint: Optional[str] = None,
19
31
  input_type: str = "text",
32
+ removable: bool = False,
33
+ token: str,
20
34
  ) -> None:
21
35
  super().__init__(
22
36
  name,
23
37
  value=value,
24
38
  title=title,
39
+ hint=hint,
25
40
  aria_label=aria_label,
26
41
  token=token,
27
42
  error=error,
28
43
  removable=removable,
29
44
  )
30
45
  self.placeholder = placeholder or ""
31
- self.hint = hint
32
46
  self.input_type = input_type
33
47
 
34
48
  def get_template(self) -> str:
35
49
  return "pydantic_form.Text"
50
+
51
+
52
+ class TextareaWidget(Widget[str]):
53
+ """
54
+ Render a Textearea for a string or event a sequence of string.
55
+
56
+ ```
57
+ from fastlife.templating.renderer.widgets.text import TextareaWidget
58
+ from pydantic import BaseModel, Field, field_validator
59
+
60
+ class TaggedParagraphForm(BaseModel):
61
+ paragraph: Annotated[str, TextareaWidget] = Field(...)
62
+ tags: Annotated[Sequence[str], TextareaWidget] = Field(
63
+ default_factory=list,
64
+ title="Tags",
65
+ description="One tag per line",
66
+ )
67
+
68
+ @field_validator("tags", mode="before")
69
+ def split(cls, s: Any) -> Sequence[str]:
70
+ return s.split() if s else []
71
+ ```
72
+
73
+ :param name: input name.
74
+ :param title: title for the widget.
75
+ :param hint: hint for human.
76
+ :param aria_label: html input aria-label value.
77
+ :param placeholder: html input placeholder value.
78
+ :param error: error of the value if any.
79
+ :param value: current value.
80
+ :param removable: display a button to remove the widget for optional fields.
81
+ :param token: token used to get unique id on the form.
82
+
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ name: str,
88
+ *,
89
+ title: Optional[str],
90
+ hint: Optional[str] = None,
91
+ aria_label: Optional[str] = None,
92
+ placeholder: Optional[str] = None,
93
+ error: str | None = None,
94
+ value: str = "",
95
+ removable: bool = False,
96
+ token: str,
97
+ ) -> None:
98
+ super().__init__(
99
+ name,
100
+ value=value,
101
+ title=title,
102
+ hint=hint,
103
+ aria_label=aria_label,
104
+ token=token,
105
+ error=error,
106
+ removable=removable,
107
+ )
108
+ self.placeholder = placeholder or ""
109
+
110
+ def get_template(self) -> str:
111
+ return "pydantic_form.Textarea"
@@ -1,3 +1,6 @@
1
+ """
2
+ Widget for field of type Union.
3
+ """
1
4
  from typing import Any, Optional, Sequence, Type, Union
2
5
 
3
6
  from markupsafe import Markup
@@ -9,22 +12,41 @@ from .base import TypeWrapper, Widget
9
12
 
10
13
 
11
14
  class UnionWidget(Widget[Widget[Any]]):
15
+ """
16
+ Widget for union types.
17
+
18
+ :param name: input name.
19
+ :param title: title for the widget.
20
+ :param hint: hint for human.
21
+ :param aria_label: html input aria-label value.
22
+ :param value: current value.
23
+ :param error: error of the value if any.
24
+ :param children_types: childrens types list.
25
+ :param removable: display a button to remove the widget for optional fields.
26
+ :param token: token used to get unique id on the form.
27
+
28
+ """
29
+
12
30
  def __init__(
13
31
  self,
14
32
  name: str,
15
33
  *,
16
34
  title: Optional[str],
35
+ hint: Optional[str] = None,
36
+ aria_label: Optional[str] = None,
17
37
  value: Optional[Widget[Any]],
18
38
  error: str | None = None,
19
39
  children_types: Sequence[Type[BaseModel]],
40
+ removable: bool = False,
20
41
  token: str,
21
- removable: bool,
22
42
  ):
23
43
  super().__init__(
24
44
  name,
25
45
  value=value,
26
46
  error=error,
27
47
  title=title,
48
+ hint=hint,
49
+ aria_label=aria_label,
28
50
  token=token,
29
51
  removable=removable,
30
52
  )
@@ -32,6 +54,7 @@ class UnionWidget(Widget[Widget[Any]]):
32
54
  self.parent_name = name
33
55
 
34
56
  def build_types(self, route_prefix: str) -> Sequence[TypeWrapper]:
57
+ """Wrap types in the union in order to get the in their own widgets."""
35
58
  return [
36
59
  TypeWrapper(typ, route_prefix, self.name, self.token)
37
60
  for typ in self.children_types
@@ -41,7 +64,7 @@ class UnionWidget(Widget[Widget[Any]]):
41
64
  return "pydantic_form.Union"
42
65
 
43
66
  def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
44
- """Return the html version"""
67
+ """Return the html version."""
45
68
  child = Markup(self.value.to_html(renderer)) if self.value else ""
46
69
  return Markup(
47
70
  renderer.render_template(