summaryrefslogtreecommitdiffstats
path: root/test/lib/ansible_test/_internal/cli/argparsing/argcompletion.py
blob: cf5776da3facb76b6a5ab62b2079151276ddec90 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Wrapper around argcomplete providing bug fixes and additional features."""
from __future__ import annotations

import argparse
import enum
import os
import typing as t


class Substitute:
    """Substitute for missing class which accepts all arguments."""
    def __init__(self, *args, **kwargs) -> None:
        pass


try:
    import argcomplete

    from argcomplete import (
        CompletionFinder,
        default_validator,
    )

    warn = argcomplete.warn  # pylint: disable=invalid-name
except ImportError:
    argcomplete = None

    CompletionFinder = Substitute
    default_validator = Substitute  # pylint: disable=invalid-name
    warn = Substitute  # pylint: disable=invalid-name


class CompType(enum.Enum):
    """
    Bash COMP_TYPE argument completion types.
    For documentation, see: https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#index-COMP_005fTYPE
    """
    COMPLETION = '\t'
    """
    Standard completion, typically triggered by a single tab.
    """
    MENU_COMPLETION = '%'
    """
    Menu completion, which cycles through each completion instead of showing a list.
    For help using this feature, see: https://stackoverflow.com/questions/12044574/getting-complete-and-menu-complete-to-work-together
    """
    LIST = '?'
    """
    Standard list, typically triggered by a double tab.
    """
    LIST_AMBIGUOUS = '!'
    """
    Listing with `show-all-if-ambiguous` set.
    For documentation, see https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html#index-show_002dall_002dif_002dambiguous
    For additional details, see: https://unix.stackexchange.com/questions/614123/explanation-of-bash-completion-comp-type
    """
    LIST_UNMODIFIED = '@'
    """
    Listing with `show-all-if-unmodified` set.
    For documentation, see https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html#index-show_002dall_002dif_002dunmodified
    For additional details, see: : https://unix.stackexchange.com/questions/614123/explanation-of-bash-completion-comp-type
    """

    @property
    def list_mode(self) -> bool:
        """True if completion is running in list mode, otherwise False."""
        return self in (CompType.LIST, CompType.LIST_AMBIGUOUS, CompType.LIST_UNMODIFIED)


def register_safe_action(action_type: t.Type[argparse.Action]) -> None:
    """Register the given action as a safe action for argcomplete to use during completion if it is not already registered."""
    if argcomplete and action_type not in argcomplete.safe_actions:
        argcomplete.safe_actions += (action_type,)


def get_comp_type() -> t.Optional[CompType]:
    """Parse the COMP_TYPE environment variable (if present) and return the associated CompType enum value."""
    value = os.environ.get('COMP_TYPE')
    comp_type = CompType(chr(int(value))) if value else None
    return comp_type


class OptionCompletionFinder(CompletionFinder):
    """
    Custom completion finder for argcomplete.
    It provides support for running completion in list mode, which argcomplete natively handles the same as standard completion.
    """
    enabled = bool(argcomplete)

    def __init__(self, *args, validator=None, **kwargs) -> None:
        if validator:
            raise ValueError()

        self.comp_type = get_comp_type()
        self.list_mode = self.comp_type.list_mode if self.comp_type else False
        self.disable_completion_mangling = False

        finder = self

        def custom_validator(completion, prefix):
            """Completion validator used to optionally bypass validation."""
            if finder.disable_completion_mangling:
                return True

            return default_validator(completion, prefix)

        super().__init__(
            *args,
            validator=custom_validator,
            **kwargs,
        )

    def __call__(self, *args, **kwargs):
        if self.enabled:
            super().__call__(*args, **kwargs)

    def quote_completions(self, completions, cword_prequote, last_wordbreak_pos):
        """Intercept default quoting behavior to optionally block mangling of completion entries."""
        if self.disable_completion_mangling:
            # Word breaks have already been handled when generating completions, don't mangle them further.
            # This is needed in many cases when returning completion lists which lack the existing completion prefix.
            last_wordbreak_pos = None

        return super().quote_completions(completions, cword_prequote, last_wordbreak_pos)