Skip to content

Commit 4fdac4b

Browse files
fix: skip Python binary validation when pip is not used (#678) (#830)
When no requirements.txt exists or download_dependencies=False, the workflow only copies files and doesn't run pip. In these cases, skip PythonRuntimeValidator and use the base RuntimeValidator instead. This allows builds to succeed even when the target Python runtime isn't installed locally, as long as no dependencies need to be installed. Co-authored-by: Reed Hamilton <reedham@amazon.com>
1 parent f6c9f2b commit 4fdac4b

3 files changed

Lines changed: 75 additions & 0 deletions

File tree

aws_lambda_builders/workflows/python_pip/workflow.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from aws_lambda_builders.actions import CleanUpAction, CopySourceAction, LinkSourceAction
88
from aws_lambda_builders.path_resolver import PathResolver
9+
from aws_lambda_builders.validator import RuntimeValidator
910
from aws_lambda_builders.workflow import BaseWorkflow, BuildDirectory, BuildInSourceSupport, Capability
1011
from aws_lambda_builders.workflows.python_pip.validator import PythonRuntimeValidator
1112

@@ -78,6 +79,8 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim
7879
if osutils is None:
7980
osutils = OSUtils()
8081

82+
self._use_pip = False
83+
8184
if not self.download_dependencies and not self.dependencies_dir:
8285
LOG.info(
8386
"download_dependencies is False and dependencies_dir is None. Copying the source files into the "
@@ -92,6 +95,7 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim
9295

9396
# If a requirements.txt exists, run pip builder before copy action.
9497
if self.download_dependencies:
98+
self._use_pip = True
9599
if self.dependencies_dir:
96100
# clean up the dependencies folder before installing
97101
self._actions.append(CleanUpAction(self.dependencies_dir))
@@ -161,4 +165,6 @@ def _should_create_parent_packages(self):
161165
return False
162166

163167
def get_validators(self):
168+
if not self._use_pip:
169+
return [RuntimeValidator(runtime=self.runtime, architecture=self.architecture)]
164170
return [PythonRuntimeValidator(runtime=self.runtime, architecture=self.architecture)]

tests/integration/workflows/python_pip/test_python_pip.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,39 @@ def test_must_log_warning_if_requirements_not_found(self):
303303
"requirements.txt file not found. Continuing the build without dependencies."
304304
)
305305

306+
def test_build_succeeds_with_mismatched_runtime_when_no_requirements(self):
307+
"""
308+
When no requirements.txt exists, build should succeed even if the target runtime
309+
binary validation would fail (issue #678). The base RuntimeValidator is used
310+
which only checks if the runtime is supported, not if the binary exists.
311+
"""
312+
# Mock PythonRuntimeValidator to always fail - simulating missing Python binary
313+
with mock.patch(
314+
"aws_lambda_builders.workflows.python_pip.workflow.PythonRuntimeValidator"
315+
) as mock_validator_class:
316+
mock_validator = mock.Mock()
317+
mock_validator.validate.side_effect = Exception("Python binary not found")
318+
mock_validator_class.return_value = mock_validator
319+
320+
# This should succeed because without requirements.txt, we use base RuntimeValidator
321+
# not PythonRuntimeValidator
322+
self.builder.build(
323+
self.source_dir,
324+
self.artifacts_dir,
325+
self.scratch_dir,
326+
os.path.join("non", "existent", "manifest"),
327+
runtime=self.runtime,
328+
experimental_flags=self.experimental_flags,
329+
)
330+
331+
# Should just copy source files
332+
expected_files = self.test_data_files
333+
output_files = set(os.listdir(self.artifacts_dir))
334+
self.assertEqual(expected_files, output_files)
335+
336+
# Verify PythonRuntimeValidator was never instantiated (we used base RuntimeValidator)
337+
mock_validator_class.assert_not_called()
338+
306339
@skipIf(IS_WINDOWS, "Skip in windows tests")
307340
def test_without_download_dependencies_with_dependencies_dir(self):
308341
source_dir = os.path.join(self.source_dir, "local-dependencies")

tests/unit/workflows/python_pip/test_workflow.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from aws_lambda_builders.actions import CopySourceAction, CleanUpAction, LinkSourceAction
77
from aws_lambda_builders.path_resolver import PathResolver
8+
from aws_lambda_builders.validator import RuntimeValidator
89
from aws_lambda_builders.workflows.python_pip.utils import OSUtils, EXPERIMENTAL_FLAG_BUILD_PERFORMANCE
910
from aws_lambda_builders.workflows.python_pip.validator import PythonRuntimeValidator
1011
from aws_lambda_builders.workflows.python_pip.workflow import PythonPipBuildAction, PythonPipWorkflow
@@ -60,6 +61,41 @@ def test_workflow_validator(self):
6061
for validator in self.workflow.get_validators():
6162
self.assertTrue(isinstance(validator, PythonRuntimeValidator))
6263

64+
def test_workflow_validator_without_requirements_skips_python_validation(self):
65+
"""When no requirements.txt exists, should use base RuntimeValidator (no Python binary check)."""
66+
self.osutils_mock.file_exists.return_value = False
67+
self.workflow = PythonPipWorkflow(
68+
"source",
69+
"artifacts",
70+
"scratch_dir",
71+
"manifest",
72+
runtime="python3.12",
73+
osutils=self.osutils_mock,
74+
experimental_flags=self.experimental_flags,
75+
)
76+
validators = self.workflow.get_validators()
77+
self.assertEqual(len(validators), 1)
78+
self.assertIsInstance(validators[0], RuntimeValidator)
79+
self.assertNotIsInstance(validators[0], PythonRuntimeValidator)
80+
81+
def test_workflow_validator_without_download_dependencies_skips_python_validation(self):
82+
"""When download_dependencies=False, should use base RuntimeValidator (no Python binary check)."""
83+
self.osutils_mock.file_exists.return_value = True
84+
self.workflow = PythonPipWorkflow(
85+
"source",
86+
"artifacts",
87+
"scratch_dir",
88+
"manifest",
89+
runtime="python3.12",
90+
osutils=self.osutils_mock,
91+
download_dependencies=False,
92+
experimental_flags=self.experimental_flags,
93+
)
94+
validators = self.workflow.get_validators()
95+
self.assertEqual(len(validators), 1)
96+
self.assertIsInstance(validators[0], RuntimeValidator)
97+
self.assertNotIsInstance(validators[0], PythonRuntimeValidator)
98+
6399
def test_workflow_resolver(self):
64100
for resolver in self.workflow.get_resolvers():
65101
self.assertTrue(isinstance(resolver, PathResolver))

0 commit comments

Comments
 (0)