XylotrechusZ
from __future__ import annotations
from collections.abc import Mapping
from datetime import date, datetime, time
from types import MappingProxyType
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Generator
from decimal import Decimal
from typing import IO, Any, Final
ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
ILLEGAL_BASIC_STR_CHARS = frozenset('"\\') | ASCII_CTRL - frozenset("\t")
BARE_KEY_CHARS = frozenset(
"abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "-_"
)
ARRAY_TYPES = (list, tuple)
MAX_LINE_LENGTH = 100
COMPACT_ESCAPES = MappingProxyType(
{
"\u0008": "\\b", # backspace
"\u000A": "\\n", # linefeed
"\u000C": "\\f", # form feed
"\u000D": "\\r", # carriage return
"\u0022": '\\"', # quote
"\u005C": "\\\\", # backslash
}
)
class Context:
def __init__(self, allow_multiline: bool, indent: int):
if indent < 0:
raise ValueError("Indent width must be non-negative")
self.allow_multiline: Final = allow_multiline
# cache rendered inline tables (mapping from object id to rendered inline table)
self.inline_table_cache: Final[dict[int, str]] = {}
self.indent_str: Final = " " * indent
def dump(
obj: Mapping[str, Any],
fp: IO[bytes],
/,
*,
multiline_strings: bool = False,
indent: int = 4,
) -> None:
ctx = Context(multiline_strings, indent)
for chunk in gen_table_chunks(obj, ctx, name=""):
fp.write(chunk.encode())
def dumps(
obj: Mapping[str, Any], /, *, multiline_strings: bool = False, indent: int = 4
) -> str:
ctx = Context(multiline_strings, indent)
return "".join(gen_table_chunks(obj, ctx, name=""))
def gen_table_chunks(
table: Mapping[str, Any],
ctx: Context,
*,
name: str,
inside_aot: bool = False,
) -> Generator[str, None, None]:
yielded = False
literals = []
tables: list[tuple[str, Any, bool]] = [] # => [(key, value, inside_aot)]
for k, v in table.items():
if isinstance(v, Mapping):
tables.append((k, v, False))
elif is_aot(v) and not all(is_suitable_inline_table(t, ctx) for t in v):
tables.extend((k, t, True) for t in v)
else:
literals.append((k, v))
if inside_aot or name and (literals or not tables):
yielded = True
yield f"[[{name}]]\n" if inside_aot else f"[{name}]\n"
if literals:
yielded = True
for k, v in literals:
yield f"{format_key_part(k)} = {format_literal(v, ctx)}\n"
for k, v, in_aot in tables:
if yielded:
yield "\n"
else:
yielded = True
key_part = format_key_part(k)
display_name = f"{name}.{key_part}" if name else key_part
yield from gen_table_chunks(v, ctx, name=display_name, inside_aot=in_aot)
def format_literal(obj: object, ctx: Context, *, nest_level: int = 0) -> str:
if isinstance(obj, bool):
return "true" if obj else "false"
if isinstance(obj, (int, float, date, datetime)):
return str(obj)
if isinstance(obj, time):
if obj.tzinfo:
raise ValueError("TOML does not support offset times")
return str(obj)
if isinstance(obj, str):
return format_string(obj, allow_multiline=ctx.allow_multiline)
if isinstance(obj, ARRAY_TYPES):
return format_inline_array(obj, ctx, nest_level)
if isinstance(obj, Mapping):
return format_inline_table(obj, ctx)
# Lazy import to improve module import time
from decimal import Decimal
if isinstance(obj, Decimal):
return format_decimal(obj)
raise TypeError(
f"Object of type '{type(obj).__qualname__}' is not TOML serializable"
)
def format_decimal(obj: Decimal) -> str:
if obj.is_nan():
return "nan"
if obj.is_infinite():
return "-inf" if obj.is_signed() else "inf"
dec_str = str(obj).lower()
return dec_str if "." in dec_str or "e" in dec_str else dec_str + ".0"
def format_inline_table(obj: Mapping, ctx: Context) -> str:
# check cache first
obj_id = id(obj)
if obj_id in ctx.inline_table_cache:
return ctx.inline_table_cache[obj_id]
if not obj:
rendered = "{}"
else:
rendered = (
"{ "
+ ", ".join(
f"{format_key_part(k)} = {format_literal(v, ctx)}"
for k, v in obj.items()
)
+ " }"
)
ctx.inline_table_cache[obj_id] = rendered
return rendered
def format_inline_array(obj: tuple | list, ctx: Context, nest_level: int) -> str:
if not obj:
return "[]"
item_indent = ctx.indent_str * (1 + nest_level)
closing_bracket_indent = ctx.indent_str * nest_level
return (
"[\n"
+ ",\n".join(
item_indent + format_literal(item, ctx, nest_level=nest_level + 1)
for item in obj
)
+ f",\n{closing_bracket_indent}]"
)
def format_key_part(part: str) -> str:
try:
only_bare_key_chars = BARE_KEY_CHARS.issuperset(part)
except TypeError:
raise TypeError(
f"Invalid mapping key '{part}' of type '{type(part).__qualname__}'."
" A string is required."
) from None
if part and only_bare_key_chars:
return part
return format_string(part, allow_multiline=False)
def format_string(s: str, *, allow_multiline: bool) -> str:
do_multiline = allow_multiline and "\n" in s
if do_multiline:
result = '"""\n'
s = s.replace("\r\n", "\n")
else:
result = '"'
pos = seq_start = 0
while True:
try:
char = s[pos]
except IndexError:
result += s[seq_start:pos]
if do_multiline:
return result + '"""'
return result + '"'
if char in ILLEGAL_BASIC_STR_CHARS:
result += s[seq_start:pos]
if char in COMPACT_ESCAPES:
if do_multiline and char == "\n":
result += "\n"
else:
result += COMPACT_ESCAPES[char]
else:
result += "\\u" + hex(ord(char))[2:].rjust(4, "0")
seq_start = pos + 1
pos += 1
def is_aot(obj: Any) -> bool:
"""Decides if an object behaves as an array of tables (i.e. a nonempty list
of dicts)."""
return bool(
isinstance(obj, ARRAY_TYPES)
and obj
and all(isinstance(v, Mapping) for v in obj)
)
def is_suitable_inline_table(obj: Mapping, ctx: Context) -> bool:
"""Use heuristics to decide if the inline-style representation is a good
choice for a given table."""
rendered_inline = f"{ctx.indent_str}{format_inline_table(obj, ctx)},"
return len(rendered_inline) <= MAX_LINE_LENGTH and "\n" not in rendered_inline