fastlifeweb 0.5.1__py3-none-any.whl → 0.6.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.
Files changed (29) hide show
  1. fastlife/__init__.py +3 -2
  2. fastlife/request/form_data.py +7 -22
  3. fastlife/request/model_result.py +91 -0
  4. fastlife/shared_utils/infer.py +1 -1
  5. fastlife/templates/pydantic_form/Boolean.jinja +7 -2
  6. fastlife/templates/pydantic_form/Checklist.jinja +3 -1
  7. fastlife/templates/pydantic_form/Dropdown.jinja +1 -0
  8. fastlife/templates/pydantic_form/Error.jinja +4 -0
  9. fastlife/templates/pydantic_form/Model.jinja +1 -0
  10. fastlife/templates/pydantic_form/Sequence.jinja +1 -0
  11. fastlife/templates/pydantic_form/Text.jinja +1 -0
  12. fastlife/templates/pydantic_form/Union.jinja +4 -4
  13. fastlife/templating/renderer/abstract.py +16 -2
  14. fastlife/templating/renderer/jinjax.py +25 -4
  15. fastlife/templating/renderer/widgets/base.py +7 -5
  16. fastlife/templating/renderer/widgets/boolean.py +7 -1
  17. fastlife/templating/renderer/widgets/checklist.py +13 -2
  18. fastlife/templating/renderer/widgets/dropdown.py +7 -1
  19. fastlife/templating/renderer/widgets/factory.py +69 -16
  20. fastlife/templating/renderer/widgets/model.py +7 -1
  21. fastlife/templating/renderer/widgets/sequence.py +7 -1
  22. fastlife/templating/renderer/widgets/text.py +3 -1
  23. fastlife/templating/renderer/widgets/union.py +7 -1
  24. fastlife/testing/testclient.py +102 -0
  25. fastlife/views/pydantic_form.py +6 -2
  26. {fastlifeweb-0.5.1.dist-info → fastlifeweb-0.6.1.dist-info}/METADATA +2 -2
  27. {fastlifeweb-0.5.1.dist-info → fastlifeweb-0.6.1.dist-info}/RECORD +29 -27
  28. {fastlifeweb-0.5.1.dist-info → fastlifeweb-0.6.1.dist-info}/LICENSE +0 -0
  29. {fastlifeweb-0.5.1.dist-info → fastlifeweb-0.6.1.dist-info}/WHEEL +0 -0
@@ -2,6 +2,7 @@ import secrets
2
2
  from collections.abc import MutableSequence, Sequence
3
3
  from decimal import Decimal
4
4
  from enum import Enum
5
+ from inspect import isclass
5
6
  from types import NoneType
6
7
  from typing import Any, Literal, Mapping, Optional, Type, cast, get_origin
7
8
  from uuid import UUID
@@ -10,6 +11,7 @@ from markupsafe import Markup
10
11
  from pydantic import BaseModel, EmailStr, SecretStr, ValidationError
11
12
  from pydantic.fields import FieldInfo
12
13
 
14
+ from fastlife.request.model_result import ModelResult
13
15
  from fastlife.shared_utils.infer import is_complex_type, is_union
14
16
  from fastlife.templating.renderer.abstract import AbstractTemplateRenderer
15
17
  from fastlife.templating.renderer.widgets.boolean import BooleanWidget
@@ -31,21 +33,25 @@ class WidgetFactory:
31
33
 
32
34
  def get_markup(
33
35
  self,
34
- base: Type[Any],
35
- form_data: Mapping[str, Any],
36
+ model: ModelResult[Any],
36
37
  *,
37
- prefix: str,
38
38
  removable: bool,
39
39
  field: FieldInfo | None = None,
40
40
  ) -> Markup:
41
41
  return self.get_widget(
42
- base, form_data, prefix=prefix, removable=removable, field=field
42
+ model.model.__class__,
43
+ model.form_data,
44
+ model.errors,
45
+ prefix=model.prefix,
46
+ removable=removable,
47
+ field=field,
43
48
  ).to_html(self.renderer)
44
49
 
45
50
  def get_widget(
46
51
  self,
47
52
  base: Type[Any],
48
53
  form_data: Mapping[str, Any],
54
+ form_errors: Mapping[str, Any],
49
55
  *,
50
56
  prefix: str,
51
57
  removable: bool,
@@ -54,6 +60,7 @@ class WidgetFactory:
54
60
  return self.build(
55
61
  base,
56
62
  value=form_data.get(prefix, {}),
63
+ form_errors=form_errors,
57
64
  name=prefix,
58
65
  removable=removable,
59
66
  field=field,
@@ -66,11 +73,12 @@ class WidgetFactory:
66
73
  name: str = "",
67
74
  value: Any,
68
75
  removable: bool,
76
+ form_errors: Mapping[str, Any],
69
77
  field: FieldInfo | None = None,
70
78
  ) -> Widget[Any]:
71
79
  if field and field.metadata:
72
80
  for widget in field.metadata:
73
- if issubclass(widget, Widget):
81
+ if isclass(widget) and issubclass(widget, Widget):
74
82
  return cast(
75
83
  Widget[Any],
76
84
  widget(
@@ -80,44 +88,59 @@ class WidgetFactory:
80
88
  title=field.title if field else "",
81
89
  aria_label=field.description if field else None,
82
90
  token=self.token,
91
+ error=form_errors.get(name),
83
92
  ),
84
93
  )
85
94
 
86
95
  type_origin = get_origin(typ)
87
96
  if type_origin:
88
97
  if is_union(typ):
89
- return self.build_union(name, typ, field, value, removable)
98
+ return self.build_union(name, typ, field, value, form_errors, removable)
90
99
 
91
100
  if (
92
101
  type_origin is Sequence
93
102
  or type_origin is MutableSequence
94
103
  or type_origin is list
95
104
  ):
96
- return self.build_sequence(name, typ, field, value, removable)
105
+ return self.build_sequence(
106
+ name, typ, field, value, form_errors, removable
107
+ )
97
108
 
98
109
  if type_origin is Literal:
99
- return self.build_literal(name, typ, field, value, removable)
110
+ return self.build_literal(
111
+ name, typ, field, value, form_errors, removable
112
+ )
100
113
 
101
114
  if type_origin is set:
102
- return self.build_set(name, typ, field, value, removable)
115
+ return self.build_set(name, typ, field, value, form_errors, removable)
103
116
 
104
117
  if issubclass(typ, Enum): # if it raises here, the type_origin is unknown
105
- return self.build_enum(name, typ, field, value, removable)
118
+ return self.build_enum(name, typ, field, value, form_errors, removable)
106
119
 
107
120
  if issubclass(typ, BaseModel): # if it raises here, the type_origin is unknown
108
- return self.build_model(name, typ, field, value or {}, removable)
121
+ return self.build_model(
122
+ name, typ, field, value or {}, form_errors, removable
123
+ )
109
124
 
110
125
  if issubclass(typ, bool):
111
- return self.build_boolean(name, typ, field, value or False, removable)
126
+ return self.build_boolean(
127
+ name, typ, field, value or False, form_errors, removable
128
+ )
112
129
 
113
130
  if issubclass(typ, EmailStr): # type: ignore
114
- return self.build_emailtype(name, typ, field, value or "", removable)
131
+ return self.build_emailtype(
132
+ name, typ, field, value or "", form_errors, removable
133
+ )
115
134
 
116
135
  if issubclass(typ, SecretStr):
117
- return self.build_secretstr(name, typ, field, value or "", removable)
136
+ return self.build_secretstr(
137
+ name, typ, field, value or "", form_errors, removable
138
+ )
118
139
 
119
140
  if issubclass(typ, (int, str, float, Decimal, UUID)):
120
- return self.build_simpletype(name, typ, field, value or "", removable)
141
+ return self.build_simpletype(
142
+ name, typ, field, value or "", form_errors, removable
143
+ )
121
144
 
122
145
  raise NotImplementedError(f"{typ} not implemented") # coverage: ignore
123
146
 
@@ -127,6 +150,7 @@ class WidgetFactory:
127
150
  typ: Type[BaseModel],
128
151
  field: Optional[FieldInfo],
129
152
  value: Mapping[str, Any],
153
+ form_errors: Mapping[str, Any],
130
154
  removable: bool,
131
155
  ) -> Widget[Any]:
132
156
  ret: dict[str, Any] = {}
@@ -143,6 +167,7 @@ class WidgetFactory:
143
167
  name=child_key,
144
168
  field=field,
145
169
  value=value.get(key),
170
+ form_errors=form_errors,
146
171
  removable=False,
147
172
  )
148
173
  return ModelWidget(
@@ -151,6 +176,7 @@ class WidgetFactory:
151
176
  removable=removable,
152
177
  title=get_title(typ),
153
178
  token=self.token,
179
+ error=form_errors.get(field_name),
154
180
  )
155
181
 
156
182
  def build_union(
@@ -159,6 +185,7 @@ class WidgetFactory:
159
185
  field_type: Type[Any],
160
186
  field: Optional[FieldInfo],
161
187
  value: Any,
188
+ form_errors: Mapping[str, Any],
162
189
  removable: bool,
163
190
  ) -> Widget[Any]:
164
191
  types: list[Type[Any]] = []
@@ -176,7 +203,12 @@ class WidgetFactory:
176
203
  and not is_complex_type(types[0])
177
204
  ):
178
205
  return self.build(
179
- types[0], name=field_name, field=field, value=value, removable=False
206
+ types[0],
207
+ name=field_name,
208
+ field=field,
209
+ value=value,
210
+ form_errors=form_errors,
211
+ removable=False,
180
212
  )
181
213
  child = None
182
214
  if value:
@@ -191,6 +223,7 @@ class WidgetFactory:
191
223
  name=field_name,
192
224
  field=field,
193
225
  value=value,
226
+ form_errors=form_errors,
194
227
  removable=False,
195
228
  )
196
229
 
@@ -202,6 +235,7 @@ class WidgetFactory:
202
235
  title=field.title if field else "",
203
236
  token=self.token,
204
237
  removable=removable,
238
+ error=form_errors.get(field_name),
205
239
  )
206
240
 
207
241
  return widget
@@ -212,6 +246,7 @@ class WidgetFactory:
212
246
  field_type: Type[Any],
213
247
  field: Optional[FieldInfo],
214
248
  value: Optional[Sequence[Any]],
249
+ form_errors: Mapping[str, Any],
215
250
  removable: bool,
216
251
  ) -> Widget[Any]:
217
252
  typ = field_type.__args__[0] # type: ignore
@@ -222,6 +257,7 @@ class WidgetFactory:
222
257
  name=f"{field_name}.{idx}",
223
258
  value=v,
224
259
  field=field,
260
+ form_errors=form_errors,
225
261
  removable=True,
226
262
  )
227
263
  for idx, v in enumerate(value)
@@ -234,6 +270,7 @@ class WidgetFactory:
234
270
  item_type=typ, # type: ignore
235
271
  token=self.token,
236
272
  removable=removable,
273
+ error=form_errors.get(field_name),
237
274
  )
238
275
 
239
276
  def build_set(
@@ -242,6 +279,7 @@ class WidgetFactory:
242
279
  field_type: Type[Any],
243
280
  field: Optional[FieldInfo],
244
281
  value: Optional[Sequence[Any]],
282
+ form_errors: Mapping[str, Any],
245
283
  removable: bool,
246
284
  ) -> Widget[Any]:
247
285
  choice_wrapper = field_type.__args__[0]
@@ -257,6 +295,7 @@ class WidgetFactory:
257
295
  checked=c in value if value else False, # type: ignore
258
296
  name=field_name,
259
297
  token=self.token,
298
+ error=form_errors.get(f"{field_name}-{c}"),
260
299
  )
261
300
  for c in litchoice
262
301
  ]
@@ -271,6 +310,7 @@ class WidgetFactory:
271
310
  checked=e.name in value if value else False, # type: ignore
272
311
  name=field_name,
273
312
  token=self.token,
313
+ error=form_errors.get(f"{field_name}-{e.name}"),
274
314
  )
275
315
  for e in choice_wrapper
276
316
  ]
@@ -283,6 +323,7 @@ class WidgetFactory:
283
323
  token=self.token,
284
324
  value=choices,
285
325
  removable=removable,
326
+ error=form_errors.get(field_name),
286
327
  )
287
328
 
288
329
  def build_boolean(
@@ -291,6 +332,7 @@ class WidgetFactory:
291
332
  field_type: Type[Any],
292
333
  field: FieldInfo | None,
293
334
  value: bool,
335
+ form_errors: Mapping[str, Any],
294
336
  removable: bool,
295
337
  ) -> Widget[Any]:
296
338
  return BooleanWidget(
@@ -299,6 +341,7 @@ class WidgetFactory:
299
341
  title=field.title if field else "",
300
342
  token=self.token,
301
343
  value=value,
344
+ error=form_errors.get(field_name),
302
345
  )
303
346
 
304
347
  def build_emailtype(
@@ -307,6 +350,7 @@ class WidgetFactory:
307
350
  field_type: Type[Any],
308
351
  field: FieldInfo | None,
309
352
  value: str | int | float,
353
+ form_errors: Mapping[str, Any],
310
354
  removable: bool,
311
355
  ) -> Widget[Any]:
312
356
  return TextWidget(
@@ -318,6 +362,7 @@ class WidgetFactory:
318
362
  title=field.title if field else "",
319
363
  token=self.token,
320
364
  value=str(value),
365
+ error=form_errors.get(field_name),
321
366
  )
322
367
 
323
368
  def build_secretstr(
@@ -326,6 +371,7 @@ class WidgetFactory:
326
371
  field_type: Type[Any],
327
372
  field: FieldInfo | None,
328
373
  value: SecretStr | str,
374
+ form_errors: Mapping[str, Any],
329
375
  removable: bool,
330
376
  ) -> Widget[Any]:
331
377
  return TextWidget(
@@ -337,6 +383,7 @@ class WidgetFactory:
337
383
  title=field.title if field else "",
338
384
  token=self.token,
339
385
  value=value.get_secret_value() if isinstance(value, SecretStr) else value,
386
+ error=form_errors.get(field_name),
340
387
  )
341
388
 
342
389
  def build_literal(
@@ -345,6 +392,7 @@ class WidgetFactory:
345
392
  field_type: Type[Any], # a literal actually
346
393
  field: FieldInfo | None,
347
394
  value: str | int | float,
395
+ form_errors: Mapping[str, Any],
348
396
  removable: bool,
349
397
  ) -> Widget[Any]:
350
398
  choices: list[str] = field_type.__args__ # type: ignore
@@ -361,6 +409,7 @@ class WidgetFactory:
361
409
  title=field.title if field else "",
362
410
  token=self.token,
363
411
  value=str(value),
412
+ error=form_errors.get(field_name),
364
413
  )
365
414
 
366
415
  def build_enum(
@@ -369,6 +418,7 @@ class WidgetFactory:
369
418
  field_type: Type[Any], # an enum subclass
370
419
  field: FieldInfo | None,
371
420
  value: str | int | float,
421
+ form_errors: Mapping[str, Any],
372
422
  removable: bool,
373
423
  ) -> Widget[Any]:
374
424
  options = [(item.name, item.value) for item in field_type] # type: ignore
@@ -379,6 +429,7 @@ class WidgetFactory:
379
429
  title=field.title if field else "",
380
430
  token=self.token,
381
431
  value=str(value),
432
+ error=form_errors.get(field_name),
382
433
  )
383
434
 
384
435
  def build_simpletype(
@@ -387,6 +438,7 @@ class WidgetFactory:
387
438
  field_type: Type[Any],
388
439
  field: FieldInfo | None,
389
440
  value: str | int | float,
441
+ form_errors: Mapping[str, Any],
390
442
  removable: bool,
391
443
  ) -> Widget[Any]:
392
444
  return TextWidget(
@@ -398,4 +450,5 @@ class WidgetFactory:
398
450
  title=field.title if field else "",
399
451
  token=self.token,
400
452
  value=str(value),
453
+ error=form_errors.get(field_name),
401
454
  )
@@ -13,12 +13,18 @@ class ModelWidget(Widget[Sequence[Widget[Any]]]):
13
13
  name: str,
14
14
  *,
15
15
  value: Sequence[Widget[Any]],
16
+ error: str | None = None,
16
17
  removable: bool,
17
18
  title: str,
18
19
  token: str,
19
20
  ):
20
21
  super().__init__(
21
- name, title=title, value=value, removable=removable, token=token
22
+ name,
23
+ title=title,
24
+ value=value,
25
+ error=error,
26
+ removable=removable,
27
+ token=token,
22
28
  )
23
29
 
24
30
  def get_template(self) -> str:
@@ -15,12 +15,18 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
15
15
  title: Optional[str],
16
16
  hint: Optional[str],
17
17
  value: Optional[Sequence[Widget[Any]]],
18
+ error: str | None = None,
18
19
  item_type: Type[Any],
19
20
  token: str,
20
21
  removable: bool,
21
22
  ):
22
23
  super().__init__(
23
- name, value=value, title=title, token=token, removable=removable
24
+ name,
25
+ value=value,
26
+ error=error,
27
+ title=title,
28
+ token=token,
29
+ removable=removable,
24
30
  )
25
31
  self.item_type = item_type
26
32
  self.hint = hint
@@ -11,11 +11,12 @@ class TextWidget(Widget[str]):
11
11
  title: Optional[str],
12
12
  aria_label: Optional[str] = None,
13
13
  placeholder: Optional[str] = None,
14
+ error: str | None = None,
14
15
  removable: bool = False,
15
16
  value: str = "",
16
17
  token: Optional[str] = None,
17
18
  hint: Optional[str] = None,
18
- input_type: str = "text"
19
+ input_type: str = "text",
19
20
  ) -> None:
20
21
  super().__init__(
21
22
  name,
@@ -23,6 +24,7 @@ class TextWidget(Widget[str]):
23
24
  title=title,
24
25
  aria_label=aria_label,
25
26
  token=token,
27
+ error=error,
26
28
  removable=removable,
27
29
  )
28
30
  self.placeholder = placeholder or ""
@@ -15,12 +15,18 @@ class UnionWidget(Widget[Widget[Any]]):
15
15
  *,
16
16
  title: Optional[str],
17
17
  value: Optional[Widget[Any]],
18
+ error: str | None = None,
18
19
  children_types: Sequence[Type[BaseModel]],
19
20
  token: str,
20
21
  removable: bool,
21
22
  ):
22
23
  super().__init__(
23
- name, value=value, title=title, token=token, removable=removable
24
+ name,
25
+ value=value,
26
+ error=error,
27
+ title=title,
28
+ token=token,
29
+ removable=removable,
24
30
  )
25
31
  self.children_types = children_types
26
32
  self.parent_name = name