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.
@@ -1,6 +1,6 @@
1
1
  """Field model implementation for compositional field definitions.
2
2
 
3
- This module provides FieldModel, a frozen dataclass that enables
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, field
14
- from typing import Annotated, Any
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[FieldMeta, ...]], type] = (
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[FieldMeta, ...] = field(default_factory=tuple)
117
-
118
- def __init__(
119
- self,
120
- base_type: type[Any] = None,
121
- metadata: tuple[FieldMeta, ...] | None = None,
122
- name: str = "field",
123
- annotation: type[Any] = None,
124
- **kwargs: Any,
125
- ) -> None:
126
- """Initialize FieldModel with optional metadata and kwargs.
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
- base_type: The base Python type for this field (or use annotation)
130
- metadata: Tuple of metadata to attach via Annotated
131
- name: Field name for backward compatibility
132
- annotation: Type annotation (alias for base_type for backward compatibility)
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
- # Handle backward compatibility: use annotation if base_type not provided
136
- if base_type is None and annotation is not None:
137
- base_type = annotation
138
- elif base_type is None:
139
- base_type = Any
140
-
141
- # Convert kwargs to FieldMeta objects
142
- meta_list = list(metadata) if metadata else []
143
-
144
- # Handle special kwargs that trigger method calls
145
- if "nullable" in kwargs and kwargs["nullable"]:
146
- # Add nullable marker
147
- meta_list.append(FieldMeta("nullable", True))
148
- kwargs.pop("nullable")
149
- if "listable" in kwargs and kwargs["listable"]:
150
- # Change base type to list
151
- base_type = list[base_type] # type: ignore
152
- meta_list.append(FieldMeta("listable", True))
153
- kwargs.pop("listable")
154
-
155
- # Add name as metadata if provided
156
- if name != "field": # Only add if non-default
157
- meta_list.append(FieldMeta("name", name))
158
-
159
- # Validate for conflicting defaults
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 FieldMeta
175
- for key, value in kwargs.items():
176
- meta_list.append(FieldMeta(key, value))
177
-
178
- # Use object.__setattr__ to set frozen dataclass fields
179
- object.__setattr__(self, "base_type", base_type)
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
- # Manually call __post_init__ since we have a custom __init__
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
- for meta in self.metadata:
189
- if meta.key == name:
190
- return meta.value
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
- new_metadata = (*self.metadata, FieldMeta("nullable", True))
218
- return type(self)(self.base_type, new_metadata)
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[self.base_type] # type: ignore
266
+ new_base = list[current_base] # type: ignore
231
267
  # Add listable marker to metadata
232
- new_metadata = (*self.metadata, FieldMeta("listable", True))
233
- return type(self)(new_base, new_metadata)
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
- new_metadata = (*self.metadata, FieldMeta("validator", f))
246
- return type(self)(self.base_type, new_metadata)
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 self.metadata if m.key != "description"
314
+ m for m in current_metadata if m.key != "description"
260
315
  )
261
316
  new_metadata = (
262
317
  *filtered_metadata,
263
- FieldMeta("description", description),
318
+ Meta("description", description),
264
319
  )
265
- return type(self)(self.base_type, new_metadata)
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 self.metadata if m.key != "default"
342
+ m for m in current_metadata if m.key != "default"
279
343
  )
280
- new_metadata = (*filtered_metadata, FieldMeta("default", default))
281
- return type(self)(self.base_type, new_metadata)
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 self.metadata if m.key != "frozen"
366
+ m for m in current_metadata if m.key != "frozen"
295
367
  )
296
- new_metadata = (*filtered_metadata, FieldMeta("frozen", frozen))
297
- return type(self)(self.base_type, new_metadata)
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, FieldMeta("alias", alias))
310
- return type(self)(self.base_type, new_metadata)
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, FieldMeta("title", title))
323
- return type(self)(self.base_type, new_metadata)
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 self.metadata if m.key != "exclude"
425
+ m for m in current_metadata if m.key != "exclude"
336
426
  )
337
- new_metadata = (*filtered_metadata, FieldMeta("exclude", exclude))
338
- return type(self)(self.base_type, new_metadata)
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, FieldMeta(key, value))
353
- return type(self)(self.base_type, new_metadata)
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 self.metadata if m.key != "json_schema_extra"
472
+ m for m in current_metadata if m.key != "json_schema_extra"
370
473
  )
371
474
  new_metadata = (
372
475
  *filtered_metadata,
373
- FieldMeta("json_schema_extra", updated),
476
+ Meta("json_schema_extra", updated),
374
477
  )
375
- return type(self)(self.base_type, new_metadata)
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
- for meta in self.metadata:
392
- if meta.key == "default":
393
- # Handle callable defaults as default_factory
394
- if callable(meta.value):
395
- field_kwargs["default_factory"] = meta.value
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
- field_kwargs["default"] = meta.value
398
- elif meta.key == "validator":
399
- # Validators are handled separately in create_model
400
- continue
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 = self.base_type
451
- if any(m.key == "nullable" and m.value for m in self.metadata):
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 self.metadata:
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(self.metadata)
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
- for m in self.metadata:
486
- if m.key == key:
487
- return m.value
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
- for i, m in enumerate(self.metadata):
529
- if m.key == "validator":
530
- validator = m.value
531
- # Try to call validator with correct signature
532
- try:
533
- # Try Pydantic-style validator (cls, value) - pass None for cls
534
- result = validator(None, value)
535
- # For Pydantic validators that return the value or raise exceptions,
536
- # if we get here without exception, validation passed
537
- except TypeError:
538
- # Try simple validator that just takes value and returns boolean
539
- result = validator(value)
540
- # If validator returns False (simple boolean validator), raise error
541
- if result is False:
542
- validator_name = getattr(
543
- validator, "__name__", f"validator_{i}"
544
- )
545
- raise ValidationError(
546
- f"Validation failed for {validator_name}",
547
- field_name=field_name,
548
- value=value,
549
- validator_name=validator_name,
550
- )
551
- except Exception:
552
- # If validator raises any other exception, let it propagate
553
- raise
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
- return f"FieldModel({self.base_type.__name__}{attr_str})"
578
-
579
-
580
- # Add backward compatibility properties and methods
581
- @property
582
- def name(self) -> str:
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
- @property
612
- def field_info(self) -> Any:
613
- """Generate Pydantic FieldInfo from current configuration for backward compatibility.
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
- This property provides compatibility with the old FieldModel API.
720
+ # Extract validators and create field_validator config
721
+ from pydantic import field_validator
616
722
 
617
- Returns:
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
- @property
624
- def field_validator(self) -> dict[str, Any] | None:
625
- """Create field validator configuration for backward compatibility.
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
- Returns:
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
- # Extract validators and create field_validator config
635
- from pydantic import field_validator
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
- validators = {}
743
+ def to_dict(self) -> dict[str, Any]:
744
+ """Convert field model to dictionary for backward compatibility.
638
745
 
639
- # Get field name from metadata or use default
640
- field_name = self.extract_metadata("name") or "field"
746
+ DEPRECATED: Use metadata_dict() instead.
641
747
 
642
- for meta in self.metadata:
643
- if meta.key == "validator":
644
- validator_name = f"{field_name}_validator"
645
- validators[validator_name] = field_validator(field_name)(
646
- meta.value
647
- )
748
+ Returns:
749
+ Dictionary representation of field configuration.
750
+ """
751
+ import warnings
648
752
 
649
- return validators if validators else None
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
- def to_dict(self) -> dict[str, Any]:
653
- """Convert field model to dictionary for backward compatibility.
774
+ Args:
775
+ exclude: List of metadata keys to exclude from the result
654
776
 
655
- Returns:
656
- Dictionary representation of field configuration.
657
- """
658
- result = {}
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
- return result
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", "FieldMeta")
792
+ __all__ = ("FieldModel",)