fh-pydantic-form 0.3.9__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.
- fh_pydantic_form/__init__.py +143 -0
- fh_pydantic_form/color_utils.py +598 -0
- fh_pydantic_form/comparison_form.py +1637 -0
- fh_pydantic_form/constants.py +12 -0
- fh_pydantic_form/defaults.py +188 -0
- fh_pydantic_form/field_renderers.py +2330 -0
- fh_pydantic_form/form_parser.py +756 -0
- fh_pydantic_form/form_renderer.py +1004 -0
- fh_pydantic_form/list_path.py +145 -0
- fh_pydantic_form/py.typed +0 -0
- fh_pydantic_form/registry.py +142 -0
- fh_pydantic_form/type_helpers.py +266 -0
- fh_pydantic_form/ui_style.py +115 -0
- fh_pydantic_form-0.3.9.dist-info/METADATA +1168 -0
- fh_pydantic_form-0.3.9.dist-info/RECORD +17 -0
- fh_pydantic_form-0.3.9.dist-info/WHEEL +4 -0
- fh_pydantic_form-0.3.9.dist-info/licenses/LICENSE +13 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as _dt
|
|
4
|
+
import decimal
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Literal, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from fh_pydantic_form.constants import _UNSET
|
|
11
|
+
from fh_pydantic_form.type_helpers import (
|
|
12
|
+
_is_optional_type,
|
|
13
|
+
_is_skip_json_schema_field,
|
|
14
|
+
get_default,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _today():
|
|
19
|
+
"""Wrapper for datetime.date.today() to enable testability."""
|
|
20
|
+
return _dt.date.today()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Simple type defaults - callables will be invoked to get fresh values
|
|
24
|
+
_SIMPLE_DEFAULTS = {
|
|
25
|
+
str: "",
|
|
26
|
+
int: 0,
|
|
27
|
+
float: 0.0,
|
|
28
|
+
bool: False,
|
|
29
|
+
decimal.Decimal: decimal.Decimal("0"),
|
|
30
|
+
_dt.date: lambda: _today(), # callable - gets current date (late-bound)
|
|
31
|
+
_dt.time: lambda: _dt.time(0, 0), # callable - midnight
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _first_literal_choice(annotation):
|
|
36
|
+
"""Get the first literal value from a Literal type annotation."""
|
|
37
|
+
args = get_args(annotation)
|
|
38
|
+
return args[0] if args else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def default_for_annotation(annotation: Any) -> Any:
|
|
42
|
+
"""
|
|
43
|
+
Return a sensible runtime default for type annotations.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
annotation: The type annotation to generate a default for
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A sensible default value for the given type
|
|
50
|
+
"""
|
|
51
|
+
origin = get_origin(annotation) or annotation
|
|
52
|
+
|
|
53
|
+
# Optional[T] → None
|
|
54
|
+
if _is_optional_type(annotation):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# List[T] → []
|
|
58
|
+
if origin is list:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
# Literal[...] → first literal value
|
|
62
|
+
if origin is Literal:
|
|
63
|
+
return _first_literal_choice(annotation)
|
|
64
|
+
|
|
65
|
+
# Enum → first member value
|
|
66
|
+
if isinstance(origin, type) and issubclass(origin, Enum):
|
|
67
|
+
enum_members = list(origin)
|
|
68
|
+
return enum_members[0].value if enum_members else None
|
|
69
|
+
|
|
70
|
+
# Simple primitives & datetime helpers
|
|
71
|
+
if origin in _SIMPLE_DEFAULTS:
|
|
72
|
+
default_val = _SIMPLE_DEFAULTS[origin]
|
|
73
|
+
return default_val() if callable(default_val) else default_val
|
|
74
|
+
|
|
75
|
+
# For unknown types, return None as a safe fallback
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _convert_enum_values(obj: Any) -> Any:
|
|
80
|
+
"""
|
|
81
|
+
Recursively convert enum instances to their values in nested structures.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
obj: Object that may contain enum instances
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Object with enum instances converted to their values
|
|
88
|
+
"""
|
|
89
|
+
if isinstance(obj, Enum):
|
|
90
|
+
return obj.value
|
|
91
|
+
elif isinstance(obj, dict):
|
|
92
|
+
return {key: _convert_enum_values(value) for key, value in obj.items()}
|
|
93
|
+
elif isinstance(obj, list):
|
|
94
|
+
return [_convert_enum_values(item) for item in obj]
|
|
95
|
+
else:
|
|
96
|
+
return obj
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def default_dict_for_model(model_cls: type[BaseModel]) -> dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Recursively build a dict with sensible defaults for all fields in a Pydantic model.
|
|
102
|
+
|
|
103
|
+
Precedence order:
|
|
104
|
+
1. User-defined @classmethod default() override
|
|
105
|
+
2. Field.default or Field.default_factory values
|
|
106
|
+
3. None for Optional fields without explicit defaults
|
|
107
|
+
4. Smart defaults for primitive types
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
model_cls: The Pydantic model class to generate defaults for
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary with default values for all model fields
|
|
114
|
+
"""
|
|
115
|
+
# Check for user-defined default classmethod first
|
|
116
|
+
if hasattr(model_cls, "default") and callable(model_cls.default):
|
|
117
|
+
instance = model_cls.default() # may return model instance or dict
|
|
118
|
+
result = (
|
|
119
|
+
instance.model_dump() if isinstance(instance, BaseModel) else dict(instance)
|
|
120
|
+
)
|
|
121
|
+
return _convert_enum_values(result)
|
|
122
|
+
|
|
123
|
+
out: dict[str, Any] = {}
|
|
124
|
+
|
|
125
|
+
for name, field in model_cls.model_fields.items():
|
|
126
|
+
# --- NEW: recognise "today" factories for date fields early ---------
|
|
127
|
+
if (get_origin(field.annotation) or field.annotation) is _dt.date and getattr(
|
|
128
|
+
field, "default_factory", None
|
|
129
|
+
) is not None:
|
|
130
|
+
# Never call the real factory – delegate to our _today() helper so
|
|
131
|
+
# tests can patch it (freeze_today fixture).
|
|
132
|
+
out[name] = _today()
|
|
133
|
+
continue
|
|
134
|
+
# --------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
# Check if this is a SkipJsonSchema field - if so, always get its default
|
|
137
|
+
if _is_skip_json_schema_field(field):
|
|
138
|
+
default_val = get_default(field)
|
|
139
|
+
if default_val is not _UNSET:
|
|
140
|
+
# Handle BaseModel defaults by converting to dict
|
|
141
|
+
if hasattr(default_val, "model_dump"):
|
|
142
|
+
out[name] = default_val.model_dump()
|
|
143
|
+
# Convert enum instances to their values
|
|
144
|
+
elif isinstance(default_val, Enum):
|
|
145
|
+
out[name] = default_val.value
|
|
146
|
+
else:
|
|
147
|
+
out[name] = default_val
|
|
148
|
+
else:
|
|
149
|
+
# No default for SkipJsonSchema field - use smart default
|
|
150
|
+
out[name] = default_for_annotation(field.annotation)
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# 1. Check for model-supplied default or factory
|
|
154
|
+
default_val = get_default(field) # returns _UNSET if no default
|
|
155
|
+
if default_val is not _UNSET:
|
|
156
|
+
# Handle BaseModel defaults by converting to dict
|
|
157
|
+
if hasattr(default_val, "model_dump"):
|
|
158
|
+
out[name] = default_val.model_dump()
|
|
159
|
+
# Convert enum instances to their values
|
|
160
|
+
elif isinstance(default_val, Enum):
|
|
161
|
+
out[name] = default_val.value
|
|
162
|
+
else:
|
|
163
|
+
out[name] = default_val
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# 2. Optional fields without explicit default → None
|
|
167
|
+
if _is_optional_type(field.annotation):
|
|
168
|
+
out[name] = None
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# 3. Handle nested structures
|
|
172
|
+
ann = field.annotation
|
|
173
|
+
base_ann = get_origin(ann) or ann
|
|
174
|
+
|
|
175
|
+
# List fields start empty
|
|
176
|
+
if base_ann is list:
|
|
177
|
+
out[name] = []
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Nested BaseModel - recurse
|
|
181
|
+
if isinstance(base_ann, type) and issubclass(base_ann, BaseModel):
|
|
182
|
+
out[name] = default_dict_for_model(base_ann)
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# 4. Fallback to smart defaults for primitives
|
|
186
|
+
out[name] = default_for_annotation(ann)
|
|
187
|
+
|
|
188
|
+
return _convert_enum_values(out)
|