• 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

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).
3
from __future__ import annotations
×
4

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

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

26
logger = logging.getLogger(__name__)
×
27

28

29
class BuildFileVisibilityRulesError(DependencyRulesError):
×
30
    @classmethod
×
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

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

66
    action: DependencyRuleAction
×
67
    glob: TargetGlob
×
68

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

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

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

104

105
def flatten(xs, *types: type) -> Iterator:
×
106
    """Return an iterator with values, regardless of the nesting of the input."""
107
    assert types
×
108
    if str in types and isinstance(xs, str):
×
109
        yield from (x.strip() for x in xs.splitlines())
×
110
    elif isinstance(xs, types):
×
111
        yield xs
×
112
    elif isinstance(xs, Iterable):
×
113
        yield from itertools.chain.from_iterable(flatten(x, *types) for x in xs)
×
114
    elif isinstance(xs, PurePath):
×
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

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

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

130
    def __post_init__(self) -> None:
×
131
        if all("!*" == str(selector) for selector in self.selectors):
×
132
            rules = tuple(map(str, self.rules))
×
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

146
    @classmethod
×
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
        """
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

159
        relpath = os.path.dirname(build_file)
×
160
        try:
×
161
            selectors = cast("Iterator[str | dict]", flatten(arg[0], str, dict))
×
162
            rules = cast("Iterator[str | dict]", flatten(arg[1:], str, dict))
×
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
            )
172
        except ValueError as e:
×
173
            raise ValueError(f"Invalid rule spec, {e}") from e
×
174

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

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

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

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

188

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

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

200
    @classmethod
×
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
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
        )
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
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
        )
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
            )
265
        if in_action is DependencyRuleAction.DENY or out_action is DependencyRuleAction.ALLOW:
×
266
            action = in_action
×
267
        else:
268
            action = out_action
×
269
        source_rule = f"{out_ruleset}[{out_pattern}]" if out_ruleset else origin_address.spec_path
×
270
        target_rule = f"{in_ruleset}[{in_pattern}]" if in_ruleset else dependency_address.spec_path
×
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

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

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

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
        """
301
        relpath = self._get_address_relpath(address)
×
302
        ruleset = self.get_ruleset(address, adaptor, relpath)
×
303
        if ruleset is None:
×
304
            return None, None, None
×
305
        for visibility_rule in ruleset.rules:
×
306
            if visibility_rule.match(other_address, other_adaptor, relpath):
×
307
                if visibility_rule.action != DependencyRuleAction.ALLOW:
×
308
                    path = self._get_address_path(other_address)
×
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
                    )
319
                return ruleset, visibility_rule.action, str(visibility_rule)
×
320
        return ruleset, None, None
×
321

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

332

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

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

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

© 2026 Coveralls, Inc