Skip to content

Commit 785af2c

Browse files
authored
Split YAMLCommentFormatter from the help formatter (#754)
1 parent 08eaf4d commit 785af2c

File tree

7 files changed

+257
-111
lines changed

7 files changed

+257
-111
lines changed

CHANGELOG.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ Changed
2424
^^^^^^^
2525
- Removed support for python 3.8 (`#752
2626
<https://github.com/omni-us/jsonargparse/pull/752>`__).
27+
- ``YAML`` comments feature is now implemented in a separate class to allow
28+
better support for custom help formatters without breaking the comments (`#754
29+
<https://github.com/omni-us/jsonargparse/pull/754>`__).
30+
31+
Deprecated
32+
^^^^^^^^^^
33+
- ``DefaultHelpFormatter.*_yaml*_comment*`` methods are deprecated and will be
34+
removed in v5.0.0. This logic has been moved to a new private class
35+
``YAMLCommentFormatter``. If deemed necessary, this class might be made public
36+
in the future (`#754 <https://github.com/omni-us/jsonargparse/pull/754>`__).
2737

2838

2939
v4.40.2 (2025-08-06)

jsonargparse/_actions.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ._common import Action, is_subclass, parser_context
1313
from ._loaders_dumpers import get_loader_exceptions, load_value
1414
from ._namespace import Namespace, NSKeyError, split_key, split_key_root
15-
from ._optionals import _get_config_read_mode
15+
from ._optionals import _get_config_read_mode, ruyaml_support
1616
from ._type_checking import ActionsContainer, ArgumentParser
1717
from ._util import (
1818
Path,
@@ -251,13 +251,16 @@ def __init__(
251251
help=(
252252
"Print the configuration after applying all other arguments and exit. The optional "
253253
"flags customizes the output and are one or more keywords separated by comma. The "
254-
"supported flags are: comments, skip_default, skip_null."
255-
),
254+
"supported flags are:%s skip_default, skip_null."
255+
)
256+
% (" comments," if ruyaml_support else ""),
256257
)
257258

258259
def __call__(self, parser, namespace, value, option_string=None):
259260
kwargs = {"subparser": parser, "key": None, "skip_none": False, "skip_validation": False}
260-
valid_flags = {"": None, "comments": "yaml_comments", "skip_default": "skip_default", "skip_null": "skip_none"}
261+
valid_flags = {"": None, "skip_default": "skip_default", "skip_null": "skip_none"}
262+
if ruyaml_support:
263+
valid_flags["comments"] = "yaml_comments"
261264
if value is not None:
262265
flags = value[0].split(",")
263266
invalid_flags = [f for f in flags if f not in valid_flags]

jsonargparse/_deprecated.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ._common import Action, null_logger
1515
from ._common import LoggerProperty as InternalLoggerProperty
1616
from ._namespace import Namespace
17-
from ._type_checking import ArgumentParser
17+
from ._type_checking import ArgumentParser, ruyamlCommentedMap
1818

1919
__all__ = [
2020
"ActionEnum",
@@ -701,3 +701,50 @@ class LoggerProperty(InternalLoggerProperty):
701701
def namespace_to_dict(namespace: Namespace) -> Dict[str, Any]:
702702
"""Returns a copy of a nested namespace converted into a nested dictionary."""
703703
return namespace.clone().as_dict()
704+
705+
706+
class HelpFormatterDeprecations:
707+
def __init__(self, *args, **kwargs):
708+
from jsonargparse._formatters import YAMLCommentFormatter
709+
710+
super().__init__(*args, **kwargs)
711+
self._yaml_formatter = YAMLCommentFormatter(self)
712+
713+
@deprecated("The add_yaml_comments method is deprecated and will be removed in v5.0.0.")
714+
def add_yaml_comments(self, cfg: str) -> str:
715+
"""Adds help text as yaml comments."""
716+
return self._yaml_formatter.add_yaml_comments(cfg)
717+
718+
@deprecated("The set_yaml_start_comment method is deprecated and will be removed in v5.0.0.")
719+
def set_yaml_start_comment(self, text: str, cfg: ruyamlCommentedMap):
720+
"""Sets the start comment to a ruyaml object.
721+
722+
Args:
723+
text: The content to use for the comment.
724+
cfg: The ruyaml object.
725+
"""
726+
self._yaml_formatter.set_yaml_start_comment(text, cfg)
727+
728+
@deprecated("The set_yaml_group_comment method is deprecated and will be removed in v5.0.0.")
729+
def set_yaml_group_comment(self, text: str, cfg: ruyamlCommentedMap, key: str, depth: int):
730+
"""Sets the comment for a group to a ruyaml object.
731+
732+
Args:
733+
text: The content to use for the comment.
734+
cfg: The parent ruyaml object.
735+
key: The key of the group.
736+
depth: The nested level of the group.
737+
"""
738+
self._yaml_formatter.set_yaml_group_comment(text, cfg, key, depth)
739+
740+
@deprecated("The set_yaml_argument_comment method is deprecated and will be removed in v5.0.0.")
741+
def set_yaml_argument_comment(self, text: str, cfg: ruyamlCommentedMap, key: str, depth: int):
742+
"""Sets the comment for an argument to a ruyaml object.
743+
744+
Args:
745+
text: The content to use for the comment.
746+
cfg: The parent ruyaml object.
747+
key: The key of the argument.
748+
depth: The nested level of the argument.
749+
"""
750+
self._yaml_formatter.set_yaml_argument_comment(text, cfg, key, depth)

jsonargparse/_formatters.py

Lines changed: 111 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
supports_optionals_as_positionals,
3131
)
3232
from ._completions import ShtabAction
33+
from ._deprecated import HelpFormatterDeprecations
3334
from ._link_arguments import ActionLink
3435
from ._namespace import Namespace, NSKeyError
3536
from ._optionals import import_ruyaml
@@ -54,7 +55,116 @@ class PercentTemplate(Template):
5455
""" # type: ignore[assignment]
5556

5657

57-
class DefaultHelpFormatter(HelpFormatter):
58+
class YAMLCommentFormatter:
59+
"""Formatter class for adding YAML comments to configuration files."""
60+
61+
def __init__(self, help_formatter: HelpFormatter):
62+
self.help_formatter = help_formatter
63+
64+
def add_yaml_comments(self, cfg: str) -> str:
65+
"""Adds help text as yaml comments."""
66+
ruyaml = import_ruyaml("add_yaml_comments")
67+
yaml = ruyaml.YAML()
68+
cfg = yaml.load(cfg)
69+
70+
def get_subparsers(parser, prefix=""):
71+
subparsers = {}
72+
if parser._subparsers is not None:
73+
for key, subparser in parser._subparsers._group_actions[0].choices.items():
74+
full_key = (prefix + "." if prefix else "") + key
75+
subparsers[full_key] = subparser
76+
subparsers.update(get_subparsers(subparser, prefix=full_key))
77+
return subparsers
78+
79+
parser = parent_parser.get()
80+
parsers = get_subparsers(parser)
81+
parsers[None] = parser
82+
83+
group_titles = {}
84+
for parser_key, parser in parsers.items():
85+
group_titles[parser_key] = parser.description
86+
prefix = "" if parser_key is None else parser_key + "."
87+
for group in parser._action_groups:
88+
actions = filter_default_actions(group._group_actions)
89+
actions = [
90+
a for a in actions if not isinstance(a, (_ActionConfigLoad, ActionConfigFile, _ActionSubCommands))
91+
]
92+
keys = {re.sub(r"\.?[^.]+$", "", a.dest) for a in actions if "." in a.dest}
93+
for key in keys:
94+
group_titles[prefix + key] = group.title
95+
96+
def set_comments(cfg, prefix="", depth=0):
97+
for key in cfg.keys():
98+
full_key = (prefix + "." if prefix else "") + key
99+
action = _find_action(parser, full_key)
100+
text = None
101+
if full_key in group_titles and isinstance(cfg[key], dict):
102+
text = group_titles[full_key]
103+
elif action is not None and action.help not in {None, SUPPRESS}:
104+
text = self.help_formatter._expand_help(action)
105+
if isinstance(cfg[key], dict):
106+
if text:
107+
self.set_yaml_group_comment(text, cfg, key, depth)
108+
set_comments(cfg[key], full_key, depth + 1)
109+
elif text:
110+
self.set_yaml_argument_comment(text, cfg, key, depth)
111+
112+
if parser.description is not None:
113+
self.set_yaml_start_comment(parser.description, cfg)
114+
set_comments(cfg)
115+
out = StringIO()
116+
yaml.dump(cfg, out)
117+
return out.getvalue()
118+
119+
def set_yaml_start_comment(
120+
self,
121+
text: str,
122+
cfg: ruyamlCommentedMap,
123+
):
124+
"""Sets the start comment to a ruyaml object.
125+
126+
Args:
127+
text: The content to use for the comment.
128+
cfg: The ruyaml object.
129+
"""
130+
cfg.yaml_set_start_comment(text)
131+
132+
def set_yaml_group_comment(
133+
self,
134+
text: str,
135+
cfg: ruyamlCommentedMap,
136+
key: str,
137+
depth: int,
138+
):
139+
"""Sets the comment for a group to a ruyaml object.
140+
141+
Args:
142+
text: The content to use for the comment.
143+
cfg: The parent ruyaml object.
144+
key: The key of the group.
145+
depth: The nested level of the group.
146+
"""
147+
cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth)
148+
149+
def set_yaml_argument_comment(
150+
self,
151+
text: str,
152+
cfg: ruyamlCommentedMap,
153+
key: str,
154+
depth: int,
155+
):
156+
"""Sets the comment for an argument to a ruyaml object.
157+
158+
Args:
159+
text: The content to use for the comment.
160+
cfg: The parent ruyaml object.
161+
key: The key of the argument.
162+
depth: The nested level of the argument.
163+
"""
164+
cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth)
165+
166+
167+
class DefaultHelpFormatter(HelpFormatterDeprecations, HelpFormatter):
58168
"""Help message formatter that includes types, default values and env var names.
59169
60170
This class is an extension of `argparse.HelpFormatter
@@ -184,108 +294,6 @@ def add_usage(self, usage: Optional[str], actions: Iterable[Action], *args, **kw
184294
actions = [a for a in actions if not isinstance(a, ActionLink)]
185295
super().add_usage(usage, actions, *args, **kwargs)
186296

187-
def add_yaml_comments(self, cfg: str) -> str:
188-
"""Adds help text as yaml comments."""
189-
ruyaml = import_ruyaml("add_yaml_comments")
190-
yaml = ruyaml.YAML()
191-
cfg = yaml.load(cfg)
192-
193-
def get_subparsers(parser, prefix=""):
194-
subparsers = {}
195-
if parser._subparsers is not None:
196-
for key, subparser in parser._subparsers._group_actions[0].choices.items():
197-
full_key = (prefix + "." if prefix else "") + key
198-
subparsers[full_key] = subparser
199-
subparsers.update(get_subparsers(subparser, prefix=full_key))
200-
return subparsers
201-
202-
parser = parent_parser.get()
203-
parsers = get_subparsers(parser)
204-
parsers[None] = parser
205-
206-
group_titles = {}
207-
for parser_key, parser in parsers.items():
208-
group_titles[parser_key] = parser.description
209-
prefix = "" if parser_key is None else parser_key + "."
210-
for group in parser._action_groups:
211-
actions = filter_default_actions(group._group_actions)
212-
actions = [
213-
a for a in actions if not isinstance(a, (_ActionConfigLoad, ActionConfigFile, _ActionSubCommands))
214-
]
215-
keys = {re.sub(r"\.?[^.]+$", "", a.dest) for a in actions if "." in a.dest}
216-
for key in keys:
217-
group_titles[prefix + key] = group.title
218-
219-
def set_comments(cfg, prefix="", depth=0):
220-
for key in cfg.keys():
221-
full_key = (prefix + "." if prefix else "") + key
222-
action = _find_action(parser, full_key)
223-
text = None
224-
if full_key in group_titles and isinstance(cfg[key], dict):
225-
text = group_titles[full_key]
226-
elif action is not None and action.help not in {None, SUPPRESS}:
227-
text = self._expand_help(action)
228-
if isinstance(cfg[key], dict):
229-
if text:
230-
self.set_yaml_group_comment(text, cfg, key, depth)
231-
set_comments(cfg[key], full_key, depth + 1)
232-
elif text:
233-
self.set_yaml_argument_comment(text, cfg, key, depth)
234-
235-
if parser.description is not None:
236-
self.set_yaml_start_comment(parser.description, cfg)
237-
set_comments(cfg)
238-
out = StringIO()
239-
yaml.dump(cfg, out)
240-
return out.getvalue()
241-
242-
def set_yaml_start_comment(
243-
self,
244-
text: str,
245-
cfg: ruyamlCommentedMap,
246-
):
247-
"""Sets the start comment to a ruyaml object.
248-
249-
Args:
250-
text: The content to use for the comment.
251-
cfg: The ruyaml object.
252-
"""
253-
cfg.yaml_set_start_comment(text)
254-
255-
def set_yaml_group_comment(
256-
self,
257-
text: str,
258-
cfg: ruyamlCommentedMap,
259-
key: str,
260-
depth: int,
261-
):
262-
"""Sets the comment for a group to a ruyaml object.
263-
264-
Args:
265-
text: The content to use for the comment.
266-
cfg: The parent ruyaml object.
267-
key: The key of the group.
268-
depth: The nested level of the group.
269-
"""
270-
cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth)
271-
272-
def set_yaml_argument_comment(
273-
self,
274-
text: str,
275-
cfg: ruyamlCommentedMap,
276-
key: str,
277-
depth: int,
278-
):
279-
"""Sets the comment for an argument to a ruyaml object.
280-
281-
Args:
282-
text: The content to use for the comment.
283-
cfg: The parent ruyaml object.
284-
key: The key of the argument.
285-
depth: The nested level of the argument.
286-
"""
287-
cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth)
288-
289297

290298
def get_env_var(
291299
parser_or_formatter: Union[ArgumentParser, DefaultHelpFormatter],

0 commit comments

Comments
 (0)