fastlifeweb 0.3.1__py3-none-any.whl → 0.4.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 (47) hide show
  1. fastlife/configurator/registry.py +2 -2
  2. fastlife/configurator/settings.py +3 -0
  3. fastlife/templates/A.jinja +30 -4
  4. fastlife/templates/Button.jinja +31 -17
  5. fastlife/templates/Checkbox.jinja +20 -2
  6. fastlife/templates/CsrfToken.jinja +0 -1
  7. fastlife/templates/Form.jinja +9 -3
  8. fastlife/templates/H1.jinja +15 -4
  9. fastlife/templates/H2.jinja +15 -4
  10. fastlife/templates/H3.jinja +14 -0
  11. fastlife/templates/H4.jinja +14 -0
  12. fastlife/templates/H5.jinja +14 -0
  13. fastlife/templates/H6.jinja +14 -0
  14. fastlife/templates/Input.jinja +29 -6
  15. fastlife/templates/Label.jinja +10 -2
  16. fastlife/templates/Option.jinja +4 -2
  17. fastlife/templates/P.jinja +9 -0
  18. fastlife/templates/Radio.jinja +29 -7
  19. fastlife/templates/Select.jinja +25 -6
  20. fastlife/templates/pydantic_form/Boolean.jinja +1 -1
  21. fastlife/templates/pydantic_form/Dropdown.jinja +4 -6
  22. fastlife/templates/pydantic_form/Hint.jinja +4 -0
  23. fastlife/templates/pydantic_form/Model.jinja +1 -1
  24. fastlife/templates/pydantic_form/Sequence.jinja +3 -3
  25. fastlife/templates/pydantic_form/Text.jinja +1 -3
  26. fastlife/templates/pydantic_form/Union.jinja +5 -5
  27. fastlife/templating/__init__.py +2 -2
  28. fastlife/templating/binding.py +4 -22
  29. fastlife/templating/renderer/__init__.py +5 -2
  30. fastlife/templating/renderer/abstract.py +7 -5
  31. fastlife/templating/renderer/jinjax.py +51 -23
  32. fastlife/templating/renderer/widgets/base.py +12 -2
  33. fastlife/templating/renderer/widgets/boolean.py +4 -3
  34. fastlife/templating/renderer/widgets/dropdown.py +7 -6
  35. fastlife/templating/renderer/widgets/factory.py +38 -21
  36. fastlife/templating/renderer/widgets/hidden.py +2 -3
  37. fastlife/templating/renderer/widgets/model.py +7 -6
  38. fastlife/templating/renderer/widgets/sequence.py +8 -9
  39. fastlife/templating/renderer/widgets/text.py +9 -5
  40. fastlife/templating/renderer/widgets/union.py +7 -6
  41. fastlife/testing/testclient.py +116 -55
  42. fastlife/views/pydantic_form.py +5 -2
  43. {fastlifeweb-0.3.1.dist-info → fastlifeweb-0.4.0.dist-info}/METADATA +5 -4
  44. fastlifeweb-0.4.0.dist-info/RECORD +69 -0
  45. fastlifeweb-0.3.1.dist-info/RECORD +0 -63
  46. {fastlifeweb-0.3.1.dist-info → fastlifeweb-0.4.0.dist-info}/LICENSE +0 -0
  47. {fastlifeweb-0.3.1.dist-info → fastlifeweb-0.4.0.dist-info}/WHEEL +0 -0
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
 
13
13
  from fastlife.shared_utils.resolver import resolve_path
14
14
 
15
- from .abstract import AbstractTemplateRenderer
15
+ from .abstract import AbstractTemplateRenderer, AbstractTemplateRendererFactory
16
16
 
17
17
 
18
18
  def build_searchpath(template_search_path: str) -> Sequence[str]:
@@ -27,33 +27,35 @@ def build_searchpath(template_search_path: str) -> Sequence[str]:
27
27
  return searchpath
28
28
 
29
29
 
30
- class JinjaxTemplateRenderer(AbstractTemplateRenderer):
31
- route_prefix: str
32
- """Used to prefix url to fetch fast life widgets."""
33
-
34
- def __init__(self, settings: "Settings") -> None:
35
- self.route_prefix = settings.fastlife_route_prefix
36
- self.form_data_model_prefix = settings.form_data_model_prefix
37
- self.csrf_token_name = settings.csrf_token_name
38
-
39
- self.catalog = Catalog()
40
- for path in build_searchpath(settings.template_search_path):
41
- self.catalog.add_folder(path)
30
+ class JinjaxRenderer(AbstractTemplateRenderer):
31
+ def __init__(
32
+ self,
33
+ catalog: Catalog,
34
+ request: Request,
35
+ csrf_token_name: str,
36
+ form_data_model_prefix: str,
37
+ route_prefix: str,
38
+ ):
39
+ self.route_prefix = route_prefix
40
+ self.catalog = catalog
41
+ self.request = request
42
+ self.csrf_token_name = csrf_token_name
43
+ self.form_data_model_prefix = form_data_model_prefix
42
44
 
43
- def render_page(self, request: Request, template: str, **params: Any) -> str:
44
- return self.catalog.render( # type:ignore
45
- template,
46
- request=request,
47
- csrf_token={
45
+ def build_globals(self) -> Mapping[str, Any]:
46
+ return {
47
+ "request": self.request,
48
+ "csrf_token": {
48
49
  "name": self.csrf_token_name,
49
- "value": request.scope.get(self.csrf_token_name, ""),
50
+ "value": self.request.scope.get(self.csrf_token_name, ""),
50
51
  },
51
- pydantic_form=self.pydantic_form,
52
- **params
53
- )
52
+ "pydantic_form": self.pydantic_form,
53
+ }
54
54
 
55
55
  def render_template(self, template: str, **params: Any) -> str:
56
- return self.catalog.render(template, **params) # type:ignore
56
+ return self.catalog.render( # type: ignore
57
+ template, __globals=self.build_globals(), **params
58
+ )
57
59
 
58
60
  def pydantic_form(
59
61
  self,
@@ -71,3 +73,29 @@ class JinjaxTemplateRenderer(AbstractTemplateRenderer):
71
73
  removable=removable,
72
74
  field=field,
73
75
  )
76
+
77
+
78
+ class JinjaxTemplateRenderer(AbstractTemplateRendererFactory):
79
+ route_prefix: str
80
+ """Used to prefix url to fetch fast life widgets."""
81
+
82
+ def __init__(self, settings: "Settings") -> None:
83
+ self.route_prefix = settings.fastlife_route_prefix
84
+ self.form_data_model_prefix = settings.form_data_model_prefix
85
+ self.csrf_token_name = settings.csrf_token_name
86
+
87
+ self.catalog = Catalog(
88
+ use_cache=settings.jinjax_use_cache,
89
+ auto_reload=settings.jinjax_auto_reload,
90
+ )
91
+ for path in build_searchpath(settings.template_search_path):
92
+ self.catalog.add_folder(path)
93
+
94
+ def __call__(self, request: Request) -> AbstractTemplateRenderer:
95
+ return JinjaxRenderer(
96
+ self.catalog,
97
+ request,
98
+ self.csrf_token_name,
99
+ self.form_data_model_prefix,
100
+ self.route_prefix,
101
+ )
@@ -1,12 +1,14 @@
1
1
  import abc
2
2
  import secrets
3
- from typing import Any, Mapping, Optional, Type
3
+ from typing import Any, Generic, Mapping, Optional, Type, TypeVar
4
4
 
5
5
  from markupsafe import Markup
6
6
 
7
7
  from fastlife.shared_utils.infer import is_union
8
8
  from fastlife.templating.renderer.abstract import AbstractTemplateRenderer
9
9
 
10
+ T = TypeVar("T")
11
+
10
12
 
11
13
  def get_title(typ: Type[Any]) -> str:
12
14
  return getattr(
@@ -16,7 +18,7 @@ def get_title(typ: Type[Any]) -> str:
16
18
  )
17
19
 
18
20
 
19
- class Widget(abc.ABC):
21
+ class Widget(abc.ABC, Generic[T]):
20
22
  name: str
21
23
  "variable name, nested variables have dots"
22
24
  title: str
@@ -32,12 +34,14 @@ class Widget(abc.ABC):
32
34
  self,
33
35
  name: str,
34
36
  *,
37
+ value: Optional[T] = None,
35
38
  title: Optional[str] = None,
36
39
  token: Optional[str] = None,
37
40
  aria_label: Optional[str] = None,
38
41
  removable: bool = False,
39
42
  ):
40
43
  self.name = name
44
+ self.value = value
41
45
  self.title = title or name.split(".")[-1]
42
46
  self.aria_label = aria_label or ""
43
47
  self.token = token or secrets.token_urlsafe(4).replace("_", "-")
@@ -79,6 +83,12 @@ class TypeWrapper:
79
83
  def fullname(self) -> str:
80
84
  return _get_fullname(self.typ)
81
85
 
86
+ @property
87
+ def id(self) -> str:
88
+ name = self.name.replace("_", "-").replace(".", "-").replace(":", "-")
89
+ typ = self.typ.__name__.replace("_", "-")
90
+ return f"{name}-{typ}-{self.token}"
91
+
82
92
  @property
83
93
  def params(self) -> Mapping[str, str]:
84
94
  return {"name": self.name, "token": self.token, "title": self.title}
@@ -3,7 +3,7 @@ from typing import Optional
3
3
  from .base import Widget
4
4
 
5
5
 
6
- class BooleanWidget(Widget):
6
+ class BooleanWidget(Widget[bool]):
7
7
  def __init__(
8
8
  self,
9
9
  name: str,
@@ -13,8 +13,9 @@ class BooleanWidget(Widget):
13
13
  removable: bool = False,
14
14
  token: str,
15
15
  ) -> None:
16
- super().__init__(name, title=title, removable=removable, token=token)
17
- self.value = value
16
+ super().__init__(
17
+ name, title=title, value=value, removable=removable, token=token
18
+ )
18
19
 
19
20
  def get_template(self) -> str:
20
21
  return "pydantic_form.Boolean"
@@ -3,27 +3,28 @@ from typing import Optional, Sequence, Tuple
3
3
  from .base import Widget
4
4
 
5
5
 
6
- class DropDownWidget(Widget):
6
+ class DropDownWidget(Widget[str]):
7
7
  def __init__(
8
8
  self,
9
9
  name: str,
10
10
  *,
11
11
  title: Optional[str],
12
+ value: Optional[str] = None,
12
13
  options: Sequence[Tuple[str, str]] | Sequence[str],
13
14
  removable: bool = False,
14
- value: str = "",
15
15
  token: Optional[str] = None,
16
- help_text: Optional[str] = None,
16
+ hint: Optional[str] = None,
17
17
  ) -> None:
18
- super().__init__(name, title=title, token=token, removable=removable)
18
+ super().__init__(
19
+ name, value=value, title=title, token=token, removable=removable
20
+ )
19
21
  self.options: list[dict[str, str]] = []
20
22
  for opt in options:
21
23
  if isinstance(opt, tuple):
22
24
  self.options.append({"value": opt[0], "text": opt[1]})
23
25
  else:
24
26
  self.options.append({"value": opt, "text": opt})
25
- self.value = value
26
- self.help_text = help_text
27
+ self.hint = hint
27
28
 
28
29
  def get_template(self) -> str:
29
30
  return "pydantic_form.Dropdown"
@@ -2,7 +2,7 @@ import secrets
2
2
  from collections.abc import MutableSequence, Sequence
3
3
  from decimal import Decimal
4
4
  from types import NoneType
5
- from typing import Any, Literal, Mapping, Optional, Type, get_origin
5
+ from typing import Any, Literal, Mapping, Optional, Type, cast, get_origin
6
6
  from uuid import UUID
7
7
 
8
8
  from markupsafe import Markup
@@ -48,7 +48,7 @@ class WidgetFactory:
48
48
  prefix: str,
49
49
  removable: bool,
50
50
  field: FieldInfo | None = None,
51
- ) -> Widget:
51
+ ) -> Widget[Any]:
52
52
  return self.build(
53
53
  base,
54
54
  value=form_data.get(prefix, {}),
@@ -65,7 +65,22 @@ class WidgetFactory:
65
65
  value: Any,
66
66
  removable: bool,
67
67
  field: FieldInfo | None = None,
68
- ) -> Widget:
68
+ ) -> Widget[Any]:
69
+ if field and field.metadata:
70
+ for widget in field.metadata:
71
+ if issubclass(widget, Widget):
72
+ return cast(
73
+ Widget[Any],
74
+ widget(
75
+ name,
76
+ value=value,
77
+ removable=removable,
78
+ title=field.title if field else "",
79
+ aria_label=field.description if field else None,
80
+ token=self.token,
81
+ ),
82
+ )
83
+
69
84
  type_origin = get_origin(typ)
70
85
  if type_origin:
71
86
  if is_union(typ):
@@ -87,7 +102,7 @@ class WidgetFactory:
87
102
  if issubclass(typ, bool):
88
103
  return self.build_boolean(name, typ, field, value or False, removable)
89
104
 
90
- if issubclass(typ, EmailStr):
105
+ if issubclass(typ, EmailStr): # type: ignore
91
106
  return self.build_emailtype(name, typ, field, value or "", removable)
92
107
 
93
108
  if issubclass(typ, SecretStr):
@@ -96,7 +111,7 @@ class WidgetFactory:
96
111
  if issubclass(typ, (int, str, float, Decimal, UUID)):
97
112
  return self.build_simpletype(name, typ, field, value or "", removable)
98
113
 
99
- raise NotImplementedError(f"{typ} not implemented")
114
+ raise NotImplementedError(f"{typ} not implemented") # coverage: ignore
100
115
 
101
116
  def build_model(
102
117
  self,
@@ -105,14 +120,16 @@ class WidgetFactory:
105
120
  field: Optional[FieldInfo],
106
121
  value: Mapping[str, Any],
107
122
  removable: bool,
108
- ) -> Widget:
123
+ ) -> Widget[Any]:
109
124
  ret: dict[str, Any] = {}
110
125
  for key, field in typ.model_fields.items():
111
126
  child_key = f"{field_name}.{key}" if field_name else key
112
127
  if field.exclude:
113
128
  continue
114
129
  if field.annotation is None:
115
- raise ValueError(f"Missing annotation for {field} in {child_key}")
130
+ raise ValueError( # coverage: ignore
131
+ f"Missing annotation for {field} in {child_key}"
132
+ )
116
133
  ret[key] = self.build(
117
134
  field.annotation,
118
135
  name=child_key,
@@ -122,7 +139,7 @@ class WidgetFactory:
122
139
  )
123
140
  return ModelWidget(
124
141
  field_name,
125
- children_widget=list(ret.values()),
142
+ value=list(ret.values()),
126
143
  removable=removable,
127
144
  title=get_title(typ),
128
145
  token=self.token,
@@ -135,7 +152,7 @@ class WidgetFactory:
135
152
  field: Optional[FieldInfo],
136
153
  value: Any,
137
154
  removable: bool,
138
- ) -> Widget:
155
+ ) -> Widget[Any]:
139
156
  types: list[Type[Any]] = []
140
157
  # required = True
141
158
  for typ in field_type.__args__: # type: ignore
@@ -172,7 +189,7 @@ class WidgetFactory:
172
189
  widget = UnionWidget(
173
190
  field_name,
174
191
  # we assume those types are BaseModel
175
- child=child,
192
+ value=child,
176
193
  children_types=types, # type: ignore
177
194
  title=field.title if field else "",
178
195
  token=self.token,
@@ -188,7 +205,7 @@ class WidgetFactory:
188
205
  field: Optional[FieldInfo],
189
206
  value: Optional[Sequence[Any]],
190
207
  removable: bool,
191
- ) -> Widget:
208
+ ) -> Widget[Any]:
192
209
  typ = field_type.__args__[0] # type: ignore
193
210
  value = value or []
194
211
  items = [
@@ -203,9 +220,9 @@ class WidgetFactory:
203
220
  ]
204
221
  return SequenceWidget(
205
222
  field_name,
206
- help_text=field.description if field else "",
223
+ hint=field.description if field else "",
207
224
  title=field.title if field else "",
208
- items=items,
225
+ value=items,
209
226
  item_type=typ, # type: ignore
210
227
  token=self.token,
211
228
  removable=removable,
@@ -218,7 +235,7 @@ class WidgetFactory:
218
235
  field: FieldInfo | None,
219
236
  value: bool,
220
237
  removable: bool,
221
- ) -> Widget:
238
+ ) -> Widget[Any]:
222
239
  return BooleanWidget(
223
240
  field_name,
224
241
  removable=removable,
@@ -234,10 +251,10 @@ class WidgetFactory:
234
251
  field: FieldInfo | None,
235
252
  value: str | int | float,
236
253
  removable: bool,
237
- ) -> Widget:
254
+ ) -> Widget[Any]:
238
255
  return TextWidget(
239
256
  field_name,
240
- help_text=field.description if field else "",
257
+ hint=field.description if field else "",
241
258
  input_type="email",
242
259
  placeholder=str(field.examples[0]) if field and field.examples else None,
243
260
  removable=removable,
@@ -253,10 +270,10 @@ class WidgetFactory:
253
270
  field: FieldInfo | None,
254
271
  value: SecretStr | str,
255
272
  removable: bool,
256
- ) -> Widget:
273
+ ) -> Widget[Any]:
257
274
  return TextWidget(
258
275
  field_name,
259
- help_text=field.description if field else "",
276
+ hint=field.description if field else "",
260
277
  input_type="password",
261
278
  placeholder=str(field.examples[0]) if field and field.examples else None,
262
279
  removable=removable,
@@ -272,7 +289,7 @@ class WidgetFactory:
272
289
  field: FieldInfo | None,
273
290
  value: str | int | float,
274
291
  removable: bool,
275
- ) -> Widget:
292
+ ) -> Widget[Any]:
276
293
  choices: list[str] = field_type.__args__ # type: ignore
277
294
  if len(choices) == 1:
278
295
  return HiddenWidget(
@@ -296,10 +313,10 @@ class WidgetFactory:
296
313
  field: FieldInfo | None,
297
314
  value: str | int | float,
298
315
  removable: bool,
299
- ) -> Widget:
316
+ ) -> Widget[Any]:
300
317
  return TextWidget(
301
318
  field_name,
302
- help_text=field.description if field else None,
319
+ hint=field.description if field else None,
303
320
  placeholder=str(field.examples[0]) if field and field.examples else None,
304
321
  aria_label=field.description if field else None,
305
322
  removable=removable,
@@ -1,7 +1,7 @@
1
1
  from .base import Widget
2
2
 
3
3
 
4
- class HiddenWidget(Widget):
4
+ class HiddenWidget(Widget[str]):
5
5
  def __init__(
6
6
  self,
7
7
  name: str,
@@ -9,8 +9,7 @@ class HiddenWidget(Widget):
9
9
  value: str,
10
10
  token: str,
11
11
  ) -> None:
12
- super().__init__(name, token=token)
13
- self.value = value
12
+ super().__init__(name, value=value, token=token)
14
13
 
15
14
  def get_template(self) -> str:
16
15
  return "pydantic_form.Hidden"
@@ -1,4 +1,4 @@
1
- from typing import Sequence
1
+ from typing import Any, Sequence
2
2
 
3
3
  from markupsafe import Markup
4
4
 
@@ -7,25 +7,26 @@ from fastlife.templating.renderer.abstract import AbstractTemplateRenderer
7
7
  from .base import Widget
8
8
 
9
9
 
10
- class ModelWidget(Widget):
10
+ class ModelWidget(Widget[Sequence[Widget[Any]]]):
11
11
  def __init__(
12
12
  self,
13
13
  name: str,
14
14
  *,
15
- children_widget: Sequence[Widget],
15
+ value: Sequence[Widget[Any]],
16
16
  removable: bool,
17
17
  title: str,
18
18
  token: str,
19
19
  ):
20
- super().__init__(name, title=title, removable=removable, token=token)
21
- self.children_widget = children_widget
20
+ super().__init__(
21
+ name, title=title, value=value, removable=removable, token=token
22
+ )
22
23
 
23
24
  def get_template(self) -> str:
24
25
  return "pydantic_form.Model"
25
26
 
26
27
  def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
27
28
  """Return the html version"""
28
- children_widget = [child.to_html(renderer) for child in self.children_widget]
29
+ children_widget = [child.to_html(renderer) for child in self.value or []]
29
30
  kwargs = {
30
31
  "widget": self,
31
32
  "children_widget": children_widget,
@@ -7,24 +7,23 @@ from fastlife.templating.renderer.abstract import AbstractTemplateRenderer
7
7
  from .base import TypeWrapper, Widget
8
8
 
9
9
 
10
- class SequenceWidget(Widget):
11
- items: Sequence[Widget]
12
-
10
+ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
13
11
  def __init__(
14
12
  self,
15
13
  name: str,
16
14
  *,
17
15
  title: Optional[str],
18
- help_text: Optional[str],
19
- items: Optional[Sequence[Widget]],
16
+ hint: Optional[str],
17
+ value: Optional[Sequence[Widget[Any]]],
20
18
  item_type: Type[Any],
21
19
  token: str,
22
20
  removable: bool,
23
21
  ):
24
- super().__init__(name, title=title, token=token, removable=removable)
25
- self.items = items or []
22
+ super().__init__(
23
+ name, value=value, title=title, token=token, removable=removable
24
+ )
26
25
  self.item_type = item_type
27
- self.help_text = help_text
26
+ self.hint = hint
28
27
 
29
28
  def get_template(self) -> str:
30
29
  return "pydantic_form/Sequence"
@@ -34,7 +33,7 @@ class SequenceWidget(Widget):
34
33
 
35
34
  def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
36
35
  """Return the html version"""
37
- children = [Markup(item.to_html(renderer)) for item in self.items]
36
+ children = [Markup(item.to_html(renderer)) for item in self.value or []]
38
37
  return Markup(
39
38
  renderer.render_template(
40
39
  self.get_template(),
@@ -3,7 +3,7 @@ from typing import Optional
3
3
  from .base import Widget
4
4
 
5
5
 
6
- class TextWidget(Widget):
6
+ class TextWidget(Widget[str]):
7
7
  def __init__(
8
8
  self,
9
9
  name: str,
@@ -14,15 +14,19 @@ class TextWidget(Widget):
14
14
  removable: bool = False,
15
15
  value: str = "",
16
16
  token: Optional[str] = None,
17
- help_text: Optional[str] = None,
17
+ hint: Optional[str] = None,
18
18
  input_type: str = "text"
19
19
  ) -> None:
20
20
  super().__init__(
21
- name, title=title, aria_label=aria_label, token=token, removable=removable
21
+ name,
22
+ value=value,
23
+ title=title,
24
+ aria_label=aria_label,
25
+ token=token,
26
+ removable=removable,
22
27
  )
23
28
  self.placeholder = placeholder or ""
24
- self.value = value
25
- self.help_text = help_text
29
+ self.hint = hint
26
30
  self.input_type = input_type
27
31
 
28
32
  def get_template(self) -> str:
@@ -1,4 +1,4 @@
1
- from typing import Optional, Sequence, Type, Union
1
+ from typing import Any, Optional, Sequence, Type, Union
2
2
 
3
3
  from markupsafe import Markup
4
4
  from pydantic import BaseModel
@@ -8,19 +8,20 @@ from fastlife.templating.renderer.abstract import AbstractTemplateRenderer
8
8
  from .base import TypeWrapper, Widget
9
9
 
10
10
 
11
- class UnionWidget(Widget):
11
+ class UnionWidget(Widget[Widget[Any]]):
12
12
  def __init__(
13
13
  self,
14
14
  name: str,
15
15
  *,
16
16
  title: Optional[str],
17
- child: Optional[Widget],
17
+ value: Optional[Widget[Any]],
18
18
  children_types: Sequence[Type[BaseModel]],
19
19
  token: str,
20
20
  removable: bool,
21
21
  ):
22
- super().__init__(name, title=title, token=token, removable=removable)
23
- self.child = child
22
+ super().__init__(
23
+ name, value=value, title=title, token=token, removable=removable
24
+ )
24
25
  self.children_types = children_types
25
26
  self.parent_name = name
26
27
 
@@ -35,7 +36,7 @@ class UnionWidget(Widget):
35
36
 
36
37
  def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
37
38
  """Return the html version"""
38
- child = Markup(self.child.to_html(renderer)) if self.child else ""
39
+ child = Markup(self.value.to_html(renderer)) if self.value else ""
39
40
  return Markup(
40
41
  renderer.render_template(
41
42
  self.get_template(),