kopia lustrzana https://github.com/jupyterhub/repo2docker
Move runtime.txt handling into base class
This will allow it to be used for Python (and other languages) in futurepull/1428/head
rodzic
e795060aec
commit
cab6fce923
|
@ -1,3 +1,4 @@
|
||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
@ -750,3 +751,41 @@ class BaseImage(BuildPack):
|
||||||
# the only path evaluated at container start time rather than build time
|
# the only path evaluated at container start time rather than build time
|
||||||
return os.path.join("${REPO_DIR}", start)
|
return os.path.join("${REPO_DIR}", start)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def runtime_info(self):
|
||||||
|
"""
|
||||||
|
Return parsed contents of runtime.txt
|
||||||
|
|
||||||
|
Returns (runtime, version, date), tuple components may be None.
|
||||||
|
Returns (None, None, None) if runtime.txt not found.
|
||||||
|
"""
|
||||||
|
if hasattr(self, "_runtime"):
|
||||||
|
return self._runtime
|
||||||
|
|
||||||
|
self._runtime = (None, None, None)
|
||||||
|
|
||||||
|
runtime_path = self.binder_path("runtime.txt")
|
||||||
|
try:
|
||||||
|
with open(runtime_path) as f:
|
||||||
|
runtime_txt = f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return self._runtime
|
||||||
|
|
||||||
|
runtime = None
|
||||||
|
version = None
|
||||||
|
date = None
|
||||||
|
|
||||||
|
parts = runtime_txt.split("-", 2)
|
||||||
|
if len(parts) > 0 and parts[0]:
|
||||||
|
runtime = parts[0]
|
||||||
|
if len(parts) > 1 and parts[1]:
|
||||||
|
version = parts[1]
|
||||||
|
if len(parts) > 2 and parts[2]:
|
||||||
|
date = parts[2]
|
||||||
|
if not re.match(r"\d\d\d\d-\d\d-\d\d", date):
|
||||||
|
raise ValueError(f"Invalid date, expected YYYY-MM-DD: {date}")
|
||||||
|
date = datetime.datetime.fromisoformat(date).date()
|
||||||
|
|
||||||
|
self._runtime = (runtime, version, date)
|
||||||
|
return self._runtime
|
||||||
|
|
|
@ -187,12 +187,9 @@ class PipfileBuildPack(CondaBuildPack):
|
||||||
def detect(self):
|
def detect(self):
|
||||||
"""Check if current repo should be built with the Pipfile buildpack."""
|
"""Check if current repo should be built with the Pipfile buildpack."""
|
||||||
# first make sure python is not explicitly unwanted
|
# first make sure python is not explicitly unwanted
|
||||||
runtime_txt = self.binder_path("runtime.txt")
|
runtime = self.runtime_info[0]
|
||||||
if os.path.exists(runtime_txt):
|
if runtime and runtime != "python":
|
||||||
with open(runtime_txt) as f:
|
return False
|
||||||
runtime = f.read().strip()
|
|
||||||
if not runtime.startswith("python-"):
|
|
||||||
return False
|
|
||||||
|
|
||||||
pipfile = self.binder_path("Pipfile")
|
pipfile = self.binder_path("Pipfile")
|
||||||
pipfile_lock = self.binder_path("Pipfile.lock")
|
pipfile_lock = self.binder_path("Pipfile.lock")
|
||||||
|
|
|
@ -15,14 +15,10 @@ class PythonBuildPack(CondaBuildPack):
|
||||||
if hasattr(self, "_python_version"):
|
if hasattr(self, "_python_version"):
|
||||||
return self._python_version
|
return self._python_version
|
||||||
|
|
||||||
try:
|
runtime, version, _ = self.runtime_info
|
||||||
with open(self.binder_path("runtime.txt")) as f:
|
|
||||||
runtime = f.read().strip()
|
|
||||||
except FileNotFoundError:
|
|
||||||
runtime = ""
|
|
||||||
|
|
||||||
if not runtime.startswith("python-"):
|
if runtime != "python" or not version:
|
||||||
# not a Python runtime (e.g. R, which subclasses this)
|
# Either not specified, or not a Python runtime (e.g. R, which subclasses this)
|
||||||
# use the default Python
|
# use the default Python
|
||||||
self._python_version = self.major_pythons["3"]
|
self._python_version = self.major_pythons["3"]
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
|
@ -30,7 +26,7 @@ class PythonBuildPack(CondaBuildPack):
|
||||||
)
|
)
|
||||||
return self._python_version
|
return self._python_version
|
||||||
|
|
||||||
py_version_info = runtime.split("-", 1)[1].split(".")
|
py_version_info = version.split(".")
|
||||||
py_version = ""
|
py_version = ""
|
||||||
if len(py_version_info) == 1:
|
if len(py_version_info) == 1:
|
||||||
py_version = self.major_pythons[py_version_info[0]]
|
py_version = self.major_pythons[py_version_info[0]]
|
||||||
|
@ -138,16 +134,10 @@ class PythonBuildPack(CondaBuildPack):
|
||||||
def detect(self):
|
def detect(self):
|
||||||
"""Check if current repo should be built with the Python buildpack."""
|
"""Check if current repo should be built with the Python buildpack."""
|
||||||
requirements_txt = self.binder_path("requirements.txt")
|
requirements_txt = self.binder_path("requirements.txt")
|
||||||
runtime_txt = self.binder_path("runtime.txt")
|
|
||||||
setup_py = "setup.py"
|
setup_py = "setup.py"
|
||||||
|
|
||||||
if os.path.exists(runtime_txt):
|
if self.runtime_info[0]:
|
||||||
with open(runtime_txt) as f:
|
return self.runtime_info[0] == "python"
|
||||||
runtime = f.read().strip()
|
|
||||||
if runtime.startswith("python-"):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
if not self.binder_dir and os.path.exists(setup_py):
|
if not self.binder_dir and os.path.exists(setup_py):
|
||||||
return True
|
return True
|
||||||
return os.path.exists(requirements_txt)
|
return os.path.exists(requirements_txt)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import re
|
import warnings
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -49,7 +49,14 @@ class RBuildPack(PythonBuildPack):
|
||||||
def runtime(self):
|
def runtime(self):
|
||||||
"""
|
"""
|
||||||
Return contents of runtime.txt if it exists, '' otherwise
|
Return contents of runtime.txt if it exists, '' otherwise
|
||||||
|
|
||||||
|
Deprecated, use `runtime_info` instead.
|
||||||
"""
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"`{self.__class__.__name__}.runtime` is deprecated. Use `runtime_info` instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
if not hasattr(self, "_runtime"):
|
if not hasattr(self, "_runtime"):
|
||||||
runtime_path = self.binder_path("runtime.txt")
|
runtime_path = self.binder_path("runtime.txt")
|
||||||
try:
|
try:
|
||||||
|
@ -90,11 +97,11 @@ class RBuildPack(PythonBuildPack):
|
||||||
r_version = version_map["4.2"]
|
r_version = version_map["4.2"]
|
||||||
|
|
||||||
if not hasattr(self, "_r_version"):
|
if not hasattr(self, "_r_version"):
|
||||||
parts = self.runtime.split("-")
|
runtime, version, date = self.runtime_info
|
||||||
# If runtime.txt is not set, or if it isn't of the form r-<version>-<yyyy>-<mm>-<dd>,
|
# If runtime.txt is not set, or if it isn't of the form r-<version>-<yyyy>-<mm>-<dd>,
|
||||||
# we don't use any of it in determining r version and just use the default
|
# we don't use any of it in determining r version and just use the default
|
||||||
if len(parts) == 5:
|
if version and date:
|
||||||
r_version = parts[1]
|
r_version = version
|
||||||
# For versions of form x.y, we want to explicitly provide x.y.z - latest patchlevel
|
# For versions of form x.y, we want to explicitly provide x.y.z - latest patchlevel
|
||||||
# available. Users can however explicitly specify the full version to get something specific
|
# available. Users can however explicitly specify the full version to get something specific
|
||||||
if r_version in version_map:
|
if r_version in version_map:
|
||||||
|
@ -116,15 +123,11 @@ class RBuildPack(PythonBuildPack):
|
||||||
Returns '' if no date is specified
|
Returns '' if no date is specified
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "_checkpoint_date"):
|
if not hasattr(self, "_checkpoint_date"):
|
||||||
match = re.match(r"r-(\d.\d(.\d)?-)?(\d\d\d\d)-(\d\d)-(\d\d)", self.runtime)
|
runtime, version, date = self.runtime_info
|
||||||
if not match:
|
if runtime == "r" and date:
|
||||||
self._checkpoint_date = False
|
self._checkpoint_date = date
|
||||||
else:
|
else:
|
||||||
# turn the last three groups of the match into a date
|
self._checkpoint_date = False
|
||||||
self._checkpoint_date = datetime.date(
|
|
||||||
*[int(s) for s in match.groups()[-3:]]
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._checkpoint_date
|
return self._checkpoint_date
|
||||||
|
|
||||||
def detect(self):
|
def detect(self):
|
||||||
|
@ -142,13 +145,9 @@ class RBuildPack(PythonBuildPack):
|
||||||
|
|
||||||
description_R = "DESCRIPTION"
|
description_R = "DESCRIPTION"
|
||||||
if not self.binder_dir and os.path.exists(description_R):
|
if not self.binder_dir and os.path.exists(description_R):
|
||||||
if not self.checkpoint_date:
|
# no R snapshot date set through runtime.txt
|
||||||
# no R snapshot date set through runtime.txt
|
# Set it to two days ago from today
|
||||||
# Set it to two days ago from today
|
self._checkpoint_date = datetime.date.today() - datetime.timedelta(days=2)
|
||||||
self._checkpoint_date = datetime.date.today() - datetime.timedelta(
|
|
||||||
days=2
|
|
||||||
)
|
|
||||||
self._runtime = f"r-{str(self._checkpoint_date)}"
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
|
from datetime import date
|
||||||
from os.path import join as pjoin
|
from os.path import join as pjoin
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from repo2docker.buildpacks import LegacyBinderDockerBuildPack, PythonBuildPack
|
from repo2docker.buildpacks import (
|
||||||
|
BaseImage,
|
||||||
|
LegacyBinderDockerBuildPack,
|
||||||
|
PythonBuildPack,
|
||||||
|
)
|
||||||
from repo2docker.utils import chdir
|
from repo2docker.utils import chdir
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,3 +51,25 @@ def test_unsupported_python(tmpdir, python_version, base_image):
|
||||||
assert bp.python_version == python_version
|
assert bp.python_version == python_version
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
bp.render()
|
bp.render()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"runtime_txt, expected",
|
||||||
|
[
|
||||||
|
(None, (None, None, None)),
|
||||||
|
("", (None, None, None)),
|
||||||
|
("abc", ("abc", None, None)),
|
||||||
|
("abc-001", ("abc", "001", None)),
|
||||||
|
("abc-001-2025-06-22", ("abc", "001", date(2025, 6, 22))),
|
||||||
|
("a_b/c-0.0.1-2025-06-22", ("a_b/c", "0.0.1", date(2025, 6, 22))),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_runtime_txt(tmpdir, runtime_txt, expected, base_image):
|
||||||
|
tmpdir.chdir()
|
||||||
|
|
||||||
|
if runtime_txt is not None:
|
||||||
|
with open("runtime.txt", "w") as f:
|
||||||
|
f.write(runtime_txt)
|
||||||
|
|
||||||
|
base = BaseImage(base_image)
|
||||||
|
assert base.runtime_info == expected
|
||||||
|
|
Ładowanie…
Reference in New Issue