|
8 | 8 |
|
9 | 9 | from __future__ import annotations |
10 | 10 |
|
| 11 | +import copy |
11 | 12 | import hashlib |
12 | 13 | import json |
13 | 14 | from collections import OrderedDict |
14 | 15 | from threading import Lock |
15 | 16 | from typing import Any, Mapping |
16 | 17 |
|
| 18 | +try: |
| 19 | + from pydantic import BaseModel |
| 20 | +except ImportError: # pragma: no cover - pydantic is part of the gui extra |
| 21 | + BaseModel = None # type: ignore[assignment] |
| 22 | + |
| 23 | + |
| 24 | +def _isolate(value: Any) -> Any: |
| 25 | + """Return a copy of ``value`` safe for an independent consumer.""" |
| 26 | + if BaseModel is not None and isinstance(value, BaseModel): |
| 27 | + return value.model_copy(deep=True) |
| 28 | + return copy.deepcopy(value) |
| 29 | + |
17 | 30 |
|
18 | 31 | DEFAULT_CAPACITY: int = 50 |
19 | 32 |
|
@@ -81,21 +94,31 @@ def __len__(self) -> int: |
81 | 94 | return len(self._data) |
82 | 95 |
|
83 | 96 | def get(self, key: str) -> Any | None: |
| 97 | + """Return a fresh deep-copy of the cached value (or None on miss). |
| 98 | +
|
| 99 | + Hits MUST return a distinct object so that a downstream consumer |
| 100 | + mutating the result cannot corrupt the next reader. Pydantic |
| 101 | + models pay one model_copy(deep=True); plain dicts/lists pay one |
| 102 | + copy.deepcopy. |
| 103 | + """ |
84 | 104 | with self._lock: |
85 | 105 | if key not in self._data: |
86 | 106 | self._misses += 1 |
87 | 107 | return None |
88 | 108 | self._data.move_to_end(key) |
89 | 109 | self._hits += 1 |
90 | | - return self._data[key] |
| 110 | + stored = self._data[key] |
| 111 | + return _isolate(stored) |
91 | 112 |
|
92 | 113 | def set(self, key: str, value: Any) -> None: |
| 114 | + """Store a deep-copy so the caller can mutate the original safely.""" |
| 115 | + snapshot = _isolate(value) |
93 | 116 | with self._lock: |
94 | 117 | if key in self._data: |
95 | 118 | self._data.move_to_end(key) |
96 | | - self._data[key] = value |
| 119 | + self._data[key] = snapshot |
97 | 120 | return |
98 | | - self._data[key] = value |
| 121 | + self._data[key] = snapshot |
99 | 122 | if len(self._data) > self._capacity: |
100 | 123 | self._data.popitem(last=False) |
101 | 124 |
|
|
0 commit comments