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

pantsbuild / pants / 19381742489

15 Nov 2025 12:52AM UTC coverage: 49.706% (-30.6%) from 80.29%
19381742489

Pull #22890

github

web-flow
Merge d961abf79 into 42e1ebd41
Pull Request #22890: Updated all python subsystem constraints to 3.14

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

14659 existing lines in 485 files now uncovered.

31583 of 63540 relevant lines covered (49.71%)

0.79 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
2✔
4

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

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

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

25

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

35

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

45

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

49

50
@_uncacheable_rule
2✔
51
async def get_un_cachable_version_manager_paths(
2✔
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):
2✔
105
    """Search paths that are valid for the current target environment."""
106

107

108
@rule(level=LogLevel.DEBUG)
2✔
109
async def validate_search_paths(request: ValidateSearchPathsRequest) -> ValidatedSearchPaths:
2✔
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

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

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

UNCOV
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.
UNCOV
133
        return ValidatedSearchPaths(path for path in search_paths if path not in request.local_only)
×
134

UNCOV
135
    any_not_allowed = set(search_paths) & request.local_only
×
UNCOV
136
    if any_not_allowed:
×
UNCOV
137
        env_type = type(env)
×
UNCOV
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

UNCOV
148
    return ValidatedSearchPaths(search_paths)
×
149

150

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

154
    def __init_subclass__(cls, **kwargs: Any) -> None:
2✔
155
        if "PATH" not in cls.env_vars_used_by_options:
2✔
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
2✔
167
    _options_env: EnvironmentVars
2✔
168

169
    _executable_search_paths = StrListOption(
2✔
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
2✔
182
    def executable_search_path(self) -> tuple[str, ...]:
2✔
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]:
2✔
196
    return collect_rules()
2✔
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