Skip to content

Commit 47d61a5

Browse files
committed
feat: add python resolver and validator
1 parent cfe0d69 commit 47d61a5

File tree

10 files changed

+142
-25
lines changed

10 files changed

+142
-25
lines changed

aws_lambda_builders/path_resolver.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
"""
2-
Basic Path Resolver that just looks for the language in the path.
2+
Basic Path Resolver that just returns the runtime.
33
"""
44

5-
from whichcraft import which
6-
75

86
class PathResolver(object):
97

@@ -12,4 +10,4 @@ def __init__(self, runtime):
1210

1311
@property
1412
def path(self):
15-
return which(self.runtime)
13+
return self.runtime

aws_lambda_builders/validator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
class RuntimeValidator(object):
1111

12-
def __init__(self, runtime_path):
12+
def __init__(self, runtime, runtime_path):
13+
self.runtime = runtime
1314
self.runtime_path = runtime_path
1415

1516
def validate_runtime(self):

aws_lambda_builders/workflow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def get_validator(self):
166166
"""
167167
No-op validator that does not validate the runtime_path.
168168
"""
169-
return RuntimeValidator(runtime_path=self.get_executable())
169+
return RuntimeValidator(runtime_path=self.get_executable(), runtime=self.runtime)
170170

171171
@sanitize
172172
def run(self):

aws_lambda_builders/workflows/python_pip/actions.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"""
44

55
from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError
6-
from .packager import PythonPipDependencyBuilder, PackagerError
6+
from aws_lambda_builders.workflows.python_pip.utils import OSUtils
7+
from .packager import PythonPipDependencyBuilder, PackagerError, DependencyBuilder, SubprocessPip, PipRunner
78

89

910
class PythonPipBuildAction(BaseAction):
@@ -12,12 +13,20 @@ class PythonPipBuildAction(BaseAction):
1213
DESCRIPTION = "Installing dependencies from PIP"
1314
PURPOSE = Purpose.RESOLVE_DEPENDENCIES
1415

15-
def __init__(self, artifacts_dir, manifest_path, scratch_dir, runtime):
16+
def __init__(self, artifacts_dir, manifest_path, scratch_dir, runtime, runtime_path):
1617
self.artifacts_dir = artifacts_dir
1718
self.manifest_path = manifest_path
1819
self.scratch_dir = scratch_dir
1920
self.runtime = runtime
20-
self.package_builder = PythonPipDependencyBuilder(runtime=runtime)
21+
self.runtime_path = runtime_path
22+
self.pip = SubprocessPip(osutils=OSUtils(), python_exe=runtime_path)
23+
self.pip_runner = PipRunner(python_exe=runtime_path, pip=self.pip)
24+
self.dependency_builder = DependencyBuilder(osutils=OSUtils(), pip_runner=self.pip_runner,
25+
runtime=runtime)
26+
27+
self.package_builder = PythonPipDependencyBuilder(osutils=OSUtils(),
28+
runtime=runtime,
29+
dependency_builder=self.dependency_builder)
2130

2231
def execute(self):
2332
try:

aws_lambda_builders/workflows/python_pip/compat.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import os
22

3+
from aws_lambda_builders.workflows.python_pip.utils import OSUtils
34

4-
def pip_import_string():
5-
import pip
6-
pip_major_version = pip.__version__.split('.')[0]
5+
6+
def pip_import_string(python_exe):
7+
os_utils = OSUtils()
8+
cmd = [
9+
python_exe,
10+
"-c",
11+
"import pip; assert pip.__version__.split('.')[0] == 9"
12+
]
13+
p = os_utils.popen(cmd,stdout=os_utils.pipe, stderr=os_utils.pipe)
14+
p.communicate()
715
# Pip moved its internals to an _internal module in version 10.
816
# In order to be compatible with version 9 which has it at at the
917
# top level we need to figure out the correct import path here.
10-
if pip_major_version == '9':
18+
if p.returncode == 0:
1119
return 'from pip import main'
1220
else:
1321
return 'from pip._internal import main'

aws_lambda_builders/workflows/python_pip/packager.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def __init__(self, osutils, runtime, pip_runner=None):
177177
"""
178178
self._osutils = osutils
179179
if pip_runner is None:
180-
pip_runner = PipRunner(SubprocessPip(osutils))
180+
pip_runner = PipRunner(python_exe=None, pip=SubprocessPip(osutils))
181181
self._pip = pip_runner
182182
self.runtime = runtime
183183

@@ -546,25 +546,25 @@ def get_package_name_and_version(self, sdist_path):
546546

547547
class SubprocessPip(object):
548548
"""Wrapper around calling pip through a subprocess."""
549-
def __init__(self, osutils=None, import_string=None):
549+
def __init__(self, osutils=None, python_exe=None, import_string=None):
550550
if osutils is None:
551551
osutils = OSUtils()
552552
self._osutils = osutils
553+
self.python_exe = python_exe
553554
if import_string is None:
554-
import_string = pip_import_string()
555+
import_string = pip_import_string(python_exe=self.python_exe)
555556
self._import_string = import_string
556557

557558
def main(self, args, env_vars=None, shim=None):
558559
if env_vars is None:
559560
env_vars = self._osutils.environ()
560561
if shim is None:
561562
shim = ''
562-
python_exe = sys.executable
563563
run_pip = (
564564
'import sys; %s; sys.exit(main(%s))'
565565
) % (self._import_string, args)
566566
exec_string = '%s%s' % (shim, run_pip)
567-
invoke_pip = [python_exe, '-c', exec_string]
567+
invoke_pip = [self.python_exe, '-c', exec_string]
568568
p = self._osutils.popen(invoke_pip,
569569
stdout=self._osutils.pipe,
570570
stderr=self._osutils.pipe,
@@ -581,9 +581,10 @@ class PipRunner(object):
581581
" Link is a directory,"
582582
" ignoring download_dir")
583583

584-
def __init__(self, pip, osutils=None):
584+
def __init__(self, python_exe, pip, osutils=None):
585585
if osutils is None:
586586
osutils = OSUtils()
587+
self.python_exe = python_exe
587588
self._wrapped_pip = pip
588589
self._osutils = osutils
589590

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""
2+
Python Path Resolver that looks for the executable by runtime first, before proceeding to 'python' in PATH.
3+
"""
4+
5+
from whichcraft import which
6+
7+
8+
class PythonPathResolver(object):
9+
10+
def __init__(self, runtime):
11+
self.language = 'python'
12+
self.runtime = runtime
13+
self.executables = [self.runtime, self.language]
14+
15+
def _which(self):
16+
for executable in self.executables:
17+
path = which(executable)
18+
if path:
19+
return path
20+
raise ValueError("Path resolution for runtime: {} of language: "
21+
"{} was not successful".format(self.runtime, self.language))
22+
23+
@property
24+
def path(self):
25+
return self._which()
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Supported Runtimes and their validations.
3+
"""
4+
5+
import logging
6+
import os
7+
import subprocess
8+
9+
from aws_lambda_builders.exceptions import MisMatchRuntimeError
10+
11+
LOG = logging.getLogger(__name__)
12+
13+
14+
class PythonRuntimeValidator(object):
15+
SUPPORTED_RUNTIMES = [
16+
"python2.7",
17+
"python3.6",
18+
"python3.7"
19+
]
20+
21+
def __init__(self, runtime, runtime_path):
22+
self.language = "python"
23+
self.runtime = runtime
24+
self.runtime_path = runtime_path
25+
26+
def has_runtime(self):
27+
"""
28+
Checks if the runtime is supported.
29+
:param string runtime: Runtime to check
30+
:return bool: True, if the runtime is supported.
31+
"""
32+
return self.runtime in self.SUPPORTED_RUNTIMES
33+
34+
def validate_runtime(self):
35+
"""
36+
Checks if the language supplied matches the required lambda runtime
37+
:param string runtime_path: runtime to check eg: /usr/bin/python3.6
38+
:raises MisMatchRuntimeError: Version mismatch of the language vs the required runtime
39+
"""
40+
if not self.has_runtime():
41+
LOG.warning("'%s' runtime is not "
42+
"a supported runtime", self.runtime_path)
43+
return
44+
cmd = self._validate_python_cmd(self.runtime_path)
45+
46+
p = subprocess.Popen(cmd,
47+
cwd=os.getcwd(),
48+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
49+
p.communicate()
50+
if p.returncode != 0:
51+
raise MisMatchRuntimeError(language=self.language,
52+
required_runtime=self.runtime,
53+
runtime_path=self.runtime_path)
54+
55+
def _validate_python_cmd(self, runtime_path):
56+
major, minor = self.runtime.replace(self.language, "").split('.')
57+
cmd = [
58+
runtime_path,
59+
"-c",
60+
"import sys; "
61+
"assert sys.version_info.major == {major} "
62+
"and sys.version_info.minor == {minor}".format(
63+
major=major,
64+
minor=minor)]
65+
return cmd

aws_lambda_builders/workflows/python_pip/workflow.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from aws_lambda_builders.workflow import BaseWorkflow, Capability
66
from aws_lambda_builders.actions import CopySourceAction
7+
from aws_lambda_builders.workflows.python_pip.path_resolver import PythonPathResolver
8+
from aws_lambda_builders.workflows.python_pip.validator import PythonRuntimeValidator
79

810
from .actions import PythonPipBuildAction
911

@@ -65,6 +67,12 @@ def __init__(self,
6567

6668
self.actions = [
6769
PythonPipBuildAction(artifacts_dir, scratch_dir,
68-
manifest_path, runtime),
70+
manifest_path, runtime, runtime_path=self.get_executable()),
6971
CopySourceAction(source_dir, artifacts_dir, excludes=self.EXCLUDED_FILES),
7072
]
73+
74+
def get_executable(self):
75+
return PythonPathResolver(runtime=self.runtime).path
76+
77+
def get_validator(self):
78+
return PythonRuntimeValidator(runtime=self.runtime, runtime_path=self.get_executable())

tests/integration/workflows/python_pip/test_python_pip.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import tempfile
66
from unittest import TestCase
77

8+
from whichcraft import which
9+
810
from aws_lambda_builders.builder import LambdaBuilder
911
from aws_lambda_builders.exceptions import WorkflowFailedError, MisMatchRuntimeError
1012

@@ -53,13 +55,13 @@ def test_must_build_python_project(self):
5355
self.assertEquals(expected_files, output_files)
5456

5557
def test_mismatch_runtime_python_project(self):
56-
with self.assertRaises(MisMatchRuntimeError) as mismatch_error:
58+
# NOTE : Build still works if other versions of python are accesible on the path. eg: /usr/bin python2.7
59+
# is still accessible within a python 3 virtualenv.
60+
try:
5761
self.builder.build(self.source_dir, self.artifacts_dir, self.scratch_dir, self.manifest_path_valid,
5862
runtime=self.runtime_mismatch[self.runtime])
59-
self.assertEquals(mismatch_error.msg,
60-
MisMatchRuntimeError(language="python",
61-
required_runtime=self.runtime_mismatch[self.runtime],
62-
found_runtime=self.runtime).MESSAGE)
63+
except MisMatchRuntimeError as ex:
64+
self.assertIsNone(which(self.runtime_mismatch[self.runtime]))
6365

6466
def test_runtime_validate_python_project_fail_open_unsupported_runtime(self):
6567
with self.assertRaises(WorkflowFailedError):

0 commit comments

Comments
 (0)