• 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/util_rules/faas.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
"""Function-as-a-service (FaaS) support like AWS Lambda and Google Cloud Functions."""
4

UNCOV
5
from __future__ import annotations
×
6

UNCOV
7
import importlib.resources
×
UNCOV
8
import json
×
UNCOV
9
import logging
×
UNCOV
10
import os.path
×
UNCOV
11
from abc import ABC, abstractmethod
×
UNCOV
12
from dataclasses import dataclass
×
UNCOV
13
from enum import Enum
×
UNCOV
14
from pathlib import Path
×
UNCOV
15
from typing import ClassVar, cast
×
16

UNCOV
17
from pants.backend.python.dependency_inference.module_mapper import (
×
18
    PythonModuleOwnersRequest,
19
    map_module_to_address,
20
)
UNCOV
21
from pants.backend.python.dependency_inference.rules import import_rules
×
UNCOV
22
from pants.backend.python.dependency_inference.subsystem import (
×
23
    AmbiguityResolution,
24
    PythonInferSubsystem,
25
)
UNCOV
26
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
27
from pants.backend.python.target_types import (
×
28
    PexCompletePlatformsField,
29
    PexLayout,
30
    PythonResolveField,
31
)
UNCOV
32
from pants.backend.python.util_rules.pex import (
×
33
    CompletePlatforms,
34
    create_pex,
35
    digest_complete_platform_addresses,
36
)
UNCOV
37
from pants.backend.python.util_rules.pex_from_targets import (
×
38
    InterpreterConstraintsRequest,
39
    PexFromTargetsRequest,
40
    interpreter_constraints_for_targets,
41
)
UNCOV
42
from pants.backend.python.util_rules.pex_from_targets import rules as pex_from_targets_rules
×
UNCOV
43
from pants.backend.python.util_rules.pex_venv import PexVenvLayout, PexVenvRequest
×
UNCOV
44
from pants.backend.python.util_rules.pex_venv import pex_venv as pex_venv_get
×
UNCOV
45
from pants.backend.python.util_rules.pex_venv import rules as pex_venv_rules
×
UNCOV
46
from pants.core.goals.package import BuiltPackage, BuiltPackageArtifact, OutputPathField
×
UNCOV
47
from pants.engine.addresses import Address
×
UNCOV
48
from pants.engine.fs import (
×
49
    EMPTY_DIGEST,
50
    CreateDigest,
51
    FileContent,
52
    GlobMatchErrorBehavior,
53
    PathGlobs,
54
)
UNCOV
55
from pants.engine.internals.graph import determine_explicitly_provided_dependencies
×
UNCOV
56
from pants.engine.internals.native_engine import MergeDigests
×
UNCOV
57
from pants.engine.intrinsics import (
×
58
    create_digest,
59
    digest_to_snapshot,
60
    merge_digests,
61
    path_globs_to_paths,
62
)
UNCOV
63
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
×
UNCOV
64
from pants.engine.target import (
×
65
    AsyncFieldMixin,
66
    Dependencies,
67
    DependenciesRequest,
68
    FieldSet,
69
    InferDependenciesRequest,
70
    InferredDependencies,
71
    InvalidFieldException,
72
    InvalidTargetException,
73
    StringField,
74
    StringSequenceField,
75
)
UNCOV
76
from pants.engine.unions import UnionRule
×
UNCOV
77
from pants.source.source_root import SourceRootRequest, get_source_root
×
UNCOV
78
from pants.util.docutil import doc_url
×
UNCOV
79
from pants.util.ordered_set import FrozenOrderedSet
×
UNCOV
80
from pants.util.strutil import help_text, softwrap
×
81

UNCOV
82
logger = logging.getLogger(__name__)
×
83

84

UNCOV
85
class PythonFaaSLayoutField(StringField):
×
UNCOV
86
    alias = "layout"
×
UNCOV
87
    valid_choices = PexVenvLayout
×
UNCOV
88
    expected_type = str
×
UNCOV
89
    default = PexVenvLayout.FLAT_ZIPPED.value
×
UNCOV
90
    help = help_text(
×
91
        """
92
        Control the layout of the final artifact: `flat` creates a directory with the
93
        source and requirements at the top level, as recommended by cloud vendors,
94
        while `flat-zipped` (the default) wraps this up into a single zip file.
95
        """
96
    )
97

98

UNCOV
99
class PythonFaaSPex3VenvCreateExtraArgsField(StringSequenceField):
×
UNCOV
100
    alias = "pex3_venv_create_extra_args"
×
UNCOV
101
    default = ()
×
UNCOV
102
    help = help_text(
×
103
        """
104
        Any extra arguments to pass to the `pex3 venv create` invocation that is used to create the
105
        final zip file or directory.
106

107
        For example, `pex3_venv_create_extra_args=["--collisions-ok"]`, if using packages that have
108
        colliding files that aren't required at runtime (errors like "Encountered collisions
109
        populating ...").
110
        """
111
    )
112

113

UNCOV
114
class PythonFaaSPexBuildExtraArgs(StringSequenceField):
×
UNCOV
115
    alias = "pex_build_extra_args"
×
UNCOV
116
    default = ()
×
UNCOV
117
    help = help_text(
×
118
        """
119
        Additional arguments to pass to the `pex` invocation that is used to collect the requirements
120
        and sources for packaging.
121

122
        For example, `pex_build_extra_args=["--exclude=pypi-package-name"]` to force a package called
123
        `pypi-package-name` isn't included in the artifact.
124

125
        Note: Excluding dependencies currently causes Pex to throw an error. You can additionally pass
126
        the `--ignore-errors` flag.
127
        """
128
    )
129

130

UNCOV
131
class PythonFaaSHandlerField(StringField, AsyncFieldMixin):
×
UNCOV
132
    alias = "handler"
×
UNCOV
133
    required = True
×
UNCOV
134
    value: str
×
UNCOV
135
    help = help_text(
×
136
        """
137
        You can specify a full module like `'path.to.module:handler_func'` or use a shorthand to
138
        specify a file name, using the same syntax as the `sources` field, e.g.
139
        `'cloud_function.py:handler_func'`.
140
        """
141
    )
142

UNCOV
143
    @classmethod
×
UNCOV
144
    def compute_value(cls, raw_value: str | None, address: Address) -> str:
×
UNCOV
145
        value = cast(str, super().compute_value(raw_value, address))
×
UNCOV
146
        if ":" not in value:
×
UNCOV
147
            raise InvalidFieldException(
×
148
                f"The `{cls.alias}` field in target at {address} must end in the "
149
                f"format `:my_handler_func`, but was {value}."
150
            )
UNCOV
151
        return value
×
152

153

UNCOV
154
@dataclass(frozen=True)
×
UNCOV
155
class ResolvedPythonFaaSHandler:
×
UNCOV
156
    module: str
×
UNCOV
157
    func: str
×
UNCOV
158
    file_name_used: bool
×
159

160

UNCOV
161
@dataclass(frozen=True)
×
UNCOV
162
class ResolvePythonFaaSHandlerRequest:
×
UNCOV
163
    field: PythonFaaSHandlerField
×
164

165

UNCOV
166
@rule(desc="Determining the handler for a python FaaS target")
×
UNCOV
167
async def resolve_python_faas_handler(
×
168
    request: ResolvePythonFaaSHandlerRequest,
169
) -> ResolvedPythonFaaSHandler:
170
    handler_val = request.field.value
×
171
    field_alias = request.field.alias
×
172
    address = request.field.address
×
173
    path, _, func = handler_val.partition(":")
×
174

175
    # If it's already a module, simply use that. Otherwise, convert the file name into a module
176
    # path.
177
    if not path.endswith(".py"):
×
178
        return ResolvedPythonFaaSHandler(module=path, func=func, file_name_used=False)
×
179

180
    # Use the engine to validate that the file exists and that it resolves to only one file.
181
    full_glob = os.path.join(address.spec_path, path)
×
182
    handler_paths = await path_globs_to_paths(
×
183
        PathGlobs(
184
            [full_glob],
185
            glob_match_error_behavior=GlobMatchErrorBehavior.error,
186
            description_of_origin=f"{address}'s `{field_alias}` field",
187
        )
188
    )
189

190
    # We will have already raised if the glob did not match, i.e. if there were no files. But
191
    # we need to check if they used a file glob (`*` or `**`) that resolved to >1 file.
192
    if len(handler_paths.files) != 1:
×
193
        raise InvalidFieldException(
×
194
            f"Multiple files matched for the `{field_alias}` {repr(handler_val)} for the target "
195
            f"{address}, but only one file expected. Are you using a glob, rather than a file "
196
            f"name?\n\nAll matching files: {list(handler_paths.files)}."
197
        )
198
    handler_path = handler_paths.files[0]
×
199
    source_root = await get_source_root(SourceRootRequest.for_file(handler_path))
×
200
    stripped_source_path = os.path.relpath(handler_path, source_root.path)
×
201
    module_base, _ = os.path.splitext(stripped_source_path)
×
202
    normalized_path = module_base.replace(os.path.sep, ".")
×
203
    return ResolvedPythonFaaSHandler(module=normalized_path, func=func, file_name_used=True)
×
204

205

UNCOV
206
class PythonFaaSDependencies(Dependencies):
×
UNCOV
207
    supports_transitive_excludes = True
×
208

209

UNCOV
210
@dataclass(frozen=True)
×
UNCOV
211
class PythonFaaSHandlerInferenceFieldSet(FieldSet):
×
UNCOV
212
    required_fields = (
×
213
        PythonFaaSDependencies,
214
        PythonFaaSHandlerField,
215
        PythonResolveField,
216
    )
217

UNCOV
218
    dependencies: PythonFaaSDependencies
×
UNCOV
219
    handler: PythonFaaSHandlerField
×
UNCOV
220
    resolve: PythonResolveField
×
221

222

UNCOV
223
class InferPythonFaaSHandlerDependency(InferDependenciesRequest):
×
UNCOV
224
    infer_from = PythonFaaSHandlerInferenceFieldSet
×
225

226

UNCOV
227
@rule(desc="Inferring dependency from the python FaaS `handler` field")
×
UNCOV
228
async def infer_faas_handler_dependency(
×
229
    request: InferPythonFaaSHandlerDependency,
230
    python_infer_subsystem: PythonInferSubsystem,
231
    python_setup: PythonSetup,
232
) -> InferredDependencies:
233
    if not python_infer_subsystem.entry_points:
×
234
        return InferredDependencies([])
×
235

236
    explicitly_provided_deps, handler = await concurrently(
×
237
        determine_explicitly_provided_dependencies(
238
            **implicitly(DependenciesRequest(request.field_set.dependencies))
239
        ),
240
        resolve_python_faas_handler(ResolvePythonFaaSHandlerRequest(request.field_set.handler)),
241
    )
242

243
    # Only set locality if needed, to avoid unnecessary rule graph memoization misses.
244
    # When set, use the source root, which is useful in practice, but incurs fewer memoization
245
    # misses than using the full spec_path.
246
    locality = None
×
247
    if python_infer_subsystem.ambiguity_resolution == AmbiguityResolution.by_source_root:
×
248
        source_root = await get_source_root(
×
249
            SourceRootRequest.for_address(request.field_set.address)
250
        )
251
        locality = source_root.path
×
252

253
    owners = await map_module_to_address(
×
254
        PythonModuleOwnersRequest(
255
            handler.module,
256
            resolve=request.field_set.resolve.normalized_value(python_setup),
257
            locality=locality,
258
        ),
259
        **implicitly(),
260
    )
261
    address = request.field_set.address
×
262
    explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
×
263
        owners.ambiguous,
264
        address,
265
        # If the handler was specified as a file, like `app.py`, we know the module must
266
        # live in the python_google_cloud_function's directory or subdirectory, so the owners must be ancestors.
267
        owners_must_be_ancestors=handler.file_name_used,
268
        import_reference="module",
269
        context=(
270
            f"The target {address} has the field "
271
            f"`handler={repr(request.field_set.handler.value)}`, which maps "
272
            f"to the Python module `{handler.module}`"
273
        ),
274
    )
275
    maybe_disambiguated = explicitly_provided_deps.disambiguated(
×
276
        owners.ambiguous, owners_must_be_ancestors=handler.file_name_used
277
    )
278
    unambiguous_owners = owners.unambiguous or (
×
279
        (maybe_disambiguated,) if maybe_disambiguated else ()
280
    )
281
    return InferredDependencies(unambiguous_owners)
×
282

283

UNCOV
284
class PythonFaaSCompletePlatforms(PexCompletePlatformsField):
×
UNCOV
285
    help = help_text(
×
286
        f"""
287
        {PexCompletePlatformsField.help}
288

289
        N.B.: only one of this and `runtime` can be set. If `runtime` is set, a default complete
290
        platform is chosen, if one is known for that runtime. Explicitly set this to `[]` to use the
291
        platform's ambient interpreter, such as when running in an docker environment.
292
        """
293
    )
294

295

UNCOV
296
class FaaSArchitecture(str, Enum):
×
UNCOV
297
    X86_64 = "x86_64"
×
UNCOV
298
    ARM64 = "arm64"
×
299

300

UNCOV
301
@dataclass(frozen=True)
×
UNCOV
302
class PythonFaaSKnownRuntime:
×
UNCOV
303
    name: str
×
UNCOV
304
    major: int
×
UNCOV
305
    minor: int
×
UNCOV
306
    docker_repo: str
×
UNCOV
307
    tag: str
×
UNCOV
308
    architecture: FaaSArchitecture
×
309

UNCOV
310
    def file_name(self) -> str:
×
311
        return f"complete_platform_{self.tag}.json"
×
312

313

UNCOV
314
class PythonFaaSRuntimeField(StringField, ABC):
×
UNCOV
315
    alias = "runtime"
×
UNCOV
316
    default = None
×
317

UNCOV
318
    known_runtimes: ClassVar[tuple[PythonFaaSKnownRuntime, ...]] = ()
×
319

UNCOV
320
    @classmethod
×
UNCOV
321
    def known_runtimes_complete_platforms_module(cls) -> str:
×
322
        # the runtime field subclasses are conventionally in a `target_types.py` file, and we want
323
        # to put the JSONs in a sibling file
324
        return cls.__module__.rsplit(".", 1)[0]
×
325

UNCOV
326
    @abstractmethod
×
UNCOV
327
    def to_interpreter_version(self) -> None | tuple[int, int]:
×
328
        """Returns the Python version implied by the runtime, as (major, minor)."""
329

UNCOV
330
    @classmethod
×
UNCOV
331
    @abstractmethod
×
UNCOV
332
    def from_interpreter_version(cls, py_major: int, py_minor: int) -> str:
×
333
        """Returns an appropriately-formatted runtime argument."""
334

UNCOV
335
    def to_platform_string(self) -> None | str:
×
336
        # We hardcode the platform value to the appropriate one for each FaaS runtime.
337
        # (Running the "hello world" cloud function in the example code will report the platform, and can be
338
        # used to verify correctness of these platform strings.)
339
        interpreter_version = self.to_interpreter_version()
×
340
        if interpreter_version is None:
×
341
            return None
×
342

343
        return _format_platform_from_major_minor(*interpreter_version)
×
344

345

UNCOV
346
def _format_platform_from_major_minor(py_major: int, py_minor: int) -> str:
×
347
    platform_str = f"linux_x86_64-cp-{py_major}{py_minor}-cp{py_major}{py_minor}"
×
348
    # set pymalloc ABI flag - this was removed in python 3.8 https://bugs.python.org/issue36707
349
    if py_major <= 3 and py_minor < 8:
×
350
        platform_str += "m"
×
351
    return platform_str
×
352

353

UNCOV
354
@rule
×
UNCOV
355
async def digest_complete_platforms(
×
356
    complete_platforms: PythonFaaSCompletePlatforms,
357
) -> CompletePlatforms:
358
    return await digest_complete_platform_addresses(complete_platforms.to_unparsed_address_inputs())
×
359

360

UNCOV
361
@dataclass(frozen=True)
×
UNCOV
362
class RuntimePlatformsRequest:
×
UNCOV
363
    address: Address
×
UNCOV
364
    target_name: str
×
365

UNCOV
366
    runtime: PythonFaaSRuntimeField
×
UNCOV
367
    complete_platforms: PythonFaaSCompletePlatforms
×
UNCOV
368
    architecture: FaaSArchitecture
×
369

370

UNCOV
371
@dataclass(frozen=True)
×
UNCOV
372
class RuntimePlatforms:
×
UNCOV
373
    interpreter_version: None | tuple[int, int]
×
UNCOV
374
    complete_platforms: CompletePlatforms = CompletePlatforms()
×
375

376

UNCOV
377
async def _infer_from_ics(request: RuntimePlatformsRequest) -> tuple[int, int]:
×
378
    ics = await interpreter_constraints_for_targets(
×
379
        InterpreterConstraintsRequest([request.address]), **implicitly()
380
    )
381

382
    # Future proofing: use naive non-universe-based IC requirement matching to determine if the
383
    # requirements cover exactly (and all patch versions of) one major.minor interpreter
384
    # version.
385
    #
386
    # Either reasonable option for a universe (`PythonSetup.interpreter_universe` or the FaaS's
387
    # known runtimes) can and will be expanded during a Pants upgrade: for instance, at the time of
388
    # writing, Pants only supports up to 3.11 but might soon add support for 3.12, or AWS Lambda
389
    # (and pants.backend.awslambda.python's known runtimes) only supports up to 3.10 but might soon
390
    # add support for 3.11.
391
    #
392
    # When this happens, some ranges (like `>=3.11`, if using `PythonSetup.interpreter_universe`)
393
    # will go from covering one major.minor interpreter version to covering more than one, and thus
394
    # inference starts breaking during the upgrade, requiring the user to do distracting changes
395
    # without deprecations/warnings to help.
396
    major_minor = ics.major_minor_version_when_single_and_entire()
×
397
    if major_minor is not None:
×
398
        return major_minor
×
399

400
    raise InvalidTargetException(
×
401
        softwrap(
402
            f"""
403
            The {request.target_name!r} target {request.address} cannot have its runtime platform
404
            inferred, because inference requires simple interpreter constraints covering exactly one
405
            minor release of Python, and all its patch version. The constraints for this target
406
            ({ics}) aren't understood.
407

408
            To fix, provide one of the following:
409

410
            - a value for the `{request.runtime.alias}` field, or
411

412
            - a value for the `{request.complete_platforms.alias}` field, or
413

414
            - simple and narrow interpreter constraints (for example, `==3.10.*` or `>=3.10,<3.11` are simple enough to imply Python 3.10)
415
            """
416
        )
417
    )
418

419

UNCOV
420
@rule
×
UNCOV
421
async def infer_runtime_platforms(request: RuntimePlatformsRequest) -> RuntimePlatforms:
×
422
    if request.complete_platforms.value is not None:
×
423
        # explicit complete platforms wins:
424

425
        complete_platforms = await digest_complete_platforms(request.complete_platforms)
×
426
        # Don't bother trying to infer the runtime version if the user has provided their own
427
        # complete platform; they probably know what they're doing.
428
        return RuntimePlatforms(interpreter_version=None, complete_platforms=complete_platforms)
×
429

430
    version = request.runtime.to_interpreter_version()
×
431
    inferred_from_ics = False
×
432
    if version is None:
×
433
        # if there's not a specified version, let's try to infer it from the interpreter constraints
434
        version = await _infer_from_ics(request)
×
435
        inferred_from_ics = True
×
436

437
    try:
×
438
        file_name = next(
×
439
            rt.file_name()
440
            for rt in request.runtime.known_runtimes
441
            if version == (rt.major, rt.minor) and request.architecture.value == rt.architecture
442
        )
443
    except StopIteration:
×
444
        # No known runtime, so prompt the user to specify
445
        version_modifier = "[inferred from interpreter constraints]" if inferred_from_ics else ""
×
446
        version_adjective = "inferred" if inferred_from_ics else "specified"
×
447
        known_runtimes_str = ", ".join(
×
448
            FrozenOrderedSet(r.name for r in request.runtime.known_runtimes)
449
        )
450
        raise InvalidTargetException(
×
451
            softwrap(
452
                f"""
453
                Could not find a known runtime for the {version_adjective} Python version and machine architecture!
454

455
                * Python version: {version} {version_modifier}
456
                * Machine architecture: {request.architecture.value}
457
                * Known runtime values: {known_runtimes_str}
458

459
                To fix, please generate a `complete_platforms` file for the given Python version and
460
                machine architecture, or specify a runtime that is known to Pants.
461

462
                You can follow the instructions at {doc_url("docs/python/overview/pex#generating-the-complete_platforms-file")}
463
                to generate a `complete_platforms` file for your Python version and machine
464
                architecture.
465
                """
466
            ),
467
            description_of_origin=f"In the {request.target_name!r} target",
468
        ) from None
469

470
    module = request.runtime.known_runtimes_complete_platforms_module()
×
471

472
    content = (importlib.resources.files(module) / file_name).read_bytes()
×
473
    snapshot = await digest_to_snapshot(
×
474
        **implicitly(CreateDigest([FileContent(file_name, content)]))
475
    )
476

477
    return RuntimePlatforms(
×
478
        interpreter_version=version, complete_platforms=CompletePlatforms.from_snapshot(snapshot)
479
    )
480

481

UNCOV
482
@dataclass(frozen=True)
×
UNCOV
483
class BuildPythonFaaSRequest:
×
UNCOV
484
    address: Address
×
UNCOV
485
    target_name: str
×
486

UNCOV
487
    complete_platforms: PythonFaaSCompletePlatforms
×
UNCOV
488
    handler: None | PythonFaaSHandlerField
×
UNCOV
489
    output_path: OutputPathField
×
UNCOV
490
    runtime: PythonFaaSRuntimeField
×
UNCOV
491
    architecture: FaaSArchitecture
×
UNCOV
492
    pex3_venv_create_extra_args: PythonFaaSPex3VenvCreateExtraArgsField
×
UNCOV
493
    pex_build_extra_args: PythonFaaSPexBuildExtraArgs
×
UNCOV
494
    layout: PythonFaaSLayoutField
×
495

UNCOV
496
    include_requirements: bool
×
UNCOV
497
    include_sources: bool
×
498

UNCOV
499
    reexported_handler_module: None | str
×
UNCOV
500
    log_only_reexported_handler_func: bool = False
×
501

UNCOV
502
    prefix_in_artifact: None | str = None
×
503

504

UNCOV
505
@rule
×
UNCOV
506
async def build_python_faas(
×
507
    request: BuildPythonFaaSRequest,
508
) -> BuiltPackage:
UNCOV
509
    additional_pex_args = (
×
510
        # Ensure we can resolve manylinux wheels in addition to any AMI-specific wheels.
511
        "--manylinux=manylinux2014",
512
        # When we're executing Pex on Linux, allow a local interpreter to be resolved if
513
        # available and matching the AMI platform.
514
        "--resolve-local-platforms",
515
        # Additional args from request
516
        *(request.pex_build_extra_args.value or ()),
517
    )
518

UNCOV
519
    platforms_get = infer_runtime_platforms(
×
520
        RuntimePlatformsRequest(
521
            address=request.address,
522
            target_name=request.target_name,
523
            runtime=request.runtime,
524
            architecture=request.architecture,
525
            complete_platforms=request.complete_platforms,
526
        ),
527
    )
528

UNCOV
529
    if request.handler:
×
530
        platforms, handler = await concurrently(
×
531
            platforms_get,
532
            resolve_python_faas_handler(ResolvePythonFaaSHandlerRequest(request.handler)),
533
        )
534
    else:
UNCOV
535
        platforms = await platforms_get
×
UNCOV
536
        handler = None
×
537

538
    # TODO: improve diagnostics if there's more than one platform/complete_platform
539

UNCOV
540
    if request.reexported_handler_module and handler:
×
541
        # synthesise a source file that gives a fixed handler path, no matter what the entry point is:
542
        # some platforms require a certain name (e.g. GCF), and even on others, giving a fixed name
543
        # means users don't need to duplicate the entry_point config in both the pants BUILD file and
544
        # infrastructure definitions (the latter can always use the same names, for every lambda).
545
        reexported_handler_file = f"{request.reexported_handler_module}.py"
×
546
        reexported_handler_func = "handler"
×
547
        reexported_handler_content = (
×
548
            f"from {handler.module} import {handler.func} as {reexported_handler_func}"
549
        )
550
        additional_sources = await create_digest(
×
551
            CreateDigest(
552
                [FileContent(reexported_handler_file, reexported_handler_content.encode())]
553
            )
554
        )
555
    else:
UNCOV
556
        additional_sources = EMPTY_DIGEST
×
UNCOV
557
        reexported_handler_func = None
×
558

UNCOV
559
    repository_filename = "faas_repository.pex"
×
UNCOV
560
    pex_request = PexFromTargetsRequest(
×
561
        addresses=[request.address],
562
        internal_only=False,
563
        include_requirements=request.include_requirements,
564
        include_source_files=request.include_sources,
565
        output_filename=repository_filename,
566
        complete_platforms=platforms.complete_platforms,
567
        layout=PexLayout.PACKED,
568
        additional_args=additional_pex_args,
569
        additional_lockfile_args=additional_pex_args,
570
        additional_sources=additional_sources,
571
        warn_for_transitive_files_targets=True,
572
    )
573

UNCOV
574
    pex_result = await create_pex(**implicitly({pex_request: PexFromTargetsRequest}))
×
575

UNCOV
576
    layout = PexVenvLayout(request.layout.value)
×
577

UNCOV
578
    output_filename = request.output_path.value_or_default(
×
579
        file_ending="zip" if layout is PexVenvLayout.FLAT_ZIPPED else None
580
    )
UNCOV
581
    metadata_filename = f"{output_filename}.metadata.json"
×
582

UNCOV
583
    result = await pex_venv_get(
×
584
        PexVenvRequest(
585
            pex=pex_result,
586
            layout=layout,
587
            complete_platforms=platforms.complete_platforms,
588
            extra_args=request.pex3_venv_create_extra_args.value or (),
589
            prefix=request.prefix_in_artifact,
590
            output_path=Path(output_filename),
591
            description=f"Build {request.target_name} artifact for {request.address}",
592
        ),
593
    )
594

UNCOV
595
    metadata = {}
×
596

UNCOV
597
    if platforms.interpreter_version is not None:
×
598
        runtime = request.runtime.from_interpreter_version(*platforms.interpreter_version)
×
599
        metadata["runtime"] = runtime
×
600

UNCOV
601
    if request.architecture is not None:
×
UNCOV
602
        metadata["architecture"] = request.architecture.value
×
603

UNCOV
604
    if reexported_handler_func is not None:
×
605
        if request.log_only_reexported_handler_func:
×
606
            handler_text = reexported_handler_func
×
607
        else:
608
            handler_text = f"{request.reexported_handler_module}.{reexported_handler_func}"
×
609
        metadata["handler"] = handler_text
×
610

UNCOV
611
    metadata_digest = await create_digest(
×
612
        CreateDigest(
613
            [
614
                FileContent(
615
                    metadata_filename, json.dumps(metadata, indent=2, sort_keys=True).encode()
616
                )
617
            ]
618
        )
619
    )
UNCOV
620
    digest = await merge_digests(MergeDigests([result.digest, metadata_digest]))
×
621

UNCOV
622
    extra_log_lines = [f"    {key.capitalize()}: {val}" for key, val in metadata.items()]
×
UNCOV
623
    artifact = BuiltPackageArtifact(
×
624
        output_filename,
625
        extra_log_lines=tuple(extra_log_lines),
626
    )
UNCOV
627
    metadata_artifact = BuiltPackageArtifact(metadata_filename)
×
628

UNCOV
629
    return BuiltPackage(digest=digest, artifacts=(artifact, metadata_artifact))
×
630

631

UNCOV
632
def rules():
×
UNCOV
633
    return (
×
634
        *collect_rules(),
635
        *import_rules(),
636
        *pex_venv_rules(),
637
        *pex_from_targets_rules(),
638
        UnionRule(InferDependenciesRequest, InferPythonFaaSHandlerDependency),
639
    )
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