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

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 hits per line

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

31.43
/src/python/pants/engine/internals/parametrize.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
5✔
5

6
import dataclasses
5✔
7
import itertools
5✔
8
import operator
5✔
9
from collections import abc, defaultdict
5✔
10
from collections.abc import Iterator, Mapping
5✔
11
from dataclasses import dataclass
5✔
12
from enum import Enum
5✔
13
from functools import reduce
5✔
14
from typing import Any, Self, cast
5✔
15

16
from pants.build_graph.address import BANNED_CHARS_IN_PARAMETERS
5✔
17
from pants.engine.addresses import Address
5✔
18
from pants.engine.collection import Collection
5✔
19
from pants.engine.engine_aware import EngineAwareParameter
5✔
20
from pants.engine.target import (
5✔
21
    Field,
22
    FieldDefaults,
23
    ImmutableValue,
24
    Target,
25
    TargetTypesToGenerateTargetsRequests,
26
)
27
from pants.util.frozendict import FrozenDict
5✔
28
from pants.util.strutil import bullet_list, pluralize, softwrap
5✔
29

30

31
def _named_args_explanation(arg: str) -> str:
5✔
UNCOV
32
    return (
×
33
        f"To use `{arg}` as a parameter, you can pass it as a keyword argument to "
34
        f"give it an alias. For example: `parametrize(short_memorable_name={arg})`"
35
    )
36

37

38
@dataclass(frozen=True)
5✔
39
class Parametrize:
5✔
40
    """A builtin function/dataclass that can be used to parametrize Targets.
41

42
    Parametrization is applied between TargetAdaptor construction and Target instantiation, which
43
    means that individual Field instances need not be aware of it.
44
    """
45

46
    class _MergeBehaviour(Enum):
5✔
47
        # Do not merge this parametrization.
48
        never = "never"
5✔
49
        # Discard this parametrization with `other`.
50
        replace = "replace"
5✔
51

52
    args: tuple[str, ...]
5✔
53
    kwargs: FrozenDict[str, ImmutableValue]
5✔
54
    is_group: bool
5✔
55
    merge_behaviour: _MergeBehaviour = dataclasses.field(compare=False)
5✔
56

57
    def __init__(self, *args: str, **kwargs: Any) -> None:
5✔
UNCOV
58
        object.__setattr__(self, "args", tuple(args))
×
UNCOV
59
        object.__setattr__(self, "kwargs", FrozenDict.deep_freeze(kwargs))
×
UNCOV
60
        object.__setattr__(self, "is_group", False)
×
UNCOV
61
        object.__setattr__(self, "merge_behaviour", Parametrize._MergeBehaviour.never)
×
62

63
    def keys(self) -> tuple[str]:
5✔
UNCOV
64
        return (f"parametrize_{hash(self.args)}:{id(self)}",)
×
65

66
    def __getitem__(self, key) -> Any:
5✔
UNCOV
67
        if isinstance(key, str) and key.startswith("parametrize_"):
×
UNCOV
68
            return self.to_group()
×
69
        else:
70
            raise KeyError(key)
×
71

72
    def to_group(self) -> Self:
5✔
UNCOV
73
        object.__setattr__(self, "is_group", True)
×
UNCOV
74
        return self
×
75

76
    def to_weak(self) -> Self:
5✔
UNCOV
77
        object.__setattr__(self, "merge_behaviour", Parametrize._MergeBehaviour.replace)
×
UNCOV
78
        return self
×
79

80
    def to_parameters(self) -> dict[str, Any]:
5✔
81
        """Validates and returns a mapping from aliases to parameter values.
82

83
        This conversion is executed lazily to allow for more context in error messages, such as the
84
        TargetAdaptor consuming the Parametrize instance.
85
        """
UNCOV
86
        parameters = dict(self.kwargs)
×
UNCOV
87
        for arg in self.args:
×
UNCOV
88
            if not isinstance(arg, str):
×
UNCOV
89
                raise Exception(
×
90
                    f"In {self}:\n  Positional arguments must be strings, but "
91
                    f"`{arg!r}` was a `{type(arg).__name__}`.\n\n"
92
                    + _named_args_explanation(f"{arg!r}")
93
                )
UNCOV
94
            previous_arg = parameters.get(arg)
×
UNCOV
95
            if previous_arg is not None:
×
UNCOV
96
                raise Exception(
×
97
                    f"In {self}:\n  Positional arguments cannot have the same name as "
98
                    f"keyword arguments. `{arg}` was also defined as `{arg}={previous_arg!r}`."
99
                )
UNCOV
100
            banned_chars = BANNED_CHARS_IN_PARAMETERS & set(arg)
×
UNCOV
101
            if banned_chars:
×
UNCOV
102
                raise Exception(
×
103
                    f"In {self}:\n  Positional argument `{arg}` contained separator characters "
104
                    f"(`{'`,`'.join(banned_chars)}`).\n\n" + _named_args_explanation(arg)
105
                )
UNCOV
106
            parameters[arg] = arg
×
UNCOV
107
        return parameters
×
108

109
    @property
5✔
110
    def group_name(self) -> str:
5✔
UNCOV
111
        assert self.is_group
×
UNCOV
112
        if len(self.args) == 1:
×
UNCOV
113
            name = self.args[0]
×
UNCOV
114
            banned_chars = BANNED_CHARS_IN_PARAMETERS & set(name)
×
UNCOV
115
            if banned_chars:
×
UNCOV
116
                raise Exception(
×
117
                    f"In {self}:\n  Parametrization group name `{name}` contained separator characters "
118
                    f"(`{'`,`'.join(banned_chars)}`)."
119
                )
UNCOV
120
            return name
×
121
        else:
UNCOV
122
            raise ValueError(
×
123
                softwrap(
124
                    f"""
125
                    A parametrize group must begin with the group name followed by the target field
126
                    values for the group.
127

128
                    Example:
129

130
                        target(
131
                            ...,
132
                            **parametrize("group-a", field_a=1, field_b=True),
133
                        )
134

135
                    Got: `{self!r}`
136
                    """
137
                )
138
            )
139

140
    @classmethod
5✔
141
    def expand(
5✔
142
        cls,
143
        address: Address,
144
        fields: Mapping[str, Any | Parametrize],
145
    ) -> Iterator[tuple[Address, Mapping[str, Any]]]:
146
        """Produces the cartesian product of fields for the given possibly-Parametrized fields.
147

148
        Only one level of expansion is performed: if individual field values might also contain
149
        Parametrize instances (in particular: an `overrides` field), expanding those will require
150
        separate calls.
151

152
        Parametrized groups are expanded however (that is: any `parametrize` field values in a
153
        `**parametrize()` group are also expanded).
154
        """
UNCOV
155
        try:
×
UNCOV
156
            yield from cls._expand(address, fields)
×
157
        except Exception as e:
1✔
UNCOV
158
            raise Exception(f"Failed to parametrize `{address}`:\n  {e}") from e
×
159

160
    @classmethod
5✔
161
    def _expand(
5✔
162
        cls,
163
        address: Address,
164
        fields: Mapping[str, Any | Parametrize],
165
        _parametrization_group_prefix: str = "",
166
    ) -> Iterator[tuple[Address, Mapping[str, Any]]]:
UNCOV
167
        parametrizations = cls._collect_parametrizations(fields)
×
UNCOV
168
        cls._check_parametrizations(parametrizations)
×
UNCOV
169
        parametrized: list[list[tuple[str, str, Any]]] = [
×
170
            [(field_name, alias, field_value) for alias, field_value in v.to_parameters().items()]
171
            for field_name, v in parametrizations.get(None, ())
172
        ]
UNCOV
173
        parametrized_groups: list[tuple[str, str, Parametrize]] = [
×
174
            ("parametrize", (_parametrization_group_prefix + group_name), vs[0][1])
175
            for group_name, vs in parametrizations.items()
176
            if group_name is not None
177
        ]
UNCOV
178
        parameters = address.parameters
×
UNCOV
179
        non_parametrized = tuple(
×
180
            (field_name, field_value)
181
            for field_name, field_value in fields.items()
182
            if not isinstance(field_value, Parametrize)
183
        )
UNCOV
184
        if parametrized_groups:
×
185
            # Add the groups as one vector for the cross-product.
UNCOV
186
            parametrized.append(parametrized_groups)
×
187

UNCOV
188
        unparametrize_keys = {k for k, _ in non_parametrized if k in parameters}
×
189

190
        # Remove non-parametrized fields from the address parameters.
UNCOV
191
        for k in unparametrize_keys:
×
UNCOV
192
            parameters.pop(k, None)
×
193

UNCOV
194
        if not parametrized:
×
UNCOV
195
            if unparametrize_keys:
×
UNCOV
196
                address = address.parametrize(parameters, replace=True)
×
UNCOV
197
            yield (address, fields)
×
UNCOV
198
            return
×
199

UNCOV
200
        for parametrized_args in itertools.product(*parametrized):
×
UNCOV
201
            expanded_parameters = parameters | {
×
202
                field_name: alias for field_name, alias, _ in parametrized_args
203
            }
204
            # There will be at most one group per cross product.
UNCOV
205
            parametrize_group: Parametrize | None = next(
×
206
                (
207
                    field_value
208
                    for _, _, field_value in parametrized_args
209
                    if isinstance(field_value, Parametrize) and field_value.is_group
210
                ),
211
                None,
212
            )
UNCOV
213
            if parametrize_group is not None:
×
214
                # Exclude fields from parametrize group from address parameters.
UNCOV
215
                for k in parametrize_group.kwargs.keys() & parameters.keys():
×
UNCOV
216
                    expanded_parameters.pop(k, None)
×
UNCOV
217
                expand_recursively = any(
×
218
                    isinstance(group_value, Parametrize)
219
                    for group_value in parametrize_group.kwargs.values()
220
                )
221
            else:
UNCOV
222
                expand_recursively = False
×
223

UNCOV
224
            parametrized_args_fields = tuple(
×
225
                (field_name, field_value)
226
                for field_name, _, field_value in parametrized_args
227
                # Exclude any parametrize group
228
                if not (isinstance(field_value, Parametrize) and field_value.is_group)
229
            )
UNCOV
230
            expanded_fields: dict[str, Any] = dict(non_parametrized + parametrized_args_fields)
×
UNCOV
231
            expanded_address = address.parametrize(expanded_parameters, replace=True)
×
232

UNCOV
233
            if expand_recursively:
×
UNCOV
234
                assert parametrize_group is not None  # Type narrowing to satisfy mypy.
×
235
                # Expand nested parametrize within a parametrized group.
UNCOV
236
                for grouped_address, grouped_fields in cls._expand(
×
237
                    expanded_address,
238
                    parametrize_group.kwargs,
239
                    _parametrization_group_prefix=_parametrization_group_prefix
240
                    + parametrize_group.group_name
241
                    + "-",
242
                ):
UNCOV
243
                    cls._check_conflicting(
×
244
                        {
245
                            name
246
                            for name in grouped_fields.keys()
247
                            if isinstance(fields.get(name), Parametrize)
248
                        }
249
                    )
UNCOV
250
                    yield (
×
251
                        expanded_address.parametrize(grouped_address.parameters),
252
                        expanded_fields | dict(grouped_fields),
253
                    )
254
            else:
UNCOV
255
                if parametrize_group is not None:
×
UNCOV
256
                    expanded_fields |= dict(parametrize_group.kwargs)
×
UNCOV
257
                yield expanded_address, expanded_fields
×
258

259
    @staticmethod
5✔
260
    def _collect_parametrizations(
5✔
261
        fields: Mapping[str, Any | Parametrize],
262
    ) -> Mapping[str | None, list[tuple[str, Parametrize]]]:
UNCOV
263
        parametrizations = defaultdict(list)
×
UNCOV
264
        for field_name, v in fields.items():
×
UNCOV
265
            if not isinstance(v, Parametrize):
×
UNCOV
266
                continue
×
UNCOV
267
            group_name = None if not v.is_group else v.group_name
×
UNCOV
268
            parametrizations[group_name].append((field_name, v))
×
UNCOV
269
        return parametrizations
×
270

271
    @staticmethod
5✔
272
    def _check_parametrizations(
5✔
273
        parametrizations: Mapping[str | None, list[tuple[str, Parametrize]]],
274
    ) -> None:
UNCOV
275
        for group_name, groups in parametrizations.items():
×
UNCOV
276
            if group_name is not None and len(groups) > 1:
×
UNCOV
277
                group = Parametrize._combine(*(group for _, group in groups))
×
UNCOV
278
                groups.clear()
×
UNCOV
279
                groups.append(("combined", group))
×
280

UNCOV
281
        parametrize_field_names = {field_name for field_name, v in parametrizations.get(None, ())}
×
UNCOV
282
        parametrize_field_names_from_groups = {
×
283
            field_name
284
            for group_name, groups in parametrizations.items()
285
            if group_name is not None
286
            for field_name in groups[0][1].kwargs.keys()
287
        }
UNCOV
288
        Parametrize._check_conflicting(
×
289
            parametrize_field_names.intersection(parametrize_field_names_from_groups)
290
        )
291

292
    @staticmethod
5✔
293
    def _check_conflicting(conflicting: abc.Collection[str]) -> None:
5✔
UNCOV
294
        if conflicting:
×
UNCOV
295
            raise ValueError(
×
296
                softwrap(
297
                    f"""
298
                    Conflicting parametrizations for {pluralize(len(conflicting), "field", include_count=False)}:
299
                    {", ".join(map(repr, sorted(conflicting)))}
300
                    """
301
                )
302
            )
303

304
    @staticmethod
5✔
305
    def _combine(head: Parametrize, *tail: Parametrize) -> Parametrize:
5✔
UNCOV
306
        return reduce(operator.add, tail, head)
×
307

308
    def __add__(self, other: Parametrize) -> Parametrize:
5✔
UNCOV
309
        if not isinstance(other, Parametrize):
×
310
            raise TypeError(f"Can not combine {self} with {other!r}")
×
UNCOV
311
        if self.merge_behaviour is Parametrize._MergeBehaviour.replace:
×
UNCOV
312
            return other
×
UNCOV
313
        if other.merge_behaviour is Parametrize._MergeBehaviour.replace:
×
314
            return self
×
UNCOV
315
        if self.is_group and other.is_group:
×
UNCOV
316
            raise ValueError(f"Parametrization group name is not unique: {self.group_name!r}")
×
317
        raise ValueError(f"Can not combine parametrizations: {self} | {other}")
×
318

319
    def __repr__(self) -> str:
5✔
UNCOV
320
        strs = [repr(s) for s in self.args]
×
UNCOV
321
        strs.extend(f"{alias}={value!r}" for alias, value in self.kwargs.items())
×
UNCOV
322
        return f"parametrize({', '.join(strs)})"
×
323

324

325
@dataclass(frozen=True)
5✔
326
class _TargetParametrization:
5✔
327
    original_target: Target | None
5✔
328
    parametrization: FrozenDict[Address, Target]
5✔
329

330
    @property
5✔
331
    def all(self) -> Iterator[Target]:
5✔
332
        if self.original_target:
×
333
            yield self.original_target
×
334
        yield from self.parametrization.values()
×
335

336
    def get(self, address: Address) -> Target | None:
5✔
337
        """Find the Target with an exact Address match, if any."""
UNCOV
338
        if self.original_target and self.original_target.address == address:
×
UNCOV
339
            return self.original_target
×
UNCOV
340
        return self.parametrization.get(address)
×
341

342

343
# TODO: This is not the right name for this class, nor the best place for it to live. But it is
344
# consumed by both `pants.engine.internals.graph` and `pants.engine.internals.build_files`, and
345
# shouldn't live in `pants.engine.target` (yet? needs more stabilization).
346
@dataclass(frozen=True)
5✔
347
class _TargetParametrizationsRequest(EngineAwareParameter):
5✔
348
    address: Address
5✔
349
    description_of_origin: str = dataclasses.field(hash=False, compare=False)
5✔
350

351
    def __post_init__(self) -> None:
5✔
352
        if self.address.is_parametrized or self.address.is_generated_target:
2✔
353
            raise ValueError(
×
354
                softwrap(
355
                    f"""
356
                    Cannot create {self.__class__.__name__} on a generated or parametrized target.
357

358
                    Self: {self}
359
                    """
360
                )
361
            )
362

363
    def debug_hint(self) -> str:
5✔
364
        return self.address.spec
×
365

366

367
# TODO: See TODO on _TargetParametrizationsRequest about naming this.
368
class _TargetParametrizations(Collection[_TargetParametrization]):
5✔
369
    """All parametrizations and generated targets for a single input Address.
370

371
    If a Target has been parametrized, the original Target might _not_ be present, due to no Target
372
    being addressable at the un-parameterized Address.
373
    """
374

375
    @property
5✔
376
    def all(self) -> Iterator[Target]:
5✔
377
        """Iterates over all Target instances which are valid after parametrization."""
378
        for parametrization in self:
×
379
            yield from parametrization.all
×
380

381
    @property
5✔
382
    def parametrizations(self) -> dict[Address, Target]:
5✔
383
        """Returns a merged dict of all generated/parametrized instances, excluding originals."""
384
        return {
2✔
385
            a: t for parametrization in self for a, t in parametrization.parametrization.items()
386
        }
387

388
    def generated_for(self, address: Address) -> FrozenDict[Address, Target]:
5✔
389
        """Find all Targets generated by the given generator Address."""
390
        assert not address.is_generated_target
×
391
        for parametrization in self:
×
392
            if (
×
393
                parametrization.original_target
394
                and parametrization.original_target.address == address
395
            ):
396
                return parametrization.parametrization
×
397

398
        raise self._bare_address_error(address)
×
399

400
    def get(
5✔
401
        self,
402
        address: Address,
403
        target_types_to_generate_requests: TargetTypesToGenerateTargetsRequests | None = None,
404
    ) -> Target | None:
405
        """Find the Target with an exact Address match, if any."""
UNCOV
406
        for parametrization in self:
×
UNCOV
407
            instance = parametrization.get(address)
×
UNCOV
408
            if instance is not None:
×
UNCOV
409
                return instance
×
410

411
        # TODO: This is an accommodation to allow using file/generator Addresses for
412
        # non-generator atom targets. See https://github.com/pantsbuild/pants/issues/14419.
UNCOV
413
        if target_types_to_generate_requests and address.is_generated_target:
×
414
            base_address = address.maybe_convert_to_target_generator()
×
415
            original_target = self.get(base_address, target_types_to_generate_requests)
×
416
            if original_target and not target_types_to_generate_requests.is_generator(
×
417
                original_target
418
            ):
419
                return original_target
×
420

UNCOV
421
        return None
×
422

423
    def get_all_superset_targets(self, address: Address) -> Iterator[Address]:
5✔
424
        """Yield the input address itself, or any parameterized addresses which are a superset of
425
        the input address.
426

427
        For example, an input address `dir:tgt` may yield `(dir:tgt@k=v1, dir:tgt@k=v2)`.
428

429
        If no targets are a match, will yield nothing.
430
        """
431
        # Check for exact matches.
UNCOV
432
        if self.get(address) is not None:
×
UNCOV
433
            yield address
×
UNCOV
434
            return
×
435

UNCOV
436
        for parametrization in self:
×
UNCOV
437
            if parametrization.original_target is not None and address.is_parametrized_subset_of(
×
438
                parametrization.original_target.address
439
            ):
440
                yield parametrization.original_target.address
×
441

UNCOV
442
            for parametrized_tgt in parametrization.parametrization.values():
×
UNCOV
443
                if address.is_parametrized_subset_of(parametrized_tgt.address):
×
UNCOV
444
                    yield parametrized_tgt.address
×
445

446
    def get_subset(
5✔
447
        self,
448
        address: Address,
449
        consumer: Target,
450
        field_defaults: FieldDefaults,
451
        target_types_to_generate_requests: TargetTypesToGenerateTargetsRequests,
452
    ) -> Target:
453
        """Find the Target with the given Address, or with fields matching the given consumer."""
454
        # Check for exact matches.
455
        instance = self.get(address, target_types_to_generate_requests)
×
456
        if instance is not None:
×
457
            return instance
×
458

459
        def remaining_fields_match(candidate: Target) -> bool:
×
460
            """Returns true if all Fields absent from the candidate's Address match the consumer."""
461
            unspecified_param_field_names = {
×
462
                key for key in candidate.address.parameters.keys() if key not in address.parameters
463
            }
464
            return all(
×
465
                _concrete_fields_are_equivalent(
466
                    field_defaults,
467
                    consumer=consumer,
468
                    candidate_field=field,
469
                )
470
                for field_type, field in candidate.field_values.items()
471
                if field_type.alias in unspecified_param_field_names
472
            )
473

474
        for parametrization in self:
×
475
            # If the given Address is a subset-match of the parametrization's original Target
476
            # (meaning that the user specified an un-parameterized generator Address), then we
477
            # need to match against one of the generated Targets instead (because a parametrized
478
            # generator does not keep its Fields).
479
            if (
×
480
                parametrization.original_target
481
                and address.is_parametrized_subset_of(parametrization.original_target.address)
482
                and parametrization.parametrization
483
                and remaining_fields_match(next(iter(parametrization.parametrization.values())))
484
            ):
485
                return parametrization.original_target
×
486

487
        consumer_parametrize_group = consumer.address.parameters.get("parametrize")
×
488

489
        def matching_parametrize_group(candidate: Target) -> bool:
×
490
            return candidate.address.parameters.get("parametrize") == consumer_parametrize_group
×
491

492
        for candidate in sorted(
×
493
            self.parametrizations.values(), key=matching_parametrize_group, reverse=True
494
        ):
495
            # Else, see whether any of the generated targets match, preferring a matching
496
            # parametrize group when available.
497
            if address.is_parametrized_subset_of(candidate.address) and remaining_fields_match(
×
498
                candidate
499
            ):
500
                return candidate
×
501

502
        raise ValueError(
×
503
            f"The explicit dependency `{address}` of the target at `{consumer.address}` does "
504
            "not provide enough address parameters to identify which parametrization of the "
505
            "dependency target should be used.\n"
506
            f"Target `{address.maybe_convert_to_target_generator()}` can be addressed as:\n"
507
            f"{bullet_list(str(t.address) for t in self.all)}"
508
        )
509

510
    def generated_or_generator(self, maybe_generator: Address) -> Iterator[Target]:
5✔
511
        """Yield either the Target, or the generated Targets for the given Address."""
512
        for parametrization in self:
×
513
            if (
×
514
                not parametrization.original_target
515
                or parametrization.original_target.address != maybe_generator
516
            ):
517
                continue
×
518
            if parametrization.parametrization:
×
519
                # Generated Targets.
520
                yield from parametrization.parametrization.values()
×
521
            else:
522
                # Did not generate targets.
523
                yield parametrization.original_target
×
524
            return
×
525

526
        raise self._bare_address_error(maybe_generator)
×
527

528
    def _bare_address_error(self, address) -> ValueError:
5✔
529
        return ValueError(
×
530
            "A `parametrized` target cannot be consumed without its parameters specified.\n"
531
            f"Target `{address}` can be addressed as:\n"
532
            f"{bullet_list(str(t.address) for t in self.all)}"
533
        )
534

535

536
def _concrete_fields_are_equivalent(
5✔
537
    field_defaults: FieldDefaults, *, consumer: Target, candidate_field: Field
538
) -> bool:
UNCOV
539
    candidate_field_type = type(candidate_field)
×
UNCOV
540
    candidate_field_value = field_defaults.value_or_default(candidate_field)
×
541

UNCOV
542
    if consumer.has_field(candidate_field_type):
×
UNCOV
543
        return cast(
×
544
            bool,
545
            field_defaults.value_or_default(consumer[candidate_field_type])
546
            == candidate_field_value,
547
        )
548
    # Else, see if the consumer has a field that is a superclass of `candidate_field_value`, to
549
    # handle https://github.com/pantsbuild/pants/issues/16190. This is only safe because we are
550
    # confident that both `candidate_field_type` and the fields from `consumer` are _concrete_,
551
    # meaning they are not abstract templates like `StringField`.
UNCOV
552
    superclass = next(
×
553
        (
554
            consumer_field
555
            for consumer_field in consumer.field_types
556
            if isinstance(candidate_field, consumer_field)
557
        ),
558
        None,
559
    )
UNCOV
560
    if superclass is None:
×
UNCOV
561
        return False
×
UNCOV
562
    return cast(
×
563
        bool, field_defaults.value_or_default(consumer[superclass]) == candidate_field_value
564
    )
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