fh-pydantic-form 0.3.3__py3-none-any.whl → 0.3.5__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.

Potentially problematic release.


This version of fh-pydantic-form might be problematic. Click here for more details.

@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ import decimal
2
3
  from enum import Enum
3
4
  from typing import Literal, get_origin
4
5
 
@@ -17,6 +18,7 @@ from fh_pydantic_form.field_renderers import (
17
18
  BaseModelFieldRenderer,
18
19
  BooleanFieldRenderer,
19
20
  DateFieldRenderer,
21
+ DecimalFieldRenderer,
20
22
  EnumFieldRenderer,
21
23
  ListFieldRenderer,
22
24
  LiteralFieldRenderer,
@@ -55,6 +57,7 @@ def register_default_renderers() -> None:
55
57
  FieldRendererRegistry.register_type_renderer(bool, BooleanFieldRenderer)
56
58
  FieldRendererRegistry.register_type_renderer(int, NumberFieldRenderer)
57
59
  FieldRendererRegistry.register_type_renderer(float, NumberFieldRenderer)
60
+ FieldRendererRegistry.register_type_renderer(decimal.Decimal, DecimalFieldRenderer)
58
61
  FieldRendererRegistry.register_type_renderer(datetime.date, DateFieldRenderer)
59
62
  FieldRendererRegistry.register_type_renderer(datetime.time, TimeFieldRenderer)
60
63
 
@@ -87,13 +90,16 @@ def register_default_renderers() -> None:
87
90
 
88
91
  # Register list renderer for List[*] types
89
92
  def is_list_field(field_info):
90
- """Check if field is a list type"""
93
+ """Check if field is a list type, including Optional[List[...]]"""
91
94
  annotation = getattr(field_info, "annotation", None)
92
- return (
93
- annotation is not None
94
- and hasattr(annotation, "__origin__")
95
- and annotation.__origin__ is list
96
- )
95
+ if annotation is None:
96
+ return False
97
+
98
+ # Handle Optional[List[...]] by unwrapping the Optional
99
+ underlying_type = _get_underlying_type_if_optional(annotation)
100
+
101
+ # Check if the underlying type is a list
102
+ return get_origin(underlying_type) is list
97
103
 
98
104
  FieldRendererRegistry.register_type_renderer_with_predicate(
99
105
  is_list_field, ListFieldRenderer
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime as _dt
4
+ import decimal
4
5
  from enum import Enum
5
6
  from typing import Any, Literal, get_args, get_origin
6
7
 
@@ -25,6 +26,7 @@ _SIMPLE_DEFAULTS = {
25
26
  int: 0,
26
27
  float: 0.0,
27
28
  bool: False,
29
+ decimal.Decimal: decimal.Decimal("0"),
28
30
  _dt.date: lambda: _today(), # callable - gets current date (late-bound)
29
31
  _dt.time: lambda: _dt.time(0, 0), # callable - midnight
30
32
  }
@@ -52,6 +54,10 @@ def default_for_annotation(annotation: Any) -> Any:
52
54
  if _is_optional_type(annotation):
53
55
  return None
54
56
 
57
+ # List[T] → []
58
+ if origin is list:
59
+ return []
60
+
55
61
  # Literal[...] → first literal value
56
62
  if origin is Literal:
57
63
  return _first_literal_choice(annotation)
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import re
3
3
  from datetime import date, time
4
+ from decimal import Decimal
4
5
  from enum import Enum
5
6
  from typing import (
6
7
  Any,
@@ -510,7 +511,7 @@ class BaseFieldRenderer(MetricsRendererMixin):
510
511
  label_text_span = fh.Span(label_text, **span_attrs)
511
512
 
512
513
  # Prepare label attributes
513
- label_attrs = {"For": self.field_name}
514
+ label_attrs = {"for": self.field_name}
514
515
 
515
516
  # Build label classes with tokenized gap
516
517
  label_gap_class = spacing("label_gap", self.spacing)
@@ -705,6 +706,66 @@ class NumberFieldRenderer(BaseFieldRenderer):
705
706
  return mui.Input(**input_attrs)
706
707
 
707
708
 
709
+ class DecimalFieldRenderer(BaseFieldRenderer):
710
+ """Renderer for decimal.Decimal fields"""
711
+
712
+ def __init__(self, *args, **kwargs):
713
+ """Initialize decimal field renderer, passing all arguments to parent"""
714
+ super().__init__(*args, **kwargs)
715
+
716
+ def render_input(self) -> FT:
717
+ """
718
+ Render input element for decimal fields
719
+
720
+ Returns:
721
+ A NumberInput component appropriate for decimal values
722
+ """
723
+ # Determine if field is required
724
+ has_default = get_default(self.field_info) is not _UNSET
725
+ is_field_required = not self.is_optional and not has_default
726
+
727
+ placeholder_text = f"Enter {self.original_field_name.replace('_', ' ')}"
728
+ if self.is_optional:
729
+ placeholder_text += " (Optional)"
730
+
731
+ input_cls_parts = ["w-full"]
732
+ input_spacing_cls = spacing_many(
733
+ ["input_size", "input_padding", "input_line_height", "input_font_size"],
734
+ self.spacing,
735
+ )
736
+ if input_spacing_cls:
737
+ input_cls_parts.append(input_spacing_cls)
738
+
739
+ # Convert Decimal value to string for display
740
+ if isinstance(self.value, Decimal):
741
+ # Use format to avoid scientific notation
742
+ display_value = format(self.value, "f")
743
+ # Normalize zero values to display as "0"
744
+ if self.value == 0:
745
+ display_value = "0"
746
+ elif self.value is not None:
747
+ display_value = str(self.value)
748
+ else:
749
+ display_value = ""
750
+
751
+ input_attrs = {
752
+ "value": display_value,
753
+ "id": self.field_name,
754
+ "name": self.field_name,
755
+ "type": "number",
756
+ "placeholder": placeholder_text,
757
+ "required": is_field_required,
758
+ "cls": " ".join(input_cls_parts),
759
+ "step": "any", # Allow arbitrary decimal precision
760
+ }
761
+
762
+ # Only add the disabled attribute if the field should actually be disabled
763
+ if self.disabled:
764
+ input_attrs["disabled"] = True
765
+
766
+ return mui.Input(**input_attrs)
767
+
768
+
708
769
  class BooleanFieldRenderer(BaseFieldRenderer):
709
770
  """Renderer for boolean fields"""
710
771
 
@@ -1523,12 +1584,15 @@ class ListFieldRenderer(BaseFieldRenderer):
1523
1584
  annotation = getattr(self.field_info, "annotation", None)
1524
1585
  item_type = None # Initialize here to avoid UnboundLocalError
1525
1586
 
1587
+ # Handle Optional[List[...]] by unwrapping the Optional first
1588
+ base_annotation = _get_underlying_type_if_optional(annotation)
1589
+
1526
1590
  if (
1527
- annotation is not None
1528
- and hasattr(annotation, "__origin__")
1529
- and annotation.__origin__ is list
1591
+ base_annotation is not None
1592
+ and hasattr(base_annotation, "__origin__")
1593
+ and base_annotation.__origin__ is list
1530
1594
  ):
1531
- item_type = annotation.__args__[0]
1595
+ item_type = base_annotation.__args__[0]
1532
1596
 
1533
1597
  if not item_type:
1534
1598
  logger.error(f"Cannot determine item type for list field {self.field_name}")
@@ -1600,10 +1664,20 @@ class ListFieldRenderer(BaseFieldRenderer):
1600
1664
  if self.disabled:
1601
1665
  add_button_attrs["disabled"] = "true"
1602
1666
 
1667
+ # Differentiate message for Optional[List] vs required List
1668
+ if self.is_optional:
1669
+ empty_message = (
1670
+ "No items in this optional list. Click 'Add Item' if needed."
1671
+ )
1672
+ else:
1673
+ empty_message = (
1674
+ "No items in this required list. Click 'Add Item' to create one."
1675
+ )
1676
+
1603
1677
  empty_state = mui.Alert(
1604
1678
  fh.Div(
1605
1679
  mui.UkIcon("info", cls="mr-2"),
1606
- "No items in this list. Click 'Add Item' to create one.",
1680
+ empty_message,
1607
1681
  mui.Button("Add Item", **add_button_attrs),
1608
1682
  cls="flex flex-col items-start",
1609
1683
  ),
@@ -7,8 +7,8 @@ from typing import (
7
7
  Optional,
8
8
  Tuple,
9
9
  Union,
10
- get_origin,
11
10
  get_args,
11
+ get_origin,
12
12
  )
13
13
 
14
14
  from fh_pydantic_form.type_helpers import (
@@ -438,7 +438,7 @@ def _parse_list_fields(
438
438
  list_field_defs: Dict[str, Dict[str, Any]],
439
439
  base_prefix: str = "",
440
440
  exclude_fields: Optional[List[str]] = None,
441
- ) -> Dict[str, List[Any]]:
441
+ ) -> Dict[str, Optional[List[Any]]]:
442
442
  """
443
443
  Parse list fields from form data by analyzing keys and reconstructing ordered lists.
444
444
 
@@ -490,7 +490,7 @@ def _parse_list_fields(
490
490
  list_items_temp[field_name][idx_str][subfield] = value
491
491
 
492
492
  # Build final lists based on tracked order
493
- final_lists = {}
493
+ final_lists: Dict[str, Optional[List[Any]]] = {}
494
494
  for field_name, ordered_indices in list_item_indices_ordered.items():
495
495
  field_def = list_field_defs[field_name]
496
496
  item_type = field_def["item_type"]
@@ -544,8 +544,12 @@ def _parse_list_fields(
544
544
  if field_name in final_lists:
545
545
  continue
546
546
 
547
- # User submitted form with zero items → honour intent with empty list
548
- final_lists[field_name] = []
547
+ # User submitted form with zero items → honour intent with None for Optional[List]
548
+ field_info = field_def["field_info"]
549
+ if _is_optional_type(field_info.annotation):
550
+ final_lists[field_name] = None # Use None for empty Optional[List]
551
+ else:
552
+ final_lists[field_name] = [] # Regular empty list for required fields
549
553
 
550
554
  return final_lists
551
555
 
@@ -435,7 +435,7 @@ class PydanticForm(Generic[ModelType]):
435
435
  try:
436
436
  default_factory = field_info.default_factory
437
437
  if callable(default_factory):
438
- initial_value = default_factory()
438
+ initial_value = default_factory() # type: ignore[call-arg]
439
439
  else:
440
440
  initial_value = None
441
441
  logger.warning(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-pydantic-form
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: a library to turn any pydantic BaseModel object into a fasthtml/monsterui input form
5
5
  Project-URL: Homepage, https://github.com/Marcura/fh-pydantic-form
6
6
  Project-URL: Repository, https://github.com/Marcura/fh-pydantic-form
@@ -1,17 +1,17 @@
1
- fh_pydantic_form/__init__.py,sha256=uNDN6UXIM25U7NazFi0Y9ivAeA8plERrRBk7TOd6P6M,4313
1
+ fh_pydantic_form/__init__.py,sha256=uPN0pwErHoe69tDoCDdIYD_iCslMKRgfYw6pvL4McHE,4608
2
2
  fh_pydantic_form/color_utils.py,sha256=M0HSXX0i-lSHkcsgesxw7d3PEAnLsZ46i_STymZAM_k,18271
3
3
  fh_pydantic_form/comparison_form.py,sha256=Y-OKAjTxMiixBC05SK1ofZ_A3zkvjUFp8tTE-_wUn60,21660
4
4
  fh_pydantic_form/constants.py,sha256=-N9wzkibFNn-V6cO8iWTQ7_xBvwSr2hBdq-m3apmW4M,169
5
- fh_pydantic_form/defaults.py,sha256=Pwv46v7e43cykx4Pt01e4nw-6FBkHmPvTZK36ZTZqgA,6068
6
- fh_pydantic_form/field_renderers.py,sha256=wX8XhesFH7Pt8l0stYR4FVQciVo2GBxADGnvwofu6YU,80944
7
- fh_pydantic_form/form_parser.py,sha256=7GTOBNQSfJltDHZnM12FxTJj0X_IMWoDV3lJbDF3EpY,25879
8
- fh_pydantic_form/form_renderer.py,sha256=3v4NPFQJ37m__kNo-sbNqaItR2AvcDzn5KRt44qCBTo,36198
5
+ fh_pydantic_form/defaults.py,sha256=9vV0f4PapTOgqNsIxoW6rEbpYO66O4uiKvpd6hzR1-M,6189
6
+ fh_pydantic_form/field_renderers.py,sha256=twAqHvWkDL6M-HRcdi_JrR2BgzkPmMyF_vojId5ABa0,83574
7
+ fh_pydantic_form/form_parser.py,sha256=DsOMiCXRRHswL37Vqv7bvMv3Q7nQdXp0oP3_ZoIJfVc,26172
8
+ fh_pydantic_form/form_renderer.py,sha256=0OrOA0--KT1r-HX37AGeDkC2rmbUSbJ4fgL04wG7pI8,36224
9
9
  fh_pydantic_form/list_path.py,sha256=AA8bmDmaYy4rlGIvQOOZ0fP2tgcimNUB2Re5aVGnYc8,5182
10
10
  fh_pydantic_form/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  fh_pydantic_form/registry.py,sha256=b5zIjOpfmCUCs2njgp4PdDu70ioDIAfl49oU-Nf2pg4,4810
12
12
  fh_pydantic_form/type_helpers.py,sha256=JUzHT8YrWj2_g7f_Wr2GL9i3BgP1zZftFrrO8xDPeis,7409
13
13
  fh_pydantic_form/ui_style.py,sha256=UPK5OBwUVVTLnfvQ-yKukz2vbKZaT_GauaNB7OGc-Uw,3848
14
- fh_pydantic_form-0.3.3.dist-info/METADATA,sha256=oLRfQeZIxBjls-FwIN8yXgcJOLDJmw-UXLeeo6R7kmg,38420
15
- fh_pydantic_form-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- fh_pydantic_form-0.3.3.dist-info/licenses/LICENSE,sha256=AOi2eNK3D2aDycRHfPRiuACZ7WPBsKHTV2tTYNl7cls,577
17
- fh_pydantic_form-0.3.3.dist-info/RECORD,,
14
+ fh_pydantic_form-0.3.5.dist-info/METADATA,sha256=S3vgfnQpOhl4YAcDPje_4w9lNOtOnIRslrgnYiiYrT8,38420
15
+ fh_pydantic_form-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ fh_pydantic_form-0.3.5.dist-info/licenses/LICENSE,sha256=AOi2eNK3D2aDycRHfPRiuACZ7WPBsKHTV2tTYNl7cls,577
17
+ fh_pydantic_form-0.3.5.dist-info/RECORD,,