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.
@@ -0,0 +1,12 @@
1
+ """
2
+ Shared constants and sentinel values used across the library.
3
+ """
4
+
5
+
6
+ class _Unset:
7
+ """Sentinel class to indicate an unset value."""
8
+
9
+ pass
10
+
11
+
12
+ _UNSET = _Unset()
@@ -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)