Skip to content

Commit a38577f

Browse files
rtibblesbotclaude
andcommitted
Cut the version helper down to what the callers need
No caller passes an sdist tar - android extracts it and passes the tree - so drop the tarfile reader, and drop argparse for a positional read. That leaves one function over a tree or a wheel, halving the helper and its test. Trim the surrounding comments and the Android error path to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6aa0494 commit a38577f

7 files changed

Lines changed: 29 additions & 171 deletions

File tree

Makefile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,8 @@ set-no-uv-python-version-for-tests:
201201
echo "__version__ = version = \"$$VERSION\"" >> kolibri/_version.py; \
202202
echo "Set version to $$VERSION"
203203

204-
# kolibri/VERSION is a plaintext copy for external consumers only; in-repo, use
205-
# build_tools/read_kolibri_version.py. Don't drop it: kolibri-installer-debian
206-
# (Makefile:18-21 at v0.16.1) reads it from our sdist, reddening the `deb` job.
204+
# Plaintext copy read out of our sdist by kolibri-installer-debian (pinned
205+
# v0.16.1) — deleting it reddens `deb`. In-repo, use read_kolibri_version.py.
207206
writeversion:
208207
uv run python -c "import kolibri; print(kolibri.__version__)" > kolibri/VERSION
209208
@echo ""

build_tools/read_kolibri_version.py

Lines changed: 16 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,90 +3,38 @@
33
# dependencies = []
44
# ///
55
"""
6-
Print the Kolibri version out of a directory tree, a wheel, or an sdist.
6+
Print the Kolibri version from ``kolibri/_version.py``, the setuptools-scm output
7+
that is Kolibri's source of truth, given either a directory holding the
8+
``kolibri`` package or a built ``.whl`` -- whichever the caller has to hand.
79
8-
``kolibri/_version.py`` -- written by setuptools-scm -- is the source of truth.
9-
Each platform under platforms/ holds a different entity at its version-read
10-
point, hence the three. Per-platform version *conversions* (PEP 440 to Debian,
11-
Android versionCode) stay with the platform that needs them.
12-
13-
Stdlib-only and Python 3.6-compatible: callers may run it under a bare
14-
``python3`` with no workspace venv, and its tests run on 3.6 and 3.7 in
15-
tox.yml's ``unit_test_no_uv`` job.
10+
Stdlib-only and 3.6-compatible: callers run it under a bare ``python3`` with no
11+
workspace venv.
1612
"""
1713

18-
import argparse
1914
import ast
2015
import os
21-
import tarfile
16+
import sys
2217
import zipfile
2318

2419
VERSION_MODULE = "kolibri/_version.py"
2520

2621

27-
def _parse_version(source):
28-
"""Return the ``__version__`` assigned in the text of a ``_version.py``."""
22+
def read_version(path):
23+
if path.endswith(".whl"):
24+
with zipfile.ZipFile(path) as wheel:
25+
source = wheel.read(VERSION_MODULE).decode("utf-8")
26+
else:
27+
with open(os.path.join(path, VERSION_MODULE)) as version_module:
28+
source = version_module.read()
29+
# ast, not exec: handles either quoting style without running build output.
2930
for node in ast.parse(source).body:
3031
if isinstance(node, ast.Assign) and any(
3132
isinstance(target, ast.Name) and target.id == "__version__"
3233
for target in node.targets
3334
):
3435
return ast.literal_eval(node.value)
35-
raise ValueError("No __version__ assignment found in {}".format(VERSION_MODULE))
36-
37-
38-
def _read_tree(path):
39-
version_module = os.path.join(path, VERSION_MODULE)
40-
if not os.path.isfile(version_module):
41-
raise ValueError("No {} in tree: {}".format(VERSION_MODULE, path))
42-
with open(version_module) as f:
43-
return f.read()
44-
45-
46-
def _read_wheel(path):
47-
# A wheel holds the package at the archive root, alongside its .dist-info.
48-
with zipfile.ZipFile(path) as archive:
49-
try:
50-
return archive.read(VERSION_MODULE).decode("utf-8")
51-
except KeyError:
52-
raise ValueError("No {} in wheel: {}".format(VERSION_MODULE, path))
53-
54-
55-
def _read_sdist(path):
56-
# An sdist nests everything under a single root component.
57-
with tarfile.open(path) as archive:
58-
for member in archive:
59-
if member.name.partition("/")[2] == VERSION_MODULE:
60-
with archive.extractfile(member) as version_module:
61-
return version_module.read().decode("utf-8")
62-
raise ValueError("No {} in sdist: {}".format(VERSION_MODULE, path))
63-
64-
65-
def read_version(path):
66-
"""Return the Kolibri version held by a tree, a wheel, or an sdist."""
67-
if os.path.isdir(path):
68-
return _parse_version(_read_tree(path))
69-
# A missing path is the likeliest failure, and "not a .whl" would mislead.
70-
if not os.path.isfile(path):
71-
raise ValueError("No such tree, wheel or sdist: {}".format(path))
72-
if path.endswith(".whl"):
73-
return _parse_version(_read_wheel(path))
74-
if path.endswith(".tar.gz"):
75-
return _parse_version(_read_sdist(path))
76-
raise ValueError("Not a .whl or a .tar.gz sdist: {}".format(path))
77-
78-
79-
def main():
80-
parser = argparse.ArgumentParser(
81-
description=__doc__,
82-
formatter_class=argparse.RawDescriptionHelpFormatter,
83-
)
84-
parser.add_argument(
85-
"path",
86-
help="a directory tree containing kolibri/, a .whl, or a .tar.gz sdist",
87-
)
88-
print(read_version(parser.parse_args().path)) # noqa: T201
36+
raise ValueError("No __version__ assignment in {}: {}".format(path, VERSION_MODULE))
8937

9038

9139
if __name__ == "__main__":
92-
main()
40+
print(read_version(sys.argv[1])) # noqa: T201

platforms/android/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ root above it, so history and `git blame` still work.
2020
The build consumes the shared workspace rather than a standalone checkout:
2121

2222
- **Python / `buildPython`:** resolves to the shared root `.venv`. Create it with
23-
`uv sync --group dev --all-packages` — uv finds the workspace root from any
24-
member directory, so this works from `platforms/android/` too. The venv lands
25-
at the monorepo root; nothing needs activating, since both the Makefile and
26-
`app/build.gradle` resolve `../../../.venv` directly.
23+
`uv sync --group dev --all-packages` from anywhere in the workspace. Nothing
24+
needs activating — the Makefile and `app/build.gradle` resolve it directly.
2725
- **Chaquopy runtime:** the embedded runtime (`requirements.txt`) stays
2826
Chaquopy-resolved and out of the workspace lock — install it into the shared
2927
`.venv` with `uv pip install -r requirements.txt`.

platforms/android/app/build.gradle

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,9 @@ def calculatedVersionName
104104
def calculatedVersionCode
105105

106106
if (extractedKolibriDir.exists() && extractedKolibriDir.isDirectory()) {
107-
// setuptools-scm's _version.py is the version source of truth; read it via
108-
// the shared monorepo helper.
109-
def kolibriVersionFile = new File(extractedKolibriDir, 'kolibri/_version.py')
110-
if (!kolibriVersionFile.exists()) {
111-
throw new GradleException(
112-
"Kolibri _version.py not found at: ${kolibriVersionFile}\n" +
113-
"Kolibri tars released before the monorepo do not contain it - " +
114-
"stage a workspace-built tar with 'make stage-workspace-tar'.")
115-
}
116-
107+
// Read the version with the shared monorepo helper, as versionCode below
108+
// shells out to scripts/version.py. Pre-monorepo tars have no _version.py,
109+
// so the helper fails here: stage one with 'make stage-workspace-tar'.
117110
def versionOutput = new ByteArrayOutputStream()
118111
exec {
119112
commandLine buildPythonExecutable,

platforms/desktop-app/Makefile

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,10 @@ guard-%:
1818
fi
1919

2020
# kolibrisrc/ is what PyInstaller ships, so its kolibri/_version.py is the app
21-
# version. $(shell) discards the helper's exit status, so guard on empty output —
22-
# otherwise a failed read leaves the version blank and the build carries on.
21+
# version. $(shell) hides a failed read as an empty one, hence the guard.
2322
needs-version:
2423
$(eval KOLIBRI_VERSION ?= $(shell $(PYTHON_EXEC) ../../build_tools/read_kolibri_version.py kolibrisrc))
25-
@if [ -z "$(KOLIBRI_VERSION)" ]; then \
26-
echo "Could not read the Kolibri version from kolibrisrc/ (see the error above)."; \
27-
echo "Stage a workspace-built whl with 'make stage-workspace-whl'."; \
28-
exit 1; \
29-
fi
24+
@test -n "$(KOLIBRI_VERSION)" || { echo "No Kolibri version in kolibrisrc/: stage a whl with 'make stage-workspace-whl'"; exit 1; }
3025

3126
# Write the app version (= the bundled Kolibri version) to _version.py so
3227
# PyInstaller bundles it and kolibri_app.__init__ can

platforms/desktop-app/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ Builds Kolibri from the monorepo workspace and stages it into `whl/`, so the app
6868
```
6969

7070
- **Or Fetch and Prepare a Published Kolibri Wheel:**
71-
You'll need the URL of the Kolibri `.whl` you intend to package — see the [Kolibri GitHub Releases page](https://github.com/learningequality/kolibri/releases). The build reads the app version from `kolibri/_version.py`, which no wheel published so far contains, so this path needs a wheel built after Kolibri adopted setuptools-scm; otherwise use `stage-workspace-whl`.
71+
Takes the URL of a `.whl` from the [Kolibri GitHub Releases page](https://github.com/learningequality/kolibri/releases). The version read needs `kolibri/_version.py`, which no released wheel ships yet.
7272
```
7373
make get-whl whl="<URL_TO_KOLIBRI_WHL_FILE>"
7474
```
75-
**Example** (a v0.18.0 wheel predates `_version.py`, so it fails the version read):
75+
**Example:**
7676
```
7777
make get-whl whl="https://github.com/learningequality/kolibri/releases/download/v0.18.0/kolibri-0.18.0-py2.py3-none-any.whl"
7878
```

test/test_read_kolibri_version.py

Lines changed: 2 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,11 @@
11
"""Unit tests for build_tools/read_kolibri_version.py."""
22

3-
Each caller holds a different entity at its version-read point: an extracted
4-
sdist tree (android), an unpacked wheel tree (desktop-app), the repo checkout
5-
(debian-server CI), a built wheel (the root `pex` target). These pin the
6-
archive-layout differences the helper absorbs.
7-
8-
Stdlib-only and 3.6-compatible: tox.yml's `unit_test_no_uv` job runs this
9-
directory under a bare `python` in python:3.6-buster and python:3.7-buster.
10-
"""
11-
12-
import importlib.util
13-
import io
143
import os
15-
import tarfile
164
import zipfile
175

186
import pytest
197

20-
_MODULE_PATH = os.path.join(
21-
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
22-
"build_tools",
23-
"read_kolibri_version.py",
24-
)
25-
_spec = importlib.util.spec_from_file_location("read_kolibri_version", _MODULE_PATH)
26-
read_kolibri_version = importlib.util.module_from_spec(_spec)
27-
_spec.loader.exec_module(read_kolibri_version)
8+
from build_tools.read_kolibri_version import read_version
289

2910
VERSION = "0.19.0.dev0+g1234abc"
3011

@@ -56,70 +37,14 @@ def test_reads_the_double_quoted_form(tmp_path):
5637

5738

5839
def test_reads_version_from_a_wheel(tmp_path):
40+
# A wheel holds the package at the archive root, unlike an extracted tree.
5941
whl = os.path.join(str(tmp_path), "kolibri-0.19.0-py2.py3-none-any.whl")
6042
with zipfile.ZipFile(whl, "w") as archive:
6143
archive.writestr("kolibri/_version.py", SCM_VERSION_PY)
6244
archive.writestr("kolibri-0.19.0.dist-info/METADATA", "Name: kolibri\n")
6345
assert read_version(whl) == VERSION
6446

6547

66-
def test_reads_version_from_an_sdist(tmp_path):
67-
# The root component is the one android strips with --strip-components=1.
68-
sdist = os.path.join(str(tmp_path), "kolibri-0.19.0.tar.gz")
69-
payload = SCM_VERSION_PY.encode("utf-8")
70-
member = tarfile.TarInfo("kolibri-0.19.0/kolibri/_version.py")
71-
member.size = len(payload)
72-
with tarfile.open(sdist, "w:gz") as archive:
73-
archive.addfile(member, io.BytesIO(payload))
74-
assert read_kolibri_version.read_version(sdist) == VERSION
75-
76-
77-
def test_wheel_without_a_version_file_raises(tmp_path):
78-
whl = os.path.join(str(tmp_path), "kolibri-0.19.0-py2.py3-none-any.whl")
79-
with zipfile.ZipFile(whl, "w") as archive:
80-
archive.writestr("kolibri-0.19.0.dist-info/METADATA", "Name: kolibri\n")
81-
with pytest.raises(ValueError) as excinfo:
82-
read_kolibri_version.read_version(whl)
83-
assert whl in str(excinfo.value)
84-
85-
86-
def test_sdist_without_a_version_file_raises(tmp_path):
87-
sdist = os.path.join(str(tmp_path), "kolibri-0.19.0.tar.gz")
88-
member = tarfile.TarInfo("kolibri-0.19.0/kolibri/__init__.py")
89-
member.size = 0
90-
with tarfile.open(sdist, "w:gz") as archive:
91-
archive.addfile(member, io.BytesIO(b""))
92-
with pytest.raises(ValueError) as excinfo:
93-
read_kolibri_version.read_version(sdist)
94-
assert sdist in str(excinfo.value)
95-
96-
97-
def test_tree_without_a_version_file_raises(tmp_path):
98-
with pytest.raises(ValueError) as excinfo:
99-
read_kolibri_version.read_version(str(tmp_path))
100-
assert str(tmp_path) in str(excinfo.value)
101-
102-
103-
def test_unrecognized_suffix_raises(tmp_path):
104-
# raspberry-pi reads a .deb filename instead, deliberately. Anyone pointing
105-
# this helper at one should get an error, not a silent misparse.
106-
deb = os.path.join(str(tmp_path), "kolibri_0.19.0-0ubuntu1_all.deb")
107-
with open(deb, "wb") as f:
108-
f.write(b"")
109-
with pytest.raises(ValueError) as excinfo:
110-
read_kolibri_version.read_version(deb)
111-
assert deb in str(excinfo.value)
112-
113-
114-
def test_missing_entity_raises(tmp_path):
115-
# The likeliest failure in every caller: nothing has been staged yet, so
116-
# kolibrisrc/ or tar/extracted/ simply is not there.
117-
missing = os.path.join(str(tmp_path), "kolibrisrc")
118-
with pytest.raises(ValueError) as excinfo:
119-
read_kolibri_version.read_version(missing)
120-
assert missing in str(excinfo.value)
121-
122-
12348
def test_version_alias_alone_raises(tmp_path):
12449
with pytest.raises(ValueError):
12550
read_version(_write_tree(tmp_path, "version = '%s'\n" % VERSION))

0 commit comments

Comments
 (0)