• 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/openapi/codegen/python/generate.py
1
# Copyright 2024 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 itertools
×
UNCOV
7
import logging
×
UNCOV
8
from collections import defaultdict
×
UNCOV
9
from collections.abc import Iterable
×
UNCOV
10
from dataclasses import dataclass
×
11

UNCOV
12
from packaging.utils import canonicalize_name
×
13

UNCOV
14
from pants.backend.codegen.utils import MissingPythonCodegenRuntimeLibrary
×
UNCOV
15
from pants.backend.openapi.codegen.python.extra_fields import (
×
16
    OpenApiPythonAdditionalPropertiesField,
17
    OpenApiPythonGeneratorNameField,
18
    OpenApiPythonSkipField,
19
)
UNCOV
20
from pants.backend.openapi.sample.resources import PETSTORE_SAMPLE_SPEC
×
UNCOV
21
from pants.backend.openapi.subsystems.openapi_generator import OpenAPIGenerator
×
UNCOV
22
from pants.backend.openapi.target_types import (
×
23
    OpenApiDocumentDependenciesField,
24
    OpenApiDocumentField,
25
    OpenApiSourceField,
26
)
UNCOV
27
from pants.backend.openapi.util_rules.generator_process import OpenAPIGeneratorProcess
×
UNCOV
28
from pants.backend.python.dependency_inference.module_mapper import AllPythonTargets
×
UNCOV
29
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
30
from pants.backend.python.target_types import (
×
31
    PrefixedPythonResolveField,
32
    PythonRequirementResolveField,
33
    PythonRequirementsField,
34
    PythonSourceField,
35
)
UNCOV
36
from pants.engine.fs import (
×
37
    EMPTY_SNAPSHOT,
38
    AddPrefix,
39
    CreateDigest,
40
    Digest,
41
    DigestSubset,
42
    Directory,
43
    FileContent,
44
    MergeDigests,
45
    PathGlobs,
46
    RemovePrefix,
47
)
UNCOV
48
from pants.engine.internals.graph import hydrate_sources
×
UNCOV
49
from pants.engine.internals.graph import transitive_targets as transitive_targets_get
×
UNCOV
50
from pants.engine.internals.native_engine import Address
×
UNCOV
51
from pants.engine.intrinsics import (
×
52
    create_digest,
53
    digest_subset_to_digest,
54
    digest_to_snapshot,
55
    get_digest_contents,
56
    merge_digests,
57
    remove_prefix,
58
)
UNCOV
59
from pants.engine.process import fallible_to_exec_result_or_raise
×
UNCOV
60
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
×
UNCOV
61
from pants.engine.target import (
×
62
    FieldSet,
63
    GeneratedSources,
64
    GenerateSourcesRequest,
65
    HydrateSourcesRequest,
66
    InferDependenciesRequest,
67
    InferredDependencies,
68
    TransitiveTargetsRequest,
69
)
UNCOV
70
from pants.engine.unions import UnionRule
×
UNCOV
71
from pants.source.source_root import SourceRootRequest, get_source_root
×
UNCOV
72
from pants.util.frozendict import FrozenDict
×
UNCOV
73
from pants.util.logging import LogLevel
×
UNCOV
74
from pants.util.pip_requirement import PipRequirement
×
UNCOV
75
from pants.util.requirements import parse_requirements_file
×
UNCOV
76
from pants.util.strutil import softwrap
×
77

UNCOV
78
logger = logging.getLogger(__name__)
×
79

80

UNCOV
81
class GeneratePythonFromOpenAPIRequest(GenerateSourcesRequest):
×
UNCOV
82
    input = OpenApiDocumentField
×
UNCOV
83
    output = PythonSourceField
×
84

85

UNCOV
86
@dataclass(frozen=True)
×
UNCOV
87
class OpenApiDocumentPythonFieldSet(FieldSet):
×
UNCOV
88
    required_fields = (OpenApiDocumentField,)
×
89

UNCOV
90
    source: OpenApiDocumentField
×
UNCOV
91
    dependencies: OpenApiDocumentDependenciesField
×
UNCOV
92
    generator_name: OpenApiPythonGeneratorNameField
×
UNCOV
93
    additional_properties: OpenApiPythonAdditionalPropertiesField
×
UNCOV
94
    skip: OpenApiPythonSkipField
×
95

96

UNCOV
97
@dataclass(frozen=True)
×
UNCOV
98
class CompileOpenApiIntoPythonRequest:
×
UNCOV
99
    input_file: str
×
UNCOV
100
    input_digest: Digest
×
UNCOV
101
    description: str
×
UNCOV
102
    generator_name: str
×
UNCOV
103
    additional_properties: FrozenDict[str, str] | None = None
×
104

105

UNCOV
106
@dataclass(frozen=True)
×
UNCOV
107
class CompiledPythonFromOpenApi:
×
UNCOV
108
    output_digest: Digest
×
UNCOV
109
    runtime_dependencies: tuple[PipRequirement, ...]
×
110

111

UNCOV
112
@rule
×
UNCOV
113
async def compile_openapi_into_python(
×
114
    request: CompileOpenApiIntoPythonRequest,
115
) -> CompiledPythonFromOpenApi:
116
    output_dir = "__gen"
×
117
    output_digest = await create_digest(CreateDigest([Directory(output_dir)]))
×
118

119
    merged_digests = await merge_digests(MergeDigests([request.input_digest, output_digest]))
×
120

121
    additional_properties: Iterable[str] = (
×
122
        itertools.chain(
123
            *[
124
                ("--additional-properties", f"{k}={v}")
125
                for k, v in request.additional_properties.items()
126
            ]
127
        )
128
        if request.additional_properties
129
        else ()
130
    )
131

132
    process = OpenAPIGeneratorProcess(
×
133
        generator_name=request.generator_name,
134
        argv=[
135
            *additional_properties,
136
            "-i",
137
            request.input_file,
138
            "-o",
139
            output_dir,
140
        ],
141
        input_digest=merged_digests,
142
        output_directories=(output_dir,),
143
        description=request.description,
144
        level=LogLevel.DEBUG,
145
    )
146

147
    result = await fallible_to_exec_result_or_raise(
×
148
        **implicitly({process: OpenAPIGeneratorProcess})
149
    )
150
    normalized_digest = await remove_prefix(RemovePrefix(result.output_digest, output_dir))
×
151

152
    requirements_digest, python_sources_digest = await concurrently(
×
153
        digest_subset_to_digest(DigestSubset(normalized_digest, PathGlobs(["requirements.txt"]))),
154
        digest_subset_to_digest(DigestSubset(normalized_digest, PathGlobs(["**/*.py"]))),
155
    )
156
    requirements_contents = await get_digest_contents(requirements_digest)
×
157
    runtime_dependencies: tuple[PipRequirement, ...] = ()
×
158
    if len(requirements_contents) > 0:
×
159
        file = requirements_contents[0]
×
160
        runtime_dependencies = tuple(
×
161
            parse_requirements_file(
162
                file.content.decode("utf-8"),
163
                rel_path=file.path,
164
            )
165
        )
166

167
    return CompiledPythonFromOpenApi(
×
168
        output_digest=python_sources_digest,
169
        runtime_dependencies=runtime_dependencies,
170
    )
171

172

UNCOV
173
@rule
×
UNCOV
174
async def generate_python_from_openapi(
×
175
    request: GeneratePythonFromOpenAPIRequest,
176
) -> GeneratedSources:
177
    field_set = OpenApiDocumentPythonFieldSet.create(request.protocol_target)
×
178
    if field_set.skip.value:
×
179
        return GeneratedSources(EMPTY_SNAPSHOT)
×
180

181
    (document_sources, transitive_targets) = await concurrently(
×
182
        hydrate_sources(HydrateSourcesRequest(field_set.source), **implicitly()),
183
        transitive_targets_get(TransitiveTargetsRequest([field_set.address]), **implicitly()),
184
    )
185

186
    document_dependencies = await concurrently(
×
187
        hydrate_sources(HydrateSourcesRequest(tgt[OpenApiSourceField]), **implicitly())
188
        for tgt in transitive_targets.dependencies
189
        if tgt.has_field(OpenApiSourceField)
190
    )
191

192
    input_digest = await merge_digests(
×
193
        MergeDigests(
194
            [
195
                document_sources.snapshot.digest,
196
                *[dependency.snapshot.digest for dependency in document_dependencies],
197
            ]
198
        )
199
    )
200

201
    gets = []
×
202
    for file in document_sources.snapshot.files:
×
203
        generator_name = field_set.generator_name.value
×
204
        if generator_name is None:
×
205
            raise ValueError(
×
206
                f"Field `{OpenApiPythonGeneratorNameField.alias}` is required for target {field_set.address}"
207
            )
208

209
        gets.append(
×
210
            compile_openapi_into_python(
211
                CompileOpenApiIntoPythonRequest(
212
                    file,
213
                    input_digest=input_digest,
214
                    description=f"Generating Python sources from OpenAPI definition {field_set.address}",
215
                    generator_name=generator_name,
216
                    additional_properties=field_set.additional_properties.value,
217
                )
218
            )
219
        )
220

221
    compiled_sources = await concurrently(gets)
×
222

223
    logger.info("digests: %s", [sources.output_digest for sources in compiled_sources])
×
224
    output_digest, source_root = await concurrently(
×
225
        merge_digests(MergeDigests([sources.output_digest for sources in compiled_sources])),
226
        get_source_root(SourceRootRequest.for_target(request.protocol_target)),
227
    )
228

229
    source_root_restored = (
×
230
        await digest_to_snapshot(**implicitly(AddPrefix(output_digest, source_root.path)))
231
        if source_root.path != "."
232
        else await digest_to_snapshot(output_digest)
233
    )
234
    return GeneratedSources(source_root_restored)
×
235

236

UNCOV
237
@dataclass(frozen=True)
×
UNCOV
238
class OpenApiDocumentPythonRuntimeInferenceFieldSet(FieldSet):
×
UNCOV
239
    required_fields = (OpenApiDocumentDependenciesField, PrefixedPythonResolveField)
×
240

UNCOV
241
    dependencies: OpenApiDocumentDependenciesField
×
UNCOV
242
    python_resolve: PrefixedPythonResolveField
×
UNCOV
243
    generator_name: OpenApiPythonGeneratorNameField
×
UNCOV
244
    additional_properties: OpenApiPythonAdditionalPropertiesField
×
UNCOV
245
    skip: OpenApiPythonSkipField
×
246

247

UNCOV
248
class InferOpenApiPythonRuntimeDependencyRequest(InferDependenciesRequest):
×
UNCOV
249
    infer_from = OpenApiDocumentPythonRuntimeInferenceFieldSet
×
250

251

UNCOV
252
@dataclass(frozen=True)
×
UNCOV
253
class PythonRequirements:
×
UNCOV
254
    resolves_to_requirements_to_addresses: FrozenDict[str, FrozenDict[str, Address]]
×
255

256

UNCOV
257
@rule
×
UNCOV
258
async def get_python_requirements(
×
259
    python_targets: AllPythonTargets,
260
    python_setup: PythonSetup,
261
) -> PythonRequirements:
262
    result: defaultdict[str, dict[str, Address]] = defaultdict(dict)
×
263
    for target in python_targets.third_party:
×
264
        for python_requirement in target[PythonRequirementsField].value:
×
265
            name = canonicalize_name(python_requirement.name)
×
266
            resolve = target[PythonRequirementResolveField].normalized_value(python_setup)
×
267
            result[resolve][name] = target.address
×
268

269
    return PythonRequirements(
×
270
        resolves_to_requirements_to_addresses=FrozenDict(
271
            (
272
                resolve,
273
                FrozenDict(
274
                    (requirements, addresses)
275
                    for requirements, addresses in requirements_to_addresses.items()
276
                ),
277
            )
278
            for resolve, requirements_to_addresses in result.items()
279
        ),
280
    )
281

282

UNCOV
283
@rule
×
UNCOV
284
async def infer_openapi_python_dependencies(
×
285
    request: InferOpenApiPythonRuntimeDependencyRequest,
286
    python_setup: PythonSetup,
287
    openapi_generator: OpenAPIGenerator,
288
    python_requirements: PythonRequirements,
289
) -> InferredDependencies:
290
    if request.field_set.skip.value:
×
291
        return InferredDependencies([])
×
292

293
    resolve = request.field_set.python_resolve.normalized_value(python_setup)
×
294

295
    # Because the runtime dependencies are the same regardless of the source being compiled
296
    # we use a sample OpenAPI spec to find out what are the runtime dependencies
297
    # for the given resolve and prevent creating a cycle in our rule engine.
298
    sample_spec_name = "__sample_spec.yaml"
×
299
    sample_source_digest = await create_digest(
×
300
        CreateDigest(
301
            [FileContent(path=sample_spec_name, content=PETSTORE_SAMPLE_SPEC.encode("utf-8"))]
302
        )
303
    )
304
    compiled_sources = await compile_openapi_into_python(
×
305
        CompileOpenApiIntoPythonRequest(
306
            input_file=sample_spec_name,
307
            input_digest=sample_source_digest,
308
            description=f"Inferring Python runtime dependencies for OpenAPI v{openapi_generator.version}",
309
            generator_name=request.field_set.generator_name.value,
310
            additional_properties=request.field_set.additional_properties.value,
311
        )
312
    )
313

314
    logger.info("Looking for thirdparty dependencies: %s", compiled_sources.runtime_dependencies)
×
315

316
    requirements_to_addresses = python_requirements.resolves_to_requirements_to_addresses[resolve]
×
317

318
    addresses, missing_requirements = [], []
×
319
    for runtime_dependency in compiled_sources.runtime_dependencies:
×
320
        name = canonicalize_name(runtime_dependency.name)
×
321
        address = requirements_to_addresses.get(name)
×
322
        if address is not None:
×
323
            addresses.append(address)
×
324
        else:
325
            missing_requirements.append(name)
×
326

327
    if missing_requirements:
×
328
        for_resolve_str = f" for the resolve '{resolve}'" if python_setup.enable_resolves else ""
×
329
        missing = ", ".join(f"`{name}`" for name in missing_requirements)
×
330
        raise MissingPythonCodegenRuntimeLibrary(
×
331
            softwrap(
332
                f"""
333
                No `python_requirement` target was found with the packages {missing}
334
                in your project{for_resolve_str}, so the Python code generated from the target
335
                {request.field_set.address} will not work properly.
336
                """
337
            )
338
        )
339

340
    return InferredDependencies(addresses)
×
341

342

UNCOV
343
def rules():
×
UNCOV
344
    return [
×
345
        *collect_rules(),
346
        UnionRule(GenerateSourcesRequest, GeneratePythonFromOpenAPIRequest),
347
        UnionRule(InferDependenciesRequest, InferOpenApiPythonRuntimeDependencyRequest),
348
    ]
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