2018-03-16 16:22:59 +00:00
|
|
|
"""Generates Dockerfiles based on an input matrix based on Python."""
|
2017-11-30 07:20:24 +00:00
|
|
|
import os
|
2018-03-16 16:22:59 +00:00
|
|
|
|
2018-04-10 13:44:03 +00:00
|
|
|
from ..conda import CondaBuildPack
|
2017-11-30 07:20:24 +00:00
|
|
|
|
|
|
|
|
2018-04-10 13:44:03 +00:00
|
|
|
class PythonBuildPack(CondaBuildPack):
|
|
|
|
"""Setup Python for use with a repository."""
|
2018-03-16 22:02:12 +00:00
|
|
|
|
2018-04-10 13:44:03 +00:00
|
|
|
@property
|
|
|
|
def python_version(self):
|
2019-05-31 09:10:17 +00:00
|
|
|
if hasattr(self, "_python_version"):
|
2018-04-10 13:44:03 +00:00
|
|
|
return self._python_version
|
2018-03-26 10:38:35 +00:00
|
|
|
|
2018-04-10 13:44:03 +00:00
|
|
|
try:
|
2019-05-31 09:10:17 +00:00
|
|
|
with open(self.binder_path("runtime.txt")) as f:
|
2018-04-10 13:44:03 +00:00
|
|
|
runtime = f.read().strip()
|
|
|
|
except FileNotFoundError:
|
2019-05-31 09:10:17 +00:00
|
|
|
runtime = ""
|
2018-04-18 10:40:04 +00:00
|
|
|
|
2019-05-31 09:10:17 +00:00
|
|
|
if not runtime.startswith("python-"):
|
2018-04-18 10:40:04 +00:00
|
|
|
# not a Python runtime (e.g. R, which subclasses this)
|
|
|
|
# use the default Python
|
2019-05-31 09:10:17 +00:00
|
|
|
self._python_version = self.major_pythons["3"]
|
2018-04-18 10:40:04 +00:00
|
|
|
return self._python_version
|
|
|
|
|
2019-05-31 09:10:17 +00:00
|
|
|
py_version_info = runtime.split("-", 1)[1].split(".")
|
|
|
|
py_version = ""
|
2018-04-10 13:44:03 +00:00
|
|
|
if len(py_version_info) == 1:
|
|
|
|
py_version = self.major_pythons[py_version_info[0]]
|
|
|
|
else:
|
|
|
|
# get major.minor
|
2019-05-31 09:10:17 +00:00
|
|
|
py_version = ".".join(py_version_info[:2])
|
2018-04-10 13:44:03 +00:00
|
|
|
self._python_version = py_version
|
|
|
|
return self._python_version
|
2017-11-30 07:20:24 +00:00
|
|
|
|
2018-09-18 12:42:58 +00:00
|
|
|
def get_assemble_files(self):
|
|
|
|
assemble_files = super().get_assemble_files()
|
|
|
|
for name in ('requirements.txt', 'requirements3.txt'):
|
|
|
|
requirements_txt = self.binder_path(name)
|
|
|
|
if os.path.exists(requirements_txt):
|
|
|
|
assemble_files.append(requirements_txt)
|
|
|
|
return assemble_files
|
|
|
|
|
|
|
|
def _is_local_requirement(self, line):
|
|
|
|
"""Return whether a line in a requirements.txt file references a local file"""
|
|
|
|
# trim comments and skip empty lines
|
|
|
|
line = line.split('#', 1)[0].strip()
|
|
|
|
if not line:
|
|
|
|
return False
|
|
|
|
if line.startswith(('-r', '-c')):
|
|
|
|
# local -r or -c references break isolation
|
|
|
|
return True
|
|
|
|
# strip off `-e, etc.`
|
|
|
|
if line.startswith('-'):
|
|
|
|
line = line.split(None, 1)[1]
|
|
|
|
if 'file://' in line:
|
|
|
|
# file references break isolation
|
|
|
|
return True
|
|
|
|
if '://' in line:
|
|
|
|
# handle git://../local/file
|
|
|
|
path = line.split('://', 1)[1]
|
|
|
|
else:
|
|
|
|
path = line
|
|
|
|
if path.startswith('.'):
|
|
|
|
# references a local file
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def assemble_from_subset(self):
|
|
|
|
"""Peek in requirements.txt to determine if we can assemble from only env files
|
|
|
|
|
|
|
|
If there are any local references, e.g. `-e .`,
|
|
|
|
stage the whole repo prior to installation.
|
|
|
|
"""
|
|
|
|
if not os.path.exists('binder') and os.path.exists('setup.py'):
|
|
|
|
# can't install from subset if we're using setup.py
|
|
|
|
return False
|
|
|
|
for name in ('requirements.txt', 'requirements3.txt'):
|
|
|
|
requirements_txt = self.binder_path(name)
|
|
|
|
if not os.path.exists(requirements_txt):
|
|
|
|
continue
|
|
|
|
with open(requirements_txt) as f:
|
|
|
|
for line in f:
|
|
|
|
if self._is_local_requirement(line):
|
|
|
|
return False
|
|
|
|
|
|
|
|
# didn't find any local references,
|
|
|
|
# allow assembly from subset
|
|
|
|
return True
|
|
|
|
|
2018-02-01 09:43:14 +00:00
|
|
|
def get_assemble_scripts(self):
|
2018-03-16 16:22:59 +00:00
|
|
|
"""Return series of build-steps specific to this repository.
|
|
|
|
"""
|
2017-11-30 07:20:24 +00:00
|
|
|
# If we have a runtime.txt & that's set to python-2.7,
|
2018-04-16 14:51:15 +00:00
|
|
|
# requirements.txt will be installed in the *kernel* env
|
|
|
|
# and requirements3.txt (if it exists)
|
|
|
|
# will be installed in the python 3 notebook server env.
|
2018-02-01 11:19:06 +00:00
|
|
|
assemble_scripts = super().get_assemble_scripts()
|
2019-05-31 09:10:17 +00:00
|
|
|
setup_py = "setup.py"
|
2018-04-16 14:53:34 +00:00
|
|
|
# KERNEL_PYTHON_PREFIX is the env with the kernel,
|
|
|
|
# whether it's distinct from the notebook or the same.
|
2019-05-31 09:10:17 +00:00
|
|
|
pip = "${KERNEL_PYTHON_PREFIX}/bin/pip"
|
2018-04-10 13:44:03 +00:00
|
|
|
if self.py2:
|
|
|
|
# using python 2 kernel,
|
2018-04-16 14:51:15 +00:00
|
|
|
# requirements3.txt allows installation in the notebook server env
|
2019-05-31 09:10:17 +00:00
|
|
|
nb_requirements_file = self.binder_path("requirements3.txt")
|
2018-04-10 13:44:03 +00:00
|
|
|
if os.path.exists(nb_requirements_file):
|
2019-05-31 09:10:17 +00:00
|
|
|
assemble_scripts.append(
|
|
|
|
(
|
|
|
|
"${NB_USER}",
|
|
|
|
# want the $NB_PYHTON_PREFIX environment variable, not for
|
|
|
|
# Python's string formatting to try and replace this
|
|
|
|
'${{NB_PYTHON_PREFIX}}/bin/pip install --no-cache-dir -r "{}"'.format(
|
|
|
|
nb_requirements_file
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
2018-04-10 13:44:03 +00:00
|
|
|
|
|
|
|
# install requirements.txt in the kernel env
|
2019-05-31 09:10:17 +00:00
|
|
|
requirements_file = self.binder_path("requirements.txt")
|
2017-11-30 07:20:24 +00:00
|
|
|
if os.path.exists(requirements_file):
|
2019-05-31 09:10:17 +00:00
|
|
|
assemble_scripts.append(
|
|
|
|
(
|
|
|
|
"${NB_USER}",
|
|
|
|
'{} install --no-cache-dir -r "{}"'.format(pip, requirements_file),
|
|
|
|
)
|
|
|
|
)
|
2018-04-10 13:44:03 +00:00
|
|
|
|
|
|
|
# setup.py exists *and* binder dir is not used
|
2019-04-27 21:28:50 +00:00
|
|
|
if not self.binder_dir and os.path.exists(setup_py):
|
2019-05-31 09:10:17 +00:00
|
|
|
assemble_scripts.append(
|
|
|
|
("${NB_USER}", "{} install --no-cache-dir .".format(pip))
|
|
|
|
)
|
2018-02-01 11:19:06 +00:00
|
|
|
return assemble_scripts
|
2017-11-30 07:20:24 +00:00
|
|
|
|
|
|
|
def detect(self):
|
2018-04-16 08:57:42 +00:00
|
|
|
"""Check if current repo should be built with the Python buildpack.
|
2018-03-16 16:22:59 +00:00
|
|
|
"""
|
2019-05-31 09:10:17 +00:00
|
|
|
requirements_txt = self.binder_path("requirements.txt")
|
|
|
|
runtime_txt = self.binder_path("runtime.txt")
|
|
|
|
setup_py = "setup.py"
|
2018-03-23 14:25:38 +00:00
|
|
|
|
|
|
|
if os.path.exists(runtime_txt):
|
|
|
|
with open(runtime_txt) as f:
|
|
|
|
runtime = f.read().strip()
|
2018-04-10 13:44:03 +00:00
|
|
|
if runtime.startswith("python-"):
|
2018-03-23 14:25:38 +00:00
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
2019-04-27 21:28:50 +00:00
|
|
|
if not self.binder_dir and os.path.exists(setup_py):
|
2018-03-27 13:28:58 +00:00
|
|
|
return True
|
2018-03-23 14:25:38 +00:00
|
|
|
return os.path.exists(requirements_txt)
|