Coverage for src/lib.mys : 76%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from string import StringReader
3class TomlError(Error):
4 message: string
6trait Value:
7 """A value in a TOML document.
9 """
11 func table(self) -> {string: Value}:
12 """As a table. Raises an error for non-table values.
14 """
16 raise NotImplementedError()
18 func get(self, key: string) -> Value:
19 """As an item from a table. Raises an error for non-table values and
20 if the key is missing.
22 """
24 raise NotImplementedError()
26 func array(self) -> [Value]:
27 """As an array. Raises an error for non-array values.
29 """
31 raise NotImplementedError()
33 func at(self, index: i64) -> Value:
34 """As an item in an array. Raises an error for non-array values.
36 """
38 raise NotImplementedError()
40 func string(self) -> string:
41 """As a string. Raises an error for non-string values.
43 """
45 raise NotImplementedError()
47 func integer(self) -> i64:
48 """As an integer. Raises an error for non-integer values.
50 """
52 raise NotImplementedError()
54 func float(self) -> f64:
55 """As a float. Raises an error for non-float values.
57 """
59 raise NotImplementedError()
61 func bool(self) -> bool:
62 """As a boolean. Raises an error for non-bool values.
64 """
66 raise NotImplementedError()
68class Table(Value):
69 """A table.
71 """
73 items: {string: Value}
75 func __init__(self):
76 self.items = {}
78 func table(self) -> {string: Value}:
79 return self.items
81 func get(self, key: string) -> Value:
82 return self.items[key]
84class Array(Value):
85 """An array.
87 """
89 items: [Value]
91 func __init__(self):
92 self.items = []
94 func array(self) -> [Value]:
95 return self.items
97 func at(self, index: i64) -> Value:
98 return self.items[index]
100class String(Value):
101 """A string.
103 """
105 value: string
107 func string(self) -> string:
108 return self.value
110class Integer(Value):
111 """An integer.
113 """
115 value: i64
117 func integer(self) -> i64:
118 return self.value
120# class Float(Value):
121# value: f64
122#
123# func float(self) -> f64:
124# return self.value
126class Bool(Value):
127 """A boolean.
129 """
131 value: bool
133 func bool(self) -> bool:
134 return self.value
136func _decode_string(reader: StringReader) -> String:
137 value = ""
139 while True:
140 ch = reader.get()
142 if ch == '\"':
143 break
144 elif ch == '\\':
145 ch = reader.get()
147 if ch in "\"/\\":
148 value += ch
149 elif ch == 'b':
150 value += '\b'
151 elif ch == 'f':
152 value += '\f'
153 elif ch == 'r':
154 value += '\r'
155 elif ch == 'n':
156 value += '\n'
157 elif ch == 't':
158 value += '\t'
159 elif ch == 'u':
160 value += char(i64(reader.get()) << 24
161 | i64(reader.get()) << 16
162 | i64(reader.get()) << 8
163 | i64(reader.get()) << 0)
164 else:
165 raise TomlError(f"Unexpected character '{ch}'.")
166 elif ch == '':
167 raise TomlError("Out of data when parsing string.")
168 else:
169 value += ch
171 return String(value)
173func _decode_integer(reader: StringReader, ch: char) -> Value:
174 data = ""
176 while True:
177 data += ch
178 ch = reader.get()
180 if ch not in "0123456789":
181 if ch != '':
182 reader.unget()
184 break
186 return Integer(i64(data))
188func _decode_true(reader: StringReader) -> Bool:
189 if reader.read(3) != "rue":
190 raise TomlError("Corrupt true.")
192 return Bool(True)
194func _decode_false(reader: StringReader) -> Bool:
195 if reader.read(4) != "alse":
196 raise TomlError("Corrupt false.")
198 return Bool(False)
200func _decode_table(root: Table, reader: StringReader) -> Table:
201 name = ""
202 table = Table()
204 while True:
205 ch = reader.get()
207 if ch == '':
208 raise TomlError("Table name missing.")
209 elif ch == ']':
210 break
211 else:
212 name += ch
214 root.items[name] = table
216 return table
218func _decode_inline_table(reader: StringReader) -> Table:
219 table = Table()
221 while True:
222 ch = reader.get()
224 if ch == '':
225 raise TomlError(f"No value found before end of input.")
226 elif ch in " \t,":
227 pass
228 elif ch == '}':
229 break
230 else:
231 reader.unget()
232 _decode_key_value(table, reader)
234 return table
236func _decode_array(reader: StringReader) -> Array:
237 array = Array()
239 while True:
240 ch = reader.get()
242 if ch == '':
243 raise TomlError(f"No value found before end of input.")
244 elif ch in " \t\n,":
245 pass
246 elif ch == '#':
247 _decode_comment(reader)
248 elif ch == ']':
249 break
250 else:
251 reader.unget()
252 array.items.append(_decode_value(reader))
254 return array
256func _decode_value(reader: StringReader) -> Value:
257 while True:
258 ch = reader.get()
260 if ch == '':
261 raise TomlError(f"No value found before end of input.")
262 elif ch in " \t":
263 pass
264 else:
265 if ch == '"':
266 return _decode_string(reader)
267 elif ch in "0123456789":
268 return _decode_integer(reader, ch)
269 elif ch == '{':
270 return _decode_inline_table(reader)
271 elif ch == '[':
272 return _decode_array(reader)
273 else:
274 raise TomlError(f"Bad value start '{ch}'.")
276func _decode_key_value(table: Table, reader: StringReader):
277 key = ""
278 value: Value? = None
280 while True:
281 ch = reader.get()
283 if ch == '':
284 return
285 elif ch in " \t":
286 pass
287 elif ch == '=':
288 break
289 else:
290 key += ch
292 table.items[key] = _decode_value(reader)
294func _decode_comment(reader: StringReader):
295 while True:
296 ch = reader.get()
298 if ch == '':
299 break
300 elif ch == '\n':
301 break
303func decode(data: string) -> Table:
304 """Decode given TOML string.
306 """
308 reader = StringReader(data)
309 root = Table()
310 table = root
312 while True:
313 ch = reader.get()
315 if ch == '':
316 break
317 elif ch in " \t\n":
318 pass
319 elif ch == '#':
320 _decode_comment(reader)
321 elif ch == '[':
322 table = _decode_table(root, reader)
323 else:
324 reader.unget()
325 _decode_key_value(table, reader)
327 return root
329test empty_document():
330 decode("")
332test comment():
333 encoded = ("# This is a full-line comment\n"
334 "key = \"value\" # This is a comment at the end of a line\n"
335 "another = \"# This is not a comment\"\n")
336 decoded = decode(encoded)
337 assert decoded.get("key").string() == "value"
338 assert decoded.get("another").string() == "# This is not a comment"
340# @test
341# func test_dotted_keys():
342# encoded = ("name = \"Orange\"\n"
343# "physical.color = \"orange\"\n"
344# "physical.shape = \"round\"\n"
345# "site.\"google.com\" = true\n")
346# decoded = decode(encoded)
347# assert decoded.get("name").string() == "Orange"
348# physical = decoded.get("physical")
349# assert physical.get("color").string() == "orange"
350# assert physical.get("shape").string() == "round"
351# assert decoded.get("site").get("google.com").bool()
353test array():
354 encoded = (
355 "integers = [ 1, 2, 3 ]\n"
356 "colors = [ \"red\", \"yellow\", \"green\" ]\n"
357 "nested_arrays_of_ints = [ [ 1, 2 ], [3, 4, 5] ]\n"
358 "nested_mixed_array = [ [ 1, 2 ], [\"a\", \"b\", \"c\"] ]\n")
359 decoded = decode(encoded)
360 assert decoded.get("integers").at(0).integer() == 1
361 assert decoded.get("integers").at(1).integer() == 2
362 assert decoded.get("integers").at(2).integer() == 3
363 assert decoded.get("colors").at(0).string() == "red"
364 assert decoded.get("colors").at(1).string() == "yellow"
365 assert decoded.get("colors").at(2).string() == "green"
366 assert decoded.get("nested_arrays_of_ints").at(0).at(0).integer() == 1
367 assert decoded.get("nested_arrays_of_ints").at(0).at(1).integer() == 2
368 assert decoded.get("nested_arrays_of_ints").at(1).at(0).integer() == 3
369 assert decoded.get("nested_arrays_of_ints").at(1).at(1).integer() == 4
370 assert decoded.get("nested_arrays_of_ints").at(1).at(2).integer() == 5
371 assert decoded.get("nested_mixed_array").at(0).at(0).integer() == 1
372 assert decoded.get("nested_mixed_array").at(0).at(1).integer() == 2
373 assert decoded.get("nested_mixed_array").at(1).at(0).string() == "a"
374 assert decoded.get("nested_mixed_array").at(1).at(1).string() == "b"
375 assert decoded.get("nested_mixed_array").at(1).at(2).string() == "c"
377test multi_line_array():
378 encoded = ("integers2 = [\n"
379 " 1, 2, 3\n"
380 "]\n"
381 "\n"
382 "integers3 = [\n"
383 " 1,\n"
384 " 2, # this is ok\n"
385 "]\n")
386 decoded = decode(encoded)
387 assert decoded.get("integers2").at(0).integer() == 1
388 assert decoded.get("integers2").at(1).integer() == 2
389 assert decoded.get("integers3").at(0).integer() == 1
390 assert decoded.get("integers3").at(1).integer() == 2
392test table():
393 encoded = ("[table-1]\n"
394 "key1 = \"some string\"\n"
395 "key2 = 123\n"
396 "\n"
397 "[table-2]\n"
398 "key1 = \"another string\"\n"
399 "key2 = 456\n")
400 decoded = decode(encoded)
401 table_1 = decoded.get("table-1")
402 assert table_1.get("key1").string() == "some string"
403 assert table_1.get("key2").integer() == 123
404 table_2 = decoded.get("table-2")
405 assert table_2.get("key1").string() == "another string"
406 assert table_2.get("key2").integer() == 456
408# @test
409# func test_table_naming():
410# encoded = ("[dog.\"tater.man\"]\n"
411# "type.name = \"pug\"\n")
412# decoded = decode(encoded)
413# assert (decoded
414# .get("dog")
415# .get("tater.man")
416# .get("type")
417# .get("name")
418# .string() == "pug")
420test root_table():
421 encoded = ("# Top-level table begins.\n"
422 "name = \"Fido\"\n"
423 "breed = \"pug\"\n"
424 "\n"
425 "# Top-level table ends.\n"
426 "[owner]\n"
427 "name = \"Regina Dogman\"\n")
428 decoded = decode(encoded)
429 assert decoded.get("name").string() == "Fido"
430 assert decoded.get("breed").string() == "pug"
431 owner = decoded.get("owner")
432 assert owner.get("name").string() == "Regina Dogman"
434test inline_table():
435 encoded = ("name = { first = \"Tom\", last = \"Preston-Werner\" }\n"
436 "point = { x = 1, y = 2 }\n")
437 # "animal = { type.name = \"pug\" }\n")
438 decoded = decode(encoded)
439 assert decoded.get("name").get("first").string() == "Tom"
440 assert decoded.get("name").get("last").string() == "Preston-Werner"
441 assert decoded.get("point").get("x").integer() == 1
442 assert decoded.get("point").get("y").integer() == 2
443 # assert decoded.get("animal").get("type").get("name").string() == "pug"