lionagi 0.18.1__py3-none-any.whl → 0.18.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Final, Literal, TypeVar, Union
4
+
5
+ __all__ = (
6
+ "Undefined",
7
+ "Unset",
8
+ "MaybeUndefined",
9
+ "MaybeUnset",
10
+ "MaybeSentinel",
11
+ "SingletonType",
12
+ "UndefinedType",
13
+ "UnsetType",
14
+ "is_sentinel",
15
+ "not_sentinel",
16
+ "T",
17
+ )
18
+
19
+ T = TypeVar("T")
20
+
21
+
22
+ class _SingletonMeta(type):
23
+ """Metaclass that guarantees exactly one instance per subclass.
24
+
25
+ This ensures that sentinel values maintain identity across the entire application,
26
+ allowing safe identity checks with 'is' operator.
27
+ """
28
+
29
+ _cache: dict[type, SingletonType] = {}
30
+
31
+ def __call__(cls, *a, **kw):
32
+ if cls not in cls._cache:
33
+ cls._cache[cls] = super().__call__(*a, **kw)
34
+ return cls._cache[cls]
35
+
36
+
37
+ class SingletonType(metaclass=_SingletonMeta):
38
+ """Base class for singleton sentinel types.
39
+
40
+ Provides consistent interface for sentinel values with:
41
+ - Identity preservation across deepcopy
42
+ - Falsy boolean evaluation
43
+ - Clear string representation
44
+ """
45
+
46
+ __slots__: tuple[str, ...] = ()
47
+
48
+ def __deepcopy__(self, memo): # copy & deepcopy both noop
49
+ return self
50
+
51
+ def __copy__(self):
52
+ return self
53
+
54
+ # concrete classes *must* override the two methods below
55
+ def __bool__(self) -> bool: ...
56
+ def __repr__(self) -> str: ...
57
+
58
+
59
+ class UndefinedType(SingletonType):
60
+ """Sentinel for a key or field entirely missing from a namespace.
61
+
62
+ Use this when:
63
+ - A field has never been set
64
+ - A key doesn't exist in a mapping
65
+ - A value is conceptually undefined (not just unset)
66
+
67
+ Example:
68
+ >>> d = {"a": 1}
69
+ >>> d.get("b", Undefined) is Undefined
70
+ True
71
+ """
72
+
73
+ __slots__ = ()
74
+
75
+ def __bool__(self) -> Literal[False]:
76
+ return False
77
+
78
+ def __repr__(self) -> Literal["Undefined"]:
79
+ return "Undefined"
80
+
81
+ def __str__(self) -> Literal["Undefined"]:
82
+ return "Undefined"
83
+
84
+ def __reduce__(self):
85
+ """Ensure pickle preservation of singleton identity."""
86
+ return "Undefined"
87
+
88
+
89
+ class UnsetType(SingletonType):
90
+ """Sentinel for a key present but value not yet provided.
91
+
92
+ Use this when:
93
+ - A parameter exists but hasn't been given a value
94
+ - Distinguishing between None and "not provided"
95
+ - API parameters that are optional but need explicit handling
96
+
97
+ Example:
98
+ >>> def func(param=Unset):
99
+ ... if param is not Unset:
100
+ ... # param was explicitly provided
101
+ ... process(param)
102
+ """
103
+
104
+ __slots__ = ()
105
+
106
+ def __bool__(self) -> Literal[False]:
107
+ return False
108
+
109
+ def __repr__(self) -> Literal["Unset"]:
110
+ return "Unset"
111
+
112
+ def __str__(self) -> Literal["Unset"]:
113
+ return "Unset"
114
+
115
+ def __reduce__(self):
116
+ """Ensure pickle preservation of singleton identity."""
117
+ return "Unset"
118
+
119
+
120
+ Undefined: Final = UndefinedType()
121
+ """A key or field entirely missing from a namespace"""
122
+ Unset: Final = UnsetType()
123
+ """A key present but value not yet provided."""
124
+
125
+ MaybeUndefined = Union[T, UndefinedType]
126
+ MaybeUnset = Union[T, UnsetType]
127
+ MaybeSentinel = Union[T, UndefinedType, UnsetType]
128
+
129
+ _EMPTY_TUPLE = (tuple(), set(), frozenset(), dict(), list(), "")
130
+
131
+
132
+ def is_sentinel(
133
+ value: Any,
134
+ *,
135
+ none_as_sentinel: bool = False,
136
+ empty_as_sentinel: bool = False,
137
+ ) -> bool:
138
+ """Check if a value is any sentinel (Undefined or Unset)."""
139
+ if none_as_sentinel and value is None:
140
+ return True
141
+ if empty_as_sentinel and value in _EMPTY_TUPLE:
142
+ return True
143
+ return value is Undefined or value is Unset
144
+
145
+
146
+ def not_sentinel(
147
+ value: Any, none_as_sentinel: bool = False, empty_as_sentinel: bool = False
148
+ ) -> bool:
149
+ """Check if a value is NOT a sentinel. Useful for filtering operations."""
150
+ return not is_sentinel(
151
+ value,
152
+ none_as_sentinel=none_as_sentinel,
153
+ empty_as_sentinel=empty_as_sentinel,
154
+ )
@@ -3,152 +3,26 @@ from __future__ import annotations
3
3
  from collections.abc import Sequence
4
4
  from dataclasses import dataclass, field
5
5
  from enum import Enum as _Enum
6
- from typing import Any, ClassVar, Final, Literal, TypeVar, Union
6
+ from typing import Any, ClassVar
7
7
 
8
8
  from typing_extensions import TypedDict, override
9
9
 
10
+ from ._sentinel import Undefined, Unset, is_sentinel
11
+
10
12
  __all__ = (
11
- "Undefined",
12
- "Unset",
13
- "MaybeUndefined",
14
- "MaybeUnset",
15
- "MaybeSentinel",
16
- "SingletonType",
17
- "UndefinedType",
18
- "UnsetType",
19
- "KeysDict",
20
- "T",
21
13
  "Enum",
22
- "is_sentinel",
23
- "not_sentinel",
14
+ "ModelConfig",
24
15
  "Params",
25
16
  "DataClass",
26
- "KeysLike",
27
17
  "Meta",
18
+ "KeysDict",
19
+ "KeysLike",
28
20
  )
29
21
 
30
- T = TypeVar("T")
31
-
32
-
33
- class _SingletonMeta(type):
34
- """Metaclass that guarantees exactly one instance per subclass.
35
-
36
- This ensures that sentinel values maintain identity across the entire application,
37
- allowing safe identity checks with 'is' operator.
38
- """
39
-
40
- _cache: dict[type, SingletonType] = {}
41
-
42
- def __call__(cls, *a, **kw):
43
- if cls not in cls._cache:
44
- cls._cache[cls] = super().__call__(*a, **kw)
45
- return cls._cache[cls]
46
-
47
-
48
- class SingletonType(metaclass=_SingletonMeta):
49
- """Base class for singleton sentinel types.
50
-
51
- Provides consistent interface for sentinel values with:
52
- - Identity preservation across deepcopy
53
- - Falsy boolean evaluation
54
- - Clear string representation
55
- """
56
-
57
- __slots__: tuple[str, ...] = ()
58
-
59
- def __deepcopy__(self, memo): # copy & deepcopy both noop
60
- return self
61
-
62
- def __copy__(self):
63
- return self
64
-
65
- # concrete classes *must* override the two methods below
66
- def __bool__(self) -> bool: ...
67
- def __repr__(self) -> str: ...
68
-
69
-
70
- class UndefinedType(SingletonType):
71
- """Sentinel for a key or field entirely missing from a namespace.
72
-
73
- Use this when:
74
- - A field has never been set
75
- - A key doesn't exist in a mapping
76
- - A value is conceptually undefined (not just unset)
77
-
78
- Example:
79
- >>> d = {"a": 1}
80
- >>> d.get("b", Undefined) is Undefined
81
- True
82
- """
83
-
84
- __slots__ = ()
85
-
86
- def __bool__(self) -> Literal[False]:
87
- return False
88
-
89
- def __repr__(self) -> Literal["Undefined"]:
90
- return "Undefined"
91
-
92
- def __str__(self) -> Literal["Undefined"]:
93
- return "Undefined"
94
-
95
- def __reduce__(self):
96
- """Ensure pickle preservation of singleton identity."""
97
- return "Undefined"
98
-
99
-
100
- class UnsetType(SingletonType):
101
- """Sentinel for a key present but value not yet provided.
102
-
103
- Use this when:
104
- - A parameter exists but hasn't been given a value
105
- - Distinguishing between None and "not provided"
106
- - API parameters that are optional but need explicit handling
107
-
108
- Example:
109
- >>> def func(param=Unset):
110
- ... if param is not Unset:
111
- ... # param was explicitly provided
112
- ... process(param)
113
- """
114
-
115
- __slots__ = ()
116
-
117
- def __bool__(self) -> Literal[False]:
118
- return False
119
-
120
- def __repr__(self) -> Literal["Unset"]:
121
- return "Unset"
122
-
123
- def __str__(self) -> Literal["Unset"]:
124
- return "Unset"
125
-
126
- def __reduce__(self):
127
- """Ensure pickle preservation of singleton identity."""
128
- return "Unset"
129
-
130
-
131
- Undefined: Final = UndefinedType()
132
- """A key or field entirely missing from a namespace"""
133
- Unset: Final = UnsetType()
134
- """A key present but value not yet provided."""
135
-
136
- MaybeUndefined = Union[T, UndefinedType]
137
- MaybeUnset = Union[T, UnsetType]
138
- MaybeSentinel = Union[T, UndefinedType, UnsetType]
139
-
140
-
141
- def is_sentinel(value: Any) -> bool:
142
- """Check if a value is any sentinel (Undefined or Unset)."""
143
- return value is Undefined or value is Unset
144
-
145
-
146
- def not_sentinel(value: Any) -> bool:
147
- """Check if a value is NOT a sentinel. Useful for filtering operations."""
148
- return value is not Undefined and value is not Unset
149
-
150
22
 
151
23
  class Enum(_Enum):
24
+ """Enhanced Enum with allowed() classmethod."""
25
+
152
26
  @classmethod
153
27
  def allowed(cls) -> tuple[str, ...]:
154
28
  return tuple(e.value for e in cls)
@@ -160,18 +34,46 @@ class KeysDict(TypedDict, total=False):
160
34
  key: Any # Represents any key-type pair
161
35
 
162
36
 
37
+ @dataclass(slots=True, frozen=True)
38
+ class ModelConfig:
39
+ """Configuration for Params and DataClass behavior.
40
+
41
+ Attributes:
42
+ none_as_sentinel: If True, None is treated as a sentinel value (excluded from to_dict).
43
+ empty_as_sentinel: If True, empty collections are treated as sentinels (excluded from to_dict).
44
+ strict: If True, no sentinels allowed (all fields must have values).
45
+ prefill_unset: If True, unset fields are prefilled with Unset.
46
+ use_enum_values: If True, use enum values instead of enum instances in to_dict().
47
+ """
48
+
49
+ # Sentinel handling (controls what gets excluded from to_dict)
50
+ none_as_sentinel: bool = False
51
+ empty_as_sentinel: bool = False
52
+
53
+ # Validation
54
+ strict: bool = False
55
+ prefill_unset: bool = True
56
+
57
+ # Serialization
58
+ use_enum_values: bool = False
59
+
60
+
163
61
  @dataclass(slots=True, frozen=True, init=False)
164
62
  class Params:
165
- """Base class for parameters used in various functions."""
63
+ """Base class for parameters used in various functions.
166
64
 
167
- _none_as_sentinel: ClassVar[bool] = False
168
- """If True, None is treated as a sentinel value."""
65
+ Use the ModelConfig class attribute to customize behavior:
169
66
 
170
- _strict: ClassVar[bool] = False
171
- """No sentinels allowed if strict is True."""
67
+ Example:
68
+ @dataclass(slots=True, frozen=True, init=False)
69
+ class MyParams(Params):
70
+ _config: ClassVar[ModelConfig] = ModelConfig(strict=True)
71
+ param1: str
72
+ param2: int
73
+ """
172
74
 
173
- _prefill_unset: ClassVar[bool] = True
174
- """If True, unset fields are prefilled with Unset."""
75
+ _config: ClassVar[ModelConfig] = ModelConfig()
76
+ """Configuration for this Params class."""
175
77
 
176
78
  _allowed_keys: ClassVar[set[str]] = field(
177
79
  default=set(), init=False, repr=False
@@ -193,9 +95,23 @@ class Params:
193
95
  @classmethod
194
96
  def _is_sentinel(cls, value: Any) -> bool:
195
97
  """Check if a value is a sentinel (Undefined or Unset)."""
196
- if value is None and cls._none_as_sentinel:
197
- return True
198
- return is_sentinel(value)
98
+ return is_sentinel(
99
+ value,
100
+ none_as_sentinel=cls._config.none_as_sentinel,
101
+ empty_as_sentinel=cls._config.empty_as_sentinel,
102
+ )
103
+
104
+ @classmethod
105
+ def _normalize_value(cls, value: Any) -> Any:
106
+ """Normalize a value for serialization.
107
+
108
+ Handles:
109
+ - Enum values if use_enum_values is True
110
+ - Can be extended for other transformations
111
+ """
112
+ if cls._config.use_enum_values and isinstance(value, _Enum):
113
+ return value.value
114
+ return value
199
115
 
200
116
  @classmethod
201
117
  def allowed(cls) -> set[str]:
@@ -210,10 +126,12 @@ class Params:
210
126
  @override
211
127
  def _validate(self) -> None:
212
128
  def _validate_strict(k):
213
- if self._strict and self._is_sentinel(getattr(self, k, Unset)):
129
+ if self._config.strict and self._is_sentinel(
130
+ getattr(self, k, Unset)
131
+ ):
214
132
  raise ValueError(f"Missing required parameter: {k}")
215
133
  if (
216
- self._prefill_unset
134
+ self._config.prefill_unset
217
135
  and getattr(self, k, Undefined) is Undefined
218
136
  ):
219
137
  object.__setattr__(self, k, Unset)
@@ -236,14 +154,14 @@ class Params:
236
154
  data = {}
237
155
  exclude = exclude or set()
238
156
  for k in self.allowed():
239
- if k not in exclude and not self._is_sentinel(
240
- v := getattr(self, k, Undefined)
241
- ):
242
- data[k] = v
157
+ if k not in exclude:
158
+ v = getattr(self, k, Undefined)
159
+ if not self._is_sentinel(v):
160
+ data[k] = self._normalize_value(v)
243
161
  return data
244
162
 
245
163
  def __hash__(self) -> int:
246
- from ._hash import hash_dict
164
+ from .._hash import hash_dict
247
165
 
248
166
  return hash_dict(self.to_dict())
249
167
 
@@ -261,16 +179,20 @@ class Params:
261
179
 
262
180
  @dataclass(slots=True)
263
181
  class DataClass:
264
- """A base class for data classes with strict parameter handling."""
182
+ """A base class for data classes with strict parameter handling.
265
183
 
266
- _none_as_sentinel: ClassVar[bool] = False
267
- """If True, None is treated as a sentinel value."""
184
+ Use the ModelConfig class attribute to customize behavior:
268
185
 
269
- _strict: ClassVar[bool] = False
270
- """No sentinels allowed if strict is True."""
186
+ Example:
187
+ @dataclass(slots=True)
188
+ class MyDataClass(DataClass):
189
+ _config: ClassVar[ModelConfig] = ModelConfig(strict=True, prefill_unset=False)
190
+ field1: str
191
+ field2: int
192
+ """
271
193
 
272
- _prefill_unset: ClassVar[bool] = True
273
- """If True, unset fields are prefilled with Unset."""
194
+ _config: ClassVar[ModelConfig] = ModelConfig()
195
+ """Configuration for this DataClass."""
274
196
 
275
197
  _allowed_keys: ClassVar[set[str]] = field(
276
198
  default=set(), init=False, repr=False
@@ -294,10 +216,12 @@ class DataClass:
294
216
  @override
295
217
  def _validate(self) -> None:
296
218
  def _validate_strict(k):
297
- if self._strict and self._is_sentinel(getattr(self, k, Unset)):
219
+ if self._config.strict and self._is_sentinel(
220
+ getattr(self, k, Unset)
221
+ ):
298
222
  raise ValueError(f"Missing required parameter: {k}")
299
223
  if (
300
- self._prefill_unset
224
+ self._config.prefill_unset
301
225
  and getattr(self, k, Undefined) is Undefined
302
226
  ):
303
227
  self.__setattr__(k, Unset)
@@ -309,18 +233,34 @@ class DataClass:
309
233
  data = {}
310
234
  exclude = exclude or set()
311
235
  for k in type(self).allowed():
312
- if k not in exclude and not self._is_sentinel(
313
- v := getattr(self, k)
314
- ):
315
- data[k] = v
236
+ if k not in exclude:
237
+ v = getattr(self, k)
238
+ if not self._is_sentinel(v):
239
+ data[k] = self._normalize_value(v)
316
240
  return data
317
241
 
318
242
  @classmethod
319
243
  def _is_sentinel(cls, value: Any) -> bool:
320
244
  """Check if a value is a sentinel (Undefined or Unset)."""
321
- if value is None and cls._none_as_sentinel:
322
- return True
323
- return is_sentinel(value)
245
+ return is_sentinel(
246
+ value,
247
+ none_as_sentinel=cls._config.none_as_sentinel,
248
+ empty_as_sentinel=cls._config.empty_as_sentinel,
249
+ )
250
+
251
+ @classmethod
252
+ def _normalize_value(cls, value: Any) -> Any:
253
+ """Normalize a value for serialization.
254
+
255
+ Handles:
256
+ - Enum values if use_enum_values is True
257
+ - Can be extended for other transformations
258
+ """
259
+ from enum import Enum as _Enum
260
+
261
+ if cls._config.use_enum_values and isinstance(value, _Enum):
262
+ return value.value
263
+ return value
324
264
 
325
265
  def with_updates(self, **kwargs: Any) -> DataClass:
326
266
  """Return a new instance with updated fields."""
@@ -329,7 +269,7 @@ class DataClass:
329
269
  return type(self)(**dict_)
330
270
 
331
271
  def __hash__(self) -> int:
332
- from ._hash import hash_dict
272
+ from .._hash import hash_dict
333
273
 
334
274
  return hash_dict(self.to_dict())
335
275