|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 |
| -import copy |
4 |
| -import datetime as _dt |
5 |
| -from decimal import Decimal |
6 |
| -from fractions import Fraction |
7 |
| -from pathlib import PurePath |
8 | 3 | from typing import Any, TypeVar
|
9 |
| -from uuid import UUID |
10 | 4 |
|
11 | 5 | T = TypeVar("T")
|
12 | 6 |
|
13 | 7 |
|
14 | 8 | def safe_copy(obj: T) -> T:
|
15 | 9 | """
|
16 |
| - Copy 'obj' without triggering deepcopy on complex/fragile objects. |
17 |
| -
|
18 |
| - Rules: |
19 |
| - - Primitive/simple atoms (ints, strs, datetimes, etc.): deepcopy (cheap and safe). |
20 |
| - - Built-in containers (dict, list, tuple, set, frozenset): recurse element-wise. |
21 |
| - - Everything else (framework objects, iterators, models, file handles, etc.): |
22 |
| - shallow copy if possible; otherwise return as-is. |
23 |
| -
|
| 10 | + Craete a copy of the given object -- it can be either str or list/set/tuple of objects. |
24 | 11 | This avoids failures like:
|
25 | 12 | TypeError: cannot pickle '...ValidatorIterator' object
|
26 | 13 | because we never call deepcopy() on non-trivial objects.
|
27 | 14 | """
|
28 |
| - memo: dict[int, Any] = {} |
29 |
| - return _safe_copy_internal(obj, memo) |
30 |
| - |
31 |
| - |
32 |
| -_SIMPLE_ATOMS = ( |
33 |
| - # basics |
34 |
| - type(None), |
35 |
| - bool, |
36 |
| - int, |
37 |
| - float, |
38 |
| - complex, |
39 |
| - str, |
40 |
| - bytes, |
41 |
| - # small buffers/scalars |
42 |
| - bytearray, |
43 |
| - memoryview, |
44 |
| - range, |
45 |
| - # "value" types |
46 |
| - Decimal, |
47 |
| - Fraction, |
48 |
| - UUID, |
49 |
| - PurePath, |
50 |
| - _dt.date, |
51 |
| - _dt.datetime, |
52 |
| - _dt.time, |
53 |
| - _dt.timedelta, |
54 |
| -) |
55 |
| - |
56 |
| - |
57 |
| -def _is_simple_atom(o: Any) -> bool: |
58 |
| - return isinstance(o, _SIMPLE_ATOMS) |
59 |
| - |
60 |
| - |
61 |
| -def _safe_copy_internal(obj: T, memo: dict[int, Any]) -> T: |
62 |
| - oid = id(obj) |
63 |
| - if oid in memo: |
64 |
| - return memo[oid] # type: ignore [no-any-return] |
65 |
| - |
66 |
| - # 1) Simple "atoms": safe to deepcopy (cheap, predictable). |
67 |
| - if _is_simple_atom(obj): |
68 |
| - return copy.deepcopy(obj) |
| 15 | + return _safe_copy_internal(obj) |
69 | 16 |
|
70 |
| - # 2) Containers: rebuild and recurse. |
71 |
| - if isinstance(obj, dict): |
72 |
| - new_dict: dict[Any, Any] = {} |
73 |
| - memo[oid] = new_dict |
74 |
| - for k, v in obj.items(): |
75 |
| - # preserve key identity/value, only copy the value |
76 |
| - new_dict[k] = _safe_copy_internal(v, memo) |
77 |
| - return new_dict # type: ignore [return-value] |
78 | 17 |
|
| 18 | +def _safe_copy_internal(obj: T) -> T: |
79 | 19 | if isinstance(obj, list):
|
80 | 20 | new_list: list[Any] = []
|
81 |
| - memo[oid] = new_list |
82 |
| - new_list.extend(_safe_copy_internal(x, memo) for x in obj) |
| 21 | + new_list.extend(_safe_copy_internal(x) for x in obj) |
83 | 22 | return new_list # type: ignore [return-value]
|
84 | 23 |
|
85 | 24 | if isinstance(obj, tuple):
|
86 |
| - new_tuple = tuple(_safe_copy_internal(x, memo) for x in obj) |
87 |
| - memo[oid] = new_tuple |
| 25 | + new_tuple = tuple(_safe_copy_internal(x) for x in obj) |
88 | 26 | return new_tuple # type: ignore [return-value]
|
89 | 27 |
|
90 | 28 | if isinstance(obj, set):
|
91 | 29 | new_set: set[Any] = set()
|
92 |
| - memo[oid] = new_set |
93 | 30 | for x in obj:
|
94 |
| - new_set.add(_safe_copy_internal(x, memo)) |
| 31 | + new_set.add(_safe_copy_internal(x)) |
95 | 32 | return new_set # type: ignore [return-value]
|
96 | 33 |
|
97 | 34 | if isinstance(obj, frozenset):
|
98 |
| - new_fset = frozenset(_safe_copy_internal(x, memo) for x in obj) |
99 |
| - memo[oid] = new_fset |
| 35 | + new_fset = frozenset(_safe_copy_internal(x) for x in obj) |
100 | 36 | return new_fset # type: ignore
|
101 | 37 |
|
102 |
| - # 3) Unknown/complex leaf: return as-is (identity preserved). |
103 |
| - memo[oid] = obj |
104 | 38 | return obj
|
0 commit comments