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