fh-pydantic-form 0.1.3__py3-none-any.whl → 0.2.0__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.
- fh_pydantic_form/__init__.py +38 -3
- fh_pydantic_form/defaults.py +160 -0
- fh_pydantic_form/field_renderers.py +339 -134
- fh_pydantic_form/form_parser.py +75 -0
- fh_pydantic_form/form_renderer.py +229 -82
- fh_pydantic_form/type_helpers.py +108 -1
- fh_pydantic_form/ui_style.py +123 -0
- fh_pydantic_form-0.2.0.dist-info/METADATA +685 -0
- fh_pydantic_form-0.2.0.dist-info/RECORD +13 -0
- fh_pydantic_form-0.1.3.dist-info/METADATA +0 -327
- fh_pydantic_form-0.1.3.dist-info/RECORD +0 -11
- {fh_pydantic_form-0.1.3.dist-info → fh_pydantic_form-0.2.0.dist-info}/WHEEL +0 -0
- {fh_pydantic_form-0.1.3.dist-info → fh_pydantic_form-0.2.0.dist-info}/licenses/LICENSE +0 -0
fh_pydantic_form/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
from enum import Enum
|
|
2
3
|
from typing import Literal, get_origin
|
|
3
4
|
|
|
4
5
|
from pydantic import BaseModel
|
|
@@ -7,6 +8,7 @@ from fh_pydantic_form.field_renderers import (
|
|
|
7
8
|
BaseModelFieldRenderer,
|
|
8
9
|
BooleanFieldRenderer,
|
|
9
10
|
DateFieldRenderer,
|
|
11
|
+
EnumFieldRenderer,
|
|
10
12
|
ListFieldRenderer,
|
|
11
13
|
LiteralFieldRenderer,
|
|
12
14
|
NumberFieldRenderer,
|
|
@@ -18,6 +20,16 @@ from fh_pydantic_form.registry import FieldRendererRegistry
|
|
|
18
20
|
from fh_pydantic_form.type_helpers import (
|
|
19
21
|
_get_underlying_type_if_optional,
|
|
20
22
|
)
|
|
23
|
+
from fh_pydantic_form.ui_style import (
|
|
24
|
+
SpacingTheme,
|
|
25
|
+
SpacingValue,
|
|
26
|
+
spacing,
|
|
27
|
+
COMPACT_EXTRA_CSS,
|
|
28
|
+
)
|
|
29
|
+
from fh_pydantic_form.defaults import (
|
|
30
|
+
default_dict_for_model,
|
|
31
|
+
default_for_annotation,
|
|
32
|
+
)
|
|
21
33
|
|
|
22
34
|
|
|
23
35
|
def register_default_renderers() -> None:
|
|
@@ -27,7 +39,7 @@ def register_default_renderers() -> None:
|
|
|
27
39
|
This method sets up:
|
|
28
40
|
- Simple type renderers (str, bool, int, float, date, time)
|
|
29
41
|
- Special field renderers (Detail)
|
|
30
|
-
- Predicate-based renderers (Literal fields, lists, BaseModels)
|
|
42
|
+
- Predicate-based renderers (Literal fields, Enum fields, lists, BaseModels)
|
|
31
43
|
"""
|
|
32
44
|
# Import renderers by getting them from globals
|
|
33
45
|
|
|
@@ -39,7 +51,20 @@ def register_default_renderers() -> None:
|
|
|
39
51
|
FieldRendererRegistry.register_type_renderer(datetime.date, DateFieldRenderer)
|
|
40
52
|
FieldRendererRegistry.register_type_renderer(datetime.time, TimeFieldRenderer)
|
|
41
53
|
|
|
42
|
-
# Register
|
|
54
|
+
# Register Enum field renderer (before Literal to prioritize Enum handling)
|
|
55
|
+
def is_enum_field(field_info):
|
|
56
|
+
"""Check if field is an Enum type"""
|
|
57
|
+
annotation = getattr(field_info, "annotation", None)
|
|
58
|
+
if not annotation:
|
|
59
|
+
return False
|
|
60
|
+
underlying_type = _get_underlying_type_if_optional(annotation)
|
|
61
|
+
return isinstance(underlying_type, type) and issubclass(underlying_type, Enum)
|
|
62
|
+
|
|
63
|
+
FieldRendererRegistry.register_type_renderer_with_predicate(
|
|
64
|
+
is_enum_field, EnumFieldRenderer
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Register Literal field renderer (after Enum to avoid conflicts)
|
|
43
68
|
def is_literal_field(field_info):
|
|
44
69
|
"""Check if field is a Literal type"""
|
|
45
70
|
annotation = getattr(field_info, "annotation", None)
|
|
@@ -87,4 +112,14 @@ def register_default_renderers() -> None:
|
|
|
87
112
|
register_default_renderers()
|
|
88
113
|
|
|
89
114
|
|
|
90
|
-
__all__ = [
|
|
115
|
+
__all__ = [
|
|
116
|
+
"PydanticForm",
|
|
117
|
+
"FieldRendererRegistry",
|
|
118
|
+
"list_manipulation_js",
|
|
119
|
+
"SpacingTheme",
|
|
120
|
+
"SpacingValue",
|
|
121
|
+
"spacing",
|
|
122
|
+
"COMPACT_EXTRA_CSS",
|
|
123
|
+
"default_dict_for_model",
|
|
124
|
+
"default_for_annotation",
|
|
125
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as _dt
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, get_args, get_origin, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from .type_helpers import _UNSET, get_default, _is_optional_type
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _today():
|
|
13
|
+
"""Wrapper for datetime.date.today() to enable testability."""
|
|
14
|
+
return _dt.date.today()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Simple type defaults - callables will be invoked to get fresh values
|
|
18
|
+
_SIMPLE_DEFAULTS = {
|
|
19
|
+
str: "",
|
|
20
|
+
int: 0,
|
|
21
|
+
float: 0.0,
|
|
22
|
+
bool: False,
|
|
23
|
+
_dt.date: lambda: _today(), # callable - gets current date (late-bound)
|
|
24
|
+
_dt.time: lambda: _dt.time(0, 0), # callable - midnight
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _first_literal_choice(annotation):
|
|
29
|
+
"""Get the first literal value from a Literal type annotation."""
|
|
30
|
+
args = get_args(annotation)
|
|
31
|
+
return args[0] if args else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_for_annotation(annotation: Any) -> Any:
|
|
35
|
+
"""
|
|
36
|
+
Return a sensible runtime default for type annotations.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
annotation: The type annotation to generate a default for
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A sensible default value for the given type
|
|
43
|
+
"""
|
|
44
|
+
origin = get_origin(annotation) or annotation
|
|
45
|
+
|
|
46
|
+
# Optional[T] → None
|
|
47
|
+
if _is_optional_type(annotation):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
# Literal[...] → first literal value
|
|
51
|
+
if origin is Literal:
|
|
52
|
+
return _first_literal_choice(annotation)
|
|
53
|
+
|
|
54
|
+
# Enum → first member value
|
|
55
|
+
if isinstance(origin, type) and issubclass(origin, Enum):
|
|
56
|
+
enum_members = list(origin)
|
|
57
|
+
return enum_members[0].value if enum_members else None
|
|
58
|
+
|
|
59
|
+
# Simple primitives & datetime helpers
|
|
60
|
+
if origin in _SIMPLE_DEFAULTS:
|
|
61
|
+
default_val = _SIMPLE_DEFAULTS[origin]
|
|
62
|
+
return default_val() if callable(default_val) else default_val
|
|
63
|
+
|
|
64
|
+
# For unknown types, return None as a safe fallback
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _convert_enum_values(obj: Any) -> Any:
|
|
69
|
+
"""
|
|
70
|
+
Recursively convert enum instances to their values in nested structures.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
obj: Object that may contain enum instances
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Object with enum instances converted to their values
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(obj, Enum):
|
|
79
|
+
return obj.value
|
|
80
|
+
elif isinstance(obj, dict):
|
|
81
|
+
return {key: _convert_enum_values(value) for key, value in obj.items()}
|
|
82
|
+
elif isinstance(obj, list):
|
|
83
|
+
return [_convert_enum_values(item) for item in obj]
|
|
84
|
+
else:
|
|
85
|
+
return obj
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def default_dict_for_model(model_cls: type[BaseModel]) -> dict[str, Any]:
|
|
89
|
+
"""
|
|
90
|
+
Recursively build a dict with sensible defaults for all fields in a Pydantic model.
|
|
91
|
+
|
|
92
|
+
Precedence order:
|
|
93
|
+
1. User-defined @classmethod default() override
|
|
94
|
+
2. Field.default or Field.default_factory values
|
|
95
|
+
3. None for Optional fields without explicit defaults
|
|
96
|
+
4. Smart defaults for primitive types
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
model_cls: The Pydantic model class to generate defaults for
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dictionary with default values for all model fields
|
|
103
|
+
"""
|
|
104
|
+
# Check for user-defined default classmethod first
|
|
105
|
+
if hasattr(model_cls, "default") and callable(model_cls.default):
|
|
106
|
+
instance = model_cls.default() # may return model instance or dict
|
|
107
|
+
result = (
|
|
108
|
+
instance.model_dump() if isinstance(instance, BaseModel) else dict(instance)
|
|
109
|
+
)
|
|
110
|
+
return _convert_enum_values(result)
|
|
111
|
+
|
|
112
|
+
out: dict[str, Any] = {}
|
|
113
|
+
|
|
114
|
+
for name, field in model_cls.model_fields.items():
|
|
115
|
+
# --- NEW: recognise "today" factories for date fields early ---------
|
|
116
|
+
if (get_origin(field.annotation) or field.annotation) is _dt.date and getattr(
|
|
117
|
+
field, "default_factory", None
|
|
118
|
+
) is not None:
|
|
119
|
+
# Never call the real factory – delegate to our _today() helper so
|
|
120
|
+
# tests can patch it (freeze_today fixture).
|
|
121
|
+
out[name] = _today()
|
|
122
|
+
continue
|
|
123
|
+
# --------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
# 1. Check for model-supplied default or factory
|
|
126
|
+
default_val = get_default(field) # returns _UNSET if no default
|
|
127
|
+
if default_val is not _UNSET:
|
|
128
|
+
# Handle BaseModel defaults by converting to dict
|
|
129
|
+
if hasattr(default_val, "model_dump"):
|
|
130
|
+
out[name] = default_val.model_dump()
|
|
131
|
+
# Convert enum instances to their values
|
|
132
|
+
elif isinstance(default_val, Enum):
|
|
133
|
+
out[name] = default_val.value
|
|
134
|
+
else:
|
|
135
|
+
out[name] = default_val
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# 2. Optional fields without explicit default → None
|
|
139
|
+
if _is_optional_type(field.annotation):
|
|
140
|
+
out[name] = None
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# 3. Handle nested structures
|
|
144
|
+
ann = field.annotation
|
|
145
|
+
base_ann = get_origin(ann) or ann
|
|
146
|
+
|
|
147
|
+
# List fields start empty
|
|
148
|
+
if base_ann is list:
|
|
149
|
+
out[name] = []
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Nested BaseModel - recurse
|
|
153
|
+
if isinstance(base_ann, type) and issubclass(base_ann, BaseModel):
|
|
154
|
+
out[name] = default_dict_for_model(base_ann)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
# 4. Fallback to smart defaults for primitives
|
|
158
|
+
out[name] = default_for_annotation(ann)
|
|
159
|
+
|
|
160
|
+
return _convert_enum_values(out)
|