Skip to content

Commit a4560cd

Browse files
committed
feat(cli): add option --multiline
Thanks to @alexvoss for suggesting how to implement this with current TOML Kit.
1 parent 3d444b5 commit a4560cd

File tree

7 files changed

+193
-10
lines changed

7 files changed

+193
-10
lines changed

README.md

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,9 @@ uv tool install https://github.com/remarshal-project/remarshal
134134

135135
```none
136136
usage: remarshal [-h] [-v] [-f {cbor,json,msgpack,toml,yaml}] [-i <input>]
137-
[--indent <n>] [-k] [--max-values <n>] [-o <output>] [-s]
138-
[-t {cbor,json,msgpack,python,toml,yaml}] [--unwrap <key>]
139-
[--verbose] [--width <n>] [--wrap <key>]
137+
[--indent <n>] [-k] [--max-values <n>] [--multiline <n>]
138+
[-o <output>] [-s] [-t {cbor,json,msgpack,python,toml,yaml}]
139+
[--unwrap <key>] [--verbose] [--width <n>] [--wrap <key>]
140140
[--yaml-style {,',",|,>}]
141141
[input] [output]
142142
@@ -158,6 +158,8 @@ options:
158158
keys and null values for TOML
159159
--max-values <n> maximum number of values in input data (default
160160
1000000, negative for unlimited)
161+
--multiline <n> minimum number of items to make non-nested TOML array
162+
multiline (default 6)
161163
-o, --output <output>
162164
output file
163165
-s, --sort-keys sort JSON, Python, and TOML keys instead of preserving
@@ -286,7 +288,7 @@ $ curl -f 'https://archive-api.open-meteo.com/v1/era5?latitude=50.43&longitude=3
286288
;
287289
latitude = 50.439365
288290
longitude = 30.476192
289-
generationtime_ms = 0.04208087921142578
291+
generationtime_ms = 0.03254413604736328
290292
utc_offset_seconds = 0
291293
timezone = "GMT"
292294
timezone_abbreviation = "GMT"
@@ -297,14 +299,65 @@ time = "iso8601"
297299
temperature_2m = "°C"
298300
299301
[hourly]
300-
time = ["2014-10-05T00:00", "2014-10-05T01:00", "2014-10-05T02:00", "2014-10-05T03:00", "2014-10-05T04:00", "2014-10-05T05:00", "2014-10-05T06:00", "2014-10-05T07:00", "2014-10-05T08:00", "2014-10-05T09:00", "2014-10-05T10:00", "2014-10-05T11:00", "2014-10-05T12:00", "2014-10-05T13:00", "2014-10-05T14:00", "2014-10-05T15:00", "2014-10-05T16:00", "2014-10-05T17:00", "2014-10-05T18:00", "2014-10-05T19:00", "2014-10-05T20:00", "2014-10-05T21:00", "2014-10-05T22:00", "2014-10-05T23:00"]
301-
temperature_2m = [5.7, 5.3, 5.0, 4.8, 4.6, 4.6, 7.0, 8.9, 10.8, 12.2, 13.3, 13.9, 13.9, 13.7, 13.3, 12.3, 11.1, 10.2, 9.4, 8.5, 8.2, 7.9, 8.0, 7.8]
302+
time = [
303+
"2014-10-05T00:00",
304+
"2014-10-05T01:00",
305+
"2014-10-05T02:00",
306+
"2014-10-05T03:00",
307+
"2014-10-05T04:00",
308+
"2014-10-05T05:00",
309+
"2014-10-05T06:00",
310+
"2014-10-05T07:00",
311+
"2014-10-05T08:00",
312+
"2014-10-05T09:00",
313+
"2014-10-05T10:00",
314+
"2014-10-05T11:00",
315+
"2014-10-05T12:00",
316+
"2014-10-05T13:00",
317+
"2014-10-05T14:00",
318+
"2014-10-05T15:00",
319+
"2014-10-05T16:00",
320+
"2014-10-05T17:00",
321+
"2014-10-05T18:00",
322+
"2014-10-05T19:00",
323+
"2014-10-05T20:00",
324+
"2014-10-05T21:00",
325+
"2014-10-05T22:00",
326+
"2014-10-05T23:00",
327+
]
328+
temperature_2m = [
329+
5.7,
330+
5.3,
331+
5.0,
332+
4.8,
333+
4.6,
334+
4.6,
335+
7.0,
336+
8.9,
337+
10.8,
338+
12.2,
339+
13.3,
340+
13.9,
341+
13.9,
342+
13.7,
343+
13.3,
344+
12.3,
345+
11.1,
346+
10.2,
347+
9.4,
348+
8.5,
349+
8.2,
350+
7.9,
351+
8.0,
352+
7.8,
353+
]
302354
```
303355

304-
Remarshal does not limit the line width in TOML.
356+
Remarshal controls the number of items at which a TOML array becomes multiline,
357+
but it does not control the line width.
305358
You can use
306359
[`taplo fmt`](https://taplo.tamasfe.dev/cli/usage/formatting.html)
307-
to reformat the TOML and break up long lines with arrays.
360+
for finer TOML formatting.
308361

309362
## License
310363

src/remarshal/main.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import cbor2 # type: ignore
3131
import colorama
3232
import tomlkit
33+
import tomlkit.items
3334
from rich_argparse import RichHelpFormatter
3435

3536
try:
@@ -58,6 +59,8 @@ class Defaults:
5859
PYTHON_INDENT = 1
5960
YAML_INDENT = 2
6061

62+
MULTILINE_THRESHOLD = 6
63+
6164
WIDTH = 80
6265

6366

@@ -97,6 +100,7 @@ class PythonOptions(FormatOptions):
97100
@dataclass(frozen=True)
98101
class TOMLOptions(FormatOptions):
99102
indent: int | None = Defaults.INDENT
103+
multiline_threshold: int = Defaults.MULTILINE_THRESHOLD
100104
sort_keys: bool = Defaults.SORT_KEYS
101105
stringify: bool = Defaults.STRINGIFY
102106

@@ -284,6 +288,18 @@ def _parse_command_line(argv: Sequence[str]) -> argparse.Namespace:
284288
),
285289
)
286290

291+
parser.add_argument(
292+
"--multiline",
293+
default=Defaults.MULTILINE_THRESHOLD,
294+
dest="multiline_threshold",
295+
metavar="<n>",
296+
type=int,
297+
help=(
298+
"minimum number of items to make non-nested TOML array multiline "
299+
"(default %(default)s)"
300+
),
301+
)
302+
287303
output_group = parser.add_mutually_exclusive_group()
288304
output_group.add_argument("output", default="-", nargs="?", help="output file")
289305
output_group.add_argument(
@@ -688,6 +704,7 @@ def _encode_python(
688704
def _encode_toml(
689705
data: Mapping[Any, Any],
690706
*,
707+
multiline_threshold: int,
691708
sort_keys: bool,
692709
stringify: bool,
693710
) -> str:
@@ -709,14 +726,28 @@ def stringify_null(x: Any) -> Any:
709726
default_callback = stringify_null if stringify else reject_null
710727

711728
try:
712-
return tomlkit.dumps(
729+
toml = tomlkit.item(
713730
traverse(
714731
data,
715732
key_callback=key_callback,
716733
default_callback=default_callback,
717734
),
718-
sort_keys=sort_keys,
735+
_sort_keys=sort_keys,
719736
)
737+
738+
def multilinify(item: tomlkit.items.Item) -> None:
739+
match item:
740+
case tomlkit.items.Array():
741+
if len(item) >= multiline_threshold:
742+
item.multiline(multiline=True)
743+
744+
case tomlkit.items.AbstractTable():
745+
for value in item.values():
746+
multilinify(value)
747+
748+
multilinify(toml)
749+
750+
return toml.as_string()
720751
except AttributeError as e:
721752
if str(e) == "'list' object has no attribute 'as_string'":
722753
msg = (
@@ -769,6 +800,7 @@ def format_options(
769800
output_format: str,
770801
*,
771802
indent: int | None = None,
803+
multiline_threshold: int = Defaults.MULTILINE_THRESHOLD,
772804
sort_keys: bool = False,
773805
stringify: bool = False,
774806
width: int = Defaults.WIDTH,
@@ -797,6 +829,7 @@ def format_options(
797829

798830
case "toml":
799831
return TOMLOptions(
832+
multiline_threshold=multiline_threshold,
800833
sort_keys=sort_keys,
801834
stringify=stringify,
802835
)
@@ -824,12 +857,14 @@ def encode(
824857
if not isinstance(options, CBOROptions):
825858
msg = "expected 'options' argument to have class 'CBOROptions'"
826859
raise TypeError(msg)
860+
827861
encoded = _encode_cbor(data)
828862

829863
case "json":
830864
if not isinstance(options, JSONOptions):
831865
msg = "expected 'options' argument to have class 'JSONOptions'"
832866
raise TypeError(msg)
867+
833868
encoded = _encode_json(
834869
data,
835870
indent=options.indent,
@@ -841,12 +876,14 @@ def encode(
841876
if not isinstance(options, MsgPackOptions):
842877
msg = "expected 'options' argument to have class 'MsgPackOptions'"
843878
raise TypeError(msg)
879+
844880
encoded = _encode_msgpack(data)
845881

846882
case "python":
847883
if not isinstance(options, PythonOptions):
848884
msg = "expected 'options' argument to have class 'PythonOptions'"
849885
raise TypeError(msg)
886+
850887
encoded = _encode_python(
851888
data,
852889
indent=options.indent,
@@ -858,14 +895,17 @@ def encode(
858895
if not isinstance(options, TOMLOptions):
859896
msg = "expected 'options' argument to have class 'TOMLOptions'"
860897
raise TypeError(msg)
898+
861899
if not isinstance(data, Mapping):
862900
msg = (
863901
f"Top-level value of type '{type(data).__name__}' cannot "
864902
"be encoded as TOML"
865903
)
866904
raise TypeError(msg)
905+
867906
encoded = _encode_toml(
868907
data,
908+
multiline_threshold=options.multiline_threshold,
869909
sort_keys=options.sort_keys,
870910
stringify=options.stringify,
871911
).encode(UTF_8)
@@ -874,6 +914,7 @@ def encode(
874914
if not isinstance(options, YAMLOptions):
875915
msg = "expected 'options' argument to have class 'YAMLOptions'"
876916
raise TypeError(msg)
917+
877918
encoded = _encode_yaml(
878919
data,
879920
indent=options.indent,
@@ -959,6 +1000,7 @@ def main() -> None:
9591000
options = format_options(
9601001
args.output_format,
9611002
indent=args.indent,
1003+
multiline_threshold=args.multiline_threshold,
9621004
sort_keys=args.sort_keys,
9631005
stringify=args.stringify,
9641006
width=args.width,

tests/multiline-3.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
foo = [
2+
1,
3+
2,
4+
[3],
5+
4,
6+
5,
7+
]
8+
bar = [
9+
1,
10+
[2, 3, 4, 5, 6],
11+
7,
12+
]
13+
14+
[baz]
15+
qux = [
16+
1,
17+
2,
18+
3,
19+
]
20+
quux = [
21+
1,
22+
2,
23+
3,
24+
4,
25+
]
26+
quuux = [
27+
1,
28+
2,
29+
3,
30+
4,
31+
5,
32+
]

tests/multiline-5.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
foo = [
2+
1,
3+
2,
4+
[3],
5+
4,
6+
5,
7+
]
8+
bar = [1, [2, 3, 4, 5, 6], 7]
9+
10+
[baz]
11+
qux = [1, 2, 3]
12+
quux = [1, 2, 3, 4]
13+
quuux = [
14+
1,
15+
2,
16+
3,
17+
4,
18+
5,
19+
]

tests/multiline.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"foo": [1, 2, [3], 4, 5],
3+
"bar": [1, [2, 3, 4, 5, 6], 7],
4+
"baz": {
5+
"qux": [1, 2, 3],
6+
"quux": [1, 2, 3, 4],
7+
"quuux": [1, 2, 3, 4, 5]
8+
}
9+
}

tests/multiline.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
foo = [1, 2, [3], 4, 5]
2+
bar = [1, [2, 3, 4, 5, 6], 7]
3+
4+
[baz]
5+
qux = [1, 2, 3]
6+
quux = [1, 2, 3, 4]
7+
quuux = [1, 2, 3, 4, 5]

tests/test_remarshal.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def _convert_and_read( # noqa: PLR0913
100100
output_format: str,
101101
*,
102102
indent: int | None = None,
103+
multiline_threshold: int = Defaults.MULTILINE_THRESHOLD,
103104
output_filename: str,
104105
sort_keys: bool = False,
105106
stringify: bool = False,
@@ -113,6 +114,7 @@ def _convert_and_read( # noqa: PLR0913
113114
options = remarshal.format_options(
114115
output_format,
115116
indent=indent,
117+
multiline_threshold=multiline_threshold,
116118
sort_keys=sort_keys,
117119
stringify=stringify,
118120
width=width,
@@ -221,6 +223,25 @@ def test_json2toml(self, convert_and_read) -> None:
221223
)
222224
assert output_sig == reference_sig
223225

226+
def test_json2toml_multiline_default(self, convert_and_read) -> None:
227+
output = convert_and_read("multiline.json", "json", "toml")
228+
reference = read_file("multiline.toml")
229+
assert output == reference
230+
231+
def test_json2toml_multiline_3(self, convert_and_read) -> None:
232+
output = convert_and_read(
233+
"multiline.json", "json", "toml", multiline_threshold=3
234+
)
235+
reference = read_file("multiline-3.toml")
236+
assert output == reference
237+
238+
def test_json2toml_multiline_5(self, convert_and_read) -> None:
239+
output = convert_and_read(
240+
"multiline.json", "json", "toml", multiline_threshold=5
241+
)
242+
reference = read_file("multiline-5.toml")
243+
assert output == reference
244+
224245
def test_json2yaml(self, convert_and_read) -> None:
225246
output = convert_and_read("example.json", "json", "yaml").decode("utf-8")
226247
reference = read_file("example.yaml").decode("utf-8")

0 commit comments

Comments
 (0)