Skip to content

Commit

Permalink
Added show-bump subcommand
Browse files Browse the repository at this point in the history
- Shows possible resulting versions of the `bump` command
  • Loading branch information
coordt committed Jan 21, 2024
1 parent 8f4bedf commit 0bbd814
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 0 deletions.
22 changes: 22 additions & 0 deletions bumpversion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from bumpversion.show import do_show, log_list
from bumpversion.ui import get_indented_logger, print_info, print_warning, setup_logging
from bumpversion.utils import get_context, get_overrides
from bumpversion.visualize import visualize

logger = get_indented_logger(__name__)

Expand Down Expand Up @@ -547,3 +548,24 @@ def sample_config(prompt: bool, destination: str) -> None:
print_info(dumps(destination_config))
else:
Path(destination).write_text(dumps(destination_config))


@cli.command()
@click.argument("version", nargs=1, type=str, required=False, default="")
@click.option(
"--config-file",
metavar="FILE",
required=False,
envvar="BUMPVERSION_CONFIG_FILE",
type=click.Path(exists=True),
help="Config file to read most of the variables from.",
)
@click.option("--ascii", is_flag=True, help="Use ASCII characters only.")
def show_bump(version: str, config_file: Optional[str], ascii: bool) -> None:
"""Show the possible versions resulting from the bump subcommand."""
found_config_file = find_config_file(config_file)
config = get_configuration(found_config_file)
if not version:
version = config.current_version
box_style = "ascii" if ascii else "light"
visualize(config=config, version_str=version, box_style=box_style)
122 changes: 122 additions & 0 deletions bumpversion/visualize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Visualize the bumpversion process."""
from dataclasses import dataclass
from typing import Optional

from bumpversion.bump import get_next_version
from bumpversion.config import Config
from bumpversion.exceptions import BumpVersionError
from bumpversion.ui import print_info
from bumpversion.utils import get_context

BOX_CHARS = {
"ascii": ["+", "+", "+", "+", "+", "+", "+", "+", "-", "|", "+"],
"light": ["╯", "╮", "╭", "╰", "┤", "┴", "┬", "├", "─", "│", "┼"],
}


@dataclass
class Border:
"""A border definition."""

corner_bottom_right: str
corner_top_right: str
corner_top_left: str
corner_bottom_left: str
divider_left: str
divider_up: str
divider_down: str
divider_right: str
line: str
pipe: str
cross: str


def lead_string(version_str: str, border: Border, blank: bool = False) -> str:
"""
Return the first part of a string with the bump character or spaces of the correct amount.
Examples:
>>> lead_string("1.0.0", Border(*BOX_CHARS["light"]))
'1.0.0 ── bump ─'
>>> lead_string("1.0.0", Border(*BOX_CHARS["light"]), blank=True)
' '
Args:
version_str: The string to render as the starting point
border: The border definition to draw the lines
blank: If `True`, return a blank string the same length as the version bump string
Returns:
The version bump string or a blank string
"""
version_bump = f"{version_str} {border.line * 2} bump {border.line}"
return " " * len(version_bump) if blank else version_bump


def connection_str(border: Border, has_next: bool = False, has_previous: bool = False) -> str:
"""
Return the correct connection string based on the next and previous.
Args:
border: The border definition to draw the lines
has_next: If `True`, there is a next line
has_previous: If `True`, there is a previous line
Returns:
A string that connects left-to-right and top-to-bottom based on the next and previous
"""
if has_next and has_previous:
return border.divider_right + border.line
elif has_next:
return border.divider_down + border.line
elif has_previous:
return border.corner_bottom_left + border.line
else:
return border.line * 2


def labeled_line(label: str, border: Border, fit_length: Optional[int] = None) -> str:
"""
Return the version part string with the correct padding.
Args:
label: The label to render
border: The border definition to draw the lines
fit_length: The length to fit the label to
Returns:
A labeled line with leading and trailing spaces
"""
if fit_length is None:
fit_length = len(label)
return f" {label} {border.line * (fit_length - len(label))}{border.line} "


def visualize(config: Config, version_str: str, box_style: str = "light") -> None:
"""Output a visualization of the bump-my-version bump process."""
version = config.version_config.parse(version_str)
version_parts = config.version_config.order
num_parts = len(version_parts)

box_style = box_style if box_style in BOX_CHARS else "light"
border = Border(*BOX_CHARS[box_style])

version_lead = lead_string(version_str, border)
blank_lead = lead_string(version_str, border, blank=True)
version_part_length = max(len(part) for part in version_parts)

for i, part in enumerate(version_parts):
line = [version_lead] if i == 0 else [blank_lead]

try:
next_version = get_next_version(version, config, part, None)
next_version_str = config.version_config.serialize(next_version, get_context(config))
except (BumpVersionError, ValueError):
next_version_str = "invalid"

has_next = i < num_parts - 1
has_previous = i > 0
line.append(connection_str(border, has_next=has_next, has_previous=has_previous))
line.append(labeled_line(part, border, version_part_length))
line.append(next_version_str)
print_info("".join(line))
59 changes: 59 additions & 0 deletions tests/test_cli/test_show_bump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Tests the show_bump command."""
import shutil
from pathlib import Path

from click.testing import CliRunner, Result
from bumpversion import cli
from tests.conftest import inside_dir


def test_show_bump_uses_current_version(tmp_path: Path, fixtures_path: Path):
"""The show_bump subcommand should list the parts of the configuration."""
# Arrange
config_path = tmp_path / "pyproject.toml"
toml_path = fixtures_path / "basic_cfg.toml"
shutil.copy(toml_path, config_path)
runner: CliRunner = CliRunner()

with inside_dir(tmp_path):
result: Result = runner.invoke(cli.cli, ["show-bump"])

if result.exit_code != 0:
print(result.output)
print(result.exception)

assert result.exit_code == 0
assert result.output == "\n".join(
[
"1.0.0 ── bump ─┬─ major ─── 2.0.0-dev",
" ├─ minor ─── 1.1.0-dev",
" ├─ patch ─── 1.0.1-dev",
" ╰─ release ─ invalid\n",
]
)


def test_show_bump_uses_passed_version(tmp_path: Path, fixtures_path: Path):
"""The show_bump subcommand should list the parts of the configuration."""
# Arrange
config_path = tmp_path / "pyproject.toml"
toml_path = fixtures_path / "basic_cfg.toml"
shutil.copy(toml_path, config_path)
runner: CliRunner = CliRunner()

with inside_dir(tmp_path):
result: Result = runner.invoke(cli.cli, ["show-bump", "1.2.3"])

if result.exit_code != 0:
print(result.output)
print(result.exception)

assert result.exit_code == 0
assert result.output == "\n".join(
[
"1.2.3 ── bump ─┬─ major ─── 2.0.0-dev",
" ├─ minor ─── 1.3.0-dev",
" ├─ patch ─── 1.2.4-dev",
" ╰─ release ─ invalid\n",
]
)
70 changes: 70 additions & 0 deletions tests/test_visualize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Tests of the visualize module."""
from pathlib import Path

from bumpversion.visualize import Border, BOX_CHARS, connection_str, lead_string, labeled_line, visualize
from bumpversion.config import get_configuration


class TestLeadString:
"""Tests of the lead_string function."""

def test_returns_string_with_bump(self):
assert lead_string("1.0.0", Border(*BOX_CHARS["light"])) == "1.0.0 ── bump ─"

def test_returns_blank_string(self):
assert lead_string("1.0.0", Border(*BOX_CHARS["light"]), blank=True) == " "


class TestConnectionStr:
"""Tests of the connection_str function."""

def test_returns_correct_connection_string(self):
border = Border(*BOX_CHARS["light"])
assert connection_str(border, has_next=True, has_previous=True) == border.divider_right + border.line
assert connection_str(border, has_next=True, has_previous=False) == border.divider_down + border.line
assert connection_str(border, has_next=False, has_previous=True) == border.corner_bottom_left + border.line
assert connection_str(border, has_next=False, has_previous=False) == border.line * 2


class TestLabeledLine:
"""Tests of the labeled_line function."""

def test_no_label_fill_when_no_fit_length(self):
"""Without a fit_length, there will be no filler in the line."""
border = Border(*BOX_CHARS["light"])
assert labeled_line("major", border, fit_length=None) == " major ─ "

def test_pads_label_if_fit_length(self):
"""Without a fit_length, there will be no filler in the line."""
border = Border(*BOX_CHARS["light"])
assert labeled_line("major", border, fit_length=10) == " major ────── "


class TestVisualize:
"""Tests of the visualize function."""

def test_outputs_using_default_config(self, tmp_path: Path, capsys):
"""Test that the correct string is returned."""
config = get_configuration(tmp_path.joinpath("missing.toml"), current_version="1.0.0")
visualize(config, "1.0.0")
captured = capsys.readouterr()
assert captured.out == "\n".join(
["1.0.0 ── bump ─┬─ major ─ 2.0.0", " ├─ minor ─ 1.1.0", " ╰─ patch ─ 1.0.1\n"]
)

def test_indicates_invalid_paths(self, tmp_path: Path, fixtures_path: Path, capsys):
"""Test that the correct string is returned."""
config_content = fixtures_path.joinpath("basic_cfg.toml").read_text()
config_path = tmp_path.joinpath(".bumpversion-1.toml")
config_path.write_text(config_content)
config = get_configuration(config_path)
visualize(config, "1.0.0")
captured = capsys.readouterr()
assert captured.out == "\n".join(
[
"1.0.0 ── bump ─┬─ major ─── 2.0.0-dev",
" ├─ minor ─── 1.1.0-dev",
" ├─ patch ─── 1.0.1-dev",
" ╰─ release ─ invalid\n",
]
)

0 comments on commit 0bbd814

Please sign in to comment.