From 37ae48b1f3f36c5143adeaf181fed6c9d827140d Mon Sep 17 00:00:00 2001
From: Jean-Christophe Fillion-Robin <jchris.fillionr@kitware.com>
Date: Sun, 31 Mar 2024 15:44:50 -0400
Subject: [PATCH] Revisit versioning and streamline maintenance adding "bump"
 and "tag_release" nox sessions (#13)

* feat: simplify versioning by hard-coding value in pyproject.toml

This major version associated with the package is now consistent with the
IDC table version hard-coded in `scripts/sql/idc_index.sql`.

Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>

* chore: Add "bump" nox session to streamline IDC index version update

Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>

* chore: Add nox session and instructions for tagging a release

Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
---
 .github/CONTRIBUTING.md                    | 21 +++++
 noxfile.py                                 | 90 ++++++++++++++++++++-
 pyproject.toml                             | 12 +--
 scripts/python/update_idc_index_version.py | 91 ++++++++++++++++++++++
 src/idc_index_data/_version.pyi            |  1 -
 tests/test_package.py                      |  8 ++
 6 files changed, 215 insertions(+), 8 deletions(-)
 create mode 100755 scripts/python/update_idc_index_version.py

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 11cc123..2292c15 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -99,3 +99,24 @@ pre-commit run -a
 ```
 
 to check all files.
+
+# Updating the IDC index version
+
+You can update the version using:
+
+```bash
+export GCP_PROJECT=idc-external-025
+export GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json
+nox -s bump -- <version>
+```
+
+And follow the instructions it gives you. Leave off the version to bump to the
+latest version. Add `-–commit` to run the commit procedure.
+
+# Tagging a release
+
+You can print the instructions for tagging a release using:
+
+```bash
+nox -s tag_release
+```
diff --git a/noxfile.py b/noxfile.py
index 31ba46e..a9a9f86 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import argparse
+import re
 import shutil
 from pathlib import Path
 
@@ -8,7 +9,7 @@
 
 DIR = Path(__file__).parent.resolve()
 
-nox.options.sessions = ["lint", "pylint", "tests"]
+nox.options.sessions = ["lint", "pylint", "tests"]  # Session run by default
 
 
 @nox.session
@@ -115,3 +116,90 @@ def build(session: nox.Session) -> None:
 
     session.install("build")
     session.run("python", "-m", "build")
+
+
+def _bump(session: nox.Session, name: str, script: str, files) -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--commit", action="store_true", help="Make a branch and commit."
+    )
+    parser.add_argument(
+        "version", nargs="?", help="The version to process - leave off for latest."
+    )
+    args = parser.parse_args(session.posargs)
+
+    session.install("db-dtypes")
+    session.install("google-cloud-bigquery")
+    session.install("pandas")
+    session.install("pyarrow")
+
+    if args.version is None:
+        gcp_project = "idc-external-025"
+        idc_index_version = session.run(
+            "python",
+            "scripts/python/idc_index_data_manager.py",
+            "--project",
+            gcp_project,
+            "--retrieve-latest-idc-release-version",
+            external=True,
+            silent=True,
+        ).strip()
+
+    else:
+        idc_index_version = args.version
+
+    extra = ["--quiet"] if args.commit else []
+    session.run("python", script, idc_index_version, *extra)
+
+    if args.commit:
+        session.run(
+            "git",
+            "switch",
+            "-c",
+            f"update-to-{name.replace(' ', '-').lower()}-{idc_index_version}",
+            external=True,
+        )
+        session.run("git", "add", "-u", *files, external=True)
+        session.run(
+            "git",
+            "commit",
+            "-m",
+            f"Update to {name} {idc_index_version}",
+            external=True,
+        )
+        session.log(
+            f'Complete! Now run: gh pr create --fill --body "Created by running `nox -s {session.name} -- --commit`"'
+        )
+
+
+@nox.session
+def bump(session: nox.Session) -> None:
+    """
+    Set to a new IDC index version, use -- <version>, otherwise will use the latest version.
+    """
+    files = (
+        "pyproject.toml",
+        "scripts/sql/idc_index.sql",
+        "tests/test_package.py",
+    )
+    _bump(
+        session,
+        "IDC index",
+        "scripts/python/update_idc_index_version.py",
+        files,
+    )
+
+
+@nox.session(venv_backend="none")
+def tag_release(session: nox.Session) -> None:
+    """
+    Print instructions for tagging a release and pushing it to GitHub.
+    """
+
+    session.log("Run the following commands to make a release:")
+    txt = Path("pyproject.toml").read_text()
+    current_version = next(iter(re.finditer(r'^version = "([\d\.]+)$"', txt))).group(1)
+    print(
+        f"git tag --sign -m 'idc-index-data {current_version}' {current_version} main"
+    )
+    print(f"git push origin {current_version}")
diff --git a/pyproject.toml b/pyproject.toml
index 2737df0..aed46eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,7 @@ build-backend = "scikit_build_core.build"
 
 [project]
 name = "idc-index-data"
+version = "17.0.0"
 authors = [
   { name = "Andrey Fedorov", email = "andrey.fedorov@gmail.com" },
   { name = "Vamsi Thiriveedhi", email = "vthiriveedhi@mgh.harvard.edu" },
@@ -39,7 +40,6 @@ classifiers = [
   "Topic :: Scientific/Engineering",
   "Typing :: Typed",
 ]
-dynamic = ["version"]
 dependencies = []
 
 [project.optional-dependencies]
@@ -69,15 +69,15 @@ Changelog = "https://github.com/ImagingDataCommons/idc-index-data/releases"
 [tool.scikit-build]
 minimum-version = "0.8.2"
 build-dir = "build/{wheel_tag}"
-metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
-sdist.include = ["src/idc_index_data/_version.py"]
 wheel.platlib = false
 wheel.py-api = "py3"
 
 
-[tool.setuptools_scm]
-write_to = "src/idc_index_data/_version.py"
-version_scheme = "no-guess-dev"
+[[tool.scikit-build.generate]]
+path = "idc_index_data/_version.py"
+template = '''
+version = "${version}"
+'''
 
 
 [tool.pytest.ini_options]
diff --git a/scripts/python/update_idc_index_version.py b/scripts/python/update_idc_index_version.py
new file mode 100755
index 0000000..d2f1513
--- /dev/null
+++ b/scripts/python/update_idc_index_version.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+"""
+Command line executable allowing to update source files given a IDC index version.
+"""
+
+from __future__ import annotations
+
+import argparse
+import contextlib
+import os
+import re
+import textwrap
+from pathlib import Path
+
+ROOT_DIR = Path(__file__).parent / "../.."
+
+
+@contextlib.contextmanager
+def _log(txt, verbose=True):
+    if verbose:
+        print(txt)  # noqa: T201
+    yield
+    if verbose:
+        print(f"{txt} - done")  # noqa: T201
+
+
+def _update_file(filepath, regex, replacement):
+    msg = "Updating %s" % os.path.relpath(str(filepath), ROOT_DIR)
+    with _log(msg):
+        pattern = re.compile(regex)
+        with filepath.open() as doc_file:
+            lines = doc_file.readlines()
+            updated_content = []
+            for line in lines:
+                updated_content.append(re.sub(pattern, replacement, line))
+        with filepath.open("w") as doc_file:
+            doc_file.writelines(updated_content)
+
+
+def update_pyproject_toml(idc_index_version):
+    pattern = re.compile(r'^version = "[\w\.]+"$')
+    replacement = f'version = "{idc_index_version}.0.0"'
+    _update_file(ROOT_DIR / "pyproject.toml", pattern, replacement)
+
+
+def update_sql_scripts(idc_index_version):
+    pattern = re.compile(r"idc_v\d+")
+    replacement = f"idc_v{idc_index_version}"
+    _update_file(ROOT_DIR / "scripts/sql/idc_index.sql", pattern, replacement)
+
+
+def update_tests(idc_index_version):
+    pattern = re.compile(r"EXPECTED_IDC_INDEX_VERSION = \d+")
+    replacement = f"EXPECTED_IDC_INDEX_VERSION = {idc_index_version}"
+    _update_file(ROOT_DIR / "tests/test_package.py", pattern, replacement)
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "idc_index_version",
+        metavar="IDC_INDEX_VERSION",
+        type=int,
+        help="IDC index version of the form NN",
+    )
+    parser.add_argument(
+        "--quiet",
+        action="store_true",
+        help="Hide the output",
+    )
+
+    args = parser.parse_args()
+
+    update_pyproject_toml(args.idc_index_version)
+    update_sql_scripts(args.idc_index_version)
+    update_tests(args.idc_index_version)
+
+    if not args.quiet:
+        msg = """\
+            Complete! Now run:
+
+            git switch -c update-to-idc-index-{release}
+            git add -u pyproject.toml scripts/sql/idc_index.sql tests/test_package.py
+            git commit -m "Update to IDC index {release}"
+            gh pr create --fill --body "Created by update_idc_index_version.py"
+            """
+        print(textwrap.dedent(msg.format(release=args.idc_index_version)))  # noqa: T201
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/idc_index_data/_version.pyi b/src/idc_index_data/_version.pyi
index 91744f9..502a8ee 100644
--- a/src/idc_index_data/_version.pyi
+++ b/src/idc_index_data/_version.pyi
@@ -1,4 +1,3 @@
 from __future__ import annotations
 
 version: str
-version_tuple: tuple[int, int, int] | tuple[int, int, int, str, str]
diff --git a/tests/test_package.py b/tests/test_package.py
index 3870f64..d27a51d 100644
--- a/tests/test_package.py
+++ b/tests/test_package.py
@@ -2,13 +2,21 @@
 
 import importlib.metadata
 
+from packaging.version import Version
+
 import idc_index_data as m
 
+EXPECTED_IDC_INDEX_VERSION = 17
+
 
 def test_version():
     assert importlib.metadata.version("idc_index_data") == m.__version__
 
 
+def test_idc_index_version():
+    assert Version(m.__version__).major == EXPECTED_IDC_INDEX_VERSION
+
+
 def test_filepath():
     if m.IDC_INDEX_CSV_ARCHIVE_FILEPATH is not None:
         assert m.IDC_INDEX_CSV_ARCHIVE_FILEPATH.is_file()