diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/python/tomlkit | |
parent | Initial commit. (diff) | |
download | firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/python/tomlkit')
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) |