diff --git a/CHANGELOG.md b/CHANGELOG.md index 396c6042..503d3a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## 0.25.3 (2024-08-13) +[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.25.2...0.25.3) + +### Fixes + +- Refactor version parsing in visualize function. [5f25300](https://github.com/callowayproject/bump-my-version/commit/5f25300b007cee294a43af64eb970e3e100931a8) + + Simplify the version parsing process by utilizing the raise_error parameter in the parse method, removing the need for a separate error check. This change ensures that parsing errors are immediately raised and handled cleanly within the visualize function. +- Refactor and rename `version_part` to `versioning.version_config`. [5b90817](https://github.com/callowayproject/bump-my-version/commit/5b90817e6210f983c691bd06008afa065c961c4f) + + Moved `version_part.py` to `versioning/version_config.py` and updated all import statements accordingly. Enhanced error handling in `VersionConfig` by adding `raise_error` flag and relevant exception raising for invalid version strings. Refined tests to reflect these changes. +- Fix version visualization and add verbose logging. [ad46978](https://github.com/callowayproject/bump-my-version/commit/ad469783c0507bf7e4160cf131d2b73b494c52e8) + + Raise an exception for unparsable versions and aggregate visualization output in a list before printing. Add a verbose logging option to the `show_bump` command for detailed logging control. + +## 0.25.2 (2024-08-11) +[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.25.1...0.25.2) + +### Fixes + +- Fix JSON serialization. [d3f3022](https://github.com/callowayproject/bump-my-version/commit/d3f3022bd4c8b8d6e41fa7b5b6ccfc2aa6cf7878) + + Extended the default_encoder function to handle Path objects by converting them to their string representation. This ensures that Path objects can be properly serialized to JSON format. + +## 0.25.1 (2024-08-07) +[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.25.0...0.25.1) + +### Fixes + +- Fixes mypy pre-commit checking. [f7d0909](https://github.com/callowayproject/bump-my-version/commit/f7d0909deb8d0cf06607c4d51090ca23f7d92664) + +- Fixes repository path checks. [ff3f72a](https://github.com/callowayproject/bump-my-version/commit/ff3f72a9a922dfbb61eea9325fc9dba7c12c7f62) + + Checked for relative paths when determining if the file was part of the repo or not. +- Fixed test to use globs. [72f9841](https://github.com/callowayproject/bump-my-version/commit/72f9841a9628e26c6cf06518b0428d3990a08421) + +### Other + +- [pre-commit.ci] pre-commit autoupdate. [58cc73e](https://github.com/callowayproject/bump-my-version/commit/58cc73ed041f978fddd9f81e995901596f6ac722) + + **updates:** - [github.com/astral-sh/ruff-pre-commit: v0.5.5 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.5...v0.5.6) + + ## 0.25.0 (2024-08-06) [Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.24.3...0.25.0) diff --git a/bumpversion/__init__.py b/bumpversion/__init__.py index 8fb5c68d..a82c5653 100644 --- a/bumpversion/__init__.py +++ b/bumpversion/__init__.py @@ -1,3 +1,3 @@ """Top-level package for bump-my-version.""" -__version__: str = "0.25.0" +__version__: str = "0.25.3" diff --git a/bumpversion/cli.py b/bumpversion/cli.py index 16c1a963..9fafdee5 100644 --- a/bumpversion/cli.py +++ b/bumpversion/cli.py @@ -575,8 +575,17 @@ def sample_config(prompt: bool, destination: str) -> None: 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: +@click.option( + "-v", + "--verbose", + count=True, + required=False, + envvar="BUMPVERSION_VERBOSE", + help="Print verbose logging to stderr. Can specify several times for more verbosity.", +) +def show_bump(version: str, config_file: Optional[str], ascii: bool, verbose: int) -> None: """Show the possible versions resulting from the bump subcommand.""" + setup_logging(verbose) found_config_file = find_config_file(config_file) config = get_configuration(found_config_file) if not version: diff --git a/bumpversion/config/models.py b/bumpversion/config/models.py index 23c3af00..00e630ad 100644 --- a/bumpversion/config/models.py +++ b/bumpversion/config/models.py @@ -15,8 +15,8 @@ if TYPE_CHECKING: from bumpversion.scm import SCMInfo - from bumpversion.version_part import VersionConfig from bumpversion.versioning.models import VersionSpec + from bumpversion.versioning.version_config import VersionConfig logger = get_indented_logger(__name__) @@ -166,7 +166,7 @@ def files_to_modify(self) -> List[FileChange]: @property def version_config(self) -> "VersionConfig": """Return the version configuration.""" - from bumpversion.version_part import VersionConfig + from bumpversion.versioning.version_config import VersionConfig return VersionConfig(self.parse, self.serialize, self.search, self.replace, self.parts) diff --git a/bumpversion/files.py b/bumpversion/files.py index eeaa82df..98708da0 100644 --- a/bumpversion/files.py +++ b/bumpversion/files.py @@ -11,8 +11,8 @@ from bumpversion.exceptions import VersionNotFoundError from bumpversion.ui import get_indented_logger from bumpversion.utils import get_nested_value, set_nested_value -from bumpversion.version_part import VersionConfig from bumpversion.versioning.models import Version, VersionComponentSpec +from bumpversion.versioning.version_config import VersionConfig logger = get_indented_logger(__name__) diff --git a/bumpversion/show.py b/bumpversion/show.py index 14ac828a..99200387 100644 --- a/bumpversion/show.py +++ b/bumpversion/show.py @@ -2,6 +2,7 @@ import dataclasses from io import StringIO +from pathlib import Path from pprint import pprint from typing import Any, Optional @@ -39,6 +40,8 @@ def default_encoder(obj: Any) -> str: return str(obj) elif isinstance(obj, type): return obj.__name__ + elif isinstance(obj, Path): + return str(obj) raise TypeError(f"Object of type {type(obj), str(obj)} is not JSON serializable") print_info(json.dumps(value, sort_keys=True, indent=2, default=default_encoder)) diff --git a/bumpversion/version_part.py b/bumpversion/versioning/version_config.py similarity index 81% rename from bumpversion/version_part.py rename to bumpversion/versioning/version_config.py index e8b1ad6f..dfa87189 100644 --- a/bumpversion/version_part.py +++ b/bumpversion/versioning/version_config.py @@ -5,6 +5,7 @@ from click import UsageError +from bumpversion.exceptions import BumpVersionError from bumpversion.ui import get_indented_logger from bumpversion.utils import labels_for_format from bumpversion.versioning.models import Version, VersionComponentSpec, VersionSpec @@ -29,7 +30,7 @@ def __init__( try: self.parse_regex = re.compile(parse, re.VERBOSE) except re.error as e: - raise UsageError(f"--parse '{parse}' is not a valid regex.") from e + raise UsageError(f"'{parse}' is not a valid regex.") from e self.serialize_formats = serialize self.part_configs = part_configs or {} @@ -38,7 +39,7 @@ def __init__( self.search = search self.replace = replace - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no-coverage return f"" def __eq__(self, other: Any) -> bool: @@ -63,19 +64,25 @@ def order(self) -> List[str]: """ return labels_for_format(self.serialize_formats[0]) - def parse(self, version_string: Optional[str] = None) -> Optional[Version]: + def parse(self, version_string: Optional[str] = None, raise_error: bool = False) -> Optional[Version]: """ Parse a version string into a Version object. Args: version_string: Version string to parse + raise_error: Raise an exception if a version string is invalid Returns: A Version object representing the string. + + Raises: + BumpversionException: If a version string is invalid and raise_error is True. """ parsed = parse_version(version_string, self.parse_regex.pattern) - if not parsed: + if not parsed and raise_error: + raise BumpVersionError(f"Unable to parse version {version_string} using {self.parse_regex.pattern}") + elif not parsed: return None version = self.version_spec.create_version(parsed) diff --git a/bumpversion/visualize.py b/bumpversion/visualize.py index 6d7c6d34..eee92b5f 100644 --- a/bumpversion/visualize.py +++ b/bumpversion/visualize.py @@ -110,7 +110,7 @@ def filter_version_parts(config: Config) -> List[str]: 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 = config.version_config.parse(version_str, raise_error=True) version_parts = filter_version_parts(config) num_parts = len(version_parts) @@ -120,7 +120,7 @@ def visualize(config: Config, version_str: str, box_style: str = "light") -> Non 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) - + lines = [] for i, part in enumerate(version_parts): line = [version_lead] if i == 0 else [blank_lead] @@ -135,4 +135,5 @@ def visualize(config: Config, version_str: str, box_style: str = "light") -> Non 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)) + lines.append("".join(line)) + print_info("\n".join(lines)) diff --git a/pyproject.toml b/pyproject.toml index 6847a770..060fad88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -216,7 +216,7 @@ order-by-type = true convention = "google" [tool.bumpversion] -current_version = "0.25.1" +current_version = "0.25.3" commit = true commit_args = "--no-verify" tag = true diff --git a/tests/conftest.py b/tests/conftest.py index a7fcd29b..bf0198b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ def inside_dir(dirpath: Path) -> Generator: def get_config_data(overrides: dict) -> tuple: """Get the configuration, version_config and version.""" from bumpversion import config - from bumpversion.version_part import VersionConfig + from bumpversion.versioning.version_config import VersionConfig conf = config.get_configuration(config_file="missing", **overrides) version_config = VersionConfig(conf.parse, conf.serialize, conf.search, conf.replace, conf.parts) diff --git a/tests/test_bump.py b/tests/test_bump.py index a376af4c..fe17d6b0 100644 --- a/tests/test_bump.py +++ b/tests/test_bump.py @@ -283,7 +283,7 @@ def test_commit_and_tag_with_config_file(mock_context): def test_key_path_required_for_toml_change(tmp_path: Path, caplog): """If the key_path is not provided, the toml file should use standard search and replace.""" from bumpversion import config - from bumpversion.version_part import VersionConfig + from bumpversion.versioning.version_config import VersionConfig # Arrange config_path = tmp_path / "pyproject.toml" diff --git a/tests/test_configuredfile.py b/tests/test_configuredfile.py index 3ef2ca66..803cd97f 100644 --- a/tests/test_configuredfile.py +++ b/tests/test_configuredfile.py @@ -1,7 +1,7 @@ """Tests for the ConfiguredFile class.""" from bumpversion.files import ConfiguredFile, FileChange -from bumpversion.version_part import VersionConfig +from bumpversion.versioning.version_config import VersionConfig from bumpversion.versioning.models import VersionComponentSpec diff --git a/tests/test_files.py b/tests/test_files.py index 099c55e0..8159b5c0 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -17,7 +17,7 @@ from bumpversion.context import get_context from bumpversion.exceptions import VersionNotFoundError from bumpversion.ui import setup_logging, get_indented_logger -from bumpversion.version_part import VersionConfig +from bumpversion.versioning.version_config import VersionConfig from tests.conftest import get_config_data, inside_dir diff --git a/tests/test_version_part.py b/tests/test_version_part.py deleted file mode 100644 index f6092a46..00000000 --- a/tests/test_version_part.py +++ /dev/null @@ -1,86 +0,0 @@ -from pathlib import Path - -import pytest -from click import UsageError - -from bumpversion import exceptions -from bumpversion.context import get_context -from tests.conftest import get_config_data, inside_dir - - -def test_bump_version_missing_part(): - overrides = { - "current_version": "1.0.0", - } - conf, version_config, current_version = get_config_data(overrides) - with pytest.raises(exceptions.InvalidVersionPartError, match="No part named 'bugfix'"): - current_version.bump("bugfix") - - -def test_build_number_configuration(): - overrides = { - "current_version": "2.1.6-5123", - "parse": r"(?P\d+)\.(?P\d+)\.(?P\d+)\-(?P\d+)", - "serialize": ["{major}.{minor}.{patch}-{build}"], - "parts": { - "build": { - "independent": True, - } - }, - } - conf, version_config, current_version = get_config_data(overrides) - - build_version_bump = current_version.bump("build") - assert build_version_bump["build"].value == "5124" - assert build_version_bump["build"].is_independent - - major_version_bump = build_version_bump.bump("major") - assert major_version_bump["build"].value == "5124" - assert build_version_bump["build"].is_independent - assert major_version_bump["major"].value == "3" - - build_version_bump = major_version_bump.bump("build") - assert build_version_bump["build"].value == "5125" - assert build_version_bump["build"].is_independent - - -def test_serialize_with_distance_to_latest_tag(): - """Using ``distance_to_latest_tag`` in the serialization string outputs correctly.""" - from bumpversion.scm import Git, SCMInfo - - overrides = { - "current_version": "19.6.0", - "parse": r"(?P\d+)\.(?P\d+)\.(?P\d+).*", - "serialize": ["{major}.{minor}.{patch}-pre{distance_to_latest_tag}"], - } - conf, version_config, current_version = get_config_data(overrides) - conf.scm_info = SCMInfo( - tool=Git, commit_sha="1234123412341234", distance_to_latest_tag=3, current_version="19.6.0", dirty=False - ) - assert version_config.serialize(current_version, get_context(conf)) == "19.6.0-pre3" - - -def test_serialize_with_environment_var(): - import os - - os.environ["BUILD_NUMBER"] = "567" - overrides = { - "current_version": "2.3.4", - "parse": r"(?P\d+)\.(?P\d+)\.(?P\d+).*", - "serialize": ["{major}.{minor}.{patch}-pre{$BUILD_NUMBER}"], - } - conf, version_config, current_version = get_config_data(overrides) - assert version_config.serialize(current_version, get_context(conf)) == "2.3.4-pre567" - del os.environ["BUILD_NUMBER"] - - -def test_version_part_invalid_regex_exit(tmp_path: Path) -> None: - """A version part with an invalid regex should raise an exception.""" - # Arrange - overrides = { - "current_version": "12", - "parse": "*kittens*", - } - with inside_dir(tmp_path): - with pytest.raises(UsageError): - get_config_data(overrides) diff --git a/tests/test_versioning/test_models_version.py b/tests/test_versioning/test_models_version.py index a1046bef..afd4a4e4 100644 --- a/tests/test_versioning/test_models_version.py +++ b/tests/test_versioning/test_models_version.py @@ -2,6 +2,7 @@ from freezegun import freeze_time +from bumpversion import exceptions from bumpversion.versioning.models import VersionComponentSpec from bumpversion.versioning.models import VersionSpec import pytest @@ -132,6 +133,34 @@ def test_changes_component_and_dependents(self, semver_version_spec: VersionSpec assert major_version_str == "2.0.0.4.6" assert build_version_str == "1.2.3.5.6" + def test_invalid_component_raises_error(self, semver_version_spec: VersionSpec): + """If you bump a version with an invalid component name raises an error.""" + version1 = semver_version_spec.create_version( + {"major": "1", "minor": "2", "patch": "3", "build": "4", "auto": "5"} + ) + with pytest.raises(exceptions.InvalidVersionPartError, match="No part named 'bugfix'"): + version1.bump("bugfix") + + def test_independent_component_is_independent(self, semver_version_spec: VersionSpec): + """An independent component can only get bumped independently.""" + # Arrange + version1 = semver_version_spec.create_version( + {"major": "1", "minor": "2", "patch": "3", "build": "4", "auto": "5"} + ) + + # Act & Assert + build_version_bump = version1.bump("build") + assert build_version_bump["build"].value == "5" + assert build_version_bump["major"].value == "1" + + major_version_bump = build_version_bump.bump("major") + assert major_version_bump["build"].value == "5" + assert major_version_bump["major"].value == "2" + + build_version_bump2 = major_version_bump.bump("build") + assert build_version_bump2["build"].value == "6" + assert build_version_bump2["major"].value == "2" + class TestRequiredComponents: """Tests of the required_keys function.""" diff --git a/tests/test_versioning/test_version_config.py b/tests/test_versioning/test_version_config.py new file mode 100644 index 00000000..a5f3103b --- /dev/null +++ b/tests/test_versioning/test_version_config.py @@ -0,0 +1,79 @@ +from pathlib import Path + +import pytest +from click import UsageError + +from bumpversion import exceptions +from bumpversion.context import get_context +from bumpversion.versioning.version_config import VersionConfig +from tests.conftest import get_config_data + + +def test_invalid_parse_raises_error(): + """An invalid parse regex value raises an error.""" + with pytest.raises(exceptions.UsageError): + VersionConfig(r"(.+", ("{major}.{minor}.{patch}",), search="", replace="") + + +class TestParse: + """Tests for parsing a version.""" + + def test_cant_parse_returns_none(self): + """The default behavior when unable to parse a string is to return None.""" + # Assemble + overrides = { + "current_version": "19.6.0", + "parse": r"(?P\d+)\.(?P\d+)\.(?P\d+).*", + "serialize": ["{major}.{minor}.{patch}"], + } + _, version_config, current_version = get_config_data(overrides) + + # Act and Assert + assert version_config.parse("A.B.C") is None + + def test_cant_parse_raises_error_when_set(self): + """When `raise_error` is enabled, the inability to parse a string will raise an error.""" + # Assemble + overrides = { + "current_version": "19.6.0", + "parse": r"(?P\d+)\.(?P\d+)\.(?P\d+).*", + "serialize": ["{major}.{minor}.{patch}"], + } + _, version_config, current_version = get_config_data(overrides) + + # Act and Assert + with pytest.raises(exceptions.UsageError): + version_config.parse("A.B.C", raise_error=True) + + +class TestSerialize: + """Tests for the VersionConfig.serialize() method.""" + + def test_distance_to_latest_tag_in_pattern(self): + """Using ``distance_to_latest_tag`` in the serialization string outputs correctly.""" + from bumpversion.scm import Git, SCMInfo + + overrides = { + "current_version": "19.6.0", + "parse": r"(?P\d+)\.(?P\d+)\.(?P\d+).*", + "serialize": ["{major}.{minor}.{patch}-pre{distance_to_latest_tag}"], + } + conf, version_config, current_version = get_config_data(overrides) + conf.scm_info = SCMInfo( + tool=Git, commit_sha="1234123412341234", distance_to_latest_tag=3, current_version="19.6.0", dirty=False + ) + assert version_config.serialize(current_version, get_context(conf)) == "19.6.0-pre3" + + def test_environment_var_in_serialize_pattern(self): + """Environment variables are serialized correctly.""" + import os + + os.environ["BUILD_NUMBER"] = "567" + overrides = { + "current_version": "2.3.4", + "parse": r"(?P\d+)\.(?P\d+)\.(?P\d+).*", + "serialize": ["{major}.{minor}.{patch}-pre{$BUILD_NUMBER}"], + } + conf, version_config, current_version = get_config_data(overrides) + assert version_config.serialize(current_version, get_context(conf)) == "2.3.4-pre567" + del os.environ["BUILD_NUMBER"]