Skip to content

Commit 83c2f80

Browse files
fix(spanner): fix equality symmetry, array tuple normalization, and dict methods in JsonObject
1 parent 9339283 commit 83c2f80

2 files changed

Lines changed: 133 additions & 7 deletions

File tree

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

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,20 @@ class JsonObject(dict):
3131
"""
3232

3333
def __init__(self, *args, **kwargs):
34-
self._is_null = (args, kwargs) == ((), {}) or args == (None,)
35-
self._is_array = len(args) and isinstance(args[0], (list, tuple))
36-
self._is_scalar_value = len(args) == 1 and not isinstance(args[0], (list, dict))
34+
self._is_null = (args, kwargs) == ((), {}) or (
35+
len(args) == 1 and args[0] is None
36+
)
37+
self._is_array = len(args) == 1 and isinstance(args[0], (list, tuple))
38+
self._is_scalar_value = (
39+
len(args) == 1
40+
and args[0] is not None
41+
and not isinstance(args[0], (list, dict))
42+
)
3743

3844
# if the JSON object is represented with an array,
3945
# the value is contained separately
4046
if self._is_array:
41-
self._array_value = args[0]
47+
self._array_value = list(args[0])
4248
return
4349

4450
# If it's a scalar value, set _simple_value and return early
@@ -50,7 +56,7 @@ def __init__(self, *args, **kwargs):
5056
self._is_array = args[0]._is_array
5157
self._is_scalar_value = args[0]._is_scalar_value
5258
if self._is_array:
53-
self._array_value = args[0]._array_value
59+
self._array_value = list(args[0]._array_value)
5460
elif self._is_scalar_value:
5561
self._simple_value = args[0]._simple_value
5662

@@ -59,6 +65,62 @@ def __init__(self, *args, **kwargs):
5965
else:
6066
super().__init__(*args, **kwargs)
6167

68+
def get(self, key, default=None):
69+
if self._is_array:
70+
try:
71+
return self._array_value[key]
72+
except (IndexError, TypeError):
73+
return default
74+
if self._is_scalar_value or self._is_null:
75+
return default
76+
return super().get(key, default)
77+
78+
def copy(self):
79+
if self._is_array:
80+
return JsonObject(list(self._array_value))
81+
if self._is_scalar_value:
82+
return JsonObject(self._simple_value)
83+
if self._is_null:
84+
return JsonObject()
85+
return JsonObject(super().copy())
86+
87+
def keys(self):
88+
if self._is_array or self._is_scalar_value or self._is_null:
89+
return {}.keys()
90+
return super().keys()
91+
92+
def values(self):
93+
if self._is_array:
94+
return dict(enumerate(self._array_value)).values()
95+
if self._is_scalar_value:
96+
return {0: self._simple_value}.values()
97+
if self._is_null:
98+
return {}.values()
99+
return super().values()
100+
101+
def items(self):
102+
if self._is_array:
103+
return dict(enumerate(self._array_value)).items()
104+
if self._is_scalar_value:
105+
return {0: self._simple_value}.items()
106+
if self._is_null:
107+
return {}.items()
108+
return super().items()
109+
110+
def pop(self, key, *args):
111+
if self._is_array:
112+
try:
113+
return self._array_value.pop(key)
114+
except (IndexError, TypeError):
115+
if args:
116+
return args[0]
117+
raise KeyError(key) from None
118+
if self._is_scalar_value or self._is_null:
119+
if args:
120+
return args[0]
121+
raise KeyError(key)
122+
return super().pop(key, *args)
123+
62124
def __len__(self):
63125
if self._is_null:
64126
return 0
@@ -78,6 +140,8 @@ def __bool__(self):
78140
return super().__len__() > 0
79141

80142
def __iter__(self):
143+
if self._is_null:
144+
return iter([])
81145
if self._is_array:
82146
return iter(self._array_value)
83147
if self._is_scalar_value:
@@ -96,6 +160,8 @@ def __getitem__(self, key):
96160
return super().__getitem__(key)
97161

98162
def __contains__(self, item):
163+
if self._is_null:
164+
return False
99165
if self._is_array:
100166
return item in self._array_value
101167
if self._is_scalar_value:

‎packages/google-cloud-spanner/tests/unit/test_datatypes.py‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Copyright 2024 Google LLC
1+
# Copyright 2024 Google LLC All rights reserved.
22
#
3-
# Licensed under the Apache License, Version 2.0 ( disputes );
3+
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
55
# You may obtain a copy of the License at
66
#
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
1516
import json
1617
import unittest
1718

@@ -63,6 +64,65 @@ class Test_JsonObject_dict_protocol(unittest.TestCase):
6364
"""Verify that JsonObject behaves correctly with standard Python
6465
operations (len, bool, iteration, indexing) for all JSON variants."""
6566

67+
def test_asymmetric_equality(self):
68+
a = JsonObject()
69+
b = JsonObject(None)
70+
self.assertEqual(a == b, b == a, "Equality symmetry is broken!")
71+
self.assertEqual(a, b)
72+
73+
def test_tuple_vs_list_equality(self):
74+
tup = JsonObject((1, 2))
75+
lst = JsonObject([1, 2])
76+
self.assertEqual(tup, lst)
77+
self.assertEqual(tup, [1, 2])
78+
79+
def test_get_method(self):
80+
arr = JsonObject([10, 20])
81+
self.assertEqual(arr.get(0), 10)
82+
self.assertEqual(arr.get(1), 20)
83+
self.assertIsNone(arr.get(2))
84+
obj = JsonObject({"a": 1})
85+
self.assertEqual(obj.get("a"), 1)
86+
self.assertIsNone(obj.get("b"))
87+
scalar = JsonObject(42)
88+
self.assertIsNone(scalar.get("a"))
89+
90+
def test_copy_method(self):
91+
arr = JsonObject([10, 20])
92+
cp_arr = arr.copy()
93+
self.assertEqual(cp_arr, arr)
94+
self.assertEqual(cp_arr.get(0), 10)
95+
96+
obj = JsonObject({"a": 1})
97+
cp_obj = obj.copy()
98+
self.assertEqual(cp_obj, obj)
99+
100+
scalar = JsonObject(42)
101+
cp_scalar = scalar.copy()
102+
self.assertEqual(cp_scalar, scalar)
103+
104+
def test_keys_values_items_pop(self):
105+
arr = JsonObject([10, 20])
106+
self.assertEqual(list(arr.keys()), [])
107+
self.assertEqual(list(arr.values()), [10, 20])
108+
self.assertEqual(list(arr.items()), [(0, 10), (1, 20)])
109+
val = arr.pop(0)
110+
self.assertEqual(val, 10)
111+
self.assertEqual(len(arr), 1)
112+
113+
obj = JsonObject({"a": 1})
114+
self.assertEqual(list(obj.keys()), ["a"])
115+
self.assertEqual(list(obj.values()), [1])
116+
self.assertEqual(list(obj.items()), [("a", 1)])
117+
118+
def test_null_sentinel_hiding(self):
119+
null_obj = JsonObject()
120+
self.assertEqual(list(null_obj), [])
121+
self.assertNotIn("__json_null__", null_obj)
122+
null_obj_none = JsonObject(None)
123+
self.assertEqual(list(null_obj_none), [])
124+
self.assertNotIn("__json_null__", null_obj_none)
125+
66126
def test_isinstance_dict(self):
67127
obj = JsonObject({"a": 1})
68128
self.assertTrue(isinstance(obj, dict))

0 commit comments

Comments
 (0)