Skip to content

Commit ffa7ca9

Browse files
authored
Merge pull request #224 from Daylily-Informatics/codex/fix-version-resolution
build: unify version resolution
2 parents 500cfec + d5e8bc3 commit ffa7ca9

File tree

6 files changed

+170
-19
lines changed

6 files changed

+170
-19
lines changed

daylily_ec/__init__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55
ParallelCluster environments for bioinformatics workloads.
66
"""
77

8-
try:
9-
from importlib.metadata import version
8+
from daylily_ec.versioning import get_version
109

11-
__version__ = version("daylily-ephemeral-cluster")
12-
except Exception:
13-
__version__ = "0.0.0.dev0"
10+
__version__ = get_version()
1411

1512
from daylily_ec.create import create_cluster
1613

daylily_ec/cli.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from cli_core_yo.app import create_app
2626
from cli_core_yo.runtime import _reset, initialize
2727
from cli_core_yo.spec import CliSpec, XdgSpec
28+
from daylily_ec import versioning
2829

2930
# ── App specification ────────────────────────────────────────────────────────
3031

@@ -42,6 +43,58 @@
4243
app = create_app(spec)
4344
pricing_app = typer.Typer(help="Spot pricing inspection helpers.")
4445
app.add_typer(pricing_app, name="pricing")
46+
app.registered_commands[:] = [
47+
cmd for cmd in app.registered_commands if cmd.name not in {"version", "info"}
48+
]
49+
50+
51+
def _installed_dist_version(dist_name: str) -> str:
52+
try:
53+
from importlib.metadata import version
54+
55+
return version(dist_name)
56+
except Exception:
57+
return "unknown"
58+
59+
60+
@app.command("version")
61+
def version_command(
62+
json: bool = typer.Option(False, "--json", "-j", help="Output as JSON."),
63+
) -> None:
64+
"""Show version."""
65+
version = versioning.get_version()
66+
if json:
67+
output.emit_json({"app": spec.app_display_name, "version": version})
68+
else:
69+
output.print_text(f"{spec.app_display_name} [cyan]{version}[/cyan]")
70+
71+
72+
@app.command("info")
73+
def info_command(
74+
json: bool = typer.Option(False, "--json", "-j", help="Output as JSON."),
75+
) -> None:
76+
"""Show system info."""
77+
xdg_paths = app._cli_core_yo_xdg_paths # type: ignore[attr-defined]
78+
rows: list[tuple[str, str]] = [
79+
("Version", versioning.get_version()),
80+
("Python", sys.version.split()[0]),
81+
("Config Dir", str(xdg_paths.config)),
82+
("Data Dir", str(xdg_paths.data)),
83+
("State Dir", str(xdg_paths.state)),
84+
("Cache Dir", str(xdg_paths.cache)),
85+
("CLI Core", _installed_dist_version("cli-core-yo")),
86+
]
87+
for hook in spec.info_hooks:
88+
rows.extend(hook())
89+
90+
if json:
91+
output.emit_json({key: value for key, value in rows})
92+
return
93+
94+
output.heading(f"{spec.app_display_name} Info")
95+
max_key = max(len(key) for key, _ in rows)
96+
for key, value in rows:
97+
output.print_text(f" {key:<{max_key}} {value}")
4598

4699

47100
# ── Root callback (global options) ───────────────────────────────────────────

daylily_ec/resources/__init__.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,11 @@
2222
from typing import Iterable
2323

2424
import importlib.resources as ir
25+
from daylily_ec import versioning
2526

2627
RES_DIR_ENV = "DAYLILY_EC_RESOURCES_DIR"
2728

2829

29-
def _package_version() -> str:
30-
try:
31-
from importlib.metadata import version
32-
33-
return version("daylily-ephemeral-cluster")
34-
except Exception:
35-
# Source-tree or otherwise not installed. Keep path stable but clearly dev.
36-
return "dev"
37-
38-
3930
def _xdg_config_home() -> Path:
4031
xdg = os.environ.get("XDG_CONFIG_HOME", "")
4132
if xdg:
@@ -75,7 +66,7 @@ def ensure_extracted() -> Path:
7566
_validate_resources_dir(root)
7667
return root
7768

78-
version = _package_version()
69+
version = versioning.get_version()
7970
dest = _xdg_config_home() / "daylily" / "resources" / version
8071
marker = dest / ".complete"
8172

@@ -132,4 +123,3 @@ def resource_path(rel_path: str) -> Path:
132123
f"Override with {RES_DIR_ENV} if needed."
133124
)
134125
return p
135-

daylily_ec/versioning.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Shared version resolution helpers for source checkouts and installed dists."""
2+
3+
from __future__ import annotations
4+
5+
from functools import lru_cache
6+
from pathlib import Path
7+
8+
DIST_NAME = "daylily-ephemeral-cluster"
9+
10+
11+
def _repo_root() -> Path:
12+
return Path(__file__).resolve().parents[1]
13+
14+
15+
def _source_tree_version() -> str | None:
16+
root = _repo_root()
17+
if not (root / ".git").exists():
18+
return None
19+
20+
try:
21+
from setuptools_scm import get_version as scm_get_version
22+
except Exception:
23+
return None
24+
25+
try:
26+
return scm_get_version(
27+
root=str(root),
28+
relative_to=__file__,
29+
version_scheme="guess-next-dev",
30+
local_scheme="node-and-date",
31+
fallback_version="0.0.0.dev0",
32+
)
33+
except Exception:
34+
return None
35+
36+
37+
def _installed_version(dist_name: str = DIST_NAME) -> str | None:
38+
try:
39+
from importlib.metadata import version
40+
41+
return version(dist_name)
42+
except Exception:
43+
return None
44+
45+
46+
@lru_cache(maxsize=1)
47+
def get_version() -> str:
48+
"""Return the best available version string for the current execution context."""
49+
version = _source_tree_version()
50+
if version:
51+
return version
52+
53+
version = _installed_version()
54+
if version:
55+
return version
56+
57+
return "0.0.0.dev0"

setup.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313
setup(
1414
name="daylily-ephemeral-cluster",
15-
use_scm_version=True, # derive from tags
16-
setup_requires=["setuptools_scm"],
1715
packages=find_packages(),
1816
include_package_data=True,
1917
scripts=script_files,

tests/test_versioning.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from __future__ import annotations
2+
3+
from typer.testing import CliRunner
4+
5+
from daylily_ec import versioning
6+
from daylily_ec.cli import app
7+
from daylily_ec.resources import ensure_extracted
8+
9+
runner = CliRunner()
10+
11+
12+
def test_get_version_prefers_source_tree(monkeypatch):
13+
versioning.get_version.cache_clear()
14+
monkeypatch.setattr(versioning, "_source_tree_version", lambda: "1.2.3")
15+
monkeypatch.setattr(versioning, "_installed_version", lambda dist_name=versioning.DIST_NAME: "9.9.9")
16+
17+
assert versioning.get_version() == "1.2.3"
18+
19+
20+
def test_get_version_falls_back_to_installed_metadata(monkeypatch):
21+
versioning.get_version.cache_clear()
22+
monkeypatch.setattr(versioning, "_source_tree_version", lambda: None)
23+
monkeypatch.setattr(versioning, "_installed_version", lambda dist_name=versioning.DIST_NAME: "2.3.4")
24+
25+
assert versioning.get_version() == "2.3.4"
26+
27+
28+
def test_cli_version_uses_shared_version_resolver(monkeypatch):
29+
versioning.get_version.cache_clear()
30+
monkeypatch.setattr(versioning, "get_version", lambda: "3.4.5")
31+
32+
result = runner.invoke(app, ["version"])
33+
34+
assert result.exit_code == 0
35+
assert "3.4.5" in result.stdout
36+
37+
38+
def test_cli_info_uses_shared_version_resolver(monkeypatch):
39+
versioning.get_version.cache_clear()
40+
monkeypatch.setattr(versioning, "get_version", lambda: "4.5.6")
41+
42+
result = runner.invoke(app, ["info"])
43+
44+
assert result.exit_code == 0
45+
assert "4.5.6" in result.stdout
46+
47+
48+
def test_resources_dir_uses_shared_version_resolver(tmp_path, monkeypatch):
49+
versioning.get_version.cache_clear()
50+
monkeypatch.setattr(versioning, "get_version", lambda: "5.6.7")
51+
monkeypatch.delenv("DAYLILY_EC_RESOURCES_DIR", raising=False)
52+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
53+
54+
root = ensure_extracted()
55+
56+
assert root.name == "5.6.7"

0 commit comments

Comments
 (0)