From a4c90b2fdcdf57a17242f67c850446b65a27470a Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sat, 9 Dec 2023 18:40:00 -0600 Subject: [PATCH] Refactored config file management Moved the INI format stuff into files_legacy.py --- bumpversion/bump.py | 3 +- bumpversion/config/files.py | 145 ++--------- bumpversion/config/files_legacy.py | 127 ++++++++++ tests/test_config/__init__.py | 0 .../test_files.py} | 226 +++++++----------- tests/test_config/test_files_legacy.py | 150 ++++++++++++ tests/test_config/test_init.py | 0 7 files changed, 386 insertions(+), 265 deletions(-) create mode 100644 bumpversion/config/files_legacy.py create mode 100644 tests/test_config/__init__.py rename tests/{test_config.py => test_config/test_files.py} (67%) create mode 100644 tests/test_config/test_files_legacy.py create mode 100644 tests/test_config/test_init.py diff --git a/bumpversion/bump.py b/bumpversion/bump.py index 595be314..34c7d91f 100644 --- a/bumpversion/bump.py +++ b/bumpversion/bump.py @@ -9,7 +9,8 @@ from bumpversion.version_part import Version from bumpversion.config import Config -from bumpversion.config.files import update_config_file, update_ini_config_file +from bumpversion.config.files import update_config_file +from bumpversion.config.files_legacy import update_ini_config_file from bumpversion.exceptions import ConfigurationError from bumpversion.utils import get_context, key_val_string diff --git a/bumpversion/config/files.py b/bumpversion/config/files.py index 124ddb92..257fc1c6 100644 --- a/bumpversion/config/files.py +++ b/bumpversion/config/files.py @@ -3,11 +3,10 @@ from __future__ import annotations import logging -import re -from difflib import context_diff from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, MutableMapping, Union +from bumpversion.config.files_legacy import read_ini_file from bumpversion.ui import print_warning if TYPE_CHECKING: # pragma: no-coverage @@ -17,10 +16,10 @@ logger = logging.getLogger(__name__) CONFIG_FILE_SEARCH_ORDER = ( - Path(".bumpversion.cfg"), - Path(".bumpversion.toml"), - Path("setup.cfg"), - Path("pyproject.toml"), + ".bumpversion.cfg", + ".bumpversion.toml", + "setup.cfg", + "pyproject.toml", ) @@ -37,7 +36,9 @@ def find_config_file(explicit_file: Union[str, Path, None] = None) -> Union[Path Returns: The configuration file path """ - search_paths = [Path(explicit_file)] if explicit_file else CONFIG_FILE_SEARCH_ORDER + search_paths = ( + [Path(explicit_file)] if explicit_file else [Path.cwd().joinpath(path) for path in CONFIG_FILE_SEARCH_ORDER] + ) return next( (cfg_file for cfg_file in search_paths if cfg_file.exists() and "bumpversion]" in cfg_file.read_text()), None, @@ -61,77 +62,21 @@ def read_config_file(config_file: Union[str, Path, None] = None) -> Dict[str, An logger.info("No configuration file found.") return {} - logger.info("Reading config file %s:", config_file) config_path = Path(config_file) + if not config_path.exists(): + logger.info("Configuration file not found: %s.", config_path) + return {} + + logger.info("Reading config file %s:", config_file) + if config_path.suffix == ".cfg": print_warning("The .cfg file format is deprecated. Please use .toml instead.") return read_ini_file(config_path) elif config_path.suffix == ".toml": return read_toml_file(config_path) - return {} - - -def read_ini_file(file_path: Path) -> Dict[str, Any]: # noqa: C901 - """ - Parse an INI file and return a dictionary of sections and their options. - - Args: - file_path: The path to the INI file. - - Returns: - dict: A dictionary of sections and their options. - """ - import configparser - - from bumpversion import autocast - - # Create a ConfigParser object and read the INI file - config_parser = configparser.RawConfigParser() - if file_path.name == "setup.cfg": - config_parser = configparser.ConfigParser() - - config_parser.read(file_path) - - # Create an empty dictionary to hold the parsed sections and options - bumpversion_options: Dict[str, Any] = {"files": [], "parts": {}} - - # Loop through each section in the INI file - for section_name in config_parser.sections(): - if not section_name.startswith("bumpversion"): - continue - - section_parts = section_name.split(":") - num_parts = len(section_parts) - options = {key: autocast.autocast_value(val) for key, val in config_parser.items(section_name)} - - if num_parts == 1: # bumpversion section - bumpversion_options.update(options) - serialize = bumpversion_options.get("serialize", []) - if "message" in bumpversion_options and isinstance(bumpversion_options["message"], list): - bumpversion_options["message"] = ",".join(bumpversion_options["message"]) - if not isinstance(serialize, list): - bumpversion_options["serialize"] = [serialize] - elif num_parts > 1 and section_parts[1].startswith("file"): - file_options = { - "filename": section_parts[2], - } - file_options.update(options) - if "replace" in file_options and isinstance(file_options["replace"], list): - file_options["replace"] = "\n".join(file_options["replace"]) - bumpversion_options["files"].append(file_options) - elif num_parts > 1 and section_parts[1].startswith("glob"): - file_options = { - "glob": section_parts[2], - } - file_options.update(options) - if "replace" in file_options and isinstance(file_options["replace"], list): - file_options["replace"] = "\n".join(file_options["replace"]) - bumpversion_options["files"].append(file_options) - elif num_parts > 1 and section_parts[1].startswith("part"): - bumpversion_options["parts"][section_parts[2]] = options - - # Return the dictionary of sections and options - return bumpversion_options + else: + logger.info("Unknown config file suffix: %s. Using defaults.", config_path.suffix) + return {} def read_toml_file(file_path: Path) -> Dict[str, Any]: @@ -180,7 +125,7 @@ def update_config_file( config_path = Path(config_file) if config_path.suffix != ".toml": - logger.info("Could not find the current version in the config file: %s.", config_path) + logger.info("You must have a `.toml` suffix to update the config file: %s.", config_path) return # TODO: Eventually this should be transformed into another default "files_to_modify" entry @@ -197,57 +142,3 @@ def update_config_file( updater = DataFileUpdater(datafile_config, config.version_config.part_configs) updater.update_file(current_version, new_version, context, dry_run) - - -def update_ini_config_file( - config_file: Union[str, Path], current_version: str, new_version: str, dry_run: bool = False -) -> None: - """ - Update the current_version key in the configuration file. - - Instead of parsing and re-writing the config file with new information, it will use - a regular expression to just replace the current_version value. The idea is it will - avoid unintentional changes (like formatting) to the config file. - - Args: - config_file: The configuration file to explicitly use. - current_version: The serialized current version. - new_version: The serialized new version. - dry_run: True if the update should be a dry run. - """ - cfg_current_version_regex = re.compile( - f"(?P\\[bumpversion]\n[^[]*current_version\\s*=\\s*)(?P{current_version})", - re.MULTILINE, - ) - - config_path = Path(config_file) - existing_config = config_path.read_text() - if config_path.suffix == ".cfg" and cfg_current_version_regex.search(existing_config): - sub_str = f"\\g{new_version}" - new_config = cfg_current_version_regex.sub(sub_str, existing_config) - else: - logger.info("Could not find the current version in the config file: %s.", config_path) - return - - logger.info( - "%s to config file %s:", - "Would write" if dry_run else "Writing", - config_path, - ) - - logger.info( - "\n".join( - list( - context_diff( - existing_config.splitlines(), - new_config.splitlines(), - fromfile=f"before {config_path}", - tofile=f"after {config_path}", - lineterm="", - ) - ) - ) - ) - - if not dry_run: - config_path.write_text(new_config) diff --git a/bumpversion/config/files_legacy.py b/bumpversion/config/files_legacy.py new file mode 100644 index 00000000..6a69b113 --- /dev/null +++ b/bumpversion/config/files_legacy.py @@ -0,0 +1,127 @@ +"""This module handles the legacy config file format.""" +from __future__ import annotations + +import logging +import re +from difflib import context_diff +from pathlib import Path +from typing import Any, Dict, Union + +logger = logging.getLogger(__name__) + + +def read_ini_file(file_path: Path) -> Dict[str, Any]: # noqa: C901 + """ + Parse an INI file and return a dictionary of sections and their options. + + Args: + file_path: The path to the INI file. + + Returns: + dict: A dictionary of sections and their options. + """ + import configparser + + from bumpversion import autocast + + # Create a ConfigParser object and read the INI file + config_parser = configparser.RawConfigParser() + if file_path.name == "setup.cfg": + config_parser = configparser.ConfigParser() + + config_parser.read(file_path) + + # Create an empty dictionary to hold the parsed sections and options + bumpversion_options: Dict[str, Any] = {"files": [], "parts": {}} + + # Loop through each section in the INI file + for section_name in config_parser.sections(): + if not section_name.startswith("bumpversion"): + continue + + section_parts = section_name.split(":") + num_parts = len(section_parts) + options = {key: autocast.autocast_value(val) for key, val in config_parser.items(section_name)} + + if num_parts == 1: # bumpversion section + bumpversion_options.update(options) + serialize = bumpversion_options.get("serialize", []) + if "message" in bumpversion_options and isinstance(bumpversion_options["message"], list): + bumpversion_options["message"] = ",".join(bumpversion_options["message"]) + if not isinstance(serialize, list): + bumpversion_options["serialize"] = [serialize] + elif num_parts > 1 and section_parts[1].startswith("file"): + file_options = { + "filename": section_parts[2], + } + file_options.update(options) + if "replace" in file_options and isinstance(file_options["replace"], list): + file_options["replace"] = "\n".join(file_options["replace"]) + bumpversion_options["files"].append(file_options) + elif num_parts > 1 and section_parts[1].startswith("glob"): + file_options = { + "glob": section_parts[2], + } + file_options.update(options) + if "replace" in file_options and isinstance(file_options["replace"], list): + file_options["replace"] = "\n".join(file_options["replace"]) + bumpversion_options["files"].append(file_options) + elif num_parts > 1 and section_parts[1].startswith("part"): + bumpversion_options["parts"][section_parts[2]] = options + + # Return the dictionary of sections and options + return bumpversion_options + + +def update_ini_config_file( + config_file: Union[str, Path], current_version: str, new_version: str, dry_run: bool = False +) -> None: + """ + Update the current_version key in the configuration file. + + Instead of parsing and re-writing the config file with new information, it will use + a regular expression to just replace the current_version value. The idea is it will + avoid unintentional changes (like formatting) to the config file. + + Args: + config_file: The configuration file to explicitly use. + current_version: The serialized current version. + new_version: The serialized new version. + dry_run: True if the update should be a dry run. + """ + cfg_current_version_regex = re.compile( + f"(?P\\[bumpversion]\n[^[]*current_version\\s*=\\s*)(?P{current_version})", + re.MULTILINE, + ) + + config_path = Path(config_file) + existing_config = config_path.read_text() + if config_path.suffix == ".cfg" and cfg_current_version_regex.search(existing_config): + sub_str = f"\\g{new_version}" + new_config = cfg_current_version_regex.sub(sub_str, existing_config) + else: + logger.info("Could not find the current version in the config file: %s.", config_path) + return + + logger.info( + "%s to config file %s:", + "Would write" if dry_run else "Writing", + config_path, + ) + + logger.info( + "\n".join( + list( + context_diff( + existing_config.splitlines(), + new_config.splitlines(), + fromfile=f"before {config_path}", + tofile=f"after {config_path}", + lineterm="", + ) + ) + ) + ) + + if not dry_run: + config_path.write_text(new_config) diff --git a/tests/test_config/__init__.py b/tests/test_config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_config.py b/tests/test_config/test_files.py similarity index 67% rename from tests/test_config.py rename to tests/test_config/test_files.py index 6516c7ce..13eda01e 100644 --- a/tests/test_config.py +++ b/tests/test_config/test_files.py @@ -2,49 +2,107 @@ import difflib import json from pathlib import Path -from textwrap import dedent -import pytest from click.testing import CliRunner, Result -from pytest import param +import pytest +from pytest import LogCaptureFixture, param -import bumpversion.config.files -import bumpversion.config.utils from bumpversion.utils import get_context from bumpversion import config +from bumpversion.config.files import find_config_file, CONFIG_FILE_SEARCH_ORDER +import bumpversion.config.utils + from tests.conftest import inside_dir, get_config_data -@pytest.fixture(params=[".bumpversion.cfg", "setup.cfg"]) -def cfg_file(request) -> str: - """Return both config-file styles ('.bumpversion.cfg', 'setup.cfg').""" - return request.param +class TestFindConfigFile: + """Tests for finding the config file.""" + class TestWhenExplictConfigFileIsPassed: + """Tests for when an explicit config file is passed.""" -@pytest.fixture( - params=[ - "file", - "file(suffix)", - "file (suffix with space)", - "file (suffix lacking closing paren", - ] -) -def cfg_file_keyword(request): - """Return multiple possible styles for the bumpversion:file keyword.""" - return request.param + def test_returns_path_to_existing_file(self, tmp_path: Path) -> None: + """If an explicit config file is passed, it should be returned.""" + cfg_file = tmp_path / "bump.toml" + cfg_file.write_text('[tool.bumpversion]\ncurrent_version = "1.0.0"') + assert find_config_file(cfg_file) == cfg_file + def test_returns_none_when_missing_file(self, tmp_path: Path) -> None: + """If an explicit config file is passed, it should be returned.""" + cfg_file = tmp_path / "bump.toml" + assert find_config_file(cfg_file) is None -@pytest.mark.parametrize( - ["conf_file", "expected_file"], - [ - param("basic_cfg.cfg", "basic_cfg_expected.json", id="ini basic cfg"), - ], -) -def test_read_ini_file(conf_file: str, expected_file: str, fixtures_path: Path) -> None: - """Parsing the config file should match the expected results.""" - result = bumpversion.config.files.read_ini_file(fixtures_path.joinpath(conf_file)) - expected = json.loads(fixtures_path.joinpath(expected_file).read_text()) - assert result == expected + class TestWhenNoExplicitConfigFileIsPassed: + """Tests for when no explicit config file is passed.""" + + @pytest.mark.parametrize( + ["cfg_file_name"], + (param(item, id=item) for item in CONFIG_FILE_SEARCH_ORDER), + ) + def test_returns_path_to_existing_default_file(self, tmp_path: Path, cfg_file_name: str) -> None: + """If no explicit config file is passed, it returns the path to an existing expected file.""" + cfg_file = tmp_path / cfg_file_name + cfg_file.write_text('[tool.bumpversion]\ncurrent_version = "1.0.0"') + with inside_dir(tmp_path): + assert find_config_file() == cfg_file + + def test_returns_none_when_missing_file(self, tmp_path: Path) -> None: + """If an explicit config file is passed, it should be returned.""" + with inside_dir(tmp_path): + assert find_config_file() is None + + def test_returns_path_to_existing_file_in_correct_order(self, tmp_path: Path) -> None: + """If no explicit config file is passed, it returns the path to an existing expected file.""" + expected_order = list(CONFIG_FILE_SEARCH_ORDER)[:] # make a copy so we can mutate it + cfg_file_paths = [tmp_path / cfg_file_name for cfg_file_name in expected_order] + for cfg_file in cfg_file_paths: # create all the files + cfg_file.write_text('[tool.bumpversion]\ncurrent_version = "1.0.0"') + + with inside_dir(tmp_path): + while expected_order: + expected_file = expected_order.pop(0) # the top item in the list is the next expected file + expected_path = tmp_path / expected_file + assert find_config_file() == expected_path + expected_path.unlink() # remove the file so it doesn't get found again + + +class TestReadConfigFile: + """Tests for reading the config file.""" + + class TestWhenExplictConfigFileIsPassed: + def test_returns_empty_dict_when_missing_file(self, tmp_path: Path, caplog: LogCaptureFixture) -> None: + """If an explicit config file is passed and doesn't exist, it returns an empty dict.""" + cfg_file = tmp_path / "bump.toml" + assert config.read_config_file(cfg_file) == {} + assert "Configuration file not found" in caplog.text + + def test_returns_dict_of_cfg_file(self, fixtures_path: Path) -> None: + """Files with a .cfg suffix is parsed into a dict and returned.""" + cfg_file = fixtures_path / "basic_cfg.cfg" + expected = json.loads(fixtures_path.joinpath("basic_cfg_expected.json").read_text()) + assert config.read_config_file(cfg_file) == expected + + def test_returns_dict_of_toml_file(self, fixtures_path: Path) -> None: + """Files with a .toml suffix is parsed into a dict and returned.""" + cfg_file = fixtures_path / "basic_cfg.toml" + expected = json.loads(fixtures_path.joinpath("basic_cfg_expected.json").read_text()) + assert config.read_config_file(cfg_file) == expected + + def test_returns_empty_dict_with_unknown_suffix(self, tmp_path: Path, caplog: LogCaptureFixture) -> None: + """Files with an unknown suffix return an empty dict.""" + cfg_file = tmp_path / "basic_cfg.unknown" + cfg_file.write_text('[tool.bumpversion]\ncurrent_version = "1.0.0"') + with inside_dir(tmp_path): + assert config.read_config_file(cfg_file) == {} + assert "Unknown config file suffix" in caplog.text + + class TestWhenNoConfigFileIsPassed: + """Tests for when no explicit config file is passed.""" + + def test_returns_empty_dict(self, caplog: LogCaptureFixture) -> None: + """If no explicit config file is passed, it returns an empty dict.""" + assert config.read_config_file() == {} + assert "No configuration file found." in caplog.text @pytest.mark.parametrize( @@ -60,61 +118,6 @@ def test_read_toml_file(conf_file: str, expected_file: str, fixtures_path: Path) assert result == expected -def test_independent_falsy_value_in_config_does_not_bump_independently(tmp_path: Path): - # tmp_path.joinpath("VERSION").write_text("2.1.0-5123") - config_file = tmp_path.joinpath(".bumpversion.cfg") - config_file.write_text( - dedent( - r""" - [bumpversion] - current_version: 2.1.0-5123 - parse = (?P\d+)\.(?P\d+)\.(?P\d+)\-(?P\d+) - serialize = {major}.{minor}.{patch}-{build} - - [bumpversion:file:VERSION] - - [bumpversion:part:build] - independent = 0 - """ - ) - ) - - conf = config.get_configuration(config_file) - assert conf.parts["build"].independent is False - - -def test_correct_interpolation_for_setup_cfg_files(tmp_path: Path, fixtures_path: Path): - """ - Reported here: https://github.com/c4urself/bump2version/issues/21. - """ - test_fixtures_path = fixtures_path.joinpath("interpolation") - setup_cfg = config.get_configuration(test_fixtures_path.joinpath("setup.cfg")) - bumpversion_cfg = config.get_configuration(test_fixtures_path.joinpath(".bumpversion.cfg")) - pyproject_toml = config.get_configuration(test_fixtures_path.joinpath("pyproject.toml")) - - assert setup_cfg.replace == "{now:%m-%d-%Y} v. {new_version}" - assert bumpversion_cfg.replace == "{now:%m-%d-%Y} v. {new_version}" - assert pyproject_toml.replace == "{now:%m-%d-%Y} v. {new_version}" - - -def test_file_keyword_with_suffix_is_accepted(tmp_path: Path, cfg_file: str, cfg_file_keyword: str): - cfg_file_path = tmp_path / cfg_file - cfg_file_path.write_text( - "[bumpversion]\n" - "current_version = 0.10.2\n" - "new_version = 0.10.3\n" - "[bumpversion:file (foobar):file2]\n" - "search = version {current_version}\n" - "replace = version {new_version}\n" - f"[bumpversion:{cfg_file_keyword}:file2]\n" - "search = The current version is {current_version}\n" - "replace = The current version is {new_version}\n" - ) - setup_cfg = config.get_configuration(cfg_file_path) - assert len(setup_cfg.files) == 2 - assert all(f.filename == "file2" for f in setup_cfg.files) - - def test_multiple_config_files(tmp_path: Path): """If there are multiple config files, the first one with content wins.""" setup_cfg = tmp_path / "setup.cfg" @@ -140,32 +143,6 @@ def test_multiple_config_files(tmp_path: Path): assert cfg.serialize == ["{major}.{minor}.{patch}-{release}", "{major}.{minor}.{patch}"] -def test_utf8_message_from_config_file(tmp_path: Path, cfg_file): - cfg_path = tmp_path / cfg_file - initial_config = ( - "[bumpversion]\n" - "current_version = 500.0.0\n" - "commit = True\n" - "message = Nová verze: {current_version} ☃, {new_version} ☀\n" - ) - cfg_path.write_bytes(initial_config.encode("utf-8")) - - with inside_dir(tmp_path): - cfg = config.get_configuration(cfg_path) - - assert cfg.message == "Nová verze: {current_version} ☃, {new_version} ☀" - - -CFG_EXPECTED_DIFF = ( - "*** \n" - "--- \n" - "***************\n" - "*** 11 ****\n" - "! current_version = 1.0.0\n" - "--- 11 ----\n" - "! current_version = 1.0.1\n" -) - TOML_EXPECTED_DIFF = ( "*** \n" "--- \n" @@ -205,31 +182,6 @@ def test_update_config_file(tmp_path: Path, cfg_file_name: str, fixtures_path: P assert "".join(difference) == expected_diff -@pytest.mark.parametrize( - [ - "cfg_file_name", - ], - [ - (".bumpversion.cfg",), - ("setup.cfg",), - ], -) -def test_update_ini_config_file(tmp_path: Path, cfg_file_name: str, fixtures_path: Path) -> None: - """ - Make sure only the version string is updated in the config file. - """ - expected_diff = CFG_EXPECTED_DIFF - cfg_path = tmp_path / cfg_file_name - orig_path = fixtures_path / f"basic_cfg{cfg_path.suffix}" - cfg_path.write_text(orig_path.read_text()) - original_content = orig_path.read_text().splitlines(keepends=True) - - bumpversion.config.files.update_ini_config_file(cfg_path, "1.0.0", "1.0.1") - new_content = cfg_path.read_text().splitlines(keepends=True) - difference = difflib.context_diff(original_content, new_content, n=0) - assert "".join(difference) == expected_diff - - def test_pep440_config(git_repo: Path, fixtures_path: Path): """ Check the PEP440 config file. diff --git a/tests/test_config/test_files_legacy.py b/tests/test_config/test_files_legacy.py new file mode 100644 index 00000000..33c2a4fd --- /dev/null +++ b/tests/test_config/test_files_legacy.py @@ -0,0 +1,150 @@ +import difflib +import json +from pathlib import Path +from textwrap import dedent + +import pytest +from _pytest.mark import param + +from bumpversion.config.files_legacy import read_ini_file, update_ini_config_file +from bumpversion import config +from ..conftest import inside_dir + + +@pytest.fixture( + params=[ + "file", + "file(suffix)", + "file (suffix with space)", + "file (suffix lacking closing paren", + ] +) +def cfg_file_keyword(request): + """Return multiple possible styles for the bumpversion:file keyword.""" + return request.param + + +@pytest.fixture(params=[".bumpversion.cfg", "setup.cfg"]) +def cfg_file(request) -> str: + """Return both config-file styles ('.bumpversion.cfg', 'setup.cfg').""" + return request.param + + +@pytest.mark.parametrize( + ["conf_file", "expected_file"], + [ + param("basic_cfg.cfg", "basic_cfg_expected.json", id="ini basic cfg"), + ], +) +def test_read_ini_file(conf_file: str, expected_file: str, fixtures_path: Path) -> None: + """Parsing the config file should match the expected results.""" + result = read_ini_file(fixtures_path.joinpath(conf_file)) + expected = json.loads(fixtures_path.joinpath(expected_file).read_text()) + assert result == expected + + +def test_independent_falsy_value_in_config_does_not_bump_independently(tmp_path: Path): + # tmp_path.joinpath("VERSION").write_text("2.1.0-5123") + config_file = tmp_path.joinpath(".bumpversion.cfg") + config_file.write_text( + dedent( + r""" + [bumpversion] + current_version: 2.1.0-5123 + parse = (?P\d+)\.(?P\d+)\.(?P\d+)\-(?P\d+) + serialize = {major}.{minor}.{patch}-{build} + + [bumpversion:file:VERSION] + + [bumpversion:part:build] + independent = 0 + """ + ) + ) + + conf = config.get_configuration(config_file) + assert conf.parts["build"].independent is False + + +def test_correct_interpolation_for_setup_cfg_files(tmp_path: Path, fixtures_path: Path): + """ + Reported here: https://github.com/c4urself/bump2version/issues/21. + """ + test_fixtures_path = fixtures_path.joinpath("interpolation") + setup_cfg = config.get_configuration(test_fixtures_path.joinpath("setup.cfg")) + bumpversion_cfg = config.get_configuration(test_fixtures_path.joinpath(".bumpversion.cfg")) + pyproject_toml = config.get_configuration(test_fixtures_path.joinpath("pyproject.toml")) + + assert setup_cfg.replace == "{now:%m-%d-%Y} v. {new_version}" + assert bumpversion_cfg.replace == "{now:%m-%d-%Y} v. {new_version}" + assert pyproject_toml.replace == "{now:%m-%d-%Y} v. {new_version}" + + +CFG_EXPECTED_DIFF = ( + "*** \n" + "--- \n" + "***************\n" + "*** 11 ****\n" + "! current_version = 1.0.0\n" + "--- 11 ----\n" + "! current_version = 1.0.1\n" +) + + +@pytest.mark.parametrize( + [ + "cfg_file_name", + ], + [ + (".bumpversion.cfg",), + ("setup.cfg",), + ], +) +def test_update_ini_config_file(tmp_path: Path, cfg_file_name: str, fixtures_path: Path) -> None: + """ + Make sure only the version string is updated in the config file. + """ + expected_diff = CFG_EXPECTED_DIFF + cfg_path = tmp_path / cfg_file_name + orig_path = fixtures_path / f"basic_cfg{cfg_path.suffix}" + cfg_path.write_text(orig_path.read_text()) + original_content = orig_path.read_text().splitlines(keepends=True) + + update_ini_config_file(cfg_path, "1.0.0", "1.0.1") + new_content = cfg_path.read_text().splitlines(keepends=True) + difference = difflib.context_diff(original_content, new_content, n=0) + assert "".join(difference) == expected_diff + + +def test_file_keyword_with_suffix_is_accepted(tmp_path: Path, cfg_file: str, cfg_file_keyword: str): + cfg_file_path = tmp_path / cfg_file + cfg_file_path.write_text( + "[bumpversion]\n" + "current_version = 0.10.2\n" + "new_version = 0.10.3\n" + "[bumpversion:file (foobar):file2]\n" + "search = version {current_version}\n" + "replace = version {new_version}\n" + f"[bumpversion:{cfg_file_keyword}:file2]\n" + "search = The current version is {current_version}\n" + "replace = The current version is {new_version}\n" + ) + setup_cfg = config.get_configuration(cfg_file_path) + assert len(setup_cfg.files) == 2 + assert all(f.filename == "file2" for f in setup_cfg.files) + + +def test_utf8_message_from_config_file(tmp_path: Path, cfg_file): + cfg_path = tmp_path / cfg_file + initial_config = ( + "[bumpversion]\n" + "current_version = 500.0.0\n" + "commit = True\n" + "message = Nová verze: {current_version} ☃, {new_version} ☀\n" + ) + cfg_path.write_bytes(initial_config.encode("utf-8")) + + with inside_dir(tmp_path): + cfg = config.get_configuration(cfg_path) + + assert cfg.message == "Nová verze: {current_version} ☃, {new_version} ☀" diff --git a/tests/test_config/test_init.py b/tests/test_config/test_init.py new file mode 100644 index 00000000..e69de29b