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

@@ -87,13 +87,16 @@ def register_default_renderers() -> None:
87
87
 
88
88
  # Register list renderer for List[*] types
89
89
  def is_list_field(field_info):
90
- """Check if field is a list type"""
90
+ """Check if field is a list type, including Optional[List[...]]"""
91
91
  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
- )
92
+ if annotation is None:
93
+ return False
94
+
95
+ # Handle Optional[List[...]] by unwrapping the Optional
96
+ underlying_type = _get_underlying_type_if_optional(annotation)
97
+
98
+ # Check if the underlying type is a list
99
+ return get_origin(underlying_type) is list
97
100
 
98
101
  FieldRendererRegistry.register_type_renderer_with_predicate(
99
102
  is_list_field, ListFieldRenderer
@@ -347,6 +347,13 @@ class ComparisonForm(Generic[ModelType]):
347
347
  # Determine comparison-specific refresh endpoint
348
348
  comparison_refresh = f"/compare/{self.name}/{'left' if form is self.left_form else 'right'}/refresh"
349
349
 
350
+ # Get label color for this field if specified
351
+ label_color = (
352
+ form.label_colors.get(field_name)
353
+ if hasattr(form, "label_colors")
354
+ else None
355
+ )
356
+
350
357
  # Create renderer
351
358
  renderer = renderer_cls(
352
359
  field_name=field_name,
@@ -357,6 +364,7 @@ class ComparisonForm(Generic[ModelType]):
357
364
  spacing=form.spacing,
358
365
  field_path=[field_name],
359
366
  form_name=form.name,
367
+ label_color=label_color, # Pass the label color if specified
360
368
  metrics_dict=form.metrics_dict, # Use form's own metrics
361
369
  refresh_endpoint_override=comparison_refresh, # Pass comparison-specific refresh endpoint
362
370
  )
@@ -375,77 +383,6 @@ class ComparisonForm(Generic[ModelType]):
375
383
  # Return wrapper with display: contents
376
384
  return fh.Div(*cells, id=wrapper_id, cls="contents")
377
385
 
378
- # def _create_field_pairs(
379
- # self,
380
- # ) -> List[Tuple[str, BaseFieldRenderer, BaseFieldRenderer]]:
381
- # """
382
- # Create pairs of renderers (left, right) for each field path
383
-
384
- # Returns:
385
- # List of (path_string, left_renderer, right_renderer) tuples
386
- # """
387
- # pairs = []
388
- # registry = FieldRendererRegistry()
389
-
390
- # # Walk through model fields to create renderer pairs
391
- # for field_name, field_info in self.model_class.model_fields.items():
392
- # # Skip fields that are excluded in either form
393
- # if field_name in (self.left_form.exclude_fields or []) or field_name in (
394
- # self.right_form.exclude_fields or []
395
- # ):
396
- # logger.debug(
397
- # f"Skipping field '{field_name}' - excluded in one or both forms"
398
- # )
399
- # continue
400
-
401
- # # Get values from each form
402
- # left_value = self.left_form.values_dict.get(field_name)
403
- # right_value = self.right_form.values_dict.get(field_name)
404
-
405
- # # Get the path string for comparison lookup
406
- # path_str = field_name
407
- # left_comparison_metric = self.left_metrics.get(path_str)
408
- # right_comparison_metric = self.right_metrics.get(path_str)
409
-
410
- # # Get renderer class
411
- # renderer_cls = registry.get_renderer(field_name, field_info)
412
- # if not renderer_cls:
413
- # from fh_pydantic_form.field_renderers import StringFieldRenderer
414
-
415
- # renderer_cls = StringFieldRenderer
416
-
417
- # # Create left renderer
418
- # left_renderer = renderer_cls(
419
- # field_name=field_name,
420
- # field_info=field_info,
421
- # value=left_value,
422
- # prefix=self.left_form.base_prefix,
423
- # disabled=self.left_form.disabled,
424
- # spacing=self.left_form.spacing,
425
- # field_path=[field_name],
426
- # form_name=self.left_form.name,
427
- # comparison=left_comparison_metric,
428
- # comparison_map=self.left_metrics, # Pass the full comparison map
429
- # )
430
-
431
- # # Create right renderer
432
- # right_renderer = renderer_cls(
433
- # field_name=field_name,
434
- # field_info=field_info,
435
- # value=right_value,
436
- # prefix=self.right_form.base_prefix,
437
- # disabled=self.right_form.disabled,
438
- # spacing=self.right_form.spacing,
439
- # field_path=[field_name],
440
- # form_name=self.right_form.name,
441
- # comparison=right_comparison_metric,
442
- # comparison_map=self.right_metrics, # Pass the full comparison map
443
- # )
444
-
445
- # pairs.append((path_str, left_renderer, right_renderer))
446
-
447
- # return pairs
448
-
449
386
  def render_inputs(self) -> FT:
450
387
  """
451
388
  Render the comparison form with side-by-side layout
@@ -1523,12 +1523,15 @@ class ListFieldRenderer(BaseFieldRenderer):
1523
1523
  annotation = getattr(self.field_info, "annotation", None)
1524
1524
  item_type = None # Initialize here to avoid UnboundLocalError
1525
1525
 
1526
+ # Handle Optional[List[...]] by unwrapping the Optional first
1527
+ base_annotation = _get_underlying_type_if_optional(annotation)
1528
+
1526
1529
  if (
1527
- annotation is not None
1528
- and hasattr(annotation, "__origin__")
1529
- and annotation.__origin__ is list
1530
+ base_annotation is not None
1531
+ and hasattr(base_annotation, "__origin__")
1532
+ and base_annotation.__origin__ is list
1530
1533
  ):
1531
- item_type = annotation.__args__[0]
1534
+ item_type = base_annotation.__args__[0]
1532
1535
 
1533
1536
  if not item_type:
1534
1537
  logger.error(f"Cannot determine item type for list field {self.field_name}")
@@ -1600,10 +1603,20 @@ class ListFieldRenderer(BaseFieldRenderer):
1600
1603
  if self.disabled:
1601
1604
  add_button_attrs["disabled"] = "true"
1602
1605
 
1606
+ # Differentiate message for Optional[List] vs required List
1607
+ if self.is_optional:
1608
+ empty_message = (
1609
+ "No items in this optional list. Click 'Add Item' if needed."
1610
+ )
1611
+ else:
1612
+ empty_message = (
1613
+ "No items in this required list. Click 'Add Item' to create one."
1614
+ )
1615
+
1603
1616
  empty_state = mui.Alert(
1604
1617
  fh.Div(
1605
1618
  mui.UkIcon("info", cls="mr-2"),
1606
- "No items in this list. Click 'Add Item' to create one.",
1619
+ empty_message,
1607
1620
  mui.Button("Add Item", **add_button_attrs),
1608
1621
  cls="flex flex-col items-start",
1609
1622
  ),
@@ -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.2
3
+ Version: 0.3.4
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=rFuRyXVlitbFQfR4BxRLZR_CxCMJwE_QbQyt1h_4568,4479
2
2
  fh_pydantic_form/color_utils.py,sha256=M0HSXX0i-lSHkcsgesxw7d3PEAnLsZ46i_STymZAM_k,18271
3
- fh_pydantic_form/comparison_form.py,sha256=iljizwqia-9J4-2lVF4yDvoVkGE56FUfHDD3k2ejSnM,24270
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
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
6
+ fh_pydantic_form/field_renderers.py,sha256=yJuV1bMVhL_7o1tZDyw20DJzy3-OQbq_Ygc_tHpsLsU,81459
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.2.dist-info/METADATA,sha256=Yk4mV476Uy8JjfHjcUurMazvQbYALsSd1Lk5TnwuJJ4,38420
15
- fh_pydantic_form-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- fh_pydantic_form-0.3.2.dist-info/licenses/LICENSE,sha256=AOi2eNK3D2aDycRHfPRiuACZ7WPBsKHTV2tTYNl7cls,577
17
- fh_pydantic_form-0.3.2.dist-info/RECORD,,
14
+ fh_pydantic_form-0.3.4.dist-info/METADATA,sha256=gKrPm2PdFdKvU2d3WvyjoioH-LRe3K0Nj4SXGa7vlhs,38420
15
+ fh_pydantic_form-0.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ fh_pydantic_form-0.3.4.dist-info/licenses/LICENSE,sha256=AOi2eNK3D2aDycRHfPRiuACZ7WPBsKHTV2tTYNl7cls,577
17
+ fh_pydantic_form-0.3.4.dist-info/RECORD,,