Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 1 deletion src/specleft/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,7 +17,7 @@
StorySpec,
)

__version__ = "0.2.0"
__version__ = SPECLEFT_VERSION
__all__ = [
"ExecutionTime",
"FeatureSpec",
Expand Down
6 changes: 5 additions & 1 deletion src/specleft/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion src/specleft/commands/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
59 changes: 59 additions & 0 deletions src/specleft/version.py
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 3 additions & 1 deletion tests/cli/test_cli_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from __future__ import annotations

from click.testing import CliRunner

from specleft import __version__
from specleft.cli.main import cli


Expand All @@ -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 result.output == f"specleft version: v{__version__}\n"

def test_cli_help(self) -> None:
"""Test --help flag."""
Expand Down
5 changes: 3 additions & 2 deletions tests/commands/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from click.testing import CliRunner
from specleft.cli.main import cli
from specleft.commands.constants import CLI_VERSION


class TestContractCommand:
Expand All @@ -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:
Expand All @@ -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"]
3 changes: 2 additions & 1 deletion tests/commands/test_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading