fh-pydantic-form 0.3.6__tar.gz → 0.3.7__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.6 → fh_pydantic_form-0.3.7}/.github/workflows/build.yaml +1 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/PKG-INFO +80 -2
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/README.md +79 -1
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/RELEASE_NOTES.md +40 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/pyproject.toml +1 -1
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/field_renderers.py +67 -24
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/form_parser.py +82 -12
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/form_renderer.py +78 -27
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/type_helpers.py +22 -2
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/.github/workflows/publish.yaml +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/.gitignore +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/.pre-commit-config.yaml +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/LICENSE +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/__init__.py +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/color_utils.py +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/comparison_form.py +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/constants.py +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/defaults.py +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/list_path.py +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/py.typed +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/src/fh_pydantic_form/registry.py +0 -0
- {fh_pydantic_form-0.3.6 → fh_pydantic_form-0.3.7}/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.7
|
|
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
|
|
@@ -480,6 +480,83 @@ form_renderer = PydanticForm(
|
|
|
480
480
|
|
|
481
481
|
This automatic default injection means you can safely exclude fields that shouldn't be user-editable while maintaining data integrity.
|
|
482
482
|
|
|
483
|
+
### SkipJsonSchema Fields
|
|
484
|
+
|
|
485
|
+
`fh-pydantic-form` provides advanced handling for `SkipJsonSchema` fields with selective visibility control. By default, fields marked with `SkipJsonSchema` are hidden from forms, but you can selectively show specific ones using the `keep_skip_json_fields` parameter.
|
|
486
|
+
|
|
487
|
+
```python
|
|
488
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
489
|
+
|
|
490
|
+
class DocumentModel(BaseModel):
|
|
491
|
+
title: str
|
|
492
|
+
content: str
|
|
493
|
+
|
|
494
|
+
# Hidden by default - system fields
|
|
495
|
+
document_id: SkipJsonSchema[str] = Field(
|
|
496
|
+
default_factory=lambda: f"doc_{uuid4().hex[:12]}",
|
|
497
|
+
description="Internal document ID"
|
|
498
|
+
)
|
|
499
|
+
created_at: SkipJsonSchema[datetime.datetime] = Field(
|
|
500
|
+
default_factory=datetime.datetime.now,
|
|
501
|
+
description="Creation timestamp"
|
|
502
|
+
)
|
|
503
|
+
version: SkipJsonSchema[int] = Field(
|
|
504
|
+
default=1,
|
|
505
|
+
description="Document version"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Normal form - all SkipJsonSchema fields hidden
|
|
509
|
+
form_normal = PydanticForm("doc_form", DocumentModel)
|
|
510
|
+
|
|
511
|
+
# Admin form - selectively show some SkipJsonSchema fields
|
|
512
|
+
form_admin = PydanticForm(
|
|
513
|
+
"admin_form",
|
|
514
|
+
DocumentModel,
|
|
515
|
+
keep_skip_json_fields=[
|
|
516
|
+
"document_id", # Show document ID
|
|
517
|
+
"version", # Show version number
|
|
518
|
+
# created_at remains hidden
|
|
519
|
+
]
|
|
520
|
+
)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### Nested SkipJsonSchema Fields
|
|
524
|
+
|
|
525
|
+
The feature supports dot notation for nested objects and list items:
|
|
526
|
+
|
|
527
|
+
```python
|
|
528
|
+
class Address(BaseModel):
|
|
529
|
+
street: str
|
|
530
|
+
city: str
|
|
531
|
+
# Hidden system field
|
|
532
|
+
internal_id: SkipJsonSchema[str] = Field(default_factory=lambda: f"addr_{uuid4().hex[:8]}")
|
|
533
|
+
|
|
534
|
+
class UserModel(BaseModel):
|
|
535
|
+
name: str
|
|
536
|
+
main_address: Address
|
|
537
|
+
other_addresses: List[Address]
|
|
538
|
+
|
|
539
|
+
# Show specific nested SkipJsonSchema fields
|
|
540
|
+
form = PydanticForm(
|
|
541
|
+
"user_form",
|
|
542
|
+
UserModel,
|
|
543
|
+
keep_skip_json_fields=[
|
|
544
|
+
"main_address.internal_id", # Show main address ID
|
|
545
|
+
"other_addresses.internal_id", # Show all address IDs in the list
|
|
546
|
+
]
|
|
547
|
+
)
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**Key Features:**
|
|
551
|
+
- **Hidden by default:** SkipJsonSchema fields are automatically excluded from forms
|
|
552
|
+
- **Selective visibility:** Use `keep_skip_json_fields` to show specific fields
|
|
553
|
+
- **Nested support:** Access nested fields with dot notation (`"main_address.internal_id"`)
|
|
554
|
+
- **List support:** Show fields in all list items (`"addresses.internal_id"`)
|
|
555
|
+
- **Smart defaults:** Non-kept fields use model defaults, kept fields retain initial values
|
|
556
|
+
- **Admin interfaces:** Perfect for admin panels or debugging where you need to see system fields
|
|
557
|
+
|
|
558
|
+
See `examples/complex_example.py` for a comprehensive demonstration of SkipJsonSchema field handling.
|
|
559
|
+
|
|
483
560
|
## Refreshing & Resetting
|
|
484
561
|
|
|
485
562
|
Forms support dynamic refresh and reset functionality:
|
|
@@ -1007,6 +1084,7 @@ form_renderer = PydanticForm(
|
|
|
1007
1084
|
| `disabled_fields` | `Optional[List[str]]` | `None` | List of specific field names to disable |
|
|
1008
1085
|
| `label_colors` | `Optional[Dict[str, str]]` | `None` | Mapping of field names to CSS colors or Tailwind classes |
|
|
1009
1086
|
| `exclude_fields` | `Optional[List[str]]` | `None` | List of field names to exclude from rendering (auto-injected on submission) |
|
|
1087
|
+
| `keep_skip_json_fields` | `Optional[List[str]]` | `None` | List of SkipJsonSchema field paths to selectively show (supports dot notation for nested fields) |
|
|
1010
1088
|
| `spacing` | `SpacingValue` | `"normal"` | Spacing theme: `"normal"`, `"compact"`, or `SpacingTheme` enum |
|
|
1011
1089
|
| `metrics_dict` | `Optional[Dict[str, Dict]]` | `None` | Field metrics for highlighting and tooltips |
|
|
1012
1090
|
|
|
@@ -1025,7 +1103,7 @@ form_renderer = PydanticForm(
|
|
|
1025
1103
|
| Method | Purpose |
|
|
1026
1104
|
|--------|---------|
|
|
1027
1105
|
| `render_inputs()` | Generate the HTML form inputs (without `<form>` wrapper) |
|
|
1028
|
-
| `with_initial_values(initial_values)` | Create a new form instance with same configuration but different initial values |
|
|
1106
|
+
| `with_initial_values(initial_values, metrics_dict=None)` | Create a new form instance with same configuration but different initial values |
|
|
1029
1107
|
| `refresh_button(text=None, **kwargs)` | Create a refresh button component |
|
|
1030
1108
|
| `reset_button(text=None, **kwargs)` | Create a reset button component |
|
|
1031
1109
|
| `register_routes(app)` | Register HTMX endpoints for list manipulation |
|
|
@@ -455,6 +455,83 @@ form_renderer = PydanticForm(
|
|
|
455
455
|
|
|
456
456
|
This automatic default injection means you can safely exclude fields that shouldn't be user-editable while maintaining data integrity.
|
|
457
457
|
|
|
458
|
+
### SkipJsonSchema Fields
|
|
459
|
+
|
|
460
|
+
`fh-pydantic-form` provides advanced handling for `SkipJsonSchema` fields with selective visibility control. By default, fields marked with `SkipJsonSchema` are hidden from forms, but you can selectively show specific ones using the `keep_skip_json_fields` parameter.
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
464
|
+
|
|
465
|
+
class DocumentModel(BaseModel):
|
|
466
|
+
title: str
|
|
467
|
+
content: str
|
|
468
|
+
|
|
469
|
+
# Hidden by default - system fields
|
|
470
|
+
document_id: SkipJsonSchema[str] = Field(
|
|
471
|
+
default_factory=lambda: f"doc_{uuid4().hex[:12]}",
|
|
472
|
+
description="Internal document ID"
|
|
473
|
+
)
|
|
474
|
+
created_at: SkipJsonSchema[datetime.datetime] = Field(
|
|
475
|
+
default_factory=datetime.datetime.now,
|
|
476
|
+
description="Creation timestamp"
|
|
477
|
+
)
|
|
478
|
+
version: SkipJsonSchema[int] = Field(
|
|
479
|
+
default=1,
|
|
480
|
+
description="Document version"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Normal form - all SkipJsonSchema fields hidden
|
|
484
|
+
form_normal = PydanticForm("doc_form", DocumentModel)
|
|
485
|
+
|
|
486
|
+
# Admin form - selectively show some SkipJsonSchema fields
|
|
487
|
+
form_admin = PydanticForm(
|
|
488
|
+
"admin_form",
|
|
489
|
+
DocumentModel,
|
|
490
|
+
keep_skip_json_fields=[
|
|
491
|
+
"document_id", # Show document ID
|
|
492
|
+
"version", # Show version number
|
|
493
|
+
# created_at remains hidden
|
|
494
|
+
]
|
|
495
|
+
)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### Nested SkipJsonSchema Fields
|
|
499
|
+
|
|
500
|
+
The feature supports dot notation for nested objects and list items:
|
|
501
|
+
|
|
502
|
+
```python
|
|
503
|
+
class Address(BaseModel):
|
|
504
|
+
street: str
|
|
505
|
+
city: str
|
|
506
|
+
# Hidden system field
|
|
507
|
+
internal_id: SkipJsonSchema[str] = Field(default_factory=lambda: f"addr_{uuid4().hex[:8]}")
|
|
508
|
+
|
|
509
|
+
class UserModel(BaseModel):
|
|
510
|
+
name: str
|
|
511
|
+
main_address: Address
|
|
512
|
+
other_addresses: List[Address]
|
|
513
|
+
|
|
514
|
+
# Show specific nested SkipJsonSchema fields
|
|
515
|
+
form = PydanticForm(
|
|
516
|
+
"user_form",
|
|
517
|
+
UserModel,
|
|
518
|
+
keep_skip_json_fields=[
|
|
519
|
+
"main_address.internal_id", # Show main address ID
|
|
520
|
+
"other_addresses.internal_id", # Show all address IDs in the list
|
|
521
|
+
]
|
|
522
|
+
)
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Key Features:**
|
|
526
|
+
- **Hidden by default:** SkipJsonSchema fields are automatically excluded from forms
|
|
527
|
+
- **Selective visibility:** Use `keep_skip_json_fields` to show specific fields
|
|
528
|
+
- **Nested support:** Access nested fields with dot notation (`"main_address.internal_id"`)
|
|
529
|
+
- **List support:** Show fields in all list items (`"addresses.internal_id"`)
|
|
530
|
+
- **Smart defaults:** Non-kept fields use model defaults, kept fields retain initial values
|
|
531
|
+
- **Admin interfaces:** Perfect for admin panels or debugging where you need to see system fields
|
|
532
|
+
|
|
533
|
+
See `examples/complex_example.py` for a comprehensive demonstration of SkipJsonSchema field handling.
|
|
534
|
+
|
|
458
535
|
## Refreshing & Resetting
|
|
459
536
|
|
|
460
537
|
Forms support dynamic refresh and reset functionality:
|
|
@@ -982,6 +1059,7 @@ form_renderer = PydanticForm(
|
|
|
982
1059
|
| `disabled_fields` | `Optional[List[str]]` | `None` | List of specific field names to disable |
|
|
983
1060
|
| `label_colors` | `Optional[Dict[str, str]]` | `None` | Mapping of field names to CSS colors or Tailwind classes |
|
|
984
1061
|
| `exclude_fields` | `Optional[List[str]]` | `None` | List of field names to exclude from rendering (auto-injected on submission) |
|
|
1062
|
+
| `keep_skip_json_fields` | `Optional[List[str]]` | `None` | List of SkipJsonSchema field paths to selectively show (supports dot notation for nested fields) |
|
|
985
1063
|
| `spacing` | `SpacingValue` | `"normal"` | Spacing theme: `"normal"`, `"compact"`, or `SpacingTheme` enum |
|
|
986
1064
|
| `metrics_dict` | `Optional[Dict[str, Dict]]` | `None` | Field metrics for highlighting and tooltips |
|
|
987
1065
|
|
|
@@ -1000,7 +1078,7 @@ form_renderer = PydanticForm(
|
|
|
1000
1078
|
| Method | Purpose |
|
|
1001
1079
|
|--------|---------|
|
|
1002
1080
|
| `render_inputs()` | Generate the HTML form inputs (without `<form>` wrapper) |
|
|
1003
|
-
| `with_initial_values(initial_values)` | Create a new form instance with same configuration but different initial values |
|
|
1081
|
+
| `with_initial_values(initial_values, metrics_dict=None)` | Create a new form instance with same configuration but different initial values |
|
|
1004
1082
|
| `refresh_button(text=None, **kwargs)` | Create a refresh button component |
|
|
1005
1083
|
| `reset_button(text=None, **kwargs)` | Create a reset button component |
|
|
1006
1084
|
| `register_routes(app)` | Register HTMX endpoints for list manipulation |
|
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## Version 0.3.7 (2025-09-19)
|
|
4
|
+
|
|
5
|
+
### 🎉 New Features
|
|
6
|
+
|
|
7
|
+
#### SkipJsonSchema Field Support with Selective Override
|
|
8
|
+
- **NEW**: Added comprehensive support for fields marked with `SkipJsonSchema` annotation
|
|
9
|
+
- **NEW**: `keep_skip_json_fields` parameter allows selective inclusion of specific SkipJsonSchema fields
|
|
10
|
+
- Supports dot-notation paths for nested fields (e.g., `"addresses.internal_id"`)
|
|
11
|
+
- Enables fine-grained control over which internal fields are exposed in forms
|
|
12
|
+
- Works with complex nested structures and list fields
|
|
13
|
+
- **ENHANCED**: SkipJsonSchema fields are automatically excluded from form rendering by default
|
|
14
|
+
- **IMPROVED**: Better field introspection for complex type scenarios including optional skip fields
|
|
15
|
+
|
|
16
|
+
### 🔧 Bug Fixes & Improvements
|
|
17
|
+
|
|
18
|
+
#### Default Values Handling
|
|
19
|
+
- **FIXED**: Default values for simple fields now work correctly without initial values
|
|
20
|
+
- **IMPROVED**: Better handling of field defaults when no initial values are provided
|
|
21
|
+
- **ENHANCED**: More robust form rendering for fields with default values
|
|
22
|
+
|
|
23
|
+
#### Documentation & Examples
|
|
24
|
+
- **UPDATED**: README.md with SkipJsonSchema handling documentation
|
|
25
|
+
- **ENHANCED**: Complex example updated to demonstrate SkipJsonSchema usage patterns
|
|
26
|
+
- **IMPROVED**: Better code documentation and examples
|
|
27
|
+
|
|
28
|
+
### 🧪 Testing
|
|
29
|
+
- **NEW**: Comprehensive test coverage for SkipJsonSchema field handling
|
|
30
|
+
- **NEW**: Tests for default values behavior without initial values
|
|
31
|
+
- **IMPROVED**: Enhanced test coverage for edge cases and type introspection
|
|
32
|
+
|
|
33
|
+
### 📊 Statistics
|
|
34
|
+
- **7 commits** since v0.3.6
|
|
35
|
+
- Focus on optional field handling and default value improvements
|
|
36
|
+
- Enhanced SkipJsonSchema support with comprehensive testing
|
|
37
|
+
|
|
38
|
+
**Key Highlights:**
|
|
39
|
+
This release significantly improves handling of optional fields, particularly those marked with `SkipJsonSchema`, and fixes important issues with default value handling when no initial values are provided.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
3
43
|
## Version 0.3.6 (2025-07-21)
|
|
4
44
|
|
|
5
45
|
- **NEW**: can now pass new metrics_dict to `.with_initial_values()` helper method.
|
|
@@ -14,7 +14,7 @@ from typing import (
|
|
|
14
14
|
import fasthtml.common as fh
|
|
15
15
|
import monsterui.all as mui
|
|
16
16
|
from fastcore.xml import FT
|
|
17
|
-
from pydantic import ValidationError
|
|
17
|
+
from pydantic import BaseModel, ValidationError
|
|
18
18
|
from pydantic.fields import FieldInfo
|
|
19
19
|
|
|
20
20
|
from fh_pydantic_form.color_utils import (
|
|
@@ -23,6 +23,7 @@ from fh_pydantic_form.color_utils import (
|
|
|
23
23
|
robust_color_to_rgba,
|
|
24
24
|
)
|
|
25
25
|
from fh_pydantic_form.constants import _UNSET
|
|
26
|
+
from fh_pydantic_form.defaults import default_dict_for_model, default_for_annotation
|
|
26
27
|
from fh_pydantic_form.registry import FieldRendererRegistry
|
|
27
28
|
from fh_pydantic_form.type_helpers import (
|
|
28
29
|
DecorationScope,
|
|
@@ -30,7 +31,9 @@ from fh_pydantic_form.type_helpers import (
|
|
|
30
31
|
MetricsDict,
|
|
31
32
|
_get_underlying_type_if_optional,
|
|
32
33
|
_is_optional_type,
|
|
34
|
+
_is_skip_json_schema_field,
|
|
33
35
|
get_default,
|
|
36
|
+
normalize_path_segments,
|
|
34
37
|
)
|
|
35
38
|
from fh_pydantic_form.ui_style import (
|
|
36
39
|
SpacingTheme,
|
|
@@ -379,6 +382,7 @@ class BaseFieldRenderer(MetricsRendererMixin):
|
|
|
379
382
|
metric_entry: Optional[MetricEntry] = None,
|
|
380
383
|
metrics_dict: Optional[MetricsDict] = None,
|
|
381
384
|
refresh_endpoint_override: Optional[str] = None,
|
|
385
|
+
keep_skip_json_pathset: Optional[set[str]] = None,
|
|
382
386
|
**kwargs, # Accept additional kwargs for extensibility
|
|
383
387
|
):
|
|
384
388
|
"""
|
|
@@ -402,6 +406,14 @@ class BaseFieldRenderer(MetricsRendererMixin):
|
|
|
402
406
|
self.field_name = f"{prefix}{field_name}" if prefix else field_name
|
|
403
407
|
self.original_field_name = field_name
|
|
404
408
|
self.field_info = field_info
|
|
409
|
+
# Normalize PydanticUndefined → None so it never renders as text
|
|
410
|
+
try:
|
|
411
|
+
from pydantic_core import PydanticUndefined
|
|
412
|
+
|
|
413
|
+
if value is PydanticUndefined:
|
|
414
|
+
value = None
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
405
417
|
self.value = value
|
|
406
418
|
self.prefix = prefix
|
|
407
419
|
self.field_path: List[str] = field_path or []
|
|
@@ -412,6 +424,7 @@ class BaseFieldRenderer(MetricsRendererMixin):
|
|
|
412
424
|
self.spacing = _normalize_spacing(spacing)
|
|
413
425
|
self.metrics_dict = metrics_dict
|
|
414
426
|
self._refresh_endpoint_override = refresh_endpoint_override
|
|
427
|
+
self._keep_skip_json_pathset = keep_skip_json_pathset or set()
|
|
415
428
|
|
|
416
429
|
# Initialize metric entry attribute
|
|
417
430
|
self.metric_entry: Optional[MetricEntry] = None
|
|
@@ -447,6 +460,15 @@ class BaseFieldRenderer(MetricsRendererMixin):
|
|
|
447
460
|
parts.append(segment)
|
|
448
461
|
return ".".join(parts)
|
|
449
462
|
|
|
463
|
+
def _normalized_dot_path(self, path_segments: List[str]) -> str:
|
|
464
|
+
"""Normalize path segments by dropping indices and joining with dots."""
|
|
465
|
+
return normalize_path_segments(path_segments)
|
|
466
|
+
|
|
467
|
+
def _is_kept_skip_field(self, full_path: List[str]) -> bool:
|
|
468
|
+
"""Return True if a SkipJsonSchema field should be kept based on keep list."""
|
|
469
|
+
normalized = self._normalized_dot_path(full_path)
|
|
470
|
+
return bool(normalized) and normalized in self._keep_skip_json_pathset
|
|
471
|
+
|
|
450
472
|
def _is_inline_color(self, color: str) -> bool:
|
|
451
473
|
"""
|
|
452
474
|
Determine if a color should be applied as an inline style or CSS class.
|
|
@@ -1249,18 +1271,26 @@ class BaseModelFieldRenderer(BaseFieldRenderer):
|
|
|
1249
1271
|
|
|
1250
1272
|
# Only use defaults if field wasn't provided
|
|
1251
1273
|
if not field_was_provided:
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
nested_field_value =
|
|
1274
|
+
dv = get_default(nested_field_info) # _UNSET if truly unset
|
|
1275
|
+
if dv is not _UNSET:
|
|
1276
|
+
nested_field_value = dv
|
|
1277
|
+
else:
|
|
1278
|
+
ann = nested_field_info.annotation
|
|
1279
|
+
base_ann = get_origin(ann) or ann
|
|
1280
|
+
if isinstance(base_ann, type) and issubclass(
|
|
1281
|
+
base_ann, BaseModel
|
|
1282
|
+
):
|
|
1283
|
+
nested_field_value = default_dict_for_model(base_ann)
|
|
1284
|
+
else:
|
|
1285
|
+
nested_field_value = default_for_annotation(ann)
|
|
1286
|
+
|
|
1287
|
+
# Skip SkipJsonSchema fields unless explicitly kept
|
|
1288
|
+
if _is_skip_json_schema_field(
|
|
1289
|
+
nested_field_info
|
|
1290
|
+
) and not self._is_kept_skip_field(
|
|
1291
|
+
self.field_path + [nested_field_name]
|
|
1292
|
+
):
|
|
1293
|
+
continue
|
|
1264
1294
|
|
|
1265
1295
|
# Get renderer for this nested field
|
|
1266
1296
|
registry = FieldRendererRegistry() # Get singleton instance
|
|
@@ -1290,6 +1320,7 @@ class BaseModelFieldRenderer(BaseFieldRenderer):
|
|
|
1290
1320
|
metric_entry=None, # Let auto-lookup handle it
|
|
1291
1321
|
metrics_dict=self.metrics_dict, # Pass down the metrics dict
|
|
1292
1322
|
refresh_endpoint_override=self._refresh_endpoint_override, # Propagate refresh override
|
|
1323
|
+
keep_skip_json_pathset=self._keep_skip_json_pathset, # Propagate keep paths
|
|
1293
1324
|
)
|
|
1294
1325
|
|
|
1295
1326
|
nested_inputs.append(renderer.render())
|
|
@@ -1843,6 +1874,7 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
1843
1874
|
metric_entry=None, # Let auto-lookup handle it
|
|
1844
1875
|
metrics_dict=self.metrics_dict, # Pass down the metrics dict
|
|
1845
1876
|
refresh_endpoint_override=self._refresh_endpoint_override, # Propagate refresh override
|
|
1877
|
+
keep_skip_json_pathset=self._keep_skip_json_pathset, # Propagate keep paths
|
|
1846
1878
|
)
|
|
1847
1879
|
# Add the rendered input to content elements
|
|
1848
1880
|
item_content_elements.append(item_renderer.render_input())
|
|
@@ -1866,18 +1898,28 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
1866
1898
|
|
|
1867
1899
|
# Use defaults only if field not provided
|
|
1868
1900
|
if not field_was_provided:
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1901
|
+
dv = get_default(nested_field_info)
|
|
1902
|
+
if dv is not _UNSET:
|
|
1903
|
+
nested_field_value = dv
|
|
1904
|
+
else:
|
|
1905
|
+
ann = nested_field_info.annotation
|
|
1906
|
+
base_ann = get_origin(ann) or ann
|
|
1907
|
+
if isinstance(base_ann, type) and issubclass(
|
|
1908
|
+
base_ann, BaseModel
|
|
1909
|
+
):
|
|
1910
|
+
nested_field_value = default_dict_for_model(
|
|
1911
|
+
base_ann
|
|
1878
1912
|
)
|
|
1879
|
-
|
|
1880
|
-
|
|
1913
|
+
else:
|
|
1914
|
+
nested_field_value = default_for_annotation(ann)
|
|
1915
|
+
|
|
1916
|
+
# Skip SkipJsonSchema fields unless explicitly kept
|
|
1917
|
+
if _is_skip_json_schema_field(
|
|
1918
|
+
nested_field_info
|
|
1919
|
+
) and not self._is_kept_skip_field(
|
|
1920
|
+
self.field_path + [nested_field_name]
|
|
1921
|
+
):
|
|
1922
|
+
continue
|
|
1881
1923
|
|
|
1882
1924
|
# Get renderer and render field with error handling
|
|
1883
1925
|
renderer_cls = FieldRendererRegistry().get_renderer(
|
|
@@ -1902,6 +1944,7 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
1902
1944
|
metric_entry=None, # Let auto-lookup handle it
|
|
1903
1945
|
metrics_dict=self.metrics_dict, # Pass down the metrics dict
|
|
1904
1946
|
refresh_endpoint_override=self._refresh_endpoint_override, # Propagate refresh override
|
|
1947
|
+
keep_skip_json_pathset=self._keep_skip_json_pathset, # Propagate keep paths
|
|
1905
1948
|
)
|
|
1906
1949
|
|
|
1907
1950
|
# Add rendered field to valid fields
|
|
@@ -54,6 +54,8 @@ def _parse_non_list_fields(
|
|
|
54
54
|
list_field_defs: Dict[str, Dict[str, Any]],
|
|
55
55
|
base_prefix: str = "",
|
|
56
56
|
exclude_fields: Optional[List[str]] = None,
|
|
57
|
+
keep_skip_json_pathset: Optional[set[str]] = None,
|
|
58
|
+
current_field_path: Optional[List[str]] = None,
|
|
57
59
|
) -> Dict[str, Any]:
|
|
58
60
|
"""
|
|
59
61
|
Parses non-list fields from form data based on the model definition.
|
|
@@ -64,12 +66,30 @@ def _parse_non_list_fields(
|
|
|
64
66
|
list_field_defs: Dictionary of list field definitions (to skip)
|
|
65
67
|
base_prefix: Prefix to use when looking up field names in form_data
|
|
66
68
|
exclude_fields: Optional list of field names to exclude from parsing
|
|
69
|
+
keep_skip_json_pathset: Optional set of normalized paths for SkipJsonSchema fields to keep
|
|
67
70
|
|
|
68
71
|
Returns:
|
|
69
72
|
Dictionary with parsed non-list fields
|
|
70
73
|
"""
|
|
71
74
|
result: Dict[str, Any] = {}
|
|
72
75
|
exclude_fields = exclude_fields or []
|
|
76
|
+
keep_skip_json_pathset = keep_skip_json_pathset or set()
|
|
77
|
+
|
|
78
|
+
# Helper function to check if a SkipJsonSchema field should be kept
|
|
79
|
+
def _should_keep_skip_field(path_segments: List[str]) -> bool:
|
|
80
|
+
from fh_pydantic_form.type_helpers import normalize_path_segments
|
|
81
|
+
|
|
82
|
+
normalized = normalize_path_segments(path_segments)
|
|
83
|
+
return bool(normalized) and normalized in keep_skip_json_pathset
|
|
84
|
+
|
|
85
|
+
# Calculate the current path context for fields at this level
|
|
86
|
+
# For top-level parsing, this will be empty
|
|
87
|
+
# For nested parsing, this will contain the nested path segments
|
|
88
|
+
current_path_segments: List[str] = []
|
|
89
|
+
if current_field_path is not None:
|
|
90
|
+
# Use explicitly passed field path
|
|
91
|
+
current_path_segments = current_field_path
|
|
92
|
+
# For top-level parsing (base_prefix is just form name), current_path_segments remains empty
|
|
73
93
|
|
|
74
94
|
for field_name, field_info in model_class.model_fields.items():
|
|
75
95
|
if field_name in list_field_defs:
|
|
@@ -79,9 +99,11 @@ def _parse_non_list_fields(
|
|
|
79
99
|
if field_name in exclude_fields:
|
|
80
100
|
continue
|
|
81
101
|
|
|
82
|
-
# Skip SkipJsonSchema fields
|
|
102
|
+
# Skip SkipJsonSchema fields unless they're explicitly kept
|
|
83
103
|
if _is_skip_json_schema_field(field_info):
|
|
84
|
-
|
|
104
|
+
field_path_segments = current_path_segments + [field_name]
|
|
105
|
+
if not _should_keep_skip_field(field_path_segments):
|
|
106
|
+
continue
|
|
85
107
|
|
|
86
108
|
# Create full key with prefix
|
|
87
109
|
full_key = f"{base_prefix}{field_name}"
|
|
@@ -128,7 +150,8 @@ def _parse_non_list_fields(
|
|
|
128
150
|
# Get the nested model class (unwrap Optional if needed)
|
|
129
151
|
nested_model_class = _get_underlying_type_if_optional(annotation)
|
|
130
152
|
|
|
131
|
-
# Parse the nested model - pass the base_prefix and
|
|
153
|
+
# Parse the nested model - pass the base_prefix, exclude_fields, and keep paths
|
|
154
|
+
nested_field_path = current_path_segments + [field_name]
|
|
132
155
|
nested_value = _parse_nested_model_field(
|
|
133
156
|
field_name,
|
|
134
157
|
form_data,
|
|
@@ -136,6 +159,8 @@ def _parse_non_list_fields(
|
|
|
136
159
|
field_info,
|
|
137
160
|
base_prefix,
|
|
138
161
|
exclude_fields,
|
|
162
|
+
keep_skip_json_pathset,
|
|
163
|
+
nested_field_path,
|
|
139
164
|
)
|
|
140
165
|
|
|
141
166
|
# Only assign if we got a non-None value or the field is not optional
|
|
@@ -276,6 +301,8 @@ def _parse_nested_model_field(
|
|
|
276
301
|
field_info,
|
|
277
302
|
parent_prefix: str = "",
|
|
278
303
|
exclude_fields: Optional[List[str]] = None,
|
|
304
|
+
keep_skip_json_pathset: Optional[set[str]] = None,
|
|
305
|
+
current_field_path: Optional[List[str]] = None,
|
|
279
306
|
) -> Optional[Dict[str, Any]]:
|
|
280
307
|
"""
|
|
281
308
|
Parse a nested Pydantic model field from form data.
|
|
@@ -286,6 +313,8 @@ def _parse_nested_model_field(
|
|
|
286
313
|
nested_model_class: The nested model class
|
|
287
314
|
field_info: The field info from the parent model
|
|
288
315
|
parent_prefix: Prefix from parent form/model to use when constructing keys
|
|
316
|
+
exclude_fields: Optional list of field names to exclude from parsing
|
|
317
|
+
keep_skip_json_pathset: Optional set of normalized paths for SkipJsonSchema fields to keep
|
|
289
318
|
|
|
290
319
|
Returns:
|
|
291
320
|
Dictionary with nested model structure or None/default if no data found
|
|
@@ -302,6 +331,16 @@ def _parse_nested_model_field(
|
|
|
302
331
|
break
|
|
303
332
|
|
|
304
333
|
if found_any_subfield:
|
|
334
|
+
# Helper function to check if a SkipJsonSchema field should be kept
|
|
335
|
+
def _should_keep_skip_field_nested(path_segments: List[str]) -> bool:
|
|
336
|
+
from fh_pydantic_form.type_helpers import normalize_path_segments
|
|
337
|
+
|
|
338
|
+
normalized = normalize_path_segments(path_segments)
|
|
339
|
+
return bool(normalized) and normalized in (keep_skip_json_pathset or set())
|
|
340
|
+
|
|
341
|
+
# Use the passed field path for calculating nested paths
|
|
342
|
+
nested_path_segments: List[str] = current_field_path or []
|
|
343
|
+
|
|
305
344
|
# ------------------------------------------------------------------
|
|
306
345
|
# 1. Process each **non-list** field in the nested model
|
|
307
346
|
# ------------------------------------------------------------------
|
|
@@ -309,12 +348,14 @@ def _parse_nested_model_field(
|
|
|
309
348
|
sub_key = f"{current_prefix}{sub_field_name}"
|
|
310
349
|
annotation = getattr(sub_field_info, "annotation", None)
|
|
311
350
|
|
|
312
|
-
# Skip SkipJsonSchema fields
|
|
351
|
+
# Skip SkipJsonSchema fields unless they're explicitly kept
|
|
313
352
|
if _is_skip_json_schema_field(sub_field_info):
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
353
|
+
sub_field_path_segments = nested_path_segments + [sub_field_name]
|
|
354
|
+
if not _should_keep_skip_field_nested(sub_field_path_segments):
|
|
355
|
+
logger.debug(
|
|
356
|
+
f"Skipping SkipJsonSchema field in nested model during parsing: {sub_field_name}"
|
|
357
|
+
)
|
|
358
|
+
continue
|
|
318
359
|
|
|
319
360
|
# Handle based on field type, with Optional unwrapping
|
|
320
361
|
is_optional = _is_optional_type(annotation)
|
|
@@ -326,9 +367,17 @@ def _parse_nested_model_field(
|
|
|
326
367
|
|
|
327
368
|
# Handle nested model fields (including Optional[NestedModel])
|
|
328
369
|
elif isinstance(base_type, type) and hasattr(base_type, "model_fields"):
|
|
329
|
-
# Pass the current_prefix to the recursive call
|
|
370
|
+
# Pass the current_prefix and keep paths to the recursive call
|
|
371
|
+
sub_field_path = nested_path_segments + [sub_field_name]
|
|
330
372
|
sub_value = _parse_nested_model_field(
|
|
331
|
-
sub_field_name,
|
|
373
|
+
sub_field_name,
|
|
374
|
+
form_data,
|
|
375
|
+
base_type,
|
|
376
|
+
sub_field_info,
|
|
377
|
+
current_prefix,
|
|
378
|
+
exclude_fields,
|
|
379
|
+
keep_skip_json_pathset,
|
|
380
|
+
sub_field_path,
|
|
332
381
|
)
|
|
333
382
|
if sub_value is not None:
|
|
334
383
|
nested_data[sub_field_name] = sub_value
|
|
@@ -358,6 +407,7 @@ def _parse_nested_model_field(
|
|
|
358
407
|
nested_list_defs,
|
|
359
408
|
current_prefix, # ← prefix for this nested model
|
|
360
409
|
exclude_fields, # Pass through exclude_fields
|
|
410
|
+
keep_skip_json_pathset,
|
|
361
411
|
)
|
|
362
412
|
# Merge without clobbering keys already set in step 1
|
|
363
413
|
for lf_name, lf_val in list_results.items():
|
|
@@ -438,6 +488,7 @@ def _parse_list_fields(
|
|
|
438
488
|
list_field_defs: Dict[str, Dict[str, Any]],
|
|
439
489
|
base_prefix: str = "",
|
|
440
490
|
exclude_fields: Optional[List[str]] = None,
|
|
491
|
+
keep_skip_json_pathset: Optional[set[str]] = None,
|
|
441
492
|
) -> Dict[str, Optional[List[Any]]]:
|
|
442
493
|
"""
|
|
443
494
|
Parse list fields from form data by analyzing keys and reconstructing ordered lists.
|
|
@@ -447,6 +498,7 @@ def _parse_list_fields(
|
|
|
447
498
|
list_field_defs: Dictionary of list field definitions
|
|
448
499
|
base_prefix: Prefix to use when looking up field names in form_data
|
|
449
500
|
exclude_fields: Optional list of field names to exclude from parsing
|
|
501
|
+
keep_skip_json_pathset: Optional set of normalized paths for SkipJsonSchema fields to keep
|
|
450
502
|
|
|
451
503
|
Returns:
|
|
452
504
|
Dictionary with parsed list fields
|
|
@@ -504,7 +556,15 @@ def _parse_list_fields(
|
|
|
504
556
|
# ------------------------------------------------------------------
|
|
505
557
|
if field_def["is_model_type"]:
|
|
506
558
|
item_prefix = f"{base_prefix}{field_name}_{idx_str}_"
|
|
507
|
-
|
|
559
|
+
# For list items, the field path is the list field name (without index)
|
|
560
|
+
item_field_path = [field_name]
|
|
561
|
+
parsed_item = _parse_model_list_item(
|
|
562
|
+
form_data,
|
|
563
|
+
item_type,
|
|
564
|
+
item_prefix,
|
|
565
|
+
keep_skip_json_pathset,
|
|
566
|
+
item_field_path,
|
|
567
|
+
)
|
|
508
568
|
items.append(parsed_item)
|
|
509
569
|
continue
|
|
510
570
|
|
|
@@ -558,6 +618,8 @@ def _parse_model_list_item(
|
|
|
558
618
|
form_data: Dict[str, Any],
|
|
559
619
|
item_type,
|
|
560
620
|
item_prefix: str,
|
|
621
|
+
keep_skip_json_pathset: Optional[set[str]] = None,
|
|
622
|
+
current_field_path: Optional[List[str]] = None,
|
|
561
623
|
) -> Dict[str, Any]:
|
|
562
624
|
"""
|
|
563
625
|
Fully parse a single BaseModel list item – including its own nested lists.
|
|
@@ -568,6 +630,7 @@ def _parse_model_list_item(
|
|
|
568
630
|
form_data: Dictionary containing form field data
|
|
569
631
|
item_type: The BaseModel class for this list item
|
|
570
632
|
item_prefix: Prefix for this specific list item (e.g., "main_form_compact_other_addresses_0_")
|
|
633
|
+
keep_skip_json_pathset: Optional set of normalized paths for SkipJsonSchema fields to keep
|
|
571
634
|
|
|
572
635
|
Returns:
|
|
573
636
|
Dictionary with fully parsed item data including nested lists
|
|
@@ -579,11 +642,18 @@ def _parse_model_list_item(
|
|
|
579
642
|
item_type,
|
|
580
643
|
nested_list_defs,
|
|
581
644
|
base_prefix=item_prefix,
|
|
645
|
+
exclude_fields=[],
|
|
646
|
+
keep_skip_json_pathset=keep_skip_json_pathset,
|
|
647
|
+
current_field_path=current_field_path,
|
|
582
648
|
)
|
|
583
649
|
# 2. Parse inner lists
|
|
584
650
|
result.update(
|
|
585
651
|
_parse_list_fields(
|
|
586
|
-
form_data,
|
|
652
|
+
form_data,
|
|
653
|
+
nested_list_defs,
|
|
654
|
+
base_prefix=item_prefix,
|
|
655
|
+
exclude_fields=[],
|
|
656
|
+
keep_skip_json_pathset=keep_skip_json_pathset,
|
|
587
657
|
)
|
|
588
658
|
)
|
|
589
659
|
return result
|
|
@@ -34,6 +34,7 @@ from fh_pydantic_form.registry import FieldRendererRegistry
|
|
|
34
34
|
from fh_pydantic_form.type_helpers import (
|
|
35
35
|
_is_skip_json_schema_field,
|
|
36
36
|
get_default,
|
|
37
|
+
normalize_path_segments,
|
|
37
38
|
)
|
|
38
39
|
from fh_pydantic_form.ui_style import (
|
|
39
40
|
SpacingTheme,
|
|
@@ -48,6 +49,21 @@ logger = logging.getLogger(__name__)
|
|
|
48
49
|
ModelType = TypeVar("ModelType", bound=BaseModel)
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
def _compile_keep_paths(paths: Optional[List[str]]) -> set[str]:
|
|
53
|
+
"""Normalize and compile keep paths for fast membership tests."""
|
|
54
|
+
if not paths:
|
|
55
|
+
return set()
|
|
56
|
+
|
|
57
|
+
compiled: set[str] = set()
|
|
58
|
+
for raw_path in paths:
|
|
59
|
+
if not raw_path:
|
|
60
|
+
continue
|
|
61
|
+
normalized = raw_path.strip()
|
|
62
|
+
if normalized:
|
|
63
|
+
compiled.add(normalized)
|
|
64
|
+
return compiled
|
|
65
|
+
|
|
66
|
+
|
|
51
67
|
def list_manipulation_js():
|
|
52
68
|
return fh.Script("""
|
|
53
69
|
function moveItem(buttonElement, direction) {
|
|
@@ -281,6 +297,7 @@ class PydanticForm(Generic[ModelType]):
|
|
|
281
297
|
disabled_fields: Optional[List[str]] = None,
|
|
282
298
|
label_colors: Optional[Dict[str, str]] = None,
|
|
283
299
|
exclude_fields: Optional[List[str]] = None,
|
|
300
|
+
keep_skip_json_fields: Optional[List[str]] = None,
|
|
284
301
|
spacing: SpacingValue = SpacingTheme.NORMAL,
|
|
285
302
|
metrics_dict: Optional[Dict[str, Any]] = None,
|
|
286
303
|
):
|
|
@@ -298,6 +315,7 @@ class PydanticForm(Generic[ModelType]):
|
|
|
298
315
|
disabled_fields: Optional list of top-level field names to disable specifically
|
|
299
316
|
label_colors: Optional dictionary mapping field names to label colors (CSS color values)
|
|
300
317
|
exclude_fields: Optional list of top-level field names to exclude from the form
|
|
318
|
+
keep_skip_json_fields: Optional list of dot-paths for SkipJsonSchema fields to force-keep
|
|
301
319
|
spacing: Spacing theme to use for form layout ("normal", "compact", or SpacingTheme enum)
|
|
302
320
|
metrics_dict: Optional metrics dictionary for field-level visual feedback
|
|
303
321
|
"""
|
|
@@ -342,6 +360,8 @@ class PydanticForm(Generic[ModelType]):
|
|
|
342
360
|
self.exclude_fields = exclude_fields or [] # Store excluded fields list
|
|
343
361
|
self.spacing = _normalize_spacing(spacing) # Store normalized spacing
|
|
344
362
|
self.metrics_dict = metrics_dict or {} # Store metrics dictionary
|
|
363
|
+
self.keep_skip_json_fields = keep_skip_json_fields or []
|
|
364
|
+
self._keep_skip_json_pathset = _compile_keep_paths(self.keep_skip_json_fields)
|
|
345
365
|
|
|
346
366
|
# Register custom renderers with the global registry if provided
|
|
347
367
|
if custom_renderers:
|
|
@@ -363,6 +383,15 @@ class PydanticForm(Generic[ModelType]):
|
|
|
363
383
|
wrapper_cls = "fhpf-wrapper w-full flex-1"
|
|
364
384
|
return fh.Div(inner, cls=wrapper_cls)
|
|
365
385
|
|
|
386
|
+
def _normalized_dot_path(self, path_segments: List[str]) -> str:
|
|
387
|
+
"""Normalize path segments by dropping indices and joining with dots."""
|
|
388
|
+
return normalize_path_segments(path_segments)
|
|
389
|
+
|
|
390
|
+
def _is_kept_skip_field(self, full_path: List[str]) -> bool:
|
|
391
|
+
"""Return True if a SkipJsonSchema field should be kept based on keep list."""
|
|
392
|
+
normalized = self._normalized_dot_path(full_path)
|
|
393
|
+
return bool(normalized) and normalized in self._keep_skip_json_pathset
|
|
394
|
+
|
|
366
395
|
def reset_state(self) -> None:
|
|
367
396
|
"""
|
|
368
397
|
Restore the live state of the form to its immutable baseline.
|
|
@@ -400,6 +429,7 @@ class PydanticForm(Generic[ModelType]):
|
|
|
400
429
|
disabled_fields=self.disabled_fields,
|
|
401
430
|
label_colors=self.label_colors,
|
|
402
431
|
exclude_fields=self.exclude_fields,
|
|
432
|
+
keep_skip_json_fields=self.keep_skip_json_fields,
|
|
403
433
|
spacing=self.spacing,
|
|
404
434
|
metrics_dict=metrics_dict
|
|
405
435
|
if metrics_dict is not None
|
|
@@ -423,8 +453,10 @@ class PydanticForm(Generic[ModelType]):
|
|
|
423
453
|
if field_name in self.exclude_fields:
|
|
424
454
|
continue
|
|
425
455
|
|
|
426
|
-
# Skip SkipJsonSchema fields
|
|
427
|
-
if _is_skip_json_schema_field(field_info)
|
|
456
|
+
# Skip SkipJsonSchema fields unless explicitly kept
|
|
457
|
+
if _is_skip_json_schema_field(field_info) and not self._is_kept_skip_field(
|
|
458
|
+
[field_name]
|
|
459
|
+
):
|
|
428
460
|
continue
|
|
429
461
|
|
|
430
462
|
# Only use what was explicitly provided in initial values
|
|
@@ -440,24 +472,14 @@ class PydanticForm(Generic[ModelType]):
|
|
|
440
472
|
|
|
441
473
|
# Only use defaults if field was not provided at all
|
|
442
474
|
if not field_was_provided:
|
|
443
|
-
# Field not provided - use model defaults
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
else:
|
|
452
|
-
initial_value = None
|
|
453
|
-
logger.warning(
|
|
454
|
-
f" - default_factory for '{field_name}' is not callable"
|
|
455
|
-
)
|
|
456
|
-
except Exception as e:
|
|
457
|
-
initial_value = None
|
|
458
|
-
logger.warning(
|
|
459
|
-
f" - Error in default_factory for '{field_name}': {e}"
|
|
460
|
-
)
|
|
475
|
+
# Field not provided - use model defaults in order of priority
|
|
476
|
+
# 1. Try explicit field default
|
|
477
|
+
default_val = get_default(field_info)
|
|
478
|
+
if default_val is not _UNSET:
|
|
479
|
+
initial_value = default_val
|
|
480
|
+
else:
|
|
481
|
+
# 2. Fall back to smart defaults for the type
|
|
482
|
+
initial_value = default_for_annotation(field_info.annotation)
|
|
461
483
|
# If field was provided (even as None), respect that value
|
|
462
484
|
|
|
463
485
|
# Get renderer from global registry
|
|
@@ -488,6 +510,7 @@ class PydanticForm(Generic[ModelType]):
|
|
|
488
510
|
field_path=[field_name], # Set top-level field path
|
|
489
511
|
form_name=self.name, # Pass form name
|
|
490
512
|
metrics_dict=self.metrics_dict, # Pass the metrics dict
|
|
513
|
+
keep_skip_json_pathset=self._keep_skip_json_pathset,
|
|
491
514
|
)
|
|
492
515
|
|
|
493
516
|
rendered_field = renderer.render()
|
|
@@ -654,19 +677,25 @@ class PydanticForm(Generic[ModelType]):
|
|
|
654
677
|
if field_name not in self.exclude_fields
|
|
655
678
|
}
|
|
656
679
|
|
|
657
|
-
# Parse non-list fields first - pass the base_prefix and
|
|
680
|
+
# Parse non-list fields first - pass the base_prefix, exclude_fields, and keep paths
|
|
658
681
|
result = _parse_non_list_fields(
|
|
659
682
|
form_dict,
|
|
660
683
|
self.model_class,
|
|
661
684
|
list_field_defs,
|
|
662
685
|
self.base_prefix,
|
|
663
686
|
self.exclude_fields,
|
|
687
|
+
self._keep_skip_json_pathset,
|
|
688
|
+
None, # Top-level parsing, no field path
|
|
664
689
|
)
|
|
665
690
|
|
|
666
|
-
# Parse list fields based on keys present in form_dict - pass the base_prefix
|
|
691
|
+
# Parse list fields based on keys present in form_dict - pass the base_prefix and keep paths
|
|
667
692
|
# Use filtered list field definitions to skip excluded list fields
|
|
668
693
|
list_results = _parse_list_fields(
|
|
669
|
-
form_dict,
|
|
694
|
+
form_dict,
|
|
695
|
+
filtered_list_field_defs,
|
|
696
|
+
self.base_prefix,
|
|
697
|
+
self.exclude_fields,
|
|
698
|
+
self._keep_skip_json_pathset,
|
|
670
699
|
)
|
|
671
700
|
|
|
672
701
|
# Merge list results into the main result
|
|
@@ -695,12 +724,33 @@ class PydanticForm(Generic[ModelType]):
|
|
|
695
724
|
"""
|
|
696
725
|
# Process ALL model fields, not just excluded ones
|
|
697
726
|
for field_name, field_info in self.model_class.model_fields.items():
|
|
698
|
-
#
|
|
699
|
-
if
|
|
700
|
-
|
|
727
|
+
# Special handling for SkipJsonSchema fields
|
|
728
|
+
if _is_skip_json_schema_field(field_info):
|
|
729
|
+
# If keep_skip_json_fields was specified and this field is not kept, always use defaults
|
|
730
|
+
if self.keep_skip_json_fields and not self._is_kept_skip_field(
|
|
731
|
+
[field_name]
|
|
732
|
+
):
|
|
733
|
+
# Remove any existing value and inject default
|
|
734
|
+
if field_name in data:
|
|
735
|
+
del data[field_name]
|
|
736
|
+
# Fall through to default injection logic below
|
|
737
|
+
elif field_name in data:
|
|
738
|
+
# This is either a kept SkipJsonSchema field or no keep list was specified, keep it
|
|
739
|
+
continue
|
|
740
|
+
# If it's a kept field but not in data, fall through to default injection
|
|
741
|
+
else:
|
|
742
|
+
# Skip if already present in parsed data (normal fields)
|
|
743
|
+
if field_name in data:
|
|
744
|
+
continue
|
|
701
745
|
|
|
702
746
|
# First priority: check if initial_values_dict has this field
|
|
703
|
-
|
|
747
|
+
# Use initial values for non-SkipJsonSchema fields, or SkipJsonSchema fields that are kept,
|
|
748
|
+
# or SkipJsonSchema fields when no keep_skip_json_fields list was specified
|
|
749
|
+
if field_name in self.initial_values_dict and (
|
|
750
|
+
not _is_skip_json_schema_field(field_info)
|
|
751
|
+
or not self.keep_skip_json_fields
|
|
752
|
+
or self._is_kept_skip_field([field_name])
|
|
753
|
+
):
|
|
704
754
|
initial_val = self.initial_values_dict[field_name]
|
|
705
755
|
# If the initial value is a BaseModel, convert to dict for consistency
|
|
706
756
|
if hasattr(initial_val, "model_dump"):
|
|
@@ -808,6 +858,7 @@ class PydanticForm(Generic[ModelType]):
|
|
|
808
858
|
field_path=segments, # Pass the full path segments
|
|
809
859
|
form_name=self.name, # Pass the explicit form name
|
|
810
860
|
metrics_dict=self.metrics_dict, # Pass the metrics dict
|
|
861
|
+
keep_skip_json_pathset=self._keep_skip_json_pathset,
|
|
811
862
|
)
|
|
812
863
|
|
|
813
864
|
# Generate a unique placeholder index
|
|
@@ -5,6 +5,7 @@ __all__ = [
|
|
|
5
5
|
"_is_literal_type",
|
|
6
6
|
"_is_enum_type",
|
|
7
7
|
"_is_skip_json_schema_field",
|
|
8
|
+
"normalize_path_segments",
|
|
8
9
|
"MetricEntry",
|
|
9
10
|
"MetricsDict",
|
|
10
11
|
"DecorationScope",
|
|
@@ -18,6 +19,7 @@ from typing import (
|
|
|
18
19
|
Annotated,
|
|
19
20
|
Any,
|
|
20
21
|
Dict,
|
|
22
|
+
List,
|
|
21
23
|
Literal,
|
|
22
24
|
TypedDict,
|
|
23
25
|
Union,
|
|
@@ -38,6 +40,18 @@ class DecorationScope(str, Enum):
|
|
|
38
40
|
BOTH = "both"
|
|
39
41
|
|
|
40
42
|
|
|
43
|
+
def normalize_path_segments(path_segments: List[str]) -> str:
|
|
44
|
+
"""Collapse path segments into a dot path ignoring list indices and placeholders."""
|
|
45
|
+
normalized: List[str] = []
|
|
46
|
+
for segment in path_segments:
|
|
47
|
+
# Coerce to string to avoid surprises from enums or numbers
|
|
48
|
+
seg_str = str(segment)
|
|
49
|
+
if seg_str.isdigit() or seg_str.startswith("new_"):
|
|
50
|
+
continue
|
|
51
|
+
normalized.append(seg_str)
|
|
52
|
+
return ".".join(normalized)
|
|
53
|
+
|
|
54
|
+
|
|
41
55
|
def _is_skip_json_schema_field(annotation_or_field_info: Any) -> bool:
|
|
42
56
|
"""
|
|
43
57
|
Check if a field annotation or field_info indicates it should be skipped in JSON schema.
|
|
@@ -101,8 +115,14 @@ def _is_skip_json_schema_field(annotation_or_field_info: Any) -> bool:
|
|
|
101
115
|
):
|
|
102
116
|
return True
|
|
103
117
|
|
|
104
|
-
# 3. Fallback – cheap but effective
|
|
105
|
-
|
|
118
|
+
# 3. Fallback – cheap but effective, but be more specific to avoid false positives
|
|
119
|
+
# Only match if SkipJsonSchema appears as a standalone word (not part of a class name)
|
|
120
|
+
repr_str = repr(annotation)
|
|
121
|
+
# Look for patterns like "SkipJsonSchema[" or "SkipJsonSchema(" or "SkipJsonSchema]"
|
|
122
|
+
# but not "SomeClassNameSkipJsonSchema"
|
|
123
|
+
import re
|
|
124
|
+
|
|
125
|
+
return bool(re.search(r"\bSkipJsonSchema\b", repr_str))
|
|
106
126
|
|
|
107
127
|
|
|
108
128
|
# Metrics types for field-level annotations
|
|
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
|