From c9591417574abb8dd2746d8a25031bcd5a13a328 Mon Sep 17 00:00:00 2001 From: Richard-Otterli Date: Sun, 15 Feb 2026 15:50:54 +0000 Subject: [PATCH 1/2] Align CLI version with package metadata (#87) --- src/specleft/__init__.py | 3 +- src/specleft/commands/constants.py | 4 +- src/specleft/version.py | 59 ++++++++++++++++++++++++++++++ tests/cli/test_cli_base.py | 4 +- tests/commands/test_contract.py | 5 ++- tests/commands/test_doctor.py | 3 +- tests/test_init.py | 4 +- 7 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 src/specleft/version.py diff --git a/src/specleft/__init__.py b/src/specleft/__init__.py index 119e89a..652b681 100644 --- a/src/specleft/__init__.py +++ b/src/specleft/__init__.py @@ -3,6 +3,7 @@ """SpecLeft - Specification-driven test management for pytest.""" +from specleft.version import SPECLEFT_VERSION from specleft.decorators import StepResult, shared_step, specleft, step from specleft.schema import ( ExecutionTime, @@ -16,7 +17,7 @@ StorySpec, ) -__version__ = "0.2.0" +__version__ = SPECLEFT_VERSION __all__ = [ "ExecutionTime", "FeatureSpec", diff --git a/src/specleft/commands/constants.py b/src/specleft/commands/constants.py index 13d0d23..c6d908e 100644 --- a/src/specleft/commands/constants.py +++ b/src/specleft/commands/constants.py @@ -5,6 +5,8 @@ from __future__ import annotations -CLI_VERSION = "0.2.0" +from specleft.version import SPECLEFT_VERSION + +CLI_VERSION = SPECLEFT_VERSION CONTRACT_VERSION = "1.0" CONTRACT_DOC_PATH = "docs/agent-contract.md" diff --git a/src/specleft/version.py b/src/specleft/version.py new file mode 100644 index 0000000..09cf8bf --- /dev/null +++ b/src/specleft/version.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 SpecLeft Contributors + +"""Shared version resolution for SpecLeft.""" + +from __future__ import annotations + +import re +from importlib import metadata +from pathlib import Path + +PACKAGE_NAME = "specleft" +DEFAULT_VERSION = "0.0.0" + + +def _version_from_metadata(package_name: str) -> str | None: + try: + return metadata.version(package_name) + except metadata.PackageNotFoundError: + return None + + +def _version_from_pyproject(pyproject_path: Path) -> str | None: + if not pyproject_path.exists(): + return None + try: + content = pyproject_path.read_text(encoding="utf-8") + except OSError: + return None + project_match = re.search( + r"^\[project\]\s*(.*?)(?=^\[[^\]]+\]|\Z)", + content, + flags=re.MULTILINE | re.DOTALL, + ) + if not project_match: + return None + version_match = re.search( + r'^\s*version\s*=\s*"([^"]+)"\s*$', + project_match.group(1), + flags=re.MULTILINE, + ) + if not version_match: + return None + return version_match.group(1) + + +def resolve_version(package_name: str = PACKAGE_NAME) -> str: + package_version = _version_from_metadata(package_name) + if package_version: + return package_version + pyproject_version = _version_from_pyproject( + Path(__file__).resolve().parents[2] / "pyproject.toml" + ) + if pyproject_version: + return pyproject_version + return DEFAULT_VERSION + + +SPECLEFT_VERSION = resolve_version() diff --git a/tests/cli/test_cli_base.py b/tests/cli/test_cli_base.py index 562c7a5..42bc554 100644 --- a/tests/cli/test_cli_base.py +++ b/tests/cli/test_cli_base.py @@ -3,6 +3,8 @@ from __future__ import annotations from click.testing import CliRunner + +from specleft import __version__ from specleft.cli.main import cli @@ -14,7 +16,7 @@ def test_cli_version(self) -> None: runner = CliRunner() result = runner.invoke(cli, ["--version"]) assert result.exit_code == 0 - assert "0.2.0" in result.output + assert __version__ in result.output def test_cli_help(self) -> None: """Test --help flag.""" diff --git a/tests/commands/test_contract.py b/tests/commands/test_contract.py index 45422a4..5209633 100644 --- a/tests/commands/test_contract.py +++ b/tests/commands/test_contract.py @@ -6,6 +6,7 @@ from click.testing import CliRunner from specleft.cli.main import cli +from specleft.commands.constants import CLI_VERSION class TestContractCommand: @@ -17,7 +18,7 @@ def test_contract_json_output(self) -> None: assert result.exit_code == 0 payload = json.loads(result.output) assert payload["contract_version"] == "1.0" - assert payload["specleft_version"] == "0.2.0" + assert payload["specleft_version"] == CLI_VERSION assert "guarantees" in payload def test_contract_test_json_output(self) -> None: @@ -26,6 +27,6 @@ def test_contract_test_json_output(self) -> None: assert result.exit_code == 0 payload = json.loads(result.output) assert payload["contract_version"] == "1.0" - assert payload["specleft_version"] == "0.2.0" + assert payload["specleft_version"] == CLI_VERSION assert payload["passed"] is True assert payload["checks"] diff --git a/tests/commands/test_doctor.py b/tests/commands/test_doctor.py index b1b6223..363d601 100644 --- a/tests/commands/test_doctor.py +++ b/tests/commands/test_doctor.py @@ -8,6 +8,7 @@ from click.testing import CliRunner from specleft.cli.main import cli +from specleft.commands.constants import CLI_VERSION from specleft.commands.doctor import _load_dependency_names @@ -53,7 +54,7 @@ def test_doctor_json_includes_version(self) -> None: result = runner.invoke(cli, ["doctor", "--format", "json"]) assert result.exit_code in {0, 1} payload = json.loads(result.output) - assert payload["version"] == "0.2.0" + assert payload["version"] == CLI_VERSION assert "healthy" in payload assert "checks" in payload diff --git a/tests/test_init.py b/tests/test_init.py index 68f0096..10a3e2c 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -4,9 +4,11 @@ import specleft +from specleft.version import SPECLEFT_VERSION + def test_package_exports() -> None: - assert specleft.__version__ == "0.2.0" + assert specleft.__version__ == SPECLEFT_VERSION assert specleft.specleft is not None assert specleft.step is not None assert specleft.shared_step is not None From c66a3e0db06a427176bb3257eb7ec502edf1a70a Mon Sep 17 00:00:00 2001 From: Richard-Otterli Date: Sun, 15 Feb 2026 15:56:06 +0000 Subject: [PATCH 2/2] Adjust CLI version output format (#87) --- src/specleft/cli/main.py | 6 +++++- tests/cli/test_cli_base.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/specleft/cli/main.py b/src/specleft/cli/main.py index c7e71dd..4f731e8 100644 --- a/src/specleft/cli/main.py +++ b/src/specleft/cli/main.py @@ -25,7 +25,11 @@ @click.group() -@click.version_option(version=CLI_VERSION, prog_name="specleft") +@click.version_option( + version=CLI_VERSION, + prog_name="specleft", + message="%(prog)s version: v%(version)s", +) def cli() -> None: """ SpecLeft - Code driven intent analysis for Python. diff --git a/tests/cli/test_cli_base.py b/tests/cli/test_cli_base.py index 42bc554..55d0d5c 100644 --- a/tests/cli/test_cli_base.py +++ b/tests/cli/test_cli_base.py @@ -16,7 +16,7 @@ def test_cli_version(self) -> None: runner = CliRunner() result = runner.invoke(cli, ["--version"]) assert result.exit_code == 0 - assert __version__ in result.output + assert result.output == f"specleft version: v{__version__}\n" def test_cli_help(self) -> None: """Test --help flag."""