lionagi 0.17.9__py3-none-any.whl → 0.17.10__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.
- lionagi/fields/reason.py +2 -0
- lionagi/ln/types.py +45 -0
- lionagi/models/field_model.py +393 -286
- lionagi/models/hashable_model.py +96 -14
- lionagi/models/model_params.py +271 -269
- lionagi/models/operable_model.py +8 -8
- lionagi/protocols/generic/pile.py +7 -4
- lionagi/protocols/operatives/operative.py +29 -6
- lionagi/service/third_party/anthropic_models.py +2 -3
- lionagi/version.py +1 -1
- {lionagi-0.17.9.dist-info → lionagi-0.17.10.dist-info}/METADATA +2 -2
- {lionagi-0.17.9.dist-info → lionagi-0.17.10.dist-info}/RECORD +14 -14
- {lionagi-0.17.9.dist-info → lionagi-0.17.10.dist-info}/WHEEL +0 -0
- {lionagi-0.17.9.dist-info → lionagi-0.17.10.dist-info}/licenses/LICENSE +0 -0
lionagi/models/field_model.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Field model implementation for compositional field definitions.
|
2
2
|
|
3
|
-
This module provides FieldModel, a
|
3
|
+
This module provides FieldModel, a Params-based class that enables
|
4
4
|
compositional field definitions with lazy materialization and aggressive caching.
|
5
5
|
"""
|
6
6
|
|
@@ -10,12 +10,14 @@ import os
|
|
10
10
|
import threading
|
11
11
|
from collections import OrderedDict
|
12
12
|
from collections.abc import Callable
|
13
|
-
from dataclasses import dataclass
|
14
|
-
from
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from dataclasses import field as dc_field
|
15
|
+
from typing import Annotated, Any, ClassVar
|
15
16
|
|
16
17
|
from typing_extensions import Self, override
|
17
18
|
|
18
19
|
from .._errors import ValidationError
|
20
|
+
from ..ln.types import Meta, Params
|
19
21
|
|
20
22
|
# Cache of valid Pydantic Field parameters
|
21
23
|
_PYDANTIC_FIELD_PARAMS: set[str] | None = None
|
@@ -38,7 +40,7 @@ def _get_pydantic_field_params() -> set[str]:
|
|
38
40
|
|
39
41
|
# Global cache for annotated types with bounded size
|
40
42
|
_MAX_CACHE_SIZE = int(os.environ.get("LIONAGI_FIELD_CACHE_SIZE", "10000"))
|
41
|
-
_annotated_cache: OrderedDict[tuple[type, tuple[
|
43
|
+
_annotated_cache: OrderedDict[tuple[type, tuple[Meta, ...]], type] = (
|
42
44
|
OrderedDict()
|
43
45
|
)
|
44
46
|
_cache_lock = threading.RLock() # Thread-safe access to cache
|
@@ -47,57 +49,20 @@ _cache_lock = threading.RLock() # Thread-safe access to cache
|
|
47
49
|
METADATA_LIMIT = int(os.environ.get("LIONAGI_FIELD_META_LIMIT", "10"))
|
48
50
|
|
49
51
|
|
50
|
-
@dataclass(slots=True, frozen=True)
|
51
|
-
class FieldMeta:
|
52
|
-
"""Immutable metadata container for field templates."""
|
53
|
-
|
54
|
-
key: str
|
55
|
-
value: Any
|
56
|
-
|
57
|
-
@override
|
58
|
-
def __hash__(self) -> int:
|
59
|
-
"""Make metadata hashable for caching.
|
60
|
-
|
61
|
-
Note: For callables, we hash by id to maintain identity semantics.
|
62
|
-
"""
|
63
|
-
# For callables, use their id
|
64
|
-
if callable(self.value):
|
65
|
-
return hash((self.key, id(self.value)))
|
66
|
-
# For other values, try to hash directly
|
67
|
-
try:
|
68
|
-
return hash((self.key, self.value))
|
69
|
-
except TypeError:
|
70
|
-
# Fallback for unhashable types
|
71
|
-
return hash((self.key, str(self.value)))
|
72
|
-
|
73
|
-
@override
|
74
|
-
def __eq__(self, other: object) -> bool:
|
75
|
-
"""Compare metadata for equality.
|
76
|
-
|
77
|
-
For callables, compare by id to increase cache hits when the same
|
78
|
-
validator instance is reused. For other values, use standard equality.
|
79
|
-
"""
|
80
|
-
if not isinstance(other, FieldMeta):
|
81
|
-
return NotImplemented
|
82
|
-
|
83
|
-
if self.key != other.key:
|
84
|
-
return False
|
85
|
-
|
86
|
-
# For callables, compare by identity
|
87
|
-
if callable(self.value) and callable(other.value):
|
88
|
-
return id(self.value) == id(other.value)
|
89
|
-
|
90
|
-
# For other values, use standard equality
|
91
|
-
return bool(self.value == other.value)
|
92
|
-
|
93
|
-
|
94
52
|
@dataclass(slots=True, frozen=True, init=False)
|
95
|
-
class FieldModel:
|
53
|
+
class FieldModel(Params):
|
96
54
|
"""Field model for compositional field definitions.
|
97
55
|
|
98
56
|
This class provides a way to define field models that can be composed
|
99
57
|
and materialized lazily with aggressive caching for performance.
|
100
58
|
|
59
|
+
Key features:
|
60
|
+
- All unspecified fields are explicitly Unset (not None or empty)
|
61
|
+
- No silent type conversions - fails fast on incorrect types
|
62
|
+
- Aggressive caching of materialized types with LRU eviction
|
63
|
+
- Thread-safe field creation and caching
|
64
|
+
- Not directly instantiable - requires keyword arguments
|
65
|
+
|
101
66
|
Attributes:
|
102
67
|
base_type: The base Python type for this field
|
103
68
|
metadata: Tuple of metadata to attach via Annotated
|
@@ -107,56 +72,123 @@ class FieldModel:
|
|
107
72
|
LIONAGI_FIELD_META_LIMIT: Maximum metadata items per template (default: 10)
|
108
73
|
|
109
74
|
Example:
|
110
|
-
>>> field = FieldModel(str)
|
75
|
+
>>> field = FieldModel(base_type=str, name="username")
|
111
76
|
>>> nullable_field = field.as_nullable()
|
112
77
|
>>> annotated_type = nullable_field.annotated()
|
113
78
|
"""
|
114
79
|
|
80
|
+
# Class configuration - let Params handle Unset population
|
81
|
+
_prefill_unset: ClassVar[bool] = True
|
82
|
+
_none_as_sentinel: ClassVar[bool] = True
|
83
|
+
|
84
|
+
# Public fields (all start as Unset when not provided)
|
115
85
|
base_type: type[Any]
|
116
|
-
metadata: tuple[
|
117
|
-
|
118
|
-
def __init__(
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
"""
|
86
|
+
metadata: tuple[Meta, ...]
|
87
|
+
|
88
|
+
def __init__(self, **kwargs: Any) -> None:
|
89
|
+
"""Initialize FieldModel with legacy compatibility.
|
90
|
+
|
91
|
+
Handles backward compatibility by converting old-style kwargs to the new
|
92
|
+
Params-based format.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
**kwargs: Arbitrary keyword arguments, including legacy ones
|
96
|
+
"""
|
97
|
+
# Convert legacy kwargs to proper format
|
98
|
+
converted = self._convert_kwargs_to_params(**kwargs)
|
99
|
+
|
100
|
+
# Set fields directly and validate
|
101
|
+
for k, v in converted.items():
|
102
|
+
if k in self.allowed():
|
103
|
+
object.__setattr__(self, k, v)
|
104
|
+
else:
|
105
|
+
raise ValueError(f"Invalid parameter: {k}")
|
106
|
+
|
107
|
+
# Validate after setting all attributes
|
108
|
+
self._validate()
|
109
|
+
|
110
|
+
def _validate(self) -> None:
|
111
|
+
"""Validate field configuration and process metadata.
|
112
|
+
|
113
|
+
This method performs minimal domain-specific validation, then processes
|
114
|
+
and validates the metadata configuration.
|
115
|
+
|
116
|
+
Raises:
|
117
|
+
ValueError: If base_type is invalid or metadata is malformed
|
118
|
+
"""
|
119
|
+
# Let parent handle basic Unset population
|
120
|
+
Params._validate(self)
|
121
|
+
|
122
|
+
# Minimal domain validation - only check what matters
|
123
|
+
if not self._is_sentinel(self.base_type):
|
124
|
+
# Allow types, GenericAlias (like list[str]), and union types (like str | None)
|
125
|
+
# Check for type, generic types, or union types
|
126
|
+
import types
|
127
|
+
|
128
|
+
is_valid_type = (
|
129
|
+
isinstance(self.base_type, type)
|
130
|
+
or hasattr(self.base_type, "__origin__")
|
131
|
+
or isinstance(
|
132
|
+
self.base_type, types.UnionType
|
133
|
+
) # Python 3.10+ union types (str | None)
|
134
|
+
or str(type(self.base_type))
|
135
|
+
== "<class 'types.UnionType'>" # Fallback check
|
136
|
+
)
|
137
|
+
if not is_valid_type:
|
138
|
+
raise ValueError(
|
139
|
+
f"base_type must be a type or type annotation, got {self.base_type}"
|
140
|
+
)
|
141
|
+
|
142
|
+
# Validate metadata limit
|
143
|
+
if not self._is_sentinel(self.metadata):
|
144
|
+
if len(self.metadata) > METADATA_LIMIT:
|
145
|
+
import warnings
|
146
|
+
|
147
|
+
warnings.warn(
|
148
|
+
f"FieldModel has {len(self.metadata)} metadata items, "
|
149
|
+
f"exceeding recommended limit of {METADATA_LIMIT}. "
|
150
|
+
"Consider simplifying the field definition.",
|
151
|
+
stacklevel=3,
|
152
|
+
)
|
153
|
+
|
154
|
+
@classmethod
|
155
|
+
def _convert_kwargs_to_params(cls, **kwargs: Any) -> dict[str, Any]:
|
156
|
+
"""Convert legacy kwargs to Params-compatible format.
|
157
|
+
|
158
|
+
This handles backward compatibility with the old FieldModel API.
|
127
159
|
|
128
160
|
Args:
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
**kwargs: Additional metadata as keyword arguments that will be converted to FieldMeta
|
161
|
+
**kwargs: Legacy keyword arguments
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
Dictionary of converted parameters
|
134
165
|
"""
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
base_type =
|
140
|
-
|
141
|
-
#
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
166
|
+
params = {}
|
167
|
+
|
168
|
+
# Handle annotation alias for base_type
|
169
|
+
if "annotation" in kwargs and "base_type" not in kwargs:
|
170
|
+
params["base_type"] = kwargs.pop("annotation")
|
171
|
+
|
172
|
+
# Handle name in metadata
|
173
|
+
if "name" in kwargs:
|
174
|
+
name = kwargs.pop("name")
|
175
|
+
if name != "field": # Only add if non-default
|
176
|
+
metadata = list(kwargs.get("metadata", ()))
|
177
|
+
metadata.append(Meta("name", name))
|
178
|
+
params["metadata"] = tuple(metadata)
|
179
|
+
|
180
|
+
# Handle special flags
|
181
|
+
if "nullable" in kwargs and kwargs.pop("nullable"):
|
182
|
+
metadata = list(params.get("metadata", ()))
|
183
|
+
metadata.append(Meta("nullable", True))
|
184
|
+
params["metadata"] = tuple(metadata)
|
185
|
+
|
186
|
+
if "listable" in kwargs and kwargs.pop("listable"):
|
187
|
+
metadata = list(params.get("metadata", ()))
|
188
|
+
metadata.append(Meta("listable", True))
|
189
|
+
params["metadata"] = tuple(metadata)
|
190
|
+
|
191
|
+
# Validate conflicting defaults
|
160
192
|
if "default" in kwargs and "default_factory" in kwargs:
|
161
193
|
raise ValueError("Cannot have both default and default_factory")
|
162
194
|
|
@@ -171,40 +203,32 @@ class FieldModel:
|
|
171
203
|
"Validators must be a list of functions or a function"
|
172
204
|
)
|
173
205
|
|
174
|
-
# Convert remaining kwargs to
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
object.__setattr__(self, "metadata", tuple(meta_list))
|
206
|
+
# Convert remaining kwargs to metadata
|
207
|
+
if kwargs:
|
208
|
+
metadata = list(params.get("metadata", ()))
|
209
|
+
for key, value in kwargs.items():
|
210
|
+
metadata.append(Meta(key, value))
|
211
|
+
params["metadata"] = tuple(metadata)
|
181
212
|
|
182
|
-
|
183
|
-
self.__post_init__()
|
213
|
+
return params
|
184
214
|
|
185
215
|
def __getattr__(self, name: str) -> Any:
|
186
216
|
"""Handle access to custom attributes stored in metadata."""
|
187
217
|
# Check if the attribute exists in metadata
|
188
|
-
|
189
|
-
|
190
|
-
|
218
|
+
if not self._is_sentinel(self.metadata):
|
219
|
+
for meta in self.metadata:
|
220
|
+
if meta.key == name:
|
221
|
+
return meta.value
|
222
|
+
|
223
|
+
# Special handling for common attributes with defaults
|
224
|
+
if name == "name":
|
225
|
+
return "field"
|
226
|
+
|
191
227
|
# If not found, raise AttributeError as usual
|
192
228
|
raise AttributeError(
|
193
229
|
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
194
230
|
)
|
195
231
|
|
196
|
-
def __post_init__(self) -> None:
|
197
|
-
"""Validate metadata limit after initialization."""
|
198
|
-
if len(self.metadata) > METADATA_LIMIT:
|
199
|
-
import warnings
|
200
|
-
|
201
|
-
warnings.warn(
|
202
|
-
f"FieldModel has {len(self.metadata)} metadata items, "
|
203
|
-
f"exceeding recommended limit of {METADATA_LIMIT}. "
|
204
|
-
"Consider simplifying the field definition.",
|
205
|
-
stacklevel=3, # Show user's call site, not __post_init__
|
206
|
-
)
|
207
|
-
|
208
232
|
# ---- factory helpers -------------------------------------------------- #
|
209
233
|
|
210
234
|
def as_nullable(self) -> Self:
|
@@ -214,8 +238,16 @@ class FieldModel:
|
|
214
238
|
New FieldModel with nullable metadata added
|
215
239
|
"""
|
216
240
|
# Add nullable marker to metadata
|
217
|
-
|
218
|
-
|
241
|
+
current_metadata = (
|
242
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
243
|
+
)
|
244
|
+
new_metadata = (*current_metadata, Meta("nullable", True))
|
245
|
+
# Create new instance directly without going through __init__
|
246
|
+
new_instance = object.__new__(type(self))
|
247
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
248
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
249
|
+
new_instance._validate()
|
250
|
+
return new_instance
|
219
251
|
|
220
252
|
def as_listable(self) -> Self:
|
221
253
|
"""Create a new field model that wraps the type in a list.
|
@@ -226,11 +258,23 @@ class FieldModel:
|
|
226
258
|
Returns:
|
227
259
|
New FieldModel with list wrapper
|
228
260
|
"""
|
261
|
+
# Get current base type
|
262
|
+
current_base = (
|
263
|
+
Any if self._is_sentinel(self.base_type) else self.base_type
|
264
|
+
)
|
229
265
|
# Change base type to list of current type
|
230
|
-
new_base = list[
|
266
|
+
new_base = list[current_base] # type: ignore
|
231
267
|
# Add listable marker to metadata
|
232
|
-
|
233
|
-
|
268
|
+
current_metadata = (
|
269
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
270
|
+
)
|
271
|
+
new_metadata = (*current_metadata, Meta("listable", True))
|
272
|
+
# Create new instance directly without going through __init__
|
273
|
+
new_instance = object.__new__(type(self))
|
274
|
+
object.__setattr__(new_instance, "base_type", new_base)
|
275
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
276
|
+
new_instance._validate()
|
277
|
+
return new_instance
|
234
278
|
|
235
279
|
def with_validator(self, f: Callable[[Any], bool]) -> Self:
|
236
280
|
"""Add a validator function to this field model.
|
@@ -242,8 +286,16 @@ class FieldModel:
|
|
242
286
|
New FieldModel with validator added
|
243
287
|
"""
|
244
288
|
# Add validator to metadata
|
245
|
-
|
246
|
-
|
289
|
+
current_metadata = (
|
290
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
291
|
+
)
|
292
|
+
new_metadata = (*current_metadata, Meta("validator", f))
|
293
|
+
# Create new instance directly without going through __init__
|
294
|
+
new_instance = object.__new__(type(self))
|
295
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
296
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
297
|
+
new_instance._validate()
|
298
|
+
return new_instance
|
247
299
|
|
248
300
|
def with_description(self, description: str) -> Self:
|
249
301
|
"""Add a description to this field model.
|
@@ -255,14 +307,23 @@ class FieldModel:
|
|
255
307
|
New FieldModel with description added
|
256
308
|
"""
|
257
309
|
# Remove any existing description
|
310
|
+
current_metadata = (
|
311
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
312
|
+
)
|
258
313
|
filtered_metadata = tuple(
|
259
|
-
m for m in
|
314
|
+
m for m in current_metadata if m.key != "description"
|
260
315
|
)
|
261
316
|
new_metadata = (
|
262
317
|
*filtered_metadata,
|
263
|
-
|
318
|
+
Meta("description", description),
|
264
319
|
)
|
265
|
-
|
320
|
+
|
321
|
+
# Create new instance directly without going through __init__
|
322
|
+
new_instance = object.__new__(type(self))
|
323
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
324
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
325
|
+
new_instance._validate()
|
326
|
+
return new_instance
|
266
327
|
|
267
328
|
def with_default(self, default: Any) -> Self:
|
268
329
|
"""Add a default value to this field model.
|
@@ -274,11 +335,19 @@ class FieldModel:
|
|
274
335
|
New FieldModel with default added
|
275
336
|
"""
|
276
337
|
# Remove any existing default metadata to avoid conflicts
|
338
|
+
current_metadata = (
|
339
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
340
|
+
)
|
277
341
|
filtered_metadata = tuple(
|
278
|
-
m for m in
|
342
|
+
m for m in current_metadata if m.key != "default"
|
279
343
|
)
|
280
|
-
new_metadata = (*filtered_metadata,
|
281
|
-
|
344
|
+
new_metadata = (*filtered_metadata, Meta("default", default))
|
345
|
+
# Create new instance directly without going through __init__
|
346
|
+
new_instance = object.__new__(type(self))
|
347
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
348
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
349
|
+
new_instance._validate()
|
350
|
+
return new_instance
|
282
351
|
|
283
352
|
def with_frozen(self, frozen: bool = True) -> Self:
|
284
353
|
"""Mark this field as frozen (immutable after creation).
|
@@ -290,11 +359,19 @@ class FieldModel:
|
|
290
359
|
New FieldModel with frozen setting
|
291
360
|
"""
|
292
361
|
# Remove any existing frozen metadata
|
362
|
+
current_metadata = (
|
363
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
364
|
+
)
|
293
365
|
filtered_metadata = tuple(
|
294
|
-
m for m in
|
366
|
+
m for m in current_metadata if m.key != "frozen"
|
295
367
|
)
|
296
|
-
new_metadata = (*filtered_metadata,
|
297
|
-
|
368
|
+
new_metadata = (*filtered_metadata, Meta("frozen", frozen))
|
369
|
+
# Create new instance directly without going through __init__
|
370
|
+
new_instance = object.__new__(type(self))
|
371
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
372
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
373
|
+
new_instance._validate()
|
374
|
+
return new_instance
|
298
375
|
|
299
376
|
def with_alias(self, alias: str) -> Self:
|
300
377
|
"""Add an alias to this field.
|
@@ -306,8 +383,13 @@ class FieldModel:
|
|
306
383
|
New FieldModel with alias
|
307
384
|
"""
|
308
385
|
filtered_metadata = tuple(m for m in self.metadata if m.key != "alias")
|
309
|
-
new_metadata = (*filtered_metadata,
|
310
|
-
|
386
|
+
new_metadata = (*filtered_metadata, Meta("alias", alias))
|
387
|
+
# Create new instance directly without going through __init__
|
388
|
+
new_instance = object.__new__(type(self))
|
389
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
390
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
391
|
+
new_instance._validate()
|
392
|
+
return new_instance
|
311
393
|
|
312
394
|
def with_title(self, title: str) -> Self:
|
313
395
|
"""Add a title to this field.
|
@@ -319,8 +401,13 @@ class FieldModel:
|
|
319
401
|
New FieldModel with title
|
320
402
|
"""
|
321
403
|
filtered_metadata = tuple(m for m in self.metadata if m.key != "title")
|
322
|
-
new_metadata = (*filtered_metadata,
|
323
|
-
|
404
|
+
new_metadata = (*filtered_metadata, Meta("title", title))
|
405
|
+
# Create new instance directly without going through __init__
|
406
|
+
new_instance = object.__new__(type(self))
|
407
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
408
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
409
|
+
new_instance._validate()
|
410
|
+
return new_instance
|
324
411
|
|
325
412
|
def with_exclude(self, exclude: bool = True) -> Self:
|
326
413
|
"""Mark this field to be excluded from serialization.
|
@@ -331,11 +418,19 @@ class FieldModel:
|
|
331
418
|
Returns:
|
332
419
|
New FieldModel with exclude setting
|
333
420
|
"""
|
421
|
+
current_metadata = (
|
422
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
423
|
+
)
|
334
424
|
filtered_metadata = tuple(
|
335
|
-
m for m in
|
425
|
+
m for m in current_metadata if m.key != "exclude"
|
336
426
|
)
|
337
|
-
new_metadata = (*filtered_metadata,
|
338
|
-
|
427
|
+
new_metadata = (*filtered_metadata, Meta("exclude", exclude))
|
428
|
+
# Create new instance directly without going through __init__
|
429
|
+
new_instance = object.__new__(type(self))
|
430
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
431
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
432
|
+
new_instance._validate()
|
433
|
+
return new_instance
|
339
434
|
|
340
435
|
def with_metadata(self, key: str, value: Any) -> Self:
|
341
436
|
"""Add custom metadata to this field.
|
@@ -349,8 +444,13 @@ class FieldModel:
|
|
349
444
|
"""
|
350
445
|
# Replace existing metadata with same key
|
351
446
|
filtered_metadata = tuple(m for m in self.metadata if m.key != key)
|
352
|
-
new_metadata = (*filtered_metadata,
|
353
|
-
|
447
|
+
new_metadata = (*filtered_metadata, Meta(key, value))
|
448
|
+
# Create new instance directly without going through __init__
|
449
|
+
new_instance = object.__new__(type(self))
|
450
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
451
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
452
|
+
new_instance._validate()
|
453
|
+
return new_instance
|
354
454
|
|
355
455
|
def with_json_schema_extra(self, **kwargs: Any) -> Self:
|
356
456
|
"""Add JSON schema extra information.
|
@@ -365,14 +465,22 @@ class FieldModel:
|
|
365
465
|
existing = self.extract_metadata("json_schema_extra") or {}
|
366
466
|
updated = {**existing, **kwargs}
|
367
467
|
|
468
|
+
current_metadata = (
|
469
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
470
|
+
)
|
368
471
|
filtered_metadata = tuple(
|
369
|
-
m for m in
|
472
|
+
m for m in current_metadata if m.key != "json_schema_extra"
|
370
473
|
)
|
371
474
|
new_metadata = (
|
372
475
|
*filtered_metadata,
|
373
|
-
|
476
|
+
Meta("json_schema_extra", updated),
|
374
477
|
)
|
375
|
-
|
478
|
+
# Create new instance directly without going through __init__
|
479
|
+
new_instance = object.__new__(type(self))
|
480
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
481
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
482
|
+
new_instance._validate()
|
483
|
+
return new_instance
|
376
484
|
|
377
485
|
def create_field(self) -> Any:
|
378
486
|
"""Create a Pydantic FieldInfo object from this template.
|
@@ -388,27 +496,28 @@ class FieldModel:
|
|
388
496
|
# Extract metadata for FieldInfo
|
389
497
|
field_kwargs = {}
|
390
498
|
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
499
|
+
if not self._is_sentinel(self.metadata):
|
500
|
+
for meta in self.metadata:
|
501
|
+
if meta.key == "default":
|
502
|
+
# Handle callable defaults as default_factory
|
503
|
+
if callable(meta.value):
|
504
|
+
field_kwargs["default_factory"] = meta.value
|
505
|
+
else:
|
506
|
+
field_kwargs["default"] = meta.value
|
507
|
+
elif meta.key == "validator":
|
508
|
+
# Validators are handled separately in create_model
|
509
|
+
continue
|
510
|
+
elif meta.key in pydantic_field_params:
|
511
|
+
# Pass through standard Pydantic field attributes
|
512
|
+
field_kwargs[meta.key] = meta.value
|
513
|
+
elif meta.key in {"nullable", "listable"}:
|
514
|
+
# These are FieldTemplate markers, don't pass to FieldInfo
|
515
|
+
pass
|
396
516
|
else:
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
elif meta.key in pydantic_field_params:
|
402
|
-
# Pass through standard Pydantic field attributes
|
403
|
-
field_kwargs[meta.key] = meta.value
|
404
|
-
elif meta.key in {"nullable", "listable"}:
|
405
|
-
# These are FieldTemplate markers, don't pass to FieldInfo
|
406
|
-
pass
|
407
|
-
else:
|
408
|
-
# Any other metadata goes in json_schema_extra
|
409
|
-
if "json_schema_extra" not in field_kwargs:
|
410
|
-
field_kwargs["json_schema_extra"] = {}
|
411
|
-
field_kwargs["json_schema_extra"][meta.key] = meta.value
|
517
|
+
# Any other metadata goes in json_schema_extra
|
518
|
+
if "json_schema_extra" not in field_kwargs:
|
519
|
+
field_kwargs["json_schema_extra"] = {}
|
520
|
+
field_kwargs["json_schema_extra"][meta.key] = meta.value
|
412
521
|
|
413
522
|
# Handle nullable case - ensure default is set if not already
|
414
523
|
if (
|
@@ -447,15 +556,21 @@ class FieldModel:
|
|
447
556
|
return _annotated_cache[cache_key]
|
448
557
|
|
449
558
|
# Handle nullable case - wrap in Optional-like union
|
450
|
-
actual_type =
|
451
|
-
|
559
|
+
actual_type = (
|
560
|
+
Any if self._is_sentinel(self.base_type) else self.base_type
|
561
|
+
)
|
562
|
+
current_metadata = (
|
563
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
564
|
+
)
|
565
|
+
|
566
|
+
if any(m.key == "nullable" and m.value for m in current_metadata):
|
452
567
|
# Use union syntax for nullable
|
453
568
|
actual_type = actual_type | None # type: ignore
|
454
569
|
|
455
|
-
if
|
570
|
+
if current_metadata:
|
456
571
|
# Python 3.10 doesn't support unpacking in Annotated, so we need to build it differently
|
457
572
|
# We'll use Annotated.__class_getitem__ to build the type dynamically
|
458
|
-
args = [actual_type] + list(
|
573
|
+
args = [actual_type] + list(current_metadata)
|
459
574
|
result = Annotated.__class_getitem__(tuple(args)) # type: ignore
|
460
575
|
else:
|
461
576
|
result = actual_type # type: ignore[misc]
|
@@ -482,9 +597,10 @@ class FieldModel:
|
|
482
597
|
Returns:
|
483
598
|
Metadata value if found, None otherwise
|
484
599
|
"""
|
485
|
-
|
486
|
-
|
487
|
-
|
600
|
+
if not self._is_sentinel(self.metadata):
|
601
|
+
for m in self.metadata:
|
602
|
+
if m.key == key:
|
603
|
+
return m.value
|
488
604
|
return None
|
489
605
|
|
490
606
|
def has_validator(self) -> bool:
|
@@ -493,6 +609,8 @@ class FieldModel:
|
|
493
609
|
Returns:
|
494
610
|
True if validator exists in metadata
|
495
611
|
"""
|
612
|
+
if self._is_sentinel(self.metadata):
|
613
|
+
return False
|
496
614
|
return any(m.key == "validator" for m in self.metadata)
|
497
615
|
|
498
616
|
def is_valid(self, value: Any) -> bool:
|
@@ -504,6 +622,8 @@ class FieldModel:
|
|
504
622
|
Returns:
|
505
623
|
True if all validators pass, False otherwise
|
506
624
|
"""
|
625
|
+
if self._is_sentinel(self.metadata):
|
626
|
+
return True
|
507
627
|
for m in self.metadata:
|
508
628
|
if m.key == "validator":
|
509
629
|
validator = m.value
|
@@ -525,41 +645,46 @@ class FieldModel:
|
|
525
645
|
if not self.has_validator():
|
526
646
|
return
|
527
647
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
648
|
+
if not self._is_sentinel(self.metadata):
|
649
|
+
for i, m in enumerate(self.metadata):
|
650
|
+
if m.key == "validator":
|
651
|
+
validator = m.value
|
652
|
+
# Try to call validator with correct signature
|
653
|
+
try:
|
654
|
+
# Try Pydantic-style validator (cls, value) - pass None for cls
|
655
|
+
result = validator(None, value)
|
656
|
+
# For Pydantic validators that return the value or raise exceptions,
|
657
|
+
# if we get here without exception, validation passed
|
658
|
+
except TypeError:
|
659
|
+
# Try simple validator that just takes value and returns boolean
|
660
|
+
result = validator(value)
|
661
|
+
# If validator returns False (simple boolean validator), raise error
|
662
|
+
if result is False:
|
663
|
+
validator_name = getattr(
|
664
|
+
validator, "__name__", f"validator_{i}"
|
665
|
+
)
|
666
|
+
raise ValidationError(
|
667
|
+
f"Validation failed for {validator_name}",
|
668
|
+
field_name=field_name,
|
669
|
+
value=value,
|
670
|
+
validator_name=validator_name,
|
671
|
+
)
|
672
|
+
except Exception:
|
673
|
+
# If validator raises any other exception, let it propagate
|
674
|
+
raise
|
554
675
|
|
555
676
|
@property
|
556
677
|
def is_nullable(self) -> bool:
|
557
678
|
"""Check if this field allows None values."""
|
679
|
+
if self._is_sentinel(self.metadata):
|
680
|
+
return False
|
558
681
|
return any(m.key == "nullable" and m.value for m in self.metadata)
|
559
682
|
|
560
683
|
@property
|
561
684
|
def is_listable(self) -> bool:
|
562
685
|
"""Check if this field is a list type."""
|
686
|
+
if self._is_sentinel(self.metadata):
|
687
|
+
return False
|
563
688
|
return any(m.key == "listable" and m.value for m in self.metadata)
|
564
689
|
|
565
690
|
@override
|
@@ -574,112 +699,94 @@ class FieldModel:
|
|
574
699
|
attrs.append("validated")
|
575
700
|
|
576
701
|
attr_str = f" [{', '.join(attrs)}]" if attrs else ""
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
"""Get field name from metadata for backward compatibility."""
|
584
|
-
return self.extract_metadata("name") or "field"
|
585
|
-
|
586
|
-
|
587
|
-
@property
|
588
|
-
def default(self) -> Any:
|
589
|
-
"""Get field default value from metadata for backward compatibility."""
|
590
|
-
return self.extract_metadata("default")
|
591
|
-
|
592
|
-
|
593
|
-
@property
|
594
|
-
def title(self) -> str | None:
|
595
|
-
"""Get field title from metadata for backward compatibility."""
|
596
|
-
return self.extract_metadata("title")
|
597
|
-
|
598
|
-
|
599
|
-
@property
|
600
|
-
def description(self) -> str | None:
|
601
|
-
"""Get field description from metadata for backward compatibility."""
|
602
|
-
return self.extract_metadata("description")
|
603
|
-
|
604
|
-
|
605
|
-
@property
|
606
|
-
def annotation(self) -> type[Any]:
|
607
|
-
"""Get field annotation (base_type) for backward compatibility."""
|
608
|
-
return self.base_type
|
702
|
+
base_type_name = (
|
703
|
+
"Any"
|
704
|
+
if self._is_sentinel(self.base_type)
|
705
|
+
else self.base_type.__name__
|
706
|
+
)
|
707
|
+
return f"FieldModel({base_type_name}{attr_str})"
|
609
708
|
|
709
|
+
@property
|
710
|
+
def field_validator(self) -> dict[str, Any] | None:
|
711
|
+
"""Create field validator configuration for backward compatibility.
|
610
712
|
|
611
|
-
|
612
|
-
|
613
|
-
|
713
|
+
Returns:
|
714
|
+
Dictionary mapping validator name to validator function if defined,
|
715
|
+
None otherwise.
|
716
|
+
"""
|
717
|
+
if not self.has_validator():
|
718
|
+
return None
|
614
719
|
|
615
|
-
|
720
|
+
# Extract validators and create field_validator config
|
721
|
+
from pydantic import field_validator
|
616
722
|
|
617
|
-
|
618
|
-
Configured Pydantic FieldInfo object.
|
619
|
-
"""
|
620
|
-
return self.create_field()
|
723
|
+
validators = {}
|
621
724
|
|
725
|
+
# Get field name from metadata or use default
|
726
|
+
field_name = self.extract_metadata("name") or "field"
|
622
727
|
|
623
|
-
|
624
|
-
|
625
|
-
|
728
|
+
if not self._is_sentinel(self.metadata):
|
729
|
+
for meta in self.metadata:
|
730
|
+
if meta.key == "validator":
|
731
|
+
validator_name = f"{field_name}_validator"
|
732
|
+
validators[validator_name] = field_validator(field_name)(
|
733
|
+
meta.value
|
734
|
+
)
|
626
735
|
|
627
|
-
|
628
|
-
Dictionary mapping validator name to validator function if defined,
|
629
|
-
None otherwise.
|
630
|
-
"""
|
631
|
-
if not self.has_validator():
|
632
|
-
return None
|
736
|
+
return validators if validators else None
|
633
737
|
|
634
|
-
|
635
|
-
|
738
|
+
@property
|
739
|
+
def annotation(self) -> type[Any]:
|
740
|
+
"""Get field annotation (base_type) for backward compatibility."""
|
741
|
+
return Any if self._is_sentinel(self.base_type) else self.base_type
|
636
742
|
|
637
|
-
|
743
|
+
def to_dict(self) -> dict[str, Any]:
|
744
|
+
"""Convert field model to dictionary for backward compatibility.
|
638
745
|
|
639
|
-
|
640
|
-
field_name = self.extract_metadata("name") or "field"
|
746
|
+
DEPRECATED: Use metadata_dict() instead.
|
641
747
|
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
meta.value
|
647
|
-
)
|
748
|
+
Returns:
|
749
|
+
Dictionary representation of field configuration.
|
750
|
+
"""
|
751
|
+
import warnings
|
648
752
|
|
649
|
-
|
753
|
+
warnings.warn(
|
754
|
+
"FieldModel.to_dict() is deprecated. Use metadata_dict() instead.",
|
755
|
+
DeprecationWarning,
|
756
|
+
stacklevel=2,
|
757
|
+
)
|
758
|
+
return self.metadata_dict(
|
759
|
+
exclude=[
|
760
|
+
"nullable",
|
761
|
+
"listable",
|
762
|
+
"validator",
|
763
|
+
"name",
|
764
|
+
"validator_kwargs",
|
765
|
+
"annotation",
|
766
|
+
]
|
767
|
+
)
|
650
768
|
|
769
|
+
def metadata_dict(
|
770
|
+
self, exclude: list[str] | None = None
|
771
|
+
) -> dict[str, Any]:
|
772
|
+
"""Convert all metadata to dictionary with optional exclusions.
|
651
773
|
|
652
|
-
|
653
|
-
|
774
|
+
Args:
|
775
|
+
exclude: List of metadata keys to exclude from the result
|
654
776
|
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
# Convert metadata to dictionary
|
661
|
-
for meta in self.metadata:
|
662
|
-
if meta.key not in (
|
663
|
-
"nullable",
|
664
|
-
"listable",
|
665
|
-
"validator",
|
666
|
-
"name",
|
667
|
-
"validator_kwargs",
|
668
|
-
"annotation",
|
669
|
-
):
|
670
|
-
result[meta.key] = meta.value
|
777
|
+
Returns:
|
778
|
+
Dictionary mapping metadata keys to their values
|
779
|
+
"""
|
780
|
+
result = {}
|
781
|
+
exclude_set = set(exclude or [])
|
671
782
|
|
672
|
-
|
783
|
+
# Convert metadata to dictionary
|
784
|
+
if not self._is_sentinel(self.metadata):
|
785
|
+
for meta in self.metadata:
|
786
|
+
if meta.key not in exclude_set:
|
787
|
+
result[meta.key] = meta.value
|
673
788
|
|
789
|
+
return result
|
674
790
|
|
675
|
-
# Monkey patch the methods onto FieldModel for backward compatibility
|
676
|
-
FieldModel.name = name
|
677
|
-
FieldModel.default = default
|
678
|
-
FieldModel.title = title
|
679
|
-
FieldModel.description = description
|
680
|
-
FieldModel.annotation = annotation
|
681
|
-
FieldModel.field_info = field_info
|
682
|
-
FieldModel.field_validator = field_validator
|
683
|
-
FieldModel.to_dict = to_dict
|
684
791
|
|
685
|
-
__all__ = ("FieldModel",
|
792
|
+
__all__ = ("FieldModel",)
|