diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b45d5..bfaffaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Anything MAY change at any time. The public API SHOULD NOT be considered stable."). While in this phase, we will denote breaking changes with a minor increase. +## 0.4.1 + +### Added + +* Introduce `dac next-version` command, that allows to find the next minor release for a given python package and (optionally) a given major version. + ## 0.4.0 ### Changed diff --git a/src/dac/_cli.py b/src/dac/_cli.py index ae28bf6..1191c4c 100644 --- a/src/dac/_cli.py +++ b/src/dac/_cli.py @@ -2,6 +2,7 @@ import sys from importlib.metadata import requires, version from pathlib import Path +from typing import Optional import typer from rich.console import Console @@ -11,6 +12,7 @@ from dac._input.config import PackConfig from dac._input.pyproject import PyProjectConfig from dac._packing import pack as py_api_pack +from dac._version_management import find_latest_version, increase_minor app = typer.Typer() console = Console() @@ -69,6 +71,30 @@ def pack( ) +@app.command() +def next_version( + pkg_name: str = typer.Option( + ..., + help="Name of the python package", + ), + major: Optional[int] = typer.Option( + None, + "--major", + help="Major of the version that should be increased. " + "If not specified just take it from the latest version of the package.", + ), +): + """ + Print the next version of the python package. + + It assumes that semantic versioning (https://semver.org) is used, + and that the next version corresponds to a minor upgrade. + """ + latest_version = find_latest_version(pkg_name=pkg_name, major=major) + next_version = increase_minor(version=latest_version) + console.print(next_version) + + @app.command() def info(): """ diff --git a/src/dac/_version_management.py b/src/dac/_version_management.py new file mode 100644 index 0000000..769da62 --- /dev/null +++ b/src/dac/_version_management.py @@ -0,0 +1,29 @@ +import re +import subprocess +from typing import Optional + + +def find_latest_version(pkg_name: str, major: Optional[int] = None) -> str: + output = subprocess.check_output( + [ + "pip", + "install", + "--no-deps", + "--ignore-installed", + "--no-cache-dir", + "--dry-run", + f"{pkg_name}{f'=={major}.*' if major is not None else ''}", + ], + stderr=subprocess.DEVNULL, + ) + last_line = output.decode("utf-8").splitlines()[-1] + regex_rule = f"{pkg_name.replace('_', '-')}-{major if major is not None else ''}.[^ ]+" + match = re.search(regex_rule, last_line) + assert match is not None + return match[0][len(f"{pkg_name}-") :] + + +def increase_minor(version: str) -> str: + major, minor, patch = version.split(".") + assert major.isdigit() and minor.isdigit() + return f"{major}.{int(minor) + 1}.0" diff --git a/test/cli_utilities.py b/test/cli_utilities.py index d5d9fed..1f303a2 100644 --- a/test/cli_utilities.py +++ b/test/cli_utilities.py @@ -8,10 +8,9 @@ from typing import Optional from click.testing import Result -from typer.testing import CliRunner - from dac._cli import app from dac._input.config import PackConfig +from typer.testing import CliRunner runner = CliRunner() @@ -66,5 +65,16 @@ def invoke_dac_pack_from_config(config: PackConfig) -> Result: ) +def invoke_dac_next_version( + pkg_name: str, + major: Optional[int] = None, +) -> Result: + major_option = [] if major is None else ["--major", str(major)] + return runner.invoke( + app, + ["next-version", "--pkg-name", pkg_name] + major_option, + ) + + def invoke_dac_info() -> Result: return runner.invoke(app, ["info"]) diff --git a/test/integration_test/version_management_test.py b/test/integration_test/version_management_test.py new file mode 100644 index 0000000..09e63ba --- /dev/null +++ b/test/integration_test/version_management_test.py @@ -0,0 +1,27 @@ +from test.cli_utilities import invoke_dac_next_version + +import pytest +from dac._version_management import find_latest_version + + +def test_if_find_latest_version_is_called_then_return_latest_version(): + assert "0.5" == find_latest_version(pkg_name="rainbow-server") + + +def test_if_find_latest_version_is_called_with_major_constraint_then_return_latest_major_version(): + assert "0.25.3" == find_latest_version(pkg_name="pandas", major=0) + + +def test_if_pkg_does_not_exist_then_find_package_raises_exception(): + with pytest.raises(Exception): + find_latest_version(pkg_name="non-existing-package") + + +def test_if_next_version_without_major_spec_then_return_latest_version_with_minor_upgrade(): + result = invoke_dac_next_version(pkg_name="rainbow-saddle") + assert result.stdout == "0.5.0\n" + + +def test_if_next_version_with_major_spec_then_return_minor_upgrade_for_that_major(): + result = invoke_dac_next_version(pkg_name="pandas", major=0) + assert result.stdout == "0.26.0\n" diff --git a/test/unit_test/_version_management/__init__.py b/test/unit_test/_version_management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit_test/_version_management/increase_minor_test.py b/test/unit_test/_version_management/increase_minor_test.py new file mode 100644 index 0000000..8fd9246 --- /dev/null +++ b/test/unit_test/_version_management/increase_minor_test.py @@ -0,0 +1,13 @@ +import pytest +from dac._version_management import increase_minor + + +def test_increase_minor_then_return_version_with_increased_minor(): + assert "0.6.0" == increase_minor(version="0.5.0") + assert "0.6.0" == increase_minor(version="0.5.1rc0") + + +@pytest.mark.parametrize("version", ["0", "0.5", "guess.what.now", "guess.0.now"]) +def test_if_invalid_version_then_increase_minor_raises_exception(version): + with pytest.raises(Exception): + increase_minor(version=version)