Skip to content
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
237 changes: 122 additions & 115 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,27 +337,17 @@ def _format_usage(self, usage, actions, groups, prefix):
elif usage is None:
prog = '%(prog)s' % dict(prog=self._prog)

# split optionals from positionals
optionals = []
positionals = []
for action in actions:
if action.option_strings:
optionals.append(action)
else:
positionals.append(action)

parts, pos_start = self._get_actions_usage_parts(actions, groups)
# build full usage string
format = self._format_actions_usage
action_usage = format(optionals + positionals, groups)
usage = ' '.join([s for s in [prog, action_usage] if s])
usage = ' '.join(filter(None, [prog, *parts]))

# wrap the usage parts if it's too long
text_width = self._width - self._current_indent
if len(prefix) + len(self._decolor(usage)) > text_width:

# break usage into wrappable parts
opt_parts = self._get_actions_usage_parts(optionals, groups)
pos_parts = self._get_actions_usage_parts(positionals, groups)
opt_parts = parts[:pos_start]
pos_parts = parts[pos_start:]

# helper for wrapping lines
def get_lines(parts, indent, prefix=None):
Expand Down Expand Up @@ -414,110 +404,114 @@ def get_lines(parts, indent, prefix=None):
# prefix with 'usage:'
return f'{t.usage}{prefix}{t.reset}{usage}\n\n'

def _format_actions_usage(self, actions, groups):
return ' '.join(self._get_actions_usage_parts(actions, groups))

def _is_long_option(self, string):
return len(string) > 2

def _get_actions_usage_parts(self, actions, groups):
# find group indices and identify actions in groups
group_actions = set()
inserts = {}
for group in groups:
if not group._group_actions:
raise ValueError(f'empty group {group}')
"""Get usage parts with split index for optionals/positionals.
if all(action.help is SUPPRESS for action in group._group_actions):
continue

try:
start = min(actions.index(item) for item in group._group_actions)
except ValueError:
continue
else:
end = start + len(group._group_actions)
if set(actions[start:end]) == set(group._group_actions):
group_actions.update(group._group_actions)
inserts[start, end] = group
Returns (parts, pos_start) where pos_start is the index in parts
where positionals begin.
This preserves mutually exclusive group formatting across the
optionals/positionals boundary (gh-75949).
"""
actions = [action for action in actions if action.help is not SUPPRESS]
# group actions by mutually exclusive groups
action_groups = dict.fromkeys(actions)
for group in groups:
for action in group._group_actions:
if action in action_groups:
action_groups[action] = group
# positional arguments keep their position
positionals = []
for action in actions:
if not action.option_strings:
group = action_groups.pop(action)
if group:
group_actions = [
action2 for action2 in group._group_actions
if action2.option_strings and
action_groups.pop(action2, None)
] + [action]
positionals.append((group.required, group_actions))
else:
positionals.append((None, [action]))
# the remaining optional arguments are sorted by the position of
# the first option in the group
optionals = []
for action in actions:
if action.option_strings and action in action_groups:
group = action_groups.pop(action)
if group:
group_actions = [action] + [
action2 for action2 in group._group_actions
if action2.option_strings and
action_groups.pop(action2, None)
]
optionals.append((group.required, group_actions))
else:
optionals.append((None, [action]))

# collect all actions format strings
parts = []
t = self._theme
for action in actions:

# suppressed arguments are marked with None
if action.help is SUPPRESS:
part = None

# produce all arg strings
elif not action.option_strings:
default = self._get_default_metavar_for_positional(action)
part = (
t.summary_action
+ self._format_args(action, default)
+ t.reset
)

# if it's in a group, strip the outer []
if action in group_actions:
if part[0] == '[' and part[-1] == ']':
part = part[1:-1]

# produce the first way to invoke the option in brackets
else:
option_string = action.option_strings[0]
if self._is_long_option(option_string):
option_color = t.summary_long_option
pos_start = None
for i, (required, group) in enumerate(optionals + positionals):
start = len(parts)
if i == len(optionals):
pos_start = start
in_group = len(group) > 1
for action in group:
# produce all arg strings
if not action.option_strings:
default = self._get_default_metavar_for_positional(action)
part = self._format_args(action, default)
# if it's in a group, strip the outer []
if in_group:
if part[0] == '[' and part[-1] == ']':
part = part[1:-1]
part = t.summary_action + part + t.reset

# produce the first way to invoke the option in brackets
else:
option_color = t.summary_short_option
option_string = action.option_strings[0]
if self._is_long_option(option_string):
option_color = t.summary_long_option
else:
option_color = t.summary_short_option

# if the Optional doesn't take a value, format is:
# -s or --long
if action.nargs == 0:
part = action.format_usage()
part = f"{option_color}{part}{t.reset}"
# if the Optional doesn't take a value, format is:
# -s or --long
if action.nargs == 0:
part = action.format_usage()
part = f"{option_color}{part}{t.reset}"

# if the Optional takes a value, format is:
# -s ARGS or --long ARGS
else:
default = self._get_default_metavar_for_optional(action)
args_string = self._format_args(action, default)
part = (
f"{option_color}{option_string} "
f"{t.summary_label}{args_string}{t.reset}"
)

# make it look optional if it's not required or in a group
if not action.required and action not in group_actions:
part = '[%s]' % part

# add the action string to the list
parts.append(part)

# group mutually exclusive actions
inserted_separators_indices = set()
for start, end in sorted(inserts, reverse=True):
group = inserts[start, end]
group_parts = [item for item in parts[start:end] if item is not None]
group_size = len(group_parts)
if group.required:
open, close = "()" if group_size > 1 else ("", "")
else:
open, close = "[]"
group_parts[0] = open + group_parts[0]
group_parts[-1] = group_parts[-1] + close
for i, part in enumerate(group_parts[:-1], start=start):
# insert a separator if not already done in a nested group
if i not in inserted_separators_indices:
parts[i] = part + ' |'
inserted_separators_indices.add(i)
parts[start + group_size - 1] = group_parts[-1]
for i in range(start + group_size, end):
parts[i] = None

# return the usage parts
return [item for item in parts if item is not None]
# if the Optional takes a value, format is:
# -s ARGS or --long ARGS
else:
default = self._get_default_metavar_for_optional(action)
args_string = self._format_args(action, default)
part = (
f"{option_color}{option_string} "
f"{t.summary_label}{args_string}{t.reset}"
)

# make it look optional if it's not required or in a group
if not (action.required or required or in_group):
part = '[%s]' % part

# add the action string to the list
parts.append(part)

if in_group:
parts[start] = ('(' if required else '[') + parts[start]
for i in range(start, len(parts) - 1):
parts[i] += ' |'
parts[-1] += ')' if required else ']'

if pos_start is None:
pos_start = len(parts)
return parts, pos_start

def _format_text(self, text):
if '%(prog)' in text:
Expand Down Expand Up @@ -745,11 +739,14 @@ def _get_help_string(self, action):
if help is None:
help = ''

if '%(default)' not in help:
if action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs:
help += _(' (default: %(default)s)')
if (
'%(default)' not in help
and action.default is not SUPPRESS
and not action.required
):
defaulting_nargs = (OPTIONAL, ZERO_OR_MORE)
if action.option_strings or action.nargs in defaulting_nargs:
help += _(' (default: %(default)s)')
return help


Expand Down Expand Up @@ -1552,8 +1549,8 @@ def add_argument(self, *args, **kwargs):
f'instance of it must be passed')

# raise an error if the metavar does not match the type
if hasattr(self, "_get_formatter"):
formatter = self._get_formatter()
if hasattr(self, "_get_validation_formatter"):
formatter = self._get_validation_formatter()
try:
formatter._format_args(action, None)
except TypeError:
Expand Down Expand Up @@ -1741,8 +1738,8 @@ def _handle_conflict_resolve(self, action, conflicting_actions):
action.container._remove_action(action)

def _check_help(self, action):
if action.help and hasattr(self, "_get_formatter"):
formatter = self._get_formatter()
if action.help and hasattr(self, "_get_validation_formatter"):
formatter = self._get_validation_formatter()
try:
formatter._expand_help(action)
except (ValueError, TypeError, KeyError) as exc:
Expand Down Expand Up @@ -1897,6 +1894,9 @@ def __init__(self,
self.suggest_on_error = suggest_on_error
self.color = color

# Cached formatter for validation (avoids repeated _set_color calls)
self._cached_formatter = None

add_group = self.add_argument_group
self._positionals = add_group(_('positional arguments'))
self._optionals = add_group(_('options'))
Expand Down Expand Up @@ -2728,6 +2728,13 @@ def _get_formatter(self):
formatter._set_color(self.color)
return formatter

def _get_validation_formatter(self):
# Return cached formatter for read-only validation operations
# (_expand_help and _format_args). Avoids repeated slow _set_color calls.
if self._cached_formatter is None:
self._cached_formatter = self._get_formatter()
return self._cached_formatter

# =====================
# Help-printing methods
# =====================
Expand Down
Loading
Loading