fh-pydantic-form 0.2.4__tar.gz → 0.2.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.

Files changed (20) hide show
  1. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/PKG-INFO +1 -1
  2. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/RELEASE_NOTES.md +2 -0
  3. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/pyproject.toml +1 -1
  4. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/form_parser.py +40 -17
  5. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/.github/workflows/build.yaml +0 -0
  6. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/.github/workflows/publish.yaml +0 -0
  7. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/.gitignore +0 -0
  8. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/.pre-commit-config.yaml +0 -0
  9. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/LICENSE +0 -0
  10. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/README.md +0 -0
  11. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/__init__.py +0 -0
  12. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/constants.py +0 -0
  13. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/defaults.py +0 -0
  14. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/field_renderers.py +0 -0
  15. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/form_renderer.py +0 -0
  16. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/list_path.py +0 -0
  17. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/py.typed +0 -0
  18. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/registry.py +0 -0
  19. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/type_helpers.py +0 -0
  20. {fh_pydantic_form-0.2.4 → fh_pydantic_form-0.2.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.2.4
3
+ Version: 0.2.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,6 +1,8 @@
1
1
  # Release Notes
2
2
 
3
+ ## Version 0.2.5 (2025-06-19)
3
4
 
5
+ - Fix bug with empty lists. Now should parse correctly to empty lists instead of returning defaults.
4
6
  ## Version 0.2.4 (2025-06-18)
5
7
 
6
8
  - Added support for SkipJsonSchema fields. They will automatically be excluded from the form and defaults used for validation.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fh-pydantic-form"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "a library to turn any pydantic BaseModel object into a fasthtml/monsterui input form"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -7,6 +7,8 @@ from typing import (
7
7
  Optional,
8
8
  Tuple,
9
9
  Union,
10
+ get_origin,
11
+ get_args,
10
12
  )
11
13
 
12
14
  from fh_pydantic_form.type_helpers import (
@@ -33,17 +35,16 @@ def _identify_list_fields(model_class) -> Dict[str, Dict[str, Any]]:
33
35
  list_fields = {}
34
36
  for field_name, field_info in model_class.model_fields.items():
35
37
  annotation = getattr(field_info, "annotation", None)
36
- if (
37
- annotation is not None
38
- and hasattr(annotation, "__origin__")
39
- and annotation.__origin__ is list
40
- ):
41
- item_type = annotation.__args__[0]
42
- list_fields[field_name] = {
43
- "item_type": item_type,
44
- "is_model_type": hasattr(item_type, "model_fields"),
45
- "field_info": field_info, # Store for later use if needed
46
- }
38
+ if annotation is not None:
39
+ # Handle Optional[List[...]] by unwrapping the Optional
40
+ base_ann = _get_underlying_type_if_optional(annotation)
41
+ if get_origin(base_ann) is list:
42
+ item_type = get_args(base_ann)[0]
43
+ list_fields[field_name] = {
44
+ "item_type": item_type,
45
+ "is_model_type": hasattr(item_type, "model_fields"),
46
+ "field_info": field_info, # Store for later use if needed
47
+ }
47
48
  return list_fields
48
49
 
49
50
 
@@ -128,9 +129,14 @@ def _parse_non_list_fields(
128
129
  # Get the nested model class (unwrap Optional if needed)
129
130
  nested_model_class = _get_underlying_type_if_optional(annotation)
130
131
 
131
- # Parse the nested model - pass the base_prefix
132
+ # Parse the nested model - pass the base_prefix and exclude_fields
132
133
  nested_value = _parse_nested_model_field(
133
- field_name, form_data, nested_model_class, field_info, base_prefix
134
+ field_name,
135
+ form_data,
136
+ nested_model_class,
137
+ field_info,
138
+ base_prefix,
139
+ exclude_fields,
134
140
  )
135
141
 
136
142
  # Only assign if we got a non-None value or the field is not optional
@@ -270,6 +276,7 @@ def _parse_nested_model_field(
270
276
  nested_model_class,
271
277
  field_info,
272
278
  parent_prefix: str = "",
279
+ exclude_fields: Optional[List[str]] = None,
273
280
  ) -> Optional[Dict[str, Any]]:
274
281
  """
275
282
  Parse a nested Pydantic model field from form data.
@@ -351,6 +358,7 @@ def _parse_nested_model_field(
351
358
  form_data,
352
359
  nested_list_defs,
353
360
  current_prefix, # ← prefix for this nested model
361
+ exclude_fields, # Pass through exclude_fields
354
362
  )
355
363
  # Merge without clobbering keys already set in step 1
356
364
  for lf_name, lf_val in list_results.items():
@@ -432,6 +440,7 @@ def _parse_list_fields(
432
440
  form_data: Dict[str, Any],
433
441
  list_field_defs: Dict[str, Dict[str, Any]],
434
442
  base_prefix: str = "",
443
+ exclude_fields: Optional[List[str]] = None,
435
444
  ) -> Dict[str, List[Any]]:
436
445
  """
437
446
  Parse list fields from form data by analyzing keys and reconstructing ordered lists.
@@ -440,10 +449,13 @@ def _parse_list_fields(
440
449
  form_data: Dictionary containing form field data
441
450
  list_field_defs: Dictionary of list field definitions
442
451
  base_prefix: Prefix to use when looking up field names in form_data
452
+ exclude_fields: Optional list of field names to exclude from parsing
443
453
 
444
454
  Returns:
445
455
  Dictionary with parsed list fields
446
456
  """
457
+ exclude_fields = exclude_fields or []
458
+
447
459
  # Skip if no list fields defined
448
460
  if not list_field_defs:
449
461
  return {}
@@ -525,9 +537,18 @@ def _parse_list_fields(
525
537
  if items: # Only add if items were found
526
538
  final_lists[field_name] = items
527
539
 
528
- # DON'T set defaults for missing list fields here - let _inject_missing_defaults handle all defaults
529
- # This allows the proper default injection mechanism to work for missing list fields
530
- # Only keep this section for excluded fields if needed, but don't inject defaults for all missing fields
540
+ # Ensure every rendered list field appears in final_lists
541
+ for field_name, field_def in list_field_defs.items():
542
+ # Skip list fields the UI never showed (those in exclude_fields)
543
+ if field_name in exclude_fields:
544
+ continue
545
+
546
+ # When user supplied ≥1 item we already captured it
547
+ if field_name in final_lists:
548
+ continue
549
+
550
+ # User submitted form with zero items → honour intent with empty list
551
+ final_lists[field_name] = []
531
552
 
532
553
  return final_lists
533
554
 
@@ -560,7 +581,9 @@ def _parse_model_list_item(
560
581
  )
561
582
  # 2. Parse inner lists
562
583
  result.update(
563
- _parse_list_fields(form_data, nested_list_defs, base_prefix=item_prefix)
584
+ _parse_list_fields(
585
+ form_data, nested_list_defs, base_prefix=item_prefix, exclude_fields=[]
586
+ )
564
587
  )
565
588
  return result
566
589