| import datetime |
| import io |
| from os import linesep |
| import re |
| import sys |
| |
| from pip._vendor.toml.tz import TomlTz |
| |
| if sys.version_info < (3,): |
| _range = xrange # noqa: F821 |
| else: |
| unicode = str |
| _range = range |
| basestring = str |
| unichr = chr |
| |
| |
| def _detect_pathlib_path(p): |
| if (3, 4) <= sys.version_info: |
| import pathlib |
| if isinstance(p, pathlib.PurePath): |
| return True |
| return False |
| |
| |
| def _ispath(p): |
| if isinstance(p, (bytes, basestring)): |
| return True |
| return _detect_pathlib_path(p) |
| |
| |
| def _getpath(p): |
| if (3, 6) <= sys.version_info: |
| import os |
| return os.fspath(p) |
| if _detect_pathlib_path(p): |
| return str(p) |
| return p |
| |
| |
| try: |
| FNFError = FileNotFoundError |
| except NameError: |
| FNFError = IOError |
| |
| |
| TIME_RE = re.compile(r"([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{3,6}))?") |
| |
| |
| class TomlDecodeError(ValueError): |
| """Base toml Exception / Error.""" |
| |
| def __init__(self, msg, doc, pos): |
| lineno = doc.count('\n', 0, pos) + 1 |
| colno = pos - doc.rfind('\n', 0, pos) |
| emsg = '{} (line {} column {} char {})'.format(msg, lineno, colno, pos) |
| ValueError.__init__(self, emsg) |
| self.msg = msg |
| self.doc = doc |
| self.pos = pos |
| self.lineno = lineno |
| self.colno = colno |
| |
| |
| # Matches a TOML number, which allows underscores for readability |
| _number_with_underscores = re.compile('([0-9])(_([0-9]))*') |
| |
| |
| class CommentValue(object): |
| def __init__(self, val, comment, beginline, _dict): |
| self.val = val |
| separator = "\n" if beginline else " " |
| self.comment = separator + comment |
| self._dict = _dict |
| |
| def __getitem__(self, key): |
| return self.val[key] |
| |
| def __setitem__(self, key, value): |
| self.val[key] = value |
| |
| def dump(self, dump_value_func): |
| retstr = dump_value_func(self.val) |
| if isinstance(self.val, self._dict): |
| return self.comment + "\n" + unicode(retstr) |
| else: |
| return unicode(retstr) + self.comment |
| |
| |
| def _strictly_valid_num(n): |
| n = n.strip() |
| if not n: |
| return False |
| if n[0] == '_': |
| return False |
| if n[-1] == '_': |
| return False |
| if "_." in n or "._" in n: |
| return False |
| if len(n) == 1: |
| return True |
| if n[0] == '0' and n[1] not in ['.', 'o', 'b', 'x']: |
| return False |
| if n[0] == '+' or n[0] == '-': |
| n = n[1:] |
| if len(n) > 1 and n[0] == '0' and n[1] != '.': |
| return False |
| if '__' in n: |
| return False |
| return True |
| |
| |
| def load(f, _dict=dict, decoder=None): |
| """Parses named file or files as toml and returns a dictionary |
| |
| Args: |
| f: Path to the file to open, array of files to read into single dict |
| or a file descriptor |
| _dict: (optional) Specifies the class of the returned toml dictionary |
| decoder: The decoder to use |
| |
| Returns: |
| Parsed toml file represented as a dictionary |
| |
| Raises: |
| TypeError -- When f is invalid type |
| TomlDecodeError: Error while decoding toml |
| IOError / FileNotFoundError -- When an array with no valid (existing) |
| (Python 2 / Python 3) file paths is passed |
| """ |
| |
| if _ispath(f): |
| with io.open(_getpath(f), encoding='utf-8') as ffile: |
| return loads(ffile.read(), _dict, decoder) |
| elif isinstance(f, list): |
| from os import path as op |
| from warnings import warn |
| if not [path for path in f if op.exists(path)]: |
| error_msg = "Load expects a list to contain filenames only." |
| error_msg += linesep |
| error_msg += ("The list needs to contain the path of at least one " |
| "existing file.") |
| raise FNFError(error_msg) |
| if decoder is None: |
| decoder = TomlDecoder(_dict) |
| d = decoder.get_empty_table() |
| for l in f: # noqa: E741 |
| if op.exists(l): |
| d.update(load(l, _dict, decoder)) |
| else: |
| warn("Non-existent filename in list with at least one valid " |
| "filename") |
| return d |
| else: |
| try: |
| return loads(f.read(), _dict, decoder) |
| except AttributeError: |
| raise TypeError("You can only load a file descriptor, filename or " |
| "list") |
| |
| |
| _groupname_re = re.compile(r'^[A-Za-z0-9_-]+$') |
| |
| |
| def loads(s, _dict=dict, decoder=None): |
| """Parses string as toml |
| |
| Args: |
| s: String to be parsed |
| _dict: (optional) Specifies the class of the returned toml dictionary |
| |
| Returns: |
| Parsed toml file represented as a dictionary |
| |
| Raises: |
| TypeError: When a non-string is passed |
| TomlDecodeError: Error while decoding toml |
| """ |
| |
| implicitgroups = [] |
| if decoder is None: |
| decoder = TomlDecoder(_dict) |
| retval = decoder.get_empty_table() |
| currentlevel = retval |
| if not isinstance(s, basestring): |
| raise TypeError("Expecting something like a string") |
| |
| if not isinstance(s, unicode): |
| s = s.decode('utf8') |
| |
| original = s |
| sl = list(s) |
| openarr = 0 |
| openstring = False |
| openstrchar = "" |
| multilinestr = False |
| arrayoftables = False |
| beginline = True |
| keygroup = False |
| dottedkey = False |
| keyname = 0 |
| key = '' |
| prev_key = '' |
| line_no = 1 |
| |
| for i, item in enumerate(sl): |
| if item == '\r' and sl[i + 1] == '\n': |
| sl[i] = ' ' |
| continue |
| if keyname: |
| key += item |
| if item == '\n': |
| raise TomlDecodeError("Key name found without value." |
| " Reached end of line.", original, i) |
| if openstring: |
| if item == openstrchar: |
| oddbackslash = False |
| k = 1 |
| while i >= k and sl[i - k] == '\\': |
| oddbackslash = not oddbackslash |
| k += 1 |
| if not oddbackslash: |
| keyname = 2 |
| openstring = False |
| openstrchar = "" |
| continue |
| elif keyname == 1: |
| if item.isspace(): |
| keyname = 2 |
| continue |
| elif item == '.': |
| dottedkey = True |
| continue |
| elif item.isalnum() or item == '_' or item == '-': |
| continue |
| elif (dottedkey and sl[i - 1] == '.' and |
| (item == '"' or item == "'")): |
| openstring = True |
| openstrchar = item |
| continue |
| elif keyname == 2: |
| if item.isspace(): |
| if dottedkey: |
| nextitem = sl[i + 1] |
| if not nextitem.isspace() and nextitem != '.': |
| keyname = 1 |
| continue |
| if item == '.': |
| dottedkey = True |
| nextitem = sl[i + 1] |
| if not nextitem.isspace() and nextitem != '.': |
| keyname = 1 |
| continue |
| if item == '=': |
| keyname = 0 |
| prev_key = key[:-1].rstrip() |
| key = '' |
| dottedkey = False |
| else: |
| raise TomlDecodeError("Found invalid character in key name: '" + |
| item + "'. Try quoting the key name.", |
| original, i) |
| if item == "'" and openstrchar != '"': |
| k = 1 |
| try: |
| while sl[i - k] == "'": |
| k += 1 |
| if k == 3: |
| break |
| except IndexError: |
| pass |
| if k == 3: |
| multilinestr = not multilinestr |
| openstring = multilinestr |
| else: |
| openstring = not openstring |
| if openstring: |
| openstrchar = "'" |
| else: |
| openstrchar = "" |
| if item == '"' and openstrchar != "'": |
| oddbackslash = False |
| k = 1 |
| tripquote = False |
| try: |
| while sl[i - k] == '"': |
| k += 1 |
| if k == 3: |
| tripquote = True |
| break |
| if k == 1 or (k == 3 and tripquote): |
| while sl[i - k] == '\\': |
| oddbackslash = not oddbackslash |
| k += 1 |
| except IndexError: |
| pass |
| if not oddbackslash: |
| if tripquote: |
| multilinestr = not multilinestr |
| openstring = multilinestr |
| else: |
| openstring = not openstring |
| if openstring: |
| openstrchar = '"' |
| else: |
| openstrchar = "" |
| if item == '#' and (not openstring and not keygroup and |
| not arrayoftables): |
| j = i |
| comment = "" |
| try: |
| while sl[j] != '\n': |
| comment += s[j] |
| sl[j] = ' ' |
| j += 1 |
| except IndexError: |
| break |
| if not openarr: |
| decoder.preserve_comment(line_no, prev_key, comment, beginline) |
| if item == '[' and (not openstring and not keygroup and |
| not arrayoftables): |
| if beginline: |
| if len(sl) > i + 1 and sl[i + 1] == '[': |
| arrayoftables = True |
| else: |
| keygroup = True |
| else: |
| openarr += 1 |
| if item == ']' and not openstring: |
| if keygroup: |
| keygroup = False |
| elif arrayoftables: |
| if sl[i - 1] == ']': |
| arrayoftables = False |
| else: |
| openarr -= 1 |
| if item == '\n': |
| if openstring or multilinestr: |
| if not multilinestr: |
| raise TomlDecodeError("Unbalanced quotes", original, i) |
| if ((sl[i - 1] == "'" or sl[i - 1] == '"') and ( |
| sl[i - 2] == sl[i - 1])): |
| sl[i] = sl[i - 1] |
| if sl[i - 3] == sl[i - 1]: |
| sl[i - 3] = ' ' |
| elif openarr: |
| sl[i] = ' ' |
| else: |
| beginline = True |
| line_no += 1 |
| elif beginline and sl[i] != ' ' and sl[i] != '\t': |
| beginline = False |
| if not keygroup and not arrayoftables: |
| if sl[i] == '=': |
| raise TomlDecodeError("Found empty keyname. ", original, i) |
| keyname = 1 |
| key += item |
| if keyname: |
| raise TomlDecodeError("Key name found without value." |
| " Reached end of file.", original, len(s)) |
| if openstring: # reached EOF and have an unterminated string |
| raise TomlDecodeError("Unterminated string found." |
| " Reached end of file.", original, len(s)) |
| s = ''.join(sl) |
| s = s.split('\n') |
| multikey = None |
| multilinestr = "" |
| multibackslash = False |
| pos = 0 |
| for idx, line in enumerate(s): |
| if idx > 0: |
| pos += len(s[idx - 1]) + 1 |
| |
| decoder.embed_comments(idx, currentlevel) |
| |
| if not multilinestr or multibackslash or '\n' not in multilinestr: |
| line = line.strip() |
| if line == "" and (not multikey or multibackslash): |
| continue |
| if multikey: |
| if multibackslash: |
| multilinestr += line |
| else: |
| multilinestr += line |
| multibackslash = False |
| closed = False |
| if multilinestr[0] == '[': |
| closed = line[-1] == ']' |
| elif len(line) > 2: |
| closed = (line[-1] == multilinestr[0] and |
| line[-2] == multilinestr[0] and |
| line[-3] == multilinestr[0]) |
| if closed: |
| try: |
| value, vtype = decoder.load_value(multilinestr) |
| except ValueError as err: |
| raise TomlDecodeError(str(err), original, pos) |
| currentlevel[multikey] = value |
| multikey = None |
| multilinestr = "" |
| else: |
| k = len(multilinestr) - 1 |
| while k > -1 and multilinestr[k] == '\\': |
| multibackslash = not multibackslash |
| k -= 1 |
| if multibackslash: |
| multilinestr = multilinestr[:-1] |
| else: |
| multilinestr += "\n" |
| continue |
| if line[0] == '[': |
| arrayoftables = False |
| if len(line) == 1: |
| raise TomlDecodeError("Opening key group bracket on line by " |
| "itself.", original, pos) |
| if line[1] == '[': |
| arrayoftables = True |
| line = line[2:] |
| splitstr = ']]' |
| else: |
| line = line[1:] |
| splitstr = ']' |
| i = 1 |
| quotesplits = decoder._get_split_on_quotes(line) |
| quoted = False |
| for quotesplit in quotesplits: |
| if not quoted and splitstr in quotesplit: |
| break |
| i += quotesplit.count(splitstr) |
| quoted = not quoted |
| line = line.split(splitstr, i) |
| if len(line) < i + 1 or line[-1].strip() != "": |
| raise TomlDecodeError("Key group not on a line by itself.", |
| original, pos) |
| groups = splitstr.join(line[:-1]).split('.') |
| i = 0 |
| while i < len(groups): |
| groups[i] = groups[i].strip() |
| if len(groups[i]) > 0 and (groups[i][0] == '"' or |
| groups[i][0] == "'"): |
| groupstr = groups[i] |
| j = i + 1 |
| while not groupstr[0] == groupstr[-1]: |
| j += 1 |
| if j > len(groups) + 2: |
| raise TomlDecodeError("Invalid group name '" + |
| groupstr + "' Something " + |
| "went wrong.", original, pos) |
| groupstr = '.'.join(groups[i:j]).strip() |
| groups[i] = groupstr[1:-1] |
| groups[i + 1:j] = [] |
| else: |
| if not _groupname_re.match(groups[i]): |
| raise TomlDecodeError("Invalid group name '" + |
| groups[i] + "'. Try quoting it.", |
| original, pos) |
| i += 1 |
| currentlevel = retval |
| for i in _range(len(groups)): |
| group = groups[i] |
| if group == "": |
| raise TomlDecodeError("Can't have a keygroup with an empty " |
| "name", original, pos) |
| try: |
| currentlevel[group] |
| if i == len(groups) - 1: |
| if group in implicitgroups: |
| implicitgroups.remove(group) |
| if arrayoftables: |
| raise TomlDecodeError("An implicitly defined " |
| "table can't be an array", |
| original, pos) |
| elif arrayoftables: |
| currentlevel[group].append(decoder.get_empty_table() |
| ) |
| else: |
| raise TomlDecodeError("What? " + group + |
| " already exists?" + |
| str(currentlevel), |
| original, pos) |
| except TypeError: |
| currentlevel = currentlevel[-1] |
| if group not in currentlevel: |
| currentlevel[group] = decoder.get_empty_table() |
| if i == len(groups) - 1 and arrayoftables: |
| currentlevel[group] = [decoder.get_empty_table()] |
| except KeyError: |
| if i != len(groups) - 1: |
| implicitgroups.append(group) |
| currentlevel[group] = decoder.get_empty_table() |
| if i == len(groups) - 1 and arrayoftables: |
| currentlevel[group] = [decoder.get_empty_table()] |
| currentlevel = currentlevel[group] |
| if arrayoftables: |
| try: |
| currentlevel = currentlevel[-1] |
| except KeyError: |
| pass |
| elif line[0] == "{": |
| if line[-1] != "}": |
| raise TomlDecodeError("Line breaks are not allowed in inline" |
| "objects", original, pos) |
| try: |
| decoder.load_inline_object(line, currentlevel, multikey, |
| multibackslash) |
| except ValueError as err: |
| raise TomlDecodeError(str(err), original, pos) |
| elif "=" in line: |
| try: |
| ret = decoder.load_line(line, currentlevel, multikey, |
| multibackslash) |
| except ValueError as err: |
| raise TomlDecodeError(str(err), original, pos) |
| if ret is not None: |
| multikey, multilinestr, multibackslash = ret |
| return retval |
| |
| |
| def _load_date(val): |
| microsecond = 0 |
| tz = None |
| try: |
| if len(val) > 19: |
| if val[19] == '.': |
| if val[-1].upper() == 'Z': |
| subsecondval = val[20:-1] |
| tzval = "Z" |
| else: |
| subsecondvalandtz = val[20:] |
| if '+' in subsecondvalandtz: |
| splitpoint = subsecondvalandtz.index('+') |
| subsecondval = subsecondvalandtz[:splitpoint] |
| tzval = subsecondvalandtz[splitpoint:] |
| elif '-' in subsecondvalandtz: |
| splitpoint = subsecondvalandtz.index('-') |
| subsecondval = subsecondvalandtz[:splitpoint] |
| tzval = subsecondvalandtz[splitpoint:] |
| else: |
| tzval = None |
| subsecondval = subsecondvalandtz |
| if tzval is not None: |
| tz = TomlTz(tzval) |
| microsecond = int(int(subsecondval) * |
| (10 ** (6 - len(subsecondval)))) |
| else: |
| tz = TomlTz(val[19:]) |
| except ValueError: |
| tz = None |
| if "-" not in val[1:]: |
| return None |
| try: |
| if len(val) == 10: |
| d = datetime.date( |
| int(val[:4]), int(val[5:7]), |
| int(val[8:10])) |
| else: |
| d = datetime.datetime( |
| int(val[:4]), int(val[5:7]), |
| int(val[8:10]), int(val[11:13]), |
| int(val[14:16]), int(val[17:19]), microsecond, tz) |
| except ValueError: |
| return None |
| return d |
| |
| |
| def _load_unicode_escapes(v, hexbytes, prefix): |
| skip = False |
| i = len(v) - 1 |
| while i > -1 and v[i] == '\\': |
| skip = not skip |
| i -= 1 |
| for hx in hexbytes: |
| if skip: |
| skip = False |
| i = len(hx) - 1 |
| while i > -1 and hx[i] == '\\': |
| skip = not skip |
| i -= 1 |
| v += prefix |
| v += hx |
| continue |
| hxb = "" |
| i = 0 |
| hxblen = 4 |
| if prefix == "\\U": |
| hxblen = 8 |
| hxb = ''.join(hx[i:i + hxblen]).lower() |
| if hxb.strip('0123456789abcdef'): |
| raise ValueError("Invalid escape sequence: " + hxb) |
| if hxb[0] == "d" and hxb[1].strip('01234567'): |
| raise ValueError("Invalid escape sequence: " + hxb + |
| ". Only scalar unicode points are allowed.") |
| v += unichr(int(hxb, 16)) |
| v += unicode(hx[len(hxb):]) |
| return v |
| |
| |
| # Unescape TOML string values. |
| |
| # content after the \ |
| _escapes = ['0', 'b', 'f', 'n', 'r', 't', '"'] |
| # What it should be replaced by |
| _escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"'] |
| # Used for substitution |
| _escape_to_escapedchars = dict(zip(_escapes, _escapedchars)) |
| |
| |
| def _unescape(v): |
| """Unescape characters in a TOML string.""" |
| i = 0 |
| backslash = False |
| while i < len(v): |
| if backslash: |
| backslash = False |
| if v[i] in _escapes: |
| v = v[:i - 1] + _escape_to_escapedchars[v[i]] + v[i + 1:] |
| elif v[i] == '\\': |
| v = v[:i - 1] + v[i:] |
| elif v[i] == 'u' or v[i] == 'U': |
| i += 1 |
| else: |
| raise ValueError("Reserved escape sequence used") |
| continue |
| elif v[i] == '\\': |
| backslash = True |
| i += 1 |
| return v |
| |
| |
| class InlineTableDict(object): |
| """Sentinel subclass of dict for inline tables.""" |
| |
| |
| class TomlDecoder(object): |
| |
| def __init__(self, _dict=dict): |
| self._dict = _dict |
| |
| def get_empty_table(self): |
| return self._dict() |
| |
| def get_empty_inline_table(self): |
| class DynamicInlineTableDict(self._dict, InlineTableDict): |
| """Concrete sentinel subclass for inline tables. |
| It is a subclass of _dict which is passed in dynamically at load |
| time |
| |
| It is also a subclass of InlineTableDict |
| """ |
| |
| return DynamicInlineTableDict() |
| |
| def load_inline_object(self, line, currentlevel, multikey=False, |
| multibackslash=False): |
| candidate_groups = line[1:-1].split(",") |
| groups = [] |
| if len(candidate_groups) == 1 and not candidate_groups[0].strip(): |
| candidate_groups.pop() |
| while len(candidate_groups) > 0: |
| candidate_group = candidate_groups.pop(0) |
| try: |
| _, value = candidate_group.split('=', 1) |
| except ValueError: |
| raise ValueError("Invalid inline table encountered") |
| value = value.strip() |
| if ((value[0] == value[-1] and value[0] in ('"', "'")) or ( |
| value[0] in '-0123456789' or |
| value in ('true', 'false') or |
| (value[0] == "[" and value[-1] == "]") or |
| (value[0] == '{' and value[-1] == '}'))): |
| groups.append(candidate_group) |
| elif len(candidate_groups) > 0: |
| candidate_groups[0] = (candidate_group + "," + |
| candidate_groups[0]) |
| else: |
| raise ValueError("Invalid inline table value encountered") |
| for group in groups: |
| status = self.load_line(group, currentlevel, multikey, |
| multibackslash) |
| if status is not None: |
| break |
| |
| def _get_split_on_quotes(self, line): |
| doublequotesplits = line.split('"') |
| quoted = False |
| quotesplits = [] |
| if len(doublequotesplits) > 1 and "'" in doublequotesplits[0]: |
| singlequotesplits = doublequotesplits[0].split("'") |
| doublequotesplits = doublequotesplits[1:] |
| while len(singlequotesplits) % 2 == 0 and len(doublequotesplits): |
| singlequotesplits[-1] += '"' + doublequotesplits[0] |
| doublequotesplits = doublequotesplits[1:] |
| if "'" in singlequotesplits[-1]: |
| singlequotesplits = (singlequotesplits[:-1] + |
| singlequotesplits[-1].split("'")) |
| quotesplits += singlequotesplits |
| for doublequotesplit in doublequotesplits: |
| if quoted: |
| quotesplits.append(doublequotesplit) |
| else: |
| quotesplits += doublequotesplit.split("'") |
| quoted = not quoted |
| return quotesplits |
| |
| def load_line(self, line, currentlevel, multikey, multibackslash): |
| i = 1 |
| quotesplits = self._get_split_on_quotes(line) |
| quoted = False |
| for quotesplit in quotesplits: |
| if not quoted and '=' in quotesplit: |
| break |
| i += quotesplit.count('=') |
| quoted = not quoted |
| pair = line.split('=', i) |
| strictly_valid = _strictly_valid_num(pair[-1]) |
| if _number_with_underscores.match(pair[-1]): |
| pair[-1] = pair[-1].replace('_', '') |
| while len(pair[-1]) and (pair[-1][0] != ' ' and pair[-1][0] != '\t' and |
| pair[-1][0] != "'" and pair[-1][0] != '"' and |
| pair[-1][0] != '[' and pair[-1][0] != '{' and |
| pair[-1].strip() != 'true' and |
| pair[-1].strip() != 'false'): |
| try: |
| float(pair[-1]) |
| break |
| except ValueError: |
| pass |
| if _load_date(pair[-1]) is not None: |
| break |
| if TIME_RE.match(pair[-1]): |
| break |
| i += 1 |
| prev_val = pair[-1] |
| pair = line.split('=', i) |
| if prev_val == pair[-1]: |
| raise ValueError("Invalid date or number") |
| if strictly_valid: |
| strictly_valid = _strictly_valid_num(pair[-1]) |
| pair = ['='.join(pair[:-1]).strip(), pair[-1].strip()] |
| if '.' in pair[0]: |
| if '"' in pair[0] or "'" in pair[0]: |
| quotesplits = self._get_split_on_quotes(pair[0]) |
| quoted = False |
| levels = [] |
| for quotesplit in quotesplits: |
| if quoted: |
| levels.append(quotesplit) |
| else: |
| levels += [level.strip() for level in |
| quotesplit.split('.')] |
| quoted = not quoted |
| else: |
| levels = pair[0].split('.') |
| while levels[-1] == "": |
| levels = levels[:-1] |
| for level in levels[:-1]: |
| if level == "": |
| continue |
| if level not in currentlevel: |
| currentlevel[level] = self.get_empty_table() |
| currentlevel = currentlevel[level] |
| pair[0] = levels[-1].strip() |
| elif (pair[0][0] == '"' or pair[0][0] == "'") and \ |
| (pair[0][-1] == pair[0][0]): |
| pair[0] = _unescape(pair[0][1:-1]) |
| k, koffset = self._load_line_multiline_str(pair[1]) |
| if k > -1: |
| while k > -1 and pair[1][k + koffset] == '\\': |
| multibackslash = not multibackslash |
| k -= 1 |
| if multibackslash: |
| multilinestr = pair[1][:-1] |
| else: |
| multilinestr = pair[1] + "\n" |
| multikey = pair[0] |
| else: |
| value, vtype = self.load_value(pair[1], strictly_valid) |
| try: |
| currentlevel[pair[0]] |
| raise ValueError("Duplicate keys!") |
| except TypeError: |
| raise ValueError("Duplicate keys!") |
| except KeyError: |
| if multikey: |
| return multikey, multilinestr, multibackslash |
| else: |
| currentlevel[pair[0]] = value |
| |
| def _load_line_multiline_str(self, p): |
| poffset = 0 |
| if len(p) < 3: |
| return -1, poffset |
| if p[0] == '[' and (p.strip()[-1] != ']' and |
| self._load_array_isstrarray(p)): |
| newp = p[1:].strip().split(',') |
| while len(newp) > 1 and newp[-1][0] != '"' and newp[-1][0] != "'": |
| newp = newp[:-2] + [newp[-2] + ',' + newp[-1]] |
| newp = newp[-1] |
| poffset = len(p) - len(newp) |
| p = newp |
| if p[0] != '"' and p[0] != "'": |
| return -1, poffset |
| if p[1] != p[0] or p[2] != p[0]: |
| return -1, poffset |
| if len(p) > 5 and p[-1] == p[0] and p[-2] == p[0] and p[-3] == p[0]: |
| return -1, poffset |
| return len(p) - 1, poffset |
| |
| def load_value(self, v, strictly_valid=True): |
| if not v: |
| raise ValueError("Empty value is invalid") |
| if v == 'true': |
| return (True, "bool") |
| elif v == 'false': |
| return (False, "bool") |
| elif v[0] == '"' or v[0] == "'": |
| quotechar = v[0] |
| testv = v[1:].split(quotechar) |
| triplequote = False |
| triplequotecount = 0 |
| if len(testv) > 1 and testv[0] == '' and testv[1] == '': |
| testv = testv[2:] |
| triplequote = True |
| closed = False |
| for tv in testv: |
| if tv == '': |
| if triplequote: |
| triplequotecount += 1 |
| else: |
| closed = True |
| else: |
| oddbackslash = False |
| try: |
| i = -1 |
| j = tv[i] |
| while j == '\\': |
| oddbackslash = not oddbackslash |
| i -= 1 |
| j = tv[i] |
| except IndexError: |
| pass |
| if not oddbackslash: |
| if closed: |
| raise ValueError("Found tokens after a closed " + |
| "string. Invalid TOML.") |
| else: |
| if not triplequote or triplequotecount > 1: |
| closed = True |
| else: |
| triplequotecount = 0 |
| if quotechar == '"': |
| escapeseqs = v.split('\\')[1:] |
| backslash = False |
| for i in escapeseqs: |
| if i == '': |
| backslash = not backslash |
| else: |
| if i[0] not in _escapes and (i[0] != 'u' and |
| i[0] != 'U' and |
| not backslash): |
| raise ValueError("Reserved escape sequence used") |
| if backslash: |
| backslash = False |
| for prefix in ["\\u", "\\U"]: |
| if prefix in v: |
| hexbytes = v.split(prefix) |
| v = _load_unicode_escapes(hexbytes[0], hexbytes[1:], |
| prefix) |
| v = _unescape(v) |
| if len(v) > 1 and v[1] == quotechar and (len(v) < 3 or |
| v[1] == v[2]): |
| v = v[2:-2] |
| return (v[1:-1], "str") |
| elif v[0] == '[': |
| return (self.load_array(v), "array") |
| elif v[0] == '{': |
| inline_object = self.get_empty_inline_table() |
| self.load_inline_object(v, inline_object) |
| return (inline_object, "inline_object") |
| elif TIME_RE.match(v): |
| h, m, s, _, ms = TIME_RE.match(v).groups() |
| time = datetime.time(int(h), int(m), int(s), int(ms) if ms else 0) |
| return (time, "time") |
| else: |
| parsed_date = _load_date(v) |
| if parsed_date is not None: |
| return (parsed_date, "date") |
| if not strictly_valid: |
| raise ValueError("Weirdness with leading zeroes or " |
| "underscores in your number.") |
| itype = "int" |
| neg = False |
| if v[0] == '-': |
| neg = True |
| v = v[1:] |
| elif v[0] == '+': |
| v = v[1:] |
| v = v.replace('_', '') |
| lowerv = v.lower() |
| if '.' in v or ('x' not in v and ('e' in v or 'E' in v)): |
| if '.' in v and v.split('.', 1)[1] == '': |
| raise ValueError("This float is missing digits after " |
| "the point") |
| if v[0] not in '0123456789': |
| raise ValueError("This float doesn't have a leading " |
| "digit") |
| v = float(v) |
| itype = "float" |
| elif len(lowerv) == 3 and (lowerv == 'inf' or lowerv == 'nan'): |
| v = float(v) |
| itype = "float" |
| if itype == "int": |
| v = int(v, 0) |
| if neg: |
| return (0 - v, itype) |
| return (v, itype) |
| |
| def bounded_string(self, s): |
| if len(s) == 0: |
| return True |
| if s[-1] != s[0]: |
| return False |
| i = -2 |
| backslash = False |
| while len(s) + i > 0: |
| if s[i] == "\\": |
| backslash = not backslash |
| i -= 1 |
| else: |
| break |
| return not backslash |
| |
| def _load_array_isstrarray(self, a): |
| a = a[1:-1].strip() |
| if a != '' and (a[0] == '"' or a[0] == "'"): |
| return True |
| return False |
| |
| def load_array(self, a): |
| atype = None |
| retval = [] |
| a = a.strip() |
| if '[' not in a[1:-1] or "" != a[1:-1].split('[')[0].strip(): |
| strarray = self._load_array_isstrarray(a) |
| if not a[1:-1].strip().startswith('{'): |
| a = a[1:-1].split(',') |
| else: |
| # a is an inline object, we must find the matching parenthesis |
| # to define groups |
| new_a = [] |
| start_group_index = 1 |
| end_group_index = 2 |
| open_bracket_count = 1 if a[start_group_index] == '{' else 0 |
| in_str = False |
| while end_group_index < len(a[1:]): |
| if a[end_group_index] == '"' or a[end_group_index] == "'": |
| if in_str: |
| backslash_index = end_group_index - 1 |
| while (backslash_index > -1 and |
| a[backslash_index] == '\\'): |
| in_str = not in_str |
| backslash_index -= 1 |
| in_str = not in_str |
| if not in_str and a[end_group_index] == '{': |
| open_bracket_count += 1 |
| if in_str or a[end_group_index] != '}': |
| end_group_index += 1 |
| continue |
| elif a[end_group_index] == '}' and open_bracket_count > 1: |
| open_bracket_count -= 1 |
| end_group_index += 1 |
| continue |
| |
| # Increase end_group_index by 1 to get the closing bracket |
| end_group_index += 1 |
| |
| new_a.append(a[start_group_index:end_group_index]) |
| |
| # The next start index is at least after the closing |
| # bracket, a closing bracket can be followed by a comma |
| # since we are in an array. |
| start_group_index = end_group_index + 1 |
| while (start_group_index < len(a[1:]) and |
| a[start_group_index] != '{'): |
| start_group_index += 1 |
| end_group_index = start_group_index + 1 |
| a = new_a |
| b = 0 |
| if strarray: |
| while b < len(a) - 1: |
| ab = a[b].strip() |
| while (not self.bounded_string(ab) or |
| (len(ab) > 2 and |
| ab[0] == ab[1] == ab[2] and |
| ab[-2] != ab[0] and |
| ab[-3] != ab[0])): |
| a[b] = a[b] + ',' + a[b + 1] |
| ab = a[b].strip() |
| if b < len(a) - 2: |
| a = a[:b + 1] + a[b + 2:] |
| else: |
| a = a[:b + 1] |
| b += 1 |
| else: |
| al = list(a[1:-1]) |
| a = [] |
| openarr = 0 |
| j = 0 |
| for i in _range(len(al)): |
| if al[i] == '[': |
| openarr += 1 |
| elif al[i] == ']': |
| openarr -= 1 |
| elif al[i] == ',' and not openarr: |
| a.append(''.join(al[j:i])) |
| j = i + 1 |
| a.append(''.join(al[j:])) |
| for i in _range(len(a)): |
| a[i] = a[i].strip() |
| if a[i] != '': |
| nval, ntype = self.load_value(a[i]) |
| if atype: |
| if ntype != atype: |
| raise ValueError("Not a homogeneous array") |
| else: |
| atype = ntype |
| retval.append(nval) |
| return retval |
| |
| def preserve_comment(self, line_no, key, comment, beginline): |
| pass |
| |
| def embed_comments(self, idx, currentlevel): |
| pass |
| |
| |
| class TomlPreserveCommentDecoder(TomlDecoder): |
| |
| def __init__(self, _dict=dict): |
| self.saved_comments = {} |
| super(TomlPreserveCommentDecoder, self).__init__(_dict) |
| |
| def preserve_comment(self, line_no, key, comment, beginline): |
| self.saved_comments[line_no] = (key, comment, beginline) |
| |
| def embed_comments(self, idx, currentlevel): |
| if idx not in self.saved_comments: |
| return |
| |
| key, comment, beginline = self.saved_comments[idx] |
| currentlevel[key] = CommentValue(currentlevel[key], comment, beginline, |
| self._dict) |