• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 18812500213

26 Oct 2025 03:42AM UTC coverage: 80.284% (+0.005%) from 80.279%
18812500213

Pull #22804

github

web-flow
Merge 2a56fdb46 into 4834308dc
Pull Request #22804: test_shell_command: use correct default cache scope for a test's environment

29 of 31 new or added lines in 2 files covered. (93.55%)

1314 existing lines in 64 files now uncovered.

77900 of 97030 relevant lines covered (80.28%)

3.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.65
/src/python/pants/goal/completion.py
1
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
# TODO: This was written as a port of the original bash script, but since we have
5
# more knowledge of the options and goals, we can make this more robust and accurate (after tests are written).
6

7
from __future__ import annotations
12✔
8

9
import logging
12✔
10
from enum import Enum
12✔
11

12
from pants.base.exiter import PANTS_SUCCEEDED_EXIT_CODE, ExitCode
12✔
13
from pants.base.specs import Specs
12✔
14
from pants.build_graph.build_configuration import BuildConfiguration
12✔
15
from pants.engine.unions import UnionMembership
12✔
16
from pants.goal.builtin_goal import BuiltinGoal
12✔
17
from pants.init.engine_initializer import GraphSession
12✔
18
from pants.option.option_types import EnumOption
12✔
19
from pants.option.options import Options
12✔
20
from pants.option.scope import GLOBAL_SCOPE
12✔
21
from pants.util.resources import read_resource
12✔
22
from pants.util.strutil import softwrap
12✔
23

24
_COMPLETIONS_PACKAGE = "pants.goal"
12✔
25

26
logger = logging.getLogger(__name__)
12✔
27

28

29
class Shell(Enum):
12✔
30
    BASH = "bash"
12✔
31
    ZSH = "zsh"
12✔
32

33

34
class CompletionBuiltinGoal(BuiltinGoal):
12✔
35
    name = "complete"
12✔
36
    help = softwrap(
12✔
37
        """
38
        Generates a completion script for the specified shell. The script is printed to stdout.
39

40
        For example, `pants complete --shell=zsh > pants-completions.zsh` will generate a zsh
41
        completion script and write it to the file `pants-completions.zsh`. You can then
42
        source this file in your `.zshrc` file to enable completion for Pants.
43

44
        This command is also used by the completion scripts to generate the completion options using
45
        passthrough options. This usage is not intended for use by end users, but could be
46
        useful for building custom completion scripts.
47

48
        An example of this usage is in the bash completion script, where we use the following command:
49
        `pants complete -- ${COMP_WORDS[@]}`. This will generate the completion options for the
50
        current args, and then pass them to the bash completion script.
51
        """
52
    )
53

54
    shell = EnumOption(
12✔
55
        default=Shell.BASH,
56
        help="Which shell completion type should be printed to stdout.",
57
    )
58

59
    def run(
12✔
60
        self,
61
        *,
62
        build_config: BuildConfiguration,
63
        graph_session: GraphSession,
64
        options: Options,
65
        specs: Specs,
66
        union_membership: UnionMembership,
67
    ) -> ExitCode:
68
        """This function is called under two main circumstances.
69

70
        - By a user generating a completion script for their shell (e.g. `pants complete --zsh > pants-completions.zsh`)
71
        - By the shell completions script when the user attempts a tab completion (e.g. `pants <tab>`, `pants fmt lint che<tab>`, etc...)
72

73
        In the first case, we should generate a completion script for the specified shell and print it to stdout.
74
        In the second case, we should generate the completion options for the current command and print them to stdout.
75

76
        The trigger to determine which case we're in is the presence of the passthrough arguments. If there are passthrough
77
        arguments, then we're generating completion options. If there are no passthrough arguments, then we're generating
78
        a completion script.
79
        """
80
        if options.native_parser.get_command().passthru():
×
81
            completion_options = self._generate_completion_options(options)
×
82
            if completion_options:
×
83
                print("\n".join(completion_options))
×
84
            return PANTS_SUCCEEDED_EXIT_CODE
×
85

86
        script = self._generate_completion_script(self.shell)
×
87
        print(script)
×
88
        return PANTS_SUCCEEDED_EXIT_CODE
×
89

90
    def _generate_completion_script(self, shell: Shell) -> str:
12✔
91
        """Generate a completion script for the specified shell.
92

93
        Implementation note: In practice, we're just going to read in
94
        and return the contents of the appropriate static completion script file.
95

96
        :param shell: The shell to generate a completion script for.
97
        :return: The completion script for the specified shell.
98
        """
99
        if shell == Shell.ZSH:
×
100
            return read_resource(_COMPLETIONS_PACKAGE, "pants-completion.zsh").decode("utf-8")
×
101
        else:
102
            return read_resource(_COMPLETIONS_PACKAGE, "pants-completion.bash").decode("utf-8")
×
103

104
    def _generate_completion_options(self, options: Options) -> list[str]:
12✔
105
        """Generate the completion options for the specified args.
106

107
        We're guaranteed to have at least two arguments (`["pants", ""]`). If there are only two arguments,
108
        then we're at the top-level Pants command, and we should show all goals.
109
        - `pants <tab>` -> `... fmt fix lint list repl run test ...`
110

111
        If we're at the top-level and the user has typed a hyphen, then we should show global options.
112
        - `pants -<tab>` -> `... --pants-config-files --pants-distdir --pants-ignore ...`
113

114
        As we add goals, we should show the remaining goals that are available.
115
        - `pants fmt fix lint <tab>` -> `... list repl run test ...`
116

117
        If there is a goal in the list of arguments and the user has typed a hyphen, then we should
118
        show the available scoped options for the previous goal.
119
        - `pants fmt -<tab>` -> `... --only ...`
120

121
        :param options: The options object for the current Pants run.
122
        :return: A list of candidate completion options.
123
        """
124
        passthru = options.native_parser.get_command().passthru()
×
125
        logger.debug(f"Completion passthrough options: {passthru}")
×
126
        args = [arg for arg in passthru if arg != "pants"]
×
127
        current_word = args.pop()
×
128
        previous_goal = self._get_previous_goal(args)
×
129
        logger.debug(f"Current word is '{current_word}', and previous goal is '{previous_goal}'")
×
130

131
        all_goals = sorted([k for k, v in options.known_scope_to_info.items() if v.is_goal])
×
132

133
        # If there is no previous goal, then we're at the top-level Pants command, so show all goals or global options
134
        if not previous_goal:
×
135
            if current_word.startswith("-"):
×
136
                global_options = self._build_options_for_goal(options)
×
137
                candidate_options = [o for o in global_options if o.startswith(current_word)]
×
138
                return candidate_options
×
139

140
            candidate_goals = [g for g in all_goals if g.startswith(current_word)]
×
141
            return candidate_goals
×
142

143
        # If there is already a previous goal and current_word starts with a hyphen, then show scoped options for that goal
144
        if current_word.startswith("-"):
×
145
            scoped_options = self._build_options_for_goal(options, previous_goal)
×
146
            candidate_options = [o for o in scoped_options if o.startswith(current_word)]
×
147
            return candidate_options
×
148

149
        # If there is a previous goal and current_word does not start with a hyphen, then show remaining goals
150
        # excluding the goals that are already in the command
151
        candidate_goals = [g for g in all_goals if g.startswith(current_word) and g not in passthru]
×
152
        return candidate_goals
×
153

154
    def _get_previous_goal(self, args: list[str]) -> str | None:
12✔
155
        """Get the most recent goal in the command arguments, so options can be correctly applied.
156

157
        A "goal" in the context of completions is simply an arg where the first character is alphanumeric.
158
        This under-specifies the goal, because detecting whether an arg is an "actual" goal happens elsewhere.
159

160
        :param args: The list of arguments to search for the previous goal.
161
        :return: The previous goal, or None if there is no previous goal.
162
        """
UNCOV
163
        return next((arg for arg in reversed(args) if arg[:1].isalnum()), None)
1✔
164

165
    def _build_options_for_goal(self, options: Options, goal: str = "") -> list[str]:
12✔
166
        """Build a list of stringified options for the specified goal, prefixed by `--`.
167

168
        :param options: The options object for the current Pants run.
169
        :param goal: The goal to build options for. Defaults to "" for the global scope.
170
        :return: A list of options for the specified goal.
171
        """
172

173
        if goal == GLOBAL_SCOPE:
×
174
            global_options = sorted(options.for_global_scope().as_dict().keys())
×
175
            return [f"--{o}" for o in global_options]
×
176

177
        try:
×
178
            scoped_options = sorted(options.for_scope(goal).as_dict().keys())
×
179
            return [f"--{o}" for o in scoped_options]
×
180
        except Exception:
×
181
            # options.for_scope will throw if the goal is unknown, so we'll just return an empty list
182
            # Since this is used for user-entered tab completion, it's not a warning or error
183
            return []
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc