Move runtime.txt handling into base class

This will allow it to be used for Python (and other languages) in future
pull/1428/head
Simon Li 2025-06-22 14:22:51 +01:00
rodzic e795060aec
commit cab6fce923
5 zmienionych plików z 94 dodań i 42 usunięć

Wyświetl plik

@ -1,3 +1,4 @@
import datetime
import hashlib
import io
import logging
@ -750,3 +751,41 @@ class BaseImage(BuildPack):
# the only path evaluated at container start time rather than build time
return os.path.join("${REPO_DIR}", start)
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

Wyświetl plik

@ -187,12 +187,9 @@ class PipfileBuildPack(CondaBuildPack):
def detect(self):
"""Check if current repo should be built with the Pipfile buildpack."""
# first make sure python is not explicitly unwanted
runtime_txt = self.binder_path("runtime.txt")
if os.path.exists(runtime_txt):
with open(runtime_txt) as f:
runtime = f.read().strip()
if not runtime.startswith("python-"):
return False
runtime = self.runtime_info[0]
if runtime and runtime != "python":
return False
pipfile = self.binder_path("Pipfile")
pipfile_lock = self.binder_path("Pipfile.lock")

Wyświetl plik

@ -15,14 +15,10 @@ class PythonBuildPack(CondaBuildPack):
if hasattr(self, "_python_version"):
return self._python_version
try:
with open(self.binder_path("runtime.txt")) as f:
runtime = f.read().strip()
except FileNotFoundError:
runtime = ""
runtime, version, _ = self.runtime_info
if not runtime.startswith("python-"):
# not a Python runtime (e.g. R, which subclasses this)
if runtime != "python" or not version:
# Either not specified, or not a Python runtime (e.g. R, which subclasses this)
# use the default Python
self._python_version = self.major_pythons["3"]
self.log.warning(
@ -30,7 +26,7 @@ class PythonBuildPack(CondaBuildPack):
)
return self._python_version
py_version_info = runtime.split("-", 1)[1].split(".")
py_version_info = version.split(".")
py_version = ""
if len(py_version_info) == 1:
py_version = self.major_pythons[py_version_info[0]]
@ -138,16 +134,10 @@ class PythonBuildPack(CondaBuildPack):
def detect(self):
"""Check if current repo should be built with the Python buildpack."""
requirements_txt = self.binder_path("requirements.txt")
runtime_txt = self.binder_path("runtime.txt")
setup_py = "setup.py"
if os.path.exists(runtime_txt):
with open(runtime_txt) as f:
runtime = f.read().strip()
if runtime.startswith("python-"):
return True
else:
return False
if self.runtime_info[0]:
return self.runtime_info[0] == "python"
if not self.binder_dir and os.path.exists(setup_py):
return True
return os.path.exists(requirements_txt)

Wyświetl plik

@ -1,6 +1,6 @@
import datetime
import os
import re
import warnings
from functools import lru_cache
import requests
@ -49,7 +49,14 @@ class RBuildPack(PythonBuildPack):
def runtime(self):
"""
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"):
runtime_path = self.binder_path("runtime.txt")
try:
@ -90,11 +97,11 @@ class RBuildPack(PythonBuildPack):
r_version = version_map["4.2"]
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>,
# we don't use any of it in determining r version and just use the default
if len(parts) == 5:
r_version = parts[1]
if version and date:
r_version = version
# 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
if r_version in version_map:
@ -116,15 +123,11 @@ class RBuildPack(PythonBuildPack):
Returns '' if no date is specified
"""
if not hasattr(self, "_checkpoint_date"):
match = re.match(r"r-(\d.\d(.\d)?-)?(\d\d\d\d)-(\d\d)-(\d\d)", self.runtime)
if not match:
self._checkpoint_date = False
runtime, version, date = self.runtime_info
if runtime == "r" and date:
self._checkpoint_date = date
else:
# turn the last three groups of the match into a date
self._checkpoint_date = datetime.date(
*[int(s) for s in match.groups()[-3:]]
)
self._checkpoint_date = False
return self._checkpoint_date
def detect(self):
@ -142,13 +145,9 @@ class RBuildPack(PythonBuildPack):
description_R = "DESCRIPTION"
if not self.binder_dir and os.path.exists(description_R):
if not self.checkpoint_date:
# no R snapshot date set through runtime.txt
# Set it to two days ago from today
self._checkpoint_date = datetime.date.today() - datetime.timedelta(
days=2
)
self._runtime = f"r-{str(self._checkpoint_date)}"
# no R snapshot date set through runtime.txt
# Set it to two days ago from today
self._checkpoint_date = datetime.date.today() - datetime.timedelta(days=2)
return True
@lru_cache

Wyświetl plik

@ -1,9 +1,14 @@
from datetime import date
from os.path import join as pjoin
from tempfile import TemporaryDirectory
import pytest
from repo2docker.buildpacks import LegacyBinderDockerBuildPack, PythonBuildPack
from repo2docker.buildpacks import (
BaseImage,
LegacyBinderDockerBuildPack,
PythonBuildPack,
)
from repo2docker.utils import chdir
@ -46,3 +51,25 @@ def test_unsupported_python(tmpdir, python_version, base_image):
assert bp.python_version == python_version
with pytest.raises(ValueError):
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