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

pantsbuild / pants / 20428083889

22 Dec 2025 09:43AM UTC coverage: 80.285% (-0.01%) from 80.296%
20428083889

Pull #21918

github

web-flow
Merge c895684e5 into 06f105be8
Pull Request #21918: [WIP] partition protobuf dependency inference by any "resolve-like" fields from plugins

191 of 263 new or added lines in 9 files covered. (72.62%)

44 existing lines in 2 files now uncovered.

78686 of 98008 relevant lines covered (80.29%)

3.65 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
13✔
11
import logging
13✔
12
import os.path
13✔
13
from collections import defaultdict
13✔
14
from collections.abc import Callable, Generator
13✔
15
from dataclasses import dataclass
13✔
16
from itertools import chain
13✔
17
from typing import DefaultDict, cast
13✔
18

19
from pants.backend.python.dependency_inference.module_mapper import (
13✔
20
    PythonModuleOwners,
21
    PythonModuleOwnersRequest,
22
    map_module_to_address,
23
)
24
from pants.backend.python.dependency_inference.rules import import_rules
13✔
25
from pants.backend.python.dependency_inference.subsystem import (
13✔
26
    AmbiguityResolution,
27
    PythonInferSubsystem,
28
)
29
from pants.backend.python.subsystems.setup import PythonSetup
13✔
30
from pants.backend.python.target_types import (
13✔
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
13✔
51
from pants.core.target_types import ResolveLikeFieldToValueRequest, ResolveLikeFieldToValueResult
13✔
52
from pants.core.util_rules.unowned_dependency_behavior import (
13✔
53
    UnownedDependencyError,
54
    UnownedDependencyUsage,
55
)
56
from pants.engine.addresses import Address, Addresses, UnparsedAddressInputs
13✔
57
from pants.engine.fs import GlobMatchErrorBehavior, PathGlobs
13✔
58
from pants.engine.internals.graph import (
13✔
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
13✔
65
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
13✔
66
from pants.engine.target import (
13✔
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
13✔
84
from pants.source.source_root import SourceRootRequest, get_source_root
13✔
85
from pants.util.docutil import doc_url
13✔
86
from pants.util.frozendict import FrozenDict
13✔
87
from pants.util.ordered_set import OrderedSet
13✔
88
from pants.util.strutil import bullet_list, softwrap
13✔
89

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

92

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

97

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

101

102
@rule
13✔
103
async def python_files_generator_settings(
13✔
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):
13✔
116
    # TODO: This can be deprecated in favor of `parametrize`.
117
    generate_from = PexBinariesGeneratorTarget
13✔
118

119

120
@rule
13✔
121
async def generate_targets_from_pex_binaries(
13✔
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")
13✔
171
async def resolve_pex_entry_point(request: ResolvePexEntryPointRequest) -> ResolvedPexEntryPoint:
13✔
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)
13✔
223
class PexBinaryEntryPointDependencyInferenceFieldSet(FieldSet):
13✔
224
    required_fields = (PexBinaryDependenciesField, PexEntryPointField, PythonResolveField)
13✔
225

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

230

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

234

235
@rule(desc="Inferring dependency from the pex_binary `entry_point` field")
13✔
236
async def infer_pex_binary_entry_point_dependency(
13✔
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(
13✔
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(
13✔
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]]
13✔
357

358

359
def _classify_entry_points(
13✔
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")
13✔
377
async def resolve_python_distribution_entry_points(
13✔
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)
13✔
501
class PythonDistributionDependenciesInferenceFieldSet(FieldSet):
13✔
502
    required_fields = (
13✔
503
        PythonDistributionDependenciesField,
504
        PythonDistributionEntryPointsField,
505
        PythonProvidesField,
506
    )
507

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

512

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

516

517
def get_python_distribution_entry_point_unambiguous_module_owners(
13✔
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
13✔
546
async def infer_python_distribution_dependencies(
13✔
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):
13✔
605
    field_type = PythonResolveField
13✔
606

607

608
@rule
13✔
609
async def python_resolve_field_default_factory(
13✔
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)
13✔
622
class DependencyValidationFieldSet(FieldSet):
13✔
623
    required_fields = (InterpreterConstraintsField,)
13✔
624

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

628

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

632

633
@rule
13✔
634
async def validate_python_dependencies(
13✔
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
13✔
684
async def python_resolve_field_to_string(
13✔
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():
13✔
694
    return (
13✔
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

© 2025 Coveralls, Inc