• 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

72.53
/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
1✔
5

6
import functools
1✔
7
import inspect
1✔
8
import sys
1✔
9
from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence
1✔
10
from dataclasses import dataclass
1✔
11
from enum import Enum
1✔
12
from types import FrameType, ModuleType
1✔
13
from typing import (
1✔
14
    Any,
15
    NotRequired,
16
    Protocol,
17
    TypedDict,
18
    TypeVar,
19
    Unpack,
20
    cast,
21
    get_type_hints,
22
    overload,
23
)
24

25
from typing_extensions import ParamSpec
1✔
26

27
from pants.engine.engine_aware import SideEffecting
1✔
28
from pants.engine.internals.rule_visitor import collect_awaitables
1✔
29
from pants.engine.internals.selectors import AwaitableConstraints, Call
1✔
30
from pants.engine.internals.selectors import Effect as Effect  # noqa: F401
1✔
31
from pants.engine.internals.selectors import Get as Get  # noqa: F401
1✔
32
from pants.engine.internals.selectors import MultiGet as MultiGet  # noqa: F401
1✔
33
from pants.engine.internals.selectors import concurrently as concurrently  # noqa: F401
1✔
34
from pants.engine.unions import UnionRule
1✔
35
from pants.util.frozendict import FrozenDict
1✔
36
from pants.util.logging import LogLevel
1✔
37
from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
1✔
38
from pants.util.strutil import softwrap
1✔
39

40
PANTS_RULES_MODULE_KEY = "__pants_rules__"
1✔
41

42

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

49

50
class RuleType(Enum):
1✔
51
    rule = "rule"
1✔
52
    goal_rule = "goal_rule"
1✔
53
    uncacheable_rule = "_uncacheable_rule"
1✔
54

55

56
P = ParamSpec("P")
1✔
57
R = TypeVar("R")
1✔
58
SyncRuleT = Callable[P, R]
1✔
59
AsyncRuleT = Callable[P, Coroutine[Any, Any, R]]
1✔
60
RuleDecorator = Callable[[SyncRuleT | AsyncRuleT], AsyncRuleT]
1✔
61

62

63
def _rule_call_trampoline(
1✔
64
    rule_id: str, output_type: type[Any], func: Callable[P, R]
65
) -> Callable[P, R]:
66
    @functools.wraps(func)  # type: ignore
1✔
67
    async def wrapper(*args, __implicitly: Sequence[Any] = (), **kwargs):
1✔
UNCOV
68
        call = Call(rule_id, output_type, args, *__implicitly)
×
UNCOV
69
        return await call
×
70

71
    return cast(Callable[P, R], wrapper)
1✔
72

73

74
def _make_rule(
1✔
75
    func_id: str,
76
    rule_type: RuleType,
77
    return_type: type[Any],
78
    parameter_types: dict[str, type[Any]],
79
    masked_types: Iterable[type[Any]],
80
    *,
81
    cacheable: bool,
82
    polymorphic: bool,
83
    canonical_name: str,
84
    desc: str | None,
85
    level: LogLevel,
86
) -> RuleDecorator:
87
    """A @decorator that declares that a particular static function may be used as a TaskRule.
88

89
    :param rule_type: The specific decorator used to declare the rule.
90
    :param return_type: The return/output type for the Rule. This must be a concrete Python type.
91
    :param parameter_types: A sequence of types that matches the number and order of arguments to
92
                            the decorated function.
93
    :param cacheable: Whether the results of executing the Rule should be cached as keyed by all of
94
                      its inputs.
95
    :param polymorphic: Whether the rule is an abstract base method for polymorphic dispatch via
96
                        a union type.
97
    """
98

99
    is_goal_cls = getattr(return_type, "__goal__", False)
1✔
100
    if rule_type == RuleType.rule and is_goal_cls:
1✔
UNCOV
101
        raise TypeError(
×
102
            "An `@rule` that returns a `Goal` must instead be declared with `@goal_rule`."
103
        )
104
    if rule_type == RuleType.goal_rule and not is_goal_cls:
1✔
UNCOV
105
        raise TypeError("An `@goal_rule` must return a subclass of `engine.goal.Goal`.")
×
106

107
    def wrapper(original_func):
1✔
108
        if not inspect.isfunction(original_func):
1✔
109
            raise ValueError("The @rule decorator must be applied innermost of all decorators.")
×
110

111
        # Set our own custom `__line_number__` dunder so that the engine may visualize the line number.
112
        original_func.__line_number__ = original_func.__code__.co_firstlineno
1✔
113
        original_func.rule_id = canonical_name
1✔
114

115
        awaitables = FrozenOrderedSet(collect_awaitables(original_func))
1✔
116

117
        validate_requirements(func_id, parameter_types, awaitables, cacheable)
1✔
118
        func = _rule_call_trampoline(canonical_name, return_type, original_func)
1✔
119

120
        # NB: The named definition of the rule ends up wrapped in a trampoline to handle memoization
121
        # and implicit arguments for direct by-name calls. But the `TaskRule` takes a reference to
122
        # the original unwrapped function, which avoids the need for a special protocol when the
123
        # engine invokes a @rule under memoization.
124
        func.rule = TaskRule(
1✔
125
            return_type,
126
            FrozenDict(parameter_types),
127
            awaitables,
128
            masked_types,
129
            original_func,
130
            canonical_name=canonical_name,
131
            desc=desc,
132
            level=level,
133
            cacheable=cacheable,
134
            polymorphic=polymorphic,
135
        )
136
        return func
1✔
137

138
    return wrapper
1✔
139

140

141
class InvalidTypeAnnotation(TypeError):
1✔
142
    """Indicates an incorrect type annotation for an `@rule`."""
143

144

145
class UnrecognizedRuleArgument(TypeError):
1✔
146
    """Indicates an unrecognized keyword argument to a `@rule`."""
147

148

149
class MissingTypeAnnotation(TypeError):
1✔
150
    """Indicates a missing type annotation for an `@rule`."""
151

152

153
class MissingReturnTypeAnnotation(InvalidTypeAnnotation):
1✔
154
    """Indicates a missing return type annotation for an `@rule`."""
155

156

157
class MissingParameterTypeAnnotation(InvalidTypeAnnotation):
1✔
158
    """Indicates a missing parameter type annotation for an `@rule`."""
159

160

161
class DuplicateRuleError(TypeError):
1✔
162
    """Invalid to overwrite `@rule`s using the same name in the same module."""
163

164

165
def _ensure_type_annotation(
1✔
166
    *,
167
    type_annotation: type[Any] | None,
168
    name: str,
169
    raise_type: type[InvalidTypeAnnotation],
170
) -> type[Any]:
171
    if type_annotation is None:
1✔
UNCOV
172
        raise raise_type(f"{name} is missing a type annotation.")
×
173
    if not isinstance(type_annotation, type):
1✔
UNCOV
174
        raise raise_type(
×
175
            f"The annotation for {name} must be a type, got {type_annotation} of type {type(type_annotation)}."
176
        )
177
    return type_annotation
1✔
178

179

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

206

207
class RuleDecoratorKwargs(TypedDict):
1✔
208
    """Public-facing @rule kwargs used in the codebase."""
209

210
    canonical_name: NotRequired[str]
1✔
211

212
    canonical_name_suffix: NotRequired[str]
1✔
213

214
    desc: NotRequired[str]
1✔
215
    """The rule's description as it appears in stacktraces/debugging. For goal rules, defaults to the goal name."""
1✔
216

217
    level: NotRequired[LogLevel]
1✔
218
    """The logging level applied to this rule. Defaults to TRACE."""
1✔
219

220
    polymorphic: NotRequired[bool]
1✔
221
    """Whether this rule represents an abstract method for a union.
222

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

227
    E.g., given
228

229
    ```
230
    @rule(polymorphic=True)
231
    async def base_rule(arg: UnionBase, other_arg: OtherType) -> OutputType
232
        ...
233

234
    @rule(polymorphic=True)
235
    async def derived_rule(arg: UnionMember, other_arg: OtherType) -> OutputType
236
       ...
237

238
    ```
239

240
    And an arg of type UnionMember, then
241

242
    `await base_rule(arg, other_arg)`
243

244
    will invoke `derived_rule(arg, other_arg)`
245

246
    This is the call-by-name equivalent of Get(OutputType, UnionBase, union_member_instance).
247
    """
248

249
    _masked_types: NotRequired[Iterable[type[Any]]]
1✔
250
    """Unstable. Internal Pants usage only."""
1✔
251

252
    _param_type_overrides: NotRequired[dict[str, type[Any]]]
1✔
253
    """Unstable. Internal Pants usage only."""
1✔
254

255

256
class _RuleDecoratorKwargs(RuleDecoratorKwargs):
1✔
257
    """Internal/Implicit @rule kwargs (not for use outside rules.py)"""
258

259
    rule_type: RuleType
1✔
260
    """The decorator used to declare the rule (see rules.py:_make_rule(...))"""
1✔
261

262
    cacheable: bool
1✔
263
    """Whether the results of this rule should be cached.
1✔
264
    Typically true for rules, false for goal_rules (see rules.py:_make_rule(...))
265
    """
266

267

268
def rule_decorator(
1✔
269
    func: SyncRuleT | AsyncRuleT, **kwargs: Unpack[_RuleDecoratorKwargs]
270
) -> AsyncRuleT:
271
    if not inspect.isfunction(func):
1✔
272
        raise ValueError("The @rule decorator expects to be placed on a function.")
×
273

274
    if (
1✔
275
        len(
276
            set(kwargs)
277
            - PUBLIC_RULE_DECORATOR_ARGUMENTS
278
            - PRIVATE_RULE_DECORATOR_ARGUMENTS
279
            - IMPLICIT_PRIVATE_RULE_DECORATOR_ARGUMENTS
280
        )
281
        != 0
282
    ):
UNCOV
283
        raise UnrecognizedRuleArgument(
×
284
            f"`@rule`s and `@goal_rule`s only accept the following keyword arguments: {PUBLIC_RULE_DECORATOR_ARGUMENTS}"
285
        )
286

287
    rule_type = kwargs["rule_type"]
1✔
288
    cacheable = kwargs["cacheable"]
1✔
289
    polymorphic = kwargs.get("polymorphic", False)
1✔
290
    masked_types: tuple[type, ...] = tuple(kwargs.get("_masked_types", ()))
1✔
291
    param_type_overrides: dict[str, type] = kwargs.get("_param_type_overrides", {})
1✔
292

293
    func_id = f"@rule {func.__module__}:{func.__name__}"
1✔
294
    type_hints = get_type_hints(func)
1✔
295
    return_type = _ensure_type_annotation(
1✔
296
        type_annotation=type_hints.get("return"),
297
        name=f"{func_id} return",
298
        raise_type=MissingReturnTypeAnnotation,
299
    )
300

301
    func_params = inspect.signature(func).parameters
1✔
302
    for parameter in param_type_overrides:
1✔
UNCOV
303
        if parameter not in func_params:
×
UNCOV
304
            raise ValueError(
×
305
                f"Unknown parameter name in `param_type_overrides`: {parameter}."
306
                + f" Parameter names: '{', '.join(func_params)}'"
307
            )
308

309
    parameter_types = {
1✔
310
        parameter: _ensure_type_annotation(
311
            type_annotation=param_type_overrides.get(parameter, type_hints.get(parameter)),
312
            name=f"{func_id} parameter {parameter}",
313
            raise_type=MissingParameterTypeAnnotation,
314
        )
315
        for parameter in func_params
316
    }
317
    is_goal_cls = getattr(return_type, "__goal__", False)
1✔
318

319
    # Set a default canonical name if one is not explicitly provided to the module and name of the
320
    # function that implements it, plus an optional suffix. This is used as the workunit name.
321
    # The suffix is a convenient way to disambiguate multiple rules registered dynamically from the
322
    # same static code (by overriding the inferred param types in the @rule decorator).
323
    # TODO: It is not yet clear how dynamically registered rules whose names are generated
324
    #  with a suffix will work in practice with the new call-by-name semantics.
325
    #  For now the suffix serves to ensure unique names. Whether they are useful is another matter.
326
    suffix = kwargs.get("canonical_name_suffix", "")
1✔
327
    effective_name = kwargs.get(
1✔
328
        "canonical_name",
329
        f"{func.__module__}.{func.__qualname__}{('_' + suffix) if suffix else ''}".replace(
330
            ".<locals>", ""
331
        ),
332
    )
333

334
    # Set a default description, which is used in the dynamic UI and stacktraces.
335
    effective_desc = kwargs.get("desc")
1✔
336
    if effective_desc is None and is_goal_cls:
1✔
337
        effective_desc = f"`{return_type.name}` goal"
1✔
338

339
    effective_level = kwargs.get("level", LogLevel.TRACE)
1✔
340
    if not isinstance(effective_level, LogLevel):  # type: ignore[unused-ignore]
1✔
341
        raise ValueError(
×
342
            "Expected to receive a value of type LogLevel for the level "
343
            f"argument, but got: {effective_level}"
344
        )
345

346
    module = sys.modules[func.__module__]
1✔
347
    pants_rules = getattr(module, PANTS_RULES_MODULE_KEY, None)
1✔
348
    if pants_rules is None:
1✔
349
        pants_rules = {}
1✔
350
        setattr(module, PANTS_RULES_MODULE_KEY, pants_rules)
1✔
351

352
    if effective_name not in pants_rules:
1✔
353
        pants_rules[effective_name] = func
1✔
354
    else:
UNCOV
355
        prev_func = pants_rules[effective_name]
×
UNCOV
356
        if prev_func.__code__ != func.__code__:
×
UNCOV
357
            raise DuplicateRuleError(
×
358
                softwrap(
359
                    f"""
360
                    Redeclaring rule {effective_name} with {func} at line
361
                    {func.__code__.co_firstlineno}, previously defined by {prev_func} at line
362
                    {prev_func.__code__.co_firstlineno}.
363
                    """
364
                )
365
            )
366

367
    return _make_rule(
1✔
368
        func_id,
369
        rule_type,
370
        return_type,
371
        parameter_types,
372
        masked_types,
373
        cacheable=cacheable,
374
        polymorphic=polymorphic,
375
        canonical_name=effective_name,
376
        desc=effective_desc,
377
        level=effective_level,
378
    )(func)
379

380

381
def validate_requirements(
1✔
382
    func_id: str,
383
    parameter_types: dict[str, type],
384
    awaitables: tuple[AwaitableConstraints, ...],
385
    cacheable: bool,
386
) -> None:
387
    # TODO: Technically this will also fire for an @_uncacheable_rule, but we don't expose those as
388
    # part of the API, so it's OK for these errors not to mention them.
389
    for ty in parameter_types.values():
1✔
390
        if cacheable and issubclass(ty, SideEffecting):
1✔
UNCOV
391
            raise ValueError(
×
392
                f"A `@rule` that is not a @goal_rule ({func_id}) may not have "
393
                f"a side-effecting parameter: {ty}."
394
            )
395
    for awaitable in awaitables:
1✔
396
        input_type_side_effecting = [
1✔
397
            it for it in awaitable.input_types if issubclass(it, SideEffecting)
398
        ]
399
        if input_type_side_effecting and not awaitable.is_effect:
1✔
400
            raise ValueError(
×
401
                f"A `Get` may not request side-effecting types ({input_type_side_effecting}). "
402
                f"Use `Effect` instead: `{awaitable}`."
403
            )
404
        if not input_type_side_effecting and awaitable.is_effect:
1✔
405
            raise ValueError(
×
406
                f"An `Effect` should not be used with pure types ({awaitable.input_types}). "
407
                f"Use `Get` instead: `{awaitable}`."
408
            )
409
        if cacheable and awaitable.is_effect:
1✔
410
            raise ValueError(
×
411
                f"A `@rule` that is not a @goal_rule ({func_id}) may not use an "
412
                f"Effect: `{awaitable}`."
413
            )
414

415

416
def inner_rule(*args, **kwargs) -> AsyncRuleT | RuleDecorator:
1✔
417
    if len(args) == 1 and inspect.isfunction(args[0]):
1✔
418
        return rule_decorator(*args, **kwargs)
1✔
419
    else:
420

421
        def wrapper(*args):
1✔
422
            return rule_decorator(*args, **kwargs)
1✔
423

424
        return wrapper
1✔
425

426

427
F = TypeVar("F", bound=Callable[..., Any | Coroutine[Any, Any, Any]])
1✔
428

429

430
@overload
1✔
431
def rule(**kwargs: Unpack[RuleDecoratorKwargs]) -> Callable[[F], F]:
1✔
432
    """Handles decorator factories of the form `@rule(foo=..., bar=...)`
433
    https://mypy.readthedocs.io/en/stable/generics.html#decorator-factories.
434

435
    Note: This needs to be the first rule, otherwise MyPy goes nuts
436
    """
437
    ...
438

439

440
@overload
1✔
441
def rule(_func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]:
1✔
442
    """Handles bare @rule decorators on async functions.
443

444
    Usage of Coroutine[...] (vs Awaitable[...]) is intentional, as `MultiGet`/`concurrently` use
445
    coroutines directly.
446
    """
447
    ...
448

449

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

455
    Usage of Coroutine[...] (vs Awaitable[...]) is intentional, as `MultiGet`/`concurrently` use
456
    coroutines directly.
457
    """
458
    ...
459

460

461
def rule(*args, **kwargs):
1✔
462
    return inner_rule(*args, **kwargs, rule_type=RuleType.rule, cacheable=True)
1✔
463

464

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

468

469
@overload
470
def goal_rule(func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]: ...
471

472

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

478

479
def goal_rule(*args, **kwargs):
1✔
480
    if "level" not in kwargs:
1✔
481
        kwargs["level"] = LogLevel.DEBUG
1✔
482
    return inner_rule(
1✔
483
        *args,
484
        **kwargs,
485
        rule_type=RuleType.goal_rule,
486
        cacheable=False,
487
    )
488

489

490
@overload
491
def _uncacheable_rule(
492
    func: Callable[P, Coroutine[Any, Any, R]],
493
) -> Callable[P, Coroutine[Any, Any, R]]: ...
494

495

496
@overload
497
def _uncacheable_rule(func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]: ...
498

499

500
@overload
501
def _uncacheable_rule(
502
    *args, func: None = None, **kwargs: Any
503
) -> Callable[[SyncRuleT | AsyncRuleT], AsyncRuleT]: ...
504

505

506
# This has a "private" name, as we don't (yet?) want it to be part of the rule API, at least
507
# until we figure out the implications, and have a handle on the semantics and use-cases.
508
def _uncacheable_rule(*args, **kwargs):
1✔
509
    return inner_rule(
1✔
510
        *args, **kwargs, rule_type=RuleType.uncacheable_rule, cacheable=False, polymorphic=False
511
    )
512

513

514
class Rule(Protocol):
1✔
515
    """Rules declare how to produce products for the product graph.
516

517
    A rule describes what dependencies must be provided to produce a particular product. They also
518
    act as factories for constructing the nodes within the graph.
519
    """
520

521
    @property
1✔
522
    def output_type(self):
1✔
523
        """An output `type` for the rule."""
524

525

526
def collect_rules(*namespaces: ModuleType | Mapping[str, Any]) -> Iterable[Rule]:
1✔
527
    """Collects all @rules in the given namespaces.
528

529
    If no namespaces are given, collects all the @rules in the caller's module namespace.
530
    """
531

UNCOV
532
    if not namespaces:
×
UNCOV
533
        currentframe = inspect.currentframe()
×
UNCOV
534
        assert isinstance(currentframe, FrameType)
×
UNCOV
535
        caller_frame = currentframe.f_back
×
UNCOV
536
        assert isinstance(caller_frame, FrameType)
×
537

UNCOV
538
        global_items = caller_frame.f_globals
×
UNCOV
539
        namespaces = (global_items,)
×
540

UNCOV
541
    def iter_rules():
×
UNCOV
542
        for namespace in namespaces:
×
UNCOV
543
            mapping = namespace.__dict__ if isinstance(namespace, ModuleType) else namespace
×
UNCOV
544
            for item in mapping.values():
×
UNCOV
545
                if not callable(item):
×
UNCOV
546
                    continue
×
UNCOV
547
                rule = getattr(item, "rule", None)
×
UNCOV
548
                if isinstance(rule, TaskRule):
×
UNCOV
549
                    for input in rule.parameters.values():
×
UNCOV
550
                        if getattr(input, "__subsystem__", False):
×
UNCOV
551
                            yield from input.rules()
×
UNCOV
552
                        if getattr(input, "__subsystem_environment_aware__", False):
×
UNCOV
553
                            yield from input.subsystem.rules()
×
UNCOV
554
                    if getattr(rule.output_type, "__goal__", False):
×
UNCOV
555
                        yield from rule.output_type.subsystem_cls.rules()
×
UNCOV
556
                    yield rule
×
557

UNCOV
558
    return list(iter_rules())
×
559

560

561
@dataclass(frozen=True)
1✔
562
class TaskRule:
1✔
563
    """A Rule that runs a task function when all of its input selectors are satisfied.
564

565
    NB: This API is not meant for direct consumption. To create a `TaskRule` you should always
566
    prefer the `@rule` constructor.
567
    """
568

569
    output_type: type[Any]
1✔
570
    parameters: FrozenDict[str, type[Any]]
1✔
571
    awaitables: tuple[AwaitableConstraints, ...]
1✔
572
    masked_types: tuple[type[Any], ...]
1✔
573
    func: Callable
1✔
574
    canonical_name: str
1✔
575
    desc: str | None = None
1✔
576
    level: LogLevel = LogLevel.TRACE
1✔
577
    cacheable: bool = True
1✔
578
    polymorphic: bool = False
1✔
579

580
    def __str__(self):
1✔
581
        return "(name={}, {}, {!r}, {}, gets={})".format(
×
582
            getattr(self, "name", "<not defined>"),
583
            self.output_type.__name__,
584
            self.parameters.values(),
585
            self.func.__name__,
586
            self.awaitables,
587
        )
588

589

590
@dataclass(frozen=True)
1✔
591
class QueryRule:
1✔
592
    """A QueryRule declares that a given set of Params will be used to request an output type.
593

594
    Every callsite to `Scheduler.product_request` should have a corresponding QueryRule to ensure
595
    that the relevant portions of the RuleGraph are generated.
596
    """
597

598
    output_type: type[Any]
1✔
599
    input_types: tuple[type[Any], ...]
1✔
600

601
    def __init__(self, output_type: type[Any], input_types: Iterable[type[Any]]) -> None:
1✔
UNCOV
602
        object.__setattr__(self, "output_type", output_type)
×
UNCOV
603
        object.__setattr__(self, "input_types", tuple(input_types))
×
604

605

606
@dataclass(frozen=True)
1✔
607
class RuleIndex:
1✔
608
    """Holds a normalized index of Rules used to instantiate Nodes."""
609

610
    rules: FrozenOrderedSet[TaskRule]
1✔
611
    queries: FrozenOrderedSet[QueryRule]
1✔
612
    union_rules: FrozenOrderedSet[UnionRule]
1✔
613

614
    @classmethod
1✔
615
    def create(cls, rule_entries: Iterable[Rule | UnionRule]) -> RuleIndex:
1✔
616
        """Creates a RuleIndex with tasks indexed by their output type."""
UNCOV
617
        rules: OrderedSet[TaskRule] = OrderedSet()
×
UNCOV
618
        queries: OrderedSet[QueryRule] = OrderedSet()
×
UNCOV
619
        union_rules: OrderedSet[UnionRule] = OrderedSet()
×
620

UNCOV
621
        for entry in rule_entries:
×
UNCOV
622
            if isinstance(entry, TaskRule):
×
UNCOV
623
                rules.add(entry)
×
UNCOV
624
            elif isinstance(entry, UnionRule):
×
UNCOV
625
                union_rules.add(entry)
×
UNCOV
626
            elif isinstance(entry, QueryRule):
×
UNCOV
627
                queries.add(entry)
×
UNCOV
628
            elif hasattr(entry, "__call__"):
×
UNCOV
629
                rule = getattr(entry, "rule", None)
×
UNCOV
630
                if rule is None:
×
631
                    raise TypeError(f"Expected function {entry} to be decorated with @rule.")
×
UNCOV
632
                rules.add(rule)
×
633
            else:
UNCOV
634
                raise TypeError(
×
635
                    f"Rule entry {entry} had an unexpected type: {type(entry)}. Rules either "
636
                    "extend Rule or UnionRule, or are static functions decorated with @rule."
637
                )
638

UNCOV
639
        return RuleIndex(
×
640
            rules=FrozenOrderedSet(rules),
641
            queries=FrozenOrderedSet(queries),
642
            union_rules=FrozenOrderedSet(union_rules),
643
        )
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