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