diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/python/toml | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
28 files changed, 6955 insertions, 0 deletions
diff --git a/third_party/python/toml/toml-0.10.2.dist-info/LICENSE b/third_party/python/toml/toml-0.10.2.dist-info/LICENSE new file mode 100644 index 0000000000..5010e3075e --- /dev/null +++ b/third_party/python/toml/toml-0.10.2.dist-info/LICENSE @@ -0,0 +1,27 @@ +The MIT License + +Copyright 2013-2019 William Pearson +Copyright 2015-2016 Julien Enselme +Copyright 2016 Google Inc. +Copyright 2017 Samuel Vasko +Copyright 2017 Nate Prewitt +Copyright 2017 Jack Evans +Copyright 2019 Filippo Broggini + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.
\ No newline at end of file diff --git a/third_party/python/toml/toml-0.10.2.dist-info/METADATA b/third_party/python/toml/toml-0.10.2.dist-info/METADATA new file mode 100644 index 0000000000..6f2635ce4d --- /dev/null +++ b/third_party/python/toml/toml-0.10.2.dist-info/METADATA @@ -0,0 +1,255 @@ +Metadata-Version: 2.1 +Name: toml +Version: 0.10.2 +Summary: Python Library for Tom's Obvious, Minimal Language +Home-page: https://github.com/uiri/toml +Author: William Pearson +Author-email: uiri@xqz.ca +License: MIT +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.* + +**** +TOML +**** + +.. image:: https://img.shields.io/pypi/v/toml + :target: https://pypi.org/project/toml/ + +.. image:: https://travis-ci.org/uiri/toml.svg?branch=master + :target: https://travis-ci.org/uiri/toml + +.. image:: https://img.shields.io/pypi/pyversions/toml.svg + :target: https://pypi.org/project/toml/ + + +A Python library for parsing and creating `TOML <https://en.wikipedia.org/wiki/TOML>`_. + +The module passes `the TOML test suite <https://github.com/BurntSushi/toml-test>`_. + +See also: + +* `The TOML Standard <https://github.com/toml-lang/toml>`_ +* `The currently supported TOML specification <https://github.com/toml-lang/toml/blob/v0.5.0/README.md>`_ + +Installation +============ + +To install the latest release on `PyPI <https://pypi.org/project/toml/>`_, +simply run: + +:: + + pip install toml + +Or to install the latest development version, run: + +:: + + git clone https://github.com/uiri/toml.git + cd toml + python setup.py install + +Quick Tutorial +============== + +*toml.loads* takes in a string containing standard TOML-formatted data and +returns a dictionary containing the parsed data. + +.. code:: pycon + + >>> import toml + >>> toml_string = """ + ... # This is a TOML document. + ... + ... title = "TOML Example" + ... + ... [owner] + ... name = "Tom Preston-Werner" + ... dob = 1979-05-27T07:32:00-08:00 # First class dates + ... + ... [database] + ... server = "192.168.1.1" + ... ports = [ 8001, 8001, 8002 ] + ... connection_max = 5000 + ... enabled = true + ... + ... [servers] + ... + ... # Indentation (tabs and/or spaces) is allowed but not required + ... [servers.alpha] + ... ip = "10.0.0.1" + ... dc = "eqdc10" + ... + ... [servers.beta] + ... ip = "10.0.0.2" + ... dc = "eqdc10" + ... + ... [clients] + ... data = [ ["gamma", "delta"], [1, 2] ] + ... + ... # Line breaks are OK when inside arrays + ... hosts = [ + ... "alpha", + ... "omega" + ... ] + ... """ + >>> parsed_toml = toml.loads(toml_string) + + +*toml.dumps* takes a dictionary and returns a string containing the +corresponding TOML-formatted data. + +.. code:: pycon + + >>> new_toml_string = toml.dumps(parsed_toml) + >>> print(new_toml_string) + title = "TOML Example" + [owner] + name = "Tom Preston-Werner" + dob = 1979-05-27T07:32:00Z + [database] + server = "192.168.1.1" + ports = [ 8001, 8001, 8002,] + connection_max = 5000 + enabled = true + [clients] + data = [ [ "gamma", "delta",], [ 1, 2,],] + hosts = [ "alpha", "omega",] + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +*toml.dump* takes a dictionary and a file descriptor and returns a string containing the +corresponding TOML-formatted data. + +.. code:: pycon + + >>> with open('new_toml_file.toml', 'w') as f: + ... new_toml_string = toml.dump(parsed_toml, f) + >>> print(new_toml_string) + title = "TOML Example" + [owner] + name = "Tom Preston-Werner" + dob = 1979-05-27T07:32:00Z + [database] + server = "192.168.1.1" + ports = [ 8001, 8001, 8002,] + connection_max = 5000 + enabled = true + [clients] + data = [ [ "gamma", "delta",], [ 1, 2,],] + hosts = [ "alpha", "omega",] + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +For more functions, view the API Reference below. + +Note +---- + +For Numpy users, by default the data types ``np.floatX`` will not be translated to floats by toml, but will instead be encoded as strings. To get around this, specify the ``TomlNumpyEncoder`` when saving your data. + +.. code:: pycon + + >>> import toml + >>> import numpy as np + >>> a = np.arange(0, 10, dtype=np.double) + >>> output = {'a': a} + >>> toml.dumps(output) + 'a = [ "0.0", "1.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0",]\n' + >>> toml.dumps(output, encoder=toml.TomlNumpyEncoder()) + 'a = [ 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0,]\n' + +API Reference +============= + +``toml.load(f, _dict=dict)`` + Parse a file or a list of files as TOML and return a dictionary. + + :Args: + * ``f``: A path to a file, list of filepaths (to be read into single + object) or a file descriptor + * ``_dict``: The class of the dictionary object to be returned + + :Returns: + A dictionary (or object ``_dict``) containing parsed TOML data + + :Raises: + * ``TypeError``: When ``f`` is an invalid type or is a list containing + invalid types + * ``TomlDecodeError``: When an error occurs while decoding the file(s) + +``toml.loads(s, _dict=dict)`` + Parse a TOML-formatted string to a dictionary. + + :Args: + * ``s``: The TOML-formatted string to be parsed + * ``_dict``: Specifies the class of the returned toml dictionary + + :Returns: + A dictionary (or object ``_dict``) containing parsed TOML data + + :Raises: + * ``TypeError``: When a non-string object is passed + * ``TomlDecodeError``: When an error occurs while decoding the + TOML-formatted string + +``toml.dump(o, f, encoder=None)`` + Write a dictionary to a file containing TOML-formatted data + + :Args: + * ``o``: An object to be converted into TOML + * ``f``: A File descriptor where the TOML-formatted output should be stored + * ``encoder``: An instance of ``TomlEncoder`` (or subclass) for encoding the object. If ``None``, will default to ``TomlEncoder`` + + :Returns: + A string containing the TOML-formatted data corresponding to object ``o`` + + :Raises: + * ``TypeError``: When anything other than file descriptor is passed + +``toml.dumps(o, encoder=None)`` + Create a TOML-formatted string from an input object + + :Args: + * ``o``: An object to be converted into TOML + * ``encoder``: An instance of ``TomlEncoder`` (or subclass) for encoding the object. If ``None``, will default to ``TomlEncoder`` + + :Returns: + A string containing the TOML-formatted data corresponding to object ``o`` + + + +Licensing +========= + +This project is released under the terms of the MIT Open Source License. View +*LICENSE.txt* for more information. + + diff --git a/third_party/python/toml/toml-0.10.2.dist-info/RECORD b/third_party/python/toml/toml-0.10.2.dist-info/RECORD new file mode 100644 index 0000000000..6b3a3a604d --- /dev/null +++ b/third_party/python/toml/toml-0.10.2.dist-info/RECORD @@ -0,0 +1,10 @@ +toml/__init__.py,sha256=Au3kqCwKD0cjbf4yJGOpUFwpsY0WHsC1ZRGvWgIKmpc,723 +toml/decoder.py,sha256=hSGTLf-2WBDZ_ddoCHWFy6N647XyMSh1o3rN2o4dEFg,38942 +toml/encoder.py,sha256=XjBc8ayvvlsLyd_qDA4tMWDNmMFRS4DpwtuDSWBq7zo,9940 +toml/ordered.py,sha256=mz03lZmV0bmc9lsYRIUOuj7Dsu5Ptwq-UtGVq5FdVZ4,354 +toml/tz.py,sha256=-5vg8wkg_atnVi2TnEveexIVE7T_FxBVr_-2WVfO1oA,701 +toml-0.10.2.dist-info/LICENSE,sha256=LZKUgj32yJNXyL5JJ_znk2HWVh5e51MtWSbmOTmqpTY,1252 +toml-0.10.2.dist-info/METADATA,sha256=n_YkspvEihd_QXLIZZ50WVSFz3rZ_k7jQP-OU1WUpWY,7142 +toml-0.10.2.dist-info/WHEEL,sha256=ADKeyaGyKF5DwBNE0sRE5pvW-bSkFMJfBuhzZ3rceP4,110 +toml-0.10.2.dist-info/top_level.txt,sha256=2BO8ZRNnvJWgXyiQv66LBb_v87qBzcoUtEBefA75Ouk,5 +toml-0.10.2.dist-info/RECORD,, diff --git a/third_party/python/toml/toml-0.10.2.dist-info/WHEEL b/third_party/python/toml/toml-0.10.2.dist-info/WHEEL new file mode 100644 index 0000000000..6d38aa0601 --- /dev/null +++ b/third_party/python/toml/toml-0.10.2.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.35.1) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/third_party/python/toml/toml-0.10.2.dist-info/top_level.txt b/third_party/python/toml/toml-0.10.2.dist-info/top_level.txt new file mode 100644 index 0000000000..bd79a658fe --- /dev/null +++ b/third_party/python/toml/toml-0.10.2.dist-info/top_level.txt @@ -0,0 +1 @@ +toml diff --git a/third_party/python/toml/toml/__init__.py b/third_party/python/toml/toml/__init__.py new file mode 100644 index 0000000000..7719ac23a7 --- /dev/null +++ b/third_party/python/toml/toml/__init__.py @@ -0,0 +1,25 @@ +"""Python module which parses and emits TOML. + +Released under the MIT license. +""" + +from toml import encoder +from toml import decoder + +__version__ = "0.10.2" +_spec_ = "0.5.0" + +load = decoder.load +loads = decoder.loads +TomlDecoder = decoder.TomlDecoder +TomlDecodeError = decoder.TomlDecodeError +TomlPreserveCommentDecoder = decoder.TomlPreserveCommentDecoder + +dump = encoder.dump +dumps = encoder.dumps +TomlEncoder = encoder.TomlEncoder +TomlArraySeparatorEncoder = encoder.TomlArraySeparatorEncoder +TomlPreserveInlineDictEncoder = encoder.TomlPreserveInlineDictEncoder +TomlNumpyEncoder = encoder.TomlNumpyEncoder +TomlPreserveCommentEncoder = encoder.TomlPreserveCommentEncoder +TomlPathlibEncoder = encoder.TomlPathlibEncoder diff --git a/third_party/python/toml/toml/decoder.py b/third_party/python/toml/toml/decoder.py new file mode 100644 index 0000000000..bf400e9761 --- /dev/null +++ b/third_party/python/toml/toml/decoder.py @@ -0,0 +1,1057 @@ +import datetime +import io +from os import linesep +import re +import sys + +from 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]) or + len(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.lower() == 'true': + raise ValueError("Only all lowercase booleans allowed") + elif v == 'false': + return (False, "bool") + elif v.lower() == 'false': + raise ValueError("Only all lowercase booleans allowed") + 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) diff --git a/third_party/python/toml/toml/encoder.py b/third_party/python/toml/toml/encoder.py new file mode 100644 index 0000000000..bf17a72b62 --- /dev/null +++ b/third_party/python/toml/toml/encoder.py @@ -0,0 +1,304 @@ +import datetime +import re +import sys +from decimal import Decimal + +from toml.decoder import InlineTableDict + +if sys.version_info >= (3,): + unicode = str + + +def dump(o, f, encoder=None): + """Writes out dict as toml to a file + + Args: + o: Object to dump into toml + f: File descriptor where the toml should be stored + encoder: The ``TomlEncoder`` to use for constructing the output string + + Returns: + String containing the toml corresponding to dictionary + + Raises: + TypeError: When anything other than file descriptor is passed + """ + + if not f.write: + raise TypeError("You can only dump an object to a file descriptor") + d = dumps(o, encoder=encoder) + f.write(d) + return d + + +def dumps(o, encoder=None): + """Stringifies input dict as toml + + Args: + o: Object to dump into toml + encoder: The ``TomlEncoder`` to use for constructing the output string + + Returns: + String containing the toml corresponding to dict + + Examples: + ```python + >>> import toml + >>> output = { + ... 'a': "I'm a string", + ... 'b': ["I'm", "a", "list"], + ... 'c': 2400 + ... } + >>> toml.dumps(output) + 'a = "I\'m a string"\nb = [ "I\'m", "a", "list",]\nc = 2400\n' + ``` + """ + + retval = "" + if encoder is None: + encoder = TomlEncoder(o.__class__) + addtoretval, sections = encoder.dump_sections(o, "") + retval += addtoretval + outer_objs = [id(o)] + while sections: + section_ids = [id(section) for section in sections.values()] + for outer_obj in outer_objs: + if outer_obj in section_ids: + raise ValueError("Circular reference detected") + outer_objs += section_ids + newsections = encoder.get_empty_table() + for section in sections: + addtoretval, addtosections = encoder.dump_sections( + sections[section], section) + + if addtoretval or (not addtoretval and not addtosections): + if retval and retval[-2:] != "\n\n": + retval += "\n" + retval += "[" + section + "]\n" + if addtoretval: + retval += addtoretval + for s in addtosections: + newsections[section + "." + s] = addtosections[s] + sections = newsections + return retval + + +def _dump_str(v): + if sys.version_info < (3,) and hasattr(v, 'decode') and isinstance(v, str): + v = v.decode('utf-8') + v = "%r" % v + if v[0] == 'u': + v = v[1:] + singlequote = v.startswith("'") + if singlequote or v.startswith('"'): + v = v[1:-1] + if singlequote: + v = v.replace("\\'", "'") + v = v.replace('"', '\\"') + v = v.split("\\x") + while len(v) > 1: + i = -1 + if not v[0]: + v = v[1:] + v[0] = v[0].replace("\\\\", "\\") + # No, I don't know why != works and == breaks + joinx = v[0][i] != "\\" + while v[0][:i] and v[0][i] == "\\": + joinx = not joinx + i -= 1 + if joinx: + joiner = "x" + else: + joiner = "u00" + v = [v[0] + joiner + v[1]] + v[2:] + return unicode('"' + v[0] + '"') + + +def _dump_float(v): + return "{}".format(v).replace("e+0", "e+").replace("e-0", "e-") + + +def _dump_time(v): + utcoffset = v.utcoffset() + if utcoffset is None: + return v.isoformat() + # The TOML norm specifies that it's local time thus we drop the offset + return v.isoformat()[:-6] + + +class TomlEncoder(object): + + def __init__(self, _dict=dict, preserve=False): + self._dict = _dict + self.preserve = preserve + self.dump_funcs = { + str: _dump_str, + unicode: _dump_str, + list: self.dump_list, + bool: lambda v: unicode(v).lower(), + int: lambda v: v, + float: _dump_float, + Decimal: _dump_float, + datetime.datetime: lambda v: v.isoformat().replace('+00:00', 'Z'), + datetime.time: _dump_time, + datetime.date: lambda v: v.isoformat() + } + + def get_empty_table(self): + return self._dict() + + def dump_list(self, v): + retval = "[" + for u in v: + retval += " " + unicode(self.dump_value(u)) + "," + retval += "]" + return retval + + def dump_inline_table(self, section): + """Preserve inline table in its compact syntax instead of expanding + into subsection. + + https://github.com/toml-lang/toml#user-content-inline-table + """ + retval = "" + if isinstance(section, dict): + val_list = [] + for k, v in section.items(): + val = self.dump_inline_table(v) + val_list.append(k + " = " + val) + retval += "{ " + ", ".join(val_list) + " }\n" + return retval + else: + return unicode(self.dump_value(section)) + + def dump_value(self, v): + # Lookup function corresponding to v's type + dump_fn = self.dump_funcs.get(type(v)) + if dump_fn is None and hasattr(v, '__iter__'): + dump_fn = self.dump_funcs[list] + # Evaluate function (if it exists) else return v + return dump_fn(v) if dump_fn is not None else self.dump_funcs[str](v) + + def dump_sections(self, o, sup): + retstr = "" + if sup != "" and sup[-1] != ".": + sup += '.' + retdict = self._dict() + arraystr = "" + for section in o: + section = unicode(section) + qsection = section + if not re.match(r'^[A-Za-z0-9_-]+$', section): + qsection = _dump_str(section) + if not isinstance(o[section], dict): + arrayoftables = False + if isinstance(o[section], list): + for a in o[section]: + if isinstance(a, dict): + arrayoftables = True + if arrayoftables: + for a in o[section]: + arraytabstr = "\n" + arraystr += "[[" + sup + qsection + "]]\n" + s, d = self.dump_sections(a, sup + qsection) + if s: + if s[0] == "[": + arraytabstr += s + else: + arraystr += s + while d: + newd = self._dict() + for dsec in d: + s1, d1 = self.dump_sections(d[dsec], sup + + qsection + "." + + dsec) + if s1: + arraytabstr += ("[" + sup + qsection + + "." + dsec + "]\n") + arraytabstr += s1 + for s1 in d1: + newd[dsec + "." + s1] = d1[s1] + d = newd + arraystr += arraytabstr + else: + if o[section] is not None: + retstr += (qsection + " = " + + unicode(self.dump_value(o[section])) + '\n') + elif self.preserve and isinstance(o[section], InlineTableDict): + retstr += (qsection + " = " + + self.dump_inline_table(o[section])) + else: + retdict[qsection] = o[section] + retstr += arraystr + return (retstr, retdict) + + +class TomlPreserveInlineDictEncoder(TomlEncoder): + + def __init__(self, _dict=dict): + super(TomlPreserveInlineDictEncoder, self).__init__(_dict, True) + + +class TomlArraySeparatorEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False, separator=","): + super(TomlArraySeparatorEncoder, self).__init__(_dict, preserve) + if separator.strip() == "": + separator = "," + separator + elif separator.strip(' \t\n\r,'): + raise ValueError("Invalid separator for arrays") + self.separator = separator + + def dump_list(self, v): + t = [] + retval = "[" + for u in v: + t.append(self.dump_value(u)) + while t != []: + s = [] + for u in t: + if isinstance(u, list): + for r in u: + s.append(r) + else: + retval += " " + unicode(u) + self.separator + t = s + retval += "]" + return retval + + +class TomlNumpyEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False): + import numpy as np + super(TomlNumpyEncoder, self).__init__(_dict, preserve) + self.dump_funcs[np.float16] = _dump_float + self.dump_funcs[np.float32] = _dump_float + self.dump_funcs[np.float64] = _dump_float + self.dump_funcs[np.int16] = self._dump_int + self.dump_funcs[np.int32] = self._dump_int + self.dump_funcs[np.int64] = self._dump_int + + def _dump_int(self, v): + return "{}".format(int(v)) + + +class TomlPreserveCommentEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False): + from toml.decoder import CommentValue + super(TomlPreserveCommentEncoder, self).__init__(_dict, preserve) + self.dump_funcs[CommentValue] = lambda v: v.dump(self.dump_value) + + +class TomlPathlibEncoder(TomlEncoder): + + def _dump_pathlib_path(self, v): + return _dump_str(str(v)) + + def dump_value(self, v): + if (3, 4) <= sys.version_info: + import pathlib + if isinstance(v, pathlib.PurePath): + v = str(v) + return super(TomlPathlibEncoder, self).dump_value(v) diff --git a/third_party/python/toml/toml/ordered.py b/third_party/python/toml/toml/ordered.py new file mode 100644 index 0000000000..9c20c41a1b --- /dev/null +++ b/third_party/python/toml/toml/ordered.py @@ -0,0 +1,15 @@ +from collections import OrderedDict +from toml import TomlEncoder +from toml import TomlDecoder + + +class TomlOrderedDecoder(TomlDecoder): + + def __init__(self): + super(self.__class__, self).__init__(_dict=OrderedDict) + + +class TomlOrderedEncoder(TomlEncoder): + + def __init__(self): + super(self.__class__, self).__init__(_dict=OrderedDict) diff --git a/third_party/python/toml/toml/tz.py b/third_party/python/toml/toml/tz.py new file mode 100644 index 0000000000..bf20593a26 --- /dev/null +++ b/third_party/python/toml/toml/tz.py @@ -0,0 +1,24 @@ +from datetime import tzinfo, timedelta + + +class TomlTz(tzinfo): + def __init__(self, toml_offset): + if toml_offset == "Z": + self._raw_offset = "+00:00" + else: + self._raw_offset = toml_offset + self._sign = -1 if self._raw_offset[0] == '-' else 1 + self._hours = int(self._raw_offset[1:3]) + self._minutes = int(self._raw_offset[4:6]) + + def __deepcopy__(self, memo): + return self.__class__(self._raw_offset) + + def tzname(self, dt): + return "UTC" + self._raw_offset + + def utcoffset(self, dt): + return self._sign * timedelta(hours=self._hours, minutes=self._minutes) + + def dst(self, dt): + return timedelta(0) diff --git a/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/LICENSE b/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/LICENSE new file mode 100644 index 0000000000..44cf2b30e6 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2018 Sébastien Eustace + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/METADATA b/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/METADATA new file mode 100644 index 0000000000..f4eb2f3ad9 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/METADATA @@ -0,0 +1,71 @@ +Metadata-Version: 2.1 +Name: tomlkit +Version: 0.12.3 +Summary: Style preserving TOML library +Home-page: https://github.com/sdispater/tomlkit +License: MIT +Author: Sébastien Eustace +Author-email: sebastien@eustace.io +Requires-Python: >=3.7 +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Project-URL: Repository, https://github.com/sdispater/tomlkit +Description-Content-Type: text/markdown + +[github_release]: https://img.shields.io/github/release/sdispater/tomlkit.svg?logo=github&logoColor=white +[pypi_version]: https://img.shields.io/pypi/v/tomlkit.svg?logo=python&logoColor=white +[python_versions]: https://img.shields.io/pypi/pyversions/tomlkit.svg?logo=python&logoColor=white +[github_license]: https://img.shields.io/github/license/sdispater/tomlkit.svg?logo=github&logoColor=white +[github_action]: https://github.com/sdispater/tomlkit/actions/workflows/tests.yml/badge.svg + +[![GitHub Release][github_release]](https://github.com/sdispater/tomlkit/releases/) +[![PyPI Version][pypi_version]](https://pypi.org/project/tomlkit/) +[![Python Versions][python_versions]](https://pypi.org/project/tomlkit/) +[![License][github_license]](https://github.com/sdispater/tomlkit/blob/master/LICENSE) +<br> +[![Tests][github_action]](https://github.com/sdispater/tomlkit/actions/workflows/tests.yml) + +# TOML Kit - Style-preserving TOML library for Python + +TOML Kit is a **1.0.0-compliant** [TOML](https://toml.io/) library. + +It includes a parser that preserves all comments, indentations, whitespace and internal element ordering, +and makes them accessible and editable via an intuitive API. + +You can also create new TOML documents from scratch using the provided helpers. + +Part of the implementation has been adapted, improved and fixed from [Molten](https://github.com/LeopoldArkham/Molten). + +## Usage + +See the [documentation](https://tomlkit.readthedocs.io/) for more information. + +## Installation + +If you are using [Poetry](https://poetry.eustace.io), +add `tomlkit` to your `pyproject.toml` file by using: + +```bash +poetry add tomlkit +``` + +If not, you can use `pip`: + +```bash +pip install tomlkit +``` + +## Running tests + +Please clone the repo with submodules with the following command +`git clone --recurse-submodules https://github.com/sdispater/tomlkit.git`. +We need the submodule - `toml-test` for running the tests. + +You can run the tests with `poetry run pytest -q tests` + diff --git a/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/RECORD b/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/RECORD new file mode 100644 index 0000000000..a7f2e582db --- /dev/null +++ b/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/RECORD @@ -0,0 +1,18 @@ +tomlkit/__init__.py,sha256=5lWJy3NIyY9fqzFAOYlPdnFY0NS7nmZrP8KD2_7dzQE,1282 +tomlkit/_compat.py,sha256=gp7P7qNh0yY1dg0wyjiCDbVwFTdUo7p0QwjV4T3Funs,513 +tomlkit/_types.py,sha256=9dcgqLBMPZ9czFJ56P8d1yENG_98tD-GCFwX5IYQpSg,2240 +tomlkit/_utils.py,sha256=m4OyWq9nw5MGabHhQKTIu1YtUD8SVJyoTImHTN6L7Yc,4089 +tomlkit/api.py,sha256=n2d8VBTddZVkLGbhlhTDHihnbMqBBmZ4zIW_E6ERcmM,7707 +tomlkit/container.py,sha256=VLXXtBsgWokC2TBxq-na06MdGB8v4YBgR3rFhBGNNyc,28637 +tomlkit/exceptions.py,sha256=TdeHy9e9yiXI8oSR-eCxqtQOWBlyFgn7tTjvpCWAqTw,5487 +tomlkit/items.py,sha256=dQyan_1zi0MNOAWsObtbiQy9DzmLG6Y1C7mQs3JFijc,53319 +tomlkit/parser.py,sha256=Zclbli3I1G9ov6EEL9MqspaRIaB-4xZ1oHkgdHZc8T4,37897 +tomlkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tomlkit/source.py,sha256=wDjYx0yjLEMAjmw98weDyxsg04LJhte1tMomzyIAF9E,4825 +tomlkit/toml_char.py,sha256=w3sQZ0dolZ1qjZ2Rxj_svvlpRNNGB_fjfBcYD0gFnDs,1291 +tomlkit/toml_document.py,sha256=OCTkWXd3P58EZT4SD8_ddc1YpkMaqtlS5_stHTBmMOI,110 +tomlkit/toml_file.py,sha256=4gVZvvs_Q1_soWaVxBo80rRzny849boXt2LzdMXQ04I,1599 +tomlkit-0.12.3.dist-info/LICENSE,sha256=8vm0YLpxnaZiat0mTTeC8nWk_3qrZ3vtoIszCRHiOts,1062 +tomlkit-0.12.3.dist-info/METADATA,sha256=L0Tin6eoX61jYadhYBv8DA_W3zee8zkr8fMX2rj5UYc,2718 +tomlkit-0.12.3.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88 +tomlkit-0.12.3.dist-info/RECORD,, diff --git a/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/WHEEL b/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/WHEEL new file mode 100644 index 0000000000..7c881525d3 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit-0.12.3.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 1.8.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/third_party/python/tomlkit/tomlkit/__init__.py b/third_party/python/tomlkit/tomlkit/__init__.py new file mode 100644 index 0000000000..5e4bdfa267 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/__init__.py @@ -0,0 +1,59 @@ +from tomlkit.api import TOMLDocument +from tomlkit.api import aot +from tomlkit.api import array +from tomlkit.api import boolean +from tomlkit.api import comment +from tomlkit.api import date +from tomlkit.api import datetime +from tomlkit.api import document +from tomlkit.api import dump +from tomlkit.api import dumps +from tomlkit.api import float_ +from tomlkit.api import inline_table +from tomlkit.api import integer +from tomlkit.api import item +from tomlkit.api import key +from tomlkit.api import key_value +from tomlkit.api import load +from tomlkit.api import loads +from tomlkit.api import nl +from tomlkit.api import parse +from tomlkit.api import register_encoder +from tomlkit.api import string +from tomlkit.api import table +from tomlkit.api import time +from tomlkit.api import unregister_encoder +from tomlkit.api import value +from tomlkit.api import ws + + +__version__ = "0.12.3" +__all__ = [ + "aot", + "array", + "boolean", + "comment", + "date", + "datetime", + "document", + "dump", + "dumps", + "float_", + "inline_table", + "integer", + "item", + "key", + "key_value", + "load", + "loads", + "nl", + "parse", + "string", + "table", + "time", + "TOMLDocument", + "value", + "ws", + "register_encoder", + "unregister_encoder", +] diff --git a/third_party/python/tomlkit/tomlkit/_compat.py b/third_party/python/tomlkit/tomlkit/_compat.py new file mode 100644 index 0000000000..8e76b7fde3 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/_compat.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import contextlib +import sys + +from typing import Any + + +PY38 = sys.version_info >= (3, 8) + + +def decode(string: Any, encodings: list[str] | None = None): + if not isinstance(string, bytes): + return string + + encodings = encodings or ["utf-8", "latin1", "ascii"] + + for encoding in encodings: + with contextlib.suppress(UnicodeEncodeError, UnicodeDecodeError): + return string.decode(encoding) + + return string.decode(encodings[0], errors="ignore") diff --git a/third_party/python/tomlkit/tomlkit/_types.py b/third_party/python/tomlkit/tomlkit/_types.py new file mode 100644 index 0000000000..cc1847b5e6 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/_types.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Any +from typing import TypeVar + + +WT = TypeVar("WT", bound="WrapperType") + +if TYPE_CHECKING: # pragma: no cover + # Define _CustomList and _CustomDict as a workaround for: + # https://github.com/python/mypy/issues/11427 + # + # According to this issue, the typeshed contains a "lie" + # (it adds MutableSequence to the ancestry of list and MutableMapping to + # the ancestry of dict) which completely messes with the type inference for + # Table, InlineTable, Array and Container. + # + # Importing from builtins is preferred over simple assignment, see issues: + # https://github.com/python/mypy/issues/8715 + # https://github.com/python/mypy/issues/10068 + from builtins import dict as _CustomDict # noqa: N812 + from builtins import float as _CustomFloat # noqa: N812 + from builtins import int as _CustomInt # noqa: N812 + from builtins import list as _CustomList # noqa: N812 + from typing import Callable + from typing import Concatenate + from typing import ParamSpec + from typing import Protocol + + P = ParamSpec("P") + + class WrapperType(Protocol): + def _new(self: WT, value: Any) -> WT: + ... + +else: + from collections.abc import MutableMapping + from collections.abc import MutableSequence + from numbers import Integral + from numbers import Real + + class _CustomList(MutableSequence, list): + """Adds MutableSequence mixin while pretending to be a builtin list""" + + class _CustomDict(MutableMapping, dict): + """Adds MutableMapping mixin while pretending to be a builtin dict""" + + class _CustomInt(Integral, int): + """Adds Integral mixin while pretending to be a builtin int""" + + class _CustomFloat(Real, float): + """Adds Real mixin while pretending to be a builtin float""" + + +def wrap_method( + original_method: Callable[Concatenate[WT, P], Any] +) -> Callable[Concatenate[WT, P], Any]: + def wrapper(self: WT, *args: P.args, **kwargs: P.kwargs) -> Any: + result = original_method(self, *args, **kwargs) + if result is NotImplemented: + return result + return self._new(result) + + return wrapper diff --git a/third_party/python/tomlkit/tomlkit/_utils.py b/third_party/python/tomlkit/tomlkit/_utils.py new file mode 100644 index 0000000000..f87fd7b586 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/_utils.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import re + +from collections.abc import Mapping +from datetime import date +from datetime import datetime +from datetime import time +from datetime import timedelta +from datetime import timezone +from typing import Collection + +from tomlkit._compat import decode + + +RFC_3339_LOOSE = re.compile( + "^" + r"(([0-9]+)-(\d{2})-(\d{2}))?" # Date + "(" + "([Tt ])?" # Separator + r"(\d{2}):(\d{2}):(\d{2})(\.([0-9]+))?" # Time + r"(([Zz])|([\+|\-]([01][0-9]|2[0-3]):([0-5][0-9])))?" # Timezone + ")?" + "$" +) + +RFC_3339_DATETIME = re.compile( + "^" + "([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])" # Date + "[Tt ]" # Separator + r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.([0-9]+))?" # Time + r"(([Zz])|([\+|\-]([01][0-9]|2[0-3]):([0-5][0-9])))?" # Timezone + "$" +) + +RFC_3339_DATE = re.compile("^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$") + +RFC_3339_TIME = re.compile( + r"^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.([0-9]+))?$" +) + +_utc = timezone(timedelta(), "UTC") + + +def parse_rfc3339(string: str) -> datetime | date | time: + m = RFC_3339_DATETIME.match(string) + if m: + year = int(m.group(1)) + month = int(m.group(2)) + day = int(m.group(3)) + hour = int(m.group(4)) + minute = int(m.group(5)) + second = int(m.group(6)) + microsecond = 0 + + if m.group(7): + microsecond = int((f"{m.group(8):<06s}")[:6]) + + if m.group(9): + # Timezone + tz = m.group(9) + if tz.upper() == "Z": + tzinfo = _utc + else: + sign = m.group(11)[0] + hour_offset, minute_offset = int(m.group(12)), int(m.group(13)) + offset = timedelta(seconds=hour_offset * 3600 + minute_offset * 60) + if sign == "-": + offset = -offset + + tzinfo = timezone(offset, f"{sign}{m.group(12)}:{m.group(13)}") + + return datetime( + year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo + ) + else: + return datetime(year, month, day, hour, minute, second, microsecond) + + m = RFC_3339_DATE.match(string) + if m: + year = int(m.group(1)) + month = int(m.group(2)) + day = int(m.group(3)) + + return date(year, month, day) + + m = RFC_3339_TIME.match(string) + if m: + hour = int(m.group(1)) + minute = int(m.group(2)) + second = int(m.group(3)) + microsecond = 0 + + if m.group(4): + microsecond = int((f"{m.group(5):<06s}")[:6]) + + return time(hour, minute, second, microsecond) + + raise ValueError("Invalid RFC 339 string") + + +# https://toml.io/en/v1.0.0#string +CONTROL_CHARS = frozenset(chr(c) for c in range(0x20)) | {chr(0x7F)} +_escaped = { + "b": "\b", + "t": "\t", + "n": "\n", + "f": "\f", + "r": "\r", + '"': '"', + "\\": "\\", +} +_compact_escapes = { + **{v: f"\\{k}" for k, v in _escaped.items()}, + '"""': '""\\"', +} +_basic_escapes = CONTROL_CHARS | {'"', "\\"} + + +def _unicode_escape(seq: str) -> str: + return "".join(f"\\u{ord(c):04x}" for c in seq) + + +def escape_string(s: str, escape_sequences: Collection[str] = _basic_escapes) -> str: + s = decode(s) + + res = [] + start = 0 + + def flush(inc=1): + if start != i: + res.append(s[start:i]) + + return i + inc + + found_sequences = {seq for seq in escape_sequences if seq in s} + + i = 0 + while i < len(s): + for seq in found_sequences: + seq_len = len(seq) + if s[i:].startswith(seq): + start = flush(seq_len) + res.append(_compact_escapes.get(seq) or _unicode_escape(seq)) + i += seq_len - 1 # fast-forward escape sequence + i += 1 + + flush() + + return "".join(res) + + +def merge_dicts(d1: dict, d2: dict) -> dict: + for k, v in d2.items(): + if k in d1 and isinstance(d1[k], dict) and isinstance(v, Mapping): + merge_dicts(d1[k], v) + else: + d1[k] = d2[k] diff --git a/third_party/python/tomlkit/tomlkit/api.py b/third_party/python/tomlkit/tomlkit/api.py new file mode 100644 index 0000000000..686fd1c09f --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/api.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +import contextlib +import datetime as _datetime + +from collections.abc import Mapping +from typing import IO +from typing import Iterable +from typing import TypeVar + +from tomlkit._utils import parse_rfc3339 +from tomlkit.container import Container +from tomlkit.exceptions import UnexpectedCharError +from tomlkit.items import CUSTOM_ENCODERS +from tomlkit.items import AoT +from tomlkit.items import Array +from tomlkit.items import Bool +from tomlkit.items import Comment +from tomlkit.items import Date +from tomlkit.items import DateTime +from tomlkit.items import DottedKey +from tomlkit.items import Encoder +from tomlkit.items import Float +from tomlkit.items import InlineTable +from tomlkit.items import Integer +from tomlkit.items import Item as _Item +from tomlkit.items import Key +from tomlkit.items import SingleKey +from tomlkit.items import String +from tomlkit.items import StringType as _StringType +from tomlkit.items import Table +from tomlkit.items import Time +from tomlkit.items import Trivia +from tomlkit.items import Whitespace +from tomlkit.items import item +from tomlkit.parser import Parser +from tomlkit.toml_document import TOMLDocument + + +def loads(string: str | bytes) -> TOMLDocument: + """ + Parses a string into a TOMLDocument. + + Alias for parse(). + """ + return parse(string) + + +def dumps(data: Mapping, sort_keys: bool = False) -> str: + """ + Dumps a TOMLDocument into a string. + """ + if not isinstance(data, Container) and isinstance(data, Mapping): + data = item(dict(data), _sort_keys=sort_keys) + + try: + # data should be a `Container` (and therefore implement `as_string`) + # for all type safe invocations of this function + return data.as_string() # type: ignore[attr-defined] + except AttributeError as ex: + msg = f"Expecting Mapping or TOML Container, {type(data)} given" + raise TypeError(msg) from ex + + +def load(fp: IO[str] | IO[bytes]) -> TOMLDocument: + """ + Load toml document from a file-like object. + """ + return parse(fp.read()) + + +def dump(data: Mapping, fp: IO[str], *, sort_keys: bool = False) -> None: + """ + Dump a TOMLDocument into a writable file stream. + + :param data: a dict-like object to dump + :param sort_keys: if true, sort the keys in alphabetic order + """ + fp.write(dumps(data, sort_keys=sort_keys)) + + +def parse(string: str | bytes) -> TOMLDocument: + """ + Parses a string or bytes into a TOMLDocument. + """ + return Parser(string).parse() + + +def document() -> TOMLDocument: + """ + Returns a new TOMLDocument instance. + """ + return TOMLDocument() + + +# Items +def integer(raw: str | int) -> Integer: + """Create an integer item from a number or string.""" + return item(int(raw)) + + +def float_(raw: str | float) -> Float: + """Create an float item from a number or string.""" + return item(float(raw)) + + +def boolean(raw: str) -> Bool: + """Turn `true` or `false` into a boolean item.""" + return item(raw == "true") + + +def string( + raw: str, + *, + literal: bool = False, + multiline: bool = False, + escape: bool = True, +) -> String: + """Create a string item. + + By default, this function will create *single line basic* strings, but + boolean flags (e.g. ``literal=True`` and/or ``multiline=True``) + can be used for personalization. + + For more information, please check the spec: `<https://toml.io/en/v1.0.0#string>`__. + + Common escaping rules will be applied for basic strings. + This can be controlled by explicitly setting ``escape=False``. + Please note that, if you disable escaping, you will have to make sure that + the given strings don't contain any forbidden character or sequence. + """ + type_ = _StringType.select(literal, multiline) + return String.from_raw(raw, type_, escape) + + +def date(raw: str) -> Date: + """Create a TOML date.""" + value = parse_rfc3339(raw) + if not isinstance(value, _datetime.date): + raise ValueError("date() only accepts date strings.") + + return item(value) + + +def time(raw: str) -> Time: + """Create a TOML time.""" + value = parse_rfc3339(raw) + if not isinstance(value, _datetime.time): + raise ValueError("time() only accepts time strings.") + + return item(value) + + +def datetime(raw: str) -> DateTime: + """Create a TOML datetime.""" + value = parse_rfc3339(raw) + if not isinstance(value, _datetime.datetime): + raise ValueError("datetime() only accepts datetime strings.") + + return item(value) + + +def array(raw: str = None) -> Array: + """Create an array item for its string representation. + + :Example: + + >>> array("[1, 2, 3]") # Create from a string + [1, 2, 3] + >>> a = array() + >>> a.extend([1, 2, 3]) # Create from a list + >>> a + [1, 2, 3] + """ + if raw is None: + raw = "[]" + + return value(raw) + + +def table(is_super_table: bool | None = None) -> Table: + """Create an empty table. + + :param is_super_table: if true, the table is a super table + + :Example: + + >>> doc = document() + >>> foo = table(True) + >>> bar = table() + >>> bar.update({'x': 1}) + >>> foo.append('bar', bar) + >>> doc.append('foo', foo) + >>> print(doc.as_string()) + [foo.bar] + x = 1 + """ + return Table(Container(), Trivia(), False, is_super_table) + + +def inline_table() -> InlineTable: + """Create an inline table. + + :Example: + + >>> table = inline_table() + >>> table.update({'x': 1, 'y': 2}) + >>> print(table.as_string()) + {x = 1, y = 2} + """ + return InlineTable(Container(), Trivia(), new=True) + + +def aot() -> AoT: + """Create an array of table. + + :Example: + + >>> doc = document() + >>> aot = aot() + >>> aot.append(item({'x': 1})) + >>> doc.append('foo', aot) + >>> print(doc.as_string()) + [[foo]] + x = 1 + """ + return AoT([]) + + +def key(k: str | Iterable[str]) -> Key: + """Create a key from a string. When a list of string is given, + it will create a dotted key. + + :Example: + + >>> doc = document() + >>> doc.append(key('foo'), 1) + >>> doc.append(key(['bar', 'baz']), 2) + >>> print(doc.as_string()) + foo = 1 + bar.baz = 2 + """ + if isinstance(k, str): + return SingleKey(k) + return DottedKey([key(_k) for _k in k]) + + +def value(raw: str) -> _Item: + """Parse a simple value from a string. + + :Example: + + >>> value("1") + 1 + >>> value("true") + True + >>> value("[1, 2, 3]") + [1, 2, 3] + """ + parser = Parser(raw) + v = parser._parse_value() + if not parser.end(): + raise parser.parse_error(UnexpectedCharError, char=parser._current) + return v + + +def key_value(src: str) -> tuple[Key, _Item]: + """Parse a key-value pair from a string. + + :Example: + + >>> key_value("foo = 1") + (Key('foo'), 1) + """ + return Parser(src)._parse_key_value() + + +def ws(src: str) -> Whitespace: + """Create a whitespace from a string.""" + return Whitespace(src, fixed=True) + + +def nl() -> Whitespace: + """Create a newline item.""" + return ws("\n") + + +def comment(string: str) -> Comment: + """Create a comment item.""" + return Comment(Trivia(comment_ws=" ", comment="# " + string)) + + +E = TypeVar("E", bound=Encoder) + + +def register_encoder(encoder: E) -> E: + """Add a custom encoder, which should be a function that will be called + if the value can't otherwise be converted. It should takes a single value + and return a TOMLKit item or raise a ``TypeError``. + """ + CUSTOM_ENCODERS.append(encoder) + return encoder + + +def unregister_encoder(encoder: Encoder) -> None: + """Unregister a custom encoder.""" + with contextlib.suppress(ValueError): + CUSTOM_ENCODERS.remove(encoder) diff --git a/third_party/python/tomlkit/tomlkit/container.py b/third_party/python/tomlkit/tomlkit/container.py new file mode 100644 index 0000000000..9251a98096 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/container.py @@ -0,0 +1,875 @@ +from __future__ import annotations + +import copy + +from typing import Any +from typing import Iterator + +from tomlkit._compat import decode +from tomlkit._types import _CustomDict +from tomlkit._utils import merge_dicts +from tomlkit.exceptions import KeyAlreadyPresent +from tomlkit.exceptions import NonExistentKey +from tomlkit.exceptions import TOMLKitError +from tomlkit.items import AoT +from tomlkit.items import Comment +from tomlkit.items import Item +from tomlkit.items import Key +from tomlkit.items import Null +from tomlkit.items import SingleKey +from tomlkit.items import Table +from tomlkit.items import Trivia +from tomlkit.items import Whitespace +from tomlkit.items import item as _item + + +_NOT_SET = object() + + +class Container(_CustomDict): + """ + A container for items within a TOMLDocument. + + This class implements the `dict` interface with copy/deepcopy protocol. + """ + + def __init__(self, parsed: bool = False) -> None: + self._map: dict[SingleKey, int | tuple[int, ...]] = {} + self._body: list[tuple[Key | None, Item]] = [] + self._parsed = parsed + self._table_keys = [] + + @property + def body(self) -> list[tuple[Key | None, Item]]: + return self._body + + def unwrap(self) -> dict[str, Any]: + """Returns as pure python object (ppo)""" + unwrapped = {} + for k, v in self.items(): + if k is None: + continue + + if isinstance(k, Key): + k = k.key + + if hasattr(v, "unwrap"): + v = v.unwrap() + + if k in unwrapped: + merge_dicts(unwrapped[k], v) + else: + unwrapped[k] = v + + return unwrapped + + @property + def value(self) -> dict[str, Any]: + """The wrapped dict value""" + d = {} + for k, v in self._body: + if k is None: + continue + + k = k.key + v = v.value + + if isinstance(v, Container): + v = v.value + + if k in d: + merge_dicts(d[k], v) + else: + d[k] = v + + return d + + def parsing(self, parsing: bool) -> None: + self._parsed = parsing + + for _, v in self._body: + if isinstance(v, Table): + v.value.parsing(parsing) + elif isinstance(v, AoT): + for t in v.body: + t.value.parsing(parsing) + + def add(self, key: Key | Item | str, item: Item | None = None) -> Container: + """ + Adds an item to the current Container. + + :Example: + + >>> # add a key-value pair + >>> doc.add('key', 'value') + >>> # add a comment or whitespace or newline + >>> doc.add(comment('# comment')) + """ + if item is None: + if not isinstance(key, (Comment, Whitespace)): + raise ValueError( + "Non comment/whitespace items must have an associated key" + ) + + key, item = None, key + + return self.append(key, item) + + def _handle_dotted_key(self, key: Key, value: Item) -> None: + if isinstance(value, (Table, AoT)): + raise TOMLKitError("Can't add a table to a dotted key") + name, *mid, last = key + name._dotted = True + table = current = Table(Container(True), Trivia(), False, is_super_table=True) + for _name in mid: + _name._dotted = True + new_table = Table(Container(True), Trivia(), False, is_super_table=True) + current.append(_name, new_table) + current = new_table + + last.sep = key.sep + current.append(last, value) + + self.append(name, table) + return + + def _get_last_index_before_table(self) -> int: + last_index = -1 + for i, (k, v) in enumerate(self._body): + if isinstance(v, Null): + continue # Null elements are inserted after deletion + + if isinstance(v, Whitespace) and not v.is_fixed(): + continue + + if isinstance(v, (Table, AoT)) and not k.is_dotted(): + break + last_index = i + return last_index + 1 + + def _validate_out_of_order_table(self, key: SingleKey | None = None) -> None: + if key is None: + for k in self._map: + assert k is not None + self._validate_out_of_order_table(k) + return + if key not in self._map or not isinstance(self._map[key], tuple): + return + OutOfOrderTableProxy(self, self._map[key]) + + def append( + self, key: Key | str | None, item: Item, validate: bool = True + ) -> Container: + """Similar to :meth:`add` but both key and value must be given.""" + if not isinstance(key, Key) and key is not None: + key = SingleKey(key) + + if not isinstance(item, Item): + item = _item(item) + + if key is not None and key.is_multi(): + self._handle_dotted_key(key, item) + return self + + if isinstance(item, (AoT, Table)) and item.name is None: + item.name = key.key + + prev = self._previous_item() + prev_ws = isinstance(prev, Whitespace) or ends_with_whitespace(prev) + if isinstance(item, Table): + if not self._parsed: + item.invalidate_display_name() + if ( + self._body + and not (self._parsed or item.trivia.indent or prev_ws) + and not key.is_dotted() + ): + item.trivia.indent = "\n" + + if isinstance(item, AoT) and self._body and not self._parsed: + item.invalidate_display_name() + if item and not ("\n" in item[0].trivia.indent or prev_ws): + item[0].trivia.indent = "\n" + item[0].trivia.indent + + if key is not None and key in self: + current_idx = self._map[key] + if isinstance(current_idx, tuple): + current_body_element = self._body[current_idx[-1]] + else: + current_body_element = self._body[current_idx] + + current = current_body_element[1] + + if isinstance(item, Table): + if not isinstance(current, (Table, AoT)): + raise KeyAlreadyPresent(key) + + if item.is_aot_element(): + # New AoT element found later on + # Adding it to the current AoT + if not isinstance(current, AoT): + current = AoT([current, item], parsed=self._parsed) + + self._replace(key, key, current) + else: + current.append(item) + + return self + elif current.is_aot(): + if not item.is_aot_element(): + # Tried to define a table after an AoT with the same name. + raise KeyAlreadyPresent(key) + + current.append(item) + + return self + elif current.is_super_table(): + if item.is_super_table(): + # We need to merge both super tables + if ( + self._table_keys[-1] != current_body_element[0] + or key.is_dotted() + or current_body_element[0].is_dotted() + ): + if key.is_dotted() and not self._parsed: + idx = self._get_last_index_before_table() + else: + idx = len(self._body) + + if idx < len(self._body): + self._insert_at(idx, key, item) + else: + self._raw_append(key, item) + + if validate: + self._validate_out_of_order_table(key) + + return self + + # Create a new element to replace the old one + current = copy.deepcopy(current) + for k, v in item.value.body: + current.append(k, v) + self._body[ + current_idx[-1] + if isinstance(current_idx, tuple) + else current_idx + ] = (current_body_element[0], current) + + return self + elif current_body_element[0].is_dotted(): + raise TOMLKitError("Redefinition of an existing table") + elif not item.is_super_table(): + raise KeyAlreadyPresent(key) + elif isinstance(item, AoT): + if not isinstance(current, AoT): + # Tried to define an AoT after a table with the same name. + raise KeyAlreadyPresent(key) + + for table in item.body: + current.append(table) + + return self + else: + raise KeyAlreadyPresent(key) + + is_table = isinstance(item, (Table, AoT)) + if ( + key is not None + and self._body + and not self._parsed + and (not is_table or key.is_dotted()) + ): + # If there is already at least one table in the current container + # and the given item is not a table, we need to find the last + # item that is not a table and insert after it + # If no such item exists, insert at the top of the table + last_index = self._get_last_index_before_table() + + if last_index < len(self._body): + return self._insert_at(last_index, key, item) + else: + previous_item = self._body[-1][1] + if not ( + isinstance(previous_item, Whitespace) + or ends_with_whitespace(previous_item) + or "\n" in previous_item.trivia.trail + ): + previous_item.trivia.trail += "\n" + + self._raw_append(key, item) + return self + + def _raw_append(self, key: Key | None, item: Item) -> None: + if key in self._map: + current_idx = self._map[key] + if not isinstance(current_idx, tuple): + current_idx = (current_idx,) + + current = self._body[current_idx[-1]][1] + if key is not None and not isinstance(current, Table): + raise KeyAlreadyPresent(key) + + self._map[key] = current_idx + (len(self._body),) + elif key is not None: + self._map[key] = len(self._body) + + self._body.append((key, item)) + if item.is_table(): + self._table_keys.append(key) + + if key is not None: + dict.__setitem__(self, key.key, item.value) + + return self + + def _remove_at(self, idx: int) -> None: + key = self._body[idx][0] + index = self._map.get(key) + if index is None: + raise NonExistentKey(key) + self._body[idx] = (None, Null()) + + if isinstance(index, tuple): + index = list(index) + index.remove(idx) + if len(index) == 1: + index = index.pop() + else: + index = tuple(index) + self._map[key] = index + else: + dict.__delitem__(self, key.key) + self._map.pop(key) + + def remove(self, key: Key | str) -> Container: + """Remove a key from the container.""" + if not isinstance(key, Key): + key = SingleKey(key) + + idx = self._map.pop(key, None) + if idx is None: + raise NonExistentKey(key) + + if isinstance(idx, tuple): + for i in idx: + self._body[i] = (None, Null()) + else: + self._body[idx] = (None, Null()) + + dict.__delitem__(self, key.key) + + return self + + def _insert_after( + self, key: Key | str, other_key: Key | str, item: Any + ) -> Container: + if key is None: + raise ValueError("Key cannot be null in insert_after()") + + if key not in self: + raise NonExistentKey(key) + + if not isinstance(key, Key): + key = SingleKey(key) + + if not isinstance(other_key, Key): + other_key = SingleKey(other_key) + + item = _item(item) + + idx = self._map[key] + # Insert after the max index if there are many. + if isinstance(idx, tuple): + idx = max(idx) + current_item = self._body[idx][1] + if "\n" not in current_item.trivia.trail: + current_item.trivia.trail += "\n" + + # Increment indices after the current index + for k, v in self._map.items(): + if isinstance(v, tuple): + new_indices = [] + for v_ in v: + if v_ > idx: + v_ = v_ + 1 + + new_indices.append(v_) + + self._map[k] = tuple(new_indices) + elif v > idx: + self._map[k] = v + 1 + + self._map[other_key] = idx + 1 + self._body.insert(idx + 1, (other_key, item)) + + if key is not None: + dict.__setitem__(self, other_key.key, item.value) + + return self + + def _insert_at(self, idx: int, key: Key | str, item: Any) -> Container: + if idx > len(self._body) - 1: + raise ValueError(f"Unable to insert at position {idx}") + + if not isinstance(key, Key): + key = SingleKey(key) + + item = _item(item) + + if idx > 0: + previous_item = self._body[idx - 1][1] + if not ( + isinstance(previous_item, Whitespace) + or ends_with_whitespace(previous_item) + or isinstance(item, (AoT, Table)) + or "\n" in previous_item.trivia.trail + ): + previous_item.trivia.trail += "\n" + + # Increment indices after the current index + for k, v in self._map.items(): + if isinstance(v, tuple): + new_indices = [] + for v_ in v: + if v_ >= idx: + v_ = v_ + 1 + + new_indices.append(v_) + + self._map[k] = tuple(new_indices) + elif v >= idx: + self._map[k] = v + 1 + + if key in self._map: + current_idx = self._map[key] + if not isinstance(current_idx, tuple): + current_idx = (current_idx,) + self._map[key] = current_idx + (idx,) + else: + self._map[key] = idx + self._body.insert(idx, (key, item)) + + dict.__setitem__(self, key.key, item.value) + + return self + + def item(self, key: Key | str) -> Item: + """Get an item for the given key.""" + if not isinstance(key, Key): + key = SingleKey(key) + + idx = self._map.get(key) + if idx is None: + raise NonExistentKey(key) + + if isinstance(idx, tuple): + # The item we are getting is an out of order table + # so we need a proxy to retrieve the proper objects + # from the parent container + return OutOfOrderTableProxy(self, idx) + + return self._body[idx][1] + + def last_item(self) -> Item | None: + """Get the last item.""" + if self._body: + return self._body[-1][1] + + def as_string(self) -> str: + """Render as TOML string.""" + s = "" + for k, v in self._body: + if k is not None: + if isinstance(v, Table): + s += self._render_table(k, v) + elif isinstance(v, AoT): + s += self._render_aot(k, v) + else: + s += self._render_simple_item(k, v) + else: + s += self._render_simple_item(k, v) + + return s + + def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str: + cur = "" + + if table.display_name is not None: + _key = table.display_name + else: + _key = key.as_string() + + if prefix is not None: + _key = prefix + "." + _key + + if not table.is_super_table() or ( + any( + not isinstance(v, (Table, AoT, Whitespace, Null)) + for _, v in table.value.body + ) + and not key.is_dotted() + ): + open_, close = "[", "]" + if table.is_aot_element(): + open_, close = "[[", "]]" + + newline_in_table_trivia = ( + "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else "" + ) + cur += ( + f"{table.trivia.indent}" + f"{open_}" + f"{decode(_key)}" + f"{close}" + f"{table.trivia.comment_ws}" + f"{decode(table.trivia.comment)}" + f"{table.trivia.trail}" + f"{newline_in_table_trivia}" + ) + elif table.trivia.indent == "\n": + cur += table.trivia.indent + + for k, v in table.value.body: + if isinstance(v, Table): + if v.is_super_table(): + if k.is_dotted() and not key.is_dotted(): + # Dotted key inside table + cur += self._render_table(k, v) + else: + cur += self._render_table(k, v, prefix=_key) + else: + cur += self._render_table(k, v, prefix=_key) + elif isinstance(v, AoT): + cur += self._render_aot(k, v, prefix=_key) + else: + cur += self._render_simple_item( + k, v, prefix=_key if key.is_dotted() else None + ) + + return cur + + def _render_aot(self, key, aot, prefix=None): + _key = key.as_string() + if prefix is not None: + _key = prefix + "." + _key + + cur = "" + _key = decode(_key) + for table in aot.body: + cur += self._render_aot_table(table, prefix=_key) + + return cur + + def _render_aot_table(self, table: Table, prefix: str | None = None) -> str: + cur = "" + _key = prefix or "" + open_, close = "[[", "]]" + + cur += ( + f"{table.trivia.indent}" + f"{open_}" + f"{decode(_key)}" + f"{close}" + f"{table.trivia.comment_ws}" + f"{decode(table.trivia.comment)}" + f"{table.trivia.trail}" + ) + + for k, v in table.value.body: + if isinstance(v, Table): + if v.is_super_table(): + if k.is_dotted(): + # Dotted key inside table + cur += self._render_table(k, v) + else: + cur += self._render_table(k, v, prefix=_key) + else: + cur += self._render_table(k, v, prefix=_key) + elif isinstance(v, AoT): + cur += self._render_aot(k, v, prefix=_key) + else: + cur += self._render_simple_item(k, v) + + return cur + + def _render_simple_item(self, key, item, prefix=None): + if key is None: + return item.as_string() + + _key = key.as_string() + if prefix is not None: + _key = prefix + "." + _key + + return ( + f"{item.trivia.indent}" + f"{decode(_key)}" + f"{key.sep}" + f"{decode(item.as_string())}" + f"{item.trivia.comment_ws}" + f"{decode(item.trivia.comment)}" + f"{item.trivia.trail}" + ) + + def __len__(self) -> int: + return dict.__len__(self) + + def __iter__(self) -> Iterator[str]: + return iter(dict.keys(self)) + + # Dictionary methods + def __getitem__(self, key: Key | str) -> Item | Container: + item = self.item(key) + if isinstance(item, Item) and item.is_boolean(): + return item.value + + return item + + def __setitem__(self, key: Key | str, value: Any) -> None: + if key is not None and key in self: + old_key = next(filter(lambda k: k == key, self._map)) + self._replace(old_key, key, value) + else: + self.append(key, value) + + def __delitem__(self, key: Key | str) -> None: + self.remove(key) + + def setdefault(self, key: Key | str, default: Any) -> Any: + super().setdefault(key, default=default) + return self[key] + + def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None: + if not isinstance(key, Key): + key = SingleKey(key) + + idx = self._map.get(key) + if idx is None: + raise NonExistentKey(key) + + self._replace_at(idx, new_key, value) + + def _replace_at( + self, idx: int | tuple[int], new_key: Key | str, value: Item + ) -> None: + value = _item(value) + + if isinstance(idx, tuple): + for i in idx[1:]: + self._body[i] = (None, Null()) + + idx = idx[0] + + k, v = self._body[idx] + if not isinstance(new_key, Key): + if ( + isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)) + or new_key != k.key + ): + new_key = SingleKey(new_key) + else: # Inherit the sep of the old key + new_key = k + + del self._map[k] + self._map[new_key] = idx + if new_key != k: + dict.__delitem__(self, k) + + if isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)): + # new tables should appear after all non-table values + self.remove(k) + for i in range(idx, len(self._body)): + if isinstance(self._body[i][1], (AoT, Table)): + self._insert_at(i, new_key, value) + idx = i + break + else: + idx = -1 + self.append(new_key, value) + else: + # Copying trivia + if not isinstance(value, (Whitespace, AoT)): + value.trivia.indent = v.trivia.indent + value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws + value.trivia.comment = value.trivia.comment or v.trivia.comment + value.trivia.trail = v.trivia.trail + self._body[idx] = (new_key, value) + + if hasattr(value, "invalidate_display_name"): + value.invalidate_display_name() # type: ignore[attr-defined] + + if isinstance(value, Table): + # Insert a cosmetic new line for tables if: + # - it does not have it yet OR is not followed by one + # - it is not the last item, or + # - The table being replaced has a newline + last, _ = self._previous_item_with_index() + idx = last if idx < 0 else idx + has_ws = ends_with_whitespace(value) + replace_has_ws = ( + isinstance(v, Table) + and v.value.body + and isinstance(v.value.body[-1][1], Whitespace) + ) + next_ws = idx < last and isinstance(self._body[idx + 1][1], Whitespace) + if (idx < last or replace_has_ws) and not (next_ws or has_ws): + value.append(None, Whitespace("\n")) + + dict.__setitem__(self, new_key.key, value.value) + + def __str__(self) -> str: + return str(self.value) + + def __repr__(self) -> str: + return repr(self.value) + + def __eq__(self, other: dict) -> bool: + if not isinstance(other, dict): + return NotImplemented + + return self.value == other + + def _getstate(self, protocol): + return (self._parsed,) + + def __reduce__(self): + return self.__reduce_ex__(2) + + def __reduce_ex__(self, protocol): + return ( + self.__class__, + self._getstate(protocol), + (self._map, self._body, self._parsed, self._table_keys), + ) + + def __setstate__(self, state): + self._map = state[0] + self._body = state[1] + self._parsed = state[2] + self._table_keys = state[3] + + for key, item in self._body: + if key is not None: + dict.__setitem__(self, key.key, item.value) + + def copy(self) -> Container: + return copy.copy(self) + + def __copy__(self) -> Container: + c = self.__class__(self._parsed) + for k, v in dict.items(self): + dict.__setitem__(c, k, v) + + c._body += self.body + c._map.update(self._map) + + return c + + def _previous_item_with_index( + self, idx: int | None = None, ignore=(Null,) + ) -> tuple[int, Item] | None: + """Find the immediate previous item before index ``idx``""" + if idx is None or idx > len(self._body): + idx = len(self._body) + for i in range(idx - 1, -1, -1): + v = self._body[i][-1] + if not isinstance(v, ignore): + return i, v + return None + + def _previous_item(self, idx: int | None = None, ignore=(Null,)) -> Item | None: + """Find the immediate previous item before index ``idx``. + If ``idx`` is not given, the last item is returned. + """ + prev = self._previous_item_with_index(idx, ignore) + return prev[-1] if prev else None + + +class OutOfOrderTableProxy(_CustomDict): + def __init__(self, container: Container, indices: tuple[int]) -> None: + self._container = container + self._internal_container = Container(True) + self._tables = [] + self._tables_map = {} + + for i in indices: + _, item = self._container._body[i] + + if isinstance(item, Table): + self._tables.append(item) + table_idx = len(self._tables) - 1 + for k, v in item.value.body: + self._internal_container.append(k, v, validate=False) + self._tables_map[k] = table_idx + if k is not None: + dict.__setitem__(self, k.key, v) + + self._internal_container._validate_out_of_order_table() + + def unwrap(self) -> str: + return self._internal_container.unwrap() + + @property + def value(self): + return self._internal_container.value + + def __getitem__(self, key: Key | str) -> Any: + if key not in self._internal_container: + raise NonExistentKey(key) + + return self._internal_container[key] + + def __setitem__(self, key: Key | str, item: Any) -> None: + if key in self._tables_map: + table = self._tables[self._tables_map[key]] + table[key] = item + elif self._tables: + table = self._tables[0] + table[key] = item + else: + self._container[key] = item + + self._internal_container[key] = item + if key is not None: + dict.__setitem__(self, key, item) + + def _remove_table(self, table: Table) -> None: + """Remove table from the parent container""" + self._tables.remove(table) + for idx, item in enumerate(self._container._body): + if item[1] is table: + self._container._remove_at(idx) + break + + def __delitem__(self, key: Key | str) -> None: + if key in self._tables_map: + table = self._tables[self._tables_map[key]] + del table[key] + if not table and len(self._tables) > 1: + self._remove_table(table) + del self._tables_map[key] + else: + raise NonExistentKey(key) + + del self._internal_container[key] + if key is not None: + dict.__delitem__(self, key) + + def __iter__(self) -> Iterator[str]: + return iter(dict.keys(self)) + + def __len__(self) -> int: + return dict.__len__(self) + + def setdefault(self, key: Key | str, default: Any) -> Any: + super().setdefault(key, default=default) + return self[key] + + +def ends_with_whitespace(it: Any) -> bool: + """Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object + ending with a ``Whitespace``. + """ + return ( + isinstance(it, Table) and isinstance(it.value._previous_item(), Whitespace) + ) or (isinstance(it, AoT) and len(it) > 0 and isinstance(it[-1], Whitespace)) diff --git a/third_party/python/tomlkit/tomlkit/exceptions.py b/third_party/python/tomlkit/tomlkit/exceptions.py new file mode 100644 index 0000000000..30d0d85cee --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/exceptions.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +from typing import Collection + + +class TOMLKitError(Exception): + pass + + +class ParseError(ValueError, TOMLKitError): + """ + This error occurs when the parser encounters a syntax error + in the TOML being parsed. The error references the line and + location within the line where the error was encountered. + """ + + def __init__(self, line: int, col: int, message: str | None = None) -> None: + self._line = line + self._col = col + + if message is None: + message = "TOML parse error" + + super().__init__(f"{message} at line {self._line} col {self._col}") + + @property + def line(self): + return self._line + + @property + def col(self): + return self._col + + +class MixedArrayTypesError(ParseError): + """ + An array was found that had two or more element types. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Mixed types found in array" + + super().__init__(line, col, message=message) + + +class InvalidNumberError(ParseError): + """ + A numeric field was improperly specified. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Invalid number" + + super().__init__(line, col, message=message) + + +class InvalidDateTimeError(ParseError): + """ + A datetime field was improperly specified. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Invalid datetime" + + super().__init__(line, col, message=message) + + +class InvalidDateError(ParseError): + """ + A date field was improperly specified. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Invalid date" + + super().__init__(line, col, message=message) + + +class InvalidTimeError(ParseError): + """ + A date field was improperly specified. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Invalid time" + + super().__init__(line, col, message=message) + + +class InvalidNumberOrDateError(ParseError): + """ + A numeric or date field was improperly specified. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Invalid number or date format" + + super().__init__(line, col, message=message) + + +class InvalidUnicodeValueError(ParseError): + """ + A unicode code was improperly specified. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Invalid unicode value" + + super().__init__(line, col, message=message) + + +class UnexpectedCharError(ParseError): + """ + An unexpected character was found during parsing. + """ + + def __init__(self, line: int, col: int, char: str) -> None: + message = f"Unexpected character: {repr(char)}" + + super().__init__(line, col, message=message) + + +class EmptyKeyError(ParseError): + """ + An empty key was found during parsing. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Empty key" + + super().__init__(line, col, message=message) + + +class EmptyTableNameError(ParseError): + """ + An empty table name was found during parsing. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Empty table name" + + super().__init__(line, col, message=message) + + +class InvalidCharInStringError(ParseError): + """ + The string being parsed contains an invalid character. + """ + + def __init__(self, line: int, col: int, char: str) -> None: + message = f"Invalid character {repr(char)} in string" + + super().__init__(line, col, message=message) + + +class UnexpectedEofError(ParseError): + """ + The TOML being parsed ended before the end of a statement. + """ + + def __init__(self, line: int, col: int) -> None: + message = "Unexpected end of file" + + super().__init__(line, col, message=message) + + +class InternalParserError(ParseError): + """ + An error that indicates a bug in the parser. + """ + + def __init__(self, line: int, col: int, message: str | None = None) -> None: + msg = "Internal parser error" + if message: + msg += f" ({message})" + + super().__init__(line, col, message=msg) + + +class NonExistentKey(KeyError, TOMLKitError): + """ + A non-existent key was used. + """ + + def __init__(self, key): + message = f'Key "{key}" does not exist.' + + super().__init__(message) + + +class KeyAlreadyPresent(TOMLKitError): + """ + An already present key was used. + """ + + def __init__(self, key): + key = getattr(key, "key", key) + message = f'Key "{key}" already exists.' + + super().__init__(message) + + +class InvalidControlChar(ParseError): + def __init__(self, line: int, col: int, char: int, type: str) -> None: + display_code = "\\u00" + + if char < 16: + display_code += "0" + + display_code += hex(char)[2:] + + message = ( + "Control characters (codes less than 0x1f and 0x7f)" + f" are not allowed in {type}, " + f"use {display_code} instead" + ) + + super().__init__(line, col, message=message) + + +class InvalidStringError(ValueError, TOMLKitError): + def __init__(self, value: str, invalid_sequences: Collection[str], delimiter: str): + repr_ = repr(value)[1:-1] + super().__init__( + f"Invalid string: {delimiter}{repr_}{delimiter}. " + f"The character sequences {invalid_sequences} are invalid." + ) diff --git a/third_party/python/tomlkit/tomlkit/items.py b/third_party/python/tomlkit/tomlkit/items.py new file mode 100644 index 0000000000..c7396e590d --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/items.py @@ -0,0 +1,1966 @@ +from __future__ import annotations + +import abc +import copy +import dataclasses +import math +import re +import string +import sys + +from datetime import date +from datetime import datetime +from datetime import time +from datetime import tzinfo +from enum import Enum +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import Collection +from typing import Iterable +from typing import Iterator +from typing import Sequence +from typing import TypeVar +from typing import cast +from typing import overload + +from tomlkit._compat import PY38 +from tomlkit._compat import decode +from tomlkit._types import _CustomDict +from tomlkit._types import _CustomFloat +from tomlkit._types import _CustomInt +from tomlkit._types import _CustomList +from tomlkit._types import wrap_method +from tomlkit._utils import CONTROL_CHARS +from tomlkit._utils import escape_string +from tomlkit.exceptions import InvalidStringError + + +if TYPE_CHECKING: + from tomlkit import container + + +ItemT = TypeVar("ItemT", bound="Item") +Encoder = Callable[[Any], "Item"] +CUSTOM_ENCODERS: list[Encoder] = [] +AT = TypeVar("AT", bound="AbstractTable") + + +class _ConvertError(TypeError, ValueError): + """An internal error raised when item() fails to convert a value. + It should be a TypeError, but due to historical reasons + it needs to subclass ValueError as well. + """ + + +@overload +def item(value: bool, _parent: Item | None = ..., _sort_keys: bool = ...) -> Bool: + ... + + +@overload +def item(value: int, _parent: Item | None = ..., _sort_keys: bool = ...) -> Integer: + ... + + +@overload +def item(value: float, _parent: Item | None = ..., _sort_keys: bool = ...) -> Float: + ... + + +@overload +def item(value: str, _parent: Item | None = ..., _sort_keys: bool = ...) -> String: + ... + + +@overload +def item( + value: datetime, _parent: Item | None = ..., _sort_keys: bool = ... +) -> DateTime: + ... + + +@overload +def item(value: date, _parent: Item | None = ..., _sort_keys: bool = ...) -> Date: + ... + + +@overload +def item(value: time, _parent: Item | None = ..., _sort_keys: bool = ...) -> Time: + ... + + +@overload +def item( + value: Sequence[dict], _parent: Item | None = ..., _sort_keys: bool = ... +) -> AoT: + ... + + +@overload +def item(value: Sequence, _parent: Item | None = ..., _sort_keys: bool = ...) -> Array: + ... + + +@overload +def item(value: dict, _parent: Array = ..., _sort_keys: bool = ...) -> InlineTable: + ... + + +@overload +def item(value: dict, _parent: Item | None = ..., _sort_keys: bool = ...) -> Table: + ... + + +@overload +def item(value: ItemT, _parent: Item | None = ..., _sort_keys: bool = ...) -> ItemT: + ... + + +def item(value: Any, _parent: Item | None = None, _sort_keys: bool = False) -> Item: + """Create a TOML item from a Python object. + + :Example: + + >>> item(42) + 42 + >>> item([1, 2, 3]) + [1, 2, 3] + >>> item({'a': 1, 'b': 2}) + a = 1 + b = 2 + """ + + from tomlkit.container import Container + + if isinstance(value, Item): + return value + + if isinstance(value, bool): + return Bool(value, Trivia()) + elif isinstance(value, int): + return Integer(value, Trivia(), str(value)) + elif isinstance(value, float): + return Float(value, Trivia(), str(value)) + elif isinstance(value, dict): + table_constructor = ( + InlineTable if isinstance(_parent, (Array, InlineTable)) else Table + ) + val = table_constructor(Container(), Trivia(), False) + for k, v in sorted( + value.items(), + key=lambda i: (isinstance(i[1], dict), i[0]) if _sort_keys else 1, + ): + val[k] = item(v, _parent=val, _sort_keys=_sort_keys) + + return val + elif isinstance(value, (list, tuple)): + if ( + value + and all(isinstance(v, dict) for v in value) + and (_parent is None or isinstance(_parent, Table)) + ): + a = AoT([]) + table_constructor = Table + else: + a = Array([], Trivia()) + table_constructor = InlineTable + + for v in value: + if isinstance(v, dict): + table = table_constructor(Container(), Trivia(), True) + + for k, _v in sorted( + v.items(), + key=lambda i: (isinstance(i[1], dict), i[0] if _sort_keys else 1), + ): + i = item(_v, _parent=table, _sort_keys=_sort_keys) + if isinstance(table, InlineTable): + i.trivia.trail = "" + + table[k] = i + + v = table + + a.append(v) + + return a + elif isinstance(value, str): + return String.from_raw(value) + elif isinstance(value, datetime): + return DateTime( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.microsecond, + value.tzinfo, + Trivia(), + value.isoformat().replace("+00:00", "Z"), + ) + elif isinstance(value, date): + return Date(value.year, value.month, value.day, Trivia(), value.isoformat()) + elif isinstance(value, time): + return Time( + value.hour, + value.minute, + value.second, + value.microsecond, + value.tzinfo, + Trivia(), + value.isoformat(), + ) + else: + for encoder in CUSTOM_ENCODERS: + try: + rv = encoder(value) + except TypeError: + pass + else: + if not isinstance(rv, Item): + raise _ConvertError( + f"Custom encoder returned {type(rv)}, not a subclass of Item" + ) + return rv + + raise _ConvertError(f"Invalid type {type(value)}") + + +class StringType(Enum): + # Single Line Basic + SLB = '"' + # Multi Line Basic + MLB = '"""' + # Single Line Literal + SLL = "'" + # Multi Line Literal + MLL = "'''" + + @classmethod + def select(cls, literal=False, multiline=False) -> StringType: + return { + (False, False): cls.SLB, + (False, True): cls.MLB, + (True, False): cls.SLL, + (True, True): cls.MLL, + }[(literal, multiline)] + + @property + def escaped_sequences(self) -> Collection[str]: + # https://toml.io/en/v1.0.0#string + escaped_in_basic = CONTROL_CHARS | {"\\"} + allowed_in_multiline = {"\n", "\r"} + return { + StringType.SLB: escaped_in_basic | {'"'}, + StringType.MLB: (escaped_in_basic | {'"""'}) - allowed_in_multiline, + StringType.SLL: (), + StringType.MLL: (), + }[self] + + @property + def invalid_sequences(self) -> Collection[str]: + # https://toml.io/en/v1.0.0#string + forbidden_in_literal = CONTROL_CHARS - {"\t"} + allowed_in_multiline = {"\n", "\r"} + return { + StringType.SLB: (), + StringType.MLB: (), + StringType.SLL: forbidden_in_literal | {"'"}, + StringType.MLL: (forbidden_in_literal | {"'''"}) - allowed_in_multiline, + }[self] + + @property + def unit(self) -> str: + return self.value[0] + + def is_basic(self) -> bool: + return self in {StringType.SLB, StringType.MLB} + + def is_literal(self) -> bool: + return self in {StringType.SLL, StringType.MLL} + + def is_singleline(self) -> bool: + return self in {StringType.SLB, StringType.SLL} + + def is_multiline(self) -> bool: + return self in {StringType.MLB, StringType.MLL} + + def toggle(self) -> StringType: + return { + StringType.SLB: StringType.MLB, + StringType.MLB: StringType.SLB, + StringType.SLL: StringType.MLL, + StringType.MLL: StringType.SLL, + }[self] + + +class BoolType(Enum): + TRUE = "true" + FALSE = "false" + + def __bool__(self): + return {BoolType.TRUE: True, BoolType.FALSE: False}[self] + + def __iter__(self): + return iter(self.value) + + def __len__(self): + return len(self.value) + + +@dataclasses.dataclass +class Trivia: + """ + Trivia information (aka metadata). + """ + + # Whitespace before a value. + indent: str = "" + # Whitespace after a value, but before a comment. + comment_ws: str = "" + # Comment, starting with # character, or empty string if no comment. + comment: str = "" + # Trailing newline. + trail: str = "\n" + + def copy(self) -> Trivia: + return dataclasses.replace(self) + + +class KeyType(Enum): + """ + The type of a Key. + + Keys can be bare (unquoted), or quoted using basic ("), or literal (') + quotes following the same escaping rules as single-line StringType. + """ + + Bare = "" + Basic = '"' + Literal = "'" + + +class Key(abc.ABC): + """Base class for a key""" + + sep: str + _original: str + _keys: list[SingleKey] + _dotted: bool + key: str + + @abc.abstractmethod + def __hash__(self) -> int: + pass + + @abc.abstractmethod + def __eq__(self, __o: object) -> bool: + pass + + def is_dotted(self) -> bool: + """If the key is followed by other keys""" + return self._dotted + + def __iter__(self) -> Iterator[SingleKey]: + return iter(self._keys) + + def concat(self, other: Key) -> DottedKey: + """Concatenate keys into a dotted key""" + keys = self._keys + other._keys + return DottedKey(keys, sep=self.sep) + + def is_multi(self) -> bool: + """Check if the key contains multiple keys""" + return len(self._keys) > 1 + + def as_string(self) -> str: + """The TOML representation""" + return self._original + + def __str__(self) -> str: + return self.as_string() + + def __repr__(self) -> str: + return f"<Key {self.as_string()}>" + + +class SingleKey(Key): + """A single key""" + + def __init__( + self, + k: str, + t: KeyType | None = None, + sep: str | None = None, + original: str | None = None, + ) -> None: + if t is None: + if not k or any( + c not in string.ascii_letters + string.digits + "-" + "_" for c in k + ): + t = KeyType.Basic + else: + t = KeyType.Bare + + self.t = t + if sep is None: + sep = " = " + + self.sep = sep + self.key = k + if original is None: + key_str = escape_string(k) if t == KeyType.Basic else k + original = f"{t.value}{key_str}{t.value}" + + self._original = original + self._keys = [self] + self._dotted = False + + @property + def delimiter(self) -> str: + """The delimiter: double quote/single quote/none""" + return self.t.value + + def is_bare(self) -> bool: + """Check if the key is bare""" + return self.t == KeyType.Bare + + def __hash__(self) -> int: + return hash(self.key) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, Key): + return isinstance(other, SingleKey) and self.key == other.key + + return self.key == other + + +class DottedKey(Key): + def __init__( + self, + keys: Iterable[SingleKey], + sep: str | None = None, + original: str | None = None, + ) -> None: + self._keys = list(keys) + if original is None: + original = ".".join(k.as_string() for k in self._keys) + + self.sep = " = " if sep is None else sep + self._original = original + self._dotted = False + self.key = ".".join(k.key for k in self._keys) + + def __hash__(self) -> int: + return hash(tuple(self._keys)) + + def __eq__(self, __o: object) -> bool: + return isinstance(__o, DottedKey) and self._keys == __o._keys + + +class Item: + """ + An item within a TOML document. + """ + + def __init__(self, trivia: Trivia) -> None: + self._trivia = trivia + + @property + def trivia(self) -> Trivia: + """The trivia element associated with this item""" + return self._trivia + + @property + def discriminant(self) -> int: + raise NotImplementedError() + + def as_string(self) -> str: + """The TOML representation""" + raise NotImplementedError() + + @property + def value(self) -> Any: + return self + + def unwrap(self) -> Any: + """Returns as pure python object (ppo)""" + raise NotImplementedError() + + # Helpers + + def comment(self, comment: str) -> Item: + """Attach a comment to this item""" + if not comment.strip().startswith("#"): + comment = "# " + comment + + self._trivia.comment_ws = " " + self._trivia.comment = comment + + return self + + def indent(self, indent: int) -> Item: + """Indent this item with given number of spaces""" + if self._trivia.indent.startswith("\n"): + self._trivia.indent = "\n" + " " * indent + else: + self._trivia.indent = " " * indent + + return self + + def is_boolean(self) -> bool: + return isinstance(self, Bool) + + def is_table(self) -> bool: + return isinstance(self, Table) + + def is_inline_table(self) -> bool: + return isinstance(self, InlineTable) + + def is_aot(self) -> bool: + return isinstance(self, AoT) + + def _getstate(self, protocol=3): + return (self._trivia,) + + def __reduce__(self): + return self.__reduce_ex__(2) + + def __reduce_ex__(self, protocol): + return self.__class__, self._getstate(protocol) + + +class Whitespace(Item): + """ + A whitespace literal. + """ + + def __init__(self, s: str, fixed: bool = False) -> None: + self._s = s + self._fixed = fixed + + @property + def s(self) -> str: + return self._s + + @property + def value(self) -> str: + """The wrapped string of the whitespace""" + return self._s + + @property + def trivia(self) -> Trivia: + raise RuntimeError("Called trivia on a Whitespace variant.") + + @property + def discriminant(self) -> int: + return 0 + + def is_fixed(self) -> bool: + """If the whitespace is fixed, it can't be merged or discarded from the output.""" + return self._fixed + + def as_string(self) -> str: + return self._s + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {repr(self._s)}>" + + def _getstate(self, protocol=3): + return self._s, self._fixed + + +class Comment(Item): + """ + A comment literal. + """ + + @property + def discriminant(self) -> int: + return 1 + + def as_string(self) -> str: + return ( + f"{self._trivia.indent}{decode(self._trivia.comment)}{self._trivia.trail}" + ) + + def __str__(self) -> str: + return f"{self._trivia.indent}{decode(self._trivia.comment)}" + + +class Integer(Item, _CustomInt): + """ + An integer literal. + """ + + def __new__(cls, value: int, trivia: Trivia, raw: str) -> Integer: + return int.__new__(cls, value) + + def __init__(self, value: int, trivia: Trivia, raw: str) -> None: + super().__init__(trivia) + self._original = value + self._raw = raw + self._sign = False + + if re.match(r"^[+\-]\d+$", raw): + self._sign = True + + def unwrap(self) -> int: + return self._original + + __int__ = unwrap + + def __hash__(self) -> int: + return hash(self.unwrap()) + + @property + def discriminant(self) -> int: + return 2 + + @property + def value(self) -> int: + """The wrapped integer value""" + return self + + def as_string(self) -> str: + return self._raw + + def _new(self, result): + raw = str(result) + if self._sign: + sign = "+" if result >= 0 else "-" + raw = sign + raw + + return Integer(result, self._trivia, raw) + + def _getstate(self, protocol=3): + return int(self), self._trivia, self._raw + + # int methods + __abs__ = wrap_method(int.__abs__) + __add__ = wrap_method(int.__add__) + __and__ = wrap_method(int.__and__) + __ceil__ = wrap_method(int.__ceil__) + __eq__ = int.__eq__ + __floor__ = wrap_method(int.__floor__) + __floordiv__ = wrap_method(int.__floordiv__) + __invert__ = wrap_method(int.__invert__) + __le__ = int.__le__ + __lshift__ = wrap_method(int.__lshift__) + __lt__ = int.__lt__ + __mod__ = wrap_method(int.__mod__) + __mul__ = wrap_method(int.__mul__) + __neg__ = wrap_method(int.__neg__) + __or__ = wrap_method(int.__or__) + __pos__ = wrap_method(int.__pos__) + __pow__ = wrap_method(int.__pow__) + __radd__ = wrap_method(int.__radd__) + __rand__ = wrap_method(int.__rand__) + __rfloordiv__ = wrap_method(int.__rfloordiv__) + __rlshift__ = wrap_method(int.__rlshift__) + __rmod__ = wrap_method(int.__rmod__) + __rmul__ = wrap_method(int.__rmul__) + __ror__ = wrap_method(int.__ror__) + __round__ = wrap_method(int.__round__) + __rpow__ = wrap_method(int.__rpow__) + __rrshift__ = wrap_method(int.__rrshift__) + __rshift__ = wrap_method(int.__rshift__) + __rxor__ = wrap_method(int.__rxor__) + __trunc__ = wrap_method(int.__trunc__) + __xor__ = wrap_method(int.__xor__) + + def __rtruediv__(self, other): + result = int.__rtruediv__(self, other) + if result is NotImplemented: + return result + return Float._new(self, result) + + def __truediv__(self, other): + result = int.__truediv__(self, other) + if result is NotImplemented: + return result + return Float._new(self, result) + + +class Float(Item, _CustomFloat): + """ + A float literal. + """ + + def __new__(cls, value: float, trivia: Trivia, raw: str) -> Float: + return float.__new__(cls, value) + + def __init__(self, value: float, trivia: Trivia, raw: str) -> None: + super().__init__(trivia) + self._original = value + self._raw = raw + self._sign = False + + if re.match(r"^[+\-].+$", raw): + self._sign = True + + def unwrap(self) -> float: + return self._original + + __float__ = unwrap + + def __hash__(self) -> int: + return hash(self.unwrap()) + + @property + def discriminant(self) -> int: + return 3 + + @property + def value(self) -> float: + """The wrapped float value""" + return self + + def as_string(self) -> str: + return self._raw + + def _new(self, result): + raw = str(result) + + if self._sign: + sign = "+" if result >= 0 else "-" + raw = sign + raw + + return Float(result, self._trivia, raw) + + def _getstate(self, protocol=3): + return float(self), self._trivia, self._raw + + # float methods + __abs__ = wrap_method(float.__abs__) + __add__ = wrap_method(float.__add__) + __eq__ = float.__eq__ + __floordiv__ = wrap_method(float.__floordiv__) + __le__ = float.__le__ + __lt__ = float.__lt__ + __mod__ = wrap_method(float.__mod__) + __mul__ = wrap_method(float.__mul__) + __neg__ = wrap_method(float.__neg__) + __pos__ = wrap_method(float.__pos__) + __pow__ = wrap_method(float.__pow__) + __radd__ = wrap_method(float.__radd__) + __rfloordiv__ = wrap_method(float.__rfloordiv__) + __rmod__ = wrap_method(float.__rmod__) + __rmul__ = wrap_method(float.__rmul__) + __round__ = wrap_method(float.__round__) + __rpow__ = wrap_method(float.__rpow__) + __rtruediv__ = wrap_method(float.__rtruediv__) + __truediv__ = wrap_method(float.__truediv__) + __trunc__ = float.__trunc__ + + if sys.version_info >= (3, 9): + __ceil__ = float.__ceil__ + __floor__ = float.__floor__ + else: + __ceil__ = math.ceil + __floor__ = math.floor + + +class Bool(Item): + """ + A boolean literal. + """ + + def __init__(self, t: int, trivia: Trivia) -> None: + super().__init__(trivia) + + self._value = bool(t) + + def unwrap(self) -> bool: + return bool(self) + + @property + def discriminant(self) -> int: + return 4 + + @property + def value(self) -> bool: + """The wrapped boolean value""" + return self._value + + def as_string(self) -> str: + return str(self._value).lower() + + def _getstate(self, protocol=3): + return self._value, self._trivia + + def __bool__(self): + return self._value + + __nonzero__ = __bool__ + + def __eq__(self, other): + if not isinstance(other, bool): + return NotImplemented + + return other == self._value + + def __hash__(self): + return hash(self._value) + + def __repr__(self): + return repr(self._value) + + +class DateTime(Item, datetime): + """ + A datetime literal. + """ + + def __new__( + cls, + year: int, + month: int, + day: int, + hour: int, + minute: int, + second: int, + microsecond: int, + tzinfo: tzinfo | None, + *_: Any, + **kwargs: Any, + ) -> datetime: + return datetime.__new__( + cls, + year, + month, + day, + hour, + minute, + second, + microsecond, + tzinfo=tzinfo, + **kwargs, + ) + + def __init__( + self, + year: int, + month: int, + day: int, + hour: int, + minute: int, + second: int, + microsecond: int, + tzinfo: tzinfo | None, + trivia: Trivia | None = None, + raw: str | None = None, + **kwargs: Any, + ) -> None: + super().__init__(trivia or Trivia()) + + self._raw = raw or self.isoformat() + + def unwrap(self) -> datetime: + ( + year, + month, + day, + hour, + minute, + second, + microsecond, + tzinfo, + _, + _, + ) = self._getstate() + return datetime(year, month, day, hour, minute, second, microsecond, tzinfo) + + @property + def discriminant(self) -> int: + return 5 + + @property + def value(self) -> datetime: + return self + + def as_string(self) -> str: + return self._raw + + def __add__(self, other): + if PY38: + result = datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + self.tzinfo, + ).__add__(other) + else: + result = super().__add__(other) + + return self._new(result) + + def __sub__(self, other): + if PY38: + result = datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + self.tzinfo, + ).__sub__(other) + else: + result = super().__sub__(other) + + if isinstance(result, datetime): + result = self._new(result) + + return result + + def replace(self, *args: Any, **kwargs: Any) -> datetime: + return self._new(super().replace(*args, **kwargs)) + + def astimezone(self, tz: tzinfo) -> datetime: + result = super().astimezone(tz) + if PY38: + return result + return self._new(result) + + def _new(self, result) -> DateTime: + raw = result.isoformat() + + return DateTime( + result.year, + result.month, + result.day, + result.hour, + result.minute, + result.second, + result.microsecond, + result.tzinfo, + self._trivia, + raw, + ) + + def _getstate(self, protocol=3): + return ( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + self.tzinfo, + self._trivia, + self._raw, + ) + + +class Date(Item, date): + """ + A date literal. + """ + + def __new__(cls, year: int, month: int, day: int, *_: Any) -> date: + return date.__new__(cls, year, month, day) + + def __init__( + self, year: int, month: int, day: int, trivia: Trivia, raw: str + ) -> None: + super().__init__(trivia) + + self._raw = raw + + def unwrap(self) -> date: + (year, month, day, _, _) = self._getstate() + return date(year, month, day) + + @property + def discriminant(self) -> int: + return 6 + + @property + def value(self) -> date: + return self + + def as_string(self) -> str: + return self._raw + + def __add__(self, other): + if PY38: + result = date(self.year, self.month, self.day).__add__(other) + else: + result = super().__add__(other) + + return self._new(result) + + def __sub__(self, other): + if PY38: + result = date(self.year, self.month, self.day).__sub__(other) + else: + result = super().__sub__(other) + + if isinstance(result, date): + result = self._new(result) + + return result + + def replace(self, *args: Any, **kwargs: Any) -> date: + return self._new(super().replace(*args, **kwargs)) + + def _new(self, result): + raw = result.isoformat() + + return Date(result.year, result.month, result.day, self._trivia, raw) + + def _getstate(self, protocol=3): + return (self.year, self.month, self.day, self._trivia, self._raw) + + +class Time(Item, time): + """ + A time literal. + """ + + def __new__( + cls, + hour: int, + minute: int, + second: int, + microsecond: int, + tzinfo: tzinfo | None, + *_: Any, + ) -> time: + return time.__new__(cls, hour, minute, second, microsecond, tzinfo) + + def __init__( + self, + hour: int, + minute: int, + second: int, + microsecond: int, + tzinfo: tzinfo | None, + trivia: Trivia, + raw: str, + ) -> None: + super().__init__(trivia) + + self._raw = raw + + def unwrap(self) -> time: + (hour, minute, second, microsecond, tzinfo, _, _) = self._getstate() + return time(hour, minute, second, microsecond, tzinfo) + + @property + def discriminant(self) -> int: + return 7 + + @property + def value(self) -> time: + return self + + def as_string(self) -> str: + return self._raw + + def replace(self, *args: Any, **kwargs: Any) -> time: + return self._new(super().replace(*args, **kwargs)) + + def _new(self, result): + raw = result.isoformat() + + return Time( + result.hour, + result.minute, + result.second, + result.microsecond, + result.tzinfo, + self._trivia, + raw, + ) + + def _getstate(self, protocol: int = 3) -> tuple: + return ( + self.hour, + self.minute, + self.second, + self.microsecond, + self.tzinfo, + self._trivia, + self._raw, + ) + + +class _ArrayItemGroup: + __slots__ = ("value", "indent", "comma", "comment") + + def __init__( + self, + value: Item | None = None, + indent: Whitespace | None = None, + comma: Whitespace | None = None, + comment: Comment | None = None, + ) -> None: + self.value = value + self.indent = indent + self.comma = comma + self.comment = comment + + def __iter__(self) -> Iterator[Item]: + return filter( + lambda x: x is not None, (self.indent, self.value, self.comma, self.comment) + ) + + def __repr__(self) -> str: + return repr(tuple(self)) + + def is_whitespace(self) -> bool: + return self.value is None and self.comment is None + + def __bool__(self) -> bool: + try: + next(iter(self)) + except StopIteration: + return False + return True + + +class Array(Item, _CustomList): + """ + An array literal + """ + + def __init__( + self, value: list[Item], trivia: Trivia, multiline: bool = False + ) -> None: + super().__init__(trivia) + list.__init__( + self, + [v for v in value if not isinstance(v, (Whitespace, Comment, Null))], + ) + self._index_map: dict[int, int] = {} + self._value = self._group_values(value) + self._multiline = multiline + self._reindex() + + def _group_values(self, value: list[Item]) -> list[_ArrayItemGroup]: + """Group the values into (indent, value, comma, comment) tuples""" + groups = [] + this_group = _ArrayItemGroup() + for item in value: + if isinstance(item, Whitespace): + if "," not in item.s: + groups.append(this_group) + this_group = _ArrayItemGroup(indent=item) + else: + if this_group.value is None: + # when comma is met and no value is provided, add a dummy Null + this_group.value = Null() + this_group.comma = item + elif isinstance(item, Comment): + if this_group.value is None: + this_group.value = Null() + this_group.comment = item + elif this_group.value is None: + this_group.value = item + else: + groups.append(this_group) + this_group = _ArrayItemGroup(value=item) + groups.append(this_group) + return [group for group in groups if group] + + def unwrap(self) -> list[Any]: + unwrapped = [] + for v in self: + if hasattr(v, "unwrap"): + unwrapped.append(v.unwrap()) + else: + unwrapped.append(v) + return unwrapped + + @property + def discriminant(self) -> int: + return 8 + + @property + def value(self) -> list: + return self + + def _iter_items(self) -> Iterator[Item]: + for v in self._value: + yield from v + + def multiline(self, multiline: bool) -> Array: + """Change the array to display in multiline or not. + + :Example: + + >>> a = item([1, 2, 3]) + >>> print(a.as_string()) + [1, 2, 3] + >>> print(a.multiline(True).as_string()) + [ + 1, + 2, + 3, + ] + """ + self._multiline = multiline + + return self + + def as_string(self) -> str: + if not self._multiline or not self._value: + return f'[{"".join(v.as_string() for v in self._iter_items())}]' + + s = "[\n" + s += "".join( + self.trivia.indent + + " " * 4 + + v.value.as_string() + + ("," if not isinstance(v.value, Null) else "") + + (v.comment.as_string() if v.comment is not None else "") + + "\n" + for v in self._value + if v.value is not None + ) + s += self.trivia.indent + "]" + + return s + + def _reindex(self) -> None: + self._index_map.clear() + index = 0 + for i, v in enumerate(self._value): + if v.value is None or isinstance(v.value, Null): + continue + self._index_map[index] = i + index += 1 + + def add_line( + self, + *items: Any, + indent: str = " ", + comment: str | None = None, + add_comma: bool = True, + newline: bool = True, + ) -> None: + """Add multiple items in a line to control the format precisely. + When add_comma is True, only accept actual values and + ", " will be added between values automatically. + + :Example: + + >>> a = array() + >>> a.add_line(1, 2, 3) + >>> a.add_line(4, 5, 6) + >>> a.add_line(indent="") + >>> print(a.as_string()) + [ + 1, 2, 3, + 4, 5, 6, + ] + """ + new_values: list[Item] = [] + first_indent = f"\n{indent}" if newline else indent + if first_indent: + new_values.append(Whitespace(first_indent)) + whitespace = "" + data_values = [] + for i, el in enumerate(items): + it = item(el, _parent=self) + if isinstance(it, Comment) or add_comma and isinstance(el, Whitespace): + raise ValueError(f"item type {type(it)} is not allowed in add_line") + if not isinstance(it, Whitespace): + if whitespace: + new_values.append(Whitespace(whitespace)) + whitespace = "" + new_values.append(it) + data_values.append(it.value) + if add_comma: + new_values.append(Whitespace(",")) + if i != len(items) - 1: + new_values.append(Whitespace(" ")) + elif "," not in it.s: + whitespace += it.s + else: + new_values.append(it) + if whitespace: + new_values.append(Whitespace(whitespace)) + if comment: + indent = " " if items else "" + new_values.append( + Comment(Trivia(indent=indent, comment=f"# {comment}", trail="")) + ) + list.extend(self, data_values) + if len(self._value) > 0: + last_item = self._value[-1] + last_value_item = next( + ( + v + for v in self._value[::-1] + if v.value is not None and not isinstance(v.value, Null) + ), + None, + ) + if last_value_item is not None: + last_value_item.comma = Whitespace(",") + if last_item.is_whitespace(): + self._value[-1:-1] = self._group_values(new_values) + else: + self._value.extend(self._group_values(new_values)) + else: + self._value.extend(self._group_values(new_values)) + self._reindex() + + def clear(self) -> None: + """Clear the array.""" + list.clear(self) + self._index_map.clear() + self._value.clear() + + def __len__(self) -> int: + return list.__len__(self) + + def __getitem__(self, key: int | slice) -> Any: + rv = cast(Item, list.__getitem__(self, key)) + if rv.is_boolean(): + return bool(rv) + return rv + + def __setitem__(self, key: int | slice, value: Any) -> Any: + it = item(value, _parent=self) + list.__setitem__(self, key, it) + if isinstance(key, slice): + raise ValueError("slice assignment is not supported") + if key < 0: + key += len(self) + self._value[self._index_map[key]].value = it + + def insert(self, pos: int, value: Any) -> None: + it = item(value, _parent=self) + length = len(self) + if not isinstance(it, (Comment, Whitespace)): + list.insert(self, pos, it) + if pos < 0: + pos += length + if pos < 0: + pos = 0 + + idx = 0 # insert position of the self._value list + default_indent = " " + if pos < length: + try: + idx = self._index_map[pos] + except KeyError as e: + raise IndexError("list index out of range") from e + else: + idx = len(self._value) + if idx >= 1 and self._value[idx - 1].is_whitespace(): + # The last item is a pure whitespace(\n ), insert before it + idx -= 1 + if ( + self._value[idx].indent is not None + and "\n" in self._value[idx].indent.s + ): + default_indent = "\n " + indent: Item | None = None + comma: Item | None = Whitespace(",") if pos < length else None + if idx < len(self._value) and not self._value[idx].is_whitespace(): + # Prefer to copy the indentation from the item after + indent = self._value[idx].indent + if idx > 0: + last_item = self._value[idx - 1] + if indent is None: + indent = last_item.indent + if not isinstance(last_item.value, Null) and "\n" in default_indent: + # Copy the comma from the last item if 1) it contains a value and + # 2) the array is multiline + comma = last_item.comma + if last_item.comma is None and not isinstance(last_item.value, Null): + # Add comma to the last item to separate it from the following items. + last_item.comma = Whitespace(",") + if indent is None and (idx > 0 or "\n" in default_indent): + # apply default indent if it isn't the first item or the array is multiline. + indent = Whitespace(default_indent) + new_item = _ArrayItemGroup(value=it, indent=indent, comma=comma) + self._value.insert(idx, new_item) + self._reindex() + + def __delitem__(self, key: int | slice): + length = len(self) + list.__delitem__(self, key) + + if isinstance(key, slice): + indices_to_remove = list( + range(key.start or 0, key.stop or length, key.step or 1) + ) + else: + indices_to_remove = [length + key if key < 0 else key] + for i in sorted(indices_to_remove, reverse=True): + try: + idx = self._index_map[i] + except KeyError as e: + if not isinstance(key, slice): + raise IndexError("list index out of range") from e + else: + del self._value[idx] + if ( + idx == 0 + and len(self._value) > 0 + and "\n" not in self._value[idx].indent.s + ): + # Remove the indentation of the first item if not newline + self._value[idx].indent = None + if len(self._value) > 0: + v = self._value[-1] + if not v.is_whitespace(): + # remove the comma of the last item + v.comma = None + + self._reindex() + + def _getstate(self, protocol=3): + return list(self._iter_items()), self._trivia, self._multiline + + +class AbstractTable(Item, _CustomDict): + """Common behaviour of both :class:`Table` and :class:`InlineTable`""" + + def __init__(self, value: container.Container, trivia: Trivia): + Item.__init__(self, trivia) + + self._value = value + + for k, v in self._value.body: + if k is not None: + dict.__setitem__(self, k.key, v) + + def unwrap(self) -> dict[str, Any]: + unwrapped = {} + for k, v in self.items(): + if isinstance(k, Key): + k = k.key + if hasattr(v, "unwrap"): + v = v.unwrap() + unwrapped[k] = v + + return unwrapped + + @property + def value(self) -> container.Container: + return self._value + + @overload + def append(self: AT, key: None, value: Comment | Whitespace) -> AT: + ... + + @overload + def append(self: AT, key: Key | str, value: Any) -> AT: + ... + + def append(self, key, value): + raise NotImplementedError + + @overload + def add(self: AT, key: Comment | Whitespace) -> AT: + ... + + @overload + def add(self: AT, key: Key | str, value: Any = ...) -> AT: + ... + + def add(self, key, value=None): + if value is None: + if not isinstance(key, (Comment, Whitespace)): + msg = "Non comment/whitespace items must have an associated key" + raise ValueError(msg) + + key, value = None, key + + return self.append(key, value) + + def remove(self: AT, key: Key | str) -> AT: + self._value.remove(key) + + if isinstance(key, Key): + key = key.key + + if key is not None: + dict.__delitem__(self, key) + + return self + + def setdefault(self, key: Key | str, default: Any) -> Any: + super().setdefault(key, default) + return self[key] + + def __str__(self): + return str(self.value) + + def copy(self: AT) -> AT: + return copy.copy(self) + + def __repr__(self) -> str: + return repr(self.value) + + def __iter__(self) -> Iterator[str]: + return iter(self._value) + + def __len__(self) -> int: + return len(self._value) + + def __delitem__(self, key: Key | str) -> None: + self.remove(key) + + def __getitem__(self, key: Key | str) -> Item: + return cast(Item, self._value[key]) + + def __setitem__(self, key: Key | str, value: Any) -> None: + if not isinstance(value, Item): + value = item(value, _parent=self) + + is_replace = key in self + self._value[key] = value + + if key is not None: + dict.__setitem__(self, key, value) + + if is_replace: + return + m = re.match("(?s)^[^ ]*([ ]+).*$", self._trivia.indent) + if not m: + return + + indent = m.group(1) + + if not isinstance(value, Whitespace): + m = re.match("(?s)^([^ ]*)(.*)$", value.trivia.indent) + if not m: + value.trivia.indent = indent + else: + value.trivia.indent = m.group(1) + indent + m.group(2) + + +class Table(AbstractTable): + """ + A table literal. + """ + + def __init__( + self, + value: container.Container, + trivia: Trivia, + is_aot_element: bool, + is_super_table: bool | None = None, + name: str | None = None, + display_name: str | None = None, + ) -> None: + super().__init__(value, trivia) + + self.name = name + self.display_name = display_name + self._is_aot_element = is_aot_element + self._is_super_table = is_super_table + + @property + def discriminant(self) -> int: + return 9 + + def __copy__(self) -> Table: + return type(self)( + self._value.copy(), + self._trivia.copy(), + self._is_aot_element, + self._is_super_table, + self.name, + self.display_name, + ) + + def append(self, key: Key | str | None, _item: Any) -> Table: + """ + Appends a (key, item) to the table. + """ + if not isinstance(_item, Item): + _item = item(_item, _parent=self) + + self._value.append(key, _item) + + if isinstance(key, Key): + key = next(iter(key)).key + _item = self._value[key] + + if key is not None: + dict.__setitem__(self, key, _item) + + m = re.match(r"(?s)^[^ ]*([ ]+).*$", self._trivia.indent) + if not m: + return self + + indent = m.group(1) + + if not isinstance(_item, Whitespace): + m = re.match("(?s)^([^ ]*)(.*)$", _item.trivia.indent) + if not m: + _item.trivia.indent = indent + else: + _item.trivia.indent = m.group(1) + indent + m.group(2) + + return self + + def raw_append(self, key: Key | str | None, _item: Any) -> Table: + """Similar to :meth:`append` but does not copy indentation.""" + if not isinstance(_item, Item): + _item = item(_item) + + self._value.append(key, _item, validate=False) + + if isinstance(key, Key): + key = next(iter(key)).key + _item = self._value[key] + + if key is not None: + dict.__setitem__(self, key, _item) + + return self + + def is_aot_element(self) -> bool: + """True if the table is the direct child of an AOT element.""" + return self._is_aot_element + + def is_super_table(self) -> bool: + """A super table is the intermediate parent of a nested table as in [a.b.c]. + If true, it won't appear in the TOML representation.""" + if self._is_super_table is not None: + return self._is_super_table + # If the table has only one child and that child is a table, then it is a super table. + if len(self) != 1: + return False + only_child = next(iter(self.values())) + return isinstance(only_child, (Table, AoT)) + + def as_string(self) -> str: + return self._value.as_string() + + # Helpers + + def indent(self, indent: int) -> Table: + """Indent the table with given number of spaces.""" + super().indent(indent) + + m = re.match("(?s)^[^ ]*([ ]+).*$", self._trivia.indent) + if not m: + indent_str = "" + else: + indent_str = m.group(1) + + for _, item in self._value.body: + if not isinstance(item, Whitespace): + item.trivia.indent = indent_str + item.trivia.indent + + return self + + def invalidate_display_name(self): + """Call ``invalidate_display_name`` on the contained tables""" + self.display_name = None + + for child in self.values(): + if hasattr(child, "invalidate_display_name"): + child.invalidate_display_name() + + def _getstate(self, protocol: int = 3) -> tuple: + return ( + self._value, + self._trivia, + self._is_aot_element, + self._is_super_table, + self.name, + self.display_name, + ) + + +class InlineTable(AbstractTable): + """ + An inline table literal. + """ + + def __init__( + self, value: container.Container, trivia: Trivia, new: bool = False + ) -> None: + super().__init__(value, trivia) + + self._new = new + + @property + def discriminant(self) -> int: + return 10 + + def append(self, key: Key | str | None, _item: Any) -> InlineTable: + """ + Appends a (key, item) to the table. + """ + if not isinstance(_item, Item): + _item = item(_item, _parent=self) + + if not isinstance(_item, (Whitespace, Comment)): + if not _item.trivia.indent and len(self._value) > 0 and not self._new: + _item.trivia.indent = " " + if _item.trivia.comment: + _item.trivia.comment = "" + + self._value.append(key, _item) + + if isinstance(key, Key): + key = key.key + + if key is not None: + dict.__setitem__(self, key, _item) + + return self + + def as_string(self) -> str: + buf = "{" + last_item_idx = next( + ( + i + for i in range(len(self._value.body) - 1, -1, -1) + if self._value.body[i][0] is not None + ), + None, + ) + for i, (k, v) in enumerate(self._value.body): + if k is None: + if i == len(self._value.body) - 1: + if self._new: + buf = buf.rstrip(", ") + else: + buf = buf.rstrip(",") + + buf += v.as_string() + + continue + + v_trivia_trail = v.trivia.trail.replace("\n", "") + buf += ( + f"{v.trivia.indent}" + f'{k.as_string() + ("." if k.is_dotted() else "")}' + f"{k.sep}" + f"{v.as_string()}" + f"{v.trivia.comment}" + f"{v_trivia_trail}" + ) + + if last_item_idx is not None and i < last_item_idx: + buf += "," + if self._new: + buf += " " + + buf += "}" + + return buf + + def __setitem__(self, key: Key | str, value: Any) -> None: + if hasattr(value, "trivia") and value.trivia.comment: + value.trivia.comment = "" + super().__setitem__(key, value) + + def __copy__(self) -> InlineTable: + return type(self)(self._value.copy(), self._trivia.copy(), self._new) + + def _getstate(self, protocol: int = 3) -> tuple: + return (self._value, self._trivia) + + +class String(str, Item): + """ + A string literal. + """ + + def __new__(cls, t, value, original, trivia): + return super().__new__(cls, value) + + def __init__(self, t: StringType, _: str, original: str, trivia: Trivia) -> None: + super().__init__(trivia) + + self._t = t + self._original = original + + def unwrap(self) -> str: + return str(self) + + @property + def discriminant(self) -> int: + return 11 + + @property + def value(self) -> str: + return self + + def as_string(self) -> str: + return f"{self._t.value}{decode(self._original)}{self._t.value}" + + def __add__(self: ItemT, other: str) -> ItemT: + if not isinstance(other, str): + return NotImplemented + result = super().__add__(other) + original = self._original + getattr(other, "_original", other) + + return self._new(result, original) + + def _new(self, result: str, original: str) -> String: + return String(self._t, result, original, self._trivia) + + def _getstate(self, protocol=3): + return self._t, str(self), self._original, self._trivia + + @classmethod + def from_raw(cls, value: str, type_=StringType.SLB, escape=True) -> String: + value = decode(value) + + invalid = type_.invalid_sequences + if any(c in value for c in invalid): + raise InvalidStringError(value, invalid, type_.value) + + escaped = type_.escaped_sequences + string_value = escape_string(value, escaped) if escape and escaped else value + + return cls(type_, decode(value), string_value, Trivia()) + + +class AoT(Item, _CustomList): + """ + An array of table literal + """ + + def __init__( + self, body: list[Table], name: str | None = None, parsed: bool = False + ) -> None: + self.name = name + self._body: list[Table] = [] + self._parsed = parsed + + super().__init__(Trivia(trail="")) + + for table in body: + self.append(table) + + def unwrap(self) -> list[dict[str, Any]]: + unwrapped = [] + for t in self._body: + if hasattr(t, "unwrap"): + unwrapped.append(t.unwrap()) + else: + unwrapped.append(t) + return unwrapped + + @property + def body(self) -> list[Table]: + return self._body + + @property + def discriminant(self) -> int: + return 12 + + @property + def value(self) -> list[dict[Any, Any]]: + return [v.value for v in self._body] + + def __len__(self) -> int: + return len(self._body) + + @overload + def __getitem__(self, key: slice) -> list[Table]: + ... + + @overload + def __getitem__(self, key: int) -> Table: + ... + + def __getitem__(self, key): + return self._body[key] + + def __setitem__(self, key: slice | int, value: Any) -> None: + raise NotImplementedError + + def __delitem__(self, key: slice | int) -> None: + del self._body[key] + list.__delitem__(self, key) + + def insert(self, index: int, value: dict) -> None: + value = item(value, _parent=self) + if not isinstance(value, Table): + raise ValueError(f"Unsupported insert value type: {type(value)}") + length = len(self) + if index < 0: + index += length + if index < 0: + index = 0 + elif index >= length: + index = length + m = re.match("(?s)^[^ ]*([ ]+).*$", self._trivia.indent) + if m: + indent = m.group(1) + + m = re.match("(?s)^([^ ]*)(.*)$", value.trivia.indent) + if not m: + value.trivia.indent = indent + else: + value.trivia.indent = m.group(1) + indent + m.group(2) + prev_table = self._body[index - 1] if 0 < index and length else None + next_table = self._body[index + 1] if index < length - 1 else None + if not self._parsed: + if prev_table and "\n" not in value.trivia.indent: + value.trivia.indent = "\n" + value.trivia.indent + if next_table and "\n" not in next_table.trivia.indent: + next_table.trivia.indent = "\n" + next_table.trivia.indent + self._body.insert(index, value) + list.insert(self, index, value) + + def invalidate_display_name(self): + """Call ``invalidate_display_name`` on the contained tables""" + for child in self: + if hasattr(child, "invalidate_display_name"): + child.invalidate_display_name() + + def as_string(self) -> str: + b = "" + for table in self._body: + b += table.as_string() + + return b + + def __repr__(self) -> str: + return f"<AoT {self.value}>" + + def _getstate(self, protocol=3): + return self._body, self.name, self._parsed + + +class Null(Item): + """ + A null item. + """ + + def __init__(self) -> None: + super().__init__(Trivia(trail="")) + + def unwrap(self) -> None: + return None + + @property + def discriminant(self) -> int: + return -1 + + @property + def value(self) -> None: + return None + + def as_string(self) -> str: + return "" + + def _getstate(self, protocol=3) -> tuple: + return () diff --git a/third_party/python/tomlkit/tomlkit/parser.py b/third_party/python/tomlkit/tomlkit/parser.py new file mode 100644 index 0000000000..89ddae2337 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/parser.py @@ -0,0 +1,1141 @@ +from __future__ import annotations + +import datetime +import re +import string + +from tomlkit._compat import decode +from tomlkit._utils import RFC_3339_LOOSE +from tomlkit._utils import _escaped +from tomlkit._utils import parse_rfc3339 +from tomlkit.container import Container +from tomlkit.exceptions import EmptyKeyError +from tomlkit.exceptions import EmptyTableNameError +from tomlkit.exceptions import InternalParserError +from tomlkit.exceptions import InvalidCharInStringError +from tomlkit.exceptions import InvalidControlChar +from tomlkit.exceptions import InvalidDateError +from tomlkit.exceptions import InvalidDateTimeError +from tomlkit.exceptions import InvalidNumberError +from tomlkit.exceptions import InvalidTimeError +from tomlkit.exceptions import InvalidUnicodeValueError +from tomlkit.exceptions import ParseError +from tomlkit.exceptions import UnexpectedCharError +from tomlkit.exceptions import UnexpectedEofError +from tomlkit.items import AoT +from tomlkit.items import Array +from tomlkit.items import Bool +from tomlkit.items import BoolType +from tomlkit.items import Comment +from tomlkit.items import Date +from tomlkit.items import DateTime +from tomlkit.items import Float +from tomlkit.items import InlineTable +from tomlkit.items import Integer +from tomlkit.items import Item +from tomlkit.items import Key +from tomlkit.items import KeyType +from tomlkit.items import Null +from tomlkit.items import SingleKey +from tomlkit.items import String +from tomlkit.items import StringType +from tomlkit.items import Table +from tomlkit.items import Time +from tomlkit.items import Trivia +from tomlkit.items import Whitespace +from tomlkit.source import Source +from tomlkit.toml_char import TOMLChar +from tomlkit.toml_document import TOMLDocument + + +CTRL_I = 0x09 # Tab +CTRL_J = 0x0A # Line feed +CTRL_M = 0x0D # Carriage return +CTRL_CHAR_LIMIT = 0x1F +CHR_DEL = 0x7F + + +class Parser: + """ + Parser for TOML documents. + """ + + def __init__(self, string: str | bytes) -> None: + # Input to parse + self._src = Source(decode(string)) + + self._aot_stack: list[Key] = [] + + @property + def _state(self): + return self._src.state + + @property + def _idx(self): + return self._src.idx + + @property + def _current(self): + return self._src.current + + @property + def _marker(self): + return self._src.marker + + def extract(self) -> str: + """ + Extracts the value between marker and index + """ + return self._src.extract() + + def inc(self, exception: type[ParseError] | None = None) -> bool: + """ + Increments the parser if the end of the input has not been reached. + Returns whether or not it was able to advance. + """ + return self._src.inc(exception=exception) + + def inc_n(self, n: int, exception: type[ParseError] | None = None) -> bool: + """ + Increments the parser by n characters + if the end of the input has not been reached. + """ + return self._src.inc_n(n=n, exception=exception) + + def consume(self, chars, min=0, max=-1): + """ + Consume chars until min/max is satisfied is valid. + """ + return self._src.consume(chars=chars, min=min, max=max) + + def end(self) -> bool: + """ + Returns True if the parser has reached the end of the input. + """ + return self._src.end() + + def mark(self) -> None: + """ + Sets the marker to the index's current position + """ + self._src.mark() + + def parse_error(self, exception=ParseError, *args, **kwargs): + """ + Creates a generic "parse error" at the current position. + """ + return self._src.parse_error(exception, *args, **kwargs) + + def parse(self) -> TOMLDocument: + body = TOMLDocument(True) + + # Take all keyvals outside of tables/AoT's. + while not self.end(): + # Break out if a table is found + if self._current == "[": + break + + # Otherwise, take and append one KV + item = self._parse_item() + if not item: + break + + key, value = item + if (key is not None and key.is_multi()) or not self._merge_ws(value, body): + # We actually have a table + try: + body.append(key, value) + except Exception as e: + raise self.parse_error(ParseError, str(e)) from e + + self.mark() + + while not self.end(): + key, value = self._parse_table() + if isinstance(value, Table) and value.is_aot_element(): + # This is just the first table in an AoT. Parse the rest of the array + # along with it. + value = self._parse_aot(value, key) + + try: + body.append(key, value) + except Exception as e: + raise self.parse_error(ParseError, str(e)) from e + + body.parsing(False) + + return body + + def _merge_ws(self, item: Item, container: Container) -> bool: + """ + Merges the given Item with the last one currently in the given Container if + both are whitespace items. + + Returns True if the items were merged. + """ + last = container.last_item() + if not last: + return False + + if not isinstance(item, Whitespace) or not isinstance(last, Whitespace): + return False + + start = self._idx - (len(last.s) + len(item.s)) + container.body[-1] = ( + container.body[-1][0], + Whitespace(self._src[start : self._idx]), + ) + + return True + + def _is_child(self, parent: Key, child: Key) -> bool: + """ + Returns whether a key is strictly a child of another key. + AoT siblings are not considered children of one another. + """ + parent_parts = tuple(parent) + child_parts = tuple(child) + + if parent_parts == child_parts: + return False + + return parent_parts == child_parts[: len(parent_parts)] + + def _parse_item(self) -> tuple[Key | None, Item] | None: + """ + Attempts to parse the next item and returns it, along with its key + if the item is value-like. + """ + self.mark() + with self._state as state: + while True: + c = self._current + if c == "\n": + # Found a newline; Return all whitespace found up to this point. + self.inc() + + return None, Whitespace(self.extract()) + elif c in " \t\r": + # Skip whitespace. + if not self.inc(): + return None, Whitespace(self.extract()) + elif c == "#": + # Found a comment, parse it + indent = self.extract() + cws, comment, trail = self._parse_comment_trail() + + return None, Comment(Trivia(indent, cws, comment, trail)) + elif c == "[": + # Found a table, delegate to the calling function. + return + else: + # Beginning of a KV pair. + # Return to beginning of whitespace so it gets included + # as indentation for the KV about to be parsed. + state.restore = True + break + + return self._parse_key_value(True) + + def _parse_comment_trail(self, parse_trail: bool = True) -> tuple[str, str, str]: + """ + Returns (comment_ws, comment, trail) + If there is no comment, comment_ws and comment will + simply be empty. + """ + if self.end(): + return "", "", "" + + comment = "" + comment_ws = "" + self.mark() + + while True: + c = self._current + + if c == "\n": + break + elif c == "#": + comment_ws = self.extract() + + self.mark() + self.inc() # Skip # + + # The comment itself + while not self.end() and not self._current.is_nl(): + code = ord(self._current) + if code == CHR_DEL or code <= CTRL_CHAR_LIMIT and code != CTRL_I: + raise self.parse_error(InvalidControlChar, code, "comments") + + if not self.inc(): + break + + comment = self.extract() + self.mark() + + break + elif c in " \t\r": + self.inc() + else: + raise self.parse_error(UnexpectedCharError, c) + + if self.end(): + break + + trail = "" + if parse_trail: + while self._current.is_spaces() and self.inc(): + pass + + if self._current == "\r": + self.inc() + + if self._current == "\n": + self.inc() + + if self._idx != self._marker or self._current.is_ws(): + trail = self.extract() + + return comment_ws, comment, trail + + def _parse_key_value(self, parse_comment: bool = False) -> tuple[Key, Item]: + # Leading indent + self.mark() + + while self._current.is_spaces() and self.inc(): + pass + + indent = self.extract() + + # Key + key = self._parse_key() + + self.mark() + + found_equals = self._current == "=" + while self._current.is_kv_sep() and self.inc(): + if self._current == "=": + if found_equals: + raise self.parse_error(UnexpectedCharError, "=") + else: + found_equals = True + if not found_equals: + raise self.parse_error(UnexpectedCharError, self._current) + + if not key.sep: + key.sep = self.extract() + else: + key.sep += self.extract() + + # Value + val = self._parse_value() + # Comment + if parse_comment: + cws, comment, trail = self._parse_comment_trail() + meta = val.trivia + if not meta.comment_ws: + meta.comment_ws = cws + + meta.comment = comment + meta.trail = trail + else: + val.trivia.trail = "" + + val.trivia.indent = indent + + return key, val + + def _parse_key(self) -> Key: + """ + Parses a Key at the current position; + WS before the key must be exhausted first at the callsite. + """ + self.mark() + while self._current.is_spaces() and self.inc(): + # Skip any leading whitespace + pass + if self._current in "\"'": + return self._parse_quoted_key() + else: + return self._parse_bare_key() + + def _parse_quoted_key(self) -> Key: + """ + Parses a key enclosed in either single or double quotes. + """ + # Extract the leading whitespace + original = self.extract() + quote_style = self._current + key_type = next((t for t in KeyType if t.value == quote_style), None) + + if key_type is None: + raise RuntimeError("Should not have entered _parse_quoted_key()") + + key_str = self._parse_string( + StringType.SLB if key_type == KeyType.Basic else StringType.SLL + ) + if key_str._t.is_multiline(): + raise self.parse_error(UnexpectedCharError, key_str._t.value) + original += key_str.as_string() + self.mark() + while self._current.is_spaces() and self.inc(): + pass + original += self.extract() + key = SingleKey(str(key_str), t=key_type, sep="", original=original) + if self._current == ".": + self.inc() + key = key.concat(self._parse_key()) + + return key + + def _parse_bare_key(self) -> Key: + """ + Parses a bare key. + """ + while ( + self._current.is_bare_key_char() or self._current.is_spaces() + ) and self.inc(): + pass + + original = self.extract() + key = original.strip() + if not key: + # Empty key + raise self.parse_error(EmptyKeyError) + + if " " in key: + # Bare key with spaces in it + raise self.parse_error(ParseError, f'Invalid key "{key}"') + + key = SingleKey(key, KeyType.Bare, "", original) + + if self._current == ".": + self.inc() + key = key.concat(self._parse_key()) + + return key + + def _parse_value(self) -> Item: + """ + Attempts to parse a value at the current position. + """ + self.mark() + c = self._current + trivia = Trivia() + + if c == StringType.SLB.value: + return self._parse_basic_string() + elif c == StringType.SLL.value: + return self._parse_literal_string() + elif c == BoolType.TRUE.value[0]: + return self._parse_true() + elif c == BoolType.FALSE.value[0]: + return self._parse_false() + elif c == "[": + return self._parse_array() + elif c == "{": + return self._parse_inline_table() + elif c in "+-" or self._peek(4) in { + "+inf", + "-inf", + "inf", + "+nan", + "-nan", + "nan", + }: + # Number + while self._current not in " \t\n\r#,]}" and self.inc(): + pass + + raw = self.extract() + + item = self._parse_number(raw, trivia) + if item is not None: + return item + + raise self.parse_error(InvalidNumberError) + elif c in string.digits: + # Integer, Float, Date, Time or DateTime + while self._current not in " \t\n\r#,]}" and self.inc(): + pass + + raw = self.extract() + + m = RFC_3339_LOOSE.match(raw) + if m: + if m.group(1) and m.group(5): + # datetime + try: + dt = parse_rfc3339(raw) + assert isinstance(dt, datetime.datetime) + return DateTime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + trivia, + raw, + ) + except ValueError: + raise self.parse_error(InvalidDateTimeError) + + if m.group(1): + try: + dt = parse_rfc3339(raw) + assert isinstance(dt, datetime.date) + date = Date(dt.year, dt.month, dt.day, trivia, raw) + self.mark() + while self._current not in "\t\n\r#,]}" and self.inc(): + pass + + time_raw = self.extract() + time_part = time_raw.rstrip() + trivia.comment_ws = time_raw[len(time_part) :] + if not time_part: + return date + + dt = parse_rfc3339(raw + time_part) + assert isinstance(dt, datetime.datetime) + return DateTime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + trivia, + raw + time_part, + ) + except ValueError: + raise self.parse_error(InvalidDateError) + + if m.group(5): + try: + t = parse_rfc3339(raw) + assert isinstance(t, datetime.time) + return Time( + t.hour, + t.minute, + t.second, + t.microsecond, + t.tzinfo, + trivia, + raw, + ) + except ValueError: + raise self.parse_error(InvalidTimeError) + + item = self._parse_number(raw, trivia) + if item is not None: + return item + + raise self.parse_error(InvalidNumberError) + else: + raise self.parse_error(UnexpectedCharError, c) + + def _parse_true(self): + return self._parse_bool(BoolType.TRUE) + + def _parse_false(self): + return self._parse_bool(BoolType.FALSE) + + def _parse_bool(self, style: BoolType) -> Bool: + with self._state: + style = BoolType(style) + + # only keep parsing for bool if the characters match the style + # try consuming rest of chars in style + for c in style: + self.consume(c, min=1, max=1) + + return Bool(style, Trivia()) + + def _parse_array(self) -> Array: + # Consume opening bracket, EOF here is an issue (middle of array) + self.inc(exception=UnexpectedEofError) + + elems: list[Item] = [] + prev_value = None + while True: + # consume whitespace + mark = self._idx + self.consume(TOMLChar.SPACES + TOMLChar.NL) + indent = self._src[mark : self._idx] + newline = set(TOMLChar.NL) & set(indent) + if newline: + elems.append(Whitespace(indent)) + continue + + # consume comment + if self._current == "#": + cws, comment, trail = self._parse_comment_trail(parse_trail=False) + elems.append(Comment(Trivia(indent, cws, comment, trail))) + continue + + # consume indent + if indent: + elems.append(Whitespace(indent)) + continue + + # consume value + if not prev_value: + try: + elems.append(self._parse_value()) + prev_value = True + continue + except UnexpectedCharError: + pass + + # consume comma + if prev_value and self._current == ",": + self.inc(exception=UnexpectedEofError) + elems.append(Whitespace(",")) + prev_value = False + continue + + # consume closing bracket + if self._current == "]": + # consume closing bracket, EOF here doesn't matter + self.inc() + break + + raise self.parse_error(UnexpectedCharError, self._current) + + try: + res = Array(elems, Trivia()) + except ValueError: + pass + else: + return res + + def _parse_inline_table(self) -> InlineTable: + # consume opening bracket, EOF here is an issue (middle of array) + self.inc(exception=UnexpectedEofError) + + elems = Container(True) + trailing_comma = None + while True: + # consume leading whitespace + mark = self._idx + self.consume(TOMLChar.SPACES) + raw = self._src[mark : self._idx] + if raw: + elems.add(Whitespace(raw)) + + if not trailing_comma: + # None: empty inline table + # False: previous key-value pair was not followed by a comma + if self._current == "}": + # consume closing bracket, EOF here doesn't matter + self.inc() + break + + if ( + trailing_comma is False + or trailing_comma is None + and self._current == "," + ): + # Either the previous key-value pair was not followed by a comma + # or the table has an unexpected leading comma. + raise self.parse_error(UnexpectedCharError, self._current) + else: + # True: previous key-value pair was followed by a comma + if self._current == "}" or self._current == ",": + raise self.parse_error(UnexpectedCharError, self._current) + + key, val = self._parse_key_value(False) + elems.add(key, val) + + # consume trailing whitespace + mark = self._idx + self.consume(TOMLChar.SPACES) + raw = self._src[mark : self._idx] + if raw: + elems.add(Whitespace(raw)) + + # consume trailing comma + trailing_comma = self._current == "," + if trailing_comma: + # consume closing bracket, EOF here is an issue (middle of inline table) + self.inc(exception=UnexpectedEofError) + + return InlineTable(elems, Trivia()) + + def _parse_number(self, raw: str, trivia: Trivia) -> Item | None: + # Leading zeros are not allowed + sign = "" + if raw.startswith(("+", "-")): + sign = raw[0] + raw = raw[1:] + + if len(raw) > 1 and ( + raw.startswith("0") + and not raw.startswith(("0.", "0o", "0x", "0b", "0e")) + or sign + and raw.startswith(".") + ): + return None + + if raw.startswith(("0o", "0x", "0b")) and sign: + return None + + digits = "[0-9]" + base = 10 + if raw.startswith("0b"): + digits = "[01]" + base = 2 + elif raw.startswith("0o"): + digits = "[0-7]" + base = 8 + elif raw.startswith("0x"): + digits = "[0-9a-f]" + base = 16 + + # Underscores should be surrounded by digits + clean = re.sub(f"(?i)(?<={digits})_(?={digits})", "", raw).lower() + + if "_" in clean: + return None + + if ( + clean.endswith(".") + or not clean.startswith("0x") + and clean.split("e", 1)[0].endswith(".") + ): + return None + + try: + return Integer(int(sign + clean, base), trivia, sign + raw) + except ValueError: + try: + return Float(float(sign + clean), trivia, sign + raw) + except ValueError: + return None + + def _parse_literal_string(self) -> String: + with self._state: + return self._parse_string(StringType.SLL) + + def _parse_basic_string(self) -> String: + with self._state: + return self._parse_string(StringType.SLB) + + def _parse_escaped_char(self, multiline): + if multiline and self._current.is_ws(): + # When the last non-whitespace character on a line is + # a \, it will be trimmed along with all whitespace + # (including newlines) up to the next non-whitespace + # character or closing delimiter. + # """\ + # hello \ + # world""" + tmp = "" + while self._current.is_ws(): + tmp += self._current + # consume the whitespace, EOF here is an issue + # (middle of string) + self.inc(exception=UnexpectedEofError) + continue + + # the escape followed by whitespace must have a newline + # before any other chars + if "\n" not in tmp: + raise self.parse_error(InvalidCharInStringError, self._current) + + return "" + + if self._current in _escaped: + c = _escaped[self._current] + + # consume this char, EOF here is an issue (middle of string) + self.inc(exception=UnexpectedEofError) + + return c + + if self._current in {"u", "U"}: + # this needs to be a unicode + u, ue = self._peek_unicode(self._current == "U") + if u is not None: + # consume the U char and the unicode value + self.inc_n(len(ue) + 1) + + return u + + raise self.parse_error(InvalidUnicodeValueError) + + raise self.parse_error(InvalidCharInStringError, self._current) + + def _parse_string(self, delim: StringType) -> String: + # only keep parsing for string if the current character matches the delim + if self._current != delim.unit: + raise self.parse_error( + InternalParserError, + f"Invalid character for string type {delim}", + ) + + # consume the opening/first delim, EOF here is an issue + # (middle of string or middle of delim) + self.inc(exception=UnexpectedEofError) + + if self._current == delim.unit: + # consume the closing/second delim, we do not care if EOF occurs as + # that would simply imply an empty single line string + if not self.inc() or self._current != delim.unit: + # Empty string + return String(delim, "", "", Trivia()) + + # consume the third delim, EOF here is an issue (middle of string) + self.inc(exception=UnexpectedEofError) + + delim = delim.toggle() # convert delim to multi delim + + self.mark() # to extract the original string with whitespace and all + value = "" + + # A newline immediately following the opening delimiter will be trimmed. + if delim.is_multiline(): + if self._current == "\n": + # consume the newline, EOF here is an issue (middle of string) + self.inc(exception=UnexpectedEofError) + else: + cur = self._current + with self._state(restore=True): + if self.inc(): + cur += self._current + if cur == "\r\n": + self.inc_n(2, exception=UnexpectedEofError) + + escaped = False # whether the previous key was ESCAPE + while True: + code = ord(self._current) + if ( + delim.is_singleline() + and not escaped + and (code == CHR_DEL or code <= CTRL_CHAR_LIMIT and code != CTRL_I) + ) or ( + delim.is_multiline() + and not escaped + and ( + code == CHR_DEL + or code <= CTRL_CHAR_LIMIT + and code not in [CTRL_I, CTRL_J, CTRL_M] + ) + ): + raise self.parse_error(InvalidControlChar, code, "strings") + elif not escaped and self._current == delim.unit: + # try to process current as a closing delim + original = self.extract() + + close = "" + if delim.is_multiline(): + # Consume the delimiters to see if we are at the end of the string + close = "" + while self._current == delim.unit: + close += self._current + self.inc() + + if len(close) < 3: + # Not a triple quote, leave in result as-is. + # Adding back the characters we already consumed + value += close + continue + + if len(close) == 3: + # We are at the end of the string + return String(delim, value, original, Trivia()) + + if len(close) >= 6: + raise self.parse_error(InvalidCharInStringError, self._current) + + value += close[:-3] + original += close[:-3] + + return String(delim, value, original, Trivia()) + else: + # consume the closing delim, we do not care if EOF occurs as + # that would simply imply the end of self._src + self.inc() + + return String(delim, value, original, Trivia()) + elif delim.is_basic() and escaped: + # attempt to parse the current char as an escaped value, an exception + # is raised if this fails + value += self._parse_escaped_char(delim.is_multiline()) + + # no longer escaped + escaped = False + elif delim.is_basic() and self._current == "\\": + # the next char is being escaped + escaped = True + + # consume this char, EOF here is an issue (middle of string) + self.inc(exception=UnexpectedEofError) + else: + # this is either a literal string where we keep everything as is, + # or this is not a special escaped char in a basic string + value += self._current + + # consume this char, EOF here is an issue (middle of string) + self.inc(exception=UnexpectedEofError) + + def _parse_table( + self, parent_name: Key | None = None, parent: Table | None = None + ) -> tuple[Key, Table | AoT]: + """ + Parses a table element. + """ + if self._current != "[": + raise self.parse_error( + InternalParserError, "_parse_table() called on non-bracket character." + ) + + indent = self.extract() + self.inc() # Skip opening bracket + + if self.end(): + raise self.parse_error(UnexpectedEofError) + + is_aot = False + if self._current == "[": + if not self.inc(): + raise self.parse_error(UnexpectedEofError) + + is_aot = True + try: + key = self._parse_key() + except EmptyKeyError: + raise self.parse_error(EmptyTableNameError) from None + if self.end(): + raise self.parse_error(UnexpectedEofError) + elif self._current != "]": + raise self.parse_error(UnexpectedCharError, self._current) + + key.sep = "" + full_key = key + name_parts = tuple(key) + if any(" " in part.key.strip() and part.is_bare() for part in name_parts): + raise self.parse_error( + ParseError, f'Invalid table name "{full_key.as_string()}"' + ) + + missing_table = False + if parent_name: + parent_name_parts = tuple(parent_name) + else: + parent_name_parts = () + + if len(name_parts) > len(parent_name_parts) + 1: + missing_table = True + + name_parts = name_parts[len(parent_name_parts) :] + + values = Container(True) + + self.inc() # Skip closing bracket + if is_aot: + # TODO: Verify close bracket + self.inc() + + cws, comment, trail = self._parse_comment_trail() + + result = Null() + table = Table( + values, + Trivia(indent, cws, comment, trail), + is_aot, + name=name_parts[0].key if name_parts else key.key, + display_name=full_key.as_string(), + is_super_table=False, + ) + + if len(name_parts) > 1: + if missing_table: + # Missing super table + # i.e. a table initialized like this: [foo.bar] + # without initializing [foo] + # + # So we have to create the parent tables + table = Table( + Container(True), + Trivia("", cws, comment, trail), + is_aot and name_parts[0] in self._aot_stack, + is_super_table=True, + name=name_parts[0].key, + ) + + result = table + key = name_parts[0] + + for i, _name in enumerate(name_parts[1:]): + child = table.get( + _name, + Table( + Container(True), + Trivia(indent, cws, comment, trail), + is_aot and i == len(name_parts) - 2, + is_super_table=i < len(name_parts) - 2, + name=_name.key, + display_name=full_key.as_string() + if i == len(name_parts) - 2 + else None, + ), + ) + + if is_aot and i == len(name_parts) - 2: + table.raw_append(_name, AoT([child], name=table.name, parsed=True)) + else: + table.raw_append(_name, child) + + table = child + values = table.value + else: + if name_parts: + key = name_parts[0] + + while not self.end(): + item = self._parse_item() + if item: + _key, item = item + if not self._merge_ws(item, values): + table.raw_append(_key, item) + else: + if self._current == "[": + _, key_next = self._peek_table() + + if self._is_child(full_key, key_next): + key_next, table_next = self._parse_table(full_key, table) + + table.raw_append(key_next, table_next) + + # Picking up any sibling + while not self.end(): + _, key_next = self._peek_table() + + if not self._is_child(full_key, key_next): + break + + key_next, table_next = self._parse_table(full_key, table) + + table.raw_append(key_next, table_next) + + break + else: + raise self.parse_error( + InternalParserError, + "_parse_item() returned None on a non-bracket character.", + ) + table.value._validate_out_of_order_table() + if isinstance(result, Null): + result = table + + if is_aot and (not self._aot_stack or full_key != self._aot_stack[-1]): + result = self._parse_aot(result, full_key) + + return key, result + + def _peek_table(self) -> tuple[bool, Key]: + """ + Peeks ahead non-intrusively by cloning then restoring the + initial state of the parser. + + Returns the name of the table about to be parsed, + as well as whether it is part of an AoT. + """ + # we always want to restore after exiting this scope + with self._state(save_marker=True, restore=True): + if self._current != "[": + raise self.parse_error( + InternalParserError, + "_peek_table() entered on non-bracket character", + ) + + # AoT + self.inc() + is_aot = False + if self._current == "[": + self.inc() + is_aot = True + try: + return is_aot, self._parse_key() + except EmptyKeyError: + raise self.parse_error(EmptyTableNameError) from None + + def _parse_aot(self, first: Table, name_first: Key) -> AoT: + """ + Parses all siblings of the provided table first and bundles them into + an AoT. + """ + payload = [first] + self._aot_stack.append(name_first) + while not self.end(): + is_aot_next, name_next = self._peek_table() + if is_aot_next and name_next == name_first: + _, table = self._parse_table(name_first) + payload.append(table) + else: + break + + self._aot_stack.pop() + + return AoT(payload, parsed=True) + + def _peek(self, n: int) -> str: + """ + Peeks ahead n characters. + + n is the max number of characters that will be peeked. + """ + # we always want to restore after exiting this scope + with self._state(restore=True): + buf = "" + for _ in range(n): + if self._current not in " \t\n\r#,]}" + self._src.EOF: + buf += self._current + self.inc() + continue + + break + return buf + + def _peek_unicode(self, is_long: bool) -> tuple[str | None, str | None]: + """ + Peeks ahead non-intrusively by cloning then restoring the + initial state of the parser. + + Returns the unicode value is it's a valid one else None. + """ + # we always want to restore after exiting this scope + with self._state(save_marker=True, restore=True): + if self._current not in {"u", "U"}: + raise self.parse_error( + InternalParserError, "_peek_unicode() entered on non-unicode value" + ) + + self.inc() # Dropping prefix + self.mark() + + if is_long: + chars = 8 + else: + chars = 4 + + if not self.inc_n(chars): + value, extracted = None, None + else: + extracted = self.extract() + + if extracted[0].lower() == "d" and extracted[1].strip("01234567"): + return None, None + + try: + value = chr(int(extracted, 16)) + except (ValueError, OverflowError): + value = None + + return value, extracted diff --git a/third_party/python/tomlkit/tomlkit/py.typed b/third_party/python/tomlkit/tomlkit/py.typed new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/py.typed diff --git a/third_party/python/tomlkit/tomlkit/source.py b/third_party/python/tomlkit/tomlkit/source.py new file mode 100644 index 0000000000..0e4db243b1 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/source.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +from copy import copy +from typing import Any + +from tomlkit.exceptions import ParseError +from tomlkit.exceptions import UnexpectedCharError +from tomlkit.toml_char import TOMLChar + + +class _State: + def __init__( + self, + source: Source, + save_marker: bool | None = False, + restore: bool | None = False, + ) -> None: + self._source = source + self._save_marker = save_marker + self.restore = restore + + def __enter__(self) -> _State: + # Entering this context manager - save the state + self._chars = copy(self._source._chars) + self._idx = self._source._idx + self._current = self._source._current + self._marker = self._source._marker + + return self + + def __exit__(self, exception_type, exception_val, trace): + # Exiting this context manager - restore the prior state + if self.restore or exception_type: + self._source._chars = self._chars + self._source._idx = self._idx + self._source._current = self._current + if self._save_marker: + self._source._marker = self._marker + + +class _StateHandler: + """ + State preserver for the Parser. + """ + + def __init__(self, source: Source) -> None: + self._source = source + self._states = [] + + def __call__(self, *args, **kwargs): + return _State(self._source, *args, **kwargs) + + def __enter__(self) -> _State: + state = self() + self._states.append(state) + return state.__enter__() + + def __exit__(self, exception_type, exception_val, trace): + state = self._states.pop() + return state.__exit__(exception_type, exception_val, trace) + + +class Source(str): + EOF = TOMLChar("\0") + + def __init__(self, _: str) -> None: + super().__init__() + + # Collection of TOMLChars + self._chars = iter([(i, TOMLChar(c)) for i, c in enumerate(self)]) + + self._idx = 0 + self._marker = 0 + self._current = TOMLChar("") + + self._state = _StateHandler(self) + + self.inc() + + def reset(self): + # initialize both idx and current + self.inc() + + # reset marker + self.mark() + + @property + def state(self) -> _StateHandler: + return self._state + + @property + def idx(self) -> int: + return self._idx + + @property + def current(self) -> TOMLChar: + return self._current + + @property + def marker(self) -> int: + return self._marker + + def extract(self) -> str: + """ + Extracts the value between marker and index + """ + return self[self._marker : self._idx] + + def inc(self, exception: type[ParseError] | None = None) -> bool: + """ + Increments the parser if the end of the input has not been reached. + Returns whether or not it was able to advance. + """ + try: + self._idx, self._current = next(self._chars) + + return True + except StopIteration: + self._idx = len(self) + self._current = self.EOF + if exception: + raise self.parse_error(exception) + + return False + + def inc_n(self, n: int, exception: type[ParseError] | None = None) -> bool: + """ + Increments the parser by n characters + if the end of the input has not been reached. + """ + return all(self.inc(exception=exception) for _ in range(n)) + + def consume(self, chars, min=0, max=-1): + """ + Consume chars until min/max is satisfied is valid. + """ + while self.current in chars and max != 0: + min -= 1 + max -= 1 + if not self.inc(): + break + + # failed to consume minimum number of characters + if min > 0: + raise self.parse_error(UnexpectedCharError, self.current) + + def end(self) -> bool: + """ + Returns True if the parser has reached the end of the input. + """ + return self._current is self.EOF + + def mark(self) -> None: + """ + Sets the marker to the index's current position + """ + self._marker = self._idx + + def parse_error( + self, + exception: type[ParseError] = ParseError, + *args: Any, + **kwargs: Any, + ) -> ParseError: + """ + Creates a generic "parse error" at the current position. + """ + line, col = self._to_linecol() + + return exception(line, col, *args, **kwargs) + + def _to_linecol(self) -> tuple[int, int]: + cur = 0 + for i, line in enumerate(self.splitlines()): + if cur + len(line) + 1 > self.idx: + return (i + 1, self.idx - cur) + + cur += len(line) + 1 + + return len(self.splitlines()), 0 diff --git a/third_party/python/tomlkit/tomlkit/toml_char.py b/third_party/python/tomlkit/tomlkit/toml_char.py new file mode 100644 index 0000000000..b4bb4110c5 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/toml_char.py @@ -0,0 +1,52 @@ +import string + + +class TOMLChar(str): + def __init__(self, c): + super().__init__() + + if len(self) > 1: + raise ValueError("A TOML character must be of length 1") + + BARE = string.ascii_letters + string.digits + "-_" + KV = "= \t" + NUMBER = string.digits + "+-_.e" + SPACES = " \t" + NL = "\n\r" + WS = SPACES + NL + + def is_bare_key_char(self) -> bool: + """ + Whether the character is a valid bare key name or not. + """ + return self in self.BARE + + def is_kv_sep(self) -> bool: + """ + Whether the character is a valid key/value separator or not. + """ + return self in self.KV + + def is_int_float_char(self) -> bool: + """ + Whether the character if a valid integer or float value character or not. + """ + return self in self.NUMBER + + def is_ws(self) -> bool: + """ + Whether the character is a whitespace character or not. + """ + return self in self.WS + + def is_nl(self) -> bool: + """ + Whether the character is a new line character or not. + """ + return self in self.NL + + def is_spaces(self) -> bool: + """ + Whether the character is a space or not + """ + return self in self.SPACES diff --git a/third_party/python/tomlkit/tomlkit/toml_document.py b/third_party/python/tomlkit/tomlkit/toml_document.py new file mode 100644 index 0000000000..71fac2e101 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/toml_document.py @@ -0,0 +1,7 @@ +from tomlkit.container import Container + + +class TOMLDocument(Container): + """ + A TOML document. + """ diff --git a/third_party/python/tomlkit/tomlkit/toml_file.py b/third_party/python/tomlkit/tomlkit/toml_file.py new file mode 100644 index 0000000000..7459130803 --- /dev/null +++ b/third_party/python/tomlkit/tomlkit/toml_file.py @@ -0,0 +1,58 @@ +import os +import re + +from typing import TYPE_CHECKING + +from tomlkit.api import loads +from tomlkit.toml_document import TOMLDocument + + +if TYPE_CHECKING: + from _typeshed import StrPath as _StrPath +else: + from typing import Union + + _StrPath = Union[str, os.PathLike] + + +class TOMLFile: + """ + Represents a TOML file. + + :param path: path to the TOML file + """ + + def __init__(self, path: _StrPath) -> None: + self._path = path + self._linesep = os.linesep + + def read(self) -> TOMLDocument: + """Read the file content as a :class:`tomlkit.toml_document.TOMLDocument`.""" + with open(self._path, encoding="utf-8", newline="") as f: + content = f.read() + + # check if consistent line endings + num_newline = content.count("\n") + if num_newline > 0: + num_win_eol = content.count("\r\n") + if num_win_eol == num_newline: + self._linesep = "\r\n" + elif num_win_eol == 0: + self._linesep = "\n" + else: + self._linesep = "mixed" + + return loads(content) + + def write(self, data: TOMLDocument) -> None: + """Write the TOMLDocument to the file.""" + content = data.as_string() + + # apply linesep + if self._linesep == "\n": + content = content.replace("\r\n", "\n") + elif self._linesep == "\r\n": + content = re.sub(r"(?<!\r)\n", "\r\n", content) + + with open(self._path, "w", encoding="utf-8", newline="") as f: + f.write(content) |