1# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors. Licensed under the terms of the
2# `Apache License, Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>`_. Distribution of the LICENSE and NOTICE
3# files with source copies of this package and derivative works is **REQUIRED** as specified by the Apache License.
4# See https://github.com/kislyuk/argcomplete for more info.
5
6# This file contains argparse introspection utilities used in the course of argcomplete execution.
7
8from __future__ import annotations
9
10from argparse import (
11 ONE_OR_MORE,
12 OPTIONAL,
13 PARSER,
14 REMAINDER,
15 SUPPRESS,
16 ZERO_OR_MORE,
17 Action,
18 ArgumentError,
19 ArgumentParser,
20 _get_action_name,
21 _SubParsersAction,
22)
23from gettext import gettext
24from typing import cast
25
26_OptionTuple = tuple[Action | None, str, str | None] | tuple[Action | None, str, str | None, str | None]
27_OptionTupleEntry = _OptionTuple | list[_OptionTuple]
28
29_num_consumed_args: dict[Action, int] = {}
30
31
32def action_is_satisfied(action):
33 '''Returns False if the parse would raise an error if no more arguments are given to this action, True otherwise.'''
34 num_consumed_args = _num_consumed_args.get(action, 0)
35
36 if action.nargs in [OPTIONAL, ZERO_OR_MORE, REMAINDER]:
37 return True
38 if action.nargs == ONE_OR_MORE:
39 return num_consumed_args >= 1
40 if action.nargs == PARSER:
41 # Not sure what this should be, but this previously always returned False
42 # so at least this won't break anything that wasn't already broken.
43 return False
44 if action.nargs is None:
45 return num_consumed_args == 1
46
47 assert isinstance(action.nargs, int), f'failed to handle a possible nargs value: {action.nargs!r}'
48 return num_consumed_args == action.nargs
49
50
51def action_is_open(action):
52 '''Returns True if action could consume more arguments (i.e., its pattern is open).'''
53 num_consumed_args = _num_consumed_args.get(action, 0)
54
55 if action.nargs in [ZERO_OR_MORE, ONE_OR_MORE, PARSER, REMAINDER]:
56 return True
57 if action.nargs == OPTIONAL or action.nargs is None:
58 return num_consumed_args == 0
59
60 assert isinstance(action.nargs, int), f'failed to handle a possible nargs value: {action.nargs!r}'
61 return num_consumed_args < action.nargs
62
63
64def action_is_greedy(action, isoptional=False):
65 '''Returns True if action will necessarily consume the next argument.
66 isoptional indicates whether the argument is an optional (starts with -).
67 '''
68 num_consumed_args = _num_consumed_args.get(action, 0)
69
70 if action.option_strings:
71 if not isoptional and not action_is_satisfied(action):
72 return True
73 return action.nargs == REMAINDER
74 else:
75 return action.nargs == REMAINDER and num_consumed_args >= 1
76
77
78class IntrospectiveArgumentParser(ArgumentParser):
79 '''The following is a verbatim copy of ArgumentParser._parse_known_args (Python 2.7.3),
80 except for the lines that contain the string "Added by argcomplete".
81 '''
82
83 def _parse_known_args(self, arg_strings, namespace, intermixed=False, **kwargs):
84 _num_consumed_args.clear() # Added by argcomplete
85 self._argcomplete_namespace = namespace
86 self.active_actions: list[Action] = [] # Added by argcomplete
87 # replace arg strings that are file references
88 if self.fromfile_prefix_chars is not None:
89 arg_strings = self._read_args_from_files(arg_strings)
90
91 # map all mutually exclusive arguments to the other arguments
92 # they can't occur with
93 action_conflicts: dict[Action, list[Action]] = {}
94 self._action_conflicts = action_conflicts # Added by argcomplete
95 for mutex_group in self._mutually_exclusive_groups:
96 group_actions = mutex_group._group_actions
97 for i, mutex_action in enumerate(mutex_group._group_actions):
98 conflicts = action_conflicts.setdefault(mutex_action, [])
99 conflicts.extend(group_actions[:i])
100 conflicts.extend(group_actions[i + 1 :])
101
102 # find all option indices, and determine the arg_string_pattern
103 # which has an 'O' if there is an option at an index,
104 # an 'A' if there is an argument, or a '-' if there is a '--'
105 option_string_indices: dict[int, _OptionTupleEntry] = {}
106 arg_string_pattern_parts = []
107 arg_strings_iter = iter(arg_strings)
108 for i, arg_string in enumerate(arg_strings_iter):
109 # all args after -- are non-options
110 if arg_string == '--':
111 arg_string_pattern_parts.append('-')
112 for arg_string in arg_strings_iter:
113 arg_string_pattern_parts.append('A')
114
115 # otherwise, add the arg to the arg strings
116 # and note the index if it was an option
117 else:
118 option_tuple = self._parse_optional(arg_string)
119 if option_tuple is None:
120 pattern = 'A'
121 else:
122 option_string_indices[i] = cast(_OptionTupleEntry, option_tuple)
123 pattern = 'O'
124 arg_string_pattern_parts.append(pattern)
125
126 # join the pieces together to form the pattern
127 arg_strings_pattern = ''.join(arg_string_pattern_parts)
128
129 # converts arg strings to the appropriate and then takes the action
130 seen_actions: set[Action] = set()
131 seen_non_default_actions: set[Action] = set()
132 self._seen_non_default_actions = seen_non_default_actions # Added by argcomplete
133
134 def take_action(action, argument_strings, option_string=None):
135 seen_actions.add(action)
136 argument_values = self._get_values(action, argument_strings)
137
138 # error if this argument is not allowed with other previously
139 # seen arguments, assuming that actions that use the default
140 # value don't really count as "present"
141 if argument_values is not action.default:
142 seen_non_default_actions.add(action)
143 for conflict_action in action_conflicts.get(action, []):
144 if conflict_action in seen_non_default_actions:
145 msg = gettext('not allowed with argument %s')
146 action_name = _get_action_name(conflict_action)
147 raise ArgumentError(action, msg % action_name)
148
149 # take the action if we didn't receive a SUPPRESS value
150 # (e.g. from a default)
151 if argument_values is not SUPPRESS or isinstance(action, _SubParsersAction):
152 try:
153 action(self, namespace, argument_values, option_string)
154 except BaseException:
155 # Begin added by argcomplete
156 # When a subparser action is taken and fails due to incomplete arguments, it does not merge the
157 # contents of its parsed namespace into the parent namespace. Do that here to allow completers to
158 # access the partially parsed arguments for the subparser.
159 if isinstance(action, _SubParsersAction):
160 subnamespace = action._name_parser_map[argument_values[0]]._argcomplete_namespace
161 for key, value in vars(subnamespace).items():
162 setattr(namespace, key, value)
163 # End added by argcomplete
164 raise
165
166 # function to convert arg_strings into an optional action
167 def consume_optional(start_index):
168 # get the optional identified at this index
169 raw_option_tuple = option_string_indices[start_index]
170 if isinstance(raw_option_tuple, list): # Python 3.12.7+
171 option_tuple = raw_option_tuple[0]
172 else:
173 option_tuple = raw_option_tuple
174 if len(option_tuple) == 3:
175 action, option_string, explicit_arg = option_tuple
176 else: # Python 3.11.9+, 3.12.3+, 3.13+
177 action, option_string, _, explicit_arg = option_tuple
178
179 # identify additional optionals in the same arg string
180 # (e.g. -xyz is the same as -x -y -z if no args are required)
181 match_argument = self._match_argument
182 action_tuples: list[tuple[Action, list[str], str]] = []
183 while True:
184 # if we found no optional action, skip it
185 if action is None:
186 extras.append(arg_strings[start_index])
187 return start_index + 1
188
189 # if there is an explicit argument, try to match the
190 # optional's string arguments to only this
191 if explicit_arg is not None:
192 arg_count = match_argument(action, 'A')
193
194 # if the action is a single-dash option and takes no
195 # arguments, try to parse more single-dash options out
196 # of the tail of the option string
197 chars = self.prefix_chars
198 if arg_count == 0 and option_string[1] not in chars:
199 action_tuples.append((action, [], option_string))
200 char = option_string[0]
201 option_string = char + explicit_arg[0]
202 new_explicit_arg = explicit_arg[1:] or None
203 optionals_map = self._option_string_actions
204 if option_string in optionals_map:
205 action = optionals_map[option_string]
206 explicit_arg = new_explicit_arg
207 else:
208 msg = gettext('ignored explicit argument %r')
209 raise ArgumentError(action, msg % explicit_arg)
210
211 # if the action expect exactly one argument, we've
212 # successfully matched the option; exit the loop
213 elif arg_count == 1:
214 stop = start_index + 1
215 args = [explicit_arg]
216 action_tuples.append((action, args, option_string))
217 break
218
219 # error if a double-dash option did not use the
220 # explicit argument
221 else:
222 msg = gettext('ignored explicit argument %r')
223 raise ArgumentError(action, msg % explicit_arg)
224
225 # if there is no explicit argument, try to match the
226 # optional's string arguments with the following strings
227 # if successful, exit the loop
228 else:
229 start = start_index + 1
230 selected_patterns = arg_strings_pattern[start:]
231 self.active_actions = [action] # Added by argcomplete
232 _num_consumed_args[action] = 0 # Added by argcomplete
233 arg_count = match_argument(action, selected_patterns)
234 stop = start + arg_count
235 args = arg_strings[start:stop]
236
237 # Begin added by argcomplete
238 # If the pattern is not open (e.g. no + at the end), remove the action from active actions (since
239 # it wouldn't be able to consume any more args)
240 _num_consumed_args[action] = len(args)
241 if not action_is_open(action):
242 self.active_actions.remove(action)
243 # End added by argcomplete
244
245 action_tuples.append((action, args, option_string))
246 break
247
248 # add the Optional to the list and return the index at which
249 # the Optional's string args stopped
250 assert action_tuples
251 for optional_action, args, option_string in action_tuples:
252 take_action(optional_action, args, option_string)
253 return stop
254
255 # the list of Positionals left to be parsed; this is modified
256 # by consume_positionals()
257 positionals = self._get_positional_actions()
258
259 # function to convert arg_strings into positional actions
260 def consume_positionals(start_index):
261 # match as many Positionals as possible
262 match_partial = self._match_arguments_partial
263 selected_pattern = arg_strings_pattern[start_index:]
264 arg_counts = match_partial(positionals, selected_pattern)
265
266 # slice off the appropriate arg strings for each Positional
267 # and add the Positional and its args to the list
268 for action, arg_count in zip(positionals, arg_counts): # Added by argcomplete
269 self.active_actions.append(action) # Added by argcomplete
270 for action, arg_count in zip(positionals, arg_counts):
271 args = arg_strings[start_index : start_index + arg_count]
272 start_index += arg_count
273 _num_consumed_args[action] = len(args) # Added by argcomplete
274 take_action(action, args)
275
276 # slice off the Positionals that we just parsed and return the
277 # index at which the Positionals' string args stopped
278 positionals[:] = positionals[len(arg_counts) :]
279 return start_index
280
281 # consume Positionals and Optionals alternately, until we have
282 # passed the last option string
283 extras = []
284 start_index = 0
285 if option_string_indices:
286 max_option_string_index = max(option_string_indices)
287 else:
288 max_option_string_index = -1
289 while start_index <= max_option_string_index:
290 # consume any Positionals preceding the next option
291 next_option_string_index = min([index for index in option_string_indices if index >= start_index])
292 if start_index != next_option_string_index:
293 positionals_end_index = consume_positionals(start_index)
294
295 # only try to parse the next optional if we didn't consume
296 # the option string during the positionals parsing
297 if positionals_end_index > start_index:
298 start_index = positionals_end_index
299 continue
300 else:
301 start_index = positionals_end_index
302
303 # if we consumed all the positionals we could and we're not
304 # at the index of an option string, there were extra arguments
305 if start_index not in option_string_indices:
306 strings = arg_strings[start_index:next_option_string_index]
307 extras.extend(strings)
308 start_index = next_option_string_index
309
310 # consume the next optional and any arguments for it
311 start_index = consume_optional(start_index)
312
313 # consume any positionals following the last Optional
314 stop_index = consume_positionals(start_index)
315
316 # if we didn't consume all the argument strings, there were extras
317 extras.extend(arg_strings[stop_index:])
318
319 # if we didn't use all the Positional objects, there were too few
320 # arg strings supplied.
321
322 if positionals:
323 self.active_actions.append(positionals[0]) # Added by argcomplete
324 self.error(gettext('too few arguments'))
325
326 # make sure all required actions were present
327 for action in self._actions:
328 if action.required and action not in seen_actions:
329 name = _get_action_name(action)
330 self.error(gettext('argument %s is required') % name)
331
332 # make sure all required groups had one option present
333 for group in self._mutually_exclusive_groups:
334 if group.required:
335 for action in group._group_actions:
336 if action in seen_non_default_actions:
337 break
338
339 # if no actions were used, report the error
340 else:
341 names = [
342 str(_get_action_name(action)) for action in group._group_actions if action.help is not SUPPRESS
343 ]
344 msg = gettext('one of the arguments %s is required')
345 self.error(msg % ' '.join(names))
346
347 # return the updated namespace and the extra arguments
348 return namespace, extras