fastlifeweb 0.20.0__py3-none-any.whl → 0.21.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 (54) hide show
  1. CHANGELOG.md +11 -1
  2. fastlife/__init__.py +5 -0
  3. fastlife/adapters/jinjax/renderer.py +4 -52
  4. fastlife/adapters/jinjax/widget_factory/bool_builder.py +2 -2
  5. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +5 -4
  6. fastlife/adapters/jinjax/widget_factory/enum_builder.py +2 -2
  7. fastlife/adapters/jinjax/widget_factory/factory.py +20 -21
  8. fastlife/adapters/jinjax/widget_factory/literal_builder.py +7 -6
  9. fastlife/adapters/jinjax/widget_factory/model_builder.py +3 -3
  10. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +2 -2
  11. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +3 -3
  12. fastlife/adapters/jinjax/widget_factory/set_builder.py +2 -2
  13. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +7 -8
  14. fastlife/adapters/jinjax/widget_factory/union_builder.py +3 -3
  15. fastlife/adapters/jinjax/widgets/base.py +35 -35
  16. fastlife/adapters/jinjax/widgets/boolean.py +13 -34
  17. fastlife/adapters/jinjax/widgets/checklist.py +36 -42
  18. fastlife/adapters/jinjax/widgets/dropdown.py +32 -38
  19. fastlife/adapters/jinjax/widgets/hidden.py +7 -15
  20. fastlife/adapters/jinjax/widgets/model.py +33 -40
  21. fastlife/adapters/jinjax/widgets/sequence.py +61 -40
  22. fastlife/adapters/jinjax/widgets/text.py +39 -78
  23. fastlife/adapters/jinjax/widgets/union.py +50 -57
  24. fastlife/components/CsrfToken.jinja +1 -1
  25. fastlife/components/pydantic_form/Widget.jinja +4 -3
  26. fastlife/config/configurator.py +65 -19
  27. fastlife/config/exceptions.py +0 -2
  28. fastlife/config/views.py +0 -2
  29. fastlife/domain/__init__.py +1 -0
  30. fastlife/domain/model/__init__.py +1 -0
  31. fastlife/domain/model/security.py +19 -0
  32. fastlife/domain/model/template.py +30 -0
  33. fastlife/domain/model/types.py +17 -0
  34. fastlife/request/request.py +19 -0
  35. fastlife/security/csrf.py +3 -13
  36. fastlife/services/templates.py +9 -42
  37. fastlife/services/translations.py +12 -0
  38. fastlife/templates/__init__.py +1 -6
  39. fastlife/templates/inline.py +18 -14
  40. {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/METADATA +1 -1
  41. {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/RECORD +44 -49
  42. fastlife/components/pydantic_form/Boolean.jinja +0 -13
  43. fastlife/components/pydantic_form/Checklist.jinja +0 -21
  44. fastlife/components/pydantic_form/Dropdown.jinja +0 -18
  45. fastlife/components/pydantic_form/Hidden.jinja +0 -3
  46. fastlife/components/pydantic_form/Model.jinja +0 -30
  47. fastlife/components/pydantic_form/Sequence.jinja +0 -47
  48. fastlife/components/pydantic_form/Text.jinja +0 -11
  49. fastlife/components/pydantic_form/Textarea.jinja +0 -38
  50. fastlife/components/pydantic_form/Union.jinja +0 -34
  51. fastlife/templates/binding.py +0 -52
  52. {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/WHEEL +0 -0
  53. {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/entry_points.txt +0 -0
  54. {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md CHANGED
@@ -1,4 +1,14 @@
1
- ## 0.20.0 - Released on 2024-11-09
1
+ ## 0.21.0 - Released on 2024-11-15
2
+ * Make the InlineTemplate the only way to render views template.
3
+ * Breaking change: template args is not supported in Configutor.add_route.
4
+ * Breaking change: template args is not supported in @view_config.
5
+ * Breaking change: template and Template dedendencies have been removed.
6
+ * Add new method in the configurator to register global vars for template:
7
+ {meth}`fastlife.config.configurator.GenericConfigurator.add_renderer_global`.
8
+ * Add npgettext i18n helper method support.
9
+ * Remove babel from dependency list (only a dev dependency).
10
+
11
+ ## 0.20.1 - Released on 2024-11-09
2
12
  * Add a new class GenericRegistry in order to properly type custom Configurator / Registry / Settings
3
13
  * Using InlineTemplate, we can pass arbitrary types for pydantic form
4
14
 
fastlife/__init__.py CHANGED
@@ -3,6 +3,7 @@ from importlib import metadata
3
3
  __version__ = metadata.version("fastlifeweb")
4
4
 
5
5
  from fastapi import Response
6
+ from fastapi.responses import RedirectResponse
6
7
 
7
8
  from .config import (
8
9
  Configurator,
@@ -15,6 +16,7 @@ from .config import (
15
16
  resource_view,
16
17
  view_config,
17
18
  )
19
+ from .domain.model.template import JinjaXTemplate
18
20
  from .request import GenericRequest, Registry, Request, get_request
19
21
 
20
22
  # from .request.form_data import model
@@ -39,4 +41,7 @@ __all__ = [
39
41
  "get_request",
40
42
  "Registry",
41
43
  "Response",
44
+ "RedirectResponse",
45
+ # Template
46
+ "JinjaXTemplate",
42
47
  ]
@@ -4,7 +4,7 @@ Template rending based on JinjaX.
4
4
 
5
5
  import logging
6
6
  import textwrap
7
- from collections.abc import Mapping, MutableMapping, Sequence
7
+ from collections.abc import Sequence
8
8
  from typing import (
9
9
  TYPE_CHECKING,
10
10
  Any,
@@ -66,58 +66,10 @@ class JinjaxRenderer(AbstractTemplateRenderer):
66
66
  super().__init__(request)
67
67
  self.catalog = catalog
68
68
  self.settings = request.registry.settings
69
- self.globals: MutableMapping[str, Any] = {}
70
69
  self.translations = get_localizer(request)
70
+ self.globals["pydantic_form"] = self.pydantic_form
71
71
 
72
- def build_globals(self) -> Mapping[str, Any]:
73
- """
74
- Build globals variables accessible in any templates.
75
-
76
- * `request` is the {class}`current request <fastlife.request.request.Request>`
77
- * `csrf_token` is used to build for {jinjax:component}`CsrfToken`.
78
- """
79
- settings = self.settings
80
- ret = {
81
- "request": self.request,
82
- "csrf_token": {
83
- "name": settings.csrf_token_name,
84
- "value": self.request.scope.get(settings.csrf_token_name, ""),
85
- },
86
- "pydantic_form": self.pydantic_form,
87
- "localizer": self.translations,
88
- **self.globals,
89
- }
90
- return ret
91
-
92
- def render_template(
93
- self,
94
- template: str,
95
- *,
96
- globals: Mapping[str, Any] | None = None,
97
- **params: Any,
98
- ) -> str:
99
- """
100
- Render the JinjaX component with the given parameter.
101
-
102
- :param template: the template to render
103
- :param globals: parameters that will be used by the JinjaX component and all its
104
- child components without "props drilling".
105
- :param params: parameters used to render the template.
106
- """
107
-
108
- template = template[: -len(self.settings.jinjax_file_ext) - 1]
109
- if globals:
110
- self.globals.update(globals)
111
-
112
- self.catalog.jinja_env.install_gettext_translations( # type: ignore
113
- self.translations, newstyle=True
114
- )
115
-
116
- return self.catalog.render( # type: ignore
117
- template, __globals=self.build_globals(), **params
118
- )
119
-
120
- def render_inline(self, template: InlineTemplate) -> str:
72
+ def render_template(self, template: InlineTemplate) -> str:
121
73
  """
122
74
  Render the JinjaX component with the given parameter.
123
75
 
@@ -134,7 +86,7 @@ class JinjaxRenderer(AbstractTemplateRenderer):
134
86
  return self.catalog.render(
135
87
  template.__class__.__qualname__,
136
88
  __source=src,
137
- __globals=self.build_globals(),
89
+ __globals=self.globals,
138
90
  **params,
139
91
  )
140
92
 
@@ -28,9 +28,9 @@ class BoolBuilder(BaseWidgetBuilder[bool]):
28
28
  ) -> BooleanWidget:
29
29
  """Build the widget."""
30
30
  return BooleanWidget(
31
- field_name,
31
+ name=field_name,
32
32
  removable=removable,
33
- title=field.title if field else "",
33
+ title=field.title or "" if field else "",
34
34
  hint=field.description if field else None,
35
35
  aria_label=(
36
36
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -8,9 +8,10 @@ from pydantic.networks import EmailStr
8
8
 
9
9
  from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
10
10
  from fastlife.adapters.jinjax.widgets.text import TextWidget
11
+ from fastlife.domain.model.types import Builtins
11
12
 
12
13
 
13
- class EmailStrBuilder(BaseWidgetBuilder[EmailStr]):
14
+ class EmailStrBuilder(BaseWidgetBuilder[Builtins]):
14
15
  """Builder for Pydantic EmailStr."""
15
16
 
16
17
  def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
@@ -23,17 +24,17 @@ class EmailStrBuilder(BaseWidgetBuilder[EmailStr]):
23
24
  field_name: str,
24
25
  field_type: type[Any],
25
26
  field: FieldInfo | None,
26
- value: EmailStr | None,
27
+ value: Builtins | None,
27
28
  form_errors: Mapping[str, Any],
28
29
  removable: bool,
29
30
  ) -> TextWidget:
30
31
  """Build the widget."""
31
32
  return TextWidget(
32
- field_name,
33
+ name=field_name,
33
34
  input_type="email",
34
35
  placeholder=str(field.examples[0]) if field and field.examples else None,
35
36
  removable=removable,
36
- title=field.title if field else "",
37
+ title=field.title or "" if field else "",
37
38
  hint=field.description if field else None,
38
39
  aria_label=(
39
40
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -31,10 +31,10 @@ class EnumBuilder(BaseWidgetBuilder[Enum]):
31
31
  """Build the widget."""
32
32
  options = [(item.name, item.value) for item in field_type] # type: ignore
33
33
  return DropDownWidget(
34
- field_name,
34
+ name=field_name,
35
35
  options=options, # type: ignore
36
36
  removable=removable,
37
- title=field.title if field else "",
37
+ title=field.title or "" if field else "",
38
38
  hint=field.description if field else None,
39
39
  aria_label=(
40
40
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -4,15 +4,16 @@ Create markup for pydantic forms.
4
4
 
5
5
  import secrets
6
6
  from collections.abc import Mapping
7
- from inspect import isclass
8
- from typing import Any, cast, get_origin
7
+ from typing import TYPE_CHECKING, Any, get_origin
9
8
 
10
9
  from markupsafe import Markup
11
10
  from pydantic.fields import FieldInfo
12
11
 
13
- from fastlife.adapters.jinjax.widgets.base import Widget
12
+ from fastlife.adapters.jinjax.widgets.base import CustomWidget, Widget
14
13
  from fastlife.request.form import FormModel
15
- from fastlife.services.templates import AbstractTemplateRenderer
14
+
15
+ if TYPE_CHECKING:
16
+ from fastlife.services.templates import AbstractTemplateRenderer
16
17
 
17
18
  from .base import BaseWidgetBuilder
18
19
  from .bool_builder import BoolBuilder
@@ -35,7 +36,7 @@ class WidgetFactory:
35
36
  :param token: reuse a token.
36
37
  """
37
38
 
38
- def __init__(self, renderer: AbstractTemplateRenderer, token: str | None = None):
39
+ def __init__(self, renderer: "AbstractTemplateRenderer", token: str | None = None):
39
40
  self.renderer = renderer
40
41
  self.token = token or secrets.token_urlsafe(4).replace("_", "-")
41
42
  self.builders: list[BaseWidgetBuilder[Any]] = [
@@ -131,24 +132,22 @@ class WidgetFactory:
131
132
  """
132
133
  if field and field.metadata:
133
134
  for widget in field.metadata:
134
- if isclass(widget) and issubclass(widget, Widget):
135
- return cast(
136
- Widget[Any],
137
- widget(
138
- name,
139
- value=value,
140
- removable=removable,
141
- title=field.title if field else "",
142
- hint=field.description if field else None,
143
- aria_label=(
144
- field.json_schema_extra.get("aria_label") # type:ignore
145
- if field and field.json_schema_extra
146
- else None
147
- ),
148
- token=self.token,
149
- error=form_errors.get(name),
135
+ if isinstance(widget, CustomWidget):
136
+ ret: Widget[Any] = widget.typ(
137
+ name=name,
138
+ value=value,
139
+ removable=removable,
140
+ title=field.title or "" if field else "",
141
+ hint=field.description if field else None,
142
+ aria_label=(
143
+ field.json_schema_extra.get("aria_label") # type:ignore
144
+ if field and field.json_schema_extra
145
+ else None
150
146
  ),
147
+ token=self.token,
148
+ error=form_errors.get(name),
151
149
  )
150
+ return ret
152
151
 
153
152
  type_origin = get_origin(typ)
154
153
  for builder in self.builders:
@@ -8,9 +8,10 @@ from pydantic.fields import FieldInfo
8
8
  from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
9
9
  from fastlife.adapters.jinjax.widgets.dropdown import DropDownWidget
10
10
  from fastlife.adapters.jinjax.widgets.hidden import HiddenWidget
11
+ from fastlife.domain.model.types import AnyLiteral
11
12
 
12
13
 
13
- class LiteralBuilder(BaseWidgetBuilder[str]): # str|int|bool
14
+ class LiteralBuilder(BaseWidgetBuilder[AnyLiteral]):
14
15
  """Builder for Literal."""
15
16
 
16
17
  def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
@@ -23,7 +24,7 @@ class LiteralBuilder(BaseWidgetBuilder[str]): # str|int|bool
23
24
  field_name: str,
24
25
  field_type: type[Any], # a literal actually
25
26
  field: FieldInfo | None,
26
- value: str | None,
27
+ value: AnyLiteral | None,
27
28
  form_errors: Mapping[str, Any],
28
29
  removable: bool,
29
30
  ) -> HiddenWidget | DropDownWidget:
@@ -31,15 +32,15 @@ class LiteralBuilder(BaseWidgetBuilder[str]): # str|int|bool
31
32
  choices: list[str] = field_type.__args__ # type: ignore
32
33
  if len(choices) == 1:
33
34
  return HiddenWidget(
34
- field_name,
35
+ name=field_name,
35
36
  value=choices[0],
36
37
  token=self.factory.token,
37
38
  )
38
39
  return DropDownWidget(
39
- field_name,
40
- options=choices,
40
+ name=field_name,
41
+ options=choices, # type: ignore
41
42
  removable=removable,
42
- title=field.title if field else "",
43
+ title=field.title or "" if field else "",
43
44
  hint=field.description if field else None,
44
45
  aria_label=(
45
46
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -47,11 +47,11 @@ class ModelBuilder(BaseWidgetBuilder[Mapping[str, Any]]):
47
47
  form_errors=form_errors,
48
48
  removable=False,
49
49
  )
50
- return ModelWidget(
51
- field_name,
50
+ return ModelWidget[Any](
51
+ name=field_name,
52
52
  value=list(ret.values()),
53
53
  removable=removable,
54
- title=field.title if field and field.title else "",
54
+ title=field.title or "" if field and field.title else "",
55
55
  hint=field.description if field else None,
56
56
  aria_label=(
57
57
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -30,11 +30,11 @@ class SecretStrBuilder(BaseWidgetBuilder[SecretStr]):
30
30
  ) -> Widget[SecretStr]:
31
31
  """Build the widget."""
32
32
  return TextWidget(
33
- field_name,
33
+ name=field_name,
34
34
  input_type="password",
35
35
  placeholder=str(field.examples[0]) if field and field.examples else None,
36
36
  removable=removable,
37
- title=field.title if field else "",
37
+ title=field.title or "" if field else "",
38
38
  hint=field.description if field else None,
39
39
  aria_label=(
40
40
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -41,9 +41,9 @@ class SequenceBuilder(BaseWidgetBuilder[Sequence[Any]]):
41
41
  )
42
42
  for idx, v in enumerate(value)
43
43
  ]
44
- return SequenceWidget(
45
- field_name,
46
- title=field.title if field else "",
44
+ return SequenceWidget[Any](
45
+ name=field_name,
46
+ title=field.title or "" if field else "",
47
47
  hint=field.description if field else None,
48
48
  aria_label=(
49
49
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -65,8 +65,8 @@ class SetBuilder(BaseWidgetBuilder[set[Any]]):
65
65
  raise NotImplementedError # coverage: ignore
66
66
 
67
67
  return ChecklistWidget(
68
- field_name,
69
- title=field.title if field else "",
68
+ name=field_name,
69
+ title=field.title or "" if field else "",
70
70
  hint=field.description if field else None,
71
71
  aria_label=(
72
72
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -1,23 +1,22 @@
1
1
  """Handle simple types (str, int, float, ...)."""
2
2
 
3
3
  from collections.abc import Mapping
4
- from decimal import Decimal
5
4
  from typing import Any
6
- from uuid import UUID
7
5
 
8
6
  from pydantic.fields import FieldInfo
9
7
 
10
8
  from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
11
9
  from fastlife.adapters.jinjax.widgets.base import Widget
12
10
  from fastlife.adapters.jinjax.widgets.text import TextWidget
11
+ from fastlife.domain.model.types import Builtins
13
12
 
14
13
 
15
- class SimpleTypeBuilder(BaseWidgetBuilder[str | int | str | float | Decimal | UUID]):
14
+ class SimpleTypeBuilder(BaseWidgetBuilder[Builtins]):
16
15
  """Builder for simple types."""
17
16
 
18
17
  def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
19
18
  """True for simple types: int, str, float, Decimal, UUID"""
20
- return issubclass(typ, int | str | float | Decimal | UUID)
19
+ return issubclass(typ, Builtins)
21
20
 
22
21
  def build(
23
22
  self,
@@ -25,15 +24,15 @@ class SimpleTypeBuilder(BaseWidgetBuilder[str | int | str | float | Decimal | UU
25
24
  field_name: str,
26
25
  field_type: type[Any],
27
26
  field: FieldInfo | None,
28
- value: int | str | float | Decimal | UUID | None,
27
+ value: Builtins | None,
29
28
  form_errors: Mapping[str, Any],
30
29
  removable: bool,
31
- ) -> Widget[int | str | float | Decimal | UUID]:
30
+ ) -> Widget[Builtins]:
32
31
  """Build the widget."""
33
32
  return TextWidget(
34
- field_name,
33
+ name=field_name,
35
34
  placeholder=str(field.examples[0]) if field and field.examples else None,
36
- title=field.title if field else "",
35
+ title=field.title or "" if field else "",
37
36
  hint=field.description if field else None,
38
37
  aria_label=(
39
38
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -70,12 +70,12 @@ class UnionBuilder(BaseWidgetBuilder[Any]):
70
70
  removable=False,
71
71
  )
72
72
 
73
- widget = UnionWidget(
74
- field_name,
73
+ widget = UnionWidget[Any](
74
+ name=field_name,
75
75
  # we assume those types are BaseModel
76
76
  value=child,
77
77
  children_types=types, # type: ignore
78
- title=field.title if field else "",
78
+ title=field.title or "" if field else "",
79
79
  hint=field.description if field else None,
80
80
  aria_label=(
81
81
  field.json_schema_extra.get("aria_label") # type:ignore
@@ -1,12 +1,13 @@
1
1
  """Widget base class."""
2
2
 
3
- import abc
4
3
  import secrets
5
4
  from collections.abc import Mapping
6
- from typing import Any, Generic, TypeVar
5
+ from typing import Any, Generic, Self, TypeVar
7
6
 
8
7
  from markupsafe import Markup
8
+ from pydantic import Field, model_validator
9
9
 
10
+ from fastlife.domain.model.template import JinjaXTemplate
10
11
  from fastlife.services.templates import AbstractTemplateRenderer
11
12
  from fastlife.shared_utils.infer import is_union
12
13
 
@@ -21,7 +22,7 @@ def get_title(typ: type[Any]) -> str:
21
22
  )
22
23
 
23
24
 
24
- class Widget(abc.ABC, Generic[T]):
25
+ class Widget(JinjaXTemplate, Generic[T]):
25
26
  """
26
27
  Base class for widget of pydantic fields.
27
28
 
@@ -39,48 +40,37 @@ class Widget(abc.ABC, Generic[T]):
39
40
 
40
41
  name: str
41
42
  "variable name, nested variables have dots."
42
- value: T | None
43
+ id: str = Field(default="")
44
+ "variable name, nested variables have dots."
45
+ value: T | None = Field(default=None)
43
46
  """Value of the field."""
44
- title: str
47
+ title: str = Field(default="")
45
48
  "Human title for the widget."
46
- hint: str
49
+ hint: str | None = Field(default=None)
47
50
  "A help message for the the widget."
48
- aria_label: str
51
+
52
+ error: str | None = Field(default=None)
53
+ "Error message."
54
+
55
+ aria_label: str | None = Field(default=None)
49
56
  "Non visible text alternative."
50
- token: str
57
+ token: str = Field(default="")
51
58
  "unique token to ensure id are unique in the DOM."
52
- removable: bool
59
+ removable: bool = Field(default=False)
53
60
  "Indicate that the widget is removable from the dom."
54
61
 
55
- def __init__(
56
- self,
57
- name: str,
58
- *,
59
- value: T | None = None,
60
- error: str | None = None,
61
- title: str | None = None,
62
- hint: str | None = None,
63
- token: str | None = None,
64
- aria_label: str | None = None,
65
- removable: bool = False,
66
- ):
67
- self.name = name
68
- self.value = value
69
- self.error = error
70
- self.title = title or name.split(".")[-1]
71
- self.hint = hint or ""
72
- self.aria_label = aria_label or ""
73
- self.token = token or secrets.token_urlsafe(4).replace("_", "-")
74
- self.removable = removable
75
- self.id = f"{self.name}-{self.token}".replace("_", "-").replace(".", "-")
76
-
77
- @abc.abstractmethod
78
- def get_template(self) -> str:
79
- """Get the widget component template."""
62
+ @model_validator(mode="after")
63
+ def fill_props(self) -> Self:
64
+ self.title = self.title or self.name.split(".")[-1]
65
+ self.token = self.token or secrets.token_urlsafe(4).replace("_", "-")
66
+ self.id = self.id or f"{self.name}-{self.token}".replace("_", "-").replace(
67
+ ".", "-"
68
+ )
69
+ return self
80
70
 
81
71
  def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
82
72
  """Return the html version."""
83
- return Markup(renderer.render_template(self.get_template(), widget=self))
73
+ return Markup(renderer.render_template(self))
84
74
 
85
75
 
86
76
  def _get_fullname(typ: type[Any]) -> str:
@@ -90,6 +80,16 @@ def _get_fullname(typ: type[Any]) -> str:
90
80
  return f"{typ.__module__}:{typ.__name__}"
91
81
 
92
82
 
83
+ TWidget = TypeVar("TWidget", bound=Widget[Any])
84
+
85
+
86
+ class CustomWidget(Generic[TWidget]):
87
+ typ: type[Any]
88
+
89
+ def __init__(self, typ: type[TWidget]) -> None:
90
+ self.typ = typ
91
+
92
+
93
93
  class TypeWrapper:
94
94
  """
95
95
  Wrap children types for union type.
@@ -8,39 +8,18 @@ from .base import Widget
8
8
  class BooleanWidget(Widget[bool]):
9
9
  """
10
10
  Widget for field of type bool.
11
-
12
- :param name: field name.
13
- :param title: title for the widget.
14
- :param hint: hint for human.
15
- :param aria_label: html input aria-label value.
16
- :param value: current value.
17
- :param error: error of the value if any.
18
- :param removable: display a button to remove the widget for optional fields.
19
- :param token: token used to get unique id on the form.
20
11
  """
21
12
 
22
- def __init__(
23
- self,
24
- name: str,
25
- *,
26
- title: str | None,
27
- hint: str | None = None,
28
- aria_label: str | None = None,
29
- value: bool = False,
30
- error: str | None = None,
31
- removable: bool = False,
32
- token: str,
33
- ) -> None:
34
- super().__init__(
35
- name,
36
- title=title,
37
- hint=hint,
38
- aria_label=aria_label,
39
- value=value,
40
- error=error,
41
- removable=removable,
42
- token=token,
43
- )
44
-
45
- def get_template(self) -> str:
46
- return "pydantic_form.Boolean.jinja"
13
+ template = """
14
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
15
+ <div class="pt-4">
16
+ <div class="flex items-center">
17
+ <Checkbox :name="name" :id="id" :checked="value" value="1" />
18
+ <Label :for="id" class="ms-2 text-base text-neutral-900 dark:text-white">
19
+ {{title|safe}}
20
+ </Label>
21
+ </div>
22
+ <pydantic_form.Error :text="error" />
23
+ </div>
24
+ </pydantic_form.Widget>
25
+ """