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

pantsbuild / pants / 19250292619

11 Nov 2025 12:09AM UTC coverage: 77.865% (-2.4%) from 80.298%
19250292619

push

github

web-flow
flag non-runnable targets used with `code_quality_tool` (#22875)

2 of 5 new or added lines in 2 files covered. (40.0%)

1487 existing lines in 72 files now uncovered.

71448 of 91759 relevant lines covered (77.86%)

3.22 hits per line

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

96.97
/src/python/pants/base/specs_parser.py
1
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
11✔
5

6
import os.path
11✔
7
from collections.abc import Iterable
11✔
8
from pathlib import Path, PurePath
11✔
9

10
from pants.base.build_environment import get_buildroot
11✔
11
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
11✔
12
from pants.base.specs import (
11✔
13
    AddressLiteralSpec,
14
    DirGlobSpec,
15
    DirLiteralSpec,
16
    FileGlobSpec,
17
    FileLiteralSpec,
18
    RawSpecs,
19
    RecursiveGlobSpec,
20
    Spec,
21
    Specs,
22
)
23
from pants.engine.internals import native_engine
11✔
24
from pants.util.frozendict import FrozenDict
11✔
25

26

27
class SpecsParser:
11✔
28
    """Parses specs as passed from the command line.
29

30
    See the `specs` module for more information on the types of objects returned.
31
    This class supports some flexibility in the path portion of the spec to allow for more natural
32
    command line use cases like tab completion leaving a trailing / for directories and relative
33
    paths, i.e. both of these::
34

35
      ./src/::
36
      /absolute/path/to/project/src/::
37

38
    Are valid command line specs even though they are not a valid BUILD file specs.  They're both
39
    normalized to::
40

41
      src::
42
    """
43

44
    class BadSpecError(Exception):
11✔
45
        """Indicates an unparseable command line selector."""
46

47
    def __init__(self, *, root_dir: str | None = None, working_dir: str | None = None) -> None:
11✔
48
        self._root_dir = os.path.realpath(root_dir or get_buildroot())
11✔
49
        self._working_dir = (
11✔
50
            os.path.relpath(os.path.join(self._root_dir, working_dir), self._root_dir)
51
            if working_dir
52
            else ""
53
        )
54
        if self._working_dir.startswith(".."):
11✔
55
            raise self.BadSpecError(
1✔
56
                f"Work directory {self._working_dir} escapes build root {self._root_dir}"
57
            )
58

59
    def _normalize_spec_path(self, path: str) -> str:
11✔
60
        is_abs = not path.startswith("//") and os.path.isabs(path)
11✔
61
        if is_abs:
11✔
62
            path = os.path.realpath(path)
1✔
63
            if os.path.commonprefix([self._root_dir, path]) != self._root_dir:
1✔
64
                raise self.BadSpecError(
1✔
65
                    f"Absolute spec path {path} does not share build root {self._root_dir}"
66
                )
67
        else:
68
            if path.startswith("//"):
11✔
69
                path = path[2:]
3✔
70
            elif self._working_dir:
11✔
71
                path = os.path.join(self._working_dir, path)
1✔
72
            path = os.path.join(self._root_dir, path)
11✔
73

74
        normalized = os.path.relpath(path, self._root_dir)
11✔
75
        if normalized.startswith(".."):
11✔
76
            raise self.BadSpecError(
1✔
77
                f"Relative spec path {path} escapes build root {self._root_dir}"
78
            )
79
        if normalized == ".":
11✔
80
            normalized = ""
7✔
81
        return normalized
11✔
82

83
    def parse_spec(self, spec: str) -> tuple[Spec, bool]:
11✔
84
        """Parse the given spec string and also return `true` if it's an ignore.
85

86
        :raises: CmdLineSpecParser.BadSpecError if the address selector could not be parsed.
87
        """
88
        is_ignore = False
11✔
89
        if spec.startswith("-"):
11✔
90
            is_ignore = True
1✔
91
            spec = spec[1:]
1✔
92

93
        (
11✔
94
            (
95
                path_component,
96
                target_component,
97
                generated_component,
98
                parameters,
99
            ),
100
            wildcard,
101
        ) = native_engine.address_spec_parse(spec)
102

103
        if wildcard == "::":
11✔
104
            return RecursiveGlobSpec(directory=self._normalize_spec_path(path_component)), is_ignore
6✔
105
        if wildcard == ":":
10✔
106
            return DirGlobSpec(directory=self._normalize_spec_path(path_component)), is_ignore
1✔
107
        if target_component or generated_component or parameters:
10✔
108
            return (
9✔
109
                AddressLiteralSpec(
110
                    path_component=self._normalize_spec_path(path_component),
111
                    target_component=target_component,
112
                    generated_component=generated_component,
113
                    parameters=FrozenDict(sorted(parameters)),
114
                ),
115
                is_ignore,
116
            )
117
        if "*" in path_component:
9✔
118
            return FileGlobSpec(spec), is_ignore
2✔
119
        if PurePath(spec).suffix:
8✔
120
            return FileLiteralSpec(self._normalize_spec_path(spec)), is_ignore
6✔
121
        spec_path = self._normalize_spec_path(spec)
3✔
122
        if spec_path == ".":
3✔
123
            return DirLiteralSpec(""), is_ignore
×
124
        # Some paths that look like dirs can actually be files without extensions.
125
        if Path(self._root_dir, spec_path).is_file():
3✔
126
            return FileLiteralSpec(spec_path), is_ignore
1✔
127
        return DirLiteralSpec(spec_path), is_ignore
3✔
128

129
    def parse_specs(
11✔
130
        self,
131
        specs: Iterable[str],
132
        *,
133
        description_of_origin: str,
134
        unmatched_glob_behavior: GlobMatchErrorBehavior = GlobMatchErrorBehavior.error,
135
    ) -> Specs:
136
        include_specs = []
11✔
137
        ignore_specs = []
11✔
138
        for spec_str in specs:
11✔
139
            spec, is_ignore = self.parse_spec(spec_str)
11✔
140
            if is_ignore:
11✔
UNCOV
141
                ignore_specs.append(spec)
×
142
            else:
143
                include_specs.append(spec)
11✔
144

145
        includes = RawSpecs.create(
11✔
146
            include_specs,
147
            description_of_origin=description_of_origin,
148
            unmatched_glob_behavior=unmatched_glob_behavior,
149
            filter_by_global_options=True,
150
        )
151
        ignores = RawSpecs.create(
11✔
152
            ignore_specs,
153
            description_of_origin=description_of_origin,
154
            unmatched_glob_behavior=unmatched_glob_behavior,
155
            # By setting the below to False, we will end up matching some targets
156
            # that cannot have been resolved by the include specs. For example, if the user runs
157
            # `--filter-target-type=my_tgt :: !dir::`, the ignores may match targets that are not
158
            # my_tgt. However, there also is no harm in over-matching with ignores. Setting
159
            # this to False (over-conservatively?) ensures that if a user says to ignore
160
            # something, we definitely do.
161
            filter_by_global_options=False,
162
        )
163
        return Specs(includes, ignores)
11✔
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