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

pantsbuild / pants / 25441711719

06 May 2026 02:31PM UTC coverage: 92.915%. Remained the same
25441711719

push

github

web-flow
use sha pin (with comment) format for generated actions (#23312)

Per the GitHub Action best practices we recently enabled at #23249, we
should pin each action to a SHA so that the reference is actually
immutable.

This will -- I hope -- knock out a large chunk of the 421 alerts we
currently get from zizmor. The next followup would then be upgrades and
harmonizing the generated and none-generated pins.

Notice: This idea was suggested by Claude while going over pinact output
and I was surprised to see that post processing the yaml wasn't too
gross.

92206 of 99237 relevant lines covered (92.91%)

4.04 hits per line

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

97.26
/src/python/pants/engine/rules.py
1
# Copyright 2016 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 inspect
12✔
7
import sys
12✔
8
from collections.abc import Callable, Coroutine, Iterable, Mapping
12✔
9
from dataclasses import dataclass
12✔
10
from enum import Enum
12✔
11
from types import FrameType, ModuleType
12✔
12
from typing import (
12✔
13
    Any,
14
    NotRequired,
15
    Protocol,
16
    TypedDict,
17
    TypeVar,
18
    Unpack,
19
    cast,
20
    get_type_hints,
21
    overload,
22
)
23

24
from typing_extensions import ParamSpec
12✔
25

26
from pants.engine.engine_aware import SideEffecting
12✔
27
from pants.engine.internals.native_engine import Call, RuleCallTrampoline
12✔
28
from pants.engine.internals.rule_visitor import collect_awaitables
12✔
29
from pants.engine.internals.selectors import AwaitableConstraints
12✔
30
from pants.engine.internals.selectors import concurrently as concurrently  # noqa: F401
12✔
31
from pants.engine.unions import UnionRule
12✔
32
from pants.util.frozendict import FrozenDict
12✔
33
from pants.util.logging import LogLevel
12✔
34
from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
12✔
35
from pants.util.strutil import softwrap
12✔
36

37
PANTS_RULES_MODULE_KEY = "__pants_rules__"
12✔
38

39

40
def implicitly(*args) -> dict[str, Any]:
12✔
41
    # NB: This function does not have a `TypedDict` return type, because the `@rule` decorator
42
    # cannot adjust the type of the `@rule` function to include a keyword argument (keyword
43
    # arguments are not supported by PEP-612).
44
    return {"__implicitly": args}
12✔
45

46

47
class RuleType(Enum):
12✔
48
    rule = "rule"
12✔
49
    goal_rule = "goal_rule"
12✔
50
    uncacheable_rule = "_uncacheable_rule"
12✔
51

52

53
P = ParamSpec("P")
12✔
54
R = TypeVar("R")
12✔
55
SyncRuleT = Callable[P, R]
12✔
56
AsyncRuleT = Callable[P, Coroutine[Any, Any, R]]
12✔
57
RuleDecorator = Callable[[SyncRuleT | AsyncRuleT], Callable[P, Call[R]]]
12✔
58

59

60
def _make_rule(
12✔
61
    func_id: str,
62
    rule_type: RuleType,
63
    return_type: type[Any],
64
    parameter_types: dict[str, type[Any]],
65
    masked_types: Iterable[type[Any]],
66
    *,
67
    cacheable: bool,
68
    polymorphic: bool,
69
    canonical_name: str,
70
    desc: str | None,
71
    level: LogLevel,
72
) -> RuleDecorator:
73
    """A @decorator that declares that a particular static function may be used as a TaskRule.
74

75
    :param rule_type: The specific decorator used to declare the rule.
76
    :param return_type: The return/output type for the Rule. This must be a concrete Python type.
77
    :param parameter_types: A sequence of types that matches the number and order of arguments to
78
                            the decorated function.
79
    :param cacheable: Whether the results of executing the Rule should be cached as keyed by all of
80
                      its inputs.
81
    :param polymorphic: Whether the rule is an abstract base method for polymorphic dispatch via
82
                        a union type.
83
    """
84

85
    is_goal_cls = getattr(return_type, "__goal__", False)
12✔
86
    if rule_type == RuleType.rule and is_goal_cls:
12✔
87
        raise TypeError(
1✔
88
            "An `@rule` that returns a `Goal` must instead be declared with `@goal_rule`."
89
        )
90
    if rule_type == RuleType.goal_rule and not is_goal_cls:
12✔
91
        raise TypeError("An `@goal_rule` must return a subclass of `engine.goal.Goal`.")
1✔
92

93
    def wrapper(original_func):
12✔
94
        if not inspect.isfunction(original_func):
12✔
95
            raise ValueError("The @rule decorator must be applied innermost of all decorators.")
×
96

97
        # Set our own custom `__line_number__` dunder so that the engine may visualize the line number.
98
        original_func.__line_number__ = original_func.__code__.co_firstlineno
12✔
99
        original_func.rule_id = canonical_name
12✔
100

101
        awaitables = FrozenOrderedSet(collect_awaitables(original_func))
12✔
102

103
        validate_requirements(func_id, parameter_types, awaitables, cacheable)
12✔
104

105
        # NB: The named definition of the rule ends up wrapped in a trampoline to handle
106
        # implicit arguments for direct by-name calls. The `TaskRule` takes a reference to
107
        # the original unwrapped function, which avoids the need for a special protocol when
108
        # the engine invokes a @rule under memoization.
109
        task_rule = TaskRule(
12✔
110
            return_type,
111
            FrozenDict(parameter_types),
112
            awaitables,
113
            masked_types,
114
            original_func,
115
            canonical_name=canonical_name,
116
            desc=desc,
117
            level=level,
118
            cacheable=cacheable,
119
            polymorphic=polymorphic,
120
        )
121
        return cast(
12✔
122
            Callable[P, Call[R]],
123
            RuleCallTrampoline(canonical_name, return_type, original_func, task_rule),
124
        )
125

126
    return wrapper
12✔
127

128

129
class InvalidTypeAnnotation(TypeError):
12✔
130
    """Indicates an incorrect type annotation for an `@rule`."""
131

132

133
class UnrecognizedRuleArgument(TypeError):
12✔
134
    """Indicates an unrecognized keyword argument to a `@rule`."""
135

136

137
class MissingTypeAnnotation(TypeError):
12✔
138
    """Indicates a missing type annotation for an `@rule`."""
139

140

141
class MissingReturnTypeAnnotation(InvalidTypeAnnotation):
12✔
142
    """Indicates a missing return type annotation for an `@rule`."""
143

144

145
class MissingParameterTypeAnnotation(InvalidTypeAnnotation):
12✔
146
    """Indicates a missing parameter type annotation for an `@rule`."""
147

148

149
class DuplicateRuleError(TypeError):
12✔
150
    """Invalid to overwrite `@rule`s using the same name in the same module."""
151

152

153
def _ensure_type_annotation(
12✔
154
    *,
155
    type_annotation: type[Any] | None,
156
    name: str,
157
    raise_type: type[InvalidTypeAnnotation],
158
) -> type[Any]:
159
    if type_annotation is None:
12✔
160
        raise raise_type(f"{name} is missing a type annotation.")
1✔
161
    if not isinstance(type_annotation, type):
12✔
162
        raise raise_type(
1✔
163
            f"The annotation for {name} must be a type, got {type_annotation} of type {type(type_annotation)}."
164
        )
165
    return type_annotation
12✔
166

167

168
PUBLIC_RULE_DECORATOR_ARGUMENTS = {
12✔
169
    "canonical_name",
170
    "canonical_name_suffix",
171
    "desc",
172
    "level",
173
    "polymorphic",
174
}
175
# We aren't sure if these'll stick around or be removed at some point, so they are "private"
176
# and should only be used in Pants' codebase.
177
PRIVATE_RULE_DECORATOR_ARGUMENTS = {
12✔
178
    # Allows callers to override the type Pants will use for the params listed.
179
    #
180
    # It is assumed (but not enforced) that the provided type is a subclass of the annotated type.
181
    # (We assume but not enforce since this is likely to be used with unions, which has the same
182
    # assumption between the union base and its members).
183
    "_param_type_overrides",
184
    # Allows callers to prevent the given list of types from being included in the identity of
185
    # a @rule. Although the type may be in scope for callers, it will not be consumable in the
186
    # `@rule` which declares the type masked.
187
    "_masked_types",
188
}
189
# We don't want @rule-writers to use 'rule_type' or 'cacheable' as kwargs directly,
190
# but rather set them implicitly based on the rule annotation.
191
# So we leave it out of PUBLIC_RULE_DECORATOR_ARGUMENTS.
192
IMPLICIT_PRIVATE_RULE_DECORATOR_ARGUMENTS = {"rule_type", "cacheable"}
12✔
193

194

195
class RuleDecoratorKwargs(TypedDict):
12✔
196
    """Public-facing @rule kwargs used in the codebase."""
197

198
    canonical_name: NotRequired[str]
12✔
199

200
    canonical_name_suffix: NotRequired[str]
12✔
201

202
    desc: NotRequired[str]
12✔
203
    """The rule's description as it appears in stacktraces/debugging. For goal rules, defaults to the goal name."""
12✔
204

205
    level: NotRequired[LogLevel]
12✔
206
    """The logging level applied to this rule. Defaults to TRACE."""
12✔
207

208
    polymorphic: NotRequired[bool]
12✔
209
    """Whether this rule represents an abstract method for a union.
210

211
    A polymorphic rule can only be called by name, and must have a single input type that is a
212
    union base type (plus other non-union arguments as needed). Execution will be dispatched to the
213
    @rule with the same signature with the union base type replaced by one of its member types.
214

215
    E.g., given
216

217
    ```
218
    @rule(polymorphic=True)
219
    async def base_rule(arg: UnionBase, other_arg: OtherType) -> OutputType
220
        ...
221

222
    @rule(polymorphic=True)
223
    async def derived_rule(arg: UnionMember, other_arg: OtherType) -> OutputType
224
       ...
225

226
    ```
227

228
    And an arg of type UnionMember, then
229

230
    `await base_rule(arg, other_arg)`
231

232
    will invoke `derived_rule(arg, other_arg)`
233
    """
234

235
    _masked_types: NotRequired[Iterable[type[Any]]]
12✔
236
    """Unstable. Internal Pants usage only."""
12✔
237

238
    _param_type_overrides: NotRequired[dict[str, type[Any]]]
12✔
239
    """Unstable. Internal Pants usage only."""
12✔
240

241

242
class _RuleDecoratorKwargs(RuleDecoratorKwargs):
12✔
243
    """Internal/Implicit @rule kwargs (not for use outside rules.py)"""
244

245
    rule_type: RuleType
12✔
246
    """The decorator used to declare the rule (see rules.py:_make_rule(...))"""
12✔
247

248
    cacheable: bool
12✔
249
    """Whether the results of this rule should be cached.
12✔
250
    Typically true for rules, false for goal_rules (see rules.py:_make_rule(...))
251
    """
252

253

254
def rule_decorator(
12✔
255
    func: SyncRuleT | AsyncRuleT, **kwargs: Unpack[_RuleDecoratorKwargs]
256
) -> Callable[P, Call[R]]:
257
    if not inspect.isfunction(func):
12✔
258
        raise ValueError("The @rule decorator expects to be placed on a function.")
×
259

260
    if (
12✔
261
        len(
262
            set(kwargs)
263
            - PUBLIC_RULE_DECORATOR_ARGUMENTS
264
            - PRIVATE_RULE_DECORATOR_ARGUMENTS
265
            - IMPLICIT_PRIVATE_RULE_DECORATOR_ARGUMENTS
266
        )
267
        != 0
268
    ):
269
        raise UnrecognizedRuleArgument(
1✔
270
            f"`@rule`s and `@goal_rule`s only accept the following keyword arguments: {PUBLIC_RULE_DECORATOR_ARGUMENTS}"
271
        )
272

273
    rule_type = kwargs["rule_type"]
12✔
274
    cacheable = kwargs["cacheable"]
12✔
275
    polymorphic = kwargs.get("polymorphic", False)
12✔
276
    masked_types: tuple[type, ...] = tuple(kwargs.get("_masked_types", ()))
12✔
277
    param_type_overrides: dict[str, type] = kwargs.get("_param_type_overrides", {})
12✔
278

279
    func_id = f"@rule {func.__module__}:{func.__name__}"
12✔
280
    type_hints = get_type_hints(func)
12✔
281
    return_type = _ensure_type_annotation(
12✔
282
        type_annotation=type_hints.get("return"),
283
        name=f"{func_id} return",
284
        raise_type=MissingReturnTypeAnnotation,
285
    )
286

287
    func_params = inspect.signature(func).parameters
12✔
288
    for parameter in param_type_overrides:
12✔
289
        if parameter not in func_params:
12✔
290
            raise ValueError(
1✔
291
                f"Unknown parameter name in `param_type_overrides`: {parameter}."
292
                + f" Parameter names: '{', '.join(func_params)}'"
293
            )
294

295
    parameter_types = {
12✔
296
        parameter: _ensure_type_annotation(
297
            type_annotation=param_type_overrides.get(parameter, type_hints.get(parameter)),
298
            name=f"{func_id} parameter {parameter}",
299
            raise_type=MissingParameterTypeAnnotation,
300
        )
301
        for parameter in func_params
302
    }
303
    is_goal_cls = getattr(return_type, "__goal__", False)
12✔
304

305
    # Set a default canonical name if one is not explicitly provided to the module and name of the
306
    # function that implements it, plus an optional suffix. This is used as the workunit name.
307
    # The suffix is a convenient way to disambiguate multiple rules registered dynamically from the
308
    # same static code (by overriding the inferred param types in the @rule decorator).
309
    # TODO: It is not yet clear how dynamically registered rules whose names are generated
310
    #  with a suffix will work in practice with the new call-by-name semantics.
311
    #  For now the suffix serves to ensure unique names. Whether they are useful is another matter.
312
    suffix = kwargs.get("canonical_name_suffix", "")
12✔
313
    effective_name = kwargs.get(
12✔
314
        "canonical_name",
315
        f"{func.__module__}.{func.__qualname__}{('_' + suffix) if suffix else ''}".replace(
316
            ".<locals>", ""
317
        ),
318
    )
319

320
    # Set a default description, which is used in the dynamic UI and stacktraces.
321
    effective_desc = kwargs.get("desc")
12✔
322
    if effective_desc is None and is_goal_cls:
12✔
323
        effective_desc = f"`{return_type.name}` goal"
12✔
324

325
    effective_level = kwargs.get("level", LogLevel.TRACE)
12✔
326
    if not isinstance(effective_level, LogLevel):  # type: ignore[unused-ignore]
12✔
327
        raise ValueError(
×
328
            "Expected to receive a value of type LogLevel for the level "
329
            f"argument, but got: {effective_level}"
330
        )
331

332
    module = sys.modules[func.__module__]
12✔
333
    pants_rules = getattr(module, PANTS_RULES_MODULE_KEY, None)
12✔
334
    if pants_rules is None:
12✔
335
        pants_rules = {}
12✔
336
        setattr(module, PANTS_RULES_MODULE_KEY, pants_rules)
12✔
337

338
    if effective_name not in pants_rules:
12✔
339
        pants_rules[effective_name] = func
12✔
340
    else:
341
        prev_func = pants_rules[effective_name]
12✔
342
        if prev_func.__code__ != func.__code__:
12✔
343
            raise DuplicateRuleError(
1✔
344
                softwrap(
345
                    f"""
346
                    Redeclaring rule {effective_name} with {func} at line
347
                    {func.__code__.co_firstlineno}, previously defined by {prev_func} at line
348
                    {prev_func.__code__.co_firstlineno}.
349
                    """
350
                )
351
            )
352

353
    return _make_rule(
12✔
354
        func_id,
355
        rule_type,
356
        return_type,
357
        parameter_types,
358
        masked_types,
359
        cacheable=cacheable,
360
        polymorphic=polymorphic,
361
        canonical_name=effective_name,
362
        desc=effective_desc,
363
        level=effective_level,
364
    )(func)
365

366

367
def validate_requirements(
12✔
368
    func_id: str,
369
    parameter_types: dict[str, type],
370
    awaitables: tuple[AwaitableConstraints, ...],
371
    cacheable: bool,
372
) -> None:
373
    # TODO: Technically this will also fire for an @_uncacheable_rule, but we don't expose those as
374
    # part of the API, so it's OK for these errors not to mention them.
375
    for ty in parameter_types.values():
12✔
376
        if cacheable and issubclass(ty, SideEffecting):
12✔
377
            raise ValueError(
1✔
378
                f"A `@rule` that is not a @goal_rule ({func_id}) may not have "
379
                f"a side-effecting parameter: {ty}."
380
            )
381
    for awaitable in awaitables:
12✔
382
        input_type_side_effecting = [
12✔
383
            it for it in awaitable.input_types if issubclass(it, SideEffecting)
384
        ]
385
        if input_type_side_effecting:
12✔
386
            raise ValueError(
×
387
                f"A `@rule` may not request side-effecting types ({input_type_side_effecting})."
388
            )
389

390

391
def inner_rule(*args, **kwargs) -> Callable[P, Call[R]] | RuleDecorator:
12✔
392
    if len(args) == 1 and inspect.isfunction(args[0]):
12✔
393
        return rule_decorator(*args, **kwargs)
12✔
394
    else:
395

396
        def wrapper(*args):
12✔
397
            return rule_decorator(*args, **kwargs)
12✔
398

399
        return wrapper
12✔
400

401

402
F = TypeVar("F", bound=Callable[..., Any | Coroutine[Any, Any, Any]])
12✔
403

404

405
@overload
12✔
406
def rule(**kwargs: Unpack[RuleDecoratorKwargs]) -> Callable[[F], F]:
12✔
407
    """Handles decorator factories of the form `@rule(foo=..., bar=...)`
408
    https://mypy.readthedocs.io/en/stable/generics.html#decorator-factories.
409

410
    Note: This needs to be the first rule, otherwise MyPy goes nuts
411
    """
412
    ...
413

414

415
@overload
12✔
416
def rule(_func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]:
12✔
417
    """Handles bare @rule decorators on async functions.
418

419
    Usage of Coroutine[...] (vs Awaitable[...]) is intentional, as `concurrently` uses coroutines
420
    directly.
421
    """
422
    ...
423

424

425
@overload
12✔
426
def rule(_func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]:
12✔
427
    """Handles bare @rule decorators on non-async functions It's debatable whether we should even
428
    have non-async @rule functions, but keeping this to not break the world for plugin authors.
429

430
    Usage of Coroutine[...] (vs Awaitable[...]) is intentional, as `concurrently` uses coroutines
431
    directly.
432
    """
433
    ...
434

435

436
def rule(*args, **kwargs):
12✔
437
    return inner_rule(*args, **kwargs, rule_type=RuleType.rule, cacheable=True)
12✔
438

439

440
@overload
441
def goal_rule(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]: ...
442

443

444
@overload
445
def goal_rule(func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]: ...
446

447

448
@overload
449
def goal_rule(
450
    *args, func: None = None, **kwargs: Any
451
) -> Callable[[SyncRuleT | AsyncRuleT], AsyncRuleT]: ...
452

453

454
def goal_rule(*args, **kwargs):
12✔
455
    if "level" not in kwargs:
12✔
456
        kwargs["level"] = LogLevel.DEBUG
12✔
457
    return inner_rule(
12✔
458
        *args,
459
        **kwargs,
460
        rule_type=RuleType.goal_rule,
461
        cacheable=False,
462
    )
463

464

465
@overload
466
def _uncacheable_rule(
467
    func: Callable[P, Coroutine[Any, Any, R]],
468
) -> Callable[P, Coroutine[Any, Any, R]]: ...
469

470

471
@overload
472
def _uncacheable_rule(func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]: ...
473

474

475
@overload
476
def _uncacheable_rule(
477
    *args, func: None = None, **kwargs: Any
478
) -> Callable[[SyncRuleT | AsyncRuleT], AsyncRuleT]: ...
479

480

481
# This has a "private" name, as we don't (yet?) want it to be part of the rule API, at least
482
# until we figure out the implications, and have a handle on the semantics and use-cases.
483
def _uncacheable_rule(*args, **kwargs):
12✔
484
    return inner_rule(
12✔
485
        *args, **kwargs, rule_type=RuleType.uncacheable_rule, cacheable=False, polymorphic=False
486
    )
487

488

489
class Rule(Protocol):
12✔
490
    """Rules declare how to produce products for the product graph.
491

492
    A rule describes what dependencies must be provided to produce a particular product. They also
493
    act as factories for constructing the nodes within the graph.
494
    """
495

496
    @property
12✔
497
    def output_type(self):
12✔
498
        """An output `type` for the rule."""
499

500

501
def collect_rules(*namespaces: ModuleType | Mapping[str, Any]) -> Iterable[Rule]:
12✔
502
    """Collects all @rules in the given namespaces.
503

504
    If no namespaces are given, collects all the @rules in the caller's module namespace.
505
    """
506

507
    if not namespaces:
12✔
508
        currentframe = inspect.currentframe()
12✔
509
        assert isinstance(currentframe, FrameType)
12✔
510
        caller_frame = currentframe.f_back
12✔
511
        assert isinstance(caller_frame, FrameType)
12✔
512

513
        global_items = caller_frame.f_globals
12✔
514
        namespaces = (global_items,)
12✔
515

516
    def iter_rules():
12✔
517
        for namespace in namespaces:
12✔
518
            mapping = namespace.__dict__ if isinstance(namespace, ModuleType) else namespace
12✔
519
            for item in mapping.values():
12✔
520
                if not callable(item):
12✔
521
                    continue
12✔
522
                rule = getattr(item, "rule", None)
12✔
523
                if isinstance(rule, TaskRule):
12✔
524
                    for input in rule.parameters.values():
12✔
525
                        if getattr(input, "__subsystem__", False):
12✔
526
                            yield from input.rules()
12✔
527
                        if getattr(input, "__subsystem_environment_aware__", False):
12✔
528
                            yield from input.subsystem.rules()
12✔
529
                    if getattr(rule.output_type, "__goal__", False):
12✔
530
                        yield from rule.output_type.subsystem_cls.rules()
12✔
531
                    yield rule
12✔
532

533
    return list(iter_rules())
12✔
534

535

536
@dataclass(frozen=True)
12✔
537
class TaskRule:
12✔
538
    """A Rule that runs a task function when all of its input selectors are satisfied.
539

540
    NB: This API is not meant for direct consumption. To create a `TaskRule` you should always
541
    prefer the `@rule` constructor.
542
    """
543

544
    output_type: type[Any]
12✔
545
    parameters: FrozenDict[str, type[Any]]
12✔
546
    awaitables: tuple[AwaitableConstraints, ...]
12✔
547
    masked_types: tuple[type[Any], ...]
12✔
548
    func: Callable
12✔
549
    canonical_name: str
12✔
550
    desc: str | None = None
12✔
551
    level: LogLevel = LogLevel.TRACE
12✔
552
    cacheable: bool = True
12✔
553
    polymorphic: bool = False
12✔
554

555
    def __str__(self):
12✔
556
        return "(name={}, {}, {!r}, {}, gets={})".format(
×
557
            getattr(self, "name", "<not defined>"),
558
            self.output_type.__name__,
559
            self.parameters.values(),
560
            self.func.__name__,
561
            self.awaitables,
562
        )
563

564

565
@dataclass(frozen=True)
12✔
566
class QueryRule:
12✔
567
    """A QueryRule declares that a given set of Params will be used to request an output type.
568

569
    Every callsite to `Scheduler.product_request` should have a corresponding QueryRule to ensure
570
    that the relevant portions of the RuleGraph are generated.
571
    """
572

573
    output_type: type[Any]
12✔
574
    input_types: tuple[type[Any], ...]
12✔
575

576
    def __init__(self, output_type: type[Any], input_types: Iterable[type[Any]]) -> None:
12✔
577
        object.__setattr__(self, "output_type", output_type)
12✔
578
        object.__setattr__(self, "input_types", tuple(input_types))
12✔
579

580

581
@dataclass(frozen=True)
12✔
582
class RuleIndex:
12✔
583
    """Holds a normalized index of Rules used to instantiate Nodes."""
584

585
    rules: FrozenOrderedSet[TaskRule]
12✔
586
    queries: FrozenOrderedSet[QueryRule]
12✔
587
    union_rules: FrozenOrderedSet[UnionRule]
12✔
588

589
    @classmethod
12✔
590
    def create(cls, rule_entries: Iterable[Rule | UnionRule]) -> RuleIndex:
12✔
591
        """Creates a RuleIndex with tasks indexed by their output type."""
592
        rules: OrderedSet[TaskRule] = OrderedSet()
12✔
593
        queries: OrderedSet[QueryRule] = OrderedSet()
12✔
594
        union_rules: OrderedSet[UnionRule] = OrderedSet()
12✔
595

596
        for entry in rule_entries:
12✔
597
            if isinstance(entry, TaskRule):
12✔
598
                rules.add(entry)
12✔
599
            elif isinstance(entry, UnionRule):
12✔
600
                union_rules.add(entry)
12✔
601
            elif isinstance(entry, QueryRule):
12✔
602
                queries.add(entry)
12✔
603
            elif hasattr(entry, "__call__"):
12✔
604
                rule = getattr(entry, "rule", None)
12✔
605
                if rule is None:
12✔
606
                    raise TypeError(f"Expected function {entry} to be decorated with @rule.")
×
607
                rules.add(rule)
12✔
608
            else:
609
                raise TypeError(
1✔
610
                    f"Rule entry {entry} had an unexpected type: {type(entry)}. Rules either "
611
                    "extend Rule or UnionRule, or are static functions decorated with @rule."
612
                )
613

614
        return RuleIndex(
12✔
615
            rules=FrozenOrderedSet(rules),
616
            queries=FrozenOrderedSet(queries),
617
            union_rules=FrozenOrderedSet(union_rules),
618
        )
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