diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e3ff2375..3b2bd8ba 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,9 @@ Changed parser/subcommand the option was given to. Also suggests running ``--help`` for the corresponding parser/subcommand (`#809 `__). +- The ``yaml_comments`` parameter of ``ArgumentParser.dump`` has been renamed to + ``with_comments`` to allow future comments support of other formats (`#811 + `__). Deprecated ^^^^^^^^^^ @@ -28,6 +31,9 @@ Deprecated ``ArgumentParser.parse_*`` are deprecated and will be removed in v5.0.0. Instead use ``.clone(with_meta=...)`` (`#810 `__). +- The ``yaml_comments`` parameter of ``ArgumentParser.dump`` is deprecated and + will be removed in v5.0.0. Use ``with_comments`` instead (`#811 + `__). v4.43.0 (2025-11-11) diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index 2455a7bc..acf22626 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -284,7 +284,7 @@ Depending on the parse method used (see :class:`.ArgumentParser`) and how the parser was built, some of the options above might not apply. Parsing of environment variables must be explicitly enabled, except if using :py:meth:`.ArgumentParser.parse_env`. If the parser does not have an -`action="config"` argument, then there is no parsing of a full config +``action="config"`` argument, then there is no parsing of a full config environment variable or a way to provide a config file from command line. @@ -755,9 +755,9 @@ If you import ``from jsonargparse import set_parsing_settings`` and then run ``set_parsing_settings(config_read_mode_fsspec_enabled=True)``, the following functions and classes will also support loading from URLs: :py:meth:`.ArgumentParser.parse_path`, :py:meth:`.ArgumentParser.get_defaults` -(``default_config_files`` argument), `action="config"`, +(``default_config_files`` argument), ``action="config"``, :class:`.ActionJsonSchema`, :class:`.ActionJsonnet` and :class:`.ActionParser`. -This means that a tool that can receive a config file via `action="config"` is +This means that a tool that can receive a config file via ``action="config"`` is able to get the content from a URL, thus something like the following would work: @@ -1338,7 +1338,7 @@ string respectively. Serialization ------------- -Parsers that have an `action="config"` argument also include a +Parsers that have an ``action="config"`` argument also include a ``--print_config`` option. This is useful particularly for command line tools with a large set of options to create an initial config file including all default values. If the `ruamel.yaml `__ @@ -2550,7 +2550,7 @@ There is also the :py:meth:`.ArgumentParser.parse_env` function to only parse environment variables, which might be useful for some use cases in which there is no command line call involved. -If a parser includes an `action="config"` argument, then the environment +If a parser includes an ``action="config"`` argument, then the environment variable for this config file will be parsed before all the other environment variables. @@ -2758,7 +2758,7 @@ following: When using the :class:`.ActionParser` class, the value of the node in a config file can be either the complex node itself, or the path to a file which will be loaded and parsed with the corresponding inner parser. Naturally using -`action="config"` to parse a complete config file will parse the inner +``action="config"`` to parse a complete config file will parse the inner nodes correctly. Note that when adding ``inner_parser`` a title was given. In the help, the added diff --git a/jsonargparse/_actions.py b/jsonargparse/_actions.py index 7eb8df47..24b02d9c 100644 --- a/jsonargparse/_actions.py +++ b/jsonargparse/_actions.py @@ -260,7 +260,7 @@ def __call__(self, parser, namespace, value, option_string=None): kwargs = {"subparser": parser, "key": None, "skip_none": False, "skip_validation": False} valid_flags = {"": None, "skip_default": "skip_default", "skip_null": "skip_none"} if ruamel_support: - valid_flags["comments"] = "yaml_comments" + valid_flags["comments"] = "with_comments" if value is not None: flags = value[0].split(",") invalid_flags = [f for f in flags if f not in valid_flags] diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index bedf2458..75b771ee 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -48,7 +48,7 @@ argcomplete_namespace, handle_completions, ) -from ._deprecated import ParserDeprecations, deprecated_skip_check +from ._deprecated import ParserDeprecations, deprecated_skip_check, deprecated_yaml_comments from ._formatters import DefaultHelpFormatter, empty_help, get_env_var from ._jsonnet import ActionJsonnet from ._jsonschema import ActionJsonSchema @@ -739,7 +739,7 @@ def dump( skip_none: bool = True, skip_default: bool = False, skip_validation: bool = False, - yaml_comments: bool = False, + with_comments: bool = False, skip_link_targets: bool = True, **kwargs, ) -> str: @@ -753,7 +753,7 @@ def dump( skip_none: Whether to exclude entries whose value is None. skip_default: Whether to exclude entries whose value is the same as the default. skip_validation: Whether to skip parser checking. - yaml_comments: Whether to add help content as comments. ``yaml_comments=True`` implies ``format='yaml'``. + with_comments: Whether to add help content as comments. Currently only supported for ``format='yaml'``. skip_link_targets: Whether to exclude link targets. Returns: @@ -762,7 +762,9 @@ def dump( Raises: TypeError: If any of the values of cfg is invalid according to the parser. """ + with_comments = deprecated_yaml_comments(kwargs, with_comments) skip_validation = deprecated_skip_check(ArgumentParser.dump, kwargs, skip_validation) + check_valid_dump_format(format) cfg = cfg.clone(with_meta=False) @@ -786,7 +788,7 @@ def dump( self._dump_delete_default_entries(cfg_dict, defaults.as_dict()) with parser_context(parent_parser=self): - return dump_using_format(self, cfg_dict, "yaml_comments" if yaml_comments else format) + return dump_using_format(self, cfg_dict, dump_format=format, with_comments=with_comments) def _dump_cleanup_actions(self, cfg, actions, dump_kwargs, prefix=""): skip_none = dump_kwargs["skip_none"] diff --git a/jsonargparse/_deprecated.py b/jsonargparse/_deprecated.py index 9e837dc2..5d15fd43 100644 --- a/jsonargparse/_deprecated.py +++ b/jsonargparse/_deprecated.py @@ -668,6 +668,21 @@ def deprecated_skip_check(component, kwargs: dict, skip_validation: bool) -> boo return skip_validation +def deprecated_yaml_comments(kwargs: dict, with_comments: bool) -> bool: + yaml_comments = kwargs.pop("yaml_comments", None) + if yaml_comments is not None: + deprecation_warning( + deprecated_yaml_comments, + ( + "yaml_comments parameter was deprecated in v4.44.0 and will be removed in " + "v5.0.0. Instead use with_comments." + ), + stacklevel=3, + ) + return yaml_comments + return with_comments + + ParserError = ArgumentError diff --git a/jsonargparse/_loaders_dumpers.py b/jsonargparse/_loaders_dumpers.py index 4620be42..5e8607e1 100644 --- a/jsonargparse/_loaders_dumpers.py +++ b/jsonargparse/_loaders_dumpers.py @@ -281,10 +281,16 @@ def check_valid_dump_format(dump_format: str): raise ValueError(f'Unknown output format "{dump_format}".') -def dump_using_format(parser: ArgumentParser, data: dict, dump_format: str) -> str: +def dump_using_format(parser: ArgumentParser, data: dict, dump_format: str, with_comments: bool = False) -> str: if dump_format == "parser_mode": dump_format = parser.parser_mode if parser.parser_mode in dumpers else "yaml" - args = (data, parser) if dump_format == "yaml_comments" else (data,) + if with_comments: + if f"{dump_format}_comments" not in dumpers: + if dump_format == "yaml": + raise ValueError("ruamel.yaml is required for dumping YAML with comments.") + raise ValueError(f"Dumping with comments is not supported for format '{dump_format}'.") + dump_format = f"{dump_format}_comments" + args = (data, parser) if dump_format.endswith("_comments") else (data,) dump = dumpers[dump_format](*args) if parser.dump_header and comment_prefix.get(dump_format): prefix = comment_prefix[dump_format] diff --git a/jsonargparse_tests/test_core.py b/jsonargparse_tests/test_core.py index 639ef471..bdd5d23e 100644 --- a/jsonargparse_tests/test_core.py +++ b/jsonargparse_tests/test_core.py @@ -566,6 +566,24 @@ def test_dump_order(parser, subtests): assert dump == "\n".join(v + ": " + str(n) for n, v in args.items()) + "\n" +def test_dump_comments_not_supported(parser): + parser.parser_mode = "json" + parser.add_argument("--op", type=int, default=1) + cfg = parser.get_defaults() + with pytest.raises(ValueError, match="Dumping with comments is not supported for format 'json'"): + parser.dump(cfg, with_comments=True) + + +@skip_if_no_pyyaml +def test_dump_comments_missing_ruamel(parser): + parser.add_argument("--op", type=int, default=1) + cfg = parser.get_defaults() + with patch.dict("jsonargparse._loaders_dumpers.dumpers") as dumpers: + dumpers.pop("yaml_comments", None) + with pytest.raises(ValueError, match="ruamel.yaml is required for dumping YAML with comments"): + parser.dump(cfg, with_comments=True) + + @pytest.fixture def parser_schema_jsonnet(parser, example_parser): parser.add_argument("--cfg", action="config") diff --git a/jsonargparse_tests/test_deprecated.py b/jsonargparse_tests/test_deprecated.py index 1184552e..6b9964e5 100644 --- a/jsonargparse_tests/test_deprecated.py +++ b/jsonargparse_tests/test_deprecated.py @@ -853,6 +853,19 @@ def test_DefaultHelpFormatter_yaml_comments(parser): assert "formatter.set_yaml_argument_comment(" in source[w[-1].lineno - 1] +@pytest.mark.skipif(not ruamel_support, reason="ruamel.yaml package is required") +def test_deprecated_dump_yaml_comments_parameter(parser): + parser.add_argument("--arg", type=int, default=1, help="Description") + cfg = parser.get_defaults() + with catch_warnings(record=True) as w: + parser.dump(cfg, yaml_comments=True) + assert_deprecation_warn( + w, + message="yaml_comments parameter was deprecated in v4.44.0 and will be removed in v5.0.0", + code="parser.dump(cfg, yaml_comments=True)", + ) + + @dataclasses.dataclass class ComposeA: a: int = 1