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