fh-pydantic-form 0.1.3__py3-none-any.whl → 0.2.1__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.

@@ -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 Literal field renderer
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__ = ["PydanticForm", "FieldRendererRegistry", "list_manipulation_js"]
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)