Skip to content

Split YAMLCommentFormatter from the help formatter #754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ Changed
^^^^^^^
- Removed support for python 3.8 (`#752
<https://github.com/omni-us/jsonargparse/pull/752>`__).
- ``YAML`` comments feature is now implemented in a separate class to allow
better support for custom help formatters without breaking the comments (`#754
<https://github.com/omni-us/jsonargparse/pull/754>`__).

Deprecated
^^^^^^^^^^
- ``DefaultHelpFormatter.*_yaml*_comment*`` methods are deprecated and will be
removed in v5.0.0. This logic has been moved to a new private class
``YAMLCommentFormatter``. If deemed necessary, this class might be made public
in the future (`#754 <https://github.com/omni-us/jsonargparse/pull/754>`__).


v4.40.2 (2025-08-06)
Expand Down
11 changes: 7 additions & 4 deletions jsonargparse/_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ._common import Action, is_subclass, parser_context
from ._loaders_dumpers import get_loader_exceptions, load_value
from ._namespace import Namespace, NSKeyError, split_key, split_key_root
from ._optionals import _get_config_read_mode
from ._optionals import _get_config_read_mode, ruyaml_support
from ._type_checking import ActionsContainer, ArgumentParser
from ._util import (
Path,
Expand Down Expand Up @@ -251,13 +251,16 @@ def __init__(
help=(
"Print the configuration after applying all other arguments and exit. The optional "
"flags customizes the output and are one or more keywords separated by comma. The "
"supported flags are: comments, skip_default, skip_null."
),
"supported flags are:%s skip_default, skip_null."
)
% (" comments," if ruyaml_support else ""),
)

def __call__(self, parser, namespace, value, option_string=None):
kwargs = {"subparser": parser, "key": None, "skip_none": False, "skip_validation": False}
valid_flags = {"": None, "comments": "yaml_comments", "skip_default": "skip_default", "skip_null": "skip_none"}
valid_flags = {"": None, "skip_default": "skip_default", "skip_null": "skip_none"}
if ruyaml_support:
valid_flags["comments"] = "yaml_comments"
if value is not None:
flags = value[0].split(",")
invalid_flags = [f for f in flags if f not in valid_flags]
Expand Down
49 changes: 48 additions & 1 deletion jsonargparse/_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ._common import Action, null_logger
from ._common import LoggerProperty as InternalLoggerProperty
from ._namespace import Namespace
from ._type_checking import ArgumentParser
from ._type_checking import ArgumentParser, ruyamlCommentedMap

__all__ = [
"ActionEnum",
Expand Down Expand Up @@ -701,3 +701,50 @@ class LoggerProperty(InternalLoggerProperty):
def namespace_to_dict(namespace: Namespace) -> Dict[str, Any]:
"""Returns a copy of a nested namespace converted into a nested dictionary."""
return namespace.clone().as_dict()


class HelpFormatterDeprecations:
def __init__(self, *args, **kwargs):
from jsonargparse._formatters import YAMLCommentFormatter

super().__init__(*args, **kwargs)
self._yaml_formatter = YAMLCommentFormatter(self)

@deprecated("The add_yaml_comments method is deprecated and will be removed in v5.0.0.")
def add_yaml_comments(self, cfg: str) -> str:
"""Adds help text as yaml comments."""
return self._yaml_formatter.add_yaml_comments(cfg)

@deprecated("The set_yaml_start_comment method is deprecated and will be removed in v5.0.0.")
def set_yaml_start_comment(self, text: str, cfg: ruyamlCommentedMap):
"""Sets the start comment to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The ruyaml object.
"""
self._yaml_formatter.set_yaml_start_comment(text, cfg)

@deprecated("The set_yaml_group_comment method is deprecated and will be removed in v5.0.0.")
def set_yaml_group_comment(self, text: str, cfg: ruyamlCommentedMap, key: str, depth: int):
"""Sets the comment for a group to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The parent ruyaml object.
key: The key of the group.
depth: The nested level of the group.
"""
self._yaml_formatter.set_yaml_group_comment(text, cfg, key, depth)

@deprecated("The set_yaml_argument_comment method is deprecated and will be removed in v5.0.0.")
def set_yaml_argument_comment(self, text: str, cfg: ruyamlCommentedMap, key: str, depth: int):
"""Sets the comment for an argument to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The parent ruyaml object.
key: The key of the argument.
depth: The nested level of the argument.
"""
self._yaml_formatter.set_yaml_argument_comment(text, cfg, key, depth)
214 changes: 111 additions & 103 deletions jsonargparse/_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
supports_optionals_as_positionals,
)
from ._completions import ShtabAction
from ._deprecated import HelpFormatterDeprecations
from ._link_arguments import ActionLink
from ._namespace import Namespace, NSKeyError
from ._optionals import import_ruyaml
Expand All @@ -54,7 +55,116 @@ class PercentTemplate(Template):
""" # type: ignore[assignment]


class DefaultHelpFormatter(HelpFormatter):
class YAMLCommentFormatter:
"""Formatter class for adding YAML comments to configuration files."""

def __init__(self, help_formatter: HelpFormatter):
self.help_formatter = help_formatter

def add_yaml_comments(self, cfg: str) -> str:
"""Adds help text as yaml comments."""
ruyaml = import_ruyaml("add_yaml_comments")
yaml = ruyaml.YAML()
cfg = yaml.load(cfg)

def get_subparsers(parser, prefix=""):
subparsers = {}
if parser._subparsers is not None:
for key, subparser in parser._subparsers._group_actions[0].choices.items():
full_key = (prefix + "." if prefix else "") + key
subparsers[full_key] = subparser
subparsers.update(get_subparsers(subparser, prefix=full_key))
return subparsers

parser = parent_parser.get()
parsers = get_subparsers(parser)
parsers[None] = parser

group_titles = {}
for parser_key, parser in parsers.items():
group_titles[parser_key] = parser.description
prefix = "" if parser_key is None else parser_key + "."
for group in parser._action_groups:
actions = filter_default_actions(group._group_actions)
actions = [
a for a in actions if not isinstance(a, (_ActionConfigLoad, ActionConfigFile, _ActionSubCommands))
]
keys = {re.sub(r"\.?[^.]+$", "", a.dest) for a in actions if "." in a.dest}
for key in keys:
group_titles[prefix + key] = group.title

def set_comments(cfg, prefix="", depth=0):
for key in cfg.keys():
full_key = (prefix + "." if prefix else "") + key
action = _find_action(parser, full_key)
text = None
if full_key in group_titles and isinstance(cfg[key], dict):
text = group_titles[full_key]
elif action is not None and action.help not in {None, SUPPRESS}:
text = self.help_formatter._expand_help(action)
if isinstance(cfg[key], dict):
if text:
self.set_yaml_group_comment(text, cfg, key, depth)
set_comments(cfg[key], full_key, depth + 1)
elif text:
self.set_yaml_argument_comment(text, cfg, key, depth)

if parser.description is not None:
self.set_yaml_start_comment(parser.description, cfg)
set_comments(cfg)
out = StringIO()
yaml.dump(cfg, out)
return out.getvalue()

def set_yaml_start_comment(
self,
text: str,
cfg: ruyamlCommentedMap,
):
"""Sets the start comment to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The ruyaml object.
"""
cfg.yaml_set_start_comment(text)

def set_yaml_group_comment(
self,
text: str,
cfg: ruyamlCommentedMap,
key: str,
depth: int,
):
"""Sets the comment for a group to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The parent ruyaml object.
key: The key of the group.
depth: The nested level of the group.
"""
cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth)

def set_yaml_argument_comment(
self,
text: str,
cfg: ruyamlCommentedMap,
key: str,
depth: int,
):
"""Sets the comment for an argument to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The parent ruyaml object.
key: The key of the argument.
depth: The nested level of the argument.
"""
cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth)


class DefaultHelpFormatter(HelpFormatterDeprecations, HelpFormatter):
"""Help message formatter that includes types, default values and env var names.

This class is an extension of `argparse.HelpFormatter
Expand Down Expand Up @@ -184,108 +294,6 @@ def add_usage(self, usage: Optional[str], actions: Iterable[Action], *args, **kw
actions = [a for a in actions if not isinstance(a, ActionLink)]
super().add_usage(usage, actions, *args, **kwargs)

def add_yaml_comments(self, cfg: str) -> str:
"""Adds help text as yaml comments."""
ruyaml = import_ruyaml("add_yaml_comments")
yaml = ruyaml.YAML()
cfg = yaml.load(cfg)

def get_subparsers(parser, prefix=""):
subparsers = {}
if parser._subparsers is not None:
for key, subparser in parser._subparsers._group_actions[0].choices.items():
full_key = (prefix + "." if prefix else "") + key
subparsers[full_key] = subparser
subparsers.update(get_subparsers(subparser, prefix=full_key))
return subparsers

parser = parent_parser.get()
parsers = get_subparsers(parser)
parsers[None] = parser

group_titles = {}
for parser_key, parser in parsers.items():
group_titles[parser_key] = parser.description
prefix = "" if parser_key is None else parser_key + "."
for group in parser._action_groups:
actions = filter_default_actions(group._group_actions)
actions = [
a for a in actions if not isinstance(a, (_ActionConfigLoad, ActionConfigFile, _ActionSubCommands))
]
keys = {re.sub(r"\.?[^.]+$", "", a.dest) for a in actions if "." in a.dest}
for key in keys:
group_titles[prefix + key] = group.title

def set_comments(cfg, prefix="", depth=0):
for key in cfg.keys():
full_key = (prefix + "." if prefix else "") + key
action = _find_action(parser, full_key)
text = None
if full_key in group_titles and isinstance(cfg[key], dict):
text = group_titles[full_key]
elif action is not None and action.help not in {None, SUPPRESS}:
text = self._expand_help(action)
if isinstance(cfg[key], dict):
if text:
self.set_yaml_group_comment(text, cfg, key, depth)
set_comments(cfg[key], full_key, depth + 1)
elif text:
self.set_yaml_argument_comment(text, cfg, key, depth)

if parser.description is not None:
self.set_yaml_start_comment(parser.description, cfg)
set_comments(cfg)
out = StringIO()
yaml.dump(cfg, out)
return out.getvalue()

def set_yaml_start_comment(
self,
text: str,
cfg: ruyamlCommentedMap,
):
"""Sets the start comment to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The ruyaml object.
"""
cfg.yaml_set_start_comment(text)

def set_yaml_group_comment(
self,
text: str,
cfg: ruyamlCommentedMap,
key: str,
depth: int,
):
"""Sets the comment for a group to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The parent ruyaml object.
key: The key of the group.
depth: The nested level of the group.
"""
cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth)

def set_yaml_argument_comment(
self,
text: str,
cfg: ruyamlCommentedMap,
key: str,
depth: int,
):
"""Sets the comment for an argument to a ruyaml object.

Args:
text: The content to use for the comment.
cfg: The parent ruyaml object.
key: The key of the argument.
depth: The nested level of the argument.
"""
cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth)


def get_env_var(
parser_or_formatter: Union[ArgumentParser, DefaultHelpFormatter],
Expand Down
Loading
Loading