diff --git a/src/python/pants/backend/awslambda/python/rules.py b/src/python/pants/backend/awslambda/python/rules.py index aee7b508639..0ab9717a3af 100644 --- a/src/python/pants/backend/awslambda/python/rules.py +++ b/src/python/pants/backend/awslambda/python/rules.py @@ -24,6 +24,7 @@ from pants.core.util_rules.environments import EnvironmentField from pants.engine.rules import Get, collect_rules, rule from pants.engine.unions import UnionRule +from pants.option.global_options import GlobalOptions from pants.util.logging import LogLevel logger = logging.getLogger(__name__) @@ -44,9 +45,9 @@ class PythonAwsLambdaFieldSet(PackageFieldSet): @rule(desc="Create Python AWS Lambda", level=LogLevel.DEBUG) async def package_python_awslambda( - field_set: PythonAwsLambdaFieldSet, + field_set: PythonAwsLambdaFieldSet, global_options: GlobalOptions ) -> BuiltPackage: - layout = PythonFaaSLayout(field_set.layout.value) + layout = field_set.layout.resolve_value(field_set.address, global_options) if layout is PythonFaaSLayout.LAMBDEX: return await Get( diff --git a/src/python/pants/backend/awslambda/python/rules_test.py b/src/python/pants/backend/awslambda/python/rules_test.py index a3158e538f2..56c945eb0a4 100644 --- a/src/python/pants/backend/awslambda/python/rules_test.py +++ b/src/python/pants/backend/awslambda/python/rules_test.py @@ -117,6 +117,9 @@ def complete_platform(rule_runner: PythonRuleRunner) -> bytes: ).stdout +_first_run_of_deprecated_warning = True + + @pytest.mark.platform_specific_behavior @pytest.mark.parametrize( "major_minor_interpreter", @@ -180,6 +183,15 @@ def handler(event, context): if sys.platform == "darwin": assert "`python_awslambda` targets built on macOS may fail to build." in caplog.text + global _first_run_of_deprecated_warning + if _first_run_of_deprecated_warning: + _first_run_of_deprecated_warning = False + assert ( + 'DEPRECATED: use of `layout="lambdex"` (set by default) in target src/python/foo/bar:lambda is scheduled to be removed' + in caplog.text + ) + assert "use_deprecated_lambdex_layout" in caplog.text + zip_file_relpath, content = create_python_awslambda( rule_runner, Address("src/python/foo/bar", target_name="slimlambda"), diff --git a/src/python/pants/backend/google_cloud_function/python/rules.py b/src/python/pants/backend/google_cloud_function/python/rules.py index 551cd31bd51..5582d053923 100644 --- a/src/python/pants/backend/google_cloud_function/python/rules.py +++ b/src/python/pants/backend/google_cloud_function/python/rules.py @@ -24,6 +24,7 @@ from pants.core.util_rules.environments import EnvironmentField from pants.engine.rules import Get, collect_rules, rule from pants.engine.unions import UnionRule +from pants.option.global_options import GlobalOptions from pants.util.logging import LogLevel logger = logging.getLogger(__name__) @@ -44,9 +45,10 @@ class PythonGoogleCloudFunctionFieldSet(PackageFieldSet): @rule(desc="Create Python Google Cloud Function", level=LogLevel.DEBUG) async def package_python_google_cloud_function( - field_set: PythonGoogleCloudFunctionFieldSet, + field_set: PythonGoogleCloudFunctionFieldSet, global_options: GlobalOptions ) -> BuiltPackage: - layout = PythonFaaSLayout(field_set.layout.value) + layout = field_set.layout.resolve_value(field_set.address, global_options) + if layout is PythonFaaSLayout.LAMBDEX: return await Get( BuiltPackage, diff --git a/src/python/pants/backend/google_cloud_function/python/rules_test.py b/src/python/pants/backend/google_cloud_function/python/rules_test.py index a5e7f3ddf12..5512af2f9db 100644 --- a/src/python/pants/backend/google_cloud_function/python/rules_test.py +++ b/src/python/pants/backend/google_cloud_function/python/rules_test.py @@ -125,6 +125,9 @@ def complete_platform(rule_runner: PythonRuleRunner) -> bytes: ).stdout +_first_run_of_deprecated_warning = True + + @pytest.mark.platform_specific_behavior @pytest.mark.parametrize( "major_minor_interpreter", @@ -180,6 +183,15 @@ def handler(event, context): in caplog.text ) + global _first_run_of_deprecated_warning + if _first_run_of_deprecated_warning: + _first_run_of_deprecated_warning = False + assert ( + 'DEPRECATED: use of `layout="lambdex"` (set by default) in target src/python/foo/bar:lambda is scheduled to be removed' + in caplog.text + ) + assert "use_deprecated_lambdex_layout" in caplog.text + def test_warn_files_targets(rule_runner: PythonRuleRunner, caplog) -> None: rule_runner.write_files( diff --git a/src/python/pants/backend/python/subsystems/lambdex.py b/src/python/pants/backend/python/subsystems/lambdex.py index 6b70ab6c3db..2b80fea9d95 100644 --- a/src/python/pants/backend/python/subsystems/lambdex.py +++ b/src/python/pants/backend/python/subsystems/lambdex.py @@ -4,11 +4,33 @@ from pants.backend.python.subsystems.python_tool_base import LockfileRules, PythonToolBase from pants.backend.python.target_types import ConsoleScript from pants.engine.rules import collect_rules +from pants.util.strutil import softwrap class Lambdex(PythonToolBase): + # these aren't read automatically, but are defined for use in faas.py + removal_hint = softwrap( + """ + Either use `layout=\"zip\"` in `python_awslambda` or `python_google_cloud_function` targets + to build flat packages without dynamic PEX start-up (recommended), or use `pex_binary` if dependency + selection is required on start-up (for instance, one package is deployed to multiple + runtimes). For a `pex_binary`, add `__pex__` to the import path for the handler: for + example, if the handler function `func` is defined in `foo/bar.py`, configure + `__pex__.foo.bar.func` as the handler. + """ + ) + removal_version = "2.19.0.dev0" + options_scope = "lambdex" - help = "A tool for turning .pex files into Function-as-a-Service artifacts (https://github.com/pantsbuild/lambdex)." + help = softwrap( + f""" + A tool for turning .pex files into Function-as-a-Service artifacts (https://github.com/pantsbuild/lambdex). + + Lambdex is no longer necessary: {removal_hint} + + This will be removed in Pants {removal_version}. + """ + ) default_version = "lambdex>=0.1.9" default_main = ConsoleScript("lambdex") diff --git a/src/python/pants/backend/python/util_rules/faas.py b/src/python/pants/backend/python/util_rules/faas.py index e520a341d0e..f931d861330 100644 --- a/src/python/pants/backend/python/util_rules/faas.py +++ b/src/python/pants/backend/python/util_rules/faas.py @@ -42,6 +42,7 @@ from pants.backend.python.util_rules.pex_from_targets import rules as pex_from_targets_rules from pants.backend.python.util_rules.pex_venv import PexVenv, PexVenvLayout, PexVenvRequest from pants.backend.python.util_rules.pex_venv import rules as pex_venv_rules +from pants.base.deprecated import warn_or_error from pants.core.goals.package import BuiltPackage, BuiltPackageArtifact, OutputPathField from pants.core.target_types import FileSourceField from pants.engine.addresses import Address, UnparsedAddressInputs @@ -72,10 +73,11 @@ targets_with_sources_types, ) from pants.engine.unions import UnionMembership, UnionRule +from pants.option.global_options import GlobalOptions from pants.source.filespec import Filespec from pants.source.source_root import SourceRoot, SourceRootRequest from pants.util.docutil import bin_name, doc_url -from pants.util.strutil import help_text +from pants.util.strutil import help_text, softwrap logger = logging.getLogger(__name__) @@ -284,7 +286,6 @@ def to_platform_string(self) -> None | str: class PythonFaaSLayout(Enum): - # TODO: deprecate lambdex layout, since PEX can be used directly now LAMBDEX = "lambdex" ZIP = "zip" @@ -292,15 +293,16 @@ class PythonFaaSLayout(Enum): class PythonFaaSLayoutField(StringField): alias = "layout" valid_choices = PythonFaaSLayout - default = PythonFaaSLayout.LAMBDEX.value + default = None help = help_text( """ The layout used for the function artifact. - With the `lambdex` layout (default), the artifact is created as a Lambdex, which is a normal - PEX that's been adjusted to include a shim file for the handler. This requires dynamically - choosing dependencies on start-up. + With the `lambdex` layout (default unless `[GLOBAL].use_deprecated_lambdex_layout` is set to + `false`, deprecated), the artifact is created as a Lambdex, which is a normal PEX that's + been adjusted to include a shim file for the handler. This requires dynamically choosing + dependencies on start-up. With the `zip` layout (recommended), the artifact contains first and third party code at the top level, similar to building with `pip install --target=...`. This layout chooses the @@ -311,6 +313,38 @@ class PythonFaaSLayoutField(StringField): """ ) + # TODO: this whole function can disappear once the LAMBDEX option is removed + def resolve_value(self, address: Address, global_options: GlobalOptions) -> PythonFaaSLayout: + if self.value is None: + layout = ( + PythonFaaSLayout.LAMBDEX + if global_options.use_deprecated_lambdex_layout + else PythonFaaSLayout.ZIP + ) + else: + layout = PythonFaaSLayout(self.value) + + if layout is PythonFaaSLayout.LAMBDEX and global_options.options.is_default( + "use_deprecated_lambdex_layout" + ): + from_default = " (set by default)" if self.value is None else "" + warn_or_error( + Lambdex.removal_version, + f'use of `layout="lambdex"`{from_default} in target {address}', + softwrap( + f""" + {Lambdex.removal_hint} + + Set `use_deprecated_lambdex_layout = false` in the `[GLOBAL]` section of + `pants.toml` to switch to using `layout="zip"` by default everywhere, or set it + to `true` to temporarily continue with the old behaviour and silence this + warning. + """ + ), + ) + + return layout + @rule async def digest_complete_platforms( diff --git a/src/python/pants/option/global_options.py b/src/python/pants/option/global_options.py index e7c4aca861c..62feccfb45e 100644 --- a/src/python/pants/option/global_options.py +++ b/src/python/pants/option/global_options.py @@ -1752,6 +1752,20 @@ class GlobalOptions(BootstrapOptions, Subsystem): default=[], ) + use_deprecated_lambdex_layout = BoolOption( + default=True, + help=softwrap( + """ + If `true`, any `python_awslambda` and `python_google_cloud_function` targets use the + `layout="lambdex"` by default. This is the older behaviour of these targets, and is + being removed. + + If `false`, they will instead use the improved `layout="zip"` by default, which will + become the default in future. + """ + ), + ) + @classmethod def validate_instance(cls, opts): """Validates an instance of global options for cases that are not prohibited via