Skip to content

Commit d004f70

Browse files
fix(spanner): implement dict protocol and nested unwrapping for JsonObject
Fixes JsonObject array/scalar/null variants breaking standard Python container protocols (len, bool, iter, getitem, contains, eq). Adds _unwrap_for_json helper to safely serialize nested JsonObject instances without data erasure. Fixes #15870
1 parent 5b2da81 commit d004f70

2 files changed

Lines changed: 300 additions & 87 deletions

File tree

packages/google-cloud-spanner/google/cloud/spanner_v1/data_types.py

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,80 @@ def __init__(self, *args, **kwargs):
5555
self._simple_value = args[0]._simple_value
5656

5757
if not self._is_null:
58-
super(JsonObject, self).__init__(*args, **kwargs)
58+
super().__init__(*args, **kwargs)
59+
60+
def __len__(self):
61+
if self._is_null:
62+
return 0
63+
if self._is_array:
64+
return len(self._array_value)
65+
if self._is_scalar_value:
66+
return 1
67+
return super().__len__()
68+
69+
def __bool__(self):
70+
if self._is_null:
71+
return False
72+
if self._is_array:
73+
return bool(self._array_value)
74+
if self._is_scalar_value:
75+
return True
76+
return super().__len__() > 0
77+
78+
def __iter__(self):
79+
if self._is_array:
80+
return iter(self._array_value)
81+
if self._is_scalar_value:
82+
raise TypeError(
83+
f"'{type(self._simple_value).__name__}' object is not iterable"
84+
)
85+
return super().__iter__()
86+
87+
def __getitem__(self, key):
88+
if self._is_array:
89+
return self._array_value[key]
90+
if self._is_scalar_value:
91+
raise TypeError(
92+
f"'{type(self._simple_value).__name__}' object is not subscriptable"
93+
)
94+
return super().__getitem__(key)
95+
96+
def __contains__(self, item):
97+
if self._is_array:
98+
return item in self._array_value
99+
if self._is_scalar_value:
100+
raise TypeError(
101+
f"argument of type '{type(self._simple_value).__name__}' "
102+
"is not iterable"
103+
)
104+
return super().__contains__(item)
105+
106+
def __eq__(self, other):
107+
if isinstance(other, JsonObject):
108+
if self._is_array:
109+
return (
110+
getattr(other, "_is_array", False)
111+
and self._array_value == other._array_value
112+
)
113+
if self._is_scalar_value:
114+
return (
115+
getattr(other, "_is_scalar_value", False)
116+
and self._simple_value == other._simple_value
117+
)
118+
if self._is_null:
119+
return getattr(other, "_is_null", False)
120+
return not (
121+
getattr(other, "_is_array", False)
122+
or getattr(other, "_is_scalar_value", False)
123+
or getattr(other, "_is_null", False)
124+
) and super().__eq__(other)
125+
if self._is_array:
126+
return self._array_value == other
127+
if self._is_scalar_value:
128+
return self._simple_value == other
129+
if self._is_null:
130+
return other is None or (isinstance(other, dict) and len(other) == 0)
131+
return super().__eq__(other)
59132

60133
def __repr__(self):
61134
if self._is_array:
@@ -64,7 +137,7 @@ def __repr__(self):
64137
if self._is_scalar_value:
65138
return str(self._simple_value)
66139

67-
return super(JsonObject, self).__repr__()
140+
return super().__repr__()
68141

69142
@classmethod
70143
def from_str(cls, str_repr):
@@ -90,17 +163,33 @@ def serialize(self):
90163
if self._is_null:
91164
return None
92165

166+
raw = _unwrap_for_json(self)
93167
if self._is_scalar_value:
94-
return json.dumps(self._simple_value)
168+
return json.dumps(raw)
169+
170+
return json.dumps(raw, sort_keys=True, separators=(",", ":"))
95171

96-
if self._is_array:
97-
return json.dumps(self._array_value, sort_keys=True, separators=(",", ":"))
98172

99-
return json.dumps(self, sort_keys=True, separators=(",", ":"))
173+
def _unwrap_for_json(val):
174+
"""Recursively unwrap JsonObject instances for safe json.dumps serialization."""
175+
if isinstance(val, JsonObject):
176+
if val._is_null:
177+
return None
178+
if val._is_array:
179+
return [_unwrap_for_json(item) for item in val._array_value]
180+
if val._is_scalar_value:
181+
return val._simple_value
182+
return {k: _unwrap_for_json(v) for k, v in val.items()}
183+
if isinstance(val, dict):
184+
return {k: _unwrap_for_json(v) for k, v in val.items()}
185+
if isinstance(val, (list, tuple)):
186+
return [_unwrap_for_json(item) for item in val]
187+
return val
100188

101189

102190
_INTERVAL_PATTERN = re.compile(
103-
r"^P(-?\d+Y)?(-?\d+M)?(-?\d+D)?(T(-?\d+H)?(-?\d+M)?(-?((\d+([.,]\d{1,9})?)|([.,]\d{1,9}))S)?)?$"
191+
r"^P(-?\d+Y)?(-?\d+M)?(-?\d+D)?"
192+
r"(T(-?\d+H)?(-?\d+M)?(-?((\d+([.,]\d{1,9})?)|([.,]\d{1,9}))S)?)?$"
104193
)
105194

106195

@@ -109,7 +198,7 @@ class Interval:
109198
"""Represents a Spanner INTERVAL type.
110199
111200
An interval is a combination of months, days and nanoseconds.
112-
Internally, Spanner supports Interval value with the following range of individual fields:
201+
Internally, Spanner supports Interval value with individual fields range:
113202
months: [-120000, 120000]
114203
days: [-3660000, 3660000]
115204
nanoseconds: [-316224000000000000000, 316224000000000000000]
@@ -199,12 +288,14 @@ def from_str(cls, s: str) -> "Interval":
199288
parts = match.groups()
200289
if not any(parts[:3]) and not parts[3]:
201290
raise ValueError(
202-
f"Invalid interval format: at least one component (Y/M/D/H/M/S) is required: {s}"
291+
"Invalid interval format: at least one component "
292+
f"(Y/M/D/H/M/S) is required: {s}"
203293
)
204294

205295
if parts[3] == "T" and not any(parts[4:7]):
206296
raise ValueError(
207-
f"Invalid interval format: time designator 'T' present but no time components specified: {s}"
297+
"Invalid interval format: time designator 'T' present "
298+
f"but no time components specified: {s}"
208299
)
209300

210301
def parse_num(s: str, suffix: str) -> int:
@@ -298,14 +389,14 @@ def _proto_enum(int_val, proto_enum_object):
298389

299390

300391
def get_proto_message(bytes_string, proto_message_object):
301-
"""parses serialized protocol buffer bytes' data or its list into proto message or list of proto message.
392+
"""Parses serialized protocol buffer bytes data or list into proto message.
302393
303394
Args:
304395
bytes_string (bytes or list[bytes]): bytes object.
305396
proto_message_object (Message): Message object for parsing
306397
307398
Returns:
308-
Message or list[Message]: parses serialized protocol buffer data into this message.
399+
Message or list[Message]: Parsed protocol buffer message(s).
309400
310401
Raises:
311402
ValueError: if the input proto_message_object is not of type Message

0 commit comments

Comments
 (0)