|
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