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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

91.84
/src/python/pants/backend/docker/util_rules/dependencies.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
2✔
5

6
from dataclasses import dataclass
2✔
7
from pathlib import PurePath
2✔
8

9
from pants.backend.docker.subsystems.dockerfile_parser import (
2✔
10
    DockerfileInfoRequest,
11
    parse_dockerfile,
12
)
13
from pants.backend.docker.target_types import DockerImageDependenciesField
2✔
14
from pants.backend.docker.util_rules.docker_build_args import (
2✔
15
    DockerBuildArgsRequest,
16
    docker_build_args,
17
)
18
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
2✔
19
from pants.base.specs import FileLiteralSpec, RawSpecs
2✔
20
from pants.core.goals.package import AllPackageableTargets, OutputPathField
2✔
21
from pants.engine.addresses import Addresses, UnparsedAddressInputs
2✔
22
from pants.engine.internals.graph import resolve_targets, resolve_unparsed_address_inputs
2✔
23
from pants.engine.rules import collect_rules, implicitly, rule
2✔
24
from pants.engine.target import FieldSet, InferDependenciesRequest, InferredDependencies
2✔
25
from pants.engine.unions import UnionRule
2✔
26
from pants.util.strutil import softwrap
2✔
27

28

29
@dataclass(frozen=True)
2✔
30
class DockerInferenceFieldSet(FieldSet):
2✔
31
    required_fields = (DockerImageDependenciesField,)
2✔
32

33
    dependencies: DockerImageDependenciesField
2✔
34

35

36
class InferDockerDependencies(InferDependenciesRequest):
2✔
37
    infer_from = DockerInferenceFieldSet
2✔
38

39

40
@rule
2✔
41
async def infer_docker_dependencies(
2✔
42
    request: InferDockerDependencies, all_packageable_targets: AllPackageableTargets
43
) -> InferredDependencies:
44
    """Inspects the Dockerfile for references to known packageable targets."""
45
    dockerfile_info = await parse_dockerfile(
1✔
46
        DockerfileInfoRequest(request.field_set.address), **implicitly()
47
    )
48
    targets = await resolve_targets(**implicitly(Addresses([request.field_set.address])))
1✔
49
    build_args = await docker_build_args(
1✔
50
        DockerBuildArgsRequest(targets.expect_single()), **implicitly()
51
    )
52
    dockerfile_build_args = dockerfile_info.from_image_build_args.with_overrides(
1✔
53
        build_args
54
    ).nonempty()
55

56
    putative_image_addresses = set(
1✔
57
        await resolve_unparsed_address_inputs(
58
            UnparsedAddressInputs(
59
                dockerfile_build_args.values(),
60
                owning_address=dockerfile_info.address,
61
                description_of_origin=softwrap(
62
                    f"""
63
                    the FROM arguments from the file {dockerfile_info.source}
64
                    from the target {dockerfile_info.address}
65
                    """
66
                ),
67
                skip_invalid_addresses=True,
68
            ),
69
            **implicitly(),
70
        )
71
    )
72
    putative_copy_target_addresses = set(
1✔
73
        await resolve_unparsed_address_inputs(
74
            UnparsedAddressInputs(
75
                dockerfile_info.copy_build_args.nonempty().values(),
76
                owning_address=dockerfile_info.address,
77
                description_of_origin=softwrap(
78
                    f"""
79
                    the COPY arguments from the file {dockerfile_info.source}
80
                    from the target {dockerfile_info.address}
81
                    """
82
                ),
83
                skip_invalid_addresses=True,
84
            ),
85
            **implicitly(),
86
        )
87
    )
88
    maybe_output_paths = set(dockerfile_info.copy_source_paths) | set(
1✔
89
        dockerfile_info.copy_build_args.nonempty().values()
90
    )
91

92
    # NB: There's no easy way of knowing the output path's default file ending as there could
93
    # be none or it could be dynamic. Instead of forcing clients to tell us, we just use all the
94
    # possible ones from the Dockerfile. In rare cases we over-infer, but it is relatively harmless.
95
    # NB: The suffix gets an `or None` `pathlib` includes the ".", but `OutputPathField` doesn't
96
    # expect it (if you give it "", it'll leave a trailing ".").
97
    possible_file_endings = {PurePath(path).suffix[1:] or None for path in maybe_output_paths}
1✔
98
    inferred_addresses = []
1✔
99
    for target in all_packageable_targets:
1✔
100
        # If the target is an image we depend on, add it
101
        if target.address in putative_image_addresses:
1✔
102
            inferred_addresses.append(target.address)
1✔
103
            continue
1✔
104

105
        # If the target looks like it could generate the file we're trying to COPY
106
        output_path_field = target.get(OutputPathField)
1✔
107
        possible_output_paths = {
1✔
108
            output_path_field.value_or_default(file_ending=file_ending)
109
            for file_ending in possible_file_endings
110
        }
111
        for output_path in possible_output_paths:
1✔
UNCOV
112
            if output_path in maybe_output_paths:
×
UNCOV
113
                inferred_addresses.append(target.address)
×
UNCOV
114
                break
×
115

116
        # If the target has the same address as an ARG that will eventually be copied
117
        if target.address in putative_copy_target_addresses:
1✔
UNCOV
118
            inferred_addresses.append(target.address)
×
119

120
    # add addresses from source paths if they are files directly
121
    addresses_from_source_paths = await resolve_targets(
1✔
122
        **implicitly(
123
            RawSpecs(
124
                description_of_origin="halp",
125
                unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,
126
                file_literals=tuple(
127
                    FileLiteralSpec(e)
128
                    for e in [
129
                        *dockerfile_info.copy_source_paths,
130
                        *dockerfile_info.copy_build_args.nonempty().values(),
131
                    ]
132
                ),
133
            )
134
        )
135
    )
136

137
    inferred_addresses.extend(e.address for e in addresses_from_source_paths)
1✔
138

139
    return InferredDependencies(Addresses(inferred_addresses))
1✔
140

141

142
def rules():
2✔
143
    return [
2✔
144
        *collect_rules(),
145
        UnionRule(InferDependenciesRequest, InferDockerDependencies),
146
    ]
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