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

pantsbuild / pants / 18517631058

15 Oct 2025 04:18AM UTC coverage: 69.207% (-11.1%) from 80.267%
18517631058

Pull #22745

github

web-flow
Merge 642a76ca1 into 99919310e
Pull Request #22745: [windows] Add windows support in the stdio crate.

53815 of 77759 relevant lines covered (69.21%)

2.42 hits per line

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

81.82
/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
7✔
5

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

10
from pants.base.build_environment import get_buildroot
7✔
11
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
7✔
12
from pants.base.specs import (
7✔
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
7✔
24
from pants.util.frozendict import FrozenDict
7✔
25

26

27
class SpecsParser:
7✔
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):
7✔
45
        """Indicates an unparseable command line selector."""
46

47
    def __init__(self, *, root_dir: str | None = None, working_dir: str | None = None) -> None:
7✔
48
        self._root_dir = os.path.realpath(root_dir or get_buildroot())
7✔
49
        self._working_dir = (
7✔
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(".."):
7✔
55
            raise self.BadSpecError(
×
56
                f"Work directory {self._working_dir} escapes build root {self._root_dir}"
57
            )
58

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

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

83
    def parse_spec(self, spec: str) -> tuple[Spec, bool]:
7✔
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
7✔
89
        if spec.startswith("-"):
7✔
90
            is_ignore = True
×
91
            spec = spec[1:]
×
92

93
        (
7✔
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 == "::":
7✔
104
            return RecursiveGlobSpec(directory=self._normalize_spec_path(path_component)), is_ignore
3✔
105
        if wildcard == ":":
6✔
106
            return DirGlobSpec(directory=self._normalize_spec_path(path_component)), is_ignore
×
107
        if target_component or generated_component or parameters:
6✔
108
            return (
5✔
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:
6✔
118
            return FileGlobSpec(spec), is_ignore
1✔
119
        if PurePath(spec).suffix:
5✔
120
            return FileLiteralSpec(self._normalize_spec_path(spec)), is_ignore
3✔
121
        spec_path = self._normalize_spec_path(spec)
2✔
122
        if spec_path == ".":
2✔
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():
2✔
126
            return FileLiteralSpec(spec_path), is_ignore
×
127
        return DirLiteralSpec(spec_path), is_ignore
2✔
128

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

145
        includes = RawSpecs.create(
7✔
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(
7✔
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)
7✔
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