Skip to content
Open
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
77 changes: 57 additions & 20 deletions openwisp_utils/releaser/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
import re
import subprocess

try:
import tomllib # pragma: no cover
except ImportError: # pragma: no cover
try:
import tomli as tomllib # pragma: no cover
except ImportError: # pragma: no cover
tomllib = None # pragma: no cover


def get_package_name_from_setup():
"""Parses setup.py to find the package name without raising an error."""
Expand All @@ -22,6 +30,7 @@ def get_package_type_from_setup():
"""Detects package type based on config files present in the project."""
package_type_files = {
"setup.py": "python",
"pyproject.toml": "python",
"package.json": "npm",
"docker-compose.yml": "docker",
".ansible-lint": "ansible",
Expand Down Expand Up @@ -58,27 +67,54 @@ def _handle_python_version(config):
netjsonconfig, netdiff).
"""
project_name = get_package_name_from_setup()
if not project_name:
if project_name:
package_directory = project_name.replace("-", "_")
candidate_files = [
os.path.join(package_directory, "__init__.py"),
os.path.join(package_directory, "version.py"),
]
for version_path in candidate_files:
if not os.path.exists(version_path):
continue
with open(version_path, "r") as f:
content = f.read()
version_match = re.search(r"^VERSION\s*=\s*\((.*)\)", content, re.M)
if version_match:
config["version_path"] = version_path
try:
version_tuple = ast.literal_eval(f"({version_match.group(1)})")
config["CURRENT_VERSION"] = list(version_tuple)
except (ValueError, SyntaxError, TypeError):
config["CURRENT_VERSION"] = None
return
_handle_pyproject_toml_version(config)


def _handle_pyproject_toml_version(config):
"""Handles version detection from pyproject.toml."""
if not os.path.exists("pyproject.toml"):
return
package_directory = project_name.replace("-", "_")
candidate_files = [
os.path.join(package_directory, "__init__.py"),
os.path.join(package_directory, "version.py"),
]
for version_path in candidate_files:
if not os.path.exists(version_path):
continue
with open(version_path, "r") as f:
content = f.read()
version_match = re.search(r"^VERSION\s*=\s*\((.*)\)", content, re.M)
if version_match:
config["version_path"] = version_path
try:
version_tuple = ast.literal_eval(f"({version_match.group(1)})")
config["CURRENT_VERSION"] = list(version_tuple)
except (ValueError, SyntaxError, TypeError):
config["CURRENT_VERSION"] = None
return
if tomllib is None:
return # pragma: no cover
with open("pyproject.toml", "rb") as f:
try:
data = tomllib.load(f)
except Exception:
return
project = data.get("project", {})
version_str = project.get("version")
if not version_str:
return
try:
parts = version_str.split(".")
if len(parts) != 3:
return
current_version = [int(parts[0]), int(parts[1]), int(parts[2]), "final"]
except (ValueError, TypeError):
return
config["package_type"] = "pyproject"
config["version_path"] = "pyproject.toml"
config["CURRENT_VERSION"] = current_version


def _handle_npm_version(config):
Expand Down Expand Up @@ -203,6 +239,7 @@ def _handle_generic_version(config):
# Maps package types to their version detection handlers
PACKAGE_VERSION_HANDLERS = {
"python": _handle_python_version,
"pyproject": _handle_pyproject_toml_version,
Comment thread
BHARATH0153 marked this conversation as resolved.
"npm": _handle_npm_version,
"docker": _handle_docker_version,
"ansible": _handle_ansible_version,
Expand Down
80 changes: 80 additions & 0 deletions openwisp_utils/releaser/tests/test_config.py
Comment thread
BHARATH0153 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -587,3 +587,83 @@ def test_no_package_type_detected(project_dir, create_changelog, init_git_repo):
assert config["package_type"] is None
assert config["version_path"] is None
assert config["CURRENT_VERSION"] is None


def test_pyproject_toml_package_detection(project_dir, create_changelog, init_git_repo):
"""Tests that python package type is detected when pyproject.toml exists."""
(project_dir / "pyproject.toml").write_text(
'[project]\nname = "my-package"\nversion = "1.2.3"\n'
)
create_changelog(project_dir)
init_git_repo(project_dir)
config = load_config()
assert config["package_type"] == "pyproject"
assert config["version_path"] == "pyproject.toml"
assert config["CURRENT_VERSION"] == [1, 2, 3, "final"]
Comment thread
BHARATH0153 marked this conversation as resolved.


def test_pyproject_toml_missing_version(project_dir, create_changelog, init_git_repo):
"""Tests pyproject.toml without a version field."""
(project_dir / "pyproject.toml").write_text('[project]\nname = "my-package"\n')
create_changelog(project_dir)
init_git_repo(project_dir)
config = load_config()
assert config["version_path"] is None
assert config["CURRENT_VERSION"] is None


def test_pyproject_toml_invalid_version(project_dir, create_changelog, init_git_repo):
"""Tests pyproject.toml with an invalid version format."""
(project_dir / "pyproject.toml").write_text(
'[project]\nname = "my-package"\nversion = "1.2"\n'
)
create_changelog(project_dir)
init_git_repo(project_dir)
config = load_config()
assert config["version_path"] is None
assert config["CURRENT_VERSION"] is None


def test_pyproject_toml_malformed(project_dir, create_changelog, init_git_repo):
"""Tests pyproject.toml with malformed TOML content."""
(project_dir / "pyproject.toml").write_text("@invalid toml\n")
create_changelog(project_dir)
init_git_repo(project_dir)
config = load_config()
assert config["version_path"] is None
assert config["CURRENT_VERSION"] is None


def test_pyproject_toml_non_numeric_version(
project_dir, create_changelog, init_git_repo
):
"""Tests pyproject.toml with a non-numeric version component."""
(project_dir / "pyproject.toml").write_text(
'[project]\nname = "my-package"\nversion = "1.2.a"\n'
)
create_changelog(project_dir)
init_git_repo(project_dir)
config = load_config()
assert config["version_path"] is None
assert config["CURRENT_VERSION"] is None


def test_setup_py_takes_priority_over_pyproject_toml(
project_dir,
create_setup_py,
create_package_dir_with_version,
create_changelog,
init_git_repo,
):
"""Tests that setup.py takes priority over pyproject.toml."""
create_setup_py(project_dir)
create_package_dir_with_version(project_dir)
(project_dir / "pyproject.toml").write_text(
'[project]\nname = "my-package"\nversion = "9.9.9"\n'
)
create_changelog(project_dir)
init_git_repo(project_dir)
config = load_config()
assert config["package_type"] == "python"
assert config["version_path"] == "my_test_package/__init__.py"
assert config["CURRENT_VERSION"] == [1, 2, 3, "final"]
85 changes: 84 additions & 1 deletion openwisp_utils/releaser/tests/test_version_bumping.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,87 @@ def test_bump_version_generic():
result = bump_version(config, "1.2.4")
assert result is True
written_content = m_open().write.call_args[0][0]
assert written_content == "1.2.4\n"
assert "1.2.4" in written_content


PYPROJECT_TOML_CONTENT = """[build-system]
requires = ["setuptools"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "my-package"
version = "1.2.3"
description = "A test package"
"""

PYPROJECT_TOML_BUMPED = """[build-system]
requires = ["setuptools"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "my-package"
version = "1.2.4"
description = "A test package"
"""


def test_get_current_version_pyproject():
"""Tests getting current version from pyproject.toml."""
config = {
"package_type": "pyproject",
"version_path": "pyproject.toml",
"CURRENT_VERSION": [1, 2, 3, "final"],
}
version, version_type = get_current_version(config)
assert version == "1.2.3"
assert version_type == "final"


def test_bump_version_pyproject_toml():
"""Tests bumping version in pyproject.toml."""
config = {
"package_type": "pyproject",
"version_path": "pyproject.toml",
"CURRENT_VERSION": [1, 2, 3, "final"],
}
m_open = mock_open(read_data=PYPROJECT_TOML_CONTENT)
with patch("os.path.exists", return_value=True), patch("builtins.open", m_open):
result = bump_version(config, "1.2.4")
assert result is True
written_content = m_open().write.call_args[0][0]
assert 'version = "1.2.4"' in written_content


def test_bump_version_pyproject_toml_not_found():
"""Tests error when version field is missing in pyproject.toml."""
config = {
"package_type": "pyproject",
"version_path": "pyproject.toml",
"CURRENT_VERSION": [1, 2, 3, "final"],
}
content_no_version = """[build-system]
requires = ["setuptools"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "my-package"
description = "No version here"
"""
m_open = mock_open(read_data=content_no_version)
with patch("os.path.exists", return_value=True), patch("builtins.open", m_open):
with pytest.raises(RuntimeError, match="Failed to find"):
bump_version(config, "1.2.4")


def test_bump_version_pyproject_toml_malformed():
"""Tests bumping with malformed TOML content (falls through to regex)."""
config = {
"package_type": "pyproject",
"version_path": "pyproject.toml",
"CURRENT_VERSION": [1, 2, 3, "final"],
}
malformed_content = "@invalid toml\n"
m_open = mock_open(read_data=malformed_content)
with patch("os.path.exists", return_value=True), patch("builtins.open", m_open):
with pytest.raises(RuntimeError, match="Failed to find"):
bump_version(config, "1.2.4")
31 changes: 31 additions & 0 deletions openwisp_utils/releaser/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
import subprocess
import sys

try:
import tomllib # pragma: no cover
except ImportError: # pragma: no cover
try:
import tomli as tomllib # pragma: no cover
except ImportError: # pragma: no cover
tomllib = None # pragma: no cover

import questionary


Expand Down Expand Up @@ -54,6 +62,28 @@ def _bump_python_version(content, new_version, version_path):
)


def _bump_pyproject_toml_version(content, new_version, version_path):
"""Handles version bumping in pyproject.toml."""
if tomllib is not None:
try:
data = tomllib.loads(content)
except Exception:
pass
else:
current = data.get("project", {}).get("version", "")
if current:
old = f'version = "{current}"'
if old in content:
return content.replace(old, f'version = "{new_version}"', 1)
Comment thread
BHARATH0153 marked this conversation as resolved.
return _bump_with_regex(
content,
r'^version\s*=\s*"([^"]+)"',
f'version = "{new_version}"',
version_path,
"version in pyproject.toml",
)
Comment thread
BHARATH0153 marked this conversation as resolved.


def _bump_npm_version(content, new_version, version_path):
"""Handles version bumping for NPM packages."""
try:
Expand Down Expand Up @@ -119,6 +149,7 @@ def _bump_generic_version(content, new_version, version_path):
# Maps package types to their version bump handlers
VERSION_BUMP_HANDLERS = {
"python": _bump_python_version,
"pyproject": _bump_pyproject_toml_version,
"npm": _bump_npm_version,
"docker": _bump_docker_version,
"ansible": _bump_ansible_version,
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"questionary~=2.1.0",
"pypandoc~=1.15",
"pypandoc-binary~=1.15",
"tomli>=1.1.0; python_version < '3.11'",
],
"github_actions": [
"google-genai>=1.62.0,<3.0.0",
Expand Down
Loading