Skip to content

Commit 624b6c3

Browse files
committed
feat: Python 37 support (#32)
* Python 37 support * Making the ABI selection explicit * Fixing the lambda_abi to a supported list * Making tests work
1 parent 0b30c14 commit 624b6c3

File tree

10 files changed

+78
-51
lines changed

10 files changed

+78
-51
lines changed

.appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ environment:
77

88
- PYTHON: "C:\\Python27-x64"
99
- PYTHON: "C:\\Python36-x64"
10-
# - PYTHON: "C:\\Python37-x64"
10+
- PYTHON: "C:\\Python37-x64"
1111

1212

1313
build: off

.travis.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ python:
55
- "2.7"
66
- "3.6"
77
# Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs
8-
#matrix:
9-
# include:
10-
# - python: 3.7
11-
# dist: xenial
12-
# sudo: true
8+
matrix:
9+
include:
10+
- python: 3.7
11+
dist: xenial
12+
sudo: true
1313
install:
1414
# Install the code requirements
1515
- make init

aws_lambda_builders/validate.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def validate_python_cmd(required_language, required_runtime_version):
3232
class RuntimeValidator(object):
3333
SUPPORTED_RUNTIMES = [
3434
"python2.7",
35-
"python3.6"
35+
"python3.6",
36+
"python3.7",
3637
]
3738

3839
@classmethod

aws_lambda_builders/workflows/python_pip/actions.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ def __init__(self, artifacts_dir, manifest_path, scratch_dir, runtime):
1717
self.manifest_path = manifest_path
1818
self.scratch_dir = scratch_dir
1919
self.runtime = runtime
20-
self.package_builder = PythonPipDependencyBuilder()
20+
self.package_builder = PythonPipDependencyBuilder(runtime=runtime)
2121

2222
def execute(self):
2323
try:
2424
self.package_builder.build_dependencies(
2525
self.artifacts_dir,
2626
self.manifest_path,
27-
self.scratch_dir,
28-
self.runtime,
27+
self.scratch_dir
2928
)
3029
except PackagerError as ex:
3130
raise ActionFailedError(str(ex))

aws_lambda_builders/workflows/python_pip/compat.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
import six
32

43

54
def pip_import_string():
@@ -101,9 +100,3 @@ def raise_compile_error(*args, **kwargs):
101100
pip_no_compile_c_env_vars = {
102101
'CC': '/var/false'
103102
}
104-
105-
106-
if six.PY3:
107-
lambda_abi = 'cp36m'
108-
else:
109-
lambda_abi = 'cp27mu'

aws_lambda_builders/workflows/python_pip/packager.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from email.parser import FeedParser
1010

1111

12-
from .compat import lambda_abi
1312
from .compat import pip_import_string
1413
from .compat import pip_no_compile_c_env_vars
1514
from .compat import pip_no_compile_c_shim
@@ -59,10 +58,36 @@ class PackageDownloadError(PackagerError):
5958
pass
6059

6160

61+
class UnsupportedPythonVersion(PackagerError):
62+
"""Generic networking error during a package download."""
63+
def __init__(self, version):
64+
super(UnsupportedPythonVersion, self).__init__(
65+
"'%s' version of python is not supported" % version
66+
)
67+
68+
69+
def get_lambda_abi(runtime):
70+
supported = {
71+
"python2.7": "cp27mu",
72+
"python3.6": "cp36m",
73+
"python3.7": "cp37m"
74+
}
75+
76+
if runtime not in supported:
77+
raise UnsupportedPythonVersion(runtime)
78+
79+
return supported[runtime]
80+
81+
6282
class PythonPipDependencyBuilder(object):
63-
def __init__(self, osutils=None, dependency_builder=None):
83+
def __init__(self, runtime, osutils=None, dependency_builder=None):
6484
"""Initialize a PythonPipDependencyBuilder.
6585
86+
:type runtime: str
87+
:param runtime: Python version to build dependencies for. This can
88+
either be python2.7, python3.6 or python3.7. These are currently the
89+
only supported values.
90+
6691
:type osutils: :class:`lambda_builders.utils.OSUtils`
6792
:param osutils: A class used for all interactions with the
6893
outside OS.
@@ -76,11 +101,11 @@ def __init__(self, osutils=None, dependency_builder=None):
76101
self.osutils = OSUtils()
77102

78103
if dependency_builder is None:
79-
dependency_builder = DependencyBuilder(self.osutils)
104+
dependency_builder = DependencyBuilder(self.osutils, runtime)
80105
self._dependency_builder = dependency_builder
81106

82107
def build_dependencies(self, artifacts_dir_path, scratch_dir_path,
83-
requirements_path, runtime, ui=None, config=None):
108+
requirements_path, ui=None, config=None):
84109
"""Builds a python project's dependencies into an artifact directory.
85110
86111
:type artifacts_dir_path: str
@@ -93,11 +118,6 @@ def build_dependencies(self, artifacts_dir_path, scratch_dir_path,
93118
:param requirements_path: Path to a requirements.txt file to inspect
94119
for a list of dependencies.
95120
96-
:type runtime: str
97-
:param runtime: Python version to build dependencies for. This can
98-
either be python2.7 or python3.6. These are currently the only
99-
supported values.
100-
101121
:type ui: :class:`lambda_builders.utils.UI` or None
102122
:param ui: A class that traps all progress information such as status
103123
and errors. If injected by the caller, it can be used to monitor
@@ -141,13 +161,16 @@ class DependencyBuilder(object):
141161
'sqlalchemy'
142162
}
143163

144-
def __init__(self, osutils, pip_runner=None):
164+
def __init__(self, osutils, runtime, pip_runner=None):
145165
"""Initialize a DependencyBuilder.
146166
147167
:type osutils: :class:`lambda_builders.utils.OSUtils`
148168
:param osutils: A class used for all interactions with the
149169
outside OS.
150170
171+
:type runtime: str
172+
:param runtime: AWS Lambda Python runtime to build for
173+
151174
:type pip_runner: :class:`PipRunner`
152175
:param pip_runner: This class is responsible for executing our pip
153176
on our behalf.
@@ -156,6 +179,7 @@ def __init__(self, osutils, pip_runner=None):
156179
if pip_runner is None:
157180
pip_runner = PipRunner(SubprocessPip(osutils))
158181
self._pip = pip_runner
182+
self.runtime = runtime
159183

160184
def build_site_packages(self, requirements_filepath,
161185
target_directory,
@@ -312,8 +336,9 @@ def _download_all_dependencies(self, requirements_filename, directory):
312336
def _download_binary_wheels(self, packages, directory):
313337
# Try to get binary wheels for each package that isn't compatible.
314338
LOG.debug("Downloading missing wheels: %s", packages)
339+
lambda_abi = get_lambda_abi(self.runtime)
315340
self._pip.download_manylinux_wheels(
316-
[pkg.identifier for pkg in packages], directory)
341+
[pkg.identifier for pkg in packages], directory, lambda_abi)
317342

318343
def _build_sdists(self, sdists, directory, compile_c=True):
319344
LOG.debug("Build missing wheels from sdists "
@@ -341,6 +366,9 @@ def _is_compatible_wheel_filename(self, filename):
341366
# Verify platform is compatible
342367
if platform not in self._MANYLINUX_COMPATIBLE_PLATFORM:
343368
return False
369+
370+
lambda_runtime_abi = get_lambda_abi(self.runtime)
371+
344372
# Verify that the ABI is compatible with lambda. Either none or the
345373
# correct type for the python version cp27mu for py27 and cp36m for
346374
# py36.
@@ -351,7 +379,7 @@ def _is_compatible_wheel_filename(self, filename):
351379
# Deploying python 3 function which means we need cp36m abi
352380
# We can also accept abi3 which is the CPython 3 Stable ABI and
353381
# will work on any version of python 3.
354-
return abi == 'cp36m' or abi == 'abi3'
382+
return abi == lambda_runtime_abi or abi == 'abi3'
355383
elif prefix_version == 'cp2':
356384
# Deploying to python 2 function which means we need cp27mu abi
357385
return abi == 'cp27mu'
@@ -615,7 +643,7 @@ def download_all_dependencies(self, requirements_filename, directory):
615643
# complain at deployment time.
616644
self.build_wheel(wheel_package_path, directory)
617645

618-
def download_manylinux_wheels(self, packages, directory):
646+
def download_manylinux_wheels(self, packages, directory, lambda_abi):
619647
"""Download wheel files for manylinux for all the given packages."""
620648
# If any one of these dependencies fails pip will bail out. Since we
621649
# are only interested in all the ones we can download, we need to feed

tests/functional/workflows/python_pip/test_packager.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import os
23
import zipfile
34
import tarfile
@@ -16,7 +17,7 @@
1617
from aws_lambda_builders.workflows.python_pip.packager import SDistMetadataFetcher
1718
from aws_lambda_builders.workflows.python_pip.packager import \
1819
InvalidSourceDistributionNameError
19-
from aws_lambda_builders.workflows.python_pip.compat import lambda_abi
20+
from aws_lambda_builders.workflows.python_pip.packager import get_lambda_abi
2021
from aws_lambda_builders.workflows.python_pip.compat import pip_no_compile_c_env_vars
2122
from aws_lambda_builders.workflows.python_pip.compat import pip_no_compile_c_shim
2223
from aws_lambda_builders.workflows.python_pip.utils import OSUtils
@@ -214,7 +215,7 @@ def _write_requirements_txt(self, packages, directory):
214215
def _make_appdir_and_dependency_builder(self, reqs, tmpdir, runner):
215216
appdir = str(_create_app_structure(tmpdir))
216217
self._write_requirements_txt(reqs, appdir)
217-
builder = DependencyBuilder(OSUtils(), runner)
218+
builder = DependencyBuilder(OSUtils(), "python3.6", runner)
218219
return appdir, builder
219220

220221
def test_can_build_local_dir_as_whl(self, tmpdir, pip_runner, osutils):
@@ -644,7 +645,7 @@ def test_can_replace_incompat_whl(self, tmpdir, osutils, pip_runner):
644645
expected_args=[
645646
'--only-binary=:all:', '--no-deps', '--platform',
646647
'manylinux1_x86_64', '--implementation', 'cp',
647-
'--abi', lambda_abi, '--dest', mock.ANY,
648+
'--abi', get_lambda_abi(builder.runtime), '--dest', mock.ANY,
648649
'bar==1.2'
649650
],
650651
packages=[
@@ -677,7 +678,7 @@ def test_whitelist_sqlalchemy(self, tmpdir, osutils, pip_runner):
677678
expected_args=[
678679
'--only-binary=:all:', '--no-deps', '--platform',
679680
'manylinux1_x86_64', '--implementation', 'cp',
680-
'--abi', lambda_abi, '--dest', mock.ANY,
681+
'--abi', get_lambda_abi(builder.runtime), '--dest', mock.ANY,
681682
'sqlalchemy==1.1.18'
682683
],
683684
packages=[
@@ -839,7 +840,7 @@ def test_build_into_existing_dir_with_preinstalled_packages(
839840
expected_args=[
840841
'--only-binary=:all:', '--no-deps', '--platform',
841842
'manylinux1_x86_64', '--implementation', 'cp',
842-
'--abi', lambda_abi, '--dest', mock.ANY,
843+
'--abi', get_lambda_abi(builder.runtime), '--dest', mock.ANY,
843844
'foo==1.2'
844845
],
845846
packages=[

tests/integration/workflows/python_pip/test_python_pip.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,9 @@ def test_must_build_python_project(self):
4747
self.assertEquals(expected_files, output_files)
4848

4949
def test_runtime_validate_python_project_fail_open_unsupported_runtime(self):
50-
self.builder.build(self.source_dir, self.artifacts_dir, self.scratch_dir, self.manifest_path_valid,
51-
runtime="python2.8")
52-
expected_files = self.test_data_files.union({"numpy", "numpy-1.15.4.data", "numpy-1.15.4.dist-info"})
53-
output_files = set(os.listdir(self.artifacts_dir))
54-
self.assertEquals(expected_files, output_files)
50+
with self.assertRaises(WorkflowFailedError):
51+
self.builder.build(self.source_dir, self.artifacts_dir, self.scratch_dir, self.manifest_path_valid,
52+
runtime="python2.8")
5553

5654
def test_must_fail_to_resolve_dependencies(self):
5755

tests/unit/workflows/python_pip/test_actions.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ def test_action_must_call_builder(self, PythonPipDependencyBuilderMock):
2020

2121
builder_instance.build_dependencies.assert_called_with("artifacts",
2222
"scratch_dir",
23-
"manifest",
24-
"runtime")
23+
"manifest")
2524

2625
@patch("aws_lambda_builders.workflows.python_pip.actions.PythonPipDependencyBuilder")
2726
def test_must_raise_exception_on_failure(self, PythonPipDependencyBuilderMock):

tests/unit/workflows/python_pip/test_packager.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import sys
21
from collections import namedtuple
32

43
import mock
@@ -12,6 +11,7 @@
1211
from aws_lambda_builders.workflows.python_pip.packager import Package
1312
from aws_lambda_builders.workflows.python_pip.packager import PipRunner
1413
from aws_lambda_builders.workflows.python_pip.packager import SubprocessPip
14+
from aws_lambda_builders.workflows.python_pip.packager import get_lambda_abi
1515
from aws_lambda_builders.workflows.python_pip.packager \
1616
import InvalidSourceDistributionNameError
1717
from aws_lambda_builders.workflows.python_pip.packager import NoSuchPackageError
@@ -85,17 +85,29 @@ def popen(self, *args, **kwargs):
8585
return self._processes.pop()
8686

8787

88+
class TestGetLambdaAbi(object):
89+
def test_get_lambda_abi_python27(self):
90+
assert "cp27mu" == get_lambda_abi("python2.7")
91+
92+
def test_get_lambda_abi_python36(self):
93+
assert "cp36m" == get_lambda_abi("python3.6")
94+
95+
def test_get_lambda_abi_python37(self):
96+
assert "cp37m" == get_lambda_abi("python3.7")
97+
98+
8899
class TestPythonPipDependencyBuilder(object):
89100
def test_can_call_dependency_builder(self, osutils):
90101
mock_dep_builder = mock.Mock(spec=DependencyBuilder)
91102
osutils_mock = mock.Mock(spec=osutils)
92103
builder = PythonPipDependencyBuilder(
93104
osutils=osutils_mock,
94105
dependency_builder=mock_dep_builder,
106+
runtime="runtime"
95107
)
96108
builder.build_dependencies(
97109
'artifacts/path/', 'scratch_dir/path/',
98-
'path/to/requirements.txt', 'python3.6'
110+
'path/to/requirements.txt'
99111
)
100112
mock_dep_builder.build_site_packages.assert_called_once_with(
101113
'path/to/requirements.txt', 'artifacts/path/', 'scratch_dir/path/')
@@ -218,14 +230,10 @@ def test_download_wheels(self, pip_factory):
218230
# for getting lambda compatible wheels.
219231
pip, runner = pip_factory()
220232
packages = ['foo', 'bar', 'baz']
221-
runner.download_manylinux_wheels(packages, 'directory')
222-
if sys.version_info[0] == 2:
223-
abi = 'cp27mu'
224-
else:
225-
abi = 'cp36m'
233+
runner.download_manylinux_wheels(packages, 'directory', "abi")
226234
expected_prefix = ['download', '--only-binary=:all:', '--no-deps',
227235
'--platform', 'manylinux1_x86_64',
228-
'--implementation', 'cp', '--abi', abi,
236+
'--implementation', 'cp', '--abi', "abi",
229237
'--dest', 'directory']
230238
for i, package in enumerate(packages):
231239
assert pip.calls[i].args == expected_prefix + [package]
@@ -234,7 +242,7 @@ def test_download_wheels(self, pip_factory):
234242

235243
def test_download_wheels_no_wheels(self, pip_factory):
236244
pip, runner = pip_factory()
237-
runner.download_manylinux_wheels([], 'directory')
245+
runner.download_manylinux_wheels([], 'directory', "abi")
238246
assert len(pip.calls) == 0
239247

240248
def test_raise_no_such_package_error(self, pip_factory):

0 commit comments

Comments
 (0)