Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/tomli_w/_writer.py: 72%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3from collections.abc import Generator, Mapping
4from datetime import date, datetime, time
5from decimal import Decimal
6import string
7from types import MappingProxyType
8from typing import IO, Any, NamedTuple
10ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
11ILLEGAL_BASIC_STR_CHARS = frozenset('"\\') | ASCII_CTRL - frozenset("\t")
12BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_")
13ARRAY_TYPES = (list, tuple)
14MAX_LINE_LENGTH = 100
16COMPACT_ESCAPES = MappingProxyType(
17 {
18 "\u0008": "\\b", # backspace
19 "\u000A": "\\n", # linefeed
20 "\u000C": "\\f", # form feed
21 "\u000D": "\\r", # carriage return
22 "\u0022": '\\"', # quote
23 "\u005C": "\\\\", # backslash
24 }
25)
28class Context(NamedTuple):
29 allow_multiline: bool
30 # cache rendered inline tables (mapping from object id to rendered inline table)
31 inline_table_cache: dict[int, str]
32 indent_str: str
35def make_context(multiline_strings: bool, indent: int) -> Context:
36 if indent < 0:
37 raise ValueError("Indent width must be non-negative")
38 return Context(multiline_strings, {}, " " * indent)
41def dump(
42 obj: Mapping[str, Any],
43 fp: IO[bytes],
44 /,
45 *,
46 multiline_strings: bool = False,
47 indent: int = 4,
48) -> None:
49 ctx = make_context(multiline_strings, indent)
50 for chunk in gen_table_chunks(obj, ctx, name=""):
51 fp.write(chunk.encode())
54def dumps(
55 obj: Mapping[str, Any], /, *, multiline_strings: bool = False, indent: int = 4
56) -> str:
57 ctx = make_context(multiline_strings, indent)
58 return "".join(gen_table_chunks(obj, ctx, name=""))
61def gen_table_chunks(
62 table: Mapping[str, Any],
63 ctx: Context,
64 *,
65 name: str,
66 inside_aot: bool = False,
67) -> Generator[str, None, None]:
68 yielded = False
69 literals = []
70 tables: list[tuple[str, Any, bool]] = [] # => [(key, value, inside_aot)]
71 for k, v in table.items():
72 if isinstance(v, Mapping):
73 tables.append((k, v, False))
74 elif is_aot(v) and not all(is_suitable_inline_table(t, ctx) for t in v):
75 tables.extend((k, t, True) for t in v)
76 else:
77 literals.append((k, v))
79 if inside_aot or name and (literals or not tables):
80 yielded = True
81 yield f"[[{name}]]\n" if inside_aot else f"[{name}]\n"
83 if literals:
84 yielded = True
85 for k, v in literals:
86 yield f"{format_key_part(k)} = {format_literal(v, ctx)}\n"
88 for k, v, in_aot in tables:
89 if yielded:
90 yield "\n"
91 else:
92 yielded = True
93 key_part = format_key_part(k)
94 display_name = f"{name}.{key_part}" if name else key_part
95 yield from gen_table_chunks(v, ctx, name=display_name, inside_aot=in_aot)
98def format_literal(obj: object, ctx: Context, *, nest_level: int = 0) -> str:
99 if isinstance(obj, bool):
100 return "true" if obj else "false"
101 if isinstance(obj, (int, float, date, datetime)):
102 return str(obj)
103 if isinstance(obj, Decimal):
104 return format_decimal(obj)
105 if isinstance(obj, time):
106 if obj.tzinfo:
107 raise ValueError("TOML does not support offset times")
108 return str(obj)
109 if isinstance(obj, str):
110 return format_string(obj, allow_multiline=ctx.allow_multiline)
111 if isinstance(obj, ARRAY_TYPES):
112 return format_inline_array(obj, ctx, nest_level)
113 if isinstance(obj, Mapping):
114 return format_inline_table(obj, ctx)
115 raise TypeError(
116 f"Object of type '{type(obj).__qualname__}' is not TOML serializable"
117 )
120def format_decimal(obj: Decimal) -> str:
121 if obj.is_nan():
122 return "nan"
123 if obj == Decimal("inf"):
124 return "inf"
125 if obj == Decimal("-inf"):
126 return "-inf"
127 return str(obj)
130def format_inline_table(obj: Mapping, ctx: Context) -> str:
131 # check cache first
132 obj_id = id(obj)
133 if obj_id in ctx.inline_table_cache:
134 return ctx.inline_table_cache[obj_id]
136 if not obj:
137 rendered = "{}"
138 else:
139 rendered = (
140 "{ "
141 + ", ".join(
142 f"{format_key_part(k)} = {format_literal(v, ctx)}"
143 for k, v in obj.items()
144 )
145 + " }"
146 )
147 ctx.inline_table_cache[obj_id] = rendered
148 return rendered
151def format_inline_array(obj: tuple | list, ctx: Context, nest_level: int) -> str:
152 if not obj:
153 return "[]"
154 item_indent = ctx.indent_str * (1 + nest_level)
155 closing_bracket_indent = ctx.indent_str * nest_level
156 return (
157 "[\n"
158 + ",\n".join(
159 item_indent + format_literal(item, ctx, nest_level=nest_level + 1)
160 for item in obj
161 )
162 + f",\n{closing_bracket_indent}]"
163 )
166def format_key_part(part: str) -> str:
167 try:
168 only_bare_key_chars = BARE_KEY_CHARS.issuperset(part)
169 except TypeError:
170 raise TypeError(
171 f"Invalid mapping key '{part}' of type '{type(part).__qualname__}'."
172 " A string is required."
173 ) from None
175 if part and only_bare_key_chars:
176 return part
177 return format_string(part, allow_multiline=False)
180def format_string(s: str, *, allow_multiline: bool) -> str:
181 do_multiline = allow_multiline and "\n" in s
182 if do_multiline:
183 result = '"""\n'
184 s = s.replace("\r\n", "\n")
185 else:
186 result = '"'
188 pos = seq_start = 0
189 while True:
190 try:
191 char = s[pos]
192 except IndexError:
193 result += s[seq_start:pos]
194 if do_multiline:
195 return result + '"""'
196 return result + '"'
197 if char in ILLEGAL_BASIC_STR_CHARS:
198 result += s[seq_start:pos]
199 if char in COMPACT_ESCAPES:
200 if do_multiline and char == "\n":
201 result += "\n"
202 else:
203 result += COMPACT_ESCAPES[char]
204 else:
205 result += "\\u" + hex(ord(char))[2:].rjust(4, "0")
206 seq_start = pos + 1
207 pos += 1
210def is_aot(obj: Any) -> bool:
211 """Decides if an object behaves as an array of tables (i.e. a nonempty list
212 of dicts)."""
213 return bool(
214 isinstance(obj, ARRAY_TYPES)
215 and obj
216 and all(isinstance(v, Mapping) for v in obj)
217 )
220def is_suitable_inline_table(obj: Mapping, ctx: Context) -> bool:
221 """Use heuristics to decide if the inline-style representation is a good
222 choice for a given table."""
223 rendered_inline = f"{ctx.indent_str}{format_inline_table(obj, ctx)},"
224 return len(rendered_inline) <= MAX_LINE_LENGTH and "\n" not in rendered_inline