diff --git a/.changelog-config.yaml b/.changelog-config.yaml index 838db4e9..94e3b9dd 100644 --- a/.changelog-config.yaml +++ b/.changelog-config.yaml @@ -80,11 +80,11 @@ commit_classifiers: - action: SummaryRegexMatch category: Updates kwargs: - pattern: (?i)^(?:update|change|rename|remove|delete|improve|refactor|chg|modif)[^\n]*$ + pattern: (?i)^(?:update|change|rename|remove|delete|improve|chg|modif)[^\n]*$ - action: SummaryRegexMatch category: Fixes kwargs: - pattern: (?i)^(?:fix)[^\n]*$ + pattern: (?i)^(?:refactor|fix)[^\n]*$ - action: category: Other diff --git a/.github/actions/setup-python-and-git/action.yaml b/.github/actions/setup-python-and-git/action.yaml new file mode 100644 index 00000000..2471ab09 --- /dev/null +++ b/.github/actions/setup-python-and-git/action.yaml @@ -0,0 +1,23 @@ +name: checkout-and-setup-python +description: 'Checkout the repository and setup Python' +inputs: + python-version: + description: 'Python version to use' + required: false + default: '3.11' +runs: + using: 'composite' + steps: + - uses: actions/setup-python@v4 + name: Setup Python + with: + python-version: ${{ inputs.python-version }} + cache: 'pip' # caching pip dependencies + + - name: Git check + run: | + git config --global user.email "bump-my-version@github.actions" + git config --global user.name "Testing Git" + git --version + git config --list + shell: bash diff --git a/.github/workflows/bumpversion-pr.yaml b/.github/workflows/bumpversion-pr.yaml index 51856910..c85ad0c8 100644 --- a/.github/workflows/bumpversion-pr.yaml +++ b/.github/workflows/bumpversion-pr.yaml @@ -22,7 +22,7 @@ jobs: - name: Install requirements run: | - python -m pip install . bump-my-version + python -m pip install generate-changelog bump-my-version - name: Get the release hint id: generate-changelog diff --git a/CHANGELOG.md b/CHANGELOG.md index 647ef4aa..71b364f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -## Unreleased (2023-12-06) +## Unreleased (2023-12-09) +[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.13.0...HEAD) + + +## 0.13.0 (2023-12-06) [Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.12.0...0.13.0) ### Fixes diff --git a/bumpversion/bump.py b/bumpversion/bump.py index 595be314..62fe2c13 100644 --- a/bumpversion/bump.py +++ b/bumpversion/bump.py @@ -1,5 +1,4 @@ """Version changing methods.""" -import logging import shlex from pathlib import Path from typing import TYPE_CHECKING, ChainMap, List, Optional @@ -9,11 +8,13 @@ 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.ui import get_indented_logger from bumpversion.utils import get_context, key_val_string -logger = logging.getLogger("bumpversion") +logger = get_indented_logger(__name__) def get_next_version( @@ -35,14 +36,18 @@ def get_next_version( ConfigurationError: If it can't generate the next version. """ if new_version: + logger.info("Attempting to set new version '%s'", new_version) + logger.indent() next_version = config.version_config.parse(new_version) elif version_part: logger.info("Attempting to increment part '%s'", version_part) + logger.indent() next_version = current_version.bump(version_part, config.version_config.order) else: raise ConfigurationError("Unable to get the next version.") logger.info("Values are now: %s", key_val_string(next_version.values)) + logger.dedent() return next_version @@ -65,8 +70,13 @@ def do_bump( """ from bumpversion.files import modify_files, resolve_file_config + logger.indent() + ctx = get_context(config) + logger.info("Parsing current version '%s'", config.current_version) + logger.indent() version = config.version_config.parse(config.current_version) + logger.dedent() next_version = get_next_version(version, config, version_part, new_version) next_version_str = config.version_config.serialize(next_version, ctx) logger.info("New version will be '%s'", next_version_str) @@ -75,6 +85,8 @@ def do_bump( logger.info("Version is already '%s'", next_version_str) return + logger.dedent() + if dry_run: logger.info("Dry run active, won't touch any files.") @@ -90,6 +102,7 @@ def do_bump( ctx = get_context(config, version, next_version) ctx["new_version"] = next_version_str commit_and_tag(config, config_file, configured_files, ctx, dry_run) + logger.info("Done.") def commit_and_tag( @@ -114,7 +127,7 @@ def commit_and_tag( extra_args = shlex.split(config.commit_args) if config.commit_args else [] - commit_files = {f.path for f in configured_files} + commit_files = {f.file_change.filename for f in configured_files} if config_file: commit_files |= {str(config_file)} diff --git a/bumpversion/cli.py b/bumpversion/cli.py index 6a82c5b2..7b9cc86e 100644 --- a/bumpversion/cli.py +++ b/bumpversion/cli.py @@ -1,5 +1,4 @@ """bump-my-version Command line interface.""" -import logging from typing import List, Optional import rich_click as click @@ -12,10 +11,10 @@ from bumpversion.config.files import find_config_file from bumpversion.files import ConfiguredFile, modify_files from bumpversion.show import do_show, log_list -from bumpversion.ui import print_warning, setup_logging +from bumpversion.ui import get_indented_logger, print_warning, setup_logging from bumpversion.utils import get_context, get_overrides -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) @click.group( @@ -307,6 +306,7 @@ def bump( config.add_files(files) config.included_paths = files + logger.dedent() do_bump(version_part, new_version, config, found_config_file, dry_run) diff --git a/bumpversion/config/__init__.py b/bumpversion/config/__init__.py index 21cbda1d..12de9b7e 100644 --- a/bumpversion/config/__init__.py +++ b/bumpversion/config/__init__.py @@ -1,17 +1,17 @@ """Configuration management.""" from __future__ import annotations -import logging from typing import TYPE_CHECKING, Union from bumpversion.config.files import read_config_file from bumpversion.config.models import Config from bumpversion.exceptions import ConfigurationError +from bumpversion.ui import get_indented_logger if TYPE_CHECKING: # pragma: no-coverage from pathlib import Path -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) DEFAULTS = { "current_version": None, @@ -49,6 +49,9 @@ def get_configuration(config_file: Union[str, Path, None] = None, **overrides) - from bumpversion.config.utils import get_all_file_configs, get_all_part_configs from bumpversion.scm import SCMInfo, SourceCodeManager, get_scm_info # noqa: F401 + logger.info("Reading configuration") + logger.indent() + config_dict = DEFAULTS.copy() parsed_config = read_config_file(config_file) if config_file else {} @@ -75,6 +78,8 @@ def get_configuration(config_file: Union[str, Path, None] = None, **overrides) - # Update and verify the current_version config.current_version = check_current_version(config) + logger.dedent() + return config diff --git a/bumpversion/config/files.py b/bumpversion/config/files.py index 124ddb92..c8287d7a 100644 --- a/bumpversion/config/files.py +++ b/bumpversion/config/files.py @@ -2,25 +2,23 @@ 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.ui import print_warning +from bumpversion.config.files_legacy import read_ini_file +from bumpversion.ui import get_indented_logger, print_warning if TYPE_CHECKING: # pragma: no-coverage from bumpversion.config.models import Config from bumpversion.version_part import Version -logger = logging.getLogger(__name__) +logger = get_indented_logger(__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 +35,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 +61,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]: @@ -171,20 +115,22 @@ def update_config_file( context: The context to use for serialization. dry_run: True if the update should be a dry run. """ - from bumpversion.config.models import FileConfig + from bumpversion.config.models import FileChange from bumpversion.files import DataFileUpdater if not config_file: - logger.info("No configuration file found to update.") + logger.info("\n%sNo configuration file found to update.", logger.indent_str) return - + else: + logger.info("\n%sProcessing config file: %s", logger.indent_str, config_file) + logger.indent() 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 - datafile_config = FileConfig( + datafile_config = FileChange( filename=str(config_path), key_path="tool.bumpversion.current_version", search=config.search, @@ -197,57 +143,4 @@ 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) + logger.dedent() diff --git a/bumpversion/config/files_legacy.py b/bumpversion/config/files_legacy.py new file mode 100644 index 00000000..7dcaba91 --- /dev/null +++ b/bumpversion/config/files_legacy.py @@ -0,0 +1,128 @@ +"""This module handles the legacy config file format.""" +from __future__ import annotations + +import re +from difflib import context_diff +from pathlib import Path +from typing import Any, Dict, Union + +from bumpversion.ui import get_indented_logger + +logger = get_indented_logger(__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/bumpversion/config/models.py b/bumpversion/config/models.py index efc88832..a69ff0ef 100644 --- a/bumpversion/config/models.py +++ b/bumpversion/config/models.py @@ -1,15 +1,22 @@ """Bump My Version configuration models.""" from __future__ import annotations -from typing import TYPE_CHECKING, Dict, List, Optional, Union +import re +from collections import defaultdict +from itertools import chain +from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Tuple, Union from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict +from bumpversion.ui import get_indented_logger + if TYPE_CHECKING: from bumpversion.scm import SCMInfo from bumpversion.version_part import VersionConfig +logger = get_indented_logger(__name__) + class VersionPartConfig(BaseModel): """Configuration of a part of the version.""" @@ -21,11 +28,11 @@ class VersionPartConfig(BaseModel): independent: bool = False -class FileConfig(BaseModel): - """Search and replace file config.""" +class FileChange(BaseModel): + """A change to make to a file.""" parse: str - serialize: List[str] + serialize: tuple search: str replace: str regex: bool @@ -34,13 +41,52 @@ class FileConfig(BaseModel): glob: Optional[str] = None # Conflicts with filename. If both are specified, glob wins key_path: Optional[str] = None # If specified, and has an appropriate extension, will be treated as a data file + def __hash__(self): + """Return a hash of the model.""" + return hash(tuple(sorted(self.model_dump().items()))) + + def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]: + """ + Render the search pattern and return the compiled regex pattern and the raw pattern. + + Args: + context: The context to use for rendering the search pattern + + Returns: + A tuple of the compiled regex pattern and the raw pattern as a string. + """ + logger.debug("Rendering search pattern with context") + logger.indent() + # the default search pattern is escaped, so we can still use it in a regex + raw_pattern = self.search.format(**context) + default = re.compile(re.escape(raw_pattern), re.MULTILINE | re.DOTALL) + if not self.regex: + logger.debug("No RegEx flag detected. Searching for the default pattern: '%s'", default.pattern) + logger.dedent() + return default, raw_pattern + + re_context = {key: re.escape(str(value)) for key, value in context.items()} + regex_pattern = self.search.format(**re_context) + try: + search_for_re = re.compile(regex_pattern, re.MULTILINE | re.DOTALL) + logger.debug("Searching for the regex: '%s'", search_for_re.pattern) + logger.dedent() + return search_for_re, raw_pattern + except re.error as e: + logger.error("Invalid regex '%s': %s.", default, e) + + logger.debug("Invalid regex. Searching for the default pattern: '%s'", raw_pattern) + logger.dedent() + + return default, raw_pattern + class Config(BaseSettings): """Bump Version configuration.""" current_version: Optional[str] parse: str - serialize: List[str] = Field(min_length=1) + serialize: tuple = Field(min_length=1) search: str replace: str regex: bool @@ -55,19 +101,19 @@ class Config(BaseSettings): commit_args: Optional[str] scm_info: Optional["SCMInfo"] parts: Dict[str, VersionPartConfig] - files: List[FileConfig] + files: List[FileChange] = Field(default_factory=list) included_paths: List[str] = Field(default_factory=list) excluded_paths: List[str] = Field(default_factory=list) model_config = SettingsConfigDict(env_prefix="bumpversion_") + _resolved_filemap: Optional[Dict[str, List[FileChange]]] = None def add_files(self, filename: Union[str, List[str]]) -> None: """Add a filename to the list of files.""" filenames = [filename] if isinstance(filename, str) else filename + files = set(self.files) for name in filenames: - if name in self.resolved_filemap: - continue - self.files.append( - FileConfig( + files.add( + FileChange( filename=name, glob=None, key_path=None, @@ -79,12 +125,22 @@ def add_files(self, filename: Union[str, List[str]]) -> None: ignore_missing_version=self.ignore_missing_version, ) ) + self.files = list(files) + + self._resolved_filemap = None @property - def resolved_filemap(self) -> Dict[str, FileConfig]: + def resolved_filemap(self) -> Dict[str, List[FileChange]]: + """Return the cached resolved filemap.""" + if self._resolved_filemap is None: + self._resolved_filemap = self._resolve_filemap() + return self._resolved_filemap + + def _resolve_filemap(self) -> Dict[str, List[FileChange]]: """Return a map of filenames to file configs, expanding any globs.""" from bumpversion.config.utils import resolve_glob_files + output = defaultdict(list) new_files = [] for file_cfg in self.files: if file_cfg.glob: @@ -92,18 +148,20 @@ def resolved_filemap(self) -> Dict[str, FileConfig]: else: new_files.append(file_cfg) - return {file_cfg.filename: file_cfg for file_cfg in new_files} + for file_cfg in new_files: + output[file_cfg.filename].append(file_cfg) + return output @property - def files_to_modify(self) -> List[FileConfig]: + def files_to_modify(self) -> List[FileChange]: """Return a list of files to modify.""" - files_not_excluded = [ - file_cfg.filename - for file_cfg in self.resolved_filemap.values() - if file_cfg.filename not in self.excluded_paths - ] + files_not_excluded = [filename for filename in self.resolved_filemap if filename not in self.excluded_paths] inclusion_set = set(self.included_paths) | set(files_not_excluded) - return [file_cfg for file_cfg in self.resolved_filemap.values() if file_cfg.filename in inclusion_set] + return list( + chain.from_iterable( + file_cfg_list for key, file_cfg_list in self.resolved_filemap.items() if key in inclusion_set + ) + ) @property def version_config(self) -> "VersionConfig": diff --git a/bumpversion/config/utils.py b/bumpversion/config/utils.py index a0b41e63..61fb1b7d 100644 --- a/bumpversion/config/utils.py +++ b/bumpversion/config/utils.py @@ -5,11 +5,11 @@ import itertools from typing import Dict, List -from bumpversion.config.models import FileConfig, VersionPartConfig +from bumpversion.config.models import FileChange, VersionPartConfig from bumpversion.utils import labels_for_format -def get_all_file_configs(config_dict: dict) -> List[FileConfig]: +def get_all_file_configs(config_dict: dict) -> List[FileChange]: """Make sure all version parts are included.""" defaults = { "parse": config_dict["parse"], @@ -22,7 +22,7 @@ def get_all_file_configs(config_dict: dict) -> List[FileConfig]: files = [{k: v for k, v in filecfg.items() if v is not None} for filecfg in config_dict["files"]] for f in files: f.update({k: v for k, v in defaults.items() if k not in f}) - return [FileConfig(**f) for f in files] + return [FileChange(**f) for f in files] def get_all_part_configs(config_dict: dict) -> Dict[str, VersionPartConfig]: @@ -36,7 +36,7 @@ def get_all_part_configs(config_dict: dict) -> Dict[str, VersionPartConfig]: } -def resolve_glob_files(file_cfg: FileConfig) -> List[FileConfig]: +def resolve_glob_files(file_cfg: FileChange) -> List[FileChange]: """ Return a list of file configurations that match the glob pattern. diff --git a/bumpversion/files.py b/bumpversion/files.py index 855e786f..efccdd3a 100644 --- a/bumpversion/files.py +++ b/bumpversion/files.py @@ -1,48 +1,16 @@ """Methods for changing files.""" -import logging import re from copy import deepcopy from difflib import context_diff from pathlib import Path -from typing import Dict, List, MutableMapping, Optional, Tuple +from typing import Dict, List, MutableMapping, Optional -from bumpversion.config.models import FileConfig, VersionPartConfig +from bumpversion.config.models import FileChange, VersionPartConfig from bumpversion.exceptions import VersionNotFoundError +from bumpversion.ui import get_indented_logger from bumpversion.version_part import Version, VersionConfig -logger = logging.getLogger(__name__) - - -def get_search_pattern(search_str: str, context: MutableMapping, use_regex: bool = False) -> Tuple[re.Pattern, str]: - """ - Render the search pattern and return the compiled regex pattern and the raw pattern. - - Args: - search_str: A string containing the search pattern as a format string - context: The context to use for rendering the search pattern - use_regex: If True, the search pattern is treated as a regex pattern - - Returns: - A tuple of the compiled regex pattern and the raw pattern as a string. - """ - # the default search pattern is escaped, so we can still use it in a regex - raw_pattern = search_str.format(**context) - default = re.compile(re.escape(raw_pattern), re.MULTILINE | re.DOTALL) - if not use_regex: - logger.debug("No RegEx flag detected. Searching for the default pattern: '%s'", default.pattern) - return default, raw_pattern - - re_context = {key: re.escape(str(value)) for key, value in context.items()} - regex_pattern = search_str.format(**re_context) - try: - search_for_re = re.compile(regex_pattern, re.MULTILINE | re.DOTALL) - logger.debug("Searching for the regex: '%s'", search_for_re.pattern) - return search_for_re, raw_pattern - except re.error as e: - logger.error("Invalid regex '%s': %s.", default, e) - - logger.debug("Invalid regex. Searching for the default pattern: '%s'", raw_pattern) - return default, raw_pattern +logger = get_indented_logger(__name__) def contains_pattern(search: re.Pattern, contents: str) -> bool: @@ -54,7 +22,7 @@ def contains_pattern(search: re.Pattern, contents: str) -> bool: line_no = contents.count("\n", 0, m.start(0)) + 1 logger.info( "Found '%s' at line %s: %s", - search, + search.pattern, line_no, m.string[m.start() : m.end(0)], ) @@ -74,8 +42,11 @@ def log_changes(file_path: str, file_content_before: str, file_content_after: st """ if file_content_before != file_content_after: logger.info("%s file %s:", "Would change" if dry_run else "Changing", file_path) + logger.indent() + indent_str = logger.indent_str + logger.info( - "\n".join( + f"\n{indent_str}".join( list( context_diff( file_content_before.splitlines(), @@ -85,8 +56,9 @@ def log_changes(file_path: str, file_content_before: str, file_content_after: st lineterm="", ) ) - ) + ), ) + logger.dedent() else: logger.info("%s file %s", "Would not change" if dry_run else "Not changing", file_path) @@ -96,26 +68,34 @@ class ConfiguredFile: def __init__( self, - file_cfg: FileConfig, + file_change: FileChange, version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None, ) -> None: - self.path = file_cfg.filename - self.parse = file_cfg.parse or version_config.parse_regex.pattern - self.serialize = file_cfg.serialize or version_config.serialize_formats - self.search = search or file_cfg.search or version_config.search - self.replace = replace or file_cfg.replace or version_config.replace - self.regex = file_cfg.regex or False - self.ignore_missing_version = file_cfg.ignore_missing_version or False + self.file_change = FileChange( + parse=file_change.parse or version_config.parse_regex.pattern, + serialize=file_change.serialize or version_config.serialize_formats, + search=search or file_change.search or version_config.search, + replace=replace or file_change.replace or version_config.replace, + regex=file_change.regex or False, + ignore_missing_version=file_change.ignore_missing_version or False, + filename=file_change.filename, + glob=file_change.glob, + key_path=file_change.key_path, + ) self.version_config = VersionConfig( - self.parse, self.serialize, self.search, self.replace, version_config.part_configs + self.file_change.parse, + self.file_change.serialize, + self.file_change.search, + self.file_change.replace, + version_config.part_configs, ) self._newlines: Optional[str] = None def get_file_contents(self) -> str: """Return the contents of the file.""" - with open(self.path, "rt", encoding="utf-8") as f: + with open(self.file_change.filename, "rt", encoding="utf-8") as f: contents = f.read() self._newlines = f.newlines[0] if isinstance(f.newlines, tuple) else f.newlines return contents @@ -125,15 +105,19 @@ def write_file_contents(self, contents: str) -> None: if self._newlines is None: _ = self.get_file_contents() - with open(self.path, "wt", encoding="utf-8", newline=self._newlines) as f: + with open(self.file_change.filename, "wt", encoding="utf-8", newline=self._newlines) as f: f.write(contents) - def contains_version(self, version: Version, context: MutableMapping) -> bool: + def _contains_change_pattern( + self, search_expression: re.Pattern, raw_search_expression: str, version: Version, context: MutableMapping + ) -> bool: """ - Check whether the version is present in the file. + Does the file contain the change pattern? Args: - version: The version to check + search_expression: The compiled search expression + raw_search_expression: The raw search expression + version: The version to check, in case it's not the same as the original context: The context to use Raises: @@ -142,18 +126,16 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: Returns: True if the version number is in fact present. """ - search_expression, raw_search_expression = get_search_pattern(self.search, context, self.regex) file_contents = self.get_file_contents() if contains_pattern(search_expression, file_contents): return True - # the `search` pattern did not match, but the original supplied + # The `search` pattern did not match, but the original supplied # version number (representing the same version part values) might - # match instead. + # match instead. This is probably the case if environment variables are used. - # check whether `search` isn't customized, i.e. should match only - # very specific parts of the file - search_pattern_is_default = self.search == self.version_config.search + # check whether `search` isn't customized + search_pattern_is_default = self.file_change.search == self.version_config.search if search_pattern_is_default and contains_pattern(re.compile(re.escape(version.original)), file_contents): # The original version is present, and we're not looking for something @@ -161,45 +143,62 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: return True # version not found - if self.ignore_missing_version: + if self.file_change.ignore_missing_version: return False - raise VersionNotFoundError(f"Did not find '{raw_search_expression}' in file: '{self.path}'") + raise VersionNotFoundError(f"Did not find '{raw_search_expression}' in file: '{self.file_change.filename}'") - def replace_version( + def make_file_change( self, current_version: Version, new_version: Version, context: MutableMapping, dry_run: bool = False ) -> None: - """Replace the current version with the new version.""" - file_content_before = self.get_file_contents() - + """Make the change to the file.""" + logger.info( + "\n%sFile %s: replace `%s` with `%s`", + logger.indent_str, + self.file_change.filename, + self.file_change.search, + self.file_change.replace, + ) + logger.indent() + logger.debug("Serializing the current version") + logger.indent() context["current_version"] = self.version_config.serialize(current_version, context) + logger.dedent() if new_version: + logger.debug("Serializing the new version") + logger.indent() context["new_version"] = self.version_config.serialize(new_version, context) + logger.dedent() - search_for, raw_search_pattern = get_search_pattern(self.search, context, self.regex) + search_for, raw_search_pattern = self.file_change.get_search_pattern(context) replace_with = self.version_config.replace.format(**context) + if not self._contains_change_pattern(search_for, raw_search_pattern, current_version, context): + return + + file_content_before = self.get_file_contents() + file_content_after = search_for.sub(replace_with, file_content_before) if file_content_before == file_content_after and current_version.original: og_context = deepcopy(context) og_context["current_version"] = current_version.original - search_for_og, og_raw_search_pattern = get_search_pattern(self.search, og_context, self.regex) + search_for_og, og_raw_search_pattern = self.file_change.get_search_pattern(og_context) file_content_after = search_for_og.sub(replace_with, file_content_before) - log_changes(self.path, file_content_before, file_content_after, dry_run) - + log_changes(self.file_change.filename, file_content_before, file_content_after, dry_run) + logger.dedent() if not dry_run: # pragma: no-coverage self.write_file_contents(file_content_after) def __str__(self) -> str: # pragma: no-coverage - return self.path + return self.file_change.filename def __repr__(self) -> str: # pragma: no-coverage - return f"" + return f"" def resolve_file_config( - files: List[FileConfig], version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None + files: List[FileChange], version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None ) -> List[ConfiguredFile]: """ Resolve the files, searching and replacing values according to the FileConfig. @@ -233,22 +232,9 @@ def modify_files( context: The context used for rendering the version dry_run: True if this should be a report-only job """ - _check_files_contain_version(files, current_version, context) + # _check_files_contain_version(files, current_version, context) for f in files: - f.replace_version(current_version, new_version, context, dry_run) - - -def _check_files_contain_version( - files: List[ConfiguredFile], current_version: Version, context: MutableMapping -) -> None: - """Make sure files exist and contain version string.""" - logger.info( - "Asserting files %s contain the version string...", - ", ".join({str(f.path) for f in files}), - ) - for f in files: - context["current_version"] = f.version_config.serialize(current_version, context) - f.contains_version(current_version, context) + f.make_file_change(current_version, new_version, context, dry_run) class FileUpdater: @@ -256,21 +242,28 @@ class FileUpdater: def __init__( self, - file_cfg: FileConfig, + file_change: FileChange, version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None, ) -> None: - self.path = file_cfg.filename - self.version_config = version_config - self.parse = file_cfg.parse or version_config.parse_regex.pattern - self.serialize = file_cfg.serialize or version_config.serialize_formats - self.search = search or file_cfg.search or version_config.search - self.replace = replace or file_cfg.replace or version_config.replace - self.regex = file_cfg.regex or False - self.ignore_missing_version = file_cfg.ignore_missing_version or False + self.file_change = FileChange( + parse=file_change.parse or version_config.parse_regex.pattern, + serialize=file_change.serialize or version_config.serialize_formats, + search=search or file_change.search or version_config.search, + replace=replace or file_change.replace or version_config.replace, + regex=file_change.regex or False, + ignore_missing_version=file_change.ignore_missing_version or False, + filename=file_change.filename, + glob=file_change.glob, + key_path=file_change.key_path, + ) self.version_config = VersionConfig( - self.parse, self.serialize, self.search, self.replace, version_config.part_configs + self.file_change.parse, + self.file_change.serialize, + self.file_change.search, + self.file_change.replace, + version_config.part_configs, ) self._newlines: Optional[str] = None @@ -287,18 +280,19 @@ class DataFileUpdater: def __init__( self, - file_cfg: FileConfig, + file_change: FileChange, version_part_configs: Dict[str, VersionPartConfig], ) -> None: - self.path = Path(file_cfg.filename) - self.key_path = file_cfg.key_path - self.search = file_cfg.search - self.replace = file_cfg.replace - self.regex = file_cfg.regex - self.ignore_missing_version = file_cfg.ignore_missing_version + self.file_change = file_change self.version_config = VersionConfig( - file_cfg.parse, file_cfg.serialize, file_cfg.search, file_cfg.replace, version_part_configs + self.file_change.parse, + self.file_change.serialize, + self.file_change.search, + self.file_change.replace, + version_part_configs, ) + self.path = Path(self.file_change.filename) + self._newlines: Optional[str] = None def update_file( self, current_version: Version, new_version: Version, context: MutableMapping, dry_run: bool = False @@ -307,8 +301,8 @@ def update_file( new_context = deepcopy(context) new_context["current_version"] = self.version_config.serialize(current_version, context) new_context["new_version"] = self.version_config.serialize(new_version, context) - search_for, raw_search_pattern = get_search_pattern(self.search, new_context, self.regex) - replace_with = self.replace.format(**new_context) + search_for, raw_search_pattern = self.file_change.get_search_pattern(new_context) + replace_with = self.file_change.replace.format(**new_context) if self.path.suffix == ".toml": self._update_toml_file(search_for, raw_search_pattern, replace_with, dry_run) @@ -320,20 +314,21 @@ def _update_toml_file( import tomlkit toml_data = tomlkit.parse(self.path.read_text()) - value_before = dotted.get(toml_data, self.key_path) + value_before = dotted.get(toml_data, self.file_change.key_path) if value_before is None: - raise KeyError(f"Key path '{self.key_path}' does not exist in {self.path}") - elif not contains_pattern(search_for, value_before) and not self.ignore_missing_version: + raise KeyError(f"Key path '{self.file_change.key_path}' does not exist in {self.path}") + elif not contains_pattern(search_for, value_before) and not self.file_change.ignore_missing_version: raise ValueError( - f"Key '{self.key_path}' in {self.path} does not contain the correct contents: {raw_search_pattern}" + f"Key '{self.file_change.key_path}' in {self.path} does not contain the correct contents: " + f"{raw_search_pattern}" ) new_value = search_for.sub(replace_with, value_before) - log_changes(f"{self.path}:{self.key_path}", value_before, new_value, dry_run) + log_changes(f"{self.path}:{self.file_change.key_path}", value_before, new_value, dry_run) if dry_run: return - dotted.update(toml_data, self.key_path, new_value) + dotted.update(toml_data, self.file_change.key_path, new_value) self.path.write_text(tomlkit.dumps(toml_data)) diff --git a/bumpversion/indented_logger.py b/bumpversion/indented_logger.py new file mode 100644 index 00000000..a8b8faad --- /dev/null +++ b/bumpversion/indented_logger.py @@ -0,0 +1,80 @@ +"""A logger adapter that adds an indent to the beginning of each message.""" +import logging +from contextvars import ContextVar +from typing import Any, MutableMapping, Optional, Tuple + +CURRENT_INDENT = ContextVar("current_indent", default=0) + + +class IndentedLoggerAdapter(logging.LoggerAdapter): + """ + Logger adapter that adds an indent to the beginning of each message. + + Parameters: + logger: The logger to adapt. + extra: Extra values to add to the logging context. + depth: The number of `indent_char` to generate for each indent level. + indent_char: The character or string to use for indenting. + reset: `True` if the indent level should be reset to zero. + """ + + def __init__( + self, + logger: logging.Logger, + extra: Optional[dict] = None, + depth: int = 2, + indent_char: str = " ", + reset: bool = False, + ): + super().__init__(logger, extra or {}) + self._depth = depth + self._indent_char = indent_char + if reset: + self.reset() + + @property + def current_indent(self) -> int: + """ + The current indent level. + """ + return CURRENT_INDENT.get() + + def indent(self, amount: int = 1) -> None: + """ + Increase the indent level by `amount`. + """ + CURRENT_INDENT.set(CURRENT_INDENT.get() + amount) + + def dedent(self, amount: int = 1) -> None: + """ + Decrease the indent level by `amount`. + """ + CURRENT_INDENT.set(max(0, CURRENT_INDENT.get() - amount)) + + def reset(self) -> None: + """ + Reset the indent level to zero. + """ + CURRENT_INDENT.set(0) + + @property + def indent_str(self) -> str: + """ + The indent string. + """ + return (self._indent_char * self._depth) * CURRENT_INDENT.get() + + def process(self, msg: str, kwargs: Optional[MutableMapping[str, Any]]) -> Tuple[str, MutableMapping[str, Any]]: + """ + Process the message and add the indent. + + Args: + msg: The logging message. + kwargs: Keyword arguments passed to the logger. + + Returns: + A tuple containing the message and keyword arguments. + """ + msg = self.indent_str + msg + + return msg, kwargs diff --git a/bumpversion/scm.py b/bumpversion/scm.py index 75240b2f..43f1e16c 100644 --- a/bumpversion/scm.py +++ b/bumpversion/scm.py @@ -1,6 +1,5 @@ """Version control system management.""" -import logging import os import re import subprocess @@ -9,6 +8,7 @@ from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, ClassVar, List, MutableMapping, Optional, Type, Union +from bumpversion.ui import get_indented_logger from bumpversion.utils import extract_regex_flags if TYPE_CHECKING: # pragma: no-coverage @@ -16,7 +16,7 @@ from bumpversion.exceptions import DirtyWorkingDirectoryError, SignedTagsError -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) @dataclass @@ -145,6 +145,7 @@ def commit_to_scm( "Preparing" if do_commit else "Would prepare", cls.__name__, ) + logger.indent() for path in files: logger.info( "%s changes in file '%s' to %s", @@ -171,6 +172,7 @@ def commit_to_scm( new_version=context["new_version"], extra_args=extra_args, ) + logger.dedent() @classmethod def tag_in_scm(cls, config: "Config", context: MutableMapping, dry_run: bool = False) -> None: diff --git a/bumpversion/ui.py b/bumpversion/ui.py index fa0278a3..a1a367e3 100644 --- a/bumpversion/ui.py +++ b/bumpversion/ui.py @@ -5,6 +5,8 @@ from click import UsageError, secho from rich.logging import RichHandler +from bumpversion.indented_logger import IndentedLoggerAdapter + logger = logging.getLogger("bumpversion") VERBOSITY = { @@ -14,6 +16,11 @@ } +def get_indented_logger(name: str) -> "IndentedLoggerAdapter": + """Get a logger with indentation.""" + return IndentedLoggerAdapter(logging.getLogger(name)) + + def setup_logging(verbose: int = 0) -> None: """Configure the logging.""" logging.basicConfig( @@ -26,7 +33,7 @@ def setup_logging(verbose: int = 0) -> None: ) ], ) - root_logger = logging.getLogger("") + root_logger = get_indented_logger("") root_logger.setLevel(VERBOSITY.get(verbose, logging.DEBUG)) diff --git a/bumpversion/version_part.py b/bumpversion/version_part.py index 41c2d054..6058fd59 100644 --- a/bumpversion/version_part.py +++ b/bumpversion/version_part.py @@ -1,18 +1,18 @@ """Module for managing Versions and their internal parts.""" -import logging import re import string from copy import copy -from typing import Any, Dict, List, MutableMapping, Optional, Union +from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union from click import UsageError from bumpversion.config.models import VersionPartConfig from bumpversion.exceptions import FormattingError, InvalidVersionPartError, MissingValueError from bumpversion.functions import NumericFunction, PartFunction, ValuesFunction +from bumpversion.ui import get_indented_logger from bumpversion.utils import key_val_string, labels_for_format -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) class VersionPart: @@ -134,7 +134,7 @@ class VersionConfig: def __init__( self, parse: str, - serialize: List[str], + serialize: Tuple[str], search: str, replace: str, part_configs: Optional[Dict[str, VersionPartConfig]] = None, @@ -183,6 +183,7 @@ def parse(self, version_string: Optional[str] = None) -> Optional[Version]: version_string, regexp_one_line, ) + logger.indent() match = self.parse_regex.search(version_string) @@ -202,6 +203,7 @@ def parse(self, version_string: Optional[str] = None) -> Optional[Version]: v = Version(_parsed, version_string) logger.info("Parsed the following values: %s", key_val_string(v.values)) + logger.dedent() return v @@ -268,8 +270,8 @@ def _serialize( def _choose_serialize_format(self, version: Version, context: MutableMapping) -> str: chosen = None - logger.debug("Available serialization formats: '%s'", "', '".join(self.serialize_formats)) - + logger.debug("Evaluating serialization formats") + logger.indent() for serialize_format in self.serialize_formats: try: self._serialize(version, serialize_format, context, raise_if_incomplete=True) @@ -291,7 +293,7 @@ def _choose_serialize_format(self, version: Version, context: MutableMapping) -> if not chosen: raise KeyError("Did not find suitable serialization format") - + logger.dedent() logger.debug("Selected serialization format '%s'", chosen) return chosen @@ -307,6 +309,9 @@ def serialize(self, version: Version, context: MutableMapping) -> str: Returns: The serialized version as a string """ + logger.debug("Serializing version '%s'", version) + logger.indent() serialized = self._serialize(version, self._choose_serialize_format(version, context), context) logger.debug("Serialized to '%s'", serialized) + logger.dedent() return serialized diff --git a/bumpversion/yaml_dump.py b/bumpversion/yaml_dump.py index 4836dbdd..c4ab0acf 100644 --- a/bumpversion/yaml_dump.py +++ b/bumpversion/yaml_dump.py @@ -3,7 +3,7 @@ from collections import UserDict from io import StringIO from textwrap import indent -from typing import Any, Callable +from typing import Any, Callable, Union DumperFunc = Callable[[Any], str] @@ -89,7 +89,7 @@ def format_dict(val: dict) -> str: for key, value in sorted(val.items()): rendered_value = dump(value).strip() - if isinstance(value, (dict, list)): + if isinstance(value, (dict, list, tuple)): rendered_value = f"\n{indent(rendered_value, INDENT)}" else: rendered_value = f" {rendered_value}" @@ -101,7 +101,7 @@ def format_dict(val: dict) -> str: YAML_DUMPERS.add_dumper(dict, format_dict) -def format_list(val: list) -> str: +def format_sequence(val: Union[list, tuple]) -> str: """Return a string representation of a value.""" buffer = StringIO() @@ -110,7 +110,7 @@ def format_list(val: list) -> str: if isinstance(item, dict): rendered_value = indent(rendered_value, INDENT).strip() - if isinstance(item, list): + if isinstance(item, (list, tuple)): rendered_value = f"\n{indent(rendered_value, INDENT)}" else: rendered_value = f" {rendered_value}" @@ -119,7 +119,8 @@ def format_list(val: list) -> str: return buffer.getvalue() -YAML_DUMPERS.add_dumper(list, format_list) +YAML_DUMPERS.add_dumper(list, format_sequence) +YAML_DUMPERS.add_dumper(tuple, format_sequence) def format_none(_: None) -> str: diff --git a/requirements/dev.txt b/requirements/dev.txt index f7fce9ac..f3863a15 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,66 +1,66 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --extra=dev --extra=docs --extra=test --output-file=requirements/dev.txt pyproject.toml # alabaster==0.7.13 # via sphinx -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic argopt==0.8.2 # via git-fame -astroid==2.15.6 +astroid==3.0.1 # via sphinx-autodoc2 -babel==2.12.1 +babel==2.13.1 # via sphinx beautifulsoup4==4.12.2 # via furo -build==0.10.0 +build==1.0.3 # via pip-tools -certifi==2023.5.7 +certifi==2023.11.17 # via requests -cfgv==3.3.1 +cfgv==3.4.0 # via pre-commit -charset-normalizer==3.1.0 +charset-normalizer==3.3.2 # via requests -click==8.1.3 +click==8.1.7 # via # bump-my-version (pyproject.toml) # pip-tools # rich-click # sphinx-click # typer -coverage[toml]==7.2.7 +coverage[toml]==7.3.2 # via # bump-my-version (pyproject.toml) # pytest-cov -distlib==0.3.6 +distlib==0.3.7 # via virtualenv docutils==0.20.1 # via # myst-parser # sphinx # sphinx-click -exceptiongroup==1.1.1 - # via pytest -filelock==3.12.2 +dotted-notation==0.10.0 + # via bump-my-version (pyproject.toml) +filelock==3.13.1 # via virtualenv -furo==2023.5.20 +furo==2023.9.10 # via bump-my-version (pyproject.toml) -generate-changelog==0.9.2 +generate-changelog==0.10.0 # via bump-my-version (pyproject.toml) ghp-import==2.1.0 # via bump-my-version (pyproject.toml) git-fame==2.0.1 # via bump-my-version (pyproject.toml) -gitdb==4.0.10 +gitdb==4.0.11 # via gitpython -gitpython==3.1.31 +gitpython==3.1.40 # via generate-changelog -identify==2.5.24 +identify==2.5.33 # via pre-commit -idna==3.4 +idna==3.6 # via requests imagesize==1.4.1 # via sphinx @@ -71,8 +71,6 @@ jinja2==3.1.2 # generate-changelog # myst-parser # sphinx -lazy-object-proxy==1.9.0 - # via astroid linkify-it-py==2.0.2 # via bump-my-version (pyproject.toml) markdown-it-py==3.0.0 @@ -86,78 +84,80 @@ mdit-py-plugins==0.4.0 # via myst-parser mdurl==0.1.2 # via markdown-it-py -more-itertools==9.1.0 +more-itertools==10.1.0 # via generate-changelog myst-parser==2.0.0 # via bump-my-version (pyproject.toml) nodeenv==1.8.0 # via pre-commit -packaging==23.1 +packaging==23.2 # via # build # pytest # sphinx -pip-tools==6.13.0 +pip-tools==7.3.0 # via bump-my-version (pyproject.toml) -platformdirs==3.6.0 +platformdirs==4.1.0 # via virtualenv -pluggy==1.0.0 +pluggy==1.3.0 # via pytest -pre-commit==3.3.3 +pre-commit==3.5.0 # via bump-my-version (pyproject.toml) -pydantic==2.4.0 +pydantic==2.5.2 # via # bump-my-version (pyproject.toml) # pydantic-settings -pydantic-core==2.10.0 +pydantic-core==2.14.5 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via bump-my-version (pyproject.toml) -pygments==2.15.1 +pygments==2.17.2 # via # furo # rich # sphinx +pyparsing==3.1.1 + # via dotted-notation pyproject-hooks==1.0.0 # via build -pytest==7.3.2 +pytest==7.4.3 # via # bump-my-version (pyproject.toml) # pytest-cov # pytest-mock pytest-cov==4.1.0 # via bump-my-version (pyproject.toml) -pytest-mock==3.11.1 +pytest-mock==3.12.0 # via bump-my-version (pyproject.toml) python-dateutil==2.8.2 # via ghp-import python-dotenv==1.0.0 # via pydantic-settings -pyyaml==6.0 +pyyaml==6.0.1 # via # myst-parser # pre-commit requests==2.31.0 # via sphinx -rich==13.4.2 +rich==13.7.0 # via # bump-my-version (pyproject.toml) # rich-click -rich-click==1.6.1 +rich-click==1.7.2 # via bump-my-version (pyproject.toml) -ruamel-yaml==0.17.32 +ruamel-yaml==0.18.5 # via generate-changelog -ruamel-yaml-clib==0.2.7 +ruamel-yaml-clib==0.2.8 # via ruamel-yaml six==1.16.0 # via python-dateutil -smmap==5.0.0 +smmap==5.0.1 # via gitdb snowballstemmer==2.2.0 # via sphinx -soupsieve==2.4.1 +soupsieve==2.5 # via beautifulsoup4 -sphinx==7.0.1 +sphinx==7.2.6 # via # bump-my-version (pyproject.toml) # furo @@ -166,60 +166,56 @@ sphinx==7.0.1 # sphinx-basic-ng # sphinx-click # sphinx-copybutton -sphinx-autodoc-typehints==1.23.2 + # sphinxcontrib-applehelp + # sphinxcontrib-devhelp + # sphinxcontrib-htmlhelp + # sphinxcontrib-qthelp + # sphinxcontrib-serializinghtml +sphinx-autodoc-typehints==1.25.2 # via bump-my-version (pyproject.toml) -sphinx-autodoc2==0.4.2 +sphinx-autodoc2==0.5.0 # via bump-my-version (pyproject.toml) -sphinx-basic-ng==1.0.0b1 +sphinx-basic-ng==1.0.0b2 # via furo -sphinx-click==4.4.0 +sphinx-click==5.1.0 # via bump-my-version (pyproject.toml) sphinx-copybutton==0.5.2 # via bump-my-version (pyproject.toml) -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==1.0.7 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.5 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.0.4 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.6 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.9 # via sphinx tabulate==0.9.0 # via git-fame -tomli==2.0.1 - # via - # build - # coverage - # pyproject-hooks - # pytest - # sphinx-autodoc2 -tomlkit==0.11.8 +tomlkit==0.12.3 # via bump-my-version (pyproject.toml) -tqdm==4.65.0 +tqdm==4.66.1 # via git-fame typer==0.9.0 # via generate-changelog -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via - # astroid # pydantic # pydantic-core + # rich-click # sphinx-autodoc2 # typer uc-micro-py==1.0.2 # via linkify-it-py -urllib3==2.0.3 +urllib3==2.1.0 # via requests -virtualenv==20.23.1 +virtualenv==20.25.0 # via pre-commit -wheel==0.40.0 +wheel==0.42.0 # via pip-tools -wrapt==1.15.0 - # via astroid # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/docs.txt b/requirements/docs.txt index 59234144..fa9959d3 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,24 +1,24 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --extra=docs --output-file=requirements/docs.txt pyproject.toml # alabaster==0.7.13 # via sphinx -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic -astroid==2.15.6 +astroid==3.0.1 # via sphinx-autodoc2 -babel==2.12.1 +babel==2.13.1 # via sphinx beautifulsoup4==4.12.2 # via furo -certifi==2023.5.7 +certifi==2023.11.17 # via requests -charset-normalizer==3.1.0 +charset-normalizer==3.3.2 # via requests -click==8.1.3 +click==8.1.7 # via # bump-my-version (pyproject.toml) # rich-click @@ -28,11 +28,13 @@ docutils==0.20.1 # myst-parser # sphinx # sphinx-click -furo==2023.5.20 +dotted-notation==0.10.0 + # via bump-my-version (pyproject.toml) +furo==2023.9.10 # via bump-my-version (pyproject.toml) ghp-import==2.1.0 # via bump-my-version (pyproject.toml) -idna==3.4 +idna==3.6 # via requests imagesize==1.4.1 # via sphinx @@ -40,8 +42,6 @@ jinja2==3.1.2 # via # myst-parser # sphinx -lazy-object-proxy==1.9.0 - # via astroid linkify-it-py==2.0.2 # via bump-my-version (pyproject.toml) markdown-it-py==3.0.0 @@ -57,42 +57,44 @@ mdurl==0.1.2 # via markdown-it-py myst-parser==2.0.0 # via bump-my-version (pyproject.toml) -packaging==23.1 +packaging==23.2 # via sphinx -pydantic==2.4.0 +pydantic==2.5.2 # via # bump-my-version (pyproject.toml) # pydantic-settings -pydantic-core==2.10.0 +pydantic-core==2.14.5 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via bump-my-version (pyproject.toml) -pygments==2.15.1 +pygments==2.17.2 # via # furo # rich # sphinx +pyparsing==3.1.1 + # via dotted-notation python-dateutil==2.8.2 # via ghp-import python-dotenv==1.0.0 # via pydantic-settings -pyyaml==6.0 +pyyaml==6.0.1 # via myst-parser requests==2.31.0 # via sphinx -rich==13.4.2 +rich==13.7.0 # via # bump-my-version (pyproject.toml) # rich-click -rich-click==1.6.1 +rich-click==1.7.2 # via bump-my-version (pyproject.toml) six==1.16.0 # via python-dateutil snowballstemmer==2.2.0 # via sphinx -soupsieve==2.4.1 +soupsieve==2.5 # via beautifulsoup4 -sphinx==7.0.1 +sphinx==7.2.6 # via # bump-my-version (pyproject.toml) # furo @@ -101,41 +103,42 @@ sphinx==7.0.1 # sphinx-basic-ng # sphinx-click # sphinx-copybutton -sphinx-autodoc-typehints==1.23.2 + # sphinxcontrib-applehelp + # sphinxcontrib-devhelp + # sphinxcontrib-htmlhelp + # sphinxcontrib-qthelp + # sphinxcontrib-serializinghtml +sphinx-autodoc-typehints==1.25.2 # via bump-my-version (pyproject.toml) -sphinx-autodoc2==0.4.2 +sphinx-autodoc2==0.5.0 # via bump-my-version (pyproject.toml) -sphinx-basic-ng==1.0.0b1 +sphinx-basic-ng==1.0.0b2 # via furo -sphinx-click==4.4.0 +sphinx-click==5.1.0 # via bump-my-version (pyproject.toml) sphinx-copybutton==0.5.2 # via bump-my-version (pyproject.toml) -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==1.0.7 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.5 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.0.4 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.6 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.9 # via sphinx -tomli==2.0.1 - # via sphinx-autodoc2 -tomlkit==0.11.8 +tomlkit==0.12.3 # via bump-my-version (pyproject.toml) -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via - # astroid # pydantic # pydantic-core + # rich-click # sphinx-autodoc2 uc-micro-py==1.0.2 # via linkify-it-py -urllib3==2.0.3 +urllib3==2.1.0 # via requests -wrapt==1.15.0 - # via astroid diff --git a/requirements/prod.txt b/requirements/prod.txt index f0ccca20..3fd69fec 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,40 +1,45 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --output-file=requirements/prod.txt pyproject.toml # -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic -click==8.1.3 +click==8.1.7 # via # bump-my-version (pyproject.toml) # rich-click +dotted-notation==0.10.0 + # via bump-my-version (pyproject.toml) markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -pydantic==2.4.0 +pydantic==2.5.2 # via # bump-my-version (pyproject.toml) # pydantic-settings -pydantic-core==2.10.0 +pydantic-core==2.14.5 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via bump-my-version (pyproject.toml) -pygments==2.15.1 +pygments==2.17.2 # via rich +pyparsing==3.1.1 + # via dotted-notation python-dotenv==1.0.0 # via pydantic-settings -rich==13.4.2 +rich==13.7.0 # via # bump-my-version (pyproject.toml) # rich-click -rich-click==1.6.1 +rich-click==1.7.2 # via bump-my-version (pyproject.toml) -tomlkit==0.11.8 +tomlkit==0.12.3 # via bump-my-version (pyproject.toml) -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # pydantic # pydantic-core + # rich-click diff --git a/requirements/test.txt b/requirements/test.txt index 05070747..9ff09f39 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,28 +1,28 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --extra=test --output-file=requirements/test.txt pyproject.toml # -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic -cfgv==3.3.1 +cfgv==3.4.0 # via pre-commit -click==8.1.3 +click==8.1.7 # via # bump-my-version (pyproject.toml) # rich-click -coverage[toml]==7.2.7 +coverage[toml]==7.3.2 # via # bump-my-version (pyproject.toml) # pytest-cov -distlib==0.3.6 +distlib==0.3.7 # via virtualenv -exceptiongroup==1.1.1 - # via pytest -filelock==3.12.2 +dotted-notation==0.10.0 + # via bump-my-version (pyproject.toml) +filelock==3.13.1 # via virtualenv -identify==2.5.24 +identify==2.5.33 # via pre-commit iniconfig==2.0.0 # via pytest @@ -32,54 +32,53 @@ mdurl==0.1.2 # via markdown-it-py nodeenv==1.8.0 # via pre-commit -packaging==23.1 +packaging==23.2 # via pytest -platformdirs==3.6.0 +platformdirs==4.1.0 # via virtualenv -pluggy==1.0.0 +pluggy==1.3.0 # via pytest -pre-commit==3.3.3 +pre-commit==3.5.0 # via bump-my-version (pyproject.toml) -pydantic==2.4.0 +pydantic==2.5.2 # via # bump-my-version (pyproject.toml) # pydantic-settings -pydantic-core==2.10.0 +pydantic-core==2.14.5 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via bump-my-version (pyproject.toml) -pygments==2.15.1 +pygments==2.17.2 # via rich -pytest==7.3.2 +pyparsing==3.1.1 + # via dotted-notation +pytest==7.4.3 # via # bump-my-version (pyproject.toml) # pytest-cov # pytest-mock pytest-cov==4.1.0 # via bump-my-version (pyproject.toml) -pytest-mock==3.11.1 +pytest-mock==3.12.0 # via bump-my-version (pyproject.toml) python-dotenv==1.0.0 # via pydantic-settings -pyyaml==6.0 +pyyaml==6.0.1 # via pre-commit -rich==13.4.2 +rich==13.7.0 # via # bump-my-version (pyproject.toml) # rich-click -rich-click==1.6.1 +rich-click==1.7.2 # via bump-my-version (pyproject.toml) -tomli==2.0.1 - # via - # coverage - # pytest -tomlkit==0.11.8 +tomlkit==0.12.3 # via bump-my-version (pyproject.toml) -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # pydantic # pydantic-core -virtualenv==20.23.1 + # rich-click +virtualenv==20.25.0 # via pre-commit # The following packages are considered to be unsafe in a requirements file: diff --git a/tests/fixtures/basic_cfg_expected.txt b/tests/fixtures/basic_cfg_expected.txt index b8d31ca7..b1d24f9d 100644 --- a/tests/fixtures/basic_cfg_expected.txt +++ b/tests/fixtures/basic_cfg_expected.txt @@ -11,8 +11,8 @@ 'regex': False, 'replace': '{new_version}', 'search': '{current_version}', - 'serialize': ['{major}.{minor}.{patch}-{release}', - '{major}.{minor}.{patch}']}, + 'serialize': ('{major}.{minor}.{patch}-{release}', + '{major}.{minor}.{patch}')}, {'filename': 'bumpversion/__init__.py', 'glob': None, 'ignore_missing_version': False, @@ -21,8 +21,8 @@ 'regex': False, 'replace': '{new_version}', 'search': '{current_version}', - 'serialize': ['{major}.{minor}.{patch}-{release}', - '{major}.{minor}.{patch}']}, + 'serialize': ('{major}.{minor}.{patch}-{release}', + '{major}.{minor}.{patch}')}, {'filename': 'CHANGELOG.md', 'glob': None, 'ignore_missing_version': False, @@ -31,8 +31,8 @@ 'regex': False, 'replace': '**unreleased**\n**v{new_version}**', 'search': '**unreleased**', - 'serialize': ['{major}.{minor}.{patch}-{release}', - '{major}.{minor}.{patch}']}], + 'serialize': ('{major}.{minor}.{patch}-{release}', + '{major}.{minor}.{patch}')}], 'ignore_missing_version': False, 'included_paths': [], 'message': 'Bump version: {current_version} → {new_version}', @@ -63,7 +63,7 @@ 'short_branch_name': None, 'tool': None}, 'search': '{current_version}', - 'serialize': ['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'], + 'serialize': ('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'), 'sign_tags': False, 'tag': True, 'tag_message': 'Bump version: {current_version} → {new_version}', diff --git a/tests/test_bump.py b/tests/test_bump.py index 0affee64..bfd47f05 100644 --- a/tests/test_bump.py +++ b/tests/test_bump.py @@ -76,7 +76,7 @@ def test_do_bump_with_version_part(mock_update_config_file, mock_modify_files): # Assert mock_modify_files.assert_called_once() mock_update_config_file.assert_called_once() - assert {f.path for f in mock_modify_files.call_args[0][0]} == { + assert {f.file_change.filename for f in mock_modify_files.call_args[0][0]} == { "foo.txt", "bar.txt", } diff --git a/tests/test_cli.py b/tests/test_cli.py index e1d99271..14314cac 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -139,7 +139,7 @@ def test_cli_options_override_config(tmp_path: Path, fixtures_path: Path, mocker assert the_config.current_version == "1.1.0" assert the_config.allow_dirty assert the_config.parse == r"XXX(?P\d+);(?P\d+);(?P\d+)" - assert the_config.serialize == ["XXX{spam};{blob};{slurp}"] + assert the_config.serialize == ("XXX{spam};{blob};{slurp}",) assert the_config.search == "my-search" assert the_config.replace == "my-replace" assert the_config.commit is False @@ -203,7 +203,7 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path): "current_version=1.0.0", "excluded_paths=[]", "parse=(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", - "serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']", + "serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')", "search={current_version}", "replace={new_version}", "regex=False", @@ -220,19 +220,19 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path): ( "files=[{'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': " "'{new_version}', 'regex': False, 'ignore_missing_version': False, " "'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': " "'{new_version}', 'regex': False, 'ignore_missing_version': False, " "'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, " "{'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '**unreleased**', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': " "'**unreleased**\\n**v{new_version}**', 'regex': False, " "'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, " "'key_path': None}]" @@ -263,7 +263,7 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path): "current_version=1.0.0", "excluded_paths=[]", "parse=(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", - "serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']", + "serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')", "search={current_version}", "replace={new_version}", "regex=False", @@ -280,19 +280,19 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path): ( "files=[{'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': " "'{new_version}', 'regex': False, 'ignore_missing_version': False, " "'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': " "'{new_version}', 'regex': False, 'ignore_missing_version': False, " "'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, " "{'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '**unreleased**', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': " "'**unreleased**\\n**v{new_version}**', 'regex': False, " "'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, " "'key_path': None}]" @@ -426,7 +426,7 @@ def test_replace(mocker, tmp_path, fixtures_path): call_args = mocked_modify_files.call_args[0] configured_files = call_args[0] assert len(configured_files) == 3 - actual_filenames = {f.path for f in configured_files} + actual_filenames = {f.file_change.filename for f in configured_files} assert actual_filenames == {"setup.py", "CHANGELOG.md", "bumpversion/__init__.py"} @@ -471,7 +471,7 @@ def test_replace_specific_files(mocker, git_repo, fixtures_path): call_args = mocked_modify_files.call_args[0] configured_files = call_args[0] assert len(configured_files) == 1 - assert configured_files[0].path == "VERSION" + assert configured_files[0].file_change.filename == "VERSION" TEST_REPLACE_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 63% rename from tests/test_config.py rename to tests/test_config/test_files.py index 6516c7ce..d75fa66f 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" @@ -137,35 +140,9 @@ def test_multiple_config_files(tmp_path: Path): assert cfg.current_version == "0.10.5" assert cfg.parse == "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?" - assert cfg.serialize == ["{major}.{minor}.{patch}-{release}", "{major}.{minor}.{patch}"] + 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. @@ -268,18 +220,6 @@ def test_pep440_config(git_repo: Path, fixtures_path: Path): cfg = config.get_configuration(cfg_path) assert cfg.current_version == "1.0.0.dev0+myreallylongbranchna" - # try: - # subprocess.run(["git", "add", "README.md"], check=True, capture_output=True) - # subprocess.run(["git", "commit", "-am", "my branch commit"], check=True, capture_output=True) - # except subprocess.CalledProcessError as e: - # print(e.stdout) - # print(e.stderr) - # raise - # result: Result = runner.invoke(cli.cli, ["bump", "dev_label", "--no-tag"]) - # assert result.exit_code == 0 - # cfg = config.get_configuration(cfg_path) - # assert cfg.current_version == "1.0.0.dev1+myreallylongbranchna" - @pytest.mark.parametrize( ["glob_pattern", "file_list"], @@ -343,7 +283,7 @@ def test_file_overrides_config(fixtures_path: Path): assert file_map["should_override_parse.txt"].ignore_missing_version == conf.ignore_missing_version assert file_map["should_override_serialize.txt"].parse == conf.parse - assert file_map["should_override_serialize.txt"].serialize == ["{major}"] + assert file_map["should_override_serialize.txt"].serialize == ("{major}",) assert file_map["should_override_serialize.txt"].search == conf.search assert file_map["should_override_serialize.txt"].replace == conf.replace assert file_map["should_override_serialize.txt"].regex == conf.regex 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 diff --git a/tests/test_files.py b/tests/test_files.py index 3227bb05..f8865104 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -11,7 +11,7 @@ from pytest import param from bumpversion import exceptions, files, config, bump -from bumpversion.config.models import FileConfig +from bumpversion.config.models import FileChange from bumpversion.utils import get_context from bumpversion.exceptions import VersionNotFoundError from bumpversion.version_part import VersionConfig @@ -53,7 +53,7 @@ def test_single_file_processed_twice(tmp_path: Path): assert len(conf.files) == 2 for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, new_version, ctx) + cfg_file.make_file_change(current_version, new_version, ctx) assert filepath.read_text() == "dots: 0.10.3\ndashes: 0-10-3" @@ -106,7 +106,7 @@ def test_multi_file_configuration(tmp_path: Path): for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, major_version, ctx) + cfg_file.make_file_change(current_version, major_version, ctx) assert full_vers_path.read_text() == "2.0.0" assert maj_vers_path.read_text() == "2" @@ -123,7 +123,7 @@ def test_multi_file_configuration(tmp_path: Path): major_patch_version = major_version.bump("patch", version_config.order) for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(major_version, major_patch_version, ctx) + cfg_file.make_file_change(major_version, major_patch_version, ctx) assert full_vers_path.read_text() == "2.0.1" assert maj_vers_path.read_text() == "2" @@ -220,7 +220,7 @@ def test_search_replace_to_avoid_updating_unconcerned_lines(tmp_path: Path, capl for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, new_version, get_context(conf)) + cfg_file.make_file_change(current_version, new_version, get_context(conf)) utc_today = datetime.now(timezone.utc).strftime("%Y-%m-%d") expected_chglog = dedent( @@ -291,7 +291,7 @@ def test_simple_replacement_in_utf8_file(tmp_path: Path): # Act for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, new_version, get_context(conf)) + cfg_file.make_file_change(current_version, new_version, get_context(conf)) # Assert out = version_path.read_text() @@ -317,7 +317,7 @@ def test_multi_line_search_is_found(tmp_path: Path) -> None: # Act for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, new_version, get_context(conf)) + cfg_file.make_file_change(current_version, new_version, get_context(conf)) # Assert assert alphabet_path.read_text() == "A\nB\nC\n10.0.0\n" @@ -446,7 +446,7 @@ def test_datafileupdater_replaces_key(tmp_path: Path, fixtures_path: Path) -> No version_config = VersionConfig(conf.parse, conf.serialize, conf.search, conf.replace, conf.parts) current_version = version_config.parse(conf.current_version) new_version = current_version.bump("minor", version_config.order) - datafile_config = FileConfig( + datafile_config = FileChange( filename=str(config_path), key_path="tool.bumpversion.current_version", search=conf.search, diff --git a/tests/test_indented_logger.py b/tests/test_indented_logger.py new file mode 100644 index 00000000..ba87f3f5 --- /dev/null +++ b/tests/test_indented_logger.py @@ -0,0 +1,97 @@ +import pytest + +from bumpversion.indented_logger import IndentedLoggerAdapter +import logging + + +def test_does_not_indent_without_intent(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.debug("test debug") + logger.info("test info") + logger.warning("test warning") + logger.error("test error") + logger.critical("test critical") + + assert caplog.record_tuples == [ + ("root", 10, "test debug"), + ("root", 20, "test info"), + ("root", 30, "test warning"), + ("root", 40, "test error"), + ("root", 50, "test critical"), + ] + + +def test_indents(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.info("test 1") + logger.indent(2) + logger.error("test %d", 2) + logger.indent() + logger.debug("test 3") + logger.warning("test 4") + logger.indent() + logger.critical("test 5") + logger.critical("test 6") + + assert caplog.record_tuples == [ + ("root", 20, "test 1"), + ("root", 40, " test 2"), + ("root", 10, " test 3"), + ("root", 30, " test 4"), + ("root", 50, " test 5"), + ("root", 50, " test 6"), + ] + + +def test_dedents(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.indent(3) + logger.info("test 1") + logger.dedent(2) + logger.error("test %d", 2) + logger.dedent() + logger.debug("test 3") + logger.warning("test 4") + + assert caplog.record_tuples == [ + ("root", 20, " test 1"), + ("root", 40, " test 2"), + ("root", 10, "test 3"), + ("root", 30, "test 4"), + ] + + +def test_cant_dedent_below_zero(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger()) + logger.dedent(4) + logger.info("test 1") + + assert caplog.record_tuples == [("root", 20, "test 1")] + + +def test_current_indent_shared_by_multiple_loggers(caplog: pytest.LogCaptureFixture): + """Indenting one logger indents all loggers.""" + caplog.set_level(logging.DEBUG) + logger1 = IndentedLoggerAdapter(logging.getLogger("logger1"), reset=True) + logger1.info("test 1") + logger1.indent() + logger2 = IndentedLoggerAdapter(logging.getLogger("logger2")) + logger2.info("test 2") + logger3 = IndentedLoggerAdapter(logging.getLogger("logger3")) + logger3.indent() + logger3.info("test 3") + logger3.info("test 4") + logger2.dedent() + logger2.info("test 5") + + assert caplog.record_tuples == [ + ("logger1", 20, "test 1"), + ("logger2", 20, " test 2"), + ("logger3", 20, " test 3"), + ("logger3", 20, " test 4"), + ("logger2", 20, " test 5"), + ] diff --git a/tests/test_version_part.py b/tests/test_version_part.py index 085101ed..4875fc05 100644 --- a/tests/test_version_part.py +++ b/tests/test_version_part.py @@ -299,7 +299,7 @@ def test_parse_doesnt_parse_current_version(tmp_path: Path, caplog: LogCaptureFi with inside_dir(tmp_path): get_config_data(overrides) - assert "Evaluating 'parse' option: 'xxx' does not parse current version '12'" in caplog.messages + assert " Evaluating 'parse' option: 'xxx' does not parse current version '12'" in caplog.messages def test_part_does_not_revert_to_zero_if_optional(tmp_path: Path) -> None: diff --git a/tests/test_yaml.py b/tests/test_yaml.py index 7037f67f..b006b43d 100644 --- a/tests/test_yaml.py +++ b/tests/test_yaml.py @@ -6,7 +6,7 @@ def test_dump_unknown(): - assert yaml_dump.dump((1, 2)) == '"(1, 2)"' + assert yaml_dump.dump({1, 2}) == '"{1, 2}"' def test_format_str(): @@ -42,10 +42,15 @@ def test_format_dict(): "key8": True, "key9": False, "key10": 1.43, + "key11": (1, 2, 3), } expected = ( 'key: "strval"\n' "key10: 1.43\n" + "key11:\n" + " - 1\n" + " - 2\n" + " - 3\n" "key2: 30\n" "key3: 2023-06-19 13:45:30\n" "key4: 2023-06-19\n" @@ -63,8 +68,10 @@ def test_format_dict(): def test_format_list(): - assert yaml_dump.format_list(["item"]) == '- "item"\n' - assert yaml_dump.format_list(["item", ["item2"]]) == '- "item"\n-\n - "item2"\n' + assert yaml_dump.format_sequence(["item"]) == '- "item"\n' + assert yaml_dump.format_sequence(["item", ["item2"]]) == '- "item"\n-\n - "item2"\n' + assert yaml_dump.format_sequence(("item",)) == '- "item"\n' + assert yaml_dump.format_sequence(("item", ("item2",))) == '- "item"\n-\n - "item2"\n' def test_dump_none_val(): diff --git a/tools/bump.sh b/tools/bump.sh old mode 100644 new mode 100755 index 1c081e54..5b36a8c1 --- a/tools/bump.sh +++ b/tools/bump.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + rm -Rf dist RELEASE_KIND=$(generate-changelog --output release-hint) echo "::notice::Suggested release type is: ${RELEASE_KIND}" diff --git a/tools/generate-requirements.sh b/tools/generate-requirements.sh index 39c222b1..f97dece8 100755 --- a/tools/generate-requirements.sh +++ b/tools/generate-requirements.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -pip-compile --output-file=requirements/prod.txt pyproject.toml -pip-compile --extra=docs --output-file=requirements/docs.txt pyproject.toml -pip-compile --extra=test --output-file=requirements/test.txt pyproject.toml -pip-compile --extra=dev --extra=docs --extra=test --output-file=requirements/dev.txt pyproject.toml +pip-compile --upgrade --output-file=requirements/prod.txt pyproject.toml +pip-compile --upgrade --extra=docs --output-file=requirements/docs.txt pyproject.toml +pip-compile --upgrade --extra=test --output-file=requirements/test.txt pyproject.toml +pip-compile --upgrade --extra=dev --extra=docs --extra=test --output-file=requirements/dev.txt pyproject.toml