summaryrefslogtreecommitdiffstats
path: root/third_party/python/tomlkit
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/tomlkit')
-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
18 files changed, 5231 insertions, 0 deletions
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)