Skip to content

Commit 293e0a1

Browse files
committed
Raise ValueError when indent is negative. Update README
1 parent 54268af commit 293e0a1

File tree

4 files changed

+68
-12
lines changed

4 files changed

+68
-12
lines changed

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
- [Write to file](#write-to-file)
1818
- [FAQ](#faq)
1919
- [Does Tomli-W sort the document?](#does-tomli-w-sort-the-document)
20-
- [Does Tomli-W support writing documents with comments or custom whitespace?](#does-tomli-w-support-writing-documents-with-comments-or-custom-whitespace)
20+
- [Does Tomli-W support writing documents with comments?](#does-tomli-w-support-writing-documents-with-comments)
21+
- [Can I customize insignificant whitespace?](#can-i-customize-insignificant-whitespace)
2122
- [Why does Tomli-W not write a multi-line string if the string value contains newlines?](#why-does-tomli-w-not-write-a-multi-line-string-if-the-string-value-contains-newlines)
2223
- [Is Tomli-W output guaranteed to be valid TOML?](#is-tomli-w-output-guaranteed-to-be-valid-toml)
2324

@@ -73,10 +74,29 @@ with open("path_to_file/conf.toml", "wb") as f:
7374
No, but it respects sort order of the input data,
7475
so one could sort the content of the `dict` (recursively) before calling `tomli_w.dumps`.
7576

76-
### Does Tomli-W support writing documents with comments or custom whitespace?<a name="does-tomli-w-support-writing-documents-with-comments-or-custom-whitespace"></a>
77+
### Does Tomli-W support writing documents with comments?<a name="does-tomli-w-support-writing-documents-with-comments"></a>
7778

7879
No.
7980

81+
### Can I customize insignificant whitespace?<a name="can-i-customize-insignificant-whitespace"></a>
82+
83+
Indent width of array content can be configured via the `indent` keyword argument.
84+
`indent` takes a non-negative integer, defaulting to 4.
85+
86+
```python
87+
import tomli_w
88+
89+
doc = {"fruits": ["orange", "kiwi", "papaya"]}
90+
expected_toml = """\
91+
fruits = [
92+
"orange",
93+
"kiwi",
94+
"papaya",
95+
]
96+
"""
97+
assert tomli_w.dumps(doc, indent=1) == expected_toml
98+
```
99+
80100
### Why does Tomli-W not write a multi-line string if the string value contains newlines?<a name="why-does-tomli-w-not-write-a-multi-line-string-if-the-string-value-contains-newlines"></a>
81101

82102
This default was chosen to achieve lossless parse/write round-trips.
@@ -117,3 +137,11 @@ No.
117137
If there's a chance that your input data is bad and you need output validation,
118138
parse the output string once with `tomli.loads`.
119139
If the parse is successful (does not raise `tomli.TOMLDecodeError`) then the string is valid TOML.
140+
141+
Examples of bad input data that can lead to writing invalid TOML without an error being raised include:
142+
143+
- A mapping where keys behave very much like strings, but aren't. E.g. a tuple of strings of length 1.
144+
- A mapping where a value is a subclass of a supported type, but which overrides the `__str__` method.
145+
146+
Given proper input (a mapping consisting of non-subclassed [types returned by Tomli](https://github.com/hukkin/tomli?tab=readme-ov-file#how-do-toml-types-map-into-python-types))
147+
the output should be valid TOML.

src/tomli_w/_writer.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@
2525
)
2626

2727

28+
class 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
33+
34+
35+
def 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)
39+
40+
2841
def dump(
2942
obj: Mapping[str, Any],
3043
fp: IO[bytes],
@@ -33,25 +46,18 @@ def dump(
3346
multiline_strings: bool = False,
3447
indent: int = 4,
3548
) -> None:
36-
ctx = Context(multiline_strings, {}, " " * indent)
49+
ctx = make_context(multiline_strings, indent)
3750
for chunk in gen_table_chunks(obj, ctx, name=""):
3851
fp.write(chunk.encode())
3952

4053

4154
def dumps(
4255
obj: Mapping[str, Any], /, *, multiline_strings: bool = False, indent: int = 4
4356
) -> str:
44-
ctx = Context(multiline_strings, {}, " " * indent)
57+
ctx = make_context(multiline_strings, indent)
4558
return "".join(gen_table_chunks(obj, ctx, name=""))
4659

4760

48-
class Context(NamedTuple):
49-
allow_multiline: bool
50-
# cache rendered inline tables (mapping from object id to rendered inline table)
51-
inline_table_cache: dict[int, str]
52-
indent_str: str
53-
54-
5561
def gen_table_chunks(
5662
table: Mapping[str, Any],
5763
ctx: Context,

tests/test_invalid.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ def test_invalid_time():
1414
offset_time = time(23, 59, 59, tzinfo=timezone.utc)
1515
with pytest.raises(ValueError):
1616
tomli_w.dumps({"offset time": offset_time})
17+
18+
19+
def test_negative_indent():
20+
with pytest.raises(ValueError):
21+
tomli_w.dumps({"k": "v"}, indent=-1)

tests/test_style.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,30 @@ def test_multiline_in_aot():
275275

276276

277277
def test_array_indent_override():
278-
data = {"k0": ["v1", "v2"]}
278+
data = {"k0": ["v1", "v2", ["v3", "v4"]]}
279279
assert (
280280
tomli_w.dumps(data, indent=2)
281281
== """\
282282
k0 = [
283283
"v1",
284284
"v2",
285+
[
286+
"v3",
287+
"v4",
288+
],
289+
]
290+
"""
291+
)
292+
assert (
293+
tomli_w.dumps(data, indent=0)
294+
== """\
295+
k0 = [
296+
"v1",
297+
"v2",
298+
[
299+
"v3",
300+
"v4",
301+
],
285302
]
286303
"""
287304
)

0 commit comments

Comments
 (0)