Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/toml/decoder.py: 93%
823 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 07:03 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 07:03 +0000
1import datetime
2import io
3from os import linesep
4import re
5import sys
7from toml.tz import TomlTz
9if sys.version_info < (3,):
10 _range = xrange # noqa: F821
11else:
12 unicode = str
13 _range = range
14 basestring = str
15 unichr = chr
18def _detect_pathlib_path(p):
19 if (3, 4) <= sys.version_info:
20 import pathlib
21 if isinstance(p, pathlib.PurePath):
22 return True
23 return False
26def _ispath(p):
27 if isinstance(p, (bytes, basestring)):
28 return True
29 return _detect_pathlib_path(p)
32def _getpath(p):
33 if (3, 6) <= sys.version_info:
34 import os
35 return os.fspath(p)
36 if _detect_pathlib_path(p):
37 return str(p)
38 return p
41try:
42 FNFError = FileNotFoundError
43except NameError:
44 FNFError = IOError
47TIME_RE = re.compile(r"([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{3,6}))?")
50class TomlDecodeError(ValueError):
51 """Base toml Exception / Error."""
53 def __init__(self, msg, doc, pos):
54 lineno = doc.count('\n', 0, pos) + 1
55 colno = pos - doc.rfind('\n', 0, pos)
56 emsg = '{} (line {} column {} char {})'.format(msg, lineno, colno, pos)
57 ValueError.__init__(self, emsg)
58 self.msg = msg
59 self.doc = doc
60 self.pos = pos
61 self.lineno = lineno
62 self.colno = colno
65# Matches a TOML number, which allows underscores for readability
66_number_with_underscores = re.compile('([0-9])(_([0-9]))*')
69class CommentValue(object):
70 def __init__(self, val, comment, beginline, _dict):
71 self.val = val
72 separator = "\n" if beginline else " "
73 self.comment = separator + comment
74 self._dict = _dict
76 def __getitem__(self, key):
77 return self.val[key]
79 def __setitem__(self, key, value):
80 self.val[key] = value
82 def dump(self, dump_value_func):
83 retstr = dump_value_func(self.val)
84 if isinstance(self.val, self._dict):
85 return self.comment + "\n" + unicode(retstr)
86 else:
87 return unicode(retstr) + self.comment
90def _strictly_valid_num(n):
91 n = n.strip()
92 if not n:
93 return False
94 if n[0] == '_':
95 return False
96 if n[-1] == '_':
97 return False
98 if "_." in n or "._" in n:
99 return False
100 if len(n) == 1:
101 return True
102 if n[0] == '0' and n[1] not in ['.', 'o', 'b', 'x']:
103 return False
104 if n[0] == '+' or n[0] == '-':
105 n = n[1:]
106 if len(n) > 1 and n[0] == '0' and n[1] != '.':
107 return False
108 if '__' in n:
109 return False
110 return True
113def load(f, _dict=dict, decoder=None):
114 """Parses named file or files as toml and returns a dictionary
116 Args:
117 f: Path to the file to open, array of files to read into single dict
118 or a file descriptor
119 _dict: (optional) Specifies the class of the returned toml dictionary
120 decoder: The decoder to use
122 Returns:
123 Parsed toml file represented as a dictionary
125 Raises:
126 TypeError -- When f is invalid type
127 TomlDecodeError: Error while decoding toml
128 IOError / FileNotFoundError -- When an array with no valid (existing)
129 (Python 2 / Python 3) file paths is passed
130 """
132 if _ispath(f):
133 with io.open(_getpath(f), encoding='utf-8') as ffile:
134 return loads(ffile.read(), _dict, decoder)
135 elif isinstance(f, list):
136 from os import path as op
137 from warnings import warn
138 if not [path for path in f if op.exists(path)]:
139 error_msg = "Load expects a list to contain filenames only."
140 error_msg += linesep
141 error_msg += ("The list needs to contain the path of at least one "
142 "existing file.")
143 raise FNFError(error_msg)
144 if decoder is None:
145 decoder = TomlDecoder(_dict)
146 d = decoder.get_empty_table()
147 for l in f: # noqa: E741
148 if op.exists(l):
149 d.update(load(l, _dict, decoder))
150 else:
151 warn("Non-existent filename in list with at least one valid "
152 "filename")
153 return d
154 else:
155 try:
156 return loads(f.read(), _dict, decoder)
157 except AttributeError:
158 raise TypeError("You can only load a file descriptor, filename or "
159 "list")
162_groupname_re = re.compile(r'^[A-Za-z0-9_-]+$')
165def loads(s, _dict=dict, decoder=None):
166 """Parses string as toml
168 Args:
169 s: String to be parsed
170 _dict: (optional) Specifies the class of the returned toml dictionary
172 Returns:
173 Parsed toml file represented as a dictionary
175 Raises:
176 TypeError: When a non-string is passed
177 TomlDecodeError: Error while decoding toml
178 """
180 implicitgroups = []
181 if decoder is None:
182 decoder = TomlDecoder(_dict)
183 retval = decoder.get_empty_table()
184 currentlevel = retval
185 if not isinstance(s, basestring):
186 raise TypeError("Expecting something like a string")
188 if not isinstance(s, unicode):
189 s = s.decode('utf8')
191 original = s
192 sl = list(s)
193 openarr = 0
194 openstring = False
195 openstrchar = ""
196 multilinestr = False
197 arrayoftables = False
198 beginline = True
199 keygroup = False
200 dottedkey = False
201 keyname = 0
202 key = ''
203 prev_key = ''
204 line_no = 1
206 for i, item in enumerate(sl):
207 if item == '\r' and sl[i + 1] == '\n':
208 sl[i] = ' '
209 continue
210 if keyname:
211 key += item
212 if item == '\n':
213 raise TomlDecodeError("Key name found without value."
214 " Reached end of line.", original, i)
215 if openstring:
216 if item == openstrchar:
217 oddbackslash = False
218 k = 1
219 while i >= k and sl[i - k] == '\\':
220 oddbackslash = not oddbackslash
221 k += 1
222 if not oddbackslash:
223 keyname = 2
224 openstring = False
225 openstrchar = ""
226 continue
227 elif keyname == 1:
228 if item.isspace():
229 keyname = 2
230 continue
231 elif item == '.':
232 dottedkey = True
233 continue
234 elif item.isalnum() or item == '_' or item == '-':
235 continue
236 elif (dottedkey and sl[i - 1] == '.' and
237 (item == '"' or item == "'")):
238 openstring = True
239 openstrchar = item
240 continue
241 elif keyname == 2:
242 if item.isspace():
243 if dottedkey:
244 nextitem = sl[i + 1]
245 if not nextitem.isspace() and nextitem != '.':
246 keyname = 1
247 continue
248 if item == '.':
249 dottedkey = True
250 nextitem = sl[i + 1]
251 if not nextitem.isspace() and nextitem != '.':
252 keyname = 1
253 continue
254 if item == '=':
255 keyname = 0
256 prev_key = key[:-1].rstrip()
257 key = ''
258 dottedkey = False
259 else:
260 raise TomlDecodeError("Found invalid character in key name: '" +
261 item + "'. Try quoting the key name.",
262 original, i)
263 if item == "'" and openstrchar != '"':
264 k = 1
265 try:
266 while sl[i - k] == "'":
267 k += 1
268 if k == 3:
269 break
270 except IndexError:
271 pass
272 if k == 3:
273 multilinestr = not multilinestr
274 openstring = multilinestr
275 else:
276 openstring = not openstring
277 if openstring:
278 openstrchar = "'"
279 else:
280 openstrchar = ""
281 if item == '"' and openstrchar != "'":
282 oddbackslash = False
283 k = 1
284 tripquote = False
285 try:
286 while sl[i - k] == '"':
287 k += 1
288 if k == 3:
289 tripquote = True
290 break
291 if k == 1 or (k == 3 and tripquote):
292 while sl[i - k] == '\\':
293 oddbackslash = not oddbackslash
294 k += 1
295 except IndexError:
296 pass
297 if not oddbackslash:
298 if tripquote:
299 multilinestr = not multilinestr
300 openstring = multilinestr
301 else:
302 openstring = not openstring
303 if openstring:
304 openstrchar = '"'
305 else:
306 openstrchar = ""
307 if item == '#' and (not openstring and not keygroup and
308 not arrayoftables):
309 j = i
310 comment = ""
311 try:
312 while sl[j] != '\n':
313 comment += s[j]
314 sl[j] = ' '
315 j += 1
316 except IndexError:
317 break
318 if not openarr:
319 decoder.preserve_comment(line_no, prev_key, comment, beginline)
320 if item == '[' and (not openstring and not keygroup and
321 not arrayoftables):
322 if beginline:
323 if len(sl) > i + 1 and sl[i + 1] == '[':
324 arrayoftables = True
325 else:
326 keygroup = True
327 else:
328 openarr += 1
329 if item == ']' and not openstring:
330 if keygroup:
331 keygroup = False
332 elif arrayoftables:
333 if sl[i - 1] == ']':
334 arrayoftables = False
335 else:
336 openarr -= 1
337 if item == '\n':
338 if openstring or multilinestr:
339 if not multilinestr:
340 raise TomlDecodeError("Unbalanced quotes", original, i)
341 if ((sl[i - 1] == "'" or sl[i - 1] == '"') and (
342 sl[i - 2] == sl[i - 1])):
343 sl[i] = sl[i - 1]
344 if sl[i - 3] == sl[i - 1]:
345 sl[i - 3] = ' '
346 elif openarr:
347 sl[i] = ' '
348 else:
349 beginline = True
350 line_no += 1
351 elif beginline and sl[i] != ' ' and sl[i] != '\t':
352 beginline = False
353 if not keygroup and not arrayoftables:
354 if sl[i] == '=':
355 raise TomlDecodeError("Found empty keyname. ", original, i)
356 keyname = 1
357 key += item
358 if keyname:
359 raise TomlDecodeError("Key name found without value."
360 " Reached end of file.", original, len(s))
361 if openstring: # reached EOF and have an unterminated string
362 raise TomlDecodeError("Unterminated string found."
363 " Reached end of file.", original, len(s))
364 s = ''.join(sl)
365 s = s.split('\n')
366 multikey = None
367 multilinestr = ""
368 multibackslash = False
369 pos = 0
370 for idx, line in enumerate(s):
371 if idx > 0:
372 pos += len(s[idx - 1]) + 1
374 decoder.embed_comments(idx, currentlevel)
376 if not multilinestr or multibackslash or '\n' not in multilinestr:
377 line = line.strip()
378 if line == "" and (not multikey or multibackslash):
379 continue
380 if multikey:
381 if multibackslash:
382 multilinestr += line
383 else:
384 multilinestr += line
385 multibackslash = False
386 closed = False
387 if multilinestr[0] == '[':
388 closed = line[-1] == ']'
389 elif len(line) > 2:
390 closed = (line[-1] == multilinestr[0] and
391 line[-2] == multilinestr[0] and
392 line[-3] == multilinestr[0])
393 if closed:
394 try:
395 value, vtype = decoder.load_value(multilinestr)
396 except ValueError as err:
397 raise TomlDecodeError(str(err), original, pos)
398 currentlevel[multikey] = value
399 multikey = None
400 multilinestr = ""
401 else:
402 k = len(multilinestr) - 1
403 while k > -1 and multilinestr[k] == '\\':
404 multibackslash = not multibackslash
405 k -= 1
406 if multibackslash:
407 multilinestr = multilinestr[:-1]
408 else:
409 multilinestr += "\n"
410 continue
411 if line[0] == '[':
412 arrayoftables = False
413 if len(line) == 1:
414 raise TomlDecodeError("Opening key group bracket on line by "
415 "itself.", original, pos)
416 if line[1] == '[':
417 arrayoftables = True
418 line = line[2:]
419 splitstr = ']]'
420 else:
421 line = line[1:]
422 splitstr = ']'
423 i = 1
424 quotesplits = decoder._get_split_on_quotes(line)
425 quoted = False
426 for quotesplit in quotesplits:
427 if not quoted and splitstr in quotesplit:
428 break
429 i += quotesplit.count(splitstr)
430 quoted = not quoted
431 line = line.split(splitstr, i)
432 if len(line) < i + 1 or line[-1].strip() != "":
433 raise TomlDecodeError("Key group not on a line by itself.",
434 original, pos)
435 groups = splitstr.join(line[:-1]).split('.')
436 i = 0
437 while i < len(groups):
438 groups[i] = groups[i].strip()
439 if len(groups[i]) > 0 and (groups[i][0] == '"' or
440 groups[i][0] == "'"):
441 groupstr = groups[i]
442 j = i + 1
443 while ((not groupstr[0] == groupstr[-1]) or
444 len(groupstr) == 1):
445 j += 1
446 if j > len(groups) + 2:
447 raise TomlDecodeError("Invalid group name '" +
448 groupstr + "' Something " +
449 "went wrong.", original, pos)
450 groupstr = '.'.join(groups[i:j]).strip()
451 groups[i] = groupstr[1:-1]
452 groups[i + 1:j] = []
453 else:
454 if not _groupname_re.match(groups[i]):
455 raise TomlDecodeError("Invalid group name '" +
456 groups[i] + "'. Try quoting it.",
457 original, pos)
458 i += 1
459 currentlevel = retval
460 for i in _range(len(groups)):
461 group = groups[i]
462 if group == "":
463 raise TomlDecodeError("Can't have a keygroup with an empty "
464 "name", original, pos)
465 try:
466 currentlevel[group]
467 if i == len(groups) - 1:
468 if group in implicitgroups:
469 implicitgroups.remove(group)
470 if arrayoftables:
471 raise TomlDecodeError("An implicitly defined "
472 "table can't be an array",
473 original, pos)
474 elif arrayoftables:
475 currentlevel[group].append(decoder.get_empty_table()
476 )
477 else:
478 raise TomlDecodeError("What? " + group +
479 " already exists?" +
480 str(currentlevel),
481 original, pos)
482 except TypeError:
483 currentlevel = currentlevel[-1]
484 if group not in currentlevel:
485 currentlevel[group] = decoder.get_empty_table()
486 if i == len(groups) - 1 and arrayoftables:
487 currentlevel[group] = [decoder.get_empty_table()]
488 except KeyError:
489 if i != len(groups) - 1:
490 implicitgroups.append(group)
491 currentlevel[group] = decoder.get_empty_table()
492 if i == len(groups) - 1 and arrayoftables:
493 currentlevel[group] = [decoder.get_empty_table()]
494 currentlevel = currentlevel[group]
495 if arrayoftables:
496 try:
497 currentlevel = currentlevel[-1]
498 except KeyError:
499 pass
500 elif line[0] == "{":
501 if line[-1] != "}":
502 raise TomlDecodeError("Line breaks are not allowed in inline"
503 "objects", original, pos)
504 try:
505 decoder.load_inline_object(line, currentlevel, multikey,
506 multibackslash)
507 except ValueError as err:
508 raise TomlDecodeError(str(err), original, pos)
509 elif "=" in line:
510 try:
511 ret = decoder.load_line(line, currentlevel, multikey,
512 multibackslash)
513 except ValueError as err:
514 raise TomlDecodeError(str(err), original, pos)
515 if ret is not None:
516 multikey, multilinestr, multibackslash = ret
517 return retval
520def _load_date(val):
521 microsecond = 0
522 tz = None
523 try:
524 if len(val) > 19:
525 if val[19] == '.':
526 if val[-1].upper() == 'Z':
527 subsecondval = val[20:-1]
528 tzval = "Z"
529 else:
530 subsecondvalandtz = val[20:]
531 if '+' in subsecondvalandtz:
532 splitpoint = subsecondvalandtz.index('+')
533 subsecondval = subsecondvalandtz[:splitpoint]
534 tzval = subsecondvalandtz[splitpoint:]
535 elif '-' in subsecondvalandtz:
536 splitpoint = subsecondvalandtz.index('-')
537 subsecondval = subsecondvalandtz[:splitpoint]
538 tzval = subsecondvalandtz[splitpoint:]
539 else:
540 tzval = None
541 subsecondval = subsecondvalandtz
542 if tzval is not None:
543 tz = TomlTz(tzval)
544 microsecond = int(int(subsecondval) *
545 (10 ** (6 - len(subsecondval))))
546 else:
547 tz = TomlTz(val[19:])
548 except ValueError:
549 tz = None
550 if "-" not in val[1:]:
551 return None
552 try:
553 if len(val) == 10:
554 d = datetime.date(
555 int(val[:4]), int(val[5:7]),
556 int(val[8:10]))
557 else:
558 d = datetime.datetime(
559 int(val[:4]), int(val[5:7]),
560 int(val[8:10]), int(val[11:13]),
561 int(val[14:16]), int(val[17:19]), microsecond, tz)
562 except ValueError:
563 return None
564 return d
567def _load_unicode_escapes(v, hexbytes, prefix):
568 skip = False
569 i = len(v) - 1
570 while i > -1 and v[i] == '\\':
571 skip = not skip
572 i -= 1
573 for hx in hexbytes:
574 if skip:
575 skip = False
576 i = len(hx) - 1
577 while i > -1 and hx[i] == '\\':
578 skip = not skip
579 i -= 1
580 v += prefix
581 v += hx
582 continue
583 hxb = ""
584 i = 0
585 hxblen = 4
586 if prefix == "\\U":
587 hxblen = 8
588 hxb = ''.join(hx[i:i + hxblen]).lower()
589 if hxb.strip('0123456789abcdef'):
590 raise ValueError("Invalid escape sequence: " + hxb)
591 if hxb[0] == "d" and hxb[1].strip('01234567'):
592 raise ValueError("Invalid escape sequence: " + hxb +
593 ". Only scalar unicode points are allowed.")
594 v += unichr(int(hxb, 16))
595 v += unicode(hx[len(hxb):])
596 return v
599# Unescape TOML string values.
601# content after the \
602_escapes = ['0', 'b', 'f', 'n', 'r', 't', '"']
603# What it should be replaced by
604_escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"']
605# Used for substitution
606_escape_to_escapedchars = dict(zip(_escapes, _escapedchars))
609def _unescape(v):
610 """Unescape characters in a TOML string."""
611 i = 0
612 backslash = False
613 while i < len(v):
614 if backslash:
615 backslash = False
616 if v[i] in _escapes:
617 v = v[:i - 1] + _escape_to_escapedchars[v[i]] + v[i + 1:]
618 elif v[i] == '\\':
619 v = v[:i - 1] + v[i:]
620 elif v[i] == 'u' or v[i] == 'U':
621 i += 1
622 else:
623 raise ValueError("Reserved escape sequence used")
624 continue
625 elif v[i] == '\\':
626 backslash = True
627 i += 1
628 return v
631class InlineTableDict(object):
632 """Sentinel subclass of dict for inline tables."""
635class TomlDecoder(object):
637 def __init__(self, _dict=dict):
638 self._dict = _dict
640 def get_empty_table(self):
641 return self._dict()
643 def get_empty_inline_table(self):
644 class DynamicInlineTableDict(self._dict, InlineTableDict):
645 """Concrete sentinel subclass for inline tables.
646 It is a subclass of _dict which is passed in dynamically at load
647 time
649 It is also a subclass of InlineTableDict
650 """
652 return DynamicInlineTableDict()
654 def load_inline_object(self, line, currentlevel, multikey=False,
655 multibackslash=False):
656 candidate_groups = line[1:-1].split(",")
657 groups = []
658 if len(candidate_groups) == 1 and not candidate_groups[0].strip():
659 candidate_groups.pop()
660 while len(candidate_groups) > 0:
661 candidate_group = candidate_groups.pop(0)
662 try:
663 _, value = candidate_group.split('=', 1)
664 except ValueError:
665 raise ValueError("Invalid inline table encountered")
666 value = value.strip()
667 if ((value[0] == value[-1] and value[0] in ('"', "'")) or (
668 value[0] in '-0123456789' or
669 value in ('true', 'false') or
670 (value[0] == "[" and value[-1] == "]") or
671 (value[0] == '{' and value[-1] == '}'))):
672 groups.append(candidate_group)
673 elif len(candidate_groups) > 0:
674 candidate_groups[0] = (candidate_group + "," +
675 candidate_groups[0])
676 else:
677 raise ValueError("Invalid inline table value encountered")
678 for group in groups:
679 status = self.load_line(group, currentlevel, multikey,
680 multibackslash)
681 if status is not None:
682 break
684 def _get_split_on_quotes(self, line):
685 doublequotesplits = line.split('"')
686 quoted = False
687 quotesplits = []
688 if len(doublequotesplits) > 1 and "'" in doublequotesplits[0]:
689 singlequotesplits = doublequotesplits[0].split("'")
690 doublequotesplits = doublequotesplits[1:]
691 while len(singlequotesplits) % 2 == 0 and len(doublequotesplits):
692 singlequotesplits[-1] += '"' + doublequotesplits[0]
693 doublequotesplits = doublequotesplits[1:]
694 if "'" in singlequotesplits[-1]:
695 singlequotesplits = (singlequotesplits[:-1] +
696 singlequotesplits[-1].split("'"))
697 quotesplits += singlequotesplits
698 for doublequotesplit in doublequotesplits:
699 if quoted:
700 quotesplits.append(doublequotesplit)
701 else:
702 quotesplits += doublequotesplit.split("'")
703 quoted = not quoted
704 return quotesplits
706 def load_line(self, line, currentlevel, multikey, multibackslash):
707 i = 1
708 quotesplits = self._get_split_on_quotes(line)
709 quoted = False
710 for quotesplit in quotesplits:
711 if not quoted and '=' in quotesplit:
712 break
713 i += quotesplit.count('=')
714 quoted = not quoted
715 pair = line.split('=', i)
716 strictly_valid = _strictly_valid_num(pair[-1])
717 if _number_with_underscores.match(pair[-1]):
718 pair[-1] = pair[-1].replace('_', '')
719 while len(pair[-1]) and (pair[-1][0] != ' ' and pair[-1][0] != '\t' and
720 pair[-1][0] != "'" and pair[-1][0] != '"' and
721 pair[-1][0] != '[' and pair[-1][0] != '{' and
722 pair[-1].strip() != 'true' and
723 pair[-1].strip() != 'false'):
724 try:
725 float(pair[-1])
726 break
727 except ValueError:
728 pass
729 if _load_date(pair[-1]) is not None:
730 break
731 if TIME_RE.match(pair[-1]):
732 break
733 i += 1
734 prev_val = pair[-1]
735 pair = line.split('=', i)
736 if prev_val == pair[-1]:
737 raise ValueError("Invalid date or number")
738 if strictly_valid:
739 strictly_valid = _strictly_valid_num(pair[-1])
740 pair = ['='.join(pair[:-1]).strip(), pair[-1].strip()]
741 if '.' in pair[0]:
742 if '"' in pair[0] or "'" in pair[0]:
743 quotesplits = self._get_split_on_quotes(pair[0])
744 quoted = False
745 levels = []
746 for quotesplit in quotesplits:
747 if quoted:
748 levels.append(quotesplit)
749 else:
750 levels += [level.strip() for level in
751 quotesplit.split('.')]
752 quoted = not quoted
753 else:
754 levels = pair[0].split('.')
755 while levels[-1] == "":
756 levels = levels[:-1]
757 for level in levels[:-1]:
758 if level == "":
759 continue
760 if level not in currentlevel:
761 currentlevel[level] = self.get_empty_table()
762 currentlevel = currentlevel[level]
763 pair[0] = levels[-1].strip()
764 elif (pair[0][0] == '"' or pair[0][0] == "'") and \
765 (pair[0][-1] == pair[0][0]):
766 pair[0] = _unescape(pair[0][1:-1])
767 k, koffset = self._load_line_multiline_str(pair[1])
768 if k > -1:
769 while k > -1 and pair[1][k + koffset] == '\\':
770 multibackslash = not multibackslash
771 k -= 1
772 if multibackslash:
773 multilinestr = pair[1][:-1]
774 else:
775 multilinestr = pair[1] + "\n"
776 multikey = pair[0]
777 else:
778 value, vtype = self.load_value(pair[1], strictly_valid)
779 try:
780 currentlevel[pair[0]]
781 raise ValueError("Duplicate keys!")
782 except TypeError:
783 raise ValueError("Duplicate keys!")
784 except KeyError:
785 if multikey:
786 return multikey, multilinestr, multibackslash
787 else:
788 currentlevel[pair[0]] = value
790 def _load_line_multiline_str(self, p):
791 poffset = 0
792 if len(p) < 3:
793 return -1, poffset
794 if p[0] == '[' and (p.strip()[-1] != ']' and
795 self._load_array_isstrarray(p)):
796 newp = p[1:].strip().split(',')
797 while len(newp) > 1 and newp[-1][0] != '"' and newp[-1][0] != "'":
798 newp = newp[:-2] + [newp[-2] + ',' + newp[-1]]
799 newp = newp[-1]
800 poffset = len(p) - len(newp)
801 p = newp
802 if p[0] != '"' and p[0] != "'":
803 return -1, poffset
804 if p[1] != p[0] or p[2] != p[0]:
805 return -1, poffset
806 if len(p) > 5 and p[-1] == p[0] and p[-2] == p[0] and p[-3] == p[0]:
807 return -1, poffset
808 return len(p) - 1, poffset
810 def load_value(self, v, strictly_valid=True):
811 if not v:
812 raise ValueError("Empty value is invalid")
813 if v == 'true':
814 return (True, "bool")
815 elif v.lower() == 'true':
816 raise ValueError("Only all lowercase booleans allowed")
817 elif v == 'false':
818 return (False, "bool")
819 elif v.lower() == 'false':
820 raise ValueError("Only all lowercase booleans allowed")
821 elif v[0] == '"' or v[0] == "'":
822 quotechar = v[0]
823 testv = v[1:].split(quotechar)
824 triplequote = False
825 triplequotecount = 0
826 if len(testv) > 1 and testv[0] == '' and testv[1] == '':
827 testv = testv[2:]
828 triplequote = True
829 closed = False
830 for tv in testv:
831 if tv == '':
832 if triplequote:
833 triplequotecount += 1
834 else:
835 closed = True
836 else:
837 oddbackslash = False
838 try:
839 i = -1
840 j = tv[i]
841 while j == '\\':
842 oddbackslash = not oddbackslash
843 i -= 1
844 j = tv[i]
845 except IndexError:
846 pass
847 if not oddbackslash:
848 if closed:
849 raise ValueError("Found tokens after a closed " +
850 "string. Invalid TOML.")
851 else:
852 if not triplequote or triplequotecount > 1:
853 closed = True
854 else:
855 triplequotecount = 0
856 if quotechar == '"':
857 escapeseqs = v.split('\\')[1:]
858 backslash = False
859 for i in escapeseqs:
860 if i == '':
861 backslash = not backslash
862 else:
863 if i[0] not in _escapes and (i[0] != 'u' and
864 i[0] != 'U' and
865 not backslash):
866 raise ValueError("Reserved escape sequence used")
867 if backslash:
868 backslash = False
869 for prefix in ["\\u", "\\U"]:
870 if prefix in v:
871 hexbytes = v.split(prefix)
872 v = _load_unicode_escapes(hexbytes[0], hexbytes[1:],
873 prefix)
874 v = _unescape(v)
875 if len(v) > 1 and v[1] == quotechar and (len(v) < 3 or
876 v[1] == v[2]):
877 v = v[2:-2]
878 return (v[1:-1], "str")
879 elif v[0] == '[':
880 return (self.load_array(v), "array")
881 elif v[0] == '{':
882 inline_object = self.get_empty_inline_table()
883 self.load_inline_object(v, inline_object)
884 return (inline_object, "inline_object")
885 elif TIME_RE.match(v):
886 h, m, s, _, ms = TIME_RE.match(v).groups()
887 time = datetime.time(int(h), int(m), int(s), int(ms) if ms else 0)
888 return (time, "time")
889 else:
890 parsed_date = _load_date(v)
891 if parsed_date is not None:
892 return (parsed_date, "date")
893 if not strictly_valid:
894 raise ValueError("Weirdness with leading zeroes or "
895 "underscores in your number.")
896 itype = "int"
897 neg = False
898 if v[0] == '-':
899 neg = True
900 v = v[1:]
901 elif v[0] == '+':
902 v = v[1:]
903 v = v.replace('_', '')
904 lowerv = v.lower()
905 if '.' in v or ('x' not in v and ('e' in v or 'E' in v)):
906 if '.' in v and v.split('.', 1)[1] == '':
907 raise ValueError("This float is missing digits after "
908 "the point")
909 if v[0] not in '0123456789':
910 raise ValueError("This float doesn't have a leading "
911 "digit")
912 v = float(v)
913 itype = "float"
914 elif len(lowerv) == 3 and (lowerv == 'inf' or lowerv == 'nan'):
915 v = float(v)
916 itype = "float"
917 if itype == "int":
918 v = int(v, 0)
919 if neg:
920 return (0 - v, itype)
921 return (v, itype)
923 def bounded_string(self, s):
924 if len(s) == 0:
925 return True
926 if s[-1] != s[0]:
927 return False
928 i = -2
929 backslash = False
930 while len(s) + i > 0:
931 if s[i] == "\\":
932 backslash = not backslash
933 i -= 1
934 else:
935 break
936 return not backslash
938 def _load_array_isstrarray(self, a):
939 a = a[1:-1].strip()
940 if a != '' and (a[0] == '"' or a[0] == "'"):
941 return True
942 return False
944 def load_array(self, a):
945 atype = None
946 retval = []
947 a = a.strip()
948 if '[' not in a[1:-1] or "" != a[1:-1].split('[')[0].strip():
949 strarray = self._load_array_isstrarray(a)
950 if not a[1:-1].strip().startswith('{'):
951 a = a[1:-1].split(',')
952 else:
953 # a is an inline object, we must find the matching parenthesis
954 # to define groups
955 new_a = []
956 start_group_index = 1
957 end_group_index = 2
958 open_bracket_count = 1 if a[start_group_index] == '{' else 0
959 in_str = False
960 while end_group_index < len(a[1:]):
961 if a[end_group_index] == '"' or a[end_group_index] == "'":
962 if in_str:
963 backslash_index = end_group_index - 1
964 while (backslash_index > -1 and
965 a[backslash_index] == '\\'):
966 in_str = not in_str
967 backslash_index -= 1
968 in_str = not in_str
969 if not in_str and a[end_group_index] == '{':
970 open_bracket_count += 1
971 if in_str or a[end_group_index] != '}':
972 end_group_index += 1
973 continue
974 elif a[end_group_index] == '}' and open_bracket_count > 1:
975 open_bracket_count -= 1
976 end_group_index += 1
977 continue
979 # Increase end_group_index by 1 to get the closing bracket
980 end_group_index += 1
982 new_a.append(a[start_group_index:end_group_index])
984 # The next start index is at least after the closing
985 # bracket, a closing bracket can be followed by a comma
986 # since we are in an array.
987 start_group_index = end_group_index + 1
988 while (start_group_index < len(a[1:]) and
989 a[start_group_index] != '{'):
990 start_group_index += 1
991 end_group_index = start_group_index + 1
992 a = new_a
993 b = 0
994 if strarray:
995 while b < len(a) - 1:
996 ab = a[b].strip()
997 while (not self.bounded_string(ab) or
998 (len(ab) > 2 and
999 ab[0] == ab[1] == ab[2] and
1000 ab[-2] != ab[0] and
1001 ab[-3] != ab[0])):
1002 a[b] = a[b] + ',' + a[b + 1]
1003 ab = a[b].strip()
1004 if b < len(a) - 2:
1005 a = a[:b + 1] + a[b + 2:]
1006 else:
1007 a = a[:b + 1]
1008 b += 1
1009 else:
1010 al = list(a[1:-1])
1011 a = []
1012 openarr = 0
1013 j = 0
1014 for i in _range(len(al)):
1015 if al[i] == '[':
1016 openarr += 1
1017 elif al[i] == ']':
1018 openarr -= 1
1019 elif al[i] == ',' and not openarr:
1020 a.append(''.join(al[j:i]))
1021 j = i + 1
1022 a.append(''.join(al[j:]))
1023 for i in _range(len(a)):
1024 a[i] = a[i].strip()
1025 if a[i] != '':
1026 nval, ntype = self.load_value(a[i])
1027 if atype:
1028 if ntype != atype:
1029 raise ValueError("Not a homogeneous array")
1030 else:
1031 atype = ntype
1032 retval.append(nval)
1033 return retval
1035 def preserve_comment(self, line_no, key, comment, beginline):
1036 pass
1038 def embed_comments(self, idx, currentlevel):
1039 pass
1042class TomlPreserveCommentDecoder(TomlDecoder):
1044 def __init__(self, _dict=dict):
1045 self.saved_comments = {}
1046 super(TomlPreserveCommentDecoder, self).__init__(_dict)
1048 def preserve_comment(self, line_no, key, comment, beginline):
1049 self.saved_comments[line_no] = (key, comment, beginline)
1051 def embed_comments(self, idx, currentlevel):
1052 if idx not in self.saved_comments:
1053 return
1055 key, comment, beginline = self.saved_comments[idx]
1056 currentlevel[key] = CommentValue(currentlevel[key], comment, beginline,
1057 self._dict)