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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ publish-all:

.PHONY: validate-version-consistency
validate-version-consistency:
@$(PYTHON) -c 'import pathlib, sys, tomllib; pyproject=tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]; expected=pyproject["version"]; src=pathlib.Path("src/ecli/__init__.py").read_text(); sys.path.insert(0, "src"); import ecli; actual=ecli.__version__; ok=(actual == expected or actual == "0.0.0+local") and "version(\"ecli-editor\")" in src; print(f"pyproject={expected} ecli.__version__={actual}"); sys.exit(0 if ok else 1)'
@$(PYTHON) -c 'import pathlib, sys, tomllib; root=pathlib.Path.cwd().resolve(); sys.path.insert(0, str(root / "src")); import ecli; pyproject=tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"]; expected=pyproject["version"]; actual=ecli.__version__; imported=pathlib.Path(ecli.__file__).resolve(); source_root=root / "src" / "ecli"; print(f"pyproject={expected} ecli.__version__={actual}"); ok=(actual == expected and imported.is_relative_to(source_root)); sys.exit(0 if ok else 1)'

.PHONY: validate-pypi-contract
validate-pypi-contract:
Expand Down
42 changes: 38 additions & 4 deletions src/ecli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,46 @@

"""ECLI — terminal-based text editor."""

import tomllib
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path


try:
__version__ = version("ecli-editor")
except PackageNotFoundError:
__version__ = "0.0.0+local"
_DISTRIBUTION_NAME = "ecli-editor"


def _read_pyproject_version(pyproject_path: Path) -> str | None:
if not pyproject_path.is_file():
return None
try:
with pyproject_path.open("rb") as pyproject_file:
raw_version = tomllib.load(pyproject_file)["project"]["version"]
except (KeyError, OSError, TypeError, tomllib.TOMLDecodeError):
return None
if not isinstance(raw_version, str) or not raw_version.strip():
return None
return raw_version


def _source_tree_version(package_file: Path | None = None) -> str | None:
source_file = Path(__file__) if package_file is None else package_file
pyproject_path = source_file.resolve().parents[2] / "pyproject.toml"
return _read_pyproject_version(pyproject_path)


def _installed_package_version(
distribution_name: str = _DISTRIBUTION_NAME,
) -> str | None:
try:
return version(distribution_name)
except PackageNotFoundError:
return None


def _resolve_version() -> str:
return _source_tree_version() or _installed_package_version() or "0.0.0+local"


__version__ = _resolve_version()

__all__ = ["__version__"]
134 changes: 134 additions & 0 deletions tests/test_version_resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# SPDX-License-Identifier: Apache-2.0
#
# Project: Ecli
# File: tests/test_version_resolution.py
# Website: https://www.ecli.io
# Repository: https://github.com/SSobol77/ecli
# PyPI: https://pypi.org/project/ecli-editor/0.0.1/
#
# Copyright (c) 2026 Siergej Sobolewski
#
# Licensed under the Apache License, Version 2.0.
# See the LICENSE file in the project root for full license text.

"""Tests for source-tree and installed-package version resolution."""

from __future__ import annotations

import shutil
from importlib.metadata import PackageNotFoundError
from pathlib import Path
from typing import Iterator

import pytest

import ecli


@pytest.fixture
def version_workspace(request: pytest.FixtureRequest) -> Iterator[Path]:
logs_root = Path.cwd() / "logs" / "test-version-resolution"
test_root = logs_root / request.node.name.replace("/", "_").replace(":", "_")
shutil.rmtree(test_root, ignore_errors=True)
test_root.mkdir(parents=True)
try:
yield test_root
finally:
shutil.rmtree(test_root, ignore_errors=True)


def package_file_under(root: Path) -> Path:
package_dir = root / "src" / "ecli"
package_dir.mkdir(parents=True)
package_file = package_dir / "__init__.py"
package_file.write_text("# test package marker\n", encoding="utf-8")
return package_file


def test_source_tree_version_resolves_from_pyproject(version_workspace: Path) -> None:
package_file = package_file_under(version_workspace)
(version_workspace / "pyproject.toml").write_text(
'[project]\nversion = "9.8.7"\n',
encoding="utf-8",
)

assert ecli._source_tree_version(package_file) == "9.8.7"


def test_source_tree_version_returns_none_when_pyproject_is_missing(
version_workspace: Path,
) -> None:
package_file = package_file_under(version_workspace)

assert ecli._source_tree_version(package_file) is None


def test_source_tree_version_returns_none_for_malformed_pyproject(
version_workspace: Path,
) -> None:
package_file = package_file_under(version_workspace)
(version_workspace / "pyproject.toml").write_text(
"[project\nversion = broken\n",
encoding="utf-8",
)

assert ecli._source_tree_version(package_file) is None


def test_source_tree_version_returns_none_when_project_version_is_missing(
version_workspace: Path,
) -> None:
package_file = package_file_under(version_workspace)
(version_workspace / "pyproject.toml").write_text(
'[project]\nname = "ecli-editor"\n',
encoding="utf-8",
)

assert ecli._source_tree_version(package_file) is None


def test_source_tree_version_returns_none_for_non_string_version(
version_workspace: Path,
) -> None:
package_file = package_file_under(version_workspace)
(version_workspace / "pyproject.toml").write_text(
"[project]\nversion = 123\n",
encoding="utf-8",
)

assert ecli._source_tree_version(package_file) is None


def test_resolve_version_falls_back_to_installed_package_metadata(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(ecli, "_source_tree_version", lambda: None)
monkeypatch.setattr(ecli, "_installed_package_version", lambda: "1.2.3")

assert ecli._resolve_version() == "1.2.3"


def test_installed_package_version_returns_none_when_metadata_is_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def missing_distribution(_: str) -> str:
raise PackageNotFoundError("ecli-editor")

monkeypatch.setattr(ecli, "version", missing_distribution)

assert ecli._installed_package_version() is None


def test_resolve_version_falls_back_to_local_when_metadata_is_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(ecli, "_source_tree_version", lambda: None)
monkeypatch.setattr(ecli, "_installed_package_version", lambda: None)

assert ecli._resolve_version() == "0.0.0+local"


def test_version_workspace_stays_under_logs(version_workspace: Path) -> None:
logs_root = (Path.cwd() / "logs").resolve(strict=False)

assert version_workspace.resolve(strict=False).is_relative_to(logs_root)
Loading