summaryrefslogtreecommitdiffstats
path: root/third_party/python/toml
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/python/toml
parentInitial commit. (diff)
downloadfirefox-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 '')
-rw-r--r--third_party/python/toml/toml-0.10.2.dist-info/LICENSE27
-rw-r--r--third_party/python/toml/toml-0.10.2.dist-info/METADATA255
-rw-r--r--third_party/python/toml/toml-0.10.2.dist-info/RECORD10
-rw-r--r--third_party/python/toml/toml-0.10.2.dist-info/WHEEL6
-rw-r--r--third_party/python/toml/toml-0.10.2.dist-info/top_level.txt1
-rw-r--r--third_party/python/toml/toml/__init__.py25
-rw-r--r--third_party/python/toml/toml/decoder.py1057
-rw-r--r--third_party/python/toml/toml/encoder.py304
-rw-r--r--third_party/python/toml/toml/ordered.py15
-rw-r--r--third_party/python/toml/toml/tz.py24
-rw-r--r--third_party/python/tomlkit/tomlkit-0.12.3.dist-info/LICENSE20
-rw-r--r--third_party/python/tomlkit/tomlkit-0.12.3.dist-info/METADATA71
-rw-r--r--third_party/python/tomlkit/tomlkit-0.12.3.dist-info/RECORD18
-rw-r--r--third_party/python/tomlkit/tomlkit-0.12.3.dist-info/WHEEL4
-rw-r--r--third_party/python/tomlkit/tomlkit/__init__.py59
-rw-r--r--third_party/python/tomlkit/tomlkit/_compat.py22
-rw-r--r--third_party/python/tomlkit/tomlkit/_types.py65
-rw-r--r--third_party/python/tomlkit/tomlkit/_utils.py158
-rw-r--r--third_party/python/tomlkit/tomlkit/api.py308
-rw-r--r--third_party/python/tomlkit/tomlkit/container.py875
-rw-r--r--third_party/python/tomlkit/tomlkit/exceptions.py227
-rw-r--r--third_party/python/tomlkit/tomlkit/items.py1966
-rw-r--r--third_party/python/tomlkit/tomlkit/parser.py1141
-rw-r--r--third_party/python/tomlkit/tomlkit/py.typed0
-rw-r--r--third_party/python/tomlkit/tomlkit/source.py180
-rw-r--r--third_party/python/tomlkit/tomlkit/toml_char.py52
-rw-r--r--third_party/python/tomlkit/tomlkit/toml_document.py7
-rw-r--r--third_party/python/tomlkit/tomlkit/toml_file.py58
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)