From e1166e4a5fb536ec04b7e735adbfeb6aee8e88a7 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:05:30 +0200 Subject: [PATCH 1/2] Dump multi-line strings as YAML literal blocks --- CHANGELOG.rst | 3 +++ DOCUMENTATION.rst | 7 ++++-- jsonargparse/_loaders_dumpers.py | 24 ++++++++++++++++++- jsonargparse_tests/test_loaders_dumpers.py | 27 ++++++++++++++++++++++ jsonargparse_tests/test_yaml_comments.py | 13 +++++++++++ 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f4f8ffc4..c1db0f26 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -53,6 +53,9 @@ Changed gives a more readable output. Use the new ``json_compact`` format for the previous single line output (`#970 `__). +- The ``yaml`` dump format now writes multi-line strings as literal blocks, i.e. + ``|``, instead of escaping the line breaks (`#??? + `__). Removed ^^^^^^^ diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index 6260085a..f403aa9c 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -1424,8 +1424,11 @@ From Python, a config object is serialized with the :meth:`dump <.ArgumentParser.dump>` and :meth:`save <.ArgumentParser.save>` methods. The supported formats are ``yaml``, ``toml``, ``json``/``json_indented``, ``json_compact`` and ``parser_mode``, the default, which uses the format of the -parser. More formats are added with :func:`.set_dumper`, for example to dump -with PyYAML's ``default_flow_style``: +parser. The ``yaml`` format dumps with a subclass of `yaml.SafeDumper +`__ that writes multi-line +strings as literal blocks, i.e. ``|``, instead of escaping the line breaks. More +formats are added with :func:`.set_dumper`, for example to dump with PyYAML's +``default_flow_style``: .. testcode:: diff --git a/jsonargparse/_loaders_dumpers.py b/jsonargparse/_loaders_dumpers.py index c26625c9..d23f83c7 100644 --- a/jsonargparse/_loaders_dumpers.py +++ b/jsonargparse/_loaders_dumpers.py @@ -28,6 +28,7 @@ not_loaded = object() yaml_default_loader = None +yaml_default_dumper = None def load_basic(value): @@ -246,9 +247,30 @@ def replace_unset(data): return data +def get_yaml_default_dumper(): + global yaml_default_dumper + if yaml_default_dumper: + return yaml_default_dumper + + yaml = import_pyyaml("get_yaml_default_dumper") + + class DefaultDumper(yaml.SafeDumper): + pass + + def represent_str(dumper, data): + # literal block style for multiline strings, unless not representable as such + style = "|" if "\n" in data else None + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style) + + DefaultDumper.add_representer(str, represent_str) + + yaml_default_dumper = DefaultDumper + return yaml_default_dumper + + def yaml_dump(data): yaml = import_pyyaml("yaml_dump") - return yaml.safe_dump(data, **dump_yaml_kwargs) + return yaml.dump(data, Dumper=get_yaml_default_dumper(), **dump_yaml_kwargs) def yaml_comments_dump(data, parser): diff --git a/jsonargparse_tests/test_loaders_dumpers.py b/jsonargparse_tests/test_loaders_dumpers.py index f0045d69..950fbd83 100644 --- a/jsonargparse_tests/test_loaders_dumpers.py +++ b/jsonargparse_tests/test_loaders_dumpers.py @@ -95,6 +95,33 @@ def test_set_loader_parser_mode_subparsers(parser, subparser): assert "custom" == subparser.parser_mode +@skip_if_no_pyyaml +def test_dump_yaml_multiline_string(parser): + parser.add_argument("--text", type=str) + cfg = parser.parse_args(["--text=first line\nsecond line\n"]) + dump = parser.dump(cfg) + assert dump == "text: |\n first line\n second line\n" + assert json_or_yaml_load(dump) == {"text": "first line\nsecond line\n"} + + +@skip_if_no_pyyaml +def test_dump_yaml_multiline_string_no_trailing_newline(parser): + parser.add_argument("--text", type=str) + cfg = parser.parse_args(["--text=first line\nsecond line"]) + dump = parser.dump(cfg) + assert dump == "text: |-\n first line\n second line\n" + assert json_or_yaml_load(dump) == {"text": "first line\nsecond line"} + + +@skip_if_no_pyyaml +def test_dump_yaml_multiline_string_block_not_possible(parser): + parser.add_argument("--text", type=str) + cfg = parser.parse_args(["--text=trailing space \nsecond line\n"]) + dump = parser.dump(cfg) + assert dump == 'text: "trailing space \\nsecond line\\n"\n' + assert json_or_yaml_load(dump) == {"text": "trailing space \nsecond line\n"} + + @skip_if_no_pyyaml def test_dump_header_yaml(parser): parser.add_argument("--int", type=int, default=1) diff --git a/jsonargparse_tests/test_yaml_comments.py b/jsonargparse_tests/test_yaml_comments.py index b4992a6c..a208ae5e 100644 --- a/jsonargparse_tests/test_yaml_comments.py +++ b/jsonargparse_tests/test_yaml_comments.py @@ -56,6 +56,19 @@ def block(text: str, depth: int = 0) -> str: return indent(dedent(text), " " * depth) +def test_dump_comments_multiline_string(parser): + parser.add_argument("--text", type=str, help="Some text.") + dump = get_dump(parser, ["--text=first line\nsecond line\n"]) + assert dump == block( + """ + # Some text. (type: str, default: null) + text: | + first line + second line + """ + ) + + class Optimizer: def __init__(self, lr: float = 0.1): """Base optimizer. From fd0a0d5ba00cfe21e7dc824085c8e1bb6af63538 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:07:54 +0200 Subject: [PATCH 2/2] Pull request number --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c1db0f26..bc13530a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -54,8 +54,8 @@ Changed previous single line output (`#970 `__). - The ``yaml`` dump format now writes multi-line strings as literal blocks, i.e. - ``|``, instead of escaping the line breaks (`#??? - `__). + ``|``, instead of escaping the line breaks (`#972 + `__). Removed ^^^^^^^