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

pantsbuild / pants / 22206482340

20 Feb 2026 12:44AM UTC coverage: 80.376% (+0.05%) from 80.324%
22206482340

Pull #23066

github

web-flow
Merge ddfa8ddd7 into 56952304a
Pull Request #23066: Extract JavaScript backend test resources to files

73 of 73 new or added lines in 2 files covered. (100.0%)

1093 existing lines in 48 files now uncovered.

78880 of 98139 relevant lines covered (80.38%)

3.35 hits per line

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

86.21
/src/python/pants/option/subsystem.py
1
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
12✔
5

6
import functools
12✔
7
import inspect
12✔
8
import re
12✔
9
from abc import ABCMeta
12✔
10
from collections.abc import Callable, Iterable, Sequence
12✔
11
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast
12✔
12

13
from pants.core.util_rules.env_vars import environment_vars_subset
12✔
14
from pants.engine.env_vars import EnvironmentVars, EnvironmentVarsRequest
12✔
15
from pants.engine.internals.options_parsing import scope_options
12✔
16
from pants.engine.internals.selectors import AwaitableConstraints
12✔
17
from pants.engine.rules import implicitly
12✔
18
from pants.engine.unions import UnionMembership, UnionRule, distinct_union_type_per_subclass
12✔
19
from pants.option.errors import OptionsError
12✔
20
from pants.option.option_types import OptionInfo, collect_options_info
12✔
21
from pants.option.option_value_container import OptionValueContainer
12✔
22
from pants.option.options import Options
12✔
23
from pants.option.scope import Scope, ScopedOptions, ScopeInfo, normalize_scope
12✔
24
from pants.util.frozendict import FrozenDict
12✔
25
from pants.util.strutil import softwrap
12✔
26

27
if TYPE_CHECKING:
28
    # Needed to avoid an import cycle.
29
    from pants.core.environments.target_types import EnvironmentTarget
30
    from pants.engine.rules import Rule
31

32
_SubsystemT = TypeVar("_SubsystemT", bound="Subsystem")
12✔
33

34

35
class _SubsystemMeta(ABCMeta):
12✔
36
    """Metaclass to link inner `EnvironmentAware` class with the enclosing subsystem."""
37

38
    def __init__(self, name, bases, namespace, **k):
12✔
39
        super().__init__(name, bases, namespace, **k)
12✔
40
        if (
12✔
41
            not (name == "Subsystem" and bases == ())
42
            and self.EnvironmentAware is not Subsystem.EnvironmentAware
43
        ):
44
            # Only `EnvironmentAware` subclasses should be linked to their enclosing scope
45
            if Subsystem.EnvironmentAware not in self.EnvironmentAware.__bases__:
12✔
46
                # Allow for `self.EnvironmentAware` to not need to explicitly derive from
47
                # `Subsystem.EnvironmentAware` (saving needless repetitive typing)
48
                self.EnvironmentAware = type(
12✔
49
                    "EnvironmentAware",
50
                    (
51
                        self.EnvironmentAware,
52
                        Subsystem.EnvironmentAware,
53
                        *self.EnvironmentAware.__bases__,
54
                    ),
55
                    {},
56
                )
57
            # A marker that allows initialization code to check for EnvironmentAware subclasses
58
            # without having to import Subsystem.EnvironmentAware and use issubclass(), which
59
            # can cause a dependency cycle.
60
            self.EnvironmentAware.__subsystem_environment_aware__ = True
12✔
61

62
            self.EnvironmentAware.subsystem = self
12✔
63

64

65
class Subsystem(metaclass=_SubsystemMeta):
12✔
66
    """A separable piece of functionality that may be reused across multiple tasks or other code.
67

68
    Subsystems encapsulate the configuration and initialization of things like JVMs,
69
    Python interpreters, SCMs and so on.
70

71
    Set the `help` class property with a description, which will be used in `./pants help`. For the
72
    best rendering, use soft wrapping (e.g. implicit string concatenation) within paragraphs, but
73
    hard wrapping (`\n`) to separate distinct paragraphs and/or lists.
74
    """
75

76
    # A marker that allows initialization code to check for Subsystem subclasses without
77
    # having to import Subsystem and use issubclass(), which can cause a dependency cycle.
78
    __subsystem__ = True
12✔
79

80
    options_scope: str
12✔
81
    help: ClassVar[str | Callable[[], str]]
12✔
82

83
    # Subclasses may override these to specify a deprecated former name for this Subsystem's scope.
84
    # Option values can be read from the deprecated scope, but a deprecation warning will be issued.
85
    # The deprecation warning becomes an error at the given Pants version (which must therefore be
86
    # a valid semver).
87
    deprecated_options_scope: str | None = None
12✔
88
    deprecated_options_scope_removal_version: str | None = None
12✔
89

90
    # // Note: must be aligned with the regex in src/rust/options/src/id.rs.
91
    _scope_name_re = re.compile(r"^(?:[a-z0-9_])+(?:-(?:[a-z0-9_])+)*$")
12✔
92

93
    _rules: ClassVar[Sequence[Rule] | None] = None
12✔
94

95
    class EnvironmentAware(metaclass=ABCMeta):
12✔
96
        """A separate container for options that may be redefined by the runtime environment.
97

98
        To define environment-aware options, create an inner class in the `Subsystem` called
99
        `EnvironmentAware`. Option fields share their scope with their enclosing `Subsystem`,
100
        and the values of fields will default to the values set through Pants' configuration.
101

102
        To consume environment-aware options, inject the `EnvironmentAware` inner class into
103
        your rule.
104

105
        Optionally, it is possible to specify environment variables that are required when
106
        post-processing raw values provided by users (e.g. `<PATH>` special strings) by specifying
107
        `env_vars_used_by_options`, and consuming `_options_env` in your post-processing property.
108
        These environment variables will be requested at construction time.
109
        """
110

111
        subsystem: ClassVar[type[Subsystem]]
12✔
112
        env_vars_used_by_options: ClassVar[tuple[str, ...]] = ()
12✔
113

114
        options: OptionValueContainer
12✔
115
        env_tgt: EnvironmentTarget
12✔
116
        _options_env: EnvironmentVars = EnvironmentVars()
12✔
117

118
        def __getattribute__(self, __name: str) -> Any:
12✔
119
            from pants.core.environments.rules import resolve_environment_sensitive_option
1✔
120

121
            # Will raise an `AttributeError` if the attribute is not defined.
122
            # MyPy should stop that from ever happening.
123
            default = super().__getattribute__(__name)
1✔
124

125
            # Check to see whether there's a definition of this attribute at the class level.
126
            # If it returns `default` then the attribute on the instance is the same object
127
            # as defined at the class, or the attribute does not exist on the class,
128
            # and we don't really need to go any further.
129
            v = getattr(type(self), __name, default)
1✔
130
            if v is default:
1✔
131
                return default
1✔
132

133
            # Resolving an attribute on the class object will return the underlying descriptor.
134
            # If the descriptor is an `OptionInfo`, we can resolve it against the environment
135
            # target.
136
            if isinstance(v, OptionInfo):
×
137
                # If the value is not defined in the `EnvironmentTarget`, return the value
138
                # from the options system.
139
                override = resolve_environment_sensitive_option(v.args[0], self)
×
140
                return override if override is not None else default
×
141

142
            # We should just return the default at this point.
143
            return default
×
144

145
        def _is_default(self, __name: str) -> bool:
12✔
146
            """Returns true if the value of the named option is unchanged from the default."""
147
            from pants.core.environments.rules import resolve_environment_sensitive_option
×
148

149
            v = getattr(type(self), __name)
×
150
            assert isinstance(v, OptionInfo)
×
151

152
            return (
×
153
                # vars beginning with `_` are exposed as option names with the leading `_` stripped
154
                self.options.is_default(__name.lstrip("_"))
155
                and resolve_environment_sensitive_option(v.args[0], self) is None
156
            )
157

158
    @classmethod
12✔
159
    def rules(cls: Any) -> Iterable[Rule]:
12✔
160
        # NB: This avoids using `memoized_classmethod` until its interaction with `mypy` can be improved.
161
        if cls._rules is None:
12✔
162
            from pants.core.environments.rules import add_option_fields_for
12✔
163
            from pants.engine.rules import Rule
12✔
164

165
            # nb. `rules` needs to be memoized so that repeated calls to add these rules
166
            # return exactly the same rule objects. As such, returning this generator
167
            # directly won't work, because the iterator needs to be replayable.
168
            def inner() -> Iterable[Rule]:
12✔
169
                yield cls._construct_subsystem_rule()
12✔
170
                if cls.EnvironmentAware is not Subsystem.EnvironmentAware:
12✔
171
                    yield cls._construct_env_aware_rule()
12✔
172
                    yield from (cast(Rule, i) for i in add_option_fields_for(cls.EnvironmentAware))
12✔
173

174
            cls._rules = tuple(inner())
12✔
175
        return cast("Sequence[Rule]", cls._rules)
12✔
176

177
    @distinct_union_type_per_subclass
12✔
178
    class PluginOption:
12✔
179
        pass
12✔
180

181
    @classmethod
12✔
182
    def register_plugin_options(cls, options_container: type) -> UnionRule:
12✔
183
        """Register additional options on the subsystem.
184

185
        In the `rules()` register.py entry-point, include `OtherSubsystem.register_plugin_options(<OptionsContainer>)`.
186
        `<OptionsContainer>` should be a type with option class attributes, similar to how they are
187
        defined for subsystems.
188

189
        This will register the option as a first-class citizen.
190
        Plugins can use this new option like any other.
191
        """
192
        return UnionRule(cls.PluginOption, options_container)
6✔
193

194
    @classmethod
12✔
195
    def _construct_subsystem_rule(cls) -> Rule:
12✔
196
        """Returns a `TaskRule` that will construct the target Subsystem."""
197

198
        # Global-level imports are conditional, we need to re-import here for runtime use
199
        from pants.engine.rules import TaskRule
12✔
200

201
        partial_construct_subsystem: Any = functools.partial(_construct_subsystem, cls)
12✔
202

203
        # NB: We must populate several dunder methods on the partial function because partial
204
        # functions do not have these defined by default and the engine uses these values to
205
        # visualize functions in error messages and the rule graph.
206
        snake_scope = normalize_scope(cls.options_scope)
12✔
207
        name = f"construct_scope_{snake_scope}"
12✔
208
        partial_construct_subsystem.__name__ = name
12✔
209
        partial_construct_subsystem.__module__ = cls.__module__
12✔
210
        partial_construct_subsystem.__doc__ = cls.help
12✔
211

212
        _, class_definition_lineno = inspect.getsourcelines(cls)
12✔
213
        partial_construct_subsystem.__line_number__ = class_definition_lineno
12✔
214

215
        return TaskRule(
12✔
216
            output_type=cls,
217
            parameters=FrozenDict(),
218
            awaitables=(
219
                AwaitableConstraints(
220
                    rule_id="pants.engine.internals.options_parsing.scope_options",
221
                    output_type=ScopedOptions,
222
                    explicit_args_arity=1,
223
                    # NB: For a call-by-name, the input_types are the explicit ones provided
224
                    # in a map passed to **implicitly(), and our call to this rule uses a
225
                    # positional arg and an empty **implicitly(), so input_types are empty here.
226
                    input_types=tuple(),
227
                ),
228
            ),
229
            masked_types=(),
230
            func=partial_construct_subsystem,
231
            canonical_name=name,
232
        )
233

234
    @classmethod
12✔
235
    def _construct_env_aware_rule(cls) -> Rule:
12✔
236
        """Returns a `TaskRule` that will construct the target Subsystem.EnvironmentAware."""
237
        # Global-level imports are conditional, we need to re-import here for runtime use
238
        from pants.core.environments.target_types import EnvironmentTarget
12✔
239
        from pants.engine.rules import TaskRule
12✔
240

241
        snake_scope = normalize_scope(cls.options_scope)
12✔
242
        name = f"construct_env_aware_scope_{snake_scope}"
12✔
243

244
        # placate the rule graph visualizer.
245
        @functools.wraps(_construct_env_aware)
12✔
246
        async def inner(*a, **k):
12✔
UNCOV
247
            return await _construct_env_aware(*a, **k)
×
248

249
        inner.__line_number__ = 0  # type: ignore[attr-defined]
12✔
250

251
        return TaskRule(
12✔
252
            output_type=cls.EnvironmentAware,
253
            parameters=FrozenDict({"subsystem_instance": cls, "env_tgt": EnvironmentTarget}),
254
            awaitables=(
255
                AwaitableConstraints(
256
                    rule_id="pants.core.util_rules.env_vars.environment_vars_subset",
257
                    output_type=EnvironmentVars,
258
                    explicit_args_arity=1,
259
                    # NB: For a call-by-name, the input_types are the explicit ones provided
260
                    # in a map passed to **implicitly(), and our call to this rule uses a
261
                    # positional arg and an empty **implicitly(), so input_types are empty here.
262
                    input_types=tuple(),
263
                ),
264
            ),
265
            masked_types=(),
266
            func=inner,
267
            canonical_name=name,
268
        )
269

270
    @classmethod
12✔
271
    def is_valid_scope_name(cls, s: str) -> bool:
12✔
272
        return s == "" or (cls._scope_name_re.match(s) is not None and s != "pants")
12✔
273

274
    @classmethod
12✔
275
    def validate_scope(cls) -> None:
12✔
276
        options_scope = getattr(cls, "options_scope", None)
12✔
277
        if options_scope is None:
12✔
278
            raise OptionsError(f"{cls.__name__} must set options_scope.")
1✔
279
        if not cls.is_valid_scope_name(options_scope):
12✔
UNCOV
280
            raise OptionsError(
×
281
                softwrap(
282
                    f"""
283
                    Options scope "{options_scope}" is not valid.
284

285
                    Replace in code with a new scope name consisting of only lower-case letters,
286
                    digits, underscores, and non-consecutive dashes.
287
                    """
288
                )
289
            )
290

291
    @classmethod
12✔
292
    def create_scope_info(cls, **scope_info_kwargs) -> ScopeInfo:
12✔
293
        """One place to create scope info, to allow subclasses to inject custom scope args."""
294
        return ScopeInfo(**scope_info_kwargs)
12✔
295

296
    @classmethod
12✔
297
    def get_scope_info(cls) -> ScopeInfo:
12✔
298
        """Returns a ScopeInfo instance representing this Subsystem's options scope."""
299
        cls.validate_scope()
12✔
300
        return cls.create_scope_info(scope=cls.options_scope, subsystem_cls=cls)
12✔
301

302
    @classmethod
12✔
303
    def register_options_on_scope(cls, options: Options, union_membership: UnionMembership):
12✔
304
        """Trigger registration of this Subsystem's options.
305

306
        Subclasses should not generally need to override this method.
307
        """
308

309
        def register(*args, **kwargs):
12✔
310
            options.register(cls.options_scope, *args, **kwargs)
12✔
311

312
        plugin_option_containers = union_membership.get(cls.PluginOption)
12✔
313
        for options_info in collect_options_info(cls):
12✔
314
            register(*options_info.args, **options_info.kwargs)
12✔
315
        for options_info in collect_options_info(cls.EnvironmentAware):
12✔
316
            register(*options_info.args, environment_aware=True, **options_info.kwargs)
12✔
317
        for options_info in (
12✔
318
            option
319
            for container in plugin_option_containers
320
            for option in collect_options_info(container)
321
        ):
322
            register(*options_info.args, **options_info.kwargs)
4✔
323

324
    def __init__(self, options: OptionValueContainer) -> None:
12✔
325
        self.validate_scope()
12✔
326
        self.options = options
12✔
327

328
    def __eq__(self, other: Any) -> bool:
12✔
UNCOV
329
        if type(self) != type(other):  # noqa: E721
×
UNCOV
330
            return False
×
331
        return bool(self.options == other.options)
×
332

333

334
async def _construct_subsystem(subsystem_typ: type[_SubsystemT]) -> _SubsystemT:
12✔
335
    scoped_options = await scope_options(Scope(str(subsystem_typ.options_scope)), **implicitly())
1✔
336
    return subsystem_typ(scoped_options.options)
1✔
337

338

339
async def _construct_env_aware(
12✔
340
    subsystem_instance: _SubsystemT,
341
    env_tgt: EnvironmentTarget,
342
) -> Subsystem.EnvironmentAware:
UNCOV
343
    t: Subsystem.EnvironmentAware = type(subsystem_instance).EnvironmentAware()
×
344
    # `_SubSystemMeta` metaclass should ensure that `EnvironmentAware` actually subclasses
345
    # `EnvironmentAware`, but if an implementer does something egregious, it's best we
346
    # catch it.
UNCOV
347
    assert isinstance(t, Subsystem.EnvironmentAware)
×
348

349
    t.options = subsystem_instance.options
×
UNCOV
350
    t.env_tgt = env_tgt
×
351

352
    if t.env_vars_used_by_options:
×
UNCOV
353
        t._options_env = await environment_vars_subset(
×
354
            EnvironmentVarsRequest(t.env_vars_used_by_options), **implicitly()
355
        )
356

UNCOV
357
    return t
×
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