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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

22.73
/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
1✔
5

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

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

26

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

47
    def __init__(self, *, root_dir: str | None = None, working_dir: str | None = None) -> None:
1✔
UNCOV
48
        self._root_dir = os.path.realpath(root_dir or get_buildroot())
×
UNCOV
49
        self._working_dir = (
×
50
            os.path.relpath(os.path.join(self._root_dir, working_dir), self._root_dir)
51
            if working_dir
52
            else ""
53
        )
UNCOV
54
        if self._working_dir.startswith(".."):
×
UNCOV
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:
1✔
UNCOV
60
        is_abs = not path.startswith("//") and os.path.isabs(path)
×
UNCOV
61
        if is_abs:
×
UNCOV
62
            path = os.path.realpath(path)
×
UNCOV
63
            if os.path.commonprefix([self._root_dir, path]) != self._root_dir:
×
UNCOV
64
                raise self.BadSpecError(
×
65
                    f"Absolute spec path {path} does not share build root {self._root_dir}"
66
                )
67
        else:
UNCOV
68
            if path.startswith("//"):
×
UNCOV
69
                path = path[2:]
×
UNCOV
70
            elif self._working_dir:
×
UNCOV
71
                path = os.path.join(self._working_dir, path)
×
UNCOV
72
            path = os.path.join(self._root_dir, path)
×
73

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

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

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

UNCOV
103
        if wildcard == "::":
×
UNCOV
104
            return RecursiveGlobSpec(directory=self._normalize_spec_path(path_component)), is_ignore
×
UNCOV
105
        if wildcard == ":":
×
UNCOV
106
            return DirGlobSpec(directory=self._normalize_spec_path(path_component)), is_ignore
×
UNCOV
107
        if target_component or generated_component or parameters:
×
UNCOV
108
            return (
×
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
            )
UNCOV
117
        if "*" in path_component:
×
UNCOV
118
            return FileGlobSpec(spec), is_ignore
×
UNCOV
119
        if PurePath(spec).suffix:
×
UNCOV
120
            return FileLiteralSpec(self._normalize_spec_path(spec)), is_ignore
×
UNCOV
121
        spec_path = self._normalize_spec_path(spec)
×
UNCOV
122
        if spec_path == ".":
×
123
            return DirLiteralSpec(""), is_ignore
×
124
        # Some paths that look like dirs can actually be files without extensions.
UNCOV
125
        if Path(self._root_dir, spec_path).is_file():
×
UNCOV
126
            return FileLiteralSpec(spec_path), is_ignore
×
UNCOV
127
        return DirLiteralSpec(spec_path), is_ignore
×
128

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

UNCOV
145
        includes = RawSpecs.create(
×
146
            include_specs,
147
            description_of_origin=description_of_origin,
148
            unmatched_glob_behavior=unmatched_glob_behavior,
149
            filter_by_global_options=True,
150
        )
UNCOV
151
        ignores = RawSpecs.create(
×
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
        )
UNCOV
163
        return Specs(includes, ignores)
×
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