fh-pydantic-form 0.3.3__tar.gz → 0.3.5__tar.gz
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.
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/PKG-INFO +1 -1
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/RELEASE_NOTES.md +12 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/pyproject.toml +1 -1
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/__init__.py +12 -6
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/defaults.py +6 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/field_renderers.py +80 -6
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/form_parser.py +9 -5
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/form_renderer.py +1 -1
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/.github/workflows/build.yaml +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/.github/workflows/publish.yaml +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/.gitignore +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/.pre-commit-config.yaml +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/LICENSE +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/README.md +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/color_utils.py +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/comparison_form.py +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/constants.py +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/list_path.py +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/py.typed +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/registry.py +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/type_helpers.py +0 -0
- {fh_pydantic_form-0.3.3 → fh_pydantic_form-0.3.5}/src/fh_pydantic_form/ui_style.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fh-pydantic-form
|
|
3
|
-
Version: 0.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,5 +1,17 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## Version 0.3.5 (2025-07-17)
|
|
4
|
+
|
|
5
|
+
- **NEW**: Added support for `decimal.Decimal` fields with dedicated field renderer
|
|
6
|
+
- **FIXED**: Scientific notation display issues in decimal values
|
|
7
|
+
- **IMPROVED**: MyPy type checking compliance
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Version 0.3.4 (2025-07-15)
|
|
12
|
+
|
|
13
|
+
- **NEW**: Added support for Optional[List[..]] types in form fields
|
|
14
|
+
|
|
3
15
|
## Version 0.3.3 (2025-07-09)
|
|
4
16
|
|
|
5
17
|
- fix bug where label_color was not passed down in ComparisonForm
|
|
@@ -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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 = {"
|
|
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
|
-
|
|
1528
|
-
and hasattr(
|
|
1529
|
-
and
|
|
1591
|
+
base_annotation is not None
|
|
1592
|
+
and hasattr(base_annotation, "__origin__")
|
|
1593
|
+
and base_annotation.__origin__ is list
|
|
1530
1594
|
):
|
|
1531
|
-
item_type =
|
|
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
|
-
|
|
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
|
|
548
|
-
|
|
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(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|