• 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/python/goals/export.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 dataclasses
×
UNCOV
7
import logging
×
UNCOV
8
import os
×
UNCOV
9
import textwrap
×
UNCOV
10
import uuid
×
UNCOV
11
from dataclasses import dataclass
×
UNCOV
12
from enum import Enum
×
UNCOV
13
from typing import cast
×
14

UNCOV
15
from pants.backend.python.subsystems.python_tool_base import PythonToolBase
×
UNCOV
16
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
17
from pants.backend.python.target_types import PexLayout, PythonResolveField, PythonSourceField
×
UNCOV
18
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
19
from pants.backend.python.util_rules.local_dists_pep660 import (
×
20
    EditableLocalDistsRequest,
21
    build_editable_local_dists,
22
)
UNCOV
23
from pants.backend.python.util_rules.pex import (
×
24
    PexRequest,
25
    create_pex,
26
    create_venv_pex,
27
    find_interpreter,
28
)
UNCOV
29
from pants.backend.python.util_rules.pex_cli import PexPEX
×
UNCOV
30
from pants.backend.python.util_rules.pex_environment import PexEnvironment, PythonExecutable
×
UNCOV
31
from pants.backend.python.util_rules.pex_requirements import EntireLockfile, Lockfile
×
UNCOV
32
from pants.core.goals.export import (
×
33
    Export,
34
    ExportError,
35
    ExportRequest,
36
    ExportResult,
37
    ExportResults,
38
    ExportSubsystem,
39
    PostProcessingCommand,
40
)
UNCOV
41
from pants.core.goals.resolves import ExportableTool
×
UNCOV
42
from pants.core.util_rules.source_files import SourceFiles
×
UNCOV
43
from pants.core.util_rules.stripped_source_files import strip_source_roots
×
UNCOV
44
from pants.engine.engine_aware import EngineAwareParameter
×
UNCOV
45
from pants.engine.fs import CreateDigest, FileContent
×
UNCOV
46
from pants.engine.internals.graph import hydrate_sources
×
UNCOV
47
from pants.engine.internals.native_engine import EMPTY_DIGEST, AddPrefix, Digest, MergeDigests
×
UNCOV
48
from pants.engine.internals.selectors import concurrently
×
UNCOV
49
from pants.engine.intrinsics import add_prefix, create_digest, digest_to_snapshot, merge_digests
×
UNCOV
50
from pants.engine.process import Process, ProcessCacheScope, execute_process_or_raise
×
UNCOV
51
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
52
from pants.engine.target import AllTargets, HydrateSourcesRequest, SourcesField
×
UNCOV
53
from pants.engine.unions import UnionMembership, UnionRule
×
UNCOV
54
from pants.option.option_types import EnumOption, StrListOption
×
UNCOV
55
from pants.util.strutil import path_safe, softwrap
×
56

UNCOV
57
logger = logging.getLogger(__name__)
×
58

59

UNCOV
60
@dataclass(frozen=True)
×
UNCOV
61
class ExportVenvsRequest(ExportRequest):
×
UNCOV
62
    pass
×
63

64

UNCOV
65
@dataclass(frozen=True)
×
UNCOV
66
class _ExportVenvForResolveRequest(EngineAwareParameter):
×
UNCOV
67
    resolve: str
×
68

69

UNCOV
70
class PythonResolveExportFormat(Enum):
×
71
    """How to export Python resolves."""
72

UNCOV
73
    mutable_virtualenv = "mutable_virtualenv"
×
UNCOV
74
    symlinked_immutable_virtualenv = "symlinked_immutable_virtualenv"
×
75

76

UNCOV
77
class ExportPluginOptions:
×
UNCOV
78
    py_resolve_format = EnumOption(
×
79
        default=PythonResolveExportFormat.mutable_virtualenv,
80
        help=softwrap(
81
            """\
82
            Export Python resolves using this format. Options are:
83
              - `mutable_virtualenv`: Export a standalone mutable virtualenv that you can
84
                further modify.
85
              - `symlinked_immutable_virtualenv`: Export a symlink into a cached Python virtualenv.
86
                This virtualenv will have no pip binary, and will be immutable. Any attempt to
87
                modify it will corrupt the cache! It may, however, take significantly less time
88
                to export than a standalone, mutable virtualenv.
89
            """
90
        ),
91
    )
92

UNCOV
93
    py_editable_in_resolve = StrListOption(
×
94
        # TODO: Is there a way to get [python].resolves in a memoized_property here?
95
        #       If so, then we can validate that all resolves here are defined there.
96
        help=softwrap(
97
            """
98
            When exporting a mutable virtualenv for a resolve, do PEP-660 editable installs
99
            of all `python_distribution` targets that own code in the exported resolve.
100

101
            If a resolve name is not in this list, `python_distribution` targets will not
102
            be installed in the virtualenv. This defaults to an empty list for backwards
103
            compatibility and to prevent unnecessary work to generate and install the
104
            PEP-660 editable wheels.
105

106
            This only applies when `[python].enable_resolves` is true and when exporting a
107
            `mutable_virtualenv` (`symlinked_immutable_virtualenv` exports are not "full"
108
            virtualenvs because they must not be edited, and do not include `pip`).
109
            """
110
        ),
111
        advanced=True,
112
    )
113

UNCOV
114
    py_non_hermetic_scripts_in_resolve = StrListOption(
×
115
        help=softwrap(
116
            """
117
            When exporting a mutable virtualenv for a resolve listed in this option, by default
118
            console script shebang lines will be made "hermetic". Specifically, the shebang of
119
            hermetic console scripts will uses the python args `-sE` where:
120

121
              - `-s` skips inclusion of the user site-packages directory,
122
              - `-E` ignores all `PYTHON*` env vars like `PYTHONPATH`.
123

124
            If you need "non-hermetic" scripts for a partcular resolve, then add that resolve's name
125
            to this option. This will allow simple python shebangs that respect vars like
126
            `PYTHONPATH`, which, for example, will allow IDEs like PyCharm to inject its debugger,
127
            coverage, or other IDE-specific libs when running a script.
128

129
            This only applies when when exporting a `mutable_virtualenv`
130
            (`symlinked_immutable_virtualenv` exports are not "full"
131
            virtualenvs because they are used internally by pants itself.
132
            Pants requires hermetic scripts to provide its reproduciblity
133
            guarantee, fine-grained caching, and other features).
134
            """
135
        ),
136
        advanced=True,
137
    )
138

UNCOV
139
    py_generated_sources_in_resolve = StrListOption(
×
140
        help=softwrap(
141
            """
142
            When exporting a mutable virtualenv for a resolve listed in this option, generate sources which result from
143
            code generation (for example, the `protobuf_sources` and `thrift_sources` target types) into the mutable
144
            virtualenv exported for that resolve. Generated sources will be placed in the appropriate location within
145
            the site-packages directory of the mutable virtualenv.
146
            """
147
        ),
148
        advanced=True,
149
    )
150

151

UNCOV
152
async def _get_full_python_version(python: PythonExecutable) -> str:
×
153
    # Get the full python version (including patch #).
154
    argv = [
×
155
        python.path,
156
        "-c",
157
        "import sys; print('.'.join(str(x) for x in sys.version_info[0:3]))",
158
    ]
159
    res = await execute_process_or_raise(
×
160
        **implicitly(Process(argv, description="Get interpreter version"))
161
    )
162
    return res.stdout.strip().decode()
×
163

164

UNCOV
165
@dataclass(frozen=True)
×
UNCOV
166
class VenvExportRequest:
×
UNCOV
167
    py_version: str
×
UNCOV
168
    pex_request: PexRequest
×
UNCOV
169
    dest_prefix: str
×
UNCOV
170
    resolve_name: str
×
UNCOV
171
    qualify_path_with_python_version: bool
×
UNCOV
172
    editable_local_dists_digest: Digest | None = None
×
173

174

UNCOV
175
@rule
×
UNCOV
176
async def do_export(
×
177
    req: VenvExportRequest,
178
    pex_pex: PexPEX,
179
    pex_env: PexEnvironment,
180
    export_subsys: ExportSubsystem,
181
) -> ExportResult:
182
    if not req.pex_request.internal_only:
×
183
        raise ExportError(f"The PEX to be exported for {req.resolve_name} must be internal_only.")
×
184
    dest_prefix = (
×
185
        os.path.join(req.dest_prefix, path_safe(req.resolve_name))
186
        if req.resolve_name
187
        else req.dest_prefix
188
    )
189
    # digest_root is the absolute path to build_root/dest_prefix/py_version
190
    # (py_version may be left off in some cases)
191
    output_path = "{digest_root}"
×
192

193
    complete_pex_env = pex_env.in_workspace()
×
194

195
    export_format = export_subsys.options.py_resolve_format
×
196

197
    if export_format == PythonResolveExportFormat.symlinked_immutable_virtualenv:
×
198
        # NB: The symlink performance hack leaks an internal named cache location as output (via
199
        #  the symlink target). If the user partially or fully deletes the named cache, the symlink
200
        #  target might point to a malformed venv, or it might not exist at all.
201
        #  To prevent returning a symlink to a busted or nonexistent venv from a cached process
202
        #  (or a memoized rule) we force the process to rerun per-session.
203
        #  This does mean re-running the process superfluously when the named cache is intact, but
204
        #  that is generally fast, since all wheels are already cached, and it's best to be safe.
205
        session_request = dataclasses.replace(
×
206
            req.pex_request, cache_scope=ProcessCacheScope.PER_SESSION
207
        )
208
        requirements_venv_pex = await create_venv_pex(**implicitly({session_request: PexRequest}))
×
209
        # Note that for symlinking we ignore qualify_path_with_python_version and always qualify,
210
        # since we need some name for the symlink anyway.
211
        dest = f"{dest_prefix}/{req.py_version}"
×
212
        description = (
×
213
            f"symlink to immutable virtualenv for {req.resolve_name or 'requirements'} "
214
            f"(using Python {req.py_version})"
215
        )
216
        venv_abspath = os.path.join(complete_pex_env.pex_root, requirements_venv_pex.venv_rel_dir)
×
217
        return ExportResult(
×
218
            description,
219
            dest,
220
            post_processing_cmds=[
221
                # export creates an empty directory for us when the digest gets written.
222
                # We have to remove that before creating the symlink in its place.
223
                PostProcessingCommand(["rmdir", output_path]),
224
                PostProcessingCommand(["ln", "-s", venv_abspath, output_path]),
225
            ],
226
            resolve=req.resolve_name or None,
227
        )
228
    elif export_format == PythonResolveExportFormat.mutable_virtualenv:
×
229
        # Note that an internal-only pex will always have the `python` field set.
230
        # See the build_pex() rule and _determine_pex_python_and_platforms() helper in pex.py.
231
        requirements_pex = await create_pex(req.pex_request)
×
232
        assert requirements_pex.python is not None
×
233
        if req.qualify_path_with_python_version:
×
234
            dest = f"{dest_prefix}/{req.py_version}"
×
235
        else:
236
            dest = dest_prefix
×
237
        description = (
×
238
            f"mutable virtualenv for {req.resolve_name or 'requirements'} "
239
            f"(using Python {req.py_version})"
240
        )
241

242
        merged_digest = await merge_digests(MergeDigests([pex_pex.digest, requirements_pex.digest]))
×
243
        tmpdir_prefix = f".{uuid.uuid4().hex}.tmp"
×
244
        tmpdir_under_digest_root = os.path.join("{digest_root}", tmpdir_prefix)
×
245
        merged_digest_under_tmpdir = await add_prefix(AddPrefix(merged_digest, tmpdir_prefix))
×
246

247
        venv_prompt = f"{req.resolve_name}/{req.py_version}" if req.resolve_name else req.py_version
×
248

249
        pex_args = [
×
250
            os.path.join(tmpdir_under_digest_root, requirements_pex.name),
251
            "venv",
252
            "--pip",
253
            "--collisions-ok",
254
            f"--prompt={venv_prompt}",
255
            output_path,
256
        ]
257
        if req.resolve_name in export_subsys.options.py_non_hermetic_scripts_in_resolve:
×
258
            pex_args.insert(-1, "--non-hermetic-scripts")
×
259

260
        post_processing_cmds = [
×
261
            PostProcessingCommand(
262
                complete_pex_env.create_argv(
263
                    os.path.join(tmpdir_under_digest_root, pex_pex.exe),
264
                    *pex_args,
265
                ),
266
                {
267
                    **complete_pex_env.environment_dict(python=requirements_pex.python),
268
                    "PEX_MODULE": "pex.tools",
269
                },
270
            ),
271
            # Remove the requirements and pex pexes, to avoid confusion.
272
            PostProcessingCommand(["rm", "-rf", tmpdir_under_digest_root]),
273
        ]
274

275
        # Insert editable wheel post-processing commands if needed.
276
        if req.editable_local_dists_digest is not None:
×
277
            # We need the snapshot to get the wheel file names which are something like:
278
            #   - pkg_name-1.2.3-0.editable-py3-none-any.whl
279
            wheels_snapshot = await digest_to_snapshot(req.editable_local_dists_digest)
×
280
            # We need the paths to the installed .dist-info directories to finish installation.
281
            py_major_minor_version = ".".join(req.py_version.split(".", 2)[:2])
×
282
            lib_dir = os.path.join(
×
283
                output_path, "lib", f"python{py_major_minor_version}", "site-packages"
284
            )
285
            dist_info_dirs = [
×
286
                # This builds: dist/.../resolve/3.8.9/lib/python3.8/site-packages/pkg_name-1.2.3.dist-info
287
                os.path.join(lib_dir, "-".join(f.split("-")[:2]) + ".dist-info")
288
                for f in wheels_snapshot.files
289
            ]
290
            # We use slice assignment to insert multiple elements at index 1.
291
            post_processing_cmds[1:1] = [
×
292
                PostProcessingCommand(
293
                    [
294
                        # The wheels are "sources" in the pex and get dumped in lib_dir
295
                        # so we move them to tmpdir where they will be removed at the end.
296
                        "mv",
297
                        *(os.path.join(lib_dir, f) for f in wheels_snapshot.files),
298
                        tmpdir_under_digest_root,
299
                    ]
300
                ),
301
                PostProcessingCommand(
302
                    [
303
                        # Now install the editable wheels.
304
                        os.path.join(output_path, "bin", "pip"),
305
                        "install",
306
                        "--no-deps",  # The deps were already installed via requirements.pex.
307
                        "--no-build-isolation",  # Avoid VCS dep downloads (as they are installed).
308
                        *(os.path.join(tmpdir_under_digest_root, f) for f in wheels_snapshot.files),
309
                    ]
310
                ),
311
                PostProcessingCommand(
312
                    [
313
                        # Replace pip's direct_url.json (which points to the temp editable wheel)
314
                        # with ours (which points to build_dir sources and is marked "editable").
315
                        # Also update INSTALLER file to indicate that pants installed it.
316
                        "sh",
317
                        "-c",
318
                        " ".join(
319
                            [
320
                                f"mv -f {src} {dst}; echo pants > {installer};"
321
                                for src, dst, installer in zip(
322
                                    [
323
                                        os.path.join(d, "direct_url__pants__.json")
324
                                        for d in dist_info_dirs
325
                                    ],
326
                                    [os.path.join(d, "direct_url.json") for d in dist_info_dirs],
327
                                    [os.path.join(d, "INSTALLER") for d in dist_info_dirs],
328
                                )
329
                            ]
330
                        ),
331
                    ]
332
                ),
333
            ]
334

335
        return ExportResult(
×
336
            description,
337
            dest,
338
            digest=merged_digest_under_tmpdir,
339
            post_processing_cmds=post_processing_cmds,
340
            resolve=req.resolve_name or None,
341
        )
342
    else:
343
        raise ExportError("Unsupported value for [export].py_resolve_format")
×
344

345

UNCOV
346
@dataclass(frozen=True)
×
UNCOV
347
class MaybeExportResult:
×
UNCOV
348
    result: ExportResult | None
×
349

350

UNCOV
351
@dataclass(frozen=True)
×
UNCOV
352
class _ExportPythonCodegenRequest:
×
UNCOV
353
    resolve: str
×
354

355

UNCOV
356
@dataclass(frozen=True)
×
UNCOV
357
class _ExportPythonCodegenResult:
×
UNCOV
358
    digest: Digest
×
359

360

UNCOV
361
@dataclass(frozen=True)
×
UNCOV
362
class _ExportPythonCodegenSetup:
×
UNCOV
363
    PKG_DIR = "__pants_codegen__"
×
UNCOV
364
    SCRIPT_NAME = "codegen_setup.py"
×
365

UNCOV
366
    setup_script_digest: Digest
×
367

368

UNCOV
369
@rule
×
UNCOV
370
async def python_codegen_export_setup() -> _ExportPythonCodegenSetup:
×
371
    codegen_setup_script_digest = await create_digest(
×
372
        CreateDigest(
373
            [
374
                FileContent(
375
                    path=f"{_ExportPythonCodegenSetup.PKG_DIR}/{_ExportPythonCodegenSetup.SCRIPT_NAME}",
376
                    is_executable=True,
377
                    content=textwrap.dedent(
378
                        f"""\
379
                        import os
380
                        import shutil
381
                        import site
382
                        import sys
383

384
                        site_packages_dirs = site.getsitepackages()
385
                        if not site_packages_dirs:
386
                            raise Exception("Unable to determine location of site-packages directory in venv.")
387
                        site_packages_dir = site_packages_dirs[0]
388

389
                        codegen_dir = sys.argv[1]
390

391
                        for item in os.listdir(codegen_dir):
392
                            if item == "{_ExportPythonCodegenSetup.SCRIPT_NAME}":
393
                                continue
394
                            src = os.path.join(codegen_dir, item)
395
                            dest = os.path.join(site_packages_dir, item)
396
                            shutil.copytree(src, dest, dirs_exist_ok=True)
397
                            shutil.rmtree(src)
398
                        """
399
                    ).encode(),
400
                )
401
            ]
402
        )
403
    )
404

405
    return _ExportPythonCodegenSetup(codegen_setup_script_digest)
×
406

407

UNCOV
408
@rule
×
UNCOV
409
async def export_python_codegen(
×
410
    request: _ExportPythonCodegenRequest, python_setup: PythonSetup, all_targets: AllTargets
411
) -> _ExportPythonCodegenResult:
412
    non_python_sources_in_python_resolve = [
×
413
        tgt.get(SourcesField)
414
        for tgt in all_targets
415
        if tgt.has_field(PythonResolveField)
416
        and tgt[PythonResolveField].normalized_value(python_setup) == request.resolve
417
        and tgt.has_field(SourcesField)
418
        and not tgt.has_field(PythonSourceField)
419
    ]
420

421
    if not non_python_sources_in_python_resolve:
×
422
        return _ExportPythonCodegenResult(EMPTY_DIGEST)
×
423

424
    hydrated_non_python_sources = await concurrently(
×
425
        hydrate_sources(
426
            HydrateSourcesRequest(
427
                sources,
428
                for_sources_types=(PythonSourceField,),
429
                enable_codegen=True,
430
            ),
431
            **implicitly(),
432
        )
433
        for sources in non_python_sources_in_python_resolve
434
    )
435

436
    merged_snapshot = await digest_to_snapshot(
×
437
        **implicitly(
438
            MergeDigests(
439
                hydrated_sources.snapshot.digest for hydrated_sources in hydrated_non_python_sources
440
            )
441
        )
442
    )
443

444
    stripped_source_files = await strip_source_roots(SourceFiles(merged_snapshot, ()))
×
445

446
    return _ExportPythonCodegenResult(stripped_source_files.snapshot.digest)
×
447

448

449
# Generate codegen Python sources and add them to the virtualenv to be exported.
UNCOV
450
async def add_codegen_to_export_result(
×
451
    resolve: str, export_result: ExportResult, codegen_setup: _ExportPythonCodegenSetup
452
) -> ExportResult:
453
    # Generate Python sources from codegen targets in this resolve.
454
    codegen_result = await export_python_codegen(
×
455
        _ExportPythonCodegenRequest(resolve=resolve), **implicitly()
456
    )
457
    if codegen_result.digest == EMPTY_DIGEST:
×
458
        return export_result
×
459

460
    codegen_digest = await add_prefix(
×
461
        AddPrefix(codegen_result.digest, _ExportPythonCodegenSetup.PKG_DIR)
462
    )
463
    export_digest_with_codegen = await merge_digests(
×
464
        MergeDigests([export_result.digest, codegen_digest, codegen_setup.setup_script_digest])
465
    )
466

467
    pkg_dir_path = os.path.join(
×
468
        "{digest_root}",
469
        _ExportPythonCodegenSetup.PKG_DIR,
470
    )
471
    script_path = os.path.join(pkg_dir_path, _ExportPythonCodegenSetup.SCRIPT_NAME)
×
472

473
    codegen_post_processing_cmds = (
×
474
        PostProcessingCommand(
475
            [
476
                os.path.join("{digest_root}", "bin", "python"),
477
                script_path,
478
                pkg_dir_path,
479
            ]
480
        ),
481
        PostProcessingCommand(["rm", script_path]),
482
        PostProcessingCommand(["rmdir", pkg_dir_path]),
483
    )
484

485
    return dataclasses.replace(
×
486
        export_result,
487
        digest=export_digest_with_codegen,
488
        post_processing_cmds=export_result.post_processing_cmds + codegen_post_processing_cmds,
489
    )
490

491

UNCOV
492
@rule
×
UNCOV
493
async def export_virtualenv_for_resolve(
×
494
    request: _ExportVenvForResolveRequest,
495
    python_setup: PythonSetup,
496
    export_subsys: ExportSubsystem,
497
    union_membership: UnionMembership,
498
    codegen_setup: _ExportPythonCodegenSetup,
499
) -> MaybeExportResult:
500
    resolve = request.resolve
×
501
    lockfile_path = python_setup.resolves.get(resolve)
×
502
    if lockfile_path:
×
503
        lockfile = Lockfile(
×
504
            url=lockfile_path,
505
            url_description_of_origin=f"the resolve `{resolve}`",
506
            resolve_name=resolve,
507
        )
508
    else:
509
        maybe_exportable = ExportableTool.filter_for_subclasses(
×
510
            union_membership, PythonToolBase
511
        ).get(resolve)
512
        if maybe_exportable:
×
513
            lockfile = cast(
×
514
                PythonToolBase, maybe_exportable
515
            ).pex_requirements_for_default_lockfile()
516
        else:
517
            lockfile = None
×
518

519
    if not lockfile:
×
520
        return MaybeExportResult(None)
×
521

522
    interpreter_constraints = InterpreterConstraints(
×
523
        python_setup.resolves_to_interpreter_constraints.get(
524
            request.resolve, python_setup.interpreter_constraints
525
        )
526
    )
527

528
    python = await find_interpreter(interpreter_constraints, **implicitly())
×
529
    py_version = await _get_full_python_version(python)
×
530

531
    if resolve in export_subsys.options.py_editable_in_resolve:
×
532
        editable_local_dists = await build_editable_local_dists(
×
533
            EditableLocalDistsRequest(resolve=resolve), **implicitly()
534
        )
535
        editable_local_dists_digest = editable_local_dists.optional_digest
×
536
    else:
537
        editable_local_dists_digest = None
×
538

539
    pex_request = PexRequest(
×
540
        description=f"Build pex for resolve `{resolve}`",
541
        output_filename=f"{path_safe(resolve)}.pex",
542
        internal_only=True,
543
        requirements=EntireLockfile(lockfile),
544
        sources=editable_local_dists_digest,
545
        python=python,
546
        # Packed layout should lead to the best performance in this use case.
547
        layout=PexLayout.PACKED,
548
    )
549

550
    dest_prefix = os.path.join("python", "virtualenvs")
×
551
    export_result = await do_export(
×
552
        VenvExportRequest(
553
            py_version,
554
            pex_request,
555
            dest_prefix,
556
            resolve,
557
            qualify_path_with_python_version=True,
558
            editable_local_dists_digest=editable_local_dists_digest,
559
        ),
560
        **implicitly(),
561
    )
562

563
    # Add generated Python sources from codegen targets to the mutable virtualenv.
564
    if (
×
565
        resolve in export_subsys.options.py_generated_sources_in_resolve
566
        and export_subsys.options.py_resolve_format == PythonResolveExportFormat.mutable_virtualenv
567
    ):
568
        export_result = await add_codegen_to_export_result(
×
569
            request.resolve, export_result, codegen_setup
570
        )
571

572
    return MaybeExportResult(export_result)
×
573

574

UNCOV
575
@rule
×
UNCOV
576
async def export_virtualenvs(
×
577
    request: ExportVenvsRequest,
578
    export_subsys: ExportSubsystem,
579
) -> ExportResults:
580
    maybe_venvs = await concurrently(
×
581
        export_virtualenv_for_resolve(_ExportVenvForResolveRequest(resolve), **implicitly())
582
        for resolve in export_subsys.options.resolve
583
    )
584
    return ExportResults(mv.result for mv in maybe_venvs if mv.result is not None)
×
585

586

UNCOV
587
def rules():
×
UNCOV
588
    return [
×
589
        *collect_rules(),
590
        Export.subsystem_cls.register_plugin_options(ExportPluginOptions),
591
        UnionRule(ExportRequest, ExportVenvsRequest),
592
    ]
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