Skip to content

Commit 9884c7d

Browse files
Fix Pydantic serialization by making _lock a property
- Convert _lock from dataclass field to property to avoid serialization issues - Lock is created on-demand via __lock attribute - Maintains thread safety while allowing Pydantic TypeAdapter serialization - Fixes PydanticSchemaGenerationError in test_agent_run_result_serialization
1 parent 6cf4cac commit 9884c7d

File tree

1 file changed

+17
-6
lines changed

1 file changed

+17
-6
lines changed

pydantic_ai_slim/pydantic_ai/usage.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,20 @@ class RunUsage(UsageBase):
190190
details: dict[str, int] = dataclasses.field(default_factory=dict)
191191
"""Any extra details returned by the model."""
192192

193-
_lock: threading.Lock = dataclasses.field(default_factory=threading.Lock, compare=False, repr=False)
194193
"""Lock to prevent race conditions when incrementing usage from concurrent tool calls."""
195194

195+
def __post_init__(self) -> None:
196+
"""Initialize the lock after dataclass initialization."""
197+
if not hasattr(self, '_lock'):
198+
self._lock = threading.Lock()
199+
200+
@property
201+
def _lock(self) -> threading.Lock:
202+
"""Get the lock, creating it if it doesn't exist."""
203+
if not hasattr(self, '__lock'):
204+
self.__lock = threading.Lock()
205+
return self.__lock
206+
196207
def incr(self, incr_usage: RunUsage | RequestUsage) -> None:
197208
"""Increment the usage in place.
198209
@@ -214,8 +225,7 @@ def __add__(self, other: RunUsage | RequestUsage) -> RunUsage:
214225
# Note: We can't use await here since __add__ must be synchronous
215226
# But __add__ creates a new object, so there's no race condition
216227
# The race condition only happens when modifying the same object concurrently
217-
# Create a new lock for the new instance
218-
new_usage._lock = threading.Lock()
228+
# The new instance will get its own lock via the property
219229

220230
if isinstance(other, RunUsage):
221231
new_usage.requests += other.requests
@@ -226,15 +236,16 @@ def __add__(self, other: RunUsage | RequestUsage) -> RunUsage:
226236
def __getstate__(self) -> dict[str, Any]:
227237
"""Exclude the lock from pickling."""
228238
state = self.__dict__.copy()
229-
# Remove the lock since it can't be pickled
239+
# Remove any lock-related attributes since they can't be pickled
230240
state.pop('_lock', None)
241+
state.pop('__lock', None)
231242
return state
232243

233244
def __setstate__(self, state: dict[str, Any]) -> None:
234245
"""Restore state and create a new lock."""
235246
self.__dict__.update(state)
236-
# Create a new lock for the unpickled instance
237-
self._lock = threading.Lock()
247+
# The lock will be created automatically via the property
248+
238249

239250

240251
def _incr_usage_tokens(slf: RunUsage | RequestUsage, incr_usage: RunUsage | RequestUsage) -> None:

0 commit comments

Comments
 (0)