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

pantsbuild / pants / 20533309613

27 Dec 2025 02:49AM UTC coverage: 80.283%. First build
20533309613

push

github

web-flow
partition protobuf targets for dependency inference using any "resolve-like" fields (#21918)

## Background

As reported in https://github.com/pantsbuild/pants/issues/21409,
protobuf dependency inference cannot handle resolve-like fields which
are attached to `protobuf_source` target types by plugins. Basically,
multiple targets own the same source file but in different resolves, but
the existing code does not know about resolves and thus has no way to
partition the targets into distinct groups and apply dependency
inference within each group.

## Solution

Partition the protobuf targets by any "resolve-like" field found
registered on a `protobuf_source` target. The new `ResolveLikeField`
mix-in is used to detect fields which are "resolve like." The dependency
inference logic then uses the new `ResolveLikeFieldToValueRequest` union
to query the applicable language backend for what the actual resolve
name is so it can be used for partitioning.

The Python and JVM backends support `ResolveLikeField`.

225 of 297 new or added lines in 11 files covered. (75.76%)

78750 of 98090 relevant lines covered (80.28%)

3.36 hits per line

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

39.17
/src/python/pants/backend/python/target_types_rules.py
1
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
"""Rules for the core Python target types.
5

6
This is a separate module to avoid circular dependencies. Note that all types used by call sites are
7
defined in `target_types.py`.
8
"""
9

10
import dataclasses
12✔
11
import logging
12✔
12
import os.path
12✔
13
from collections import defaultdict
12✔
14
from collections.abc import Callable, Generator
12✔
15
from dataclasses import dataclass
12✔
16
from itertools import chain
12✔
17
from typing import DefaultDict, cast
12✔
18

19
from pants.backend.python.dependency_inference.module_mapper import (
12✔
20
    PythonModuleOwners,
21
    PythonModuleOwnersRequest,
22
    map_module_to_address,
23
)
24
from pants.backend.python.dependency_inference.rules import import_rules
12✔
25
from pants.backend.python.dependency_inference.subsystem import (
12✔
26
    AmbiguityResolution,
27
    PythonInferSubsystem,
28
)
29
from pants.backend.python.subsystems.setup import PythonSetup
12✔
30
from pants.backend.python.target_types import (
12✔
31
    EntryPoint,
32
    InterpreterConstraintsField,
33
    PexBinariesGeneratorTarget,
34
    PexBinary,
35
    PexBinaryDependenciesField,
36
    PexEntryPointField,
37
    PexEntryPointsField,
38
    PythonDistributionDependenciesField,
39
    PythonDistributionEntryPoint,
40
    PythonDistributionEntryPointsField,
41
    PythonFilesGeneratorSettingsRequest,
42
    PythonProvidesField,
43
    PythonResolveField,
44
    PythonResolveLikeFieldToValueRequest,
45
    ResolvedPexEntryPoint,
46
    ResolvedPythonDistributionEntryPoints,
47
    ResolvePexEntryPointRequest,
48
    ResolvePythonDistributionEntryPointsRequest,
49
)
50
from pants.backend.python.util_rules.interpreter_constraints import interpreter_constraints_contains
12✔
51
from pants.core.target_types import ResolveLikeFieldToValueRequest, ResolveLikeFieldToValueResult
12✔
52
from pants.core.util_rules.unowned_dependency_behavior import (
12✔
53
    UnownedDependencyError,
54
    UnownedDependencyUsage,
55
)
56
from pants.engine.addresses import Address, Addresses, UnparsedAddressInputs
12✔
57
from pants.engine.fs import GlobMatchErrorBehavior, PathGlobs
12✔
58
from pants.engine.internals.graph import (
12✔
59
    determine_explicitly_provided_dependencies,
60
    resolve_target,
61
    resolve_targets,
62
    resolve_unparsed_address_inputs,
63
)
64
from pants.engine.intrinsics import path_globs_to_paths
12✔
65
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
12✔
66
from pants.engine.target import (
12✔
67
    DependenciesRequest,
68
    ExplicitlyProvidedDependencies,
69
    FieldDefaultFactoryRequest,
70
    FieldDefaultFactoryResult,
71
    FieldSet,
72
    GeneratedTargets,
73
    GenerateTargetsRequest,
74
    InferDependenciesRequest,
75
    InferredDependencies,
76
    InvalidFieldException,
77
    TargetFilesGeneratorSettings,
78
    TargetFilesGeneratorSettingsRequest,
79
    ValidatedDependencies,
80
    ValidateDependenciesRequest,
81
    WrappedTargetRequest,
82
)
83
from pants.engine.unions import UnionMembership, UnionRule
12✔
84
from pants.source.source_root import SourceRootRequest, get_source_root
12✔
85
from pants.util.docutil import doc_url
12✔
86
from pants.util.frozendict import FrozenDict
12✔
87
from pants.util.ordered_set import OrderedSet
12✔
88
from pants.util.strutil import bullet_list, softwrap
12✔
89

90
logger = logging.getLogger(__name__)
12✔
91

92

93
class SetupPyError(Exception):
12✔
94
    def __init__(self, msg: str):
12✔
95
        super().__init__(f"{msg} See {doc_url('docs/python/overview/building-distributions')}.")
1✔
96

97

98
class InvalidEntryPoint(SetupPyError, InvalidFieldException):
12✔
99
    """Indicates that a specified binary entry point was invalid."""
100

101

102
@rule
12✔
103
async def python_files_generator_settings(
12✔
104
    _: PythonFilesGeneratorSettingsRequest,
105
    python_infer: PythonInferSubsystem,
106
) -> TargetFilesGeneratorSettings:
107
    return TargetFilesGeneratorSettings(add_dependencies_on_all_siblings=not python_infer.imports)
×
108

109

110
# -----------------------------------------------------------------------------------------------
111
# `pex_binary` target generation rules
112
# -----------------------------------------------------------------------------------------------
113

114

115
class GenerateTargetsFromPexBinaries(GenerateTargetsRequest):
12✔
116
    # TODO: This can be deprecated in favor of `parametrize`.
117
    generate_from = PexBinariesGeneratorTarget
12✔
118

119

120
@rule
12✔
121
async def generate_targets_from_pex_binaries(
12✔
122
    request: GenerateTargetsFromPexBinaries,
123
    union_membership: UnionMembership,
124
) -> GeneratedTargets:
125
    generator_addr = request.template_address
×
126
    entry_points_field = request.generator[PexEntryPointsField].value or []
×
127
    overrides = request.require_unparametrized_overrides()
×
128

129
    # Note that we don't check for overlap because it seems unlikely to be a problem.
130
    # If it does, we should add this check. (E.g. `path.to.app` and `path/to/app.py`)
131

132
    def create_pex_binary(entry_point_spec: str) -> PexBinary:
×
133
        return PexBinary(
×
134
            {
135
                PexEntryPointField.alias: entry_point_spec,
136
                **request.template,
137
                # Note that overrides comes last to make sure that it indeed overrides.
138
                **overrides.pop(entry_point_spec, {}),
139
            },
140
            # ":" is a forbidden character in target names
141
            generator_addr.create_generated(entry_point_spec.replace(":", "-")),
142
            union_membership,
143
            residence_dir=generator_addr.spec_path,
144
        )
145

146
    pex_binaries = [create_pex_binary(entry_point) for entry_point in entry_points_field]
×
147

148
    if overrides:
×
149
        raise InvalidFieldException(
×
150
            softwrap(
151
                f"""
152
                Unused key in the `overrides` field for {generator_addr}:
153
                {sorted(overrides)}
154

155
                Tip: if you'd like to override a field's value for every `{PexBinary.alias}` target
156
                generated by this target, change the field directly on this target rather than using
157
                the `overrides` field.
158
                """
159
            )
160
        )
161

162
    return GeneratedTargets(request.generator, pex_binaries)
×
163

164

165
# -----------------------------------------------------------------------------------------------
166
# `pex_binary` rules
167
# -----------------------------------------------------------------------------------------------
168

169

170
@rule(desc="Determining the entry point for a `pex_binary` target")
12✔
171
async def resolve_pex_entry_point(request: ResolvePexEntryPointRequest) -> ResolvedPexEntryPoint:
12✔
172
    ep_val = request.entry_point_field.value
×
173
    if ep_val is None:
×
174
        return ResolvedPexEntryPoint(None, file_name_used=False)
×
175
    address = request.entry_point_field.address
×
176

177
    # We support several different schemes:
178
    #  1) `path.to.module` => preserve exactly.
179
    #  2) `path.to.module:func` => preserve exactly.
180
    #  3) `app.py` => convert into `path.to.app`.
181
    #  4) `app.py:func` => convert into `path.to.app:func`.
182

183
    # If it's already a module (cases #1 and #2), simply use that. Otherwise, convert the file name
184
    # into a module path (cases #3 and #4).
185
    if not ep_val.module.endswith(".py"):
×
186
        return ResolvedPexEntryPoint(ep_val, file_name_used=False)
×
187

188
    # Use the engine to validate that the file exists and that it resolves to only one file.
189
    full_glob = os.path.join(address.spec_path, ep_val.module)
×
190
    entry_point_paths = await path_globs_to_paths(
×
191
        PathGlobs(
192
            [full_glob],
193
            glob_match_error_behavior=GlobMatchErrorBehavior.error,
194
            description_of_origin=f"{address}'s `{request.entry_point_field.alias}` field",
195
        )
196
    )
197

198
    # We will have already raised if the glob did not match, i.e. if there were no files. But
199
    # we need to check if they used a file glob (`*` or `**`) that resolved to >1 file.
200
    if len(entry_point_paths.files) != 1:
×
201
        raise InvalidFieldException(
×
202
            softwrap(
203
                f"""
204
                Multiple files matched for the `{request.entry_point_field.alias}`
205
                {ep_val.spec!r} for the target {address}, but only one file expected. Are you using
206
                a glob, rather than a file name?
207

208
                All matching files: {list(entry_point_paths.files)}.
209
                """
210
            )
211
        )
212
    entry_point_path = entry_point_paths.files[0]
×
213
    source_root = await get_source_root(SourceRootRequest.for_file(entry_point_path))
×
214
    stripped_source_path = os.path.relpath(entry_point_path, source_root.path)
×
215
    module_base, _ = os.path.splitext(stripped_source_path)
×
216
    normalized_path = module_base.replace(os.path.sep, ".")
×
217
    return ResolvedPexEntryPoint(
×
218
        dataclasses.replace(ep_val, module=normalized_path), file_name_used=True
219
    )
220

221

222
@dataclass(frozen=True)
12✔
223
class PexBinaryEntryPointDependencyInferenceFieldSet(FieldSet):
12✔
224
    required_fields = (PexBinaryDependenciesField, PexEntryPointField, PythonResolveField)
12✔
225

226
    dependencies: PexBinaryDependenciesField
12✔
227
    entry_point: PexEntryPointField
12✔
228
    resolve: PythonResolveField
12✔
229

230

231
class InferPexBinaryEntryPointDependency(InferDependenciesRequest):
12✔
232
    infer_from = PexBinaryEntryPointDependencyInferenceFieldSet
12✔
233

234

235
@rule(desc="Inferring dependency from the pex_binary `entry_point` field")
12✔
236
async def infer_pex_binary_entry_point_dependency(
12✔
237
    request: InferPexBinaryEntryPointDependency,
238
    python_infer_subsystem: PythonInferSubsystem,
239
    python_setup: PythonSetup,
240
) -> InferredDependencies:
241
    if not python_infer_subsystem.entry_points:
×
242
        return InferredDependencies([])
×
243

244
    entry_point_field = request.field_set.entry_point
×
245
    if entry_point_field.value is None:
×
246
        return InferredDependencies([])
×
247

248
    explicitly_provided_deps, entry_point = await concurrently(
×
249
        determine_explicitly_provided_dependencies(
250
            **implicitly(DependenciesRequest(request.field_set.dependencies))
251
        ),
252
        resolve_pex_entry_point(ResolvePexEntryPointRequest(entry_point_field)),
253
    )
254
    if entry_point.val is None:
×
255
        return InferredDependencies([])
×
256

257
    # Only set locality if needed, to avoid unnecessary rule graph memoization misses.
258
    # When set, use the source root, which is useful in practice, but incurs fewer memoization
259
    # misses than using the full spec_path.
260
    locality = None
×
261
    if python_infer_subsystem.ambiguity_resolution == AmbiguityResolution.by_source_root:
×
262
        source_root = await get_source_root(
×
263
            SourceRootRequest.for_address(request.field_set.address)
264
        )
265
        locality = source_root.path
×
266

267
    owners = await map_module_to_address(
×
268
        PythonModuleOwnersRequest(
269
            entry_point.val.module,
270
            resolve=request.field_set.resolve.normalized_value(python_setup),
271
            locality=locality,
272
        ),
273
        **implicitly(),
274
    )
275
    address = request.field_set.address
×
276
    explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
×
277
        owners.ambiguous,
278
        address,
279
        # If the entry point was specified as a file, like `app.py`, we know the module must
280
        # live in the pex_binary's directory or subdirectory, so the owners must be ancestors.
281
        owners_must_be_ancestors=entry_point.file_name_used,
282
        import_reference="module",
283
        context=softwrap(
284
            f"""
285
            The pex_binary target {address} has the field
286
            `entry_point={repr(entry_point_field.value.spec)}`, which
287
            maps to the Python module `{entry_point.val.module}`
288
            """
289
        ),
290
    )
291
    maybe_disambiguated = explicitly_provided_deps.disambiguated(
×
292
        owners.ambiguous, owners_must_be_ancestors=entry_point.file_name_used
293
    )
294

295
    unambiguous_owners = _determine_entry_point_owner(
×
296
        maybe_disambiguated,
297
        owners,
298
        unresolved_ambiguity_handler=lambda: _handle_unresolved_pex_entrypoint(
299
            address,
300
            entry_point_field.value,
301
            owners.ambiguous,
302
            python_infer_subsystem.unowned_dependency_behavior,
303
        ),
304
    )
305
    return InferredDependencies(unambiguous_owners)
×
306

307

308
def _determine_entry_point_owner(
12✔
309
    maybe_disambiguated: Address | None,
310
    owners: PythonModuleOwners,
311
    unresolved_ambiguity_handler: Callable[[], None],
312
) -> tuple[Address, ...]:
313
    """Determine what should be the unambiguous owner for a PEX's entrypoint.
314

315
    This might be empty.
316
    """
317
    if owners.unambiguous:
×
318
        return owners.unambiguous
×
319
    elif maybe_disambiguated:
×
320
        return (maybe_disambiguated,)
×
321
    elif owners.ambiguous and not maybe_disambiguated:
×
322
        unresolved_ambiguity_handler()
×
323
        return ()
×
324
    else:
325
        return ()
×
326

327

328
def _handle_unresolved_pex_entrypoint(
12✔
329
    address: Address,
330
    entry_point: str,
331
    ambiguous_owners: tuple[Address, ...],
332
    unowned_dependency_behavior: UnownedDependencyUsage,
333
) -> None:
334
    """Raise an error if we could not disambiguate an entrypoint for the PEX."""
335
    msg = softwrap(
×
336
        f"""
337
        Pants cannot resolve the entrypoint for the target {address}.
338
        The entrypoint {entry_point} might refer to the following:
339

340
        {bullet_list(o.spec for o in ambiguous_owners)}
341
        """
342
    )
343
    if unowned_dependency_behavior is UnownedDependencyUsage.DoNothing:
×
344
        pass
×
345
    elif unowned_dependency_behavior is UnownedDependencyUsage.LogWarning:
×
346
        logger.warning(msg)
×
347
    else:
348
        raise UnownedDependencyError(msg)
×
349

350

351
# -----------------------------------------------------------------------------------------------
352
# `python_distribution` rules
353
# -----------------------------------------------------------------------------------------------
354

355

356
_EntryPointsDictType = dict[str, dict[str, str]]
12✔
357

358

359
def _classify_entry_points(
12✔
360
    all_entry_points: _EntryPointsDictType,
361
) -> Generator[tuple[bool, str, str, str], None, None]:
362
    """Looks at each entry point to see if it is a target address or not.
363

364
    Yields tuples: is_target, category, name, entry_point_str.
365
    """
366
    for category, entry_points in all_entry_points.items():
×
367
        for name, entry_point_str in entry_points.items():
×
368
            yield (
×
369
                entry_point_str.startswith(":") or "/" in entry_point_str,
370
                category,
371
                name,
372
                entry_point_str,
373
            )
374

375

376
@rule(desc="Determining the entry points for a `python_distribution` target")
12✔
377
async def resolve_python_distribution_entry_points(
12✔
378
    request: ResolvePythonDistributionEntryPointsRequest,
379
) -> ResolvedPythonDistributionEntryPoints:
380
    if request.entry_points_field:
×
381
        if request.entry_points_field.value is None:
×
382
            return ResolvedPythonDistributionEntryPoints()
×
383
        address = request.entry_points_field.address
×
384
        all_entry_points = cast(_EntryPointsDictType, request.entry_points_field.value)
×
385
        description_of_origin = (
×
386
            f"the `{request.entry_points_field.alias}` field from the target {address}"
387
        )
388

389
    elif request.provides_field:
×
390
        address = request.provides_field.address
×
391
        provides_field_value = cast(
×
392
            _EntryPointsDictType, request.provides_field.value.kwargs.get("entry_points") or {}
393
        )
394
        if not provides_field_value:
×
395
            return ResolvedPythonDistributionEntryPoints()
×
396

397
        all_entry_points = provides_field_value
×
398
        description_of_origin = softwrap(
×
399
            f"""
400
            the `entry_points` argument from the `{request.provides_field.alias}` field from
401
            the target {address}
402
            """
403
        )
404

405
    else:
406
        return ResolvedPythonDistributionEntryPoints()
×
407

408
    classified_entry_points = list(_classify_entry_points(all_entry_points))
×
409

410
    # Pick out all target addresses up front, so we can use concurrently later.
411
    #
412
    # This calls for a bit of trickery however (using the "y_by_x" mapping dicts), so we keep track
413
    # of which address belongs to which entry point. I.e. the `address_by_ref` and
414
    # `binary_entry_point_by_address` variables.
415

416
    target_refs = [
×
417
        entry_point_str for is_target, _, _, entry_point_str in classified_entry_points if is_target
418
    ]
419

420
    # Intermediate step, as resolve_targets() returns a deduplicated set which breaks in case of
421
    # multiple input refs that map to the same target.
422
    target_addresses = await resolve_unparsed_address_inputs(
×
423
        UnparsedAddressInputs(
424
            target_refs,
425
            owning_address=address,
426
            description_of_origin=description_of_origin,
427
        ),
428
        **implicitly(),
429
    )
430
    address_by_ref = dict(zip(target_refs, target_addresses))
×
431
    targets = await resolve_targets(**implicitly(target_addresses))
×
432

433
    # Check that we only have targets with a pex entry_point field.
434
    for target in targets:
×
435
        if not target.has_field(PexEntryPointField):
×
436
            raise InvalidEntryPoint(
×
437
                softwrap(
438
                    f"""
439
                    All target addresses in the entry_points field must be for pex_binary targets,
440
                    but the target {address} includes the value {target.address}, which has the
441
                    target type {target.alias}.
442

443
                    Alternatively, you can use a module like "project.app:main".
444
                    See {doc_url("docs/python/overview/building-distributions")}.
445
                    """
446
                )
447
            )
448

449
    binary_entry_points = await concurrently(
×
450
        resolve_pex_entry_point(ResolvePexEntryPointRequest(target[PexEntryPointField]))
451
        for target in targets
452
    )
453
    binary_entry_point_by_address = {
×
454
        target.address: entry_point for target, entry_point in zip(targets, binary_entry_points)
455
    }
456

457
    entry_points: DefaultDict[str, dict[str, PythonDistributionEntryPoint]] = defaultdict(dict)
×
458

459
    # Parse refs/replace with resolved pex entry point, and validate console entry points have function.
460
    for is_target, category, name, ref in classified_entry_points:
×
461
        owner: Address | None = None
×
462
        if is_target:
×
463
            owner = address_by_ref[ref]
×
464
            entry_point = binary_entry_point_by_address[owner].val
×
465
            if entry_point is None:
×
466
                logger.warning(
×
467
                    softwrap(
468
                        f"""
469
                        The entry point {name} in {category} references a pex_binary target {ref}
470
                        which does not set `entry_point`. Skipping.
471
                        """
472
                    )
473
                )
474
                continue
×
475
        else:
476
            entry_point = EntryPoint.parse(ref, f"{name} for {address} {category}")
×
477

478
        if category in ["console_scripts", "gui_scripts"] and not entry_point.function:
×
479
            url = "https://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point"
×
480
            raise InvalidEntryPoint(
×
481
                softwrap(
482
                    f"""
483
                    Every entry point in `{category}` for {address} must end in the format
484
                    `:my_func`, but {name} set it to {entry_point.spec!r}. For example, set
485
                    `entry_points={{"{category}": {{"{name}": "{entry_point.module}:main}} }}`. See
486
                    {url}.
487
                    """
488
                )
489
            )
490

491
        entry_points[category][name] = PythonDistributionEntryPoint(entry_point, owner)
×
492

493
    return ResolvedPythonDistributionEntryPoints(
×
494
        FrozenDict(
495
            {category: FrozenDict(entry_points) for category, entry_points in entry_points.items()}
496
        )
497
    )
498

499

500
@dataclass(frozen=True)
12✔
501
class PythonDistributionDependenciesInferenceFieldSet(FieldSet):
12✔
502
    required_fields = (
12✔
503
        PythonDistributionDependenciesField,
504
        PythonDistributionEntryPointsField,
505
        PythonProvidesField,
506
    )
507

508
    dependencies: PythonDistributionDependenciesField
12✔
509
    entry_points: PythonDistributionEntryPointsField
12✔
510
    provides: PythonProvidesField
12✔
511

512

513
class InferPythonDistributionDependencies(InferDependenciesRequest):
12✔
514
    infer_from = PythonDistributionDependenciesInferenceFieldSet
12✔
515

516

517
def get_python_distribution_entry_point_unambiguous_module_owners(
12✔
518
    address: Address,
519
    entry_point_group: str,  # group is the pypa term; aka category or namespace
520
    entry_point_name: str,
521
    entry_point: EntryPoint,
522
    explicitly_provided_deps: ExplicitlyProvidedDependencies,
523
    owners: PythonModuleOwners,
524
) -> tuple[Address, ...]:
525
    field_str = repr({entry_point_group: {entry_point_name: entry_point.spec}})
×
526
    explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
×
527
        owners.ambiguous,
528
        address,
529
        import_reference="module",
530
        context=softwrap(
531
            f"""
532
            The python_distribution target {address} has the field
533
            `entry_points={field_str}`, which maps to the Python module
534
            `{entry_point.module}`
535
            """
536
        ),
537
    )
538
    maybe_disambiguated = explicitly_provided_deps.disambiguated(owners.ambiguous)
×
539
    unambiguous_owners = owners.unambiguous or (
×
540
        (maybe_disambiguated,) if maybe_disambiguated else ()
541
    )
542
    return unambiguous_owners
×
543

544

545
@rule
12✔
546
async def infer_python_distribution_dependencies(
12✔
547
    request: InferPythonDistributionDependencies, python_infer_subsystem: PythonInferSubsystem
548
) -> InferredDependencies:
549
    """Infer dependencies that we can infer from entry points in the distribution."""
550
    if not python_infer_subsystem.entry_points:
×
551
        return InferredDependencies([])
×
552

553
    explicitly_provided_deps, distribution_entry_points, provides_entry_points = await concurrently(
×
554
        determine_explicitly_provided_dependencies(
555
            **implicitly(DependenciesRequest(request.field_set.dependencies))
556
        ),
557
        resolve_python_distribution_entry_points(
558
            ResolvePythonDistributionEntryPointsRequest(
559
                entry_points_field=request.field_set.entry_points
560
            )
561
        ),
562
        resolve_python_distribution_entry_points(
563
            ResolvePythonDistributionEntryPointsRequest(provides_field=request.field_set.provides)
564
        ),
565
    )
566

567
    address = request.field_set.address
×
568
    all_module_entry_points = [
×
569
        (category, name, entry_point)
570
        for category, entry_points in chain(
571
            distribution_entry_points.explicit_modules.items(),
572
            provides_entry_points.explicit_modules.items(),
573
        )
574
        for name, entry_point in entry_points.items()
575
    ]
576
    all_module_owners = iter(
×
577
        await concurrently(
578
            map_module_to_address(
579
                PythonModuleOwnersRequest(entry_point.module, resolve=None), **implicitly()
580
            )
581
            for _, _, entry_point in all_module_entry_points
582
        )
583
    )
584
    module_owners: OrderedSet[Address] = OrderedSet()
×
585
    for (category, name, entry_point), owners in zip(all_module_entry_points, all_module_owners):
×
586
        module_owners.update(
×
587
            get_python_distribution_entry_point_unambiguous_module_owners(
588
                address, category, name, entry_point, explicitly_provided_deps, owners
589
            )
590
        )
591

592
    return InferredDependencies(
×
593
        Addresses(module_owners)
594
        + distribution_entry_points.pex_binary_addresses
595
        + provides_entry_points.pex_binary_addresses
596
    )
597

598

599
# -----------------------------------------------------------------------------------------------
600
# Dynamic Field defaults
601
# -----------------------------------------------------------------------------------------------
602

603

604
class PythonResolveFieldDefaultFactoryRequest(FieldDefaultFactoryRequest):
12✔
605
    field_type = PythonResolveField
12✔
606

607

608
@rule
12✔
609
async def python_resolve_field_default_factory(
12✔
610
    request: PythonResolveFieldDefaultFactoryRequest,
611
    python_setup: PythonSetup,
612
) -> FieldDefaultFactoryResult:
613
    return FieldDefaultFactoryResult(lambda f: f.normalized_value(python_setup))
×
614

615

616
# -----------------------------------------------------------------------------------------------
617
# Dependency validation
618
# -----------------------------------------------------------------------------------------------
619

620

621
@dataclass(frozen=True)
12✔
622
class DependencyValidationFieldSet(FieldSet):
12✔
623
    required_fields = (InterpreterConstraintsField,)
12✔
624

625
    interpreter_constraints: InterpreterConstraintsField
12✔
626
    resolve: PythonResolveField | None = None
12✔
627

628

629
class PythonValidateDependenciesRequest(ValidateDependenciesRequest):
12✔
630
    field_set_type = DependencyValidationFieldSet
12✔
631

632

633
@rule
12✔
634
async def validate_python_dependencies(
12✔
635
    request: PythonValidateDependenciesRequest,
636
    python_setup: PythonSetup,
637
) -> ValidatedDependencies:
638
    dependencies = await concurrently(
×
639
        resolve_target(
640
            WrappedTargetRequest(
641
                d, description_of_origin=f"the dependencies of {request.field_set.address}"
642
            ),
643
            **implicitly(),
644
        )
645
        for d in request.dependencies
646
    )
647

648
    # Validate that the ICs for dependencies are all compatible with our own.
649
    target_ics = request.field_set.interpreter_constraints.value_or_configured_default(
×
650
        python_setup, request.field_set.resolve
651
    )
652
    non_subset_items = []
×
653
    for dep in dependencies:
×
654
        if not dep.target.has_field(InterpreterConstraintsField):
×
655
            continue
×
656
        dep_ics = dep.target[InterpreterConstraintsField].value_or_configured_default(
×
657
            python_setup,
658
            dep.target[PythonResolveField] if dep.target.has_field(PythonResolveField) else None,
659
        )
660
        if not interpreter_constraints_contains(
×
661
            dep_ics, target_ics, python_setup.interpreter_versions_universe
662
        ):
663
            non_subset_items.append(f"{dep_ics}: {dep.target.address}")
×
664

665
    if non_subset_items:
×
666
        raise InvalidFieldException(
×
667
            softwrap(
668
                f"""
669
            The target {request.field_set.address} has the `interpreter_constraints` {target_ics},
670
            which are not a subset of the `interpreter_constraints` of some of its dependencies:
671

672
            {bullet_list(sorted(non_subset_items))}
673

674
            To fix this, you should likely adjust {request.field_set.address}'s
675
            `interpreter_constraints` to match the narrowest range in the above list.
676
            """
677
            )
678
        )
679

680
    return ValidatedDependencies()
×
681

682

683
@rule
12✔
684
async def python_get_resolve_from_resolve_like_field_request(
12✔
685
    request: PythonResolveLikeFieldToValueRequest, python_setup: PythonSetup
686
) -> ResolveLikeFieldToValueResult:
NEW
687
    if not python_setup.enable_resolves:
×
NEW
688
        return ResolveLikeFieldToValueResult(value=None)
×
NEW
689
    resolve = request.target[PythonResolveField].normalized_value(python_setup)
×
NEW
690
    return ResolveLikeFieldToValueResult(value=resolve)
×
691

692

693
def rules():
12✔
694
    return (
12✔
695
        *collect_rules(),
696
        *import_rules(),
697
        UnionRule(FieldDefaultFactoryRequest, PythonResolveFieldDefaultFactoryRequest),
698
        UnionRule(TargetFilesGeneratorSettingsRequest, PythonFilesGeneratorSettingsRequest),
699
        UnionRule(GenerateTargetsRequest, GenerateTargetsFromPexBinaries),
700
        UnionRule(InferDependenciesRequest, InferPexBinaryEntryPointDependency),
701
        UnionRule(InferDependenciesRequest, InferPythonDistributionDependencies),
702
        UnionRule(ValidateDependenciesRequest, PythonValidateDependenciesRequest),
703
        UnionRule(ResolveLikeFieldToValueRequest, PythonResolveLikeFieldToValueRequest),
704
    )
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