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,14 @@
1
+ """
2
+ Template Constants injects as global variables in templates.
3
+
4
+ Constants are configurable using the setting :attrs:`jinjax_global_catalog_class`,
5
+ in order to customize templates.
6
+
7
+ Those constants are heavy used to inject CSS classes in primary html element
8
+ that are bound to Jinja component.
9
+
10
+ """
11
+
1
12
  from pydantic import BaseModel
2
13
 
3
14
 
@@ -6,6 +17,8 @@ def space_join(*segments: str) -> str:
6
17
 
7
18
 
8
19
  class Constants(BaseModel):
20
+ """Templates constants."""
21
+
9
22
  A_CLASS: str = space_join(
10
23
  "text-primary-600",
11
24
  "hover:text-primary-500",
@@ -13,6 +26,7 @@ class Constants(BaseModel):
13
26
  "dark:text-primary-300",
14
27
  "dark:hover:text-primary-400",
15
28
  )
29
+ """Default css class for {jinjax:component}`A`."""
16
30
 
17
31
  BUTTON_CLASS: str = space_join(
18
32
  "bg-primary-600",
@@ -32,8 +46,10 @@ class Constants(BaseModel):
32
46
  "dark:focus:ring-primary-800",
33
47
  "dark:hover:bg-primary-700",
34
48
  )
49
+ """Default css class for {jinjax:component}`Button`."""
35
50
 
36
51
  DETAILS_CLASS: str = "border border-neutral-100 p-4 rounded-m"
52
+ """Default css class for {jinjax:component}`Details`."""
37
53
 
38
54
  SECONDARY_BUTTON_CLASS: str = space_join(
39
55
  "bg-neutral-300",
@@ -52,6 +68,15 @@ class Constants(BaseModel):
52
68
  "dark:focus:ring-neutral-100",
53
69
  "dark:hover:bg-neutral-300",
54
70
  )
71
+ """
72
+ css class for {jinjax:component}`Button`.
73
+
74
+ usage:
75
+
76
+ ```html
77
+ <Button :class="SECONDARY_BUTTON_CLASS">secondary</Button>
78
+ ```
79
+ """
55
80
 
56
81
  ICON_BUTTON_CLASS: str = space_join(
57
82
  "bg-white",
@@ -70,6 +95,18 @@ class Constants(BaseModel):
70
95
  "dark:focus:ring-primary-500",
71
96
  "dark:hover:text-primary-300",
72
97
  )
98
+ """
99
+ css class for {jinjax:component}`Button`.
100
+
101
+ usage:
102
+
103
+ ```html
104
+ <Button :class="ICON_BUTTON_CLASS">
105
+ <icons.PencilSquare class="w-6 h-6" title="Copy" />
106
+ </Button>
107
+
108
+ ```
109
+ """
73
110
 
74
111
  CHECKBOX_CLASS: str = space_join(
75
112
  "bg-neutral-100",
@@ -85,7 +122,11 @@ class Constants(BaseModel):
85
122
  "focus:ring-2",
86
123
  "focus:ring-primary-500",
87
124
  )
125
+ """Default css class for {jinjax:component}`Checkbox`."""
126
+
88
127
  FORM_CLASS: str = "space-y-4 md:space-y-6"
128
+ """Default css class for {jinjax:component}`Form`."""
129
+
89
130
  H1_CLASS: str = space_join(
90
131
  "block",
91
132
  "font-bold",
@@ -98,6 +139,8 @@ class Constants(BaseModel):
98
139
  "dark:text-white",
99
140
  "md:text-4xl",
100
141
  )
142
+ """Default css class for {jinjax:component}`H1`."""
143
+
101
144
  H2_CLASS: str = space_join(
102
145
  "block",
103
146
  "font-bold",
@@ -110,6 +153,8 @@ class Constants(BaseModel):
110
153
  "dark:text-white",
111
154
  "md:text-4xl",
112
155
  )
156
+ """Default css class for {jinjax:component}`H2`."""
157
+
113
158
  H3_CLASS: str = space_join(
114
159
  "block",
115
160
  "font-bold",
@@ -122,6 +167,8 @@ class Constants(BaseModel):
122
167
  "dark:text-white",
123
168
  "md:text-3xl",
124
169
  )
170
+ """Default css class for {jinjax:component}`H3`."""
171
+
125
172
  H3_SUMMARY_CLASS: str = space_join(
126
173
  "block",
127
174
  "font-bold",
@@ -133,6 +180,9 @@ class Constants(BaseModel):
133
180
  "dark:text-white",
134
181
  "md:text-3xl",
135
182
  )
183
+ """
184
+ Default css class for {jinjax:component}`H3` inside {jinjax:component}`Summary`.
185
+ """
136
186
 
137
187
  H4_CLASS: str = space_join(
138
188
  "block",
@@ -146,6 +196,8 @@ class Constants(BaseModel):
146
196
  "dark:text-white",
147
197
  "md:text-2xl",
148
198
  )
199
+ """Default css class for {jinjax:component}`H4`."""
200
+
149
201
  H5_CLASS: str = space_join(
150
202
  "block",
151
203
  "font-bold",
@@ -158,6 +210,8 @@ class Constants(BaseModel):
158
210
  "dark:text-white",
159
211
  "md:text-xl",
160
212
  )
213
+ """Default css class for {jinjax:component}`H5`."""
214
+
161
215
  H6_CLASS: str = space_join(
162
216
  "block",
163
217
  "font-bold",
@@ -170,6 +224,8 @@ class Constants(BaseModel):
170
224
  "dark:text-white",
171
225
  "md:text-l",
172
226
  )
227
+ """Default css class for {jinjax:component}`H6`."""
228
+
173
229
  INPUT_CLASS: str = space_join(
174
230
  "bg-neutral-50",
175
231
  "block",
@@ -189,6 +245,8 @@ class Constants(BaseModel):
189
245
  "focus:border-primary-500",
190
246
  "focus:ring-primary-500",
191
247
  )
248
+ """Default css class for {jinjax:component}`Input`."""
249
+
192
250
  LABEL_CLASS: str = space_join(
193
251
  "block",
194
252
  "font-bold",
@@ -197,12 +255,18 @@ class Constants(BaseModel):
197
255
  "text-neutral-900",
198
256
  "dark:text-white",
199
257
  )
258
+ """Default css class for {jinjax:component}`Label`."""
259
+
200
260
  P_CLASS: str = space_join(
201
261
  "text-base",
202
262
  "text-neutral-900",
203
263
  "dark:text-white",
204
264
  )
265
+ """Default css class for {jinjax:component}`P`."""
266
+
205
267
  RADIO_DIV_CLASS: str = "flex items-center mb-4"
268
+ """Default css class for {jinjax:component}`Radio` `<div>` container."""
269
+
206
270
  RADIO_INPUT_CLASS: str = space_join(
207
271
  "bg-neutral-100",
208
272
  "border-neutral-300",
@@ -216,9 +280,17 @@ class Constants(BaseModel):
216
280
  "dark:focus:ring-primary-600",
217
281
  "dark:ring-offset-neutral-800",
218
282
  )
283
+ """
284
+ Default css class for {jinjax:component}`Radio` `<input type="radio">`.
285
+ """
286
+
219
287
  RADIO_LABEL_CLASS: str = (
220
288
  "ms-2 text-sm font-medium text-neutral-900 dark:text-neutral-300"
221
289
  )
290
+ """
291
+ Default css class for {jinjax:component}`Radio` `<label>` element.
292
+ """
293
+
222
294
  SELECT_CLASS: str = space_join(
223
295
  "bg-neutral-50",
224
296
  "block",
@@ -238,6 +310,16 @@ class Constants(BaseModel):
238
310
  "dark:placeholder-neutral-400",
239
311
  "dark:text-white",
240
312
  )
313
+ """Default css class for {jinjax:component}`Select`."""
314
+
315
+ SUMMARY_CLASS: str = "flex items-center items-center font-medium cursor-pointer"
316
+ """Default css class for {jinjax:component}`Summary`."""
317
+
241
318
  TABLE_CLASS: str = "table-auto w-full text-left border-colapse"
319
+ """Default css class for {jinjax:component}`Table`."""
320
+
242
321
  TD_CLASS: str = "px-4 py-2 font-normal border-b dark:border-neutral-500"
322
+ """Default css class for {jinjax:component}`Td`."""
323
+
243
324
  TH_CLASS: str = "px-4 py-2 font-medium border-b dark:border-neutral-500"
325
+ """Default css class for {jinjax:component}`Th`."""
@@ -1,22 +1,277 @@
1
- from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional, Sequence, Type
1
+ """Template rending based on JinjaX."""
2
+
3
+ import ast
4
+ import logging
5
+ import re
6
+ import textwrap
7
+ from pathlib import Path
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ Iterator,
12
+ Mapping,
13
+ MutableMapping,
14
+ Optional,
15
+ Sequence,
16
+ Type,
17
+ cast,
18
+ )
2
19
 
3
20
  from fastapi import Request
21
+ from jinja2 import Template
22
+ from jinja2.exceptions import TemplateSyntaxError
23
+ from jinjax import InvalidArgument
4
24
  from jinjax.catalog import Catalog
25
+ from jinjax.component import RX_ARGS_START, RX_META_HEADER, Component
26
+ from jinjax.exceptions import DuplicateDefDeclaration
5
27
  from markupsafe import Markup
6
28
  from pydantic.fields import FieldInfo
7
29
 
8
- from fastlife.request.model_result import ModelResult
30
+ from fastlife.request.form import FormModel
9
31
  from fastlife.templating.renderer.widgets.factory import WidgetFactory
10
32
 
11
33
  if TYPE_CHECKING:
12
- from fastlife.configurator.settings import Settings # coverage: ignore
34
+ from fastlife.config.settings import Settings # coverage: ignore
13
35
 
14
36
  from fastlife.shared_utils.resolver import resolve, resolve_path
15
37
 
16
38
  from .abstract import AbstractTemplateRenderer, AbstractTemplateRendererFactory
17
39
 
40
+ log = logging.getLogger(__name__)
41
+
42
+ RX_DOC_START = re.compile(r"{#-?\s*doc\s+")
43
+ RX_CONTENT = re.compile(r"\{\{-?\s*content\s*-?\}\}", re.DOTALL)
44
+ RX_COMMENT_REPLACE = re.compile(r"{#[^#]+#}")
45
+
46
+
47
+ def has_content(source: str) -> bool:
48
+ nocomment = RX_COMMENT_REPLACE.sub("", source)
49
+ return len(RX_CONTENT.findall(nocomment)) > 0
50
+
51
+
52
+ def generate_docstring(
53
+ func_def: ast.FunctionDef, component_name: str, add_content: bool
54
+ ) -> str:
55
+ """Generate a docstring for the component."""
56
+ # Extract function name and docstring
57
+ docstring = (ast.get_docstring(func_def, clean=True) or "").strip()
58
+ if docstring:
59
+ docstring = textwrap.dedent(docstring)
60
+ docstring_lines = [l for l in docstring.split("\n")]
61
+ # Add a newline for separation after the function docstring
62
+ docstring_lines.append("")
63
+ else:
64
+ docstring_lines = []
65
+
66
+ component_params: list[str] = []
67
+
68
+ # Function for processing an argument and adding its docstring lines
69
+ def process_arg(arg: ast.arg, default_value: Any = None) -> None:
70
+ arg_name = arg.arg
71
+ param_desc = ""
72
+ # Extract the type annotation (if any)
73
+ if (
74
+ isinstance(arg.annotation, ast.Subscript)
75
+ and isinstance(arg.annotation.value, ast.Name)
76
+ and arg.annotation.value.id == "Annotated"
77
+ ):
78
+ # For Annotated types, we expect the first argument to be the type and
79
+ # the second to be the description
80
+ type_annotation = arg.annotation.slice.elts[0] # type: ignore
81
+ param_type = ast.unparse(type_annotation)
82
+
83
+ if len(arg.annotation.slice.elts) > 1 and isinstance( # type: ignore
84
+ arg.annotation.slice.elts[1], # type: ignore
85
+ ast.Constant, # type: ignore
86
+ ):
87
+ param_desc = arg.annotation.slice.elts[1].value # type: ignore
88
+ else:
89
+ # Otherwise, just use the type if available
90
+ param_type = ast.unparse(arg.annotation) if arg.annotation else "Any"
91
+
92
+ # Build the parameter docstring line
93
+ docstring_lines.append(f":param {arg_name.rstrip('_')}: {param_desc}".strip())
94
+
95
+ # Build the string representation of the parameter
96
+ param_str = f"{arg_name}: {param_type}"
97
+ if default_value is not None:
98
+ param_str += f" = {ast.unparse(default_value)}" # type: ignore
99
+
100
+ component_params.append(param_str)
101
+
102
+ # Process keyword-only arguments
103
+ kwonlyargs = func_def.args.kwonlyargs
104
+ kw_defaults = func_def.args.kw_defaults
105
+
106
+ for arg, default in zip(kwonlyargs, kw_defaults):
107
+ process_arg(arg, default)
108
+
109
+ if add_content:
110
+ component_params.append("content: Any")
111
+ docstring_lines.append(":param content: child node.")
112
+
113
+ return (
114
+ f"{component_name}({', '.join(component_params)})"
115
+ + "\n"
116
+ + "\n "
117
+ + ("\n ".join(docstring_lines).strip()).replace("\n \n", "\n\n")
118
+ + "\n"
119
+ )
120
+
121
+
122
+ class InspectableComponent(Component):
123
+ __slots__ = (
124
+ "name",
125
+ "prefix",
126
+ "url_prefix",
127
+ "required",
128
+ "optional",
129
+ "css",
130
+ "js",
131
+ "path",
132
+ "mtime",
133
+ "tmpl",
134
+ "source",
135
+ )
136
+
137
+ def __init__(
138
+ self,
139
+ *,
140
+ name: str,
141
+ prefix: str = "",
142
+ url_prefix: str = "",
143
+ source: str = "",
144
+ mtime: float = 0,
145
+ tmpl: "Template | None" = None,
146
+ path: "Path | None" = None,
147
+ ) -> None:
148
+ super().__init__(
149
+ name=name,
150
+ prefix=prefix,
151
+ url_prefix=url_prefix,
152
+ source=source,
153
+ mtime=mtime,
154
+ tmpl=tmpl,
155
+ path=path,
156
+ )
157
+ self.source = source
158
+
159
+ def as_def(self) -> ast.FunctionDef:
160
+ signature = "def component(): pass"
161
+ match = RX_META_HEADER.match(self.source)
162
+ if match:
163
+ headers = match.group(0)
164
+ header = headers.split("#}")[:-1]
165
+ def_found = False
166
+ docstring = ""
167
+
168
+ expr = None
169
+ while header:
170
+ item = header.pop(0).strip(" -\n")
171
+
172
+ expr = self.read_metadata_item(item, RX_ARGS_START)
173
+ if expr:
174
+ if def_found:
175
+ raise DuplicateDefDeclaration(self.name)
176
+ def_found = True
177
+ continue
178
+
179
+ doc = self.read_metadata_item(item, RX_DOC_START)
180
+ if doc:
181
+ docstring += f" {doc.strip()}\n"
182
+ continue
183
+
184
+ if expr:
185
+ signature = f"""\
186
+ def component(*, {expr}):
187
+ '''
188
+ {docstring or ""}
189
+ '''
190
+ ...
191
+ """
192
+ elif docstring:
193
+ signature = f"""\
194
+ def component():
195
+ '''
196
+ {docstring}
197
+ '''
198
+ ...
199
+ """
200
+
201
+ try:
202
+ astree = ast.parse(signature)
203
+ except SyntaxError as err:
204
+ raise InvalidArgument(err) from err
205
+ return cast(ast.FunctionDef, astree.body[0])
206
+
207
+ def build_docstring(self) -> str:
208
+ func_def = self.as_def()
209
+ prefix = f"{self.prefix}." if self.prefix else ""
210
+ ret = ".. jinjax:component:: " + generate_docstring(
211
+ func_def, f"{prefix}{self.name}", has_content(self.source)
212
+ )
213
+ return ret
214
+
215
+
216
+ class InspectableCatalog(Catalog):
217
+ """
218
+ Override the catalog in order to iterate over components to build the doc.
219
+ """
220
+
221
+ def iter_components(
222
+ self,
223
+ ignores: Sequence[re.Pattern[str]] | None = None,
224
+ includes: Sequence[re.Pattern[str]] | None = None,
225
+ ) -> Iterator[InspectableComponent]:
226
+ for prefix, loader in self.prefixes.items():
227
+ for t in loader.list_templates():
228
+ name, file_ext = t.split(".", maxsplit=1)
229
+ name = name.replace("/", ".")
230
+ path, tmpl_name = self._get_component_path(
231
+ prefix, name, file_ext=file_ext
232
+ )
233
+
234
+ to_include = True
235
+ if includes:
236
+ to_include = False
237
+ for include in includes:
238
+ if include.match(name):
239
+ to_include = True
240
+ break
241
+ if to_include and ignores:
242
+ for ignore in ignores:
243
+ if ignore.match(name):
244
+ to_include = False
245
+ break
246
+
247
+ if to_include:
248
+ component = InspectableComponent(
249
+ name=name, prefix=prefix, path=path, source=path.read_text()
250
+ )
251
+
252
+ self.jinja_env.loader = loader
253
+ try:
254
+ component.tmpl = self.jinja_env.get_template(
255
+ tmpl_name, globals=self._tmpl_globals
256
+ )
257
+ except TemplateSyntaxError as exc:
258
+ log.error(f"Syntax Error: {exc} on {exc.lineno} :")
259
+ log.error(path.read_text())
260
+ continue
261
+ yield component
262
+
18
263
 
19
264
  def build_searchpath(template_search_path: str) -> Sequence[str]:
265
+ """
266
+ Build the path containing templates.
267
+
268
+ Path may be absolute directories or directories relative to a python
269
+ package. For instance, the `fastlife:templates` is the directory templates
270
+ inside the fastlife installation dir.
271
+
272
+ :param template_search_path: list of path separated by a comma (`,`).
273
+ :return: List resolved path.
274
+ """
20
275
  searchpath: list[str] = []
21
276
  paths = template_search_path.split(",")
22
277
 
@@ -29,9 +284,11 @@ def build_searchpath(template_search_path: str) -> Sequence[str]:
29
284
 
30
285
 
31
286
  class JinjaxRenderer(AbstractTemplateRenderer):
287
+ """Render templates using JinjaX."""
288
+
32
289
  def __init__(
33
290
  self,
34
- catalog: Catalog,
291
+ catalog: InspectableCatalog,
35
292
  request: Request,
36
293
  csrf_token_name: str,
37
294
  form_data_model_prefix: str,
@@ -45,6 +302,12 @@ class JinjaxRenderer(AbstractTemplateRenderer):
45
302
  self.globals: MutableMapping[str, Any] = {}
46
303
 
47
304
  def build_globals(self) -> Mapping[str, Any]:
305
+ """
306
+ Build globals variables accessible in any templates.
307
+
308
+ * `request` is the {class}`current request <fastlife.request.request.Request>`
309
+ * `csrf_token` is used to build for {jinjax:component}`CsrfToken`.
310
+ """
48
311
  return {
49
312
  "request": self.request,
50
313
  "csrf_token": {
@@ -69,7 +332,7 @@ class JinjaxRenderer(AbstractTemplateRenderer):
69
332
  )
70
333
 
71
334
  def pydantic_form(
72
- self, model: ModelResult[Any], *, token: Optional[str] = None
335
+ self, model: FormModel[Any], *, token: Optional[str] = None
73
336
  ) -> Markup:
74
337
  return WidgetFactory(self, token).get_markup(model)
75
338
 
@@ -112,7 +375,7 @@ class JinjaxTemplateRenderer(AbstractTemplateRendererFactory):
112
375
  self.csrf_token_name = settings.csrf_token_name
113
376
  globals = resolve(settings.jinjax_global_catalog_class)().model_dump()
114
377
 
115
- self.catalog = Catalog(
378
+ self.catalog = InspectableCatalog(
116
379
  use_cache=settings.jinjax_use_cache,
117
380
  auto_reload=settings.jinjax_auto_reload,
118
381
  globals=globals,
@@ -19,16 +19,35 @@ def get_title(typ: Type[Any]) -> str:
19
19
 
20
20
 
21
21
  class Widget(abc.ABC, Generic[T]):
22
+ """
23
+ Base class for widget of pydantic fields.
24
+
25
+ :param name: field name.
26
+ :param value: field value.
27
+ :param title: title for the widget.
28
+ :param hint: hint for human.
29
+ :param aria_label: html input aria-label value.
30
+ :param value: current value.
31
+ :param error: error of the value if any.
32
+ :param children_types: childrens types list.
33
+ :param removable: display a button to remove the widget for optional fields.
34
+ :param token: token used to get unique id on the form.
35
+ """
36
+
22
37
  name: str
23
- "variable name, nested variables have dots"
38
+ "variable name, nested variables have dots."
39
+ value: T | None
40
+ """Value of the field."""
24
41
  title: str
25
- "Human title for the widget"
42
+ "Human title for the widget."
43
+ hint: str
44
+ "A help message for the the widget."
26
45
  aria_label: str
27
- "Non visible text alternative"
46
+ "Non visible text alternative."
28
47
  token: str
29
- "unique token to ensure id are unique in the DOM"
48
+ "unique token to ensure id are unique in the DOM."
30
49
  removable: bool
31
- "Indicate that the widget is removable from the dom"
50
+ "Indicate that the widget is removable from the dom."
32
51
 
33
52
  def __init__(
34
53
  self,
@@ -37,6 +56,7 @@ class Widget(abc.ABC, Generic[T]):
37
56
  value: T | None = None,
38
57
  error: str | None = None,
39
58
  title: str | None = None,
59
+ hint: str | None = None,
40
60
  token: str | None = None,
41
61
  aria_label: str | None = None,
42
62
  removable: bool = False,
@@ -45,6 +65,7 @@ class Widget(abc.ABC, Generic[T]):
45
65
  self.value = value
46
66
  self.error = error
47
67
  self.title = title or name.split(".")[-1]
68
+ self.hint = hint or ""
48
69
  self.aria_label = aria_label or ""
49
70
  self.token = token or secrets.token_urlsafe(4).replace("_", "-")
50
71
  self.removable = removable
@@ -52,10 +73,10 @@ class Widget(abc.ABC, Generic[T]):
52
73
 
53
74
  @abc.abstractmethod
54
75
  def get_template(self) -> str:
55
- ...
76
+ """Get the widget component template."""
56
77
 
57
78
  def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
58
- """Return the html version"""
79
+ """Return the html version."""
59
80
  return Markup(renderer.render_template(self.get_template(), widget=self))
60
81
 
61
82
 
@@ -67,6 +88,17 @@ def _get_fullname(typ: Type[Any]) -> str:
67
88
 
68
89
 
69
90
  class TypeWrapper:
91
+ """
92
+ Wrap children types for union type.
93
+
94
+ :param typ: Wrapped type.
95
+ :param route_prefix: route prefix used for ajax query to build type.
96
+ :param name: name of the field wrapped.
97
+ :param token: unique token to render unique id.
98
+ :param title: title to display.
99
+
100
+ """
101
+
70
102
  def __init__(
71
103
  self,
72
104
  typ: Type[Any],
@@ -83,19 +115,23 @@ class TypeWrapper:
83
115
 
84
116
  @property
85
117
  def fullname(self) -> str:
118
+ """Full name for the type."""
86
119
  return _get_fullname(self.typ)
87
120
 
88
121
  @property
89
122
  def id(self) -> str:
123
+ """Unique id to inject in the DOM."""
90
124
  name = self.name.replace("_", "-").replace(".", "-").replace(":", "-")
91
125
  typ = self.typ.__name__.replace("_", "-")
92
126
  return f"{name}-{typ}-{self.token}"
93
127
 
94
128
  @property
95
129
  def params(self) -> Mapping[str, str]:
130
+ """Params for the widget to render."""
96
131
  return {"name": self.name, "token": self.token, "title": self.title}
97
132
 
98
133
  @property
99
134
  def url(self) -> str:
135
+ """Url to fetch the widget."""
100
136
  ret = f"{self.route_prefix}/pydantic-form/widgets/{self.fullname}"
101
137
  return ret
@@ -1,12 +1,31 @@
1
+ """
2
+ Widget for field of type bool.
3
+ """
4
+
1
5
  from .base import Widget
2
6
 
3
7
 
4
8
  class BooleanWidget(Widget[bool]):
9
+ """
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
+ """
21
+
5
22
  def __init__(
6
23
  self,
7
24
  name: str,
8
25
  *,
9
26
  title: str | None,
27
+ hint: str | None = None,
28
+ aria_label: str | None = None,
10
29
  value: bool = False,
11
30
  error: str | None = None,
12
31
  removable: bool = False,
@@ -15,6 +34,8 @@ class BooleanWidget(Widget[bool]):
15
34
  super().__init__(
16
35
  name,
17
36
  title=title,
37
+ hint=hint,
38
+ aria_label=aria_label,
18
39
  value=value,
19
40
  error=error,
20
41
  removable=removable,