Skip to content

Commit f9f0242

Browse files
authored
Merge pull request #9 from Neradoc/iterator-on-objects
Iterator on JSON objects
2 parents d6d0ace + a2739d7 commit f9f0242

File tree

3 files changed

+214
-7
lines changed

3 files changed

+214
-7
lines changed

adafruit_json_stream.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def __init__(self, stream):
154154
self.finish_char = ""
155155

156156
def finish(self):
157-
"""Consume all of the characters for this list from the stream."""
157+
"""Consume all of the characters for this container from the stream."""
158158
if not self.done:
159159
if self.active_child:
160160
self.active_child.finish()
@@ -163,7 +163,8 @@ def finish(self):
163163
self.done = True
164164

165165
def as_object(self):
166-
"""Consume all of the characters for this list from the stream and return as an object."""
166+
"""Consume all of the characters for this container from the stream
167+
and return as an object."""
167168
if self.has_read:
168169
raise BufferError("Object has already been partly read.")
169170

@@ -207,10 +208,17 @@ class TransientObject(Transient):
207208
def __init__(self, stream):
208209
super().__init__(stream)
209210
self.finish_char = "}"
210-
self.active_child_key = None
211+
self.active_key = None
212+
213+
def finish(self):
214+
"""Consume all of the characters for this container from the stream."""
215+
if self.active_key and not self.active_child:
216+
self.done = self.data.fast_forward(",")
217+
self.active_key = None
218+
super().finish()
211219

212220
def __getitem__(self, key):
213-
if self.active_child and self.active_child_key == key:
221+
if self.active_child and self.active_key == key:
214222
return self.active_child
215223

216224
self.has_read = True
@@ -219,12 +227,16 @@ def __getitem__(self, key):
219227
self.active_child.finish()
220228
self.done = self.data.fast_forward(",")
221229
self.active_child = None
222-
self.active_child_key = None
230+
self.active_key = None
223231
if self.done:
224232
raise KeyError(key)
225233

226234
while not self.done:
227-
current_key = self.data.next_value(":")
235+
if self.active_key:
236+
current_key = self.active_key
237+
self.active_key = None
238+
else:
239+
current_key = self.data.next_value(":")
228240
if current_key is None:
229241
self.done = True
230242
break
@@ -234,11 +246,47 @@ def __getitem__(self, key):
234246
self.done = True
235247
if isinstance(next_value, Transient):
236248
self.active_child = next_value
237-
self.active_child_key = key
249+
self.active_key = key
238250
return next_value
239251
self.done = self.data.fast_forward(",")
240252
raise KeyError(key)
241253

254+
def __iter__(self):
255+
return self
256+
257+
def _next_key(self):
258+
"""Return the next item's key, without consuming the value."""
259+
if self.active_key:
260+
if self.active_child:
261+
self.active_child.finish()
262+
self.active_child = None
263+
self.done = self.data.fast_forward(",")
264+
self.active_key = None
265+
if self.done:
266+
raise StopIteration()
267+
268+
self.has_read = True
269+
270+
current_key = self.data.next_value(":")
271+
if current_key is None:
272+
self.done = True
273+
raise StopIteration()
274+
275+
self.active_key = current_key
276+
return current_key
277+
278+
def __next__(self):
279+
return self._next_key()
280+
281+
def items(self):
282+
"""Return iterator in the dictionary’s items ((key, value) pairs)."""
283+
try:
284+
while not self.done:
285+
key = self._next_key()
286+
yield (key, self[key])
287+
except StopIteration:
288+
return
289+
242290

243291
def load(data_iter):
244292
"""Returns an object to represent the top level of the given JSON stream."""
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2023 Scott Shawcroft for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: Unlicense
4+
5+
import sys
6+
import time
7+
8+
import adafruit_json_stream as json_stream
9+
10+
# import json_stream
11+
12+
13+
class FakeResponse:
14+
def __init__(self, file):
15+
self.file = file
16+
17+
def iter_content(self, chunk_size):
18+
while True:
19+
yield self.file.read(chunk_size)
20+
21+
22+
f = open(sys.argv[1], "rb") # pylint: disable=consider-using-with
23+
obj = json_stream.load(FakeResponse(f).iter_content(32))
24+
25+
26+
def find_keys(haystack, keys):
27+
"""If we don't know the order in which the keys are,
28+
go through all of them and pick the ones we want"""
29+
out = {}
30+
# iterate on the items of an object
31+
for key in haystack:
32+
if key in keys:
33+
# retrieve the value only if needed
34+
value = haystack[key]
35+
# if it's a sub object, get it all
36+
if hasattr(value, "as_object"):
37+
value = value.as_object()
38+
out[key] = value
39+
return out
40+
41+
42+
months = [
43+
"January",
44+
"February",
45+
"March",
46+
"April",
47+
"May",
48+
"June",
49+
"July",
50+
"August",
51+
"September",
52+
"October",
53+
"November",
54+
"December",
55+
]
56+
57+
58+
def time_to_date(stamp):
59+
tt = time.localtime(stamp)
60+
month = months[tt.tm_mon]
61+
return f"{tt.tm_mday:2d}th of {month}"
62+
63+
64+
def ftoc(temp):
65+
return (temp - 32) * 5 / 9
66+
67+
68+
currently = obj["currently"]
69+
print("Currently:")
70+
print(" ", time_to_date(currently["time"]))
71+
print(" ", currently["icon"])
72+
73+
# iterate on the content of a list
74+
for i, day in enumerate(obj["daily"]["data"]):
75+
day_items = find_keys(day, ("time", "summary", "temperatureHigh"))
76+
date = time_to_date(day_items["time"])
77+
print(
78+
f'On {date}: {day_items["summary"]},',
79+
f'Max: {int(day_items["temperatureHigh"])}F',
80+
f'({int(ftoc(day_items["temperatureHigh"]))}C)',
81+
)
82+
83+
if i > 4:
84+
break

tests/test_json_stream.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,3 +685,78 @@ def test_as_object_grabbing_multiple_subscriptable_levels_again_after_passed_rai
685685
assert next(dict_1["sub_list"]) == "a"
686686
with pytest.raises(KeyError, match="sub_dict"):
687687
dict_1["sub_dict"]["sub_dict_name"]
688+
689+
690+
def test_iterating_keys(dict_with_keys):
691+
"""Iterate through keys of a simple object."""
692+
693+
bytes_io_chunk = BytesChunkIO(dict_with_keys.encode())
694+
stream = adafruit_json_stream.load(bytes_io_chunk)
695+
output = list(stream)
696+
assert output == ["field_1", "field_2", "field_3"]
697+
698+
699+
def test_iterating_keys_get(dict_with_keys):
700+
"""Iterate through keys of a simple object and get values."""
701+
702+
the_dict = json.loads(dict_with_keys)
703+
704+
bytes_io_chunk = BytesChunkIO(dict_with_keys.encode())
705+
stream = adafruit_json_stream.load(bytes_io_chunk)
706+
for key in stream:
707+
value = stream[key]
708+
assert value == the_dict[key]
709+
710+
711+
def test_iterating_items(dict_with_keys):
712+
"""Iterate through items of a simple object."""
713+
714+
bytes_io_chunk = BytesChunkIO(dict_with_keys.encode())
715+
stream = adafruit_json_stream.load(bytes_io_chunk)
716+
output = list(stream.items())
717+
assert output == [("field_1", 1), ("field_2", 2), ("field_3", 3)]
718+
719+
720+
def test_iterating_keys_after_get(dict_with_keys):
721+
"""Iterate through keys of a simple object after an item has already been read."""
722+
723+
bytes_io_chunk = BytesChunkIO(dict_with_keys.encode())
724+
stream = adafruit_json_stream.load(bytes_io_chunk)
725+
assert stream["field_1"] == 1
726+
output = list(stream)
727+
assert output == ["field_2", "field_3"]
728+
729+
730+
def test_iterating_items_after_get(dict_with_keys):
731+
"""Iterate through items of a simple object after an item has already been read."""
732+
733+
bytes_io_chunk = BytesChunkIO(dict_with_keys.encode())
734+
stream = adafruit_json_stream.load(bytes_io_chunk)
735+
assert stream["field_1"] == 1
736+
output = list(stream.items())
737+
assert output == [("field_2", 2), ("field_3", 3)]
738+
739+
740+
def test_iterating_complex_dict(complex_dict):
741+
"""Mix iterating over items of objects in objects in arrays."""
742+
743+
names = ["one", "two", "three", "four"]
744+
sub_values = [None, "two point one", "three point one", None]
745+
746+
stream = adafruit_json_stream.load(BytesChunkIO(complex_dict.encode()))
747+
748+
thing_num = 0
749+
for (index, item) in enumerate(stream.items()):
750+
key, a_list = item
751+
assert key == f"list_{index+1}"
752+
for thing in a_list:
753+
assert thing["dict_name"] == names[thing_num]
754+
for sub_key in thing["sub_dict"]:
755+
# break after getting a key with or without the value
756+
# (testing finish() called from the parent list)
757+
if sub_key == "sub_dict_name":
758+
if thing_num in {1, 2}:
759+
value = thing["sub_dict"][sub_key]
760+
assert value == sub_values[thing_num]
761+
break
762+
thing_num += 1

0 commit comments

Comments
 (0)