|
39 | 39 | __version__ = "0.0.0+auto.0"
|
40 | 40 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_JSON_Stream.git"
|
41 | 41 |
|
42 |
| -class IterToStream: |
43 |
| - def __init__(self, data_iter): |
44 |
| - self.data_iter = data_iter |
45 |
| - self.i = 0 |
46 |
| - self.chunk = b"" |
47 |
| - |
48 |
| - def read(self): |
49 |
| - if self.i >= len(self.chunk): |
50 |
| - try: |
51 |
| - self.chunk = next(self.data_iter) |
52 |
| - except StopIteration: |
53 |
| - raise EOFError |
54 |
| - self.i = 0 |
55 |
| - c = self.chunk[self.i] |
56 |
| - self.i += 1 |
57 |
| - return c |
58 |
| - |
59 |
| - def fast_forward(self, closer): |
60 |
| - closer = ord(closer) |
61 |
| - close_stack = [closer] |
62 |
| - count = 0 |
63 |
| - while close_stack: |
64 |
| - c = self.read() |
65 |
| - count += 1 |
66 |
| - if c == close_stack[-1]: |
67 |
| - close_stack.pop() |
68 |
| - elif c == ord("\""): |
69 |
| - close_stack.append(ord("\"")) |
70 |
| - elif close_stack[-1] == ord("\""): |
71 |
| - # in a string so ignore [] and {} |
72 |
| - pass |
73 |
| - elif c in (ord("}"), ord("]")): |
74 |
| - # Mismatched list or object means we're done and already past the last comma. |
75 |
| - return True |
76 |
| - elif c == ord("{"): |
77 |
| - close_stack.append(ord("}")) |
78 |
| - elif c == ord("["): |
79 |
| - close_stack.append(ord("]")) |
80 |
| - return False |
81 |
| - |
82 |
| - def next_value(self, endswith): |
83 |
| - buf = array.array("B") |
84 |
| - endswith = ord(endswith) |
85 |
| - in_string = False |
86 |
| - while True: |
87 |
| - try: |
88 |
| - c = self.read() |
89 |
| - except EOFError: |
90 |
| - c = endswith |
91 |
| - if c == endswith or (not in_string and c in (ord("]"), ord("}"))): |
92 |
| - if len(buf) == 0: |
93 |
| - return None |
94 |
| - value_string = bytes(buf).decode("utf-8") |
95 |
| - # print(repr(value_string)) |
96 |
| - return json.loads(value_string) |
97 |
| - elif c == ord("{"): |
98 |
| - return TransientObject(self) |
99 |
| - elif c == ord("["): |
100 |
| - return TransientList(self) |
101 |
| - else: |
102 |
| - if not in_string: |
103 |
| - in_string = c == ord("\"") |
104 |
| - else: |
105 |
| - in_string = c != ord("\"") |
106 |
| - buf.append(c) |
107 |
| - |
108 |
| -class Transient: |
109 |
| - pass |
110 | 42 |
|
111 |
| -class TransientList(Transient): |
112 |
| - def __init__(self, stream): |
113 |
| - self.data = stream |
114 |
| - self.done = False |
115 |
| - self.active_child = None |
116 |
| - |
117 |
| - def finish(self): |
118 |
| - if not self.done: |
119 |
| - if self.active_child: |
120 |
| - self.active_child.finish() |
121 |
| - self.active_child = None |
122 |
| - self.data.fast_forward("]") |
123 |
| - self.done = True |
124 |
| - |
125 |
| - def __iter__(self): |
126 |
| - return self |
127 |
| - |
128 |
| - def __next__(self): |
129 |
| - if self.active_child: |
130 |
| - self.active_child.finish() |
131 |
| - self.done = self.data.fast_forward(",") |
132 |
| - self.active_child = None |
133 |
| - if self.done: |
134 |
| - raise StopIteration() |
135 |
| - next_value = self.data.next_value(",") |
136 |
| - if next_value is None: |
137 |
| - self.done = True |
138 |
| - raise StopIteration() |
139 |
| - if isinstance(next_value, Transient): |
140 |
| - self.active_child = next_value |
141 |
| - return next_value |
| 43 | +class _IterToStream: |
| 44 | + """Converts an iterator to a JSON data stream.""" |
| 45 | + |
| 46 | + def __init__(self, data_iter): |
| 47 | + self.data_iter = data_iter |
| 48 | + self.i = 0 |
| 49 | + self.chunk = b"" |
| 50 | + |
| 51 | + def read(self): |
| 52 | + """Read the next character from the stream.""" |
| 53 | + if self.i >= len(self.chunk): |
| 54 | + try: |
| 55 | + self.chunk = next(self.data_iter) |
| 56 | + except StopIteration as exc: |
| 57 | + raise EOFError from exc |
| 58 | + self.i = 0 |
| 59 | + char = self.chunk[self.i] |
| 60 | + self.i += 1 |
| 61 | + return char |
| 62 | + |
| 63 | + def fast_forward(self, closer): |
| 64 | + """Read through the stream until the character is ``closer``, ``]`` |
| 65 | + (ending a list) or ``}`` (ending an object.) Intermediate lists and |
| 66 | + objects are skipped.""" |
| 67 | + closer = ord(closer) |
| 68 | + close_stack = [closer] |
| 69 | + count = 0 |
| 70 | + while close_stack: |
| 71 | + char = self.read() |
| 72 | + count += 1 |
| 73 | + if char == close_stack[-1]: |
| 74 | + close_stack.pop() |
| 75 | + elif char == ord('"'): |
| 76 | + close_stack.append(ord('"')) |
| 77 | + elif close_stack[-1] == ord('"'): |
| 78 | + # in a string so ignore [] and {} |
| 79 | + pass |
| 80 | + elif char in (ord("}"), ord("]")): |
| 81 | + # Mismatched list or object means we're done and already past the last comma. |
| 82 | + return True |
| 83 | + elif char == ord("{"): |
| 84 | + close_stack.append(ord("}")) |
| 85 | + elif char == ord("["): |
| 86 | + close_stack.append(ord("]")) |
| 87 | + return False |
| 88 | + |
| 89 | + def next_value(self, endswith=None): |
| 90 | + """Read and parse the next JSON data.""" |
| 91 | + buf = array.array("B") |
| 92 | + if isinstance(endswith, str): |
| 93 | + endswith = ord(endswith) |
| 94 | + in_string = False |
| 95 | + while True: |
| 96 | + try: |
| 97 | + char = self.read() |
| 98 | + except EOFError: |
| 99 | + char = endswith |
| 100 | + if char == endswith or (not in_string and char in (ord("]"), ord("}"))): |
| 101 | + if len(buf) == 0: |
| 102 | + return None |
| 103 | + value_string = bytes(buf).decode("utf-8") |
| 104 | + # print(repr(value_string)) |
| 105 | + return json.loads(value_string) |
| 106 | + if char == ord("{"): |
| 107 | + return TransientObject(self) |
| 108 | + if char == ord("["): |
| 109 | + return TransientList(self) |
| 110 | + |
| 111 | + if not in_string: |
| 112 | + in_string = char == ord('"') |
| 113 | + else: |
| 114 | + in_string = char != ord('"') |
| 115 | + buf.append(char) |
| 116 | + |
| 117 | + |
| 118 | +class Transient: # pylint: disable=too-few-public-methods |
| 119 | + """Transient object representing a JSON object.""" |
| 120 | + |
| 121 | + # This is helpful for checking that something is a TransientList or TransientObject. |
142 | 122 |
|
143 |
| -class TransientObject(Transient): |
144 |
| - def __init__(self, stream): |
145 |
| - self.data = stream |
146 |
| - self.done = False |
147 |
| - self.buf = array.array("B") |
148 | 123 |
|
149 |
| - self.active_child = None |
| 124 | +class TransientList(Transient): |
| 125 | + """Transient object that acts like a list through the stream.""" |
150 | 126 |
|
151 |
| - def finish(self): |
152 |
| - if not self.done: |
153 |
| - if self.active_child: |
154 |
| - self.active_child.finish() |
| 127 | + def __init__(self, stream): |
| 128 | + self.data = stream |
| 129 | + self.done = False |
155 | 130 | self.active_child = None
|
156 |
| - self.data.fast_forward("}") |
157 |
| - self.done = True |
158 |
| - |
159 |
| - def __getitem__(self, key): |
160 |
| - if self.active_child: |
161 |
| - self.active_child.finish() |
162 |
| - self.done = self.data.fast_forward(",") |
163 |
| - self.active_child = None |
164 |
| - if self.done: |
165 |
| - raise KeyError() |
166 |
| - # print("get", key) |
167 |
| - found = False |
168 |
| - while True: |
169 |
| - current_key = self.data.next_value(":") |
170 |
| - if current_key is None: |
171 |
| - # print("object done", self) |
| 131 | + |
| 132 | + def finish(self): |
| 133 | + """Consume all of the characters for this list from the stream.""" |
| 134 | + if not self.done: |
| 135 | + if self.active_child: |
| 136 | + self.active_child.finish() |
| 137 | + self.active_child = None |
| 138 | + self.data.fast_forward("]") |
172 | 139 | self.done = True
|
173 |
| - break |
174 |
| - if current_key == key: |
| 140 | + |
| 141 | + def __iter__(self): |
| 142 | + return self |
| 143 | + |
| 144 | + def __next__(self): |
| 145 | + if self.active_child: |
| 146 | + self.active_child.finish() |
| 147 | + self.done = self.data.fast_forward(",") |
| 148 | + self.active_child = None |
| 149 | + if self.done: |
| 150 | + raise StopIteration() |
175 | 151 | next_value = self.data.next_value(",")
|
| 152 | + if next_value is None: |
| 153 | + self.done = True |
| 154 | + raise StopIteration() |
176 | 155 | if isinstance(next_value, Transient):
|
177 |
| - self.active_child = next_value |
| 156 | + self.active_child = next_value |
178 | 157 | return next_value
|
179 |
| - else: |
180 |
| - self.data.fast_forward(",") |
181 |
| - raise KeyError() |
| 158 | + |
| 159 | + |
| 160 | +class TransientObject(Transient): |
| 161 | + """Transient object that acts like a dictionary through the stream.""" |
| 162 | + |
| 163 | + def __init__(self, stream): |
| 164 | + self.data = stream |
| 165 | + self.done = False |
| 166 | + self.buf = array.array("B") |
| 167 | + |
| 168 | + self.active_child = None |
| 169 | + |
| 170 | + def finish(self): |
| 171 | + """Consume all of the characters for this object from the stream.""" |
| 172 | + if not self.done: |
| 173 | + if self.active_child: |
| 174 | + self.active_child.finish() |
| 175 | + self.active_child = None |
| 176 | + self.data.fast_forward("}") |
| 177 | + self.done = True |
| 178 | + |
| 179 | + def __getitem__(self, key): |
| 180 | + if self.active_child: |
| 181 | + self.active_child.finish() |
| 182 | + self.done = self.data.fast_forward(",") |
| 183 | + self.active_child = None |
| 184 | + if self.done: |
| 185 | + raise KeyError() |
| 186 | + |
| 187 | + while True: |
| 188 | + current_key = self.data.next_value(":") |
| 189 | + if current_key is None: |
| 190 | + # print("object done", self) |
| 191 | + self.done = True |
| 192 | + break |
| 193 | + if current_key == key: |
| 194 | + next_value = self.data.next_value(",") |
| 195 | + if isinstance(next_value, Transient): |
| 196 | + self.active_child = next_value |
| 197 | + return next_value |
| 198 | + self.data.fast_forward(",") |
| 199 | + raise KeyError() |
182 | 200 |
|
183 | 201 |
|
184 | 202 | def load(data_iter):
|
185 |
| - stream = IterToStream(data_iter) |
186 |
| - c = stream.read() |
187 |
| - if c == ord("{"): |
188 |
| - return TransientObject(stream) |
189 |
| - elif c == ord("["): |
190 |
| - return TransientList(stream) |
191 |
| - else: |
192 |
| - # TODO: single value? |
193 |
| - return None |
| 203 | + """Returns an object to represent the top level of the given JSON stream.""" |
| 204 | + stream = _IterToStream(data_iter) |
| 205 | + return stream.next_value(None) |
0 commit comments