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