Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Python 3.12 #458

Merged
merged 33 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a74a4db
Add support for Python 3.12
elikoga May 28, 2024
e423dc5
Update black version to 24.4.2 in .pre-commit-config.yaml
elikoga May 28, 2024
562561a
Update appenv.py to include https://github.com/flyingcircusio/appenv/…
elikoga Jun 3, 2024
c1e95a7
light reformatting due to black update
elikoga Jun 3, 2024
1261115
Update packaging version to 24.0
elikoga Jun 3, 2024
43c3790
Update tox version to 4.7.0
elikoga Jun 3, 2024
b033340
Update virtualenv version to 20.26.2
elikoga Jun 3, 2024
24cfd07
Update filelock version to 3.12.2
elikoga Jun 3, 2024
7e7f38a
Update pluggy version to 1.2.0
elikoga Jun 3, 2024
e8750c0
Update distlib version to 0.3.8
elikoga Jun 3, 2024
7232443
Add pyproject-api to requirements-dev.txt
elikoga Jun 3, 2024
f9385e5
Update pyproject-api version
elikoga Jun 3, 2024
080580f
Change batou bootstrap script to download newest batou version from g…
elikoga Jun 3, 2024
8b6495f
Update appenv.py URL in bootstrap script
elikoga Jun 3, 2024
71086ce
Actually test bootstrap script of current branch
elikoga Jun 3, 2024
e8d3e14
Update requirements.lock files
elikoga Jun 3, 2024
4c3e4ed
Update pip versions in test files
elikoga Jun 3, 2024
461247b
Update requirements.txt in tests to include python 3.12
elikoga Jun 3, 2024
9c5dee7
Update pip version in test_buildout.py
elikoga Jun 3, 2024
7074e7a
Update pip version in test_buildout.py
elikoga Jun 4, 2024
bb5ad01
Update setuptools version in test_buildout.py
elikoga Jun 4, 2024
4b4bde5
Update setuptools version to 69.5.1
elikoga Jun 4, 2024
330b72d
Update pip version in test_buildout.py
elikoga Jun 4, 2024
4870a63
Update setuptools version in test_buildout.py
elikoga Jun 17, 2024
31879a9
Update buildout version to 3.0.1
elikoga Jun 17, 2024
ab79af1
Update setuptools version to 68.0.0
elikoga Jun 17, 2024
ab4fa86
Update zc.buildout version to 3.0.1
elikoga Jun 17, 2024
6adb80c
Update supervisor version to 4.2.5
elikoga Jun 17, 2024
f715a70
Update appenv
elikoga Jun 17, 2024
0b78bd3
update appenv
elikoga Jun 20, 2024
e290835
Update appenv, setuptools dependency
elikoga Jul 23, 2024
a82f210
Merge remote-tracking branch 'origin/main' into 402-update-python-ver…
elikoga Jul 23, 2024
42352fd
Update appenv
elikoga Jul 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ base=$(pwd)
mkdir myproject
cd myproject
git init
# Extract current bootstrap command from the docs.
grep "bootstrap | sh" $base/doc/source/user/install.txt | sed -e 's/\$//' | sh
cat $base/bootstrap | sh
2 changes: 1 addition & 1 deletion .github/workflows/bootstrap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
build:
strategy:
matrix:
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ]

# The type of runner that the job will run on
runs-on: ubuntu-22.04
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
build:
strategy:
matrix:
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ]

# The type of runner that the job will run on
runs-on: ubuntu-22.04
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ repos:
args: ["--profile", "black", "--filter-files"]

- repo: https://github.com/psf/black
rev: 22.10.0
rev: 24.4.2
hooks:
- id: black
exclude:
Expand Down
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
-----------------------

- batou migrate now writes .batou.json with a newline at the end as `pre-commit` hooks expect (usually).
- Add support for python 3.12
- Unused Components, that is, Components that are initialized, but not used in the deployment, are now reported as warnings.


Expand Down
166 changes: 102 additions & 64 deletions appenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,41 @@
import venv


class TColors:
"""Terminal colors for pretty output."""

RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RESET = "\033[0m"


def cmd(c, merge_stderr=True, quiet=False):
# TODO revisit the cmd() architecture w/ python 3
# XXX better IO management for interactive output and seeing original
# errors and output at appropriate places ...
try:
kwargs = {"shell": True}
kwargs = {}
if isinstance(c, str):
kwargs["shell"] = True
c = [c]
if merge_stderr:
kwargs["stderr"] = subprocess.STDOUT
return subprocess.check_output([c], **kwargs)
return subprocess.check_output(c, **kwargs)
except subprocess.CalledProcessError as e:
print("{} returned with exit code {}".format(c, e.returncode))
print(e.output.decode("utf-8", "replace"))
raise ValueError(e.output.decode("utf-8", "replace"))


def python(path, c, **kwargs):
return cmd([os.path.join(path, "bin/python")] + c, **kwargs)


def pip(path, c, **kwargs):
return python(path, ["-m", "pip"] + c, **kwargs)


def get(host, path, f):
conn = http.client.HTTPSConnection(host)
conn.request("GET", path)
Expand All @@ -63,29 +83,28 @@ def ensure_venv(target):

if os.path.exists(target):
print("Deleting unclean target)")
cmd("rm -rf {target}".format(target=target))
cmd(["rm", "-rf", target])

version = sys.version.split()[0]
python_maj_min = ".".join(str(x) for x in sys.version_info[:2])
print("Creating venv ...")
venv.create(target, with_pip=False)
venv.create(target, with_pip=False, symlinks=True)

try:
# This is trying to detect whether we're on a proper Python stdlib
# or on a broken Debian. See various StackOverflow questions about
# this.
import distutils.util # noqa: F401 imported but unused
import ensurepip # noqa: F401 imported but unused
except ImportError:
# Okay, lets repair this, if we can. May need privilege escalation
# at some point.
# We could do: apt-get -y -q install python3-distutils python3-venv
# We could do: apt-get -y -q install python3-venv
# on some systems but it requires root and is specific to Debian.
# I decided to go a more sledge hammer route.

# XXX we can speed this up by storing this in ~/.appenv/overlay instead
# of doing the download for every venv we manage
print("Activating broken distutils/ensurepip stdlib workaround ...")
print("Activating broken ensurepip stdlib workaround ...")

tmp_base = tempfile.mkdtemp()
try:
Expand All @@ -97,12 +116,12 @@ def ensure_venv(target):
f,
)

cmd("tar xf {} -C {}".format(download, tmp_base))
cmd(["tar", "xf", download, "-C", tmp_base])

assert os.path.exists(
os.path.join(tmp_base, "Python-{}".format(version))
)
for module in ["ensurepip", "distutils"]:
for module in ["ensurepip"]:
print(module)
shutil.copytree(
os.path.join(
Expand All @@ -118,7 +137,7 @@ def ensure_venv(target):
)

# (always) prepend the site packages so we can actually have a
# fixed distutils installation.
# fixed installation.
site_packages = os.path.abspath(
os.path.join(
target, "lib", "python" + python_maj_min, "site-packages"
Expand All @@ -135,14 +154,11 @@ def ensure_venv(target):
shutil.rmtree(tmp_base)

print("Ensuring pip ...")
cmd("{target}/bin/python -m ensurepip --default-pip".format(target=target))
cmd(
"{target}/bin/python -m pip install --upgrade pip".format(target=target)
)
python(target, ["-m", "ensurepip", "--default-pip"])
pip(target, ["install", "--upgrade", "pip"])


def ensure_minimal_python():
current_python = os.path.realpath(sys.executable)
def parse_preferences():
preferences = None
if os.path.exists("requirements.txt"):
with open("requirements.txt") as f:
Expand All @@ -155,9 +171,15 @@ def ensure_minimal_python():
preferences = [x.strip() for x in preferences.split(",")]
preferences = list(filter(None, preferences))
break
return preferences


def ensure_minimal_python():
current_python = os.path.realpath(sys.executable)
preferences = parse_preferences()
if not preferences:
# We have no preferences defined, use the current python.
print("Update lockfile with with {}.".format(current_python))
print("Updating lockfile with with {}.".format(current_python))
print("If you want to use a different version, set it via")
print(" `# appenv-python-preference:` in requirements.txt.")
return
Expand Down Expand Up @@ -202,20 +224,18 @@ def ensure_best_python(base):
return
import shutil

# use newest Python available if nothing else is requested
preferences = ["3.{}".format(x) for x in reversed(range(4, 20))]
preferences = parse_preferences()

if os.path.exists("requirements.txt"):
with open("requirements.txt") as f:
for line in f:
# Expected format:
# # appenv-python-preference: 3.1,3.9,3.4
if not line.startswith("# appenv-python-preference: "):
continue
preferences = line.split(":")[1]
preferences = [x.strip() for x in preferences.split(",")]
preferences = list(filter(None, preferences))
break
if preferences is None:
if sys.version_info >= (3, 12):
print("You are using a Python version >= 3.12.")
print(
"Please specify a Python version in the requirements.txt file."
)
print("Lockfiles created with a Python version lower than 3.12")
print("may create a broken venv with a Python version >= 3.12.")
# use newest Python available if nothing else is requested
preferences = ["3.{}".format(x) for x in reversed(range(4, 20))]

current_python = os.path.realpath(sys.executable)
for version in preferences:
Expand Down Expand Up @@ -245,6 +265,22 @@ def ensure_best_python(base):
sys.exit(65)


class ParsedRequirement:
"""A parsed requirement from a requirement string.

Has a similiar interface to the real Requirement class from
packaging.requirements, but is reduced to the parts we need.
"""

def __init__(self, name, url, requirement_string):
self.name = name
self.url = url
self.requirement_string = requirement_string

def __str__(self):
return self.requirement_string


def parse_requirement_string(requirement_string):
"""Parse a requirement from a requirement string.

Expand Down Expand Up @@ -290,12 +326,7 @@ def parse_requirement_string(requirement_string):
)
url = url_match.group("url") if url_match else None

# now we want an object with .name, .url and str(obj) == requirement_string
return type(
"ParsedRequirement",
(),
{"name": name, "url": url, "__str__": lambda self: requirement_string},
)()
return ParsedRequirement(name, url, requirement_string)


class AppEnv(object):
Expand All @@ -308,7 +339,7 @@ class AppEnv(object):
env_dir = None # The current specific venv that we're working with.
appenv_dir = None # The directory where to place specific venvs.

def __init__(self, base):
def __init__(self, base, original_cwd):
self.base = base

# This used to be computed based on the application name but
Expand All @@ -319,7 +350,7 @@ def __init__(self, base):
# Allow simplifying a lot of code by assuming that all the
# meta-operations happen in the base directory. Store the original
# working directory here so we switch back at the appropriate time.
self.original_cwd = os.path.abspath(os.curdir)
self.original_cwd = original_cwd

def meta(self):
# Parse the appenv arguments
Expand Down Expand Up @@ -440,7 +471,7 @@ def prepare(self, args=None, remaining=None):
raise Exception()
except Exception:
print("Existing envdir not consistent, deleting")
cmd("rm -rf {env_dir}".format(env_dir=env_dir))
cmd(["rm", "-rf", env_dir])

if not os.path.exists(env_dir):
ensure_venv(env_dir)
Expand All @@ -449,12 +480,16 @@ def prepare(self, args=None, remaining=None):
f.write(requirements)

print("Installing ...")
cmd(
"{env_dir}/bin/python -m pip install --no-deps -r"
" {env_dir}/requirements.lock".format(env_dir=env_dir)
pip(
env_dir,
[
"install",
"--no-deps",
"-r",
"{env_dir}/requirements.lock".format(env_dir=env_dir),
],
)

cmd("{env_dir}/bin/python -m pip check".format(env_dir=env_dir))
pip(env_dir, ["check"])

with open(os.path.join(env_dir, "appenv.ready"), "w") as f:
f.write("Ready or not, here I come, you can't hide\n")
Expand Down Expand Up @@ -525,28 +560,33 @@ def reset(self, args=None, remaining=None):
appenvdir=self.appenv_dir
)
)
cmd("rm -rf {appenvdir}".format(appenvdir=self.appenv_dir))
cmd(["rm", "-rf", self.appenv_dir])

def update_lockfile(self, args=None, remaining=None):
ensure_minimal_python()
preferences = parse_preferences()
python312_mixed_setuptools_workaround = False
if preferences is not None:
if any(f"3.{x}" in preferences for x in range(4, 12)):
if any(f"3.{x}" in preferences for x in range(12, 20)):
python312_mixed_setuptools_workaround = True
os.chdir(self.base)
print("Updating lockfile")
tmpdir = os.path.join(self.appenv_dir, "updatelock")
if os.path.exists(tmpdir):
cmd("rm -rf {tmpdir}".format(tmpdir=tmpdir))
cmd(["rm", "-rf", tmpdir])
ensure_venv(tmpdir)
print("Installing packages ...")
cmd(
"{tmpdir}/bin/python -m pip install -r requirements.txt".format(
tmpdir=tmpdir
)
)
pip(tmpdir, ["install", "-r", "requirements.txt"])

extra_specs = []
result = cmd(
"{tmpdir}/bin/python -m pip freeze".format(tmpdir=tmpdir),
merge_stderr=False,
).decode("ascii")
pip_freeze_args = ["freeze"]
if python312_mixed_setuptools_workaround:
pip_freeze_args.extend(["--all", "--exclude", "pip"])
result = pip(tmpdir, pip_freeze_args, merge_stderr=False).decode(
"ascii"
)
# They changed this behaviour in https://github.com/pypa/pip/pull/12032
pinned_versions = {}
for line in result.splitlines():
if line.strip().startswith("-e "):
Expand Down Expand Up @@ -592,11 +632,12 @@ def update_lockfile(self, args=None, remaining=None):
)
f.write("\n".join(lines))
f.write("\n")
cmd("rm -rf {tmpdir}".format(tmpdir=tmpdir))
cmd(["rm", "-rf", tmpdir])


def main():
base = os.path.dirname(__file__)
original_cwd = os.getcwd()

ensure_best_python(base)
# clear PYTHONPATH variable to get a defined environment
Expand All @@ -608,14 +649,11 @@ def main():
# Determine whether we're being called as appenv or as an application name
application_name = os.path.splitext(os.path.basename(__file__))[0]

appenv = AppEnv(base)
try:
if application_name == "appenv":
appenv.meta()
else:
appenv.run(application_name, sys.argv[1:])
finally:
os.chdir(appenv.original_cwd)
appenv = AppEnv(base, original_cwd)
if application_name == "appenv":
appenv.meta()
else:
appenv.run(application_name, sys.argv[1:])


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ oops() {
exit 1
}

curl -sL https://raw.githubusercontent.com/flyingcircusio/batou/main/appenv.py -o appenv || oops "failed to download appenv"
curl -sL https://raw.githubusercontent.com/flyingcircusio/appenv/master/src/appenv.py -o appenv || oops "failed to download appenv"
chmod +x appenv
ln -sf appenv batou
echo "batou>=2.3b4" >> requirements.txt
echo "batou>=2.4.1" >> requirements.txt
sed -e 's!.*batou_ext.*!batou_ext @ https://github.com/flyingcircusio/batou_ext/archive/1e95cbd216d9b4891a0c9301b195de86e430fb0d.zip#sha256=4242d65a4cb0721812a308d2cfa87d647ba78f3e03ae4d189f9d2ac78157df93!' requirements.txt > requirements.txt.new
mv requirements.txt.new requirements.txt
./appenv update-lockfile
Expand Down
2 changes: 1 addition & 1 deletion doc/source/user/install.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A new project is started by placing the batou master command into the project an
$ mkdir myproject
$ cd myproject
$ git init
$ curl -sL https://raw.githubusercontent.com/flyingcircusio/batou/2.3b3/bootstrap | sh
$ curl -sL https://raw.githubusercontent.com/flyingcircusio/batou/main/bootstrap | sh
$ git commit -m "Start a batou project."

Local
Expand Down
Loading
Loading