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

pantsbuild / pants / 18252174847

05 Oct 2025 01:36AM UTC coverage: 43.382% (-36.9%) from 80.261%
18252174847

push

github

web-flow
run tests on mac arm (#22717)

Just doing the minimal to pull forward the x86_64 pattern.

ref #20993

25776 of 59416 relevant lines covered (43.38%)

1.3 hits per line

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

52.0
/src/python/pants/core/util_rules/search_paths.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
from __future__ import annotations
3✔
4

5
import logging
3✔
6
import os
3✔
7
from collections.abc import Iterable
3✔
8
from dataclasses import dataclass
3✔
9
from pathlib import Path
3✔
10
from typing import Any, ClassVar
3✔
11

12
from pants.base.build_environment import get_buildroot
3✔
13
from pants.core.environments.target_types import EnvironmentTarget
3✔
14
from pants.engine.collection import DeduplicatedCollection
3✔
15
from pants.engine.env_vars import EnvironmentVars
3✔
16
from pants.engine.rules import Rule, _uncacheable_rule, collect_rules, rule
3✔
17
from pants.option.option_types import StrListOption
3✔
18
from pants.util.logging import LogLevel
3✔
19
from pants.util.memo import memoized_property
3✔
20
from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
3✔
21
from pants.util.strutil import help_text, softwrap
3✔
22

23
_logger = logging.getLogger(__name__)
3✔
24

25

26
@dataclass(frozen=True)
3✔
27
class ValidateSearchPathsRequest:
3✔
28
    env_tgt: EnvironmentTarget
3✔
29
    search_paths: tuple[str, ...]
3✔
30
    option_origin: str
3✔
31
    environment_key: str
3✔
32
    is_default: bool
3✔
33
    local_only: FrozenOrderedSet[str]
3✔
34

35

36
@dataclass(frozen=True)
3✔
37
class VersionManagerSearchPathsRequest:
3✔
38
    env_tgt: EnvironmentTarget
3✔
39
    root_dir: str | None
3✔
40
    tool_path: str
3✔
41
    option: str
3✔
42
    version_files: tuple[str, ...] = tuple()
3✔
43
    local_token: str | None = None
3✔
44

45

46
class VersionManagerSearchPaths(DeduplicatedCollection[str]):
3✔
47
    pass
3✔
48

49

50
@_uncacheable_rule
3✔
51
async def get_un_cachable_version_manager_paths(
3✔
52
    request: VersionManagerSearchPathsRequest,
53
) -> VersionManagerSearchPaths:
54
    """Inspects the directory of a version manager tool like pyenv or nvm to find installations."""
55
    if not request.env_tgt.can_access_local_system_paths:
×
56
        return VersionManagerSearchPaths()
×
57

58
    manager_root_dir = request.root_dir
×
59
    if not manager_root_dir:
×
60
        return VersionManagerSearchPaths()
×
61
    root_path = Path(manager_root_dir)
×
62
    if not root_path.exists():
×
63
        return VersionManagerSearchPaths()
×
64

65
    tool_versions_path = root_path / request.tool_path
×
66
    if not tool_versions_path.is_dir():
×
67
        return VersionManagerSearchPaths()
×
68

69
    if request.local_token and request.version_files:
×
70
        local_version_files = [Path(get_buildroot(), file) for file in request.version_files]
×
71
        first_version_file = next((file for file in local_version_files if file.exists()), None)
×
72
        if not first_version_file:
×
73
            file_string = ", ".join(f"`{file}`" for file in local_version_files)
×
74
            no_file = (
×
75
                f"No {file_string}" if len(local_version_files) == 1 else f"None of {file_string}"
76
            )
77
            _logger.warning(
×
78
                softwrap(
79
                    f"""
80
                    {no_file} found in the build root,
81
                    but {request.local_token} was set in `{request.option}`.
82
                    """
83
                )
84
            )
85
            return VersionManagerSearchPaths()
×
86

87
        _logger.info(
×
88
            f"Reading {first_version_file} to determine desired version for {request.option}."
89
        )
90
        local_version = first_version_file.read_text().strip()
×
91
        path = Path(tool_versions_path, local_version, "bin")
×
92
        if path.is_dir():
×
93
            return VersionManagerSearchPaths([str(path)])
×
94
        return VersionManagerSearchPaths()
×
95

96
    versions_in_dir = (
×
97
        tool_versions_path / version / "bin" for version in sorted(tool_versions_path.iterdir())
98
    )
99
    return VersionManagerSearchPaths(
×
100
        str(version) for version in versions_in_dir if version.is_dir()
101
    )
102

103

104
class ValidatedSearchPaths(FrozenOrderedSet):
3✔
105
    """Search paths that are valid for the current target environment."""
106

107

108
@rule(level=LogLevel.DEBUG)
3✔
109
async def validate_search_paths(request: ValidateSearchPathsRequest) -> ValidatedSearchPaths:
3✔
110
    """Checks for special search path strings, and errors if any are invalid for the environment.
111

112
    This will return:
113
    * The search paths, unaltered, for local/undefined environments, OR
114
    * The search paths, with invalid tokens removed, if the provided value was unaltered from the
115
      default value in the options system.
116
    * The search paths unaltered, if the search paths are all valid tokens for this environment
117

118
    If the environment is non-local and there are invalid tokens for those environments, raise
119
    `ValueError`.
120
    """
121

122
    env = request.env_tgt.val
×
123
    search_paths = request.search_paths
×
124

125
    if request.env_tgt.can_access_local_system_paths:
×
126
        return ValidatedSearchPaths(search_paths)
×
127
    assert env is not None, "Expected request.env_tgt to be defined"
×
128

129
    if request.is_default:
×
130
        # Strip out the not-allowed special strings from search_paths.
131
        # An error will occur on the off chance the non-local environment expects local_only tokens,
132
        # but there's nothing we can do here to detect it.
133
        return ValidatedSearchPaths(path for path in search_paths if path not in request.local_only)
×
134

135
    any_not_allowed = set(search_paths) & request.local_only
×
136
    if any_not_allowed:
×
137
        env_type = type(env)
×
138
        raise ValueError(
×
139
            softwrap(
140
                f"`{request.option_origin}` is configured to use local discovery "
141
                f"tools, which do not work in {env_type.__name__} runtime environments. To fix "
142
                f"this, set the value of `{request.environment_key}` in the `{env.alias}` "
143
                f"defined at `{env.address}` to contain only hardcoded paths or the `<PATH>` "
144
                "special string."
145
            )
146
        )
147

148
    return ValidatedSearchPaths(search_paths)
×
149

150

151
class ExecutableSearchPathsOptionMixin:
3✔
152
    env_vars_used_by_options: ClassVar[tuple[str, ...]] = ("PATH",)
3✔
153

154
    def __init_subclass__(cls, **kwargs: Any) -> None:
3✔
155
        if "PATH" not in cls.env_vars_used_by_options:
3✔
156
            raise ValueError(
×
157
                softwrap(
158
                    f"""
159
                    {ExecutableSearchPathsOptionMixin.__name__} depends on the PATH environment variable.
160

161
                    Please add it to the {cls.__name__}.env_vars_used_by_options.
162
                    """
163
                )
164
            )
165

166
    executable_search_paths_help: str
3✔
167
    _options_env: EnvironmentVars
3✔
168

169
    _executable_search_paths = StrListOption(
3✔
170
        default=["<PATH>"],
171
        help=lambda cls: help_text(
172
            f"""
173
            {cls.executable_search_paths_help}
174
            The special string `"<PATH>"` will expand to the contents of the PATH env var.
175
            """
176
        ),
177
        advanced=True,
178
        metavar="<binary-paths>",
179
    )
180

181
    @memoized_property
3✔
182
    def executable_search_path(self) -> tuple[str, ...]:
3✔
183
        def iter_path_entries():
×
184
            for entry in self._executable_search_paths:
×
185
                if entry == "<PATH>":
×
186
                    path = self._options_env.get("PATH")
×
187
                    if path:
×
188
                        yield from path.split(os.pathsep)
×
189
                else:
190
                    yield entry
×
191

192
        return tuple(OrderedSet(iter_path_entries()))
×
193

194

195
def rules() -> Iterable[Rule]:
3✔
196
    return collect_rules()
3✔
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