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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

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

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
import json
×
UNCOV
7
from dataclasses import dataclass
×
UNCOV
8
from pathlib import PurePath
×
9

UNCOV
10
from pants.backend.docker.target_types import DockerImageSourceField
×
UNCOV
11
from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs
×
UNCOV
12
from pants.backend.python.subsystems.python_tool_base import PythonToolRequirementsBase
×
UNCOV
13
from pants.backend.python.target_types import EntryPoint
×
UNCOV
14
from pants.backend.python.util_rules import pex
×
UNCOV
15
from pants.backend.python.util_rules.pex import (
×
16
    VenvPex,
17
    VenvPexProcess,
18
    create_venv_pex,
19
    setup_venv_pex_process,
20
)
UNCOV
21
from pants.base.deprecated import warn_or_error
×
UNCOV
22
from pants.engine.addresses import Address
×
UNCOV
23
from pants.engine.fs import CreateDigest, Digest, FileContent
×
UNCOV
24
from pants.engine.internals.graph import hydrate_sources, resolve_target
×
UNCOV
25
from pants.engine.internals.native_engine import NativeDependenciesRequest
×
UNCOV
26
from pants.engine.intrinsics import create_digest, parse_dockerfile_info
×
UNCOV
27
from pants.engine.process import Process, execute_process_or_raise
×
UNCOV
28
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
29
from pants.engine.target import HydrateSourcesRequest, SourcesField, WrappedTargetRequest
×
UNCOV
30
from pants.option.option_types import BoolOption
×
UNCOV
31
from pants.util.docutil import bin_name, doc_url
×
UNCOV
32
from pants.util.logging import LogLevel
×
UNCOV
33
from pants.util.resources import read_resource
×
UNCOV
34
from pants.util.strutil import softwrap
×
35

UNCOV
36
_DOCKERFILE_SANDBOX_TOOL = "dockerfile_wrapper_script.py"
×
UNCOV
37
_DOCKERFILE_PACKAGE = "pants.backend.docker.subsystems"
×
38

39

UNCOV
40
class DockerfileParser(PythonToolRequirementsBase):
×
UNCOV
41
    options_scope = "dockerfile-parser"
×
UNCOV
42
    help_short = "Used to parse Dockerfile build specs to infer their dependencies."
×
43

UNCOV
44
    default_requirements = ["dockerfile>=3.2.0,<4"]
×
45

UNCOV
46
    register_interpreter_constraints = True
×
47

UNCOV
48
    default_lockfile_resource = (_DOCKERFILE_PACKAGE, "dockerfile.lock")
×
49

UNCOV
50
    use_rust_parser = BoolOption(
×
51
        default=True,
52
        help=softwrap(
53
            f"""
54
            Use the new Rust-based, multithreaded, in-process dependency parser.
55

56
            This new parser does not require the `dockerfile` dependency and thus, for instance,
57
            doesn't require Go to be installed to run on platforms for which that package doesn't
58
            provide pre-built wheels.
59

60
            If you think the new behaviour is causing problems, it is recommended that you run
61
            `{bin_name()} --dockerfile-parser-use-rust-parser=True peek :: > new-parser.json` and then
62
            `{bin_name()} --dockerfile-parser-use-rust-parser=False peek :: > old-parser.json` and compare the
63
            two results.
64

65
            If you think there is a bug, please file an issue:
66
            https://github.com/pantsbuild/pants/issues/new/choose.
67
            """
68
        ),
69
    )
70

71

UNCOV
72
@dataclass(frozen=True)
×
UNCOV
73
class ParserSetup:
×
UNCOV
74
    pex: VenvPex
×
75

76

UNCOV
77
@rule
×
UNCOV
78
async def setup_parser(dockerfile_parser: DockerfileParser) -> ParserSetup:
×
79
    parser_script_content = read_resource(_DOCKERFILE_PACKAGE, _DOCKERFILE_SANDBOX_TOOL)
×
80
    if not parser_script_content:
×
81
        raise ValueError(
×
82
            f"Unable to find source to {_DOCKERFILE_SANDBOX_TOOL!r} in {_DOCKERFILE_PACKAGE}."
83
        )
84

85
    parser_content = FileContent(
×
86
        path="__pants_df_parser.py",
87
        content=parser_script_content,
88
        is_executable=True,
89
    )
90
    parser_digest = await create_digest(CreateDigest([parser_content]))
×
91

92
    parser_pex = await create_venv_pex(
×
93
        **implicitly(
94
            dockerfile_parser.to_pex_request(
95
                main=EntryPoint(PurePath(parser_content.path).stem), sources=parser_digest
96
            )
97
        )
98
    )
99
    return ParserSetup(parser_pex)
×
100

101

UNCOV
102
@dataclass(frozen=True)
×
UNCOV
103
class DockerfileParseRequest:
×
UNCOV
104
    sources_digest: Digest
×
UNCOV
105
    args: tuple[str, ...]
×
106

107

UNCOV
108
@rule
×
UNCOV
109
async def setup_process_for_parse_dockerfile(
×
110
    request: DockerfileParseRequest, parser: ParserSetup
111
) -> Process:
112
    process = await setup_venv_pex_process(
×
113
        VenvPexProcess(
114
            parser.pex,
115
            argv=request.args,
116
            description="Parse Dockerfile.",
117
            input_digest=request.sources_digest,
118
            level=LogLevel.DEBUG,
119
        ),
120
        **implicitly(),
121
    )
122
    return process
×
123

124

UNCOV
125
class DockerfileInfoError(Exception):
×
UNCOV
126
    pass
×
127

128

UNCOV
129
@dataclass(frozen=True)
×
UNCOV
130
class DockerfileInfo:
×
UNCOV
131
    address: Address
×
UNCOV
132
    digest: Digest
×
133

134
    # Data from the parsed Dockerfile, keep in sync with
135
    # `dockerfile_wrapper_script.py:ParsedDockerfileInfo`:
UNCOV
136
    source: str
×
UNCOV
137
    build_args: DockerBuildArgs = DockerBuildArgs()
×
UNCOV
138
    copy_source_paths: tuple[str, ...] = ()
×
UNCOV
139
    copy_build_args: DockerBuildArgs = DockerBuildArgs()
×
UNCOV
140
    from_image_build_args: DockerBuildArgs = DockerBuildArgs()
×
UNCOV
141
    version_tags: tuple[str, ...] = ()
×
142

143

UNCOV
144
@dataclass(frozen=True)
×
UNCOV
145
class DockerfileInfoRequest:
×
UNCOV
146
    address: Address
×
147

148

UNCOV
149
async def _natively_parse_dockerfile(address: Address, digest: Digest) -> DockerfileInfo:
×
150
    result = await parse_dockerfile_info(NativeDependenciesRequest(digest))
×
151
    return DockerfileInfo(
×
152
        address=address,
153
        digest=digest,
154
        source=result.source,
155
        build_args=DockerBuildArgs.from_strings(*result.build_args, duplicates_must_match=True),
156
        copy_source_paths=tuple(result.copy_source_paths),
157
        copy_build_args=DockerBuildArgs.from_strings(
158
            *result.copy_build_args, duplicates_must_match=True
159
        ),
160
        from_image_build_args=DockerBuildArgs.from_strings(
161
            *result.from_image_build_args, duplicates_must_match=True
162
        ),
163
        version_tags=tuple(result.version_tags),
164
    )
165

166

UNCOV
167
async def _legacy_parse_dockerfile(
×
168
    address: Address, digest: Digest, dockerfiles: tuple[str, ...]
169
) -> DockerfileInfo:
170
    result = await execute_process_or_raise(
×
171
        **implicitly(DockerfileParseRequest(digest, dockerfiles))
172
    )
173

174
    try:
×
175
        raw_output = result.stdout.decode("utf-8")
×
176
        outputs = json.loads(raw_output)
×
177
        assert len(outputs) == len(dockerfiles)
×
178
    except Exception as e:
×
179
        raise DockerfileInfoError(
×
180
            f"Unexpected failure to parse Dockerfiles: {', '.join(dockerfiles)}, "
181
            f"for the {address} target: {e}\nDockerfile parser output:\n{raw_output}"
182
        ) from e
183
    info = outputs[0]
×
184
    return DockerfileInfo(
×
185
        address=address,
186
        digest=digest,
187
        source=info["source"],
188
        build_args=DockerBuildArgs.from_strings(*info["build_args"], duplicates_must_match=True),
189
        copy_source_paths=tuple(info["copy_source_paths"]),
190
        copy_build_args=DockerBuildArgs.from_strings(
191
            *info["copy_build_args"], duplicates_must_match=True
192
        ),
193
        from_image_build_args=DockerBuildArgs.from_strings(
194
            *info["from_image_build_args"], duplicates_must_match=True
195
        ),
196
        version_tags=tuple(info["version_tags"]),
197
    )
198

199

UNCOV
200
@rule
×
UNCOV
201
async def parse_dockerfile(
×
202
    request: DockerfileInfoRequest, dockerfile_parser: DockerfileParser
203
) -> DockerfileInfo:
204
    wrapped_target = await resolve_target(
×
205
        WrappedTargetRequest(request.address, description_of_origin="<infallible>"), **implicitly()
206
    )
207
    target = wrapped_target.target
×
208
    sources = await hydrate_sources(
×
209
        HydrateSourcesRequest(
210
            target.get(SourcesField),
211
            for_sources_types=(DockerImageSourceField,),
212
            enable_codegen=True,
213
        ),
214
        **implicitly(),
215
    )
216

217
    dockerfiles = sources.snapshot.files
×
218
    assert len(dockerfiles) == 1, (
×
219
        f"Internal error: Expected a single source file to Dockerfile parse request {request}, "
220
        f"got: {dockerfiles}."
221
    )
222

223
    if not dockerfile_parser.use_rust_parser:
×
224
        warn_or_error(
×
225
            removal_version="2.32.0.dev1",
226
            entity="Using the old Dockerfile parser",
227
            hint=softwrap(
228
                f"""
229
                Future versions of Pants will only support the Rust-based parser for Dockerfiles. The new
230
                parser is faster and does not require installing extra dependencies.
231

232
                The `[dockerfile-parser].use_rust_parser` option is currently explicitly set to `false` to
233
                force the use of the old parser. This parser will be removed in future.
234

235
                Please remove this setting to use the new parser. If you find issues with the new parser,
236
                please let us know: <https://github.com/pantsbuild/pants/issues/new/choose>
237

238
                See {doc_url("reference/subsystems/dockerfile-parser#use_rust_parser")} for
239
                additional information.
240
                """
241
            ),
242
        )
243

244
    try:
×
245
        if dockerfile_parser.use_rust_parser:
×
246
            return await _natively_parse_dockerfile(target.address, sources.snapshot.digest)
×
247
        else:
248
            return await _legacy_parse_dockerfile(
×
249
                target.address, sources.snapshot.digest, dockerfiles
250
            )
251
    except ValueError as e:
×
252
        raise DockerfileInfoError(
×
253
            f"Error while parsing {dockerfiles[0]} for the {request.address} target: {e}"
254
        ) from e
255

256

UNCOV
257
def rules():
×
UNCOV
258
    return (
×
259
        *collect_rules(),
260
        *pex.rules(),
261
    )
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