Skip to content

Commit d320378

Browse files
committed
feat(yaml): support YAML 1.1
1 parent a8cb0f1 commit d320378

File tree

6 files changed

+132
-33
lines changed

6 files changed

+132
-33
lines changed

README.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ Remarshal can also convert all supported formats to Python code.
1414

1515
## Known limitations and quirks
1616

17-
### YAML 1.2 only
17+
### YAML versions
1818

19-
Remarshal works with YAML 1.2.
20-
The last version that read and wrote YAML 1.1 was 0.17.1.
21-
Install it if you need YAML 1.1.
19+
Remarshal works with YAML 1.2 by default.
20+
You can choose the input and/or output format `yaml-1.1` to work with YAML 1.1; `yaml-1.2` lets you emphasize you mean 1.2.
21+
Conversion from YAML 1.1 to YAML 1.2 and from YAML 1.2 to YAML 1.1 is supported.
2222

2323
### Lossless by default; lossy must be enabled
2424

@@ -133,9 +133,11 @@ uv tool install https://github.com/remarshal-project/remarshal
133133
## Usage
134134

135135
```none
136-
usage: remarshal [-h] [-v] [-f {cbor,json,msgpack,toml,yaml}] [-i <input>]
137-
[--indent <n>] [-k] [--max-values <n>] [--multiline <n>]
138-
[-o <output>] [-s] [-t {cbor,json,msgpack,python,toml,yaml}]
136+
usage: remarshal [-h] [-v]
137+
[-f {cbor,json,msgpack,toml,yaml,yaml-1.1,yaml-1.2}]
138+
[-i <input>] [--indent <n>] [-k] [--max-values <n>]
139+
[--multiline <n>] [-o <output>] [-s]
140+
[-t {cbor,json,msgpack,python,toml,yaml,yaml-1.1,yaml-1.2}]
139141
[--unwrap <key>] [--verbose] [--width <n>] [--wrap <key>]
140142
[--yaml-style {,',",|,>}] [--yaml-style-newline {,',",|,>}]
141143
[input] [output]
@@ -149,7 +151,8 @@ positional arguments:
149151
options:
150152
-h, --help show this help message and exit
151153
-v, --version show program's version number and exit
152-
-f, --from, --if, --input-format {cbor,json,msgpack,toml,yaml}
154+
-f, --from, --if, --input-format
155+
{cbor,json,msgpack,toml,yaml,yaml-1.1,yaml-1.2}
153156
input format
154157
-i, --input <input> input file
155158
--indent <n> JSON and YAML indentation
@@ -164,7 +167,8 @@ options:
164167
output file
165168
-s, --sort-keys sort JSON, Python, and TOML keys instead of preserving
166169
key order
167-
-t, --to, --of, --output-format {cbor,json,msgpack,python,toml,yaml}
170+
-t, --to, --of, --output-format
171+
{cbor,json,msgpack,python,toml,yaml,yaml-1.1,yaml-1.2}
168172
output format
169173
--unwrap <key> only output the data stored under the given key
170174
--verbose print debug information when an error occurs

completions/remarshal.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ _remarshal() {
55
cur=${COMP_WORDS[COMP_CWORD]}
66
prev=${COMP_WORDS[COMP_CWORD - 1]}
77

8-
formats='cbor json msgpack toml yaml'
8+
formats='cbor json msgpack toml yaml yaml-1.1 yaml-1.2'
99
input_formats=$formats
1010
output_formats="$formats python"
1111

completions/remarshal.fish

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ set --local yaml_styles '"\'" "\\"" "|" ">"'
33
complete -c remarshal -s h -l help -d "Show help message and exit"
44
complete -c remarshal -s v -l version -d "Show program's version number and exit"
55

6-
complete -c remarshal -s f -l from -l if -l input-format -x -a "cbor json msgpack toml yaml" -d "Input format"
6+
complete -c remarshal -s f -l from -l if -l input-format -x -a "cbor json msgpack toml yaml yaml-1.1 yaml-1.2" -d "Input format"
77
complete -c remarshal -s i -l input -r -d "Input file"
88
complete -c remarshal -l indent -x -d "JSON and YAML indentation"
99
complete -c remarshal -s k -l stringify -d "Turn special values into strings"
1010
complete -c remarshal -l max-values -x -d "Maximum number of values in input data"
1111
complete -c remarshal -l multiline -x -d "Minimum items for multiline TOML array"
1212
complete -c remarshal -s o -l output -r -d "Output file"
1313
complete -c remarshal -s s -l sort-keys -d "Sort JSON, Python, and TOML keys"
14-
complete -c remarshal -s t -l to -l of -l output-format -x -a "cbor json msgpack python toml yaml" -d "Output format"
14+
complete -c remarshal -s t -l to -l of -l output-format -x -a "cbor json msgpack python toml yaml yaml-1.1 yaml-1.2" -d "Output format"
1515
complete -c remarshal -l unwrap -x -d "Only output data under given key"
1616
complete -c remarshal -l verbose -d "Print debug information on error"
1717
complete -c remarshal -l width -x -d "Python and YAML line width"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[project]
66
name = "remarshal"
7-
version = "1.1.0"
7+
version = "1.2.0"
88
description = "Convert between CBOR, JSON, MessagePack, TOML, and YAML"
99
authors = [{ name = "D. Bohdan", email = "dbohdan@dbohdan.com" }]
1010
license = { text = "MIT" }

src/remarshal/main.py

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class Defaults:
6767

6868
Document = Union[bool, bytes, datetime.datetime, Mapping, None, Sequence, str]
6969
YAMLStyle = Literal["", "'", '"', "|", ">"]
70+
YAMLVersion = Union[tuple[int, int], None]
7071

7172

7273
@dataclass(frozen=True)
@@ -111,6 +112,7 @@ class YAMLOptions(FormatOptions):
111112
indent: int = Defaults.YAML_INDENT
112113
style: YAMLStyle = Defaults.YAML_STYLE
113114
style_newline: YAMLStyle | None = None
115+
version: YAMLVersion = (1, 2)
114116
width: int = Defaults.WIDTH
115117

116118

@@ -124,6 +126,7 @@ class YAMLOptions(FormatOptions):
124126
"Document",
125127
"TooManyValuesError",
126128
"YAMLStyle",
129+
"YAMLVersion",
127130
# Format dataclasses.
128131
"FormatOptions",
129132
"CBOROptions",
@@ -142,16 +145,36 @@ class YAMLOptions(FormatOptions):
142145
]
143146

144147

145-
INPUT_FORMATS = ["cbor", "json", "msgpack", "toml", "yaml"]
146-
OUTPUT_FORMATS = ["cbor", "json", "msgpack", "python", "toml", "yaml"]
147-
OUTPUT_FORMATS_ARGV0 = ["cbor", "json", "msgpack", "py", "toml", "yaml"]
148+
INPUT_FORMATS = ["cbor", "json", "msgpack", "toml", "yaml", "yaml-1.1", "yaml-1.2"]
149+
OUTPUT_FORMATS = [
150+
"cbor",
151+
"json",
152+
"msgpack",
153+
"python",
154+
"toml",
155+
"yaml",
156+
"yaml-1.1",
157+
"yaml-1.2",
158+
]
159+
OUTPUT_FORMATS_ARGV0 = [
160+
"cbor",
161+
"json",
162+
"msgpack",
163+
"py",
164+
"toml",
165+
"yaml",
166+
"yaml-1.1",
167+
"yaml-1.2",
168+
]
148169
OPTIONS_CLASSES = {
149170
"cbor": CBOROptions,
150171
"json": JSONOptions,
151172
"msgpack": MsgPackOptions,
152173
"python": PythonOptions,
153174
"toml": TOMLOptions,
154175
"yaml": YAMLOptions,
176+
"yaml-1.1": YAMLOptions,
177+
"yaml-1.2": YAMLOptions,
155178
}
156179
UTF_8 = "utf-8"
157180

@@ -436,6 +459,17 @@ def output_width(value: str) -> int:
436459
return args
437460

438461

462+
def _yaml_version(format: str) -> YAMLVersion:
463+
match format:
464+
case "yaml-1.1":
465+
return (1, 1)
466+
467+
case "yaml-1.2":
468+
return (1, 2)
469+
470+
return None
471+
472+
439473
# === Parser/serializer wrappers ===
440474

441475

@@ -532,9 +566,11 @@ def _decode_toml(input_data: bytes) -> Document:
532566
raise ValueError(msg)
533567

534568

535-
def _decode_yaml(input_data: bytes) -> Document:
569+
def _decode_yaml(input_data: bytes, version: YAMLVersion) -> Document:
536570
try:
537571
yaml = ruamel.yaml.YAML(pure=True, typ="safe")
572+
yaml.version = version
573+
538574
doc = yaml.load(input_data)
539575

540576
return cast("Document", doc)
@@ -545,19 +581,25 @@ def _decode_yaml(input_data: bytes) -> Document:
545581

546582

547583
def decode(input_format: str, input_data: bytes) -> Document:
548-
decoder = {
549-
"cbor": _decode_cbor,
550-
"json": _decode_json,
551-
"msgpack": _decode_msgpack,
552-
"toml": _decode_toml,
553-
"yaml": _decode_yaml,
554-
}
555-
556-
if input_format not in decoder:
557-
msg = f"Unknown input format: {input_format}"
558-
raise ValueError(msg)
584+
match input_format:
585+
case "cbor":
586+
return _decode_cbor(input_data)
559587

560-
return decoder[input_format](input_data)
588+
case "json":
589+
return _decode_json(input_data)
590+
591+
case "msgpack":
592+
return _decode_msgpack(input_data)
593+
594+
case "toml":
595+
return _decode_toml(input_data)
596+
597+
case "yaml" | "yaml-1.1" | "yaml-1.2":
598+
return _decode_yaml(input_data, version=_yaml_version(input_format))
599+
600+
case _:
601+
msg = f"Unknown input format: {input_format}"
602+
raise ValueError(msg)
561603

562604

563605
class TooManyValuesError(BaseException):
@@ -776,13 +818,14 @@ def _encode_yaml(
776818
indent: int | None,
777819
style: YAMLStyle,
778820
style_newline: YAMLStyle | None,
821+
version: YAMLVersion,
779822
width: int,
780823
) -> str:
781824
yaml = ruamel.yaml.YAML(pure=True)
782825
yaml.default_flow_style = False
783-
784826
yaml.default_style = style # type: ignore
785827
yaml.indent = indent
828+
yaml.version = version
786829
yaml.width = width
787830

788831
def represent_none(self, data):
@@ -848,11 +891,12 @@ def format_options(
848891
stringify=stringify,
849892
)
850893

851-
case "yaml":
894+
case "yaml" | "yaml-1.1" | "yaml-1.2":
852895
return YAMLOptions(
853896
indent=Defaults.YAML_INDENT if indent is None else indent,
854897
style=yaml_style,
855898
style_newline=yaml_style_newline,
899+
version=_yaml_version(output_format),
856900
width=width,
857901
)
858902

@@ -925,7 +969,7 @@ def encode(
925969
stringify=options.stringify,
926970
).encode(UTF_8)
927971

928-
case "yaml":
972+
case "yaml" | "yaml-1.1" | "yaml-1.2":
929973
if not isinstance(options, YAMLOptions):
930974
msg = "expected 'options' argument to have class 'YAMLOptions'"
931975
raise TypeError(msg)
@@ -935,6 +979,7 @@ def encode(
935979
indent=options.indent,
936980
style=options.style,
937981
style_newline=options.style_newline,
982+
version=options.version,
938983
width=options.width,
939984
).encode(UTF_8)
940985

tests/test_remarshal.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,16 @@ def test_binary_to_toml(self, convert_and_read) -> None:
458458
def test_binary_to_yaml(self, convert_and_read) -> None:
459459
convert_and_read("bin.msgpack", "msgpack", "yaml")
460460

461+
def test_yaml_1_1_features(self, convert_and_read) -> None:
462+
output = convert_and_read("yaml-1.1-features.yaml", "yaml-1.1", "json")
463+
reference = read_file("yaml-1.1-features.json")
464+
assert output == reference
465+
466+
def test_yaml_1_1_features_in_1_2(self, convert_and_read) -> None:
467+
output = convert_and_read("yaml-1.1-features.yaml", "yaml-1.2", "json")
468+
reference = read_file("yaml-1.1-features-in-1.2.json")
469+
assert output == reference
470+
461471
def test_yaml_null(self, convert_and_read) -> None:
462472
output = convert_and_read("null.json", "json", "yaml")
463473
reference = read_file("null.yaml")
@@ -789,7 +799,7 @@ def test_yaml_billion_laughs(self, convert_and_read) -> None:
789799
with pytest.raises(remarshal.TooManyValuesError):
790800
convert_and_read("lol.yml", "yaml", "json")
791801

792-
def test_yaml_norway_problem(self, convert_and_read) -> None:
802+
def test_yaml_norway_problem_from_default(self, convert_and_read) -> None:
793803
output = convert_and_read(
794804
"norway.yaml",
795805
"yaml",
@@ -799,6 +809,44 @@ def test_yaml_norway_problem(self, convert_and_read) -> None:
799809
reference = read_file("norway.json")
800810
assert output == reference
801811

812+
def test_yaml_norway_problem_from_yaml_1_1(self, convert_and_read) -> None:
813+
output = convert_and_read(
814+
"norway.yaml",
815+
"yaml-1.1",
816+
"json",
817+
indent=None,
818+
)
819+
reference = read_file("norway-yaml-1.1.json")
820+
assert output == reference
821+
822+
def test_yaml_norway_problem_from_yaml_1_2(self, convert_and_read) -> None:
823+
output = convert_and_read(
824+
"norway.yaml",
825+
"yaml-1.2",
826+
"json",
827+
indent=None,
828+
)
829+
reference = read_file("norway.json")
830+
assert output == reference
831+
832+
def test_yaml_norway_problem_to_yaml_1_1(self, convert_and_read) -> None:
833+
output = convert_and_read(
834+
"norway.json",
835+
"json",
836+
"yaml-1.1",
837+
)
838+
reference = read_file("norway-yaml-1.1.yaml")
839+
assert output == reference
840+
841+
def test_yaml_norway_problem_to_yaml_1_2(self, convert_and_read) -> None:
842+
output = convert_and_read(
843+
"norway.json",
844+
"json",
845+
"yaml-1.2",
846+
)
847+
reference = read_file("norway-yaml-1.2.yaml")
848+
assert output == reference
849+
802850
def test_toml2cbor_date(self, convert_and_read) -> None:
803851
output = convert_and_read("date.toml", "toml", "cbor")
804852
reference = read_file("date.cbor")
@@ -826,10 +874,12 @@ def test_toml2msgpack_date(self, convert_and_read) -> None:
826874
def test_toml2toml_date(self, convert_and_read) -> None:
827875
output = convert_and_read("date.toml", "toml", "toml")
828876
reference = read_file("date.toml")
877+
assert output == reference
829878

830879
def test_toml2yaml_date(self, convert_and_read) -> None:
831880
output = convert_and_read("date.toml", "toml", "yaml")
832881
reference = read_file("date.yaml")
882+
assert output == reference
833883

834884
def test_toml2cbor_datetime_local(self, convert_and_read) -> None:
835885
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)