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

pantsbuild / pants / 26080722777

19 May 2026 06:37AM UTC coverage: 52.106% (-11.5%) from 63.597%
26080722777

Pull #23250

github

web-flow
Merge 63ec06323 into 2693df832
Pull Request #23250: Feature: Add generic option to docker image

12 of 50 new or added lines in 3 files covered. (24.0%)

5382 existing lines in 201 files now uncovered.

32053 of 61515 relevant lines covered (52.11%)

1.04 hits per line

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

85.26
/src/python/pants/build_graph/build_configuration.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
2✔
5

6
import logging
2✔
7
from collections import defaultdict
2✔
8
from collections.abc import Callable, Iterable
2✔
9
from dataclasses import dataclass, field
2✔
10
from enum import Enum
2✔
11
from typing import Any, DefaultDict
2✔
12

13
from pants.backend.project_info.filter_targets import FilterSubsystem
2✔
14
from pants.build_graph.build_file_aliases import BuildFileAliases
2✔
15
from pants.core.environments.subsystems import EnvironmentsSubsystem
2✔
16
from pants.engine.goal import GoalSubsystem
2✔
17
from pants.engine.rules import Rule, RuleIndex, collect_rules, rule
2✔
18
from pants.engine.target import Target
2✔
19
from pants.engine.unions import UnionRule
2✔
20
from pants.ng.goal import GoalSubsystemNg
2✔
21
from pants.option.alias import CliOptions
2✔
22
from pants.option.global_options import GlobalOptions
2✔
23
from pants.option.scope import OptionsParsingSettings, ScopeInfo, normalize_scope
2✔
24
from pants.option.subsystem import Subsystem
2✔
25
from pants.util.frozendict import FrozenDict
2✔
26
from pants.util.ordered_set import FrozenOrderedSet
2✔
27
from pants.vcs.changed import Changed
2✔
28

29
logger = logging.getLogger(__name__)
2✔
30

31

32
# No goal or target_type can have a name from this set, so that `./pants help <name>`
33
# is unambiguous.
34
_RESERVED_NAMES = {
2✔
35
    "api-types",
36
    "backends",
37
    "global",
38
    "goals",
39
    "subsystems",
40
    "symbols",
41
    "targets",
42
    "tools",
43
}
44

45

46
# Subsystems used outside of any rule.
47
_GLOBAL_SUBSYSTEMS: set[type[Subsystem]] = {
2✔
48
    GlobalOptions,
49
    Changed,
50
    CliOptions,
51
    FilterSubsystem,
52
    EnvironmentsSubsystem,
53
}
54

55

56
@dataclass(frozen=True)
2✔
57
class BuildConfiguration:
2✔
58
    """Stores the types and helper functions exposed to BUILD files."""
59

60
    registered_aliases: BuildFileAliases
2✔
61
    subsystem_to_providers: FrozenDict[type[Subsystem], tuple[str, ...]]
2✔
62
    target_type_to_providers: FrozenDict[type[Target], tuple[str, ...]]
2✔
63
    rule_to_providers: FrozenDict[Rule, tuple[str, ...]]
2✔
64
    union_rule_to_providers: FrozenDict[UnionRule, tuple[str, ...]]
2✔
65
    allow_unknown_options: bool
2✔
66
    remote_auth_plugin_func: Callable | None
2✔
67

68
    @property
2✔
69
    def all_subsystems(self) -> tuple[type[Subsystem], ...]:
2✔
70
        """Return all subsystems in the system: global and those registered via rule usage."""
71
        # Sort by options_scope, for consistency.
72
        return tuple(
2✔
73
            sorted(
74
                _GLOBAL_SUBSYSTEMS | self.subsystem_to_providers.keys(),
75
                key=lambda x: x.options_scope,
76
            )
77
        )
78

79
    @property
2✔
80
    def known_scope_infos(self) -> tuple[ScopeInfo, ...]:
2✔
81
        return tuple(subsystem.get_scope_info() for subsystem in self.all_subsystems)
2✔
82

83
    @property
2✔
84
    def target_types(self) -> tuple[type[Target], ...]:
2✔
85
        return tuple(sorted(self.target_type_to_providers.keys(), key=lambda x: x.alias))
2✔
86

87
    @property
2✔
88
    def rules(self) -> FrozenOrderedSet[Rule]:
2✔
89
        return FrozenOrderedSet(self.rule_to_providers.keys())
2✔
90

91
    @property
2✔
92
    def union_rules(self) -> FrozenOrderedSet[UnionRule]:
2✔
93
        return FrozenOrderedSet(self.union_rule_to_providers.keys())
2✔
94

95
    def __post_init__(self) -> None:
2✔
96
        class Category(Enum):
2✔
97
            goal = "goal"
2✔
98
            reserved_name = "reserved name"
2✔
99
            subsystem = "subsystem"
2✔
100
            target_type = "target type"
2✔
101

102
        name_to_categories: DefaultDict[str, set[Category]] = defaultdict(set)
2✔
103
        normalized_to_orig_name: dict[str, str] = {}
2✔
104

105
        for opt in self.all_subsystems:
2✔
106
            scope = opt.options_scope
2✔
107
            normalized_scope = normalize_scope(scope)
2✔
108
            name_to_categories[normalized_scope].add(
2✔
109
                Category.goal if issubclass(opt, GoalSubsystem) else Category.subsystem
110
            )
111
            normalized_to_orig_name[normalized_scope] = scope
2✔
112
        for tgt_type in self.target_types:
2✔
113
            name_to_categories[normalize_scope(tgt_type.alias)].add(Category.target_type)
2✔
114
        for reserved_name in _RESERVED_NAMES:
2✔
115
            name_to_categories[normalize_scope(reserved_name)].add(Category.reserved_name)
2✔
116

117
        found_collision = False
2✔
118
        for name, cats in name_to_categories.items():
2✔
119
            if len(cats) > 1:
2✔
120
                scats = sorted(cat.value for cat in cats)
×
121
                cats_str = ", ".join(f"a {cat}" for cat in scats[:-1]) + f" and a {scats[-1]}."
×
122
                colliding_names = "`/`".join(
×
123
                    sorted({name, normalized_to_orig_name.get(name, name)})
124
                )
125
                logger.error(f"Naming collision: `{colliding_names}` is registered as {cats_str}")
×
126
                found_collision = True
×
127

128
        if found_collision:
2✔
129
            raise TypeError("Found naming collisions. See log for details.")
×
130

131
    @dataclass
2✔
132
    class Builder:
2✔
133
        _exposed_object_by_alias: dict[Any, Any] = field(default_factory=dict)
2✔
134
        _exposed_context_aware_object_factory_by_alias: dict[Any, Any] = field(default_factory=dict)
2✔
135
        _subsystem_to_providers: dict[type[Subsystem], list[str]] = field(
2✔
136
            default_factory=lambda: defaultdict(list)
137
        )
138
        _target_type_to_providers: dict[type[Target], list[str]] = field(
2✔
139
            default_factory=lambda: defaultdict(list)
140
        )
141
        _rule_to_providers: dict[Rule, list[str]] = field(default_factory=lambda: defaultdict(list))
2✔
142
        _union_rule_to_providers: dict[UnionRule, list[str]] = field(
2✔
143
            default_factory=lambda: defaultdict(list)
144
        )
145
        _allow_unknown_options: bool = False
2✔
146
        _remote_auth_plugin: Callable | None = None
2✔
147
        _pants_ng: bool = False
2✔
148

149
        def registered_aliases(self) -> BuildFileAliases:
2✔
150
            """Return the registered aliases exposed in BUILD files.
151

152
            These returned aliases aren't so useful for actually parsing BUILD files.
153
            They are useful for generating online documentation.
154

155
            :returns: A new BuildFileAliases instance containing this BuildConfiguration's
156
                      registered alias mappings.
157
            """
158
            return BuildFileAliases(
×
159
                objects=self._exposed_object_by_alias.copy(),
160
                context_aware_object_factories=self._exposed_context_aware_object_factory_by_alias.copy(),
161
            )
162

163
        def register_aliases(self, aliases):
2✔
164
            """Registers the given aliases to be exposed in parsed BUILD files.
165

166
            :param aliases: The BuildFileAliases to register.
167
            :type aliases: :class:`pants.build_graph.build_file_aliases.BuildFileAliases`
168
            """
169
            if not isinstance(aliases, BuildFileAliases):
2✔
170
                raise TypeError(f"The aliases must be a BuildFileAliases, given {aliases}")
×
171

172
            for alias, obj in aliases.objects.items():
2✔
173
                self._register_exposed_object(alias, obj)
2✔
174

175
            for (
2✔
176
                alias,
177
                context_aware_object_factory,
178
            ) in aliases.context_aware_object_factories.items():
179
                self._register_exposed_context_aware_object_factory(
×
180
                    alias, context_aware_object_factory
181
                )
182

183
        def _register_exposed_object(self, alias, obj):
2✔
184
            if alias in self._exposed_object_by_alias:
2✔
UNCOV
185
                logger.debug(f"Object alias {alias} has already been registered. Overwriting!")
×
186

187
            self._exposed_object_by_alias[alias] = obj
2✔
188

189
        def _register_exposed_context_aware_object_factory(
2✔
190
            self, alias, context_aware_object_factory
191
        ):
192
            if alias in self._exposed_context_aware_object_factory_by_alias:
×
193
                logger.debug(
×
194
                    f"This context aware object factory alias {alias} has already been registered. "
195
                    "Overwriting!"
196
                )
197

198
            self._exposed_context_aware_object_factory_by_alias[alias] = (
×
199
                context_aware_object_factory
200
            )
201

202
        def register_subsystems(
2✔
203
            self, plugin_or_backend: str, subsystems: Iterable[type[Subsystem]]
204
        ):
205
            """Registers the given subsystem types."""
206
            if not isinstance(subsystems, Iterable):
2✔
207
                raise TypeError(f"The subsystems must be an iterable, given {subsystems}")
×
208
            subsystems = tuple(subsystems)
2✔
209
            if not subsystems:
2✔
210
                return
2✔
211

212
            invalid_subsystems = [
2✔
213
                s for s in subsystems if not isinstance(s, type) or not issubclass(s, Subsystem)
214
            ]
215
            if invalid_subsystems:
2✔
216
                raise TypeError(
×
217
                    "The following items from the given subsystems are not Subsystems "
218
                    "subclasses:\n\t{}".format("\n\t".join(str(i) for i in invalid_subsystems))
219
                )
220

221
            for subsystem in subsystems:
2✔
222
                if (
2✔
223
                    issubclass(subsystem, GoalSubsystem)
224
                    and issubclass(subsystem, GoalSubsystemNg) != self._pants_ng
225
                ):
226
                    # Don't register goal subsystems that aren't pertinent to our og/ng state.
227
                    # This allows GoalSubsystemNgs to use the same scopes as og GoalSubsystems
228
                    # without collision. Note that the @rules that consume these subsystems
229
                    # can still be registered, but the subsystems cannot be configured.
230
                    continue
×
231
                self._subsystem_to_providers[subsystem].append(plugin_or_backend)
2✔
232

233
        def register_rules(self, plugin_or_backend: str, rules: Iterable[Rule | UnionRule]):
2✔
234
            """Registers the given rules."""
235
            if not isinstance(rules, Iterable):
2✔
236
                raise TypeError(f"The rules must be an iterable, given {rules!r}")
×
237

238
            # "Index" the rules to normalize them and expand their dependencies.
239
            rule_index = RuleIndex.create(rules)
2✔
240
            rules_and_queries: tuple[Rule, ...] = (*rule_index.rules, *rule_index.queries)
2✔
241
            for _rule in rules_and_queries:
2✔
242
                self._rule_to_providers[_rule].append(plugin_or_backend)
2✔
243
            for union_rule in rule_index.union_rules:
2✔
244
                self._union_rule_to_providers[union_rule].append(plugin_or_backend)
2✔
245
            self.register_subsystems(
2✔
246
                plugin_or_backend,
247
                (
248
                    _rule.output_type
249
                    for _rule in rules_and_queries
250
                    if issubclass(_rule.output_type, Subsystem)
251
                ),
252
            )
253

254
        # NB: We expect the parameter to be Iterable[Type[Target]], but we can't be confident in
255
        # this because we pass whatever people put in their `register.py`s to this function;
256
        # I.e., this is an impure function that reads from the outside world. So, we use the type
257
        # hint `Any` and perform runtime type checking.
258
        def register_target_types(
2✔
259
            self, plugin_or_backend: str, target_types: Iterable[type[Target]] | Any
260
        ) -> None:
261
            """Registers the given target types."""
262
            if not isinstance(target_types, Iterable):
2✔
263
                raise TypeError(
×
264
                    f"The entrypoint `target_types` must return an iterable. "
265
                    f"Given {repr(target_types)}"
266
                )
267
            bad_elements = [
2✔
268
                tgt_type
269
                for tgt_type in target_types
270
                if not isinstance(tgt_type, type) or not issubclass(tgt_type, Target)
271
            ]
272
            if bad_elements:
2✔
273
                raise TypeError(
×
274
                    "Every element of the entrypoint `target_types` must be a subclass of "
275
                    f"{Target.__name__}. Bad elements: {bad_elements}."
276
                )
277
            for target_type in target_types:
2✔
278
                self._target_type_to_providers[target_type].append(plugin_or_backend)
2✔
279
                # Access the Target.PluginField here to ensure the PluginField class is
280
                # created before the UnionMembership is instantiated, as the class hierarchy is
281
                # walked during union membership setup.
282
                _ = target_type.PluginField
2✔
283

284
        def register_remote_auth_plugin(self, remote_auth_plugin: Callable) -> None:
2✔
285
            self._remote_auth_plugin = remote_auth_plugin
×
286

287
        def register_auxiliary_goals(self, plugin_or_backend: str, auxiliary_goals: Iterable[type]):
2✔
288
            """Registers the given auxiliary goals."""
289
            if not isinstance(auxiliary_goals, Iterable):
2✔
290
                raise TypeError(
×
291
                    f"The entrypoint `auxiliary_goals` must return an iterable. "
292
                    f"Given {repr(auxiliary_goals)}"
293
                )
294
            # Import `AuxiliaryGoal` here to avoid import cycle.
295
            from pants.goal.auxiliary_goal import AuxiliaryGoal
2✔
296

297
            bad_elements = [goal for goal in auxiliary_goals if not issubclass(goal, AuxiliaryGoal)]
2✔
298
            if bad_elements:
2✔
299
                raise TypeError(
×
300
                    "Every element of the entrypoint `auxiliary_goals` must be a subclass of "
301
                    f"{AuxiliaryGoal.__name__}. Bad elements: {bad_elements}."
302
                )
303
            self.register_subsystems(plugin_or_backend, auxiliary_goals)
2✔
304

305
        def allow_unknown_options(self, allow: bool = True) -> None:
2✔
306
            """Allows overriding whether Options parsing will fail for unrecognized Options.
307

308
            Used to defer options failures while bootstrapping BuildConfiguration until after the
309
            complete set of plugins is known.
310
            """
UNCOV
311
            self._allow_unknown_options = True
×
312

313
        def create(self) -> BuildConfiguration:
2✔
314
            registered_aliases = BuildFileAliases(
2✔
315
                objects=self._exposed_object_by_alias.copy(),
316
                context_aware_object_factories=self._exposed_context_aware_object_factory_by_alias.copy(),
317
            )
318
            return BuildConfiguration(
2✔
319
                registered_aliases=registered_aliases,
320
                subsystem_to_providers=FrozenDict(
321
                    (k, tuple(v)) for k, v in self._subsystem_to_providers.items()
322
                ),
323
                target_type_to_providers=FrozenDict(
324
                    (k, tuple(v)) for k, v in self._target_type_to_providers.items()
325
                ),
326
                rule_to_providers=FrozenDict(
327
                    (k, tuple(v)) for k, v in self._rule_to_providers.items()
328
                ),
329
                union_rule_to_providers=FrozenDict(
330
                    (k, tuple(v)) for k, v in self._union_rule_to_providers.items()
331
                ),
332
                allow_unknown_options=self._allow_unknown_options,
333
                remote_auth_plugin_func=self._remote_auth_plugin,
334
            )
335

336

337
@rule
2✔
338
async def get_full_options_parsing_settings(
2✔
339
    build_config: BuildConfiguration,
340
) -> OptionsParsingSettings:
341
    return OptionsParsingSettings(
2✔
342
        build_config.known_scope_infos,
343
        build_config.allow_unknown_options,
344
    )
345

346

347
def rules():
2✔
348
    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

© 2026 Coveralls, Inc