Skip to content

Commit 63bc9e4

Browse files
committed
refactor!: put all format options in classes
This removes the formatting arguments to `remarshal.remarshal` and makes it clear what format uses what options. Customizing the output is one extra function call. Conversions with the default formatting are the same as before. I am not very concerned about breaking the Python API because I have never heard from API users and have found none on GitHub. Remarshal users use it through the CLI.
1 parent 6bd8273 commit 63bc9e4

File tree

2 files changed

+184
-86
lines changed

2 files changed

+184
-86
lines changed

src/remarshal/main.py

Lines changed: 163 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -46,35 +46,73 @@
4646
if TYPE_CHECKING:
4747
from rich.style import StyleType
4848

49-
YAMLStyle = Literal["", "'", '"', "|", ">"]
50-
5149

5250
class CLIDefaults:
5351
INDENT = None
5452
SORT_KEYS = False
5553
STRINGIFY = False
56-
WIDTH = 80
5754

5855

5956
class Defaults:
6057
JSON_INDENT = 4
6158
MAX_VALUES = 1000000
6259
YAML_INDENT = 2
6360
YAML_STYLE = ""
61+
WIDTH = 80
62+
63+
PYTHON_WIDTH = WIDTH
64+
YAML_WIDTH = WIDTH
65+
66+
67+
YAMLStyle = Literal["", "'", '"', "|", ">"]
6468

6569

6670
@dataclass(frozen=True)
6771
class FormatOptions:
6872
pass
6973

7074

75+
@dataclass(frozen=True)
76+
class CBOROptions(FormatOptions):
77+
pass
78+
79+
80+
@dataclass(frozen=True)
81+
class JSONOptions(FormatOptions):
82+
indent: int | None = Defaults.JSON_INDENT
83+
sort_keys: bool = True
84+
stringify: bool = False
85+
86+
87+
@dataclass(frozen=True)
88+
class MsgPackOptions(FormatOptions):
89+
pass
90+
91+
92+
@dataclass(frozen=True)
93+
class PythonOptions(FormatOptions):
94+
indent: int | None = None
95+
sort_keys: bool = True
96+
width: int = Defaults.PYTHON_WIDTH
97+
98+
99+
@dataclass(frozen=True)
100+
class TOMLOptions(FormatOptions):
101+
indent: int | None = None
102+
sort_keys: bool = True
103+
stringify: bool = False
104+
105+
71106
@dataclass(frozen=True)
72107
class YAMLOptions(FormatOptions):
108+
indent: int = Defaults.YAML_INDENT
73109
style: YAMLStyle = Defaults.YAML_STYLE
110+
width: int = Defaults.YAML_WIDTH
74111

75112

76113
__all__ = [
77114
"INPUT_FORMATS",
115+
"OPTIONS_CLASSES",
78116
"OUTPUT_FORMATS",
79117
"RICH_ARGPARSE_STYLES",
80118
"CLIDefaults",
@@ -85,6 +123,7 @@ class YAMLOptions(FormatOptions):
85123
"YAMLOptions",
86124
"decode",
87125
"encode",
126+
"format_options",
88127
"identity",
89128
"main",
90129
"remarshal",
@@ -94,6 +133,14 @@ class YAMLOptions(FormatOptions):
94133

95134
INPUT_FORMATS = ["cbor", "json", "msgpack", "toml", "yaml"]
96135
OUTPUT_FORMATS = ["cbor", "json", "msgpack", "python", "toml", "yaml"]
136+
OPTIONS_CLASSES = {
137+
"cbor": CBOROptions,
138+
"json": JSONOptions,
139+
"msgpack": MsgPackOptions,
140+
"python": PythonOptions,
141+
"toml": TOMLOptions,
142+
"yaml": YAMLOptions,
143+
}
97144
UTF_8 = "utf-8"
98145

99146
RICH_ARGPARSE_STYLES: dict[str, StyleType] = {
@@ -289,7 +336,7 @@ def output_width(value: str) -> int:
289336

290337
parser.add_argument(
291338
"--width",
292-
default=CLIDefaults.WIDTH,
339+
default=Defaults.WIDTH,
293340
metavar="<n>",
294341
type=output_width, # Allow "inf".
295342
help=(
@@ -351,13 +398,6 @@ def output_width(value: str) -> int:
351398
if args.output_format == "":
352399
parser.error("Need an explicit output format")
353400

354-
# Replace `yaml_*` options with `YAMLOptions`.
355-
vars(args)["yaml_options"] = YAMLOptions(
356-
style=args.yaml_style,
357-
)
358-
359-
del vars(args)["yaml_style"]
360-
361401
return args
362402

363403

@@ -622,22 +662,21 @@ def _encode_python(
622662
indent: int | None,
623663
sort_keys: bool,
624664
width: int,
625-
) -> bytes:
665+
) -> str:
626666
compact = False
627667
if indent is None:
628668
compact = True
629669
indent = 0
630670

631-
return bytes(
671+
return (
632672
pprint.pformat(
633673
data,
634674
compact=compact,
635675
indent=indent,
636676
sort_dicts=sort_keys,
637677
width=width,
638678
)
639-
+ "\n",
640-
UTF_8,
679+
+ "\n"
641680
)
642681

643682

@@ -695,20 +734,13 @@ def _encode_yaml(
695734
data: Document,
696735
*,
697736
indent: int | None,
698-
options: FormatOptions | None,
737+
style: YAMLStyle,
699738
width: int,
700739
) -> str:
701-
if options is None:
702-
options = YAMLOptions()
703-
704-
if not isinstance(options, YAMLOptions):
705-
msg = "'options' not of type 'YAMLOptions'"
706-
raise TypeError(msg)
707-
708740
yaml = ruamel.yaml.YAML()
709741
yaml.default_flow_style = False
710742

711-
yaml.default_style = options.style # type: ignore
743+
yaml.default_style = style # type: ignore
712744
yaml.indent = indent
713745
yaml.width = width
714746

@@ -728,53 +760,123 @@ def _encode_yaml(
728760
raise ValueError(msg)
729761

730762

763+
def format_options(
764+
output_format: str,
765+
*,
766+
indent: int | None = None,
767+
sort_keys: bool = False,
768+
stringify: bool = False,
769+
width: int = Defaults.WIDTH,
770+
yaml_style: YAMLStyle = Defaults.YAML_STYLE,
771+
) -> FormatOptions:
772+
if output_format == "cbor":
773+
return CBOROptions()
774+
775+
if output_format == "json":
776+
return JSONOptions(
777+
indent=indent,
778+
sort_keys=sort_keys,
779+
stringify=stringify,
780+
)
781+
782+
if output_format == "msgpack":
783+
return MsgPackOptions()
784+
785+
if output_format == "python":
786+
return PythonOptions(
787+
indent=indent,
788+
sort_keys=sort_keys,
789+
width=width,
790+
)
791+
792+
if output_format == "toml":
793+
return TOMLOptions(
794+
sort_keys=sort_keys,
795+
stringify=stringify,
796+
)
797+
798+
if output_format == "yaml":
799+
return YAMLOptions(
800+
indent=Defaults.YAML_INDENT if indent is None else indent,
801+
style=yaml_style,
802+
width=width,
803+
)
804+
805+
msg = f"Unknown output format: {output_format}"
806+
raise ValueError(msg)
807+
808+
731809
def encode(
732810
output_format: str,
733811
data: Document,
734812
*,
735-
indent: int | None,
736813
options: FormatOptions | None,
737-
sort_keys: bool,
738-
stringify: bool,
739-
width: int,
740814
) -> bytes:
741-
if output_format == "json":
815+
if output_format == "cbor":
816+
if not isinstance(options, CBOROptions):
817+
msg = "expected 'options' argument to have class 'CBOROptions'"
818+
raise TypeError(msg)
819+
820+
encoded = _encode_cbor(data)
821+
822+
elif output_format == "json":
823+
if not isinstance(options, JSONOptions):
824+
msg = "expected 'options' argument to have class 'JSONOptions'"
825+
raise TypeError(msg)
826+
742827
encoded = _encode_json(
743828
data,
744-
indent=indent,
745-
sort_keys=sort_keys,
746-
stringify=stringify,
829+
indent=options.indent,
830+
sort_keys=options.sort_keys,
831+
stringify=options.stringify,
747832
).encode(UTF_8)
833+
748834
elif output_format == "msgpack":
835+
if not isinstance(options, MsgPackOptions):
836+
msg = "expected 'options' argument to have class 'MsgPackOptions'"
837+
raise TypeError(msg)
749838
encoded = _encode_msgpack(data)
839+
750840
elif output_format == "python":
841+
if not isinstance(options, PythonOptions):
842+
msg = "expected 'options' argument to have class 'PythonOptions'"
843+
raise TypeError(msg)
751844
encoded = _encode_python(
752845
data,
753-
indent=indent,
754-
sort_keys=sort_keys,
755-
width=width,
756-
)
846+
indent=options.indent,
847+
sort_keys=options.sort_keys,
848+
width=options.width,
849+
).encode(UTF_8)
850+
757851
elif output_format == "toml":
852+
if not isinstance(options, TOMLOptions):
853+
msg = "expected 'options' argument to have class 'TOMLOptions'"
854+
raise TypeError(msg)
855+
758856
if not isinstance(data, Mapping):
759857
msg = (
760858
f"Top-level value of type '{type(data).__name__}' cannot "
761859
"be encoded as TOML"
762860
)
763861
raise TypeError(msg)
764-
encoded = _encode_toml(data, sort_keys=sort_keys, stringify=stringify).encode(
765-
UTF_8
766-
)
862+
encoded = _encode_toml(
863+
data,
864+
sort_keys=options.sort_keys,
865+
stringify=options.stringify,
866+
).encode(UTF_8)
867+
767868
elif output_format == "yaml":
869+
if not isinstance(options, YAMLOptions):
870+
msg = "expected 'options' argument to have class 'YAMLOptions'"
871+
raise TypeError(msg)
872+
768873
encoded = _encode_yaml(
769874
data,
770-
indent=indent,
771-
options=options,
772-
width=width,
875+
indent=options.indent,
876+
style=options.style,
877+
width=options.width,
773878
).encode(UTF_8)
774-
elif output_format == "msgpack":
775-
encoded = _encode_msgpack(data)
776-
elif output_format == "cbor":
777-
encoded = _encode_cbor(data)
879+
778880
else:
779881
msg = f"Unknown output format: {output_format}"
780882
raise ValueError(msg)
@@ -785,32 +887,31 @@ def encode(
785887
# === Main ===
786888

787889

788-
def remarshal( # noqa: PLR0913
890+
def remarshal(
789891
input_format: str,
790892
output_format: str,
791893
input: Path | str,
792894
output: Path | str,
793895
*,
794-
indent: int | None = None,
795896
max_values: int = Defaults.MAX_VALUES,
796897
options: FormatOptions | None = None,
797-
sort_keys: bool = True,
798-
stringify: bool = False,
799898
transform: Callable[[Document], Document] | None = None,
800899
unwrap: str | None = None,
801-
width: int = CLIDefaults.WIDTH,
802900
wrap: str | None = None,
803901
) -> None:
804902
input_file = None
805903
output_file = None
806904

905+
if options is None:
906+
options = format_options(output_format)
907+
807908
try:
808909
input_file = sys.stdin.buffer if input == "-" else Path(input).open("rb")
809910
output_file = sys.stdout.buffer if output == "-" else Path(output).open("wb")
810911

811912
input_data = input_file.read()
812913
if not isinstance(input_data, bytes):
813-
msg = "input_data must be bytes"
914+
msg = "'input_data' must be 'bytes'"
814915
raise TypeError(msg)
815916

816917
parsed = decode(input_format, input_data)
@@ -836,11 +937,7 @@ def remarshal( # noqa: PLR0913
836937
encoded = encode(
837938
output_format,
838939
parsed,
839-
indent=indent,
840940
options=options,
841-
sort_keys=sort_keys,
842-
stringify=stringify,
843-
width=width,
844941
)
845942

846943
output_file.write(encoded)
@@ -855,16 +952,22 @@ def main() -> None:
855952
args = _parse_command_line(sys.argv)
856953

857954
try:
955+
options = format_options(
956+
args.output_format,
957+
indent=args.indent,
958+
sort_keys=args.sort_keys,
959+
stringify=args.stringify,
960+
width=args.width,
961+
yaml_style=args.yaml_style,
962+
)
963+
858964
remarshal(
859965
args.input_format,
860966
args.output_format,
861967
args.input,
862968
args.output,
863-
indent=args.indent,
864969
max_values=args.max_values,
865-
options=args.yaml_options,
866-
sort_keys=args.sort_keys,
867-
stringify=args.stringify,
970+
options=options,
868971
unwrap=args.unwrap,
869972
wrap=args.wrap,
870973
)

0 commit comments

Comments
 (0)