• 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

0.0
/src/python/pants/backend/visibility/rule_types.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
UNCOV
3
from __future__ import annotations
×
4

UNCOV
5
import itertools
×
UNCOV
6
import logging
×
UNCOV
7
import os.path
×
UNCOV
8
from collections.abc import Iterable, Iterator, Sequence
×
UNCOV
9
from dataclasses import dataclass, field
×
UNCOV
10
from pathlib import PurePath
×
UNCOV
11
from pprint import pformat
×
UNCOV
12
from typing import Any, cast
×
13

UNCOV
14
from pants.backend.visibility.glob import TargetGlob
×
UNCOV
15
from pants.engine.addresses import Address
×
UNCOV
16
from pants.engine.internals.dep_rules import (
×
17
    BuildFileDependencyRules,
18
    BuildFileDependencyRulesParserState,
19
    DependencyRuleAction,
20
    DependencyRuleApplication,
21
    DependencyRulesError,
22
)
UNCOV
23
from pants.engine.internals.target_adaptor import TargetAdaptor
×
UNCOV
24
from pants.util.strutil import softwrap
×
25

UNCOV
26
logger = logging.getLogger(__name__)
×
27

28

UNCOV
29
class BuildFileVisibilityRulesError(DependencyRulesError):
×
UNCOV
30
    @classmethod
×
UNCOV
31
    def create(
×
32
        cls,
33
        kind: str,
34
        rules: BuildFileVisibilityRules,
35
        ruleset: VisibilityRuleSet | None,
36
        origin_address: Address,
37
        origin_adaptor: TargetAdaptor,
38
        dependency_address: Address,
39
        dependency_adaptor: TargetAdaptor,
40
    ) -> BuildFileVisibilityRulesError:
41
        example = (
×
42
            pformat((tuple(map(str, ruleset.selectors)), *map(str, ruleset.rules), "!*"))
43
            if ruleset is not None
44
            else '(<target selector(s)>, <rule(s), ...>, "!*"),'
45
        )
46
        return cls(
×
47
            softwrap(
48
                f"""
49
                There is no matching rule from the `{kind}` defined in {rules.path} for the
50
                `{origin_adaptor.type_alias}` target {origin_address} for the dependency on the
51
                `{dependency_adaptor.type_alias}` target {dependency_address}
52

53
                Consider adding the required catch-all rule at the end of the rules spec.  Example
54
                adding a "deny all" at the end:
55

56
                  {example}
57
                """
58
            )
59
        )
60

61

UNCOV
62
@dataclass(frozen=True)
×
UNCOV
63
class VisibilityRule:
×
64
    """A single rule with an associated action when matched against a given path."""
65

UNCOV
66
    action: DependencyRuleAction
×
UNCOV
67
    glob: TargetGlob
×
68

UNCOV
69
    @classmethod
×
UNCOV
70
    def parse(
×
71
        cls,
72
        rule: str | dict,
73
        relpath: str,
74
    ) -> VisibilityRule:
75
        pattern: str | dict
UNCOV
76
        if isinstance(rule, str):
×
UNCOV
77
            if rule.startswith("!"):
×
UNCOV
78
                action = DependencyRuleAction.DENY
×
UNCOV
79
                pattern = rule[1:]
×
UNCOV
80
            elif rule.startswith("?"):
×
UNCOV
81
                action = DependencyRuleAction.WARN
×
UNCOV
82
                pattern = rule[1:]
×
83
            else:
UNCOV
84
                action = DependencyRuleAction.ALLOW
×
UNCOV
85
                pattern = rule
×
UNCOV
86
        elif isinstance(rule, dict):
×
UNCOV
87
            action = DependencyRuleAction(rule.get("action", "allow"))
×
UNCOV
88
            pattern = rule
×
89
        else:
90
            raise ValueError(f"invalid visibility rule: {rule!r}")
×
UNCOV
91
        return cls(action, TargetGlob.parse(pattern, relpath))
×
92

UNCOV
93
    def match(self, address: Address, adaptor: TargetAdaptor, relpath: str) -> bool:
×
UNCOV
94
        return self.glob.match(address, adaptor, relpath)
×
95

UNCOV
96
    def __str__(self) -> str:
×
UNCOV
97
        prefix = ""
×
UNCOV
98
        if self.action is DependencyRuleAction.DENY:
×
UNCOV
99
            prefix = "!"
×
UNCOV
100
        elif self.action is DependencyRuleAction.WARN:
×
UNCOV
101
            prefix = "?"
×
UNCOV
102
        return f"{prefix}{self.glob}"
×
103

104

UNCOV
105
def flatten(xs, *types: type) -> Iterator:
×
106
    """Return an iterator with values, regardless of the nesting of the input."""
UNCOV
107
    assert types
×
UNCOV
108
    if str in types and isinstance(xs, str):
×
UNCOV
109
        yield from (x.strip() for x in xs.splitlines())
×
UNCOV
110
    elif isinstance(xs, types):
×
UNCOV
111
        yield xs
×
UNCOV
112
    elif isinstance(xs, Iterable):
×
UNCOV
113
        yield from itertools.chain.from_iterable(flatten(x, *types) for x in xs)
×
UNCOV
114
    elif isinstance(xs, PurePath):
×
UNCOV
115
        yield str(xs)
×
116
    elif type(xs).__name__ == "Registrar":
×
117
        yield f"<{xs}>"
×
118
    else:
119
        raise ValueError(f"expected {' or '.join(typ.__name__ for typ in types)} but got: {xs!r}")
×
120

121

UNCOV
122
@dataclass(frozen=True)
×
UNCOV
123
class VisibilityRuleSet:
×
124
    """An ordered set of rules that applies to some set of target types."""
125

UNCOV
126
    build_file: str
×
UNCOV
127
    selectors: tuple[TargetGlob, ...]
×
UNCOV
128
    rules: tuple[VisibilityRule, ...]
×
129

UNCOV
130
    def __post_init__(self) -> None:
×
UNCOV
131
        if all("!*" == str(selector) for selector in self.selectors):
×
UNCOV
132
            rules = tuple(map(str, self.rules))
×
UNCOV
133
            raise BuildFileVisibilityRulesError(
×
134
                softwrap(
135
                    f"""
136
                    The rule set will never apply to anything, for the rules: {rules}
137

138
                    At least one target selector must have a filtering rule that can match
139
                    something, for example:
140

141
                        ("<python_*>", {rules})
142
                    """
143
                )
144
            )
145

UNCOV
146
    @classmethod
×
UNCOV
147
    def parse(cls, build_file: str, arg: Any) -> VisibilityRuleSet:
×
148
        """Translate input `arg` from BUILD file call.
149

150
        The arg is a rule spec tuple with two or more elements, where the first is the target
151
        selector(s) and the rest are target rules.
152
        """
UNCOV
153
        if not isinstance(arg, Sequence) or isinstance(arg, str) or len(arg) < 2:
×
154
            raise ValueError(
×
155
                "Invalid rule spec, expected (<target selector(s)>, <rule(s)>, <rule(s)>, ...) "
156
                f"but got: {arg!r}"
157
            )
158

UNCOV
159
        relpath = os.path.dirname(build_file)
×
UNCOV
160
        try:
×
UNCOV
161
            selectors = cast("Iterator[str | dict]", flatten(arg[0], str, dict))
×
UNCOV
162
            rules = cast("Iterator[str | dict]", flatten(arg[1:], str, dict))
×
UNCOV
163
            return cls(
×
164
                build_file,
165
                tuple(TargetGlob.parse(selector, relpath) for selector in selectors),
166
                tuple(
167
                    VisibilityRule.parse(rule, relpath)
168
                    for rule in rules
169
                    if not cls._noop_rule(rule)
170
                ),
171
            )
UNCOV
172
        except ValueError as e:
×
173
            raise ValueError(f"Invalid rule spec, {e}") from e
×
174

UNCOV
175
    def __str__(self) -> str:
×
UNCOV
176
        return self.build_file
×
177

UNCOV
178
    def peek(self) -> tuple[str, ...]:
×
179
        return tuple(map(str, self.rules))
×
180

UNCOV
181
    @staticmethod
×
UNCOV
182
    def _noop_rule(rule: str | dict) -> bool:
×
UNCOV
183
        return not rule or isinstance(rule, str) and rule.startswith("#")
×
184

UNCOV
185
    def match(self, address: Address, adaptor: TargetAdaptor, relpath: str) -> bool:
×
UNCOV
186
        return any(selector.match(address, adaptor, relpath) for selector in self.selectors)
×
187

188

UNCOV
189
@dataclass(frozen=True)
×
UNCOV
190
class BuildFileVisibilityRules(BuildFileDependencyRules):
×
UNCOV
191
    path: str
×
UNCOV
192
    rulesets: tuple[VisibilityRuleSet, ...]
×
193

UNCOV
194
    @staticmethod
×
UNCOV
195
    def create_parser_state(
×
196
        path: str, parent: BuildFileDependencyRules | None
197
    ) -> BuildFileDependencyRulesParserState:
198
        return BuildFileVisibilityRulesParserState(path, cast(BuildFileVisibilityRules, parent))
×
199

UNCOV
200
    @classmethod
×
UNCOV
201
    def check_dependency_rules(
×
202
        cls,
203
        *,
204
        origin_address: Address,
205
        origin_adaptor: TargetAdaptor,
206
        dependencies_rules: BuildFileDependencyRules | None,
207
        dependency_address: Address,
208
        dependency_adaptor: TargetAdaptor,
209
        dependents_rules: BuildFileDependencyRules | None,
210
    ) -> DependencyRuleApplication:
211
        """Check all rules for any that apply to the relation between the two targets.
212

213
        The `__dependencies_rules__` are the rules applicable for the origin target.
214
        The `__dependents_rules__` are the rules applicable for the dependency target.
215

216
        Return dependency rule application describing the resulting action to take: ALLOW, DENY or
217
        WARN. WARN is effectively the same as ALLOW, but with a logged warning.
218
        """
219
        # We can safely cast the `dependencies_rules` and `dependents_rules` here as they're the
220
        # same type as the class being used to call `check_dependency_rules()`.
221

222
        # Check outgoing dependency action
UNCOV
223
        out_ruleset, out_action, out_pattern = (
×
224
            cast(BuildFileVisibilityRules, dependencies_rules).get_action(
225
                address=origin_address,
226
                adaptor=origin_adaptor,
227
                other_address=dependency_address,
228
                other_adaptor=dependency_adaptor,
229
            )
230
            if dependencies_rules is not None
231
            else (None, DependencyRuleAction.ALLOW, None)
232
        )
UNCOV
233
        if out_action is None:
×
234
            raise BuildFileVisibilityRulesError.create(
×
235
                kind="__dependencies_rules__",
236
                rules=cast(BuildFileVisibilityRules, dependencies_rules),
237
                ruleset=out_ruleset,
238
                origin_address=origin_address,
239
                origin_adaptor=origin_adaptor,
240
                dependency_address=dependency_address,
241
                dependency_adaptor=dependency_adaptor,
242
            )
243

244
        # Check incoming dependency action
UNCOV
245
        in_ruleset, in_action, in_pattern = (
×
246
            cast(BuildFileVisibilityRules, dependents_rules).get_action(
247
                address=dependency_address,
248
                adaptor=dependency_adaptor,
249
                other_address=origin_address,
250
                other_adaptor=origin_adaptor,
251
            )
252
            if dependents_rules is not None
253
            else (None, DependencyRuleAction.ALLOW, None)
254
        )
UNCOV
255
        if in_action is None:
×
256
            raise BuildFileVisibilityRulesError.create(
×
257
                kind="__dependents_rules__",
258
                rules=cast(BuildFileVisibilityRules, dependents_rules),
259
                ruleset=in_ruleset,
260
                origin_address=origin_address,
261
                origin_adaptor=origin_adaptor,
262
                dependency_address=dependency_address,
263
                dependency_adaptor=dependency_adaptor,
264
            )
UNCOV
265
        if in_action is DependencyRuleAction.DENY or out_action is DependencyRuleAction.ALLOW:
×
UNCOV
266
            action = in_action
×
267
        else:
UNCOV
268
            action = out_action
×
UNCOV
269
        source_rule = f"{out_ruleset}[{out_pattern}]" if out_ruleset else origin_address.spec_path
×
UNCOV
270
        target_rule = f"{in_ruleset}[{in_pattern}]" if in_ruleset else dependency_address.spec_path
×
UNCOV
271
        return DependencyRuleApplication(
×
272
            action=action,
273
            rule_description=f"{source_rule} -> {target_rule}",
274
            origin_address=origin_address,
275
            origin_type=origin_adaptor.type_alias,
276
            dependency_address=dependency_address,
277
            dependency_type=dependency_adaptor.type_alias,
278
        )
279

UNCOV
280
    @staticmethod
×
UNCOV
281
    def _get_address_relpath(address: Address) -> str:
×
UNCOV
282
        if address.is_file_target:
×
UNCOV
283
            return os.path.dirname(address.filename)
×
UNCOV
284
        return address.spec_path
×
285

UNCOV
286
    @staticmethod
×
UNCOV
287
    def _get_address_path(address: Address) -> str:
×
UNCOV
288
        return TargetGlob.address_path(address)
×
289

UNCOV
290
    def get_action(
×
291
        self,
292
        address: Address,
293
        adaptor: TargetAdaptor,
294
        other_address: Address,
295
        other_adaptor: TargetAdaptor,
296
    ) -> tuple[VisibilityRuleSet | None, DependencyRuleAction | None, str | None]:
297
        """Get applicable rule for target type from `path`.
298

299
        The rules are declared in `relpath`.
300
        """
UNCOV
301
        relpath = self._get_address_relpath(address)
×
UNCOV
302
        ruleset = self.get_ruleset(address, adaptor, relpath)
×
UNCOV
303
        if ruleset is None:
×
304
            return None, None, None
×
UNCOV
305
        for visibility_rule in ruleset.rules:
×
UNCOV
306
            if visibility_rule.match(other_address, other_adaptor, relpath):
×
UNCOV
307
                if visibility_rule.action != DependencyRuleAction.ALLOW:
×
UNCOV
308
                    path = self._get_address_path(other_address)
×
UNCOV
309
                    logger.debug(
×
310
                        softwrap(
311
                            f"""
312
                            {visibility_rule.action.name}: type={adaptor.type_alias}
313
                            address={address} [{relpath}] other={other_address} [{path}]
314
                            rule={str(visibility_rule)!r} {self.path}:
315
                            {", ".join(map(str, ruleset.rules))}
316
                            """
317
                        )
318
                    )
UNCOV
319
                return ruleset, visibility_rule.action, str(visibility_rule)
×
320
        return ruleset, None, None
×
321

UNCOV
322
    def get_ruleset(
×
323
        self, address: Address, target: TargetAdaptor, relpath: str | None = None
324
    ) -> VisibilityRuleSet | None:
UNCOV
325
        if relpath is None:
×
326
            relpath = self._get_address_relpath(address)
×
UNCOV
327
        for ruleset in self.rulesets:
×
UNCOV
328
            if ruleset.match(address, target, relpath):
×
UNCOV
329
                return ruleset
×
330
        return None
×
331

332

UNCOV
333
@dataclass
×
UNCOV
334
class BuildFileVisibilityRulesParserState(BuildFileDependencyRulesParserState):
×
UNCOV
335
    path: str
×
UNCOV
336
    parent: BuildFileVisibilityRules | None
×
UNCOV
337
    rulesets: list[VisibilityRuleSet] = field(default_factory=list)
×
338

UNCOV
339
    def get_frozen_dependency_rules(self) -> BuildFileDependencyRules | None:
×
340
        if not self.rulesets:
×
341
            return self.parent
×
342
        else:
343
            return BuildFileVisibilityRules(self.path, tuple(self.rulesets))
×
344

UNCOV
345
    def set_dependency_rules(
×
346
        self,
347
        build_file: str,
348
        *args,
349
        extend: bool = False,
350
        **kwargs,
351
    ) -> None:
352
        if self.rulesets:
×
353
            raise BuildFileVisibilityRulesError(
×
354
                softwrap(
355
                    """
356
                    There must be at most one each of the `__dependencies_rules__()` /
357
                    `__dependents_rules__()` declarations per BUILD file.
358

359
                    To declare multiple rule sets, simply provide them in a single call.
360

361
                    Example:
362

363
                      __dependencies_rules__(
364
                        (
365
                           (files, resources),
366
                           "//resources/**",
367
                           "//files/**",
368
                           "!*",
369
                        ),
370
                        (
371
                          python_sources,
372
                          "//src/**",
373
                          "!*",
374
                        ),
375
                        ("*", "*"),
376
                      )
377
                    """
378
                )
379
            )
380

381
        try:
×
382
            self.rulesets = [VisibilityRuleSet.parse(build_file, arg) for arg in args if arg]
×
383
            self.path = build_file
×
384
        except ValueError as e:
×
385
            raise BuildFileVisibilityRulesError(str(e)) from e
×
386

387
        if extend and self.parent:
×
388
            self.rulesets.extend(self.parent.rulesets)
×
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