Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/argcomplete/finders.py: 14%
321 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:31 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:31 +0000
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.
6import argparse
7import os
8import sys
9from collections.abc import Mapping
10from typing import Callable, Dict, List, Optional, Sequence, Union
12from . import io as _io
13from .completers import ChoicesCompleter, FilesCompleter, SuppressCompleter
14from .io import debug, mute_stderr
15from .lexers import split_line
16from .packages._argparse import IntrospectiveArgumentParser, action_is_greedy, action_is_open, action_is_satisfied
18safe_actions = {
19 argparse._StoreAction,
20 argparse._StoreConstAction,
21 argparse._StoreTrueAction,
22 argparse._StoreFalseAction,
23 argparse._AppendAction,
24 argparse._AppendConstAction,
25 argparse._CountAction,
26}
29def default_validator(completion, prefix):
30 return completion.startswith(prefix)
33class CompletionFinder(object):
34 """
35 Inherit from this class if you wish to override any of the stages below. Otherwise, use
36 ``argcomplete.autocomplete()`` directly (it's a convenience instance of this class). It has the same signature as
37 :meth:`CompletionFinder.__call__()`.
38 """
40 def __init__(
41 self,
42 argument_parser=None,
43 always_complete_options=True,
44 exclude=None,
45 validator=None,
46 print_suppressed=False,
47 default_completer=FilesCompleter(),
48 append_space=None,
49 ):
50 self._parser = argument_parser
51 self.always_complete_options = always_complete_options
52 self.exclude = exclude
53 if validator is None:
54 validator = default_validator
55 self.validator = validator
56 self.print_suppressed = print_suppressed
57 self.completing = False
58 self._display_completions: Dict[str, str] = {}
59 self.default_completer = default_completer
60 if append_space is None:
61 append_space = os.environ.get("_ARGCOMPLETE_SUPPRESS_SPACE") != "1"
62 self.append_space = append_space
64 def __call__(
65 self,
66 argument_parser: argparse.ArgumentParser,
67 always_complete_options: Union[bool, str] = True,
68 exit_method: Callable = os._exit,
69 output_stream=None,
70 exclude: Optional[Sequence[str]] = None,
71 validator: Optional[Callable] = None,
72 print_suppressed: bool = False,
73 append_space: Optional[bool] = None,
74 default_completer=FilesCompleter(),
75 ):
76 """
77 :param argument_parser: The argument parser to autocomplete on
78 :param always_complete_options:
79 Controls the autocompletion of option strings if an option string opening character (normally ``-``) has not
80 been entered. If ``True`` (default), both short (``-x``) and long (``--x``) option strings will be
81 suggested. If ``False``, no option strings will be suggested. If ``long``, long options and short options
82 with no long variant will be suggested. If ``short``, short options and long options with no short variant
83 will be suggested.
84 :param exit_method:
85 Method used to stop the program after printing completions. Defaults to :meth:`os._exit`. If you want to
86 perform a normal exit that calls exit handlers, use :meth:`sys.exit`.
87 :param exclude: List of strings representing options to be omitted from autocompletion
88 :param validator:
89 Function to filter all completions through before returning (called with two string arguments, completion
90 and prefix; return value is evaluated as a boolean)
91 :param print_suppressed:
92 Whether or not to autocomplete options that have the ``help=argparse.SUPPRESS`` keyword argument set.
93 :param append_space:
94 Whether to append a space to unique matches. The default is ``True``.
96 .. note::
97 If you are not subclassing CompletionFinder to override its behaviors,
98 use :meth:`argcomplete.autocomplete()` directly. It has the same signature as this method.
100 Produces tab completions for ``argument_parser``. See module docs for more info.
102 Argcomplete only executes actions if their class is known not to have side effects. Custom action classes can be
103 added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or
104 their execution is otherwise desirable.
105 """
106 self.__init__( # type: ignore
107 argument_parser,
108 always_complete_options=always_complete_options,
109 exclude=exclude,
110 validator=validator,
111 print_suppressed=print_suppressed,
112 append_space=append_space,
113 default_completer=default_completer,
114 )
116 if "_ARGCOMPLETE" not in os.environ:
117 # not an argument completion invocation
118 return
120 try:
121 _io.debug_stream = os.fdopen(9, "w")
122 except Exception:
123 _io.debug_stream = sys.stderr
124 debug()
126 if output_stream is None:
127 filename = os.environ.get("_ARGCOMPLETE_STDOUT_FILENAME")
128 if filename is not None:
129 debug("Using output file {}".format(filename))
130 output_stream = open(filename, "w")
132 if output_stream is None:
133 try:
134 output_stream = os.fdopen(8, "w")
135 except Exception:
136 debug("Unable to open fd 8 for writing, quitting")
137 exit_method(1)
139 ifs = os.environ.get("_ARGCOMPLETE_IFS", "\013")
140 if len(ifs) != 1:
141 debug("Invalid value for IFS, quitting [{v}]".format(v=ifs))
142 exit_method(1)
144 dfs = os.environ.get("_ARGCOMPLETE_DFS")
145 if dfs and len(dfs) != 1:
146 debug("Invalid value for DFS, quitting [{v}]".format(v=dfs))
147 exit_method(1)
149 comp_line = os.environ["COMP_LINE"]
150 comp_point = int(os.environ["COMP_POINT"])
152 cword_prequote, cword_prefix, cword_suffix, comp_words, last_wordbreak_pos = split_line(comp_line, comp_point)
154 # _ARGCOMPLETE is set by the shell script to tell us where comp_words
155 # should start, based on what we're completing.
156 # 1: <script> [args]
157 # 2: python <script> [args]
158 # 3: python -m <module> [args]
159 start = int(os.environ["_ARGCOMPLETE"]) - 1
160 comp_words = comp_words[start:]
162 if cword_prefix and cword_prefix[0] in self._parser.prefix_chars and "=" in cword_prefix:
163 # Special case for when the current word is "--optional=PARTIAL_VALUE". Give the optional to the parser.
164 comp_words.append(cword_prefix.split("=", 1)[0])
166 debug(
167 "\nLINE: {!r}".format(comp_line),
168 "\nPOINT: {!r}".format(comp_point),
169 "\nPREQUOTE: {!r}".format(cword_prequote),
170 "\nPREFIX: {!r}".format(cword_prefix),
171 "\nSUFFIX: {!r}".format(cword_suffix),
172 "\nWORDS:",
173 comp_words,
174 )
176 completions = self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos)
178 if dfs:
179 display_completions = {
180 key: value.replace(ifs, " ") if value else "" for key, value in self._display_completions.items()
181 }
182 completions = [dfs.join((key, display_completions.get(key) or "")) for key in completions]
184 if os.environ.get("_ARGCOMPLETE_SHELL") == "zsh":
185 completions = [f"{c}:{self._display_completions.get(c)}" for c in completions]
187 debug("\nReturning completions:", completions)
188 output_stream.write(ifs.join(completions))
189 output_stream.flush()
190 _io.debug_stream.flush()
191 exit_method(0)
193 def _get_completions(self, comp_words, cword_prefix, cword_prequote, last_wordbreak_pos):
194 active_parsers = self._patch_argument_parser()
196 parsed_args = argparse.Namespace()
197 self.completing = True
199 try:
200 debug("invoking parser with", comp_words[1:])
201 with mute_stderr():
202 a = self._parser.parse_known_args(comp_words[1:], namespace=parsed_args)
203 debug("parsed args:", a)
204 except BaseException as e:
205 debug("\nexception", type(e), str(e), "while parsing args")
207 self.completing = False
209 if "--" in comp_words:
210 self.always_complete_options = False
212 completions = self.collect_completions(active_parsers, parsed_args, cword_prefix)
213 completions = self.filter_completions(completions)
214 completions = self.quote_completions(completions, cword_prequote, last_wordbreak_pos)
215 return completions
217 def _patch_argument_parser(self):
218 """
219 Since argparse doesn't support much introspection, we monkey-patch it to replace the parse_known_args method and
220 all actions with hooks that tell us which action was last taken or about to be taken, and let us have the parser
221 figure out which subparsers need to be activated (then recursively monkey-patch those).
222 We save all active ArgumentParsers to extract all their possible option names later.
223 """
224 self.active_parsers: List[argparse.ArgumentParser] = []
225 self.visited_positionals: List[argparse.Action] = []
227 completer = self
229 def patch(parser):
230 completer.visited_positionals.append(parser)
231 completer.active_parsers.append(parser)
233 if isinstance(parser, IntrospectiveArgumentParser):
234 return
236 classname = "MonkeyPatchedIntrospectiveArgumentParser"
238 parser.__class__ = type(classname, (IntrospectiveArgumentParser, parser.__class__), {})
240 for action in parser._actions:
241 if hasattr(action, "_orig_class"):
242 continue
244 # TODO: accomplish this with super
245 class IntrospectAction(action.__class__): # type: ignore
246 def __call__(self, parser, namespace, values, option_string=None):
247 debug("Action stub called on", self)
248 debug("\targs:", parser, namespace, values, option_string)
249 debug("\torig class:", self._orig_class)
250 debug("\torig callable:", self._orig_callable)
252 if not completer.completing:
253 self._orig_callable(parser, namespace, values, option_string=option_string)
254 elif issubclass(self._orig_class, argparse._SubParsersAction):
255 debug("orig class is a subparsers action: patching and running it")
256 patch(self._name_parser_map[values[0]])
257 self._orig_callable(parser, namespace, values, option_string=option_string)
258 elif self._orig_class in safe_actions:
259 if not self.option_strings:
260 completer.visited_positionals.append(self)
262 self._orig_callable(parser, namespace, values, option_string=option_string)
264 action._orig_class = action.__class__
265 action._orig_callable = action.__call__
266 action.__class__ = IntrospectAction
268 patch(self._parser)
270 debug("Active parsers:", self.active_parsers)
271 debug("Visited positionals:", self.visited_positionals)
273 return self.active_parsers
275 def _get_subparser_completions(self, parser, cword_prefix):
276 aliases_by_parser: Dict[argparse.ArgumentParser, List[str]] = {}
277 for key in parser.choices.keys():
278 p = parser.choices[key]
279 aliases_by_parser.setdefault(p, []).append(key)
281 for action in parser._get_subactions():
282 for alias in aliases_by_parser[parser.choices[action.dest]]:
283 if alias.startswith(cword_prefix):
284 self._display_completions[alias] = action.help or ""
286 completions = [subcmd for subcmd in parser.choices.keys() if subcmd.startswith(cword_prefix)]
287 return completions
289 def _include_options(self, action, cword_prefix):
290 if len(cword_prefix) > 0 or self.always_complete_options is True:
291 return [opt for opt in action.option_strings if opt.startswith(cword_prefix)]
292 long_opts = [opt for opt in action.option_strings if len(opt) > 2]
293 short_opts = [opt for opt in action.option_strings if len(opt) <= 2]
294 if self.always_complete_options == "long":
295 return long_opts if long_opts else short_opts
296 elif self.always_complete_options == "short":
297 return short_opts if short_opts else long_opts
298 return []
300 def _get_option_completions(self, parser, cword_prefix):
301 for action in parser._actions:
302 if action.option_strings:
303 for option_string in action.option_strings:
304 if option_string.startswith(cword_prefix):
305 self._display_completions[option_string] = action.help or ""
307 option_completions = []
308 for action in parser._actions:
309 if not self.print_suppressed:
310 completer = getattr(action, "completer", None)
311 if isinstance(completer, SuppressCompleter) and completer.suppress():
312 continue
313 if action.help == argparse.SUPPRESS:
314 continue
315 if not self._action_allowed(action, parser):
316 continue
317 if not isinstance(action, argparse._SubParsersAction):
318 option_completions += self._include_options(action, cword_prefix)
319 return option_completions
321 @staticmethod
322 def _action_allowed(action, parser):
323 # Logic adapted from take_action in ArgumentParser._parse_known_args
324 # (members are saved by vendor._argparse.IntrospectiveArgumentParser)
325 for conflict_action in parser._action_conflicts.get(action, []):
326 if conflict_action in parser._seen_non_default_actions:
327 return False
328 return True
330 def _complete_active_option(self, parser, next_positional, cword_prefix, parsed_args, completions):
331 debug("Active actions (L={l}): {a}".format(l=len(parser.active_actions), a=parser.active_actions))
333 isoptional = cword_prefix and cword_prefix[0] in parser.prefix_chars
334 optional_prefix = ""
335 greedy_actions = [x for x in parser.active_actions if action_is_greedy(x, isoptional)]
336 if greedy_actions:
337 assert len(greedy_actions) == 1, "expect at most 1 greedy action"
338 # This means the action will fail to parse if the word under the cursor is not given
339 # to it, so give it exclusive control over completions (flush previous completions)
340 debug("Resetting completions because", greedy_actions[0], "must consume the next argument")
341 self._display_completions = {}
342 completions = []
343 elif isoptional:
344 if "=" in cword_prefix:
345 # Special case for when the current word is "--optional=PARTIAL_VALUE".
346 # The completer runs on PARTIAL_VALUE. The prefix is added back to the completions
347 # (and chopped back off later in quote_completions() by the COMP_WORDBREAKS logic).
348 optional_prefix, _, cword_prefix = cword_prefix.partition("=")
349 else:
350 # Only run completers if current word does not start with - (is not an optional)
351 return completions
353 complete_remaining_positionals = False
354 # Use the single greedy action (if there is one) or all active actions.
355 for active_action in greedy_actions or parser.active_actions:
356 if not active_action.option_strings: # action is a positional
357 if action_is_open(active_action):
358 # Any positional arguments after this may slide down into this action
359 # if more arguments are added (since the user may not be done yet),
360 # so it is extremely difficult to tell which completers to run.
361 # Running all remaining completers will probably show more than the user wants
362 # but it also guarantees we won't miss anything.
363 complete_remaining_positionals = True
364 if not complete_remaining_positionals:
365 if action_is_satisfied(active_action) and not action_is_open(active_action):
366 debug("Skipping", active_action)
367 continue
369 debug("Activating completion for", active_action, active_action._orig_class)
370 # completer = getattr(active_action, "completer", DefaultCompleter())
371 completer = getattr(active_action, "completer", None)
373 if completer is None:
374 if active_action.choices is not None and not isinstance(active_action, argparse._SubParsersAction):
375 completer = ChoicesCompleter(active_action.choices)
376 elif not isinstance(active_action, argparse._SubParsersAction):
377 completer = self.default_completer
379 if completer:
380 if isinstance(completer, SuppressCompleter) and completer.suppress():
381 continue
383 if callable(completer):
384 completer_output = completer(
385 prefix=cword_prefix, action=active_action, parser=parser, parsed_args=parsed_args
386 )
387 if isinstance(completer_output, Mapping):
388 for completion, description in completer_output.items():
389 if self.validator(completion, cword_prefix):
390 completions.append(completion)
391 self._display_completions[completion] = description
392 else:
393 for completion in completer_output:
394 if self.validator(completion, cword_prefix):
395 completions.append(completion)
396 if isinstance(completer, ChoicesCompleter):
397 self._display_completions[completion] = active_action.help or ""
398 else:
399 self._display_completions[completion] = ""
400 else:
401 debug("Completer is not callable, trying the readline completer protocol instead")
402 for i in range(9999):
403 next_completion = completer.complete(cword_prefix, i) # type: ignore
404 if next_completion is None:
405 break
406 if self.validator(next_completion, cword_prefix):
407 self._display_completions[next_completion] = ""
408 completions.append(next_completion)
409 if optional_prefix:
410 completions = [optional_prefix + "=" + completion for completion in completions]
411 debug("Completions:", completions)
412 return completions
414 def collect_completions(
415 self, active_parsers: List[argparse.ArgumentParser], parsed_args: argparse.Namespace, cword_prefix: str
416 ) -> List[str]:
417 """
418 Visits the active parsers and their actions, executes their completers or introspects them to collect their
419 option strings. Returns the resulting completions as a list of strings.
421 This method is exposed for overriding in subclasses; there is no need to use it directly.
422 """
423 completions: List[str] = []
425 debug("all active parsers:", active_parsers)
426 active_parser = active_parsers[-1]
427 debug("active_parser:", active_parser)
428 if self.always_complete_options or (len(cword_prefix) > 0 and cword_prefix[0] in active_parser.prefix_chars):
429 completions += self._get_option_completions(active_parser, cword_prefix)
430 debug("optional options:", completions)
432 next_positional = self._get_next_positional()
433 debug("next_positional:", next_positional)
435 if isinstance(next_positional, argparse._SubParsersAction):
436 completions += self._get_subparser_completions(next_positional, cword_prefix)
438 completions = self._complete_active_option(
439 active_parser, next_positional, cword_prefix, parsed_args, completions
440 )
441 debug("active options:", completions)
442 debug("display completions:", self._display_completions)
444 return completions
446 def _get_next_positional(self):
447 """
448 Get the next positional action if it exists.
449 """
450 active_parser = self.active_parsers[-1]
451 last_positional = self.visited_positionals[-1]
453 all_positionals = active_parser._get_positional_actions()
454 if not all_positionals:
455 return None
457 if active_parser == last_positional:
458 return all_positionals[0]
460 i = 0
461 for i in range(len(all_positionals)):
462 if all_positionals[i] == last_positional:
463 break
465 if i + 1 < len(all_positionals):
466 return all_positionals[i + 1]
468 return None
470 def filter_completions(self, completions: List[str]) -> List[str]:
471 """
472 De-duplicates completions and excludes those specified by ``exclude``.
473 Returns the filtered completions as a list.
475 This method is exposed for overriding in subclasses; there is no need to use it directly.
476 """
477 filtered_completions = []
478 for completion in completions:
479 if self.exclude is not None:
480 if completion in self.exclude:
481 continue
482 if completion not in filtered_completions:
483 filtered_completions.append(completion)
484 return filtered_completions
486 def quote_completions(
487 self, completions: List[str], cword_prequote: str, last_wordbreak_pos: Optional[int]
488 ) -> List[str]:
489 """
490 If the word under the cursor started with a quote (as indicated by a nonempty ``cword_prequote``), escapes
491 occurrences of that quote character in the completions, and adds the quote to the beginning of each completion.
492 Otherwise, escapes all characters that bash splits words on (``COMP_WORDBREAKS``), and removes portions of
493 completions before the first colon if (``COMP_WORDBREAKS``) contains a colon.
495 If there is only one completion, and it doesn't end with a **continuation character** (``/``, ``:``, or ``=``),
496 adds a space after the completion.
498 This method is exposed for overriding in subclasses; there is no need to use it directly.
499 """
500 special_chars = "\\"
501 # If the word under the cursor was quoted, escape the quote char.
502 # Otherwise, escape all special characters and specially handle all COMP_WORDBREAKS chars.
503 if cword_prequote == "":
504 # Bash mangles completions which contain characters in COMP_WORDBREAKS.
505 # This workaround has the same effect as __ltrim_colon_completions in bash_completion
506 # (extended to characters other than the colon).
507 if last_wordbreak_pos:
508 completions = [c[last_wordbreak_pos + 1 :] for c in completions]
509 special_chars += "();<>|&!`$* \t\n\"'"
510 elif cword_prequote == '"':
511 special_chars += '"`$!'
513 if os.environ.get("_ARGCOMPLETE_SHELL") in ("tcsh", "fish"):
514 # tcsh and fish escapes special characters itself.
515 special_chars = ""
516 elif cword_prequote == "'":
517 # Nothing can be escaped in single quotes, so we need to close
518 # the string, escape the single quote, then open a new string.
519 special_chars = ""
520 completions = [c.replace("'", r"'\''") for c in completions]
522 # PowerShell uses ` as escape character.
523 if os.environ.get("_ARGCOMPLETE_SHELL") == "powershell":
524 escape_char = '`'
525 special_chars = special_chars.replace('`', '')
526 else:
527 escape_char = "\\"
528 for char in special_chars:
529 completions = [c.replace(char, escape_char + char) for c in completions]
531 if self.append_space:
532 # Similar functionality in bash was previously turned off by supplying the "-o nospace" option to complete.
533 # Now it is conditionally disabled using "compopt -o nospace" if the match ends in a continuation character.
534 # This code is retained for environments where this isn't done natively.
535 continuation_chars = "=/:"
536 if len(completions) == 1 and completions[0][-1] not in continuation_chars:
537 if cword_prequote == "":
538 completions[0] += " "
540 return completions
542 def rl_complete(self, text, state):
543 """
544 Alternate entry point for using the argcomplete completer in a readline-based REPL. See also
545 `rlcompleter <https://docs.python.org/3/library/rlcompleter.html#completer-objects>`_.
546 Usage:
548 .. code-block:: python
550 import argcomplete, argparse, readline
551 parser = argparse.ArgumentParser()
552 ...
553 completer = argcomplete.CompletionFinder(parser)
554 readline.set_completer_delims("")
555 readline.set_completer(completer.rl_complete)
556 readline.parse_and_bind("tab: complete")
557 result = input("prompt> ")
558 """
559 if state == 0:
560 cword_prequote, cword_prefix, cword_suffix, comp_words, first_colon_pos = split_line(text)
561 comp_words.insert(0, sys.argv[0])
562 matches = self._get_completions(comp_words, cword_prefix, cword_prequote, first_colon_pos)
563 self._rl_matches = [text + match[len(cword_prefix) :] for match in matches]
565 if state < len(self._rl_matches):
566 return self._rl_matches[state]
567 else:
568 return None
570 def get_display_completions(self):
571 """
572 This function returns a mapping of option names to their help strings for displaying to the user.
573 """
574 return self._display_completions
577class ExclusiveCompletionFinder(CompletionFinder):
578 @staticmethod
579 def _action_allowed(action, parser):
580 if not CompletionFinder._action_allowed(action, parser):
581 return False
583 append_classes = (argparse._AppendAction, argparse._AppendConstAction)
584 if action._orig_class in append_classes:
585 return True
587 if action not in parser._seen_non_default_actions:
588 return True
590 return False