Clone a template config value. - Deep-copies built-in containers recursively (dict/list/tuple/set) so per-node configs don't share mutable containers. - For non-container objects, the default is *per-node instance* (prototype): attempt `copy.deepcopy`. If deepcopy fails, ra
(value: Any, memo: dict[int, Any] | None = None)
| 167 | |
| 168 | |
| 169 | def _safe_clone(value: Any, memo: dict[int, Any] | None = None) -> Any: |
| 170 | """ |
| 171 | Clone a template config value. |
| 172 | |
| 173 | - Deep-copies built-in containers recursively (dict/list/tuple/set) so per-node configs don't share mutable |
| 174 | containers. |
| 175 | - For non-container objects, the default is *per-node instance* (prototype): attempt `copy.deepcopy`. |
| 176 | If deepcopy fails, raise with a clear suggestion to use `Shared(...)` or `Factory(...)` explicitly. |
| 177 | - Some objects are intended to be singletons/shared services. Such objects can opt-in |
| 178 | by setting `__node_template_scope__ = "shared"` on the instance or its class, or by being wrapped in |
| 179 | `Shared(...)`. |
| 180 | |
| 181 | This makes NodeTemplate behavior predictable: |
| 182 | - Containers are always copied. |
| 183 | - Custom objects are copied (factory-like) unless explicitly marked as shared. |
| 184 | - Non-copyable objects must be explicitly scoped via Shared/Factory. |
| 185 | """ |
| 186 | if memo is None: |
| 187 | memo = {} |
| 188 | |
| 189 | obj_id = id(value) |
| 190 | if obj_id in memo: |
| 191 | return memo[obj_id] |
| 192 | |
| 193 | if isinstance(value, _IMMUTABLE_SCALARS): |
| 194 | memo[obj_id] = value |
| 195 | return value |
| 196 | |
| 197 | if isinstance(value, Shared): |
| 198 | memo[obj_id] = value.value |
| 199 | return value.value |
| 200 | |
| 201 | if isinstance(value, Factory): |
| 202 | produced = value.make() |
| 203 | memo[obj_id] = produced |
| 204 | return produced |
| 205 | |
| 206 | scope = getattr(value, "__node_template_scope__", None) |
| 207 | if scope is None: |
| 208 | scope = getattr(getattr(value, "__class__", object), "__node_template_scope__", None) |
| 209 | if isinstance(scope, str) and scope.lower() in {"shared", "singleton"}: |
| 210 | memo[obj_id] = value |
| 211 | return value |
| 212 | |
| 213 | if isinstance(value, dict): |
| 214 | cloned: dict[Any, Any] = {} |
| 215 | memo[obj_id] = cloned |
| 216 | for k, v in value.items(): |
| 217 | cloned[_safe_clone(k, memo)] = _safe_clone(v, memo) |
| 218 | return cloned |
| 219 | |
| 220 | if isinstance(value, list): |
| 221 | cloned_list: list[Any] = [] |
| 222 | memo[obj_id] = cloned_list |
| 223 | cloned_list.extend(_safe_clone(v, memo) for v in value) |
| 224 | return cloned_list |
| 225 | |
| 226 | if isinstance(value, tuple): |
no test coverage detected