Skip to content

Commit 79007a4

Browse files
committed
feat: re-write of path resolver
- Every workflow has a list of validators, resolvers and binaries - Actions can choose to use it or not.
1 parent fb9bfbf commit 79007a4

27 files changed

+299
-217
lines changed

.appveyor.yml

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@ build: off
1515
install:
1616
# To run Nodejs workflow integ tests
1717
- ps: Install-Product node 8.10
18-
- ps: $env:Path = "$env:PYTHON;$env:PYTHON\\Scripts;$env:PYTHON\\bin;$env:Path"
19-
- ps: python --version
20-
- ps: python -m pip install -r requirements/dev.txt
21-
- ps: python -m pip install -e .
22-
- ps: python --version
23-
- ps: $env:Path = "C:\\Ruby25-x64\\bin;$env:Path"
24-
- ps: ruby --version
25-
- ps: gem install bundler --no-ri --no-rdoc
26-
- ps: bundler --version
18+
19+
- "set PATH=%PYTHON%\\Scripts;%PYTHON%\\bin;%PATH%"
20+
- "%PYTHON%\\python.exe -m pip install -r requirements/dev.txt"
21+
- "%PYTHON%\\python.exe -m pip install -e ."
22+
- "set PATH=C:\\Ruby25-x64\\bin;%PATH%"
23+
- "gem install bundler --no-ri --no-rdoc"
24+
- "bundler --version"
2725

2826
test_script:
29-
- ps: python -m pytest --cov aws_lambda_builders --cov-report term-missing tests/unit tests/functional
30-
- ps: python -m pytest tests/integration
27+
- "%PYTHON%\\python.exe -m pytest --cov aws_lambda_builders --cov-report term-missing tests/unit tests/functional"
28+
- "%PYTHON%\\python.exe -m pytest tests/integration"

.pylintrc

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

1010
# Add files or directories to the blacklist. They should be base names, not
1111
# paths.
12-
ignore=compat.py, path_resolver.py, validator.py
12+
ignore=compat.py, utils.py, path_resolver.py, validator.py
1313

1414
# Pickle collected data for later comparisons.
1515
persistent=yes
@@ -360,4 +360,4 @@ int-import-graph=
360360

361361
# Exceptions that will emit a warning when being caught. Defaults to
362362
# "Exception"
363-
overgeneral-exceptions=Exception
363+
overgeneral-exceptions=Exception

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ init:
33

44
test:
55
# Run unit tests
6-
# Fail if coverage falls below 94%
7-
LAMBDA_BUILDERS_DEV=1 pytest --cov aws_lambda_builders --cov-report term-missing --cov-fail-under 94 tests/unit tests/functional
6+
# Fail if coverage falls below 93%
7+
LAMBDA_BUILDERS_DEV=1 pytest --cov aws_lambda_builders --cov-report term-missing --cov-fail-under 93 tests/unit tests/functional
88

99
func-test:
1010
LAMBDA_BUILDERS_DEV=1 pytest tests/functional

aws_lambda_builders/binary_path.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
Class containing resolved path of binary given a validator and a resolver and the name of the binary.
3+
"""
4+
5+
6+
class BinaryPath(object):
7+
8+
def __init__(self, resolver, validator, binary, binary_path=None):
9+
self.resolver = resolver
10+
self.validator = validator
11+
setattr(self, binary, binary)
12+
self._binary_path = binary_path
13+
self.path_provided = True if self._binary_path else False
14+
15+
@property
16+
def binary_path(self):
17+
return self._binary_path
18+
19+
@binary_path.setter
20+
def binary_path(self, binary_path):
21+
self._binary_path = binary_path

aws_lambda_builders/path_resolver.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
"""
2-
Basic Path Resolver that just returns the runtime.
2+
Basic Path Resolver that looks for the executable by runtime first, before proceeding to 'language' in PATH.
33
"""
44

5+
from aws_lambda_builders.utils import which
6+
57

68
class PathResolver(object):
79

8-
def __init__(self, runtime):
10+
def __init__(self, binary, runtime):
11+
self.binary = binary
912
self.runtime = runtime
13+
self.executables = [self.runtime, self.binary]
14+
15+
def _which(self):
16+
exec_paths = []
17+
for executable in [executable for executable in self.executables if executable is not None]:
18+
paths = which(executable)
19+
exec_paths.extend(paths)
20+
21+
if not exec_paths:
22+
raise ValueError("Path resolution for runtime: {} of binary: "
23+
"{} was not successful".format(self.runtime, self.binary))
24+
return exec_paths
1025

1126
@property
12-
def exec_path(self):
13-
return self.runtime
27+
def exec_paths(self):
28+
return self._which()

aws_lambda_builders/utils.py

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

55
import shutil
6+
import sys
67
import os
78
import logging
89

@@ -57,3 +58,69 @@ def copytree(source, destination, ignore=None):
5758
copytree(new_source, new_destination, ignore=ignore)
5859
else:
5960
shutil.copy2(new_source, new_destination)
61+
62+
63+
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
64+
"""Given a command, mode, and a PATH string, return the paths which
65+
conforms to the given mode on the PATH, or None if there is no such
66+
file.
67+
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
68+
of os.environ.get("PATH"), or can be overridden with a custom search
69+
path.
70+
Note: This function was backported from the Python 3 source code.
71+
"""
72+
# Check that a given file can be accessed with the correct mode.
73+
# Additionally check that `file` is not a directory, as on Windows
74+
# directories pass the os.access check.
75+
76+
def _access_check(fn, mode):
77+
return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn)
78+
79+
# If we're given a path with a directory part, look it up directly
80+
# rather than referring to PATH directories. This includes checking
81+
# relative to the current directory, e.g. ./script
82+
if os.path.dirname(cmd):
83+
if _access_check(cmd, mode):
84+
return cmd
85+
86+
return None
87+
88+
if path is None:
89+
path = os.environ.get("PATH", os.defpath)
90+
if not path:
91+
return None
92+
93+
path = path.split(os.pathsep)
94+
95+
if sys.platform == "win32":
96+
# The current directory takes precedence on Windows.
97+
if os.curdir not in path:
98+
path.insert(0, os.curdir)
99+
100+
# PATHEXT is necessary to check on Windows.
101+
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
102+
# See if the given file matches any of the expected path
103+
# extensions. This will allow us to short circuit when given
104+
# "python.exe". If it does match, only test that one, otherwise we
105+
# have to try others.
106+
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
107+
files = [cmd]
108+
else:
109+
files = [cmd + ext for ext in pathext]
110+
else:
111+
# On other platforms you don't have things like PATHEXT to tell you
112+
# what file suffixes are executable, so just pass on cmd as-is.
113+
files = [cmd]
114+
115+
seen = set()
116+
paths = []
117+
118+
for dir in path:
119+
normdir = os.path.normcase(dir)
120+
if normdir not in seen:
121+
seen.add(normdir)
122+
for thefile in files:
123+
name = os.path.join(dir, thefile)
124+
if _access_check(name, mode):
125+
paths.append(name)
126+
return paths

aws_lambda_builders/validator.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99

1010
class RuntimeValidator(object):
1111

12-
def __init__(self, runtime, runtime_path):
12+
def __init__(self, runtime):
1313
self.runtime = runtime
14-
self.runtime_path = runtime_path
14+
self._runtime_path = None
15+
16+
def validate(self, runtime_path):
17+
self._runtime_path = runtime_path
18+
return runtime_path
19+
1520

16-
def validate_runtime(self):
17-
pass

aws_lambda_builders/workflow.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections import namedtuple
99
import six
1010

11+
from aws_lambda_builders.binary_path import BinaryPath
1112
from aws_lambda_builders.path_resolver import PathResolver
1213
from aws_lambda_builders.validator import RuntimeValidator
1314
from aws_lambda_builders.registry import DEFAULT_REGISTRY
@@ -33,9 +34,24 @@ def sanitize(func):
3334

3435
@functools.wraps(func)
3536
def wrapper(self, *args, **kwargs):
37+
valid_paths = []
3638
# NOTE: we need to access workflow object to get the validator.
37-
validator = self.get_validator()
38-
validator.validate_runtime()
39+
self.get_binaries()
40+
# import ipdb
41+
# ipdb.set_trace()
42+
for binary_path in self.binaries:
43+
validator = binary_path.validator
44+
exec_paths = binary_path.resolver.exec_paths if not binary_path.path_provided else binary_path.binary_path
45+
for executable_path in exec_paths:
46+
valid_path = validator.validate(executable_path)
47+
if valid_path:
48+
binary_path.binary_path = valid_path
49+
valid_paths.append(valid_path)
50+
break
51+
if not valid_paths:
52+
raise WorkflowFailedError(workflow_name=self.NAME,
53+
action_name=None,
54+
reason='Binary validation failed!')
3955
func(self, *args, **kwargs)
4056
return wrapper
4157

@@ -144,6 +160,7 @@ def __init__(self,
144160

145161
# Actions are registered by the subclasses as they seem fit
146162
self.actions = []
163+
self.binaries = None
147164

148165
def is_supported(self):
149166
"""
@@ -156,17 +173,25 @@ def is_supported(self):
156173

157174
return True
158175

159-
def get_executable(self):
176+
def get_resolvers(self):
160177
"""
161-
Non specialized path resolver that just returns the first executable for the runtime on the path.
178+
Non specialized path resolver that just returns the list of executable for the runtime on the path.
162179
"""
163-
return PathResolver(runtime=self.runtime).exec_path
180+
return [PathResolver(runtime=self.runtime, binary=self.CAPABILITY.language)]
164181

165-
def get_validator(self):
182+
def get_validators(self):
166183
"""
167184
No-op validator that does not validate the runtime_path.
168185
"""
169-
return RuntimeValidator(runtime_path=self.get_executable(), runtime=self.runtime)
186+
return [RuntimeValidator(runtime=self.runtime)]
187+
188+
def get_binaries(self):
189+
if not self.binaries:
190+
resolvers = self.get_resolvers()
191+
validators = self.get_validators()
192+
self.binaries = [BinaryPath(resolver=resolver, validator=validator, binary=resolver.binary)
193+
for resolver, validator in zip(resolvers, validators)]
194+
return self.binaries
170195

171196
@sanitize
172197
def run(self):

aws_lambda_builders/workflows/nodejs_npm/workflow.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
NodeJS NPM Workflow
33
"""
4-
4+
from aws_lambda_builders.path_resolver import PathResolver
55
from aws_lambda_builders.workflow import BaseWorkflow, Capability
66
from aws_lambda_builders.actions import CopySourceAction
77
from .actions import NodejsNpmPackAction, NodejsNpmInstallAction
@@ -60,3 +60,9 @@ def __init__(self,
6060
CopySourceAction(tar_package_dir, artifacts_dir, excludes=self.EXCLUDED_FILES),
6161
npm_install,
6262
]
63+
64+
def get_resolvers(self):
65+
"""
66+
specialized path resolver that just returns the list of executable for the runtime on the path.
67+
"""
68+
return [PathResolver(runtime=self.runtime, binary="node")]

aws_lambda_builders/workflows/python_pip/actions.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,30 @@ class PythonPipBuildAction(BaseAction):
1212
NAME = 'ResolveDependencies'
1313
DESCRIPTION = "Installing dependencies from PIP"
1414
PURPOSE = Purpose.RESOLVE_DEPENDENCIES
15+
LANGUAGE = 'python'
1516

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

3224
def execute(self):
25+
os_utils = OSUtils()
26+
# import ipdb
27+
# ipdb.set_trace()
28+
python_path = [binary.binary_path for binary in self.binaries if getattr(binary, self.LANGUAGE)][0]
29+
pip = SubprocessPip(osutils=os_utils, python_exe=python_path)
30+
pip_runner = PipRunner(python_exe=python_path, pip=pip)
31+
dependency_builder = DependencyBuilder(osutils=os_utils, pip_runner=pip_runner,
32+
runtime=self.runtime)
33+
34+
package_builder = PythonPipDependencyBuilder(osutils=os_utils,
35+
runtime=self.runtime,
36+
dependency_builder=dependency_builder)
3337
try:
34-
self.package_builder.build_dependencies(
38+
package_builder.build_dependencies(
3539
self.artifacts_dir,
3640
self.manifest_path,
3741
self.scratch_dir

aws_lambda_builders/workflows/python_pip/path_resolver.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)