Skip to content

Commit f5531bd

Browse files
committed
Split YAMLCommentFormatter from the help formatter.
1 parent 1be4976 commit f5531bd

3 files changed

Lines changed: 245 additions & 104 deletions

File tree

jsonargparse/_formatters.py

Lines changed: 174 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@
3030
supports_optionals_as_positionals,
3131
)
3232
from ._completions import ShtabAction
33+
from ._deprecated import deprecated
3334
from ._link_arguments import ActionLink
3435
from ._namespace import Namespace, NSKeyError
3536
from ._optionals import import_ruyaml
3637
from ._type_checking import ArgumentParser, ruyamlCommentedMap
3738
from ._typehints import ActionTypeHint, type_to_str
3839

39-
__all__ = ["DefaultHelpFormatter"]
40+
__all__ = ["DefaultHelpFormatter", "YAMLCommentFormatter"]
4041

4142

4243
empty_help: str = "_EMPTY_HELP_"
@@ -54,6 +55,115 @@ class PercentTemplate(Template):
5455
""" # type: ignore[assignment]
5556

5657

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+
57167
class DefaultHelpFormatter(HelpFormatter):
58168
"""Help message formatter that includes types, default values and env var names.
59169
@@ -64,6 +174,69 @@ class DefaultHelpFormatter(HelpFormatter):
64174
the respective environment variable name is included preceded by 'ENV:'.
65175
"""
66176

177+
def __init__(self, *args, **kwargs):
178+
super().__init__(*args, **kwargs)
179+
self._yaml_formatter = YAMLCommentFormatter(self)
180+
181+
@deprecated(
182+
"""
183+
The add_yaml_comments method is deprecated and will be removed in a future version.
184+
Use :class:`YAMLCommentFormatter` instead.
185+
"""
186+
)
187+
def add_yaml_comments(self, cfg: str) -> str:
188+
"""Adds help text as yaml comments."""
189+
return self._yaml_formatter.add_yaml_comments(cfg)
190+
191+
@deprecated(
192+
"""
193+
The set_yaml_start_comment method is deprecated and will be removed in a future version.
194+
Use :class:`YAMLCommentFormatter` instead.
195+
"""
196+
)
197+
def set_yaml_start_comment(self, text: str, cfg: "ruyamlCommentedMap"):
198+
"""Sets the start comment to a ruyaml object.
199+
200+
Args:
201+
text: The content to use for the comment.
202+
cfg: The ruyaml object.
203+
"""
204+
self._yaml_formatter.set_yaml_start_comment(text, cfg)
205+
206+
@deprecated(
207+
"""
208+
The set_yaml_group_comment method is deprecated and will be removed in a future version.
209+
Use :class:`YAMLCommentFormatter` instead.
210+
"""
211+
)
212+
def set_yaml_group_comment(self, text: str, cfg: "ruyamlCommentedMap", key: str, depth: int):
213+
"""Sets the comment for a group to a ruyaml object.
214+
215+
Args:
216+
text: The content to use for the comment.
217+
cfg: The parent ruyaml object.
218+
key: The key of the group.
219+
depth: The nested level of the group.
220+
"""
221+
self._yaml_formatter.set_yaml_group_comment(text, cfg, key, depth)
222+
223+
@deprecated(
224+
"""
225+
The set_yaml_argument_comment method is deprecated and will be removed in a future version.
226+
Use :class:`YAMLCommentFormatter` instead.
227+
"""
228+
)
229+
def set_yaml_argument_comment(self, text: str, cfg: "ruyamlCommentedMap", key: str, depth: int):
230+
"""Sets the comment for an argument to a ruyaml object.
231+
232+
Args:
233+
text: The content to use for the comment.
234+
cfg: The parent ruyaml object.
235+
key: The key of the argument.
236+
depth: The nested level of the argument.
237+
"""
238+
self._yaml_formatter.set_yaml_argument_comment(text, cfg, key, depth)
239+
67240
def _get_help_string(self, action: Action) -> str:
68241
action_help = " " if action.help == empty_help else action.help
69242
assert isinstance(action_help, str)
@@ -184,108 +357,6 @@ def add_usage(self, usage: Optional[str], actions: Iterable[Action], *args, **kw
184357
actions = [a for a in actions if not isinstance(a, ActionLink)]
185358
super().add_usage(usage, actions, *args, **kwargs)
186359

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-
289360

290361
def get_env_var(
291362
parser_or_formatter: Union[ArgumentParser, DefaultHelpFormatter],

jsonargparse/_loaders_dumpers.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ def yaml_dump(data):
225225

226226
def yaml_comments_dump(data, parser):
227227
dump = dumpers["yaml"](data)
228-
formatter = parser.formatter_class(parser.prog)
228+
formatter_class = create_help_formatter_with_comments(parser.formatter_class)
229+
formatter = formatter_class(parser.prog)
229230
return formatter.add_yaml_comments(dump)
230231

231232

@@ -333,3 +334,26 @@ def set_omegaconf_loader():
333334

334335

335336
set_loader("jsonnet", jsonnet_load, get_loader_exceptions("jsonnet"))
337+
338+
339+
def create_help_formatter_with_comments(formatter_class: Type["HelpFormatter"]) -> Type["HelpFormatter"]:
340+
"""Creates a dynamic class that combines a formatter with YAML comment functionality.
341+
342+
Args:
343+
formatter_class: The base formatter class to extend.
344+
345+
Returns:
346+
A new class that inherits from both the formatter and YAMLCommentFormatter.
347+
"""
348+
from ._formatters import YAMLCommentFormatter
349+
350+
class DynamicHelpFormatter(formatter_class):
351+
def __init__(self, *args, **kwargs):
352+
super().__init__(*args, **kwargs)
353+
self._yaml_formatter = YAMLCommentFormatter(self)
354+
355+
def add_yaml_comments(self, cfg: str) -> str:
356+
"""Adds help text as yaml comments."""
357+
return self._yaml_formatter.add_yaml_comments(cfg)
358+
359+
return DynamicHelpFormatter

0 commit comments

Comments
 (0)