Hide keyboard shortcuts

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 

2 

3class TomlError(Error): 

4 message: string 

5 

6trait Value: 

7 """A value in a TOML document. 

8 

9 """ 

10 

11 func table(self) -> {string: Value}: 

12 """As a table. Raises an error for non-table values. 

13 

14 """ 

15 

16 raise NotImplementedError() 

17 

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. 

21 

22 """ 

23 

24 raise NotImplementedError() 

25 

26 func array(self) -> [Value]: 

27 """As an array. Raises an error for non-array values. 

28 

29 """ 

30 

31 raise NotImplementedError() 

32 

33 func at(self, index: i64) -> Value: 

34 """As an item in an array. Raises an error for non-array values. 

35 

36 """ 

37 

38 raise NotImplementedError() 

39 

40 func string(self) -> string: 

41 """As a string. Raises an error for non-string values. 

42 

43 """ 

44 

45 raise NotImplementedError() 

46 

47 func integer(self) -> i64: 

48 """As an integer. Raises an error for non-integer values. 

49 

50 """ 

51 

52 raise NotImplementedError() 

53 

54 func float(self) -> f64: 

55 """As a float. Raises an error for non-float values. 

56 

57 """ 

58 

59 raise NotImplementedError() 

60 

61 func bool(self) -> bool: 

62 """As a boolean. Raises an error for non-bool values. 

63 

64 """ 

65 

66 raise NotImplementedError() 

67 

68class Table(Value): 

69 """A table. 

70 

71 """ 

72 

73 items: {string: Value} 

74 

75 func __init__(self): 

76 self.items = {} 

77 

78 func table(self) -> {string: Value}: 

79 return self.items 

80 

81 func get(self, key: string) -> Value: 

82 return self.items[key] 

83 

84class Array(Value): 

85 """An array. 

86 

87 """ 

88 

89 items: [Value] 

90 

91 func __init__(self): 

92 self.items = [] 

93 

94 func array(self) -> [Value]: 

95 return self.items 

96 

97 func at(self, index: i64) -> Value: 

98 return self.items[index] 

99 

100class String(Value): 

101 """A string. 

102 

103 """ 

104 

105 value: string 

106 

107 func string(self) -> string: 

108 return self.value 

109 

110class Integer(Value): 

111 """An integer. 

112 

113 """ 

114 

115 value: i64 

116 

117 func integer(self) -> i64: 

118 return self.value 

119 

120# class Float(Value): 

121# value: f64 

122# 

123# func float(self) -> f64: 

124# return self.value 

125 

126class Bool(Value): 

127 """A boolean. 

128 

129 """ 

130 

131 value: bool 

132 

133 func bool(self) -> bool: 

134 return self.value 

135 

136func _decode_string(reader: StringReader) -> String: 

137 value = "" 

138 

139 while True: 

140 ch = reader.get() 

141 

142 if ch == '\"': 

143 break 

144 elif ch == '\\': 

145 ch = reader.get() 

146 

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 

170 

171 return String(value) 

172 

173func _decode_integer(reader: StringReader, ch: char) -> Value: 

174 data = "" 

175 

176 while True: 

177 data += ch 

178 ch = reader.get() 

179 

180 if ch not in "0123456789": 

181 if ch != '': 

182 reader.unget() 

183 

184 break 

185 

186 return Integer(i64(data)) 

187 

188func _decode_true(reader: StringReader) -> Bool: 

189 if reader.read(3) != "rue": 

190 raise TomlError("Corrupt true.") 

191 

192 return Bool(True) 

193 

194func _decode_false(reader: StringReader) -> Bool: 

195 if reader.read(4) != "alse": 

196 raise TomlError("Corrupt false.") 

197 

198 return Bool(False) 

199 

200func _decode_table(root: Table, reader: StringReader) -> Table: 

201 name = "" 

202 table = Table() 

203 

204 while True: 

205 ch = reader.get() 

206 

207 if ch == '': 

208 raise TomlError("Table name missing.") 

209 elif ch == ']': 

210 break 

211 else: 

212 name += ch 

213 

214 root.items[name] = table 

215 

216 return table 

217 

218func _decode_inline_table(reader: StringReader) -> Table: 

219 table = Table() 

220 

221 while True: 

222 ch = reader.get() 

223 

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) 

233 

234 return table 

235 

236func _decode_array(reader: StringReader) -> Array: 

237 array = Array() 

238 

239 while True: 

240 ch = reader.get() 

241 

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)) 

253 

254 return array 

255 

256func _decode_value(reader: StringReader) -> Value: 

257 while True: 

258 ch = reader.get() 

259 

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}'.") 

275 

276func _decode_key_value(table: Table, reader: StringReader): 

277 key = "" 

278 value: Value? = None 

279 

280 while True: 

281 ch = reader.get() 

282 

283 if ch == '': 

284 return 

285 elif ch in " \t": 

286 pass 

287 elif ch == '=': 

288 break 

289 else: 

290 key += ch 

291 

292 table.items[key] = _decode_value(reader) 

293 

294func _decode_comment(reader: StringReader): 

295 while True: 

296 ch = reader.get() 

297 

298 if ch == '': 

299 break 

300 elif ch == '\n': 

301 break 

302 

303func decode(data: string) -> Table: 

304 """Decode given TOML string. 

305 

306 """ 

307 

308 reader = StringReader(data) 

309 root = Table() 

310 table = root 

311 

312 while True: 

313 ch = reader.get() 

314 

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) 

326 

327 return root 

328 

329test empty_document(): 

330 decode("") 

331 

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" 

339 

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() 

352 

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" 

376 

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 

391 

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 

407 

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") 

419 

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" 

433 

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"