diff --git a/bumpversion/cli.py b/bumpversion/cli.py index 652ad136..7578a815 100644 --- a/bumpversion/cli.py +++ b/bumpversion/cli.py @@ -141,10 +141,10 @@ def cli(ctx: Context) -> None: help="Template for complete string to replace", ) @click.option( - "--no-regex", - is_flag=True, - envvar="BUMPVERSION_NO_REGEX", - help="Do not treat the search parameter as a regular expression", + "--regex/--no-regex", + default=False, + envvar="BUMPVERSION_REGEX", + help="Treat the search parameter as a regular expression or explicitly do not treat it as a regular expression.", ) @click.option( "--no-configured-files", @@ -233,7 +233,7 @@ def bump( serialize: Optional[List[str]], search: Optional[str], replace: Optional[str], - no_regex: bool, + regex: bool, no_configured_files: bool, ignore_missing_version: bool, dry_run: bool, @@ -277,7 +277,7 @@ def bump( message=message, commit_args=commit_args, ignore_missing_version=ignore_missing_version, - no_regex=no_regex, + regex=regex, ) found_config_file = find_config_file(config_file) @@ -418,10 +418,10 @@ def show(args: List[str], config_file: Optional[str], format_: str, increment: O help="Template for complete string to replace", ) @click.option( - "--no-regex", - is_flag=True, - envvar="BUMPVERSION_NO_REGEX", - help="Do not treat the search parameter as a regular expression", + "--regex/--no-regex", + default=False, + envvar="BUMPVERSION_REGEX", + help="Treat the search parameter as a regular expression or explicitly do not treat it as a regular expression.", ) @click.option( "--no-configured-files", @@ -456,7 +456,7 @@ def replace( serialize: Optional[List[str]], search: Optional[str], replace: Optional[str], - no_regex: bool, + regex: bool, no_configured_files: bool, ignore_missing_version: bool, dry_run: bool, @@ -486,7 +486,7 @@ def replace( message=None, commit_args=None, ignore_missing_version=ignore_missing_version, - no_regex=no_regex, + regex=regex, ) found_config_file = find_config_file(config_file) diff --git a/bumpversion/config.py b/bumpversion/config.py index e9873d26..48701fdb 100644 --- a/bumpversion/config.py +++ b/bumpversion/config.py @@ -43,7 +43,7 @@ class FileConfig(BaseModel): serialize: Optional[List[str]] = None # If different from outer scope search: Optional[str] = None # If different from outer scope replace: Optional[str] = None # If different from outer scope - no_regex: Optional[bool] = None # If different from outer scope + regex: Optional[bool] = None # If different from outer scope ignore_missing_version: Optional[bool] = None @@ -55,7 +55,7 @@ class Config(BaseSettings): serialize: List[str] = Field(min_length=1) search: str replace: str - no_regex: bool + regex: bool ignore_missing_version: bool tag: bool sign_tags: bool @@ -86,7 +86,7 @@ def add_files(self, filename: Union[str, List[str]]) -> None: serialize=self.serialize, search=self.search, replace=self.replace, - no_regex=self.no_regex, + regex=self.regex, ignore_missing_version=self.ignore_missing_version, ) ) @@ -128,7 +128,7 @@ def version_config(self) -> "VersionConfig": "serialize": ["{major}.{minor}.{patch}"], "search": "{current_version}", "replace": "{new_version}", - "no_regex": False, + "regex": False, "ignore_missing_version": False, "tag": False, "sign_tags": False, @@ -159,7 +159,7 @@ def get_all_file_configs(config_dict: dict) -> List[FileConfig]: "search": config_dict["search"], "replace": config_dict["replace"], "ignore_missing_version": config_dict["ignore_missing_version"], - "no_regex": config_dict["no_regex"], + "regex": config_dict["regex"], } files = [{k: v for k, v in filecfg.items() if v is not None} for filecfg in config_dict["files"]] for f in files: diff --git a/bumpversion/files.py b/bumpversion/files.py index 546a07f9..f5e8b23f 100644 --- a/bumpversion/files.py +++ b/bumpversion/files.py @@ -3,7 +3,7 @@ import re from copy import deepcopy from difflib import context_diff -from typing import List, MutableMapping, Optional +from typing import List, MutableMapping, Optional, Tuple from bumpversion.config import FileConfig from bumpversion.exceptions import VersionNotFoundError @@ -27,7 +27,7 @@ def __init__( 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.no_regex = file_cfg.no_regex or False + self.regex = file_cfg.regex or False self.ignore_missing_version = file_cfg.ignore_missing_version or False self.version_config = VersionConfig( self.parse, self.serialize, self.search, self.replace, version_config.part_configs @@ -63,7 +63,7 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: Returns: True if the version number is in fact present. """ - search_expression = self.get_search_pattern(context) + search_expression, raw_search_expression = self.get_search_pattern(context) if self.contains(search_expression): return True @@ -84,7 +84,7 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: # version not found if self.ignore_missing_version: return False - raise VersionNotFoundError(f"Did not find '{search_expression.pattern}' in file: '{self.path}'") + raise VersionNotFoundError(f"Did not find '{raw_search_expression}' in file: '{self.path}'") def contains(self, search: re.Pattern) -> bool: """Does the work of the contains_version method.""" @@ -115,7 +115,7 @@ def replace_version( if new_version: context["new_version"] = self.version_config.serialize(new_version, context) - search_for = self.get_search_pattern(context) + search_for, raw_search_pattern = self.get_search_pattern(context) replace_with = self.version_config.replace.format(**context) file_content_after = search_for.sub(replace_with, file_content_before) @@ -123,7 +123,7 @@ def replace_version( 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 = self.get_search_pattern(og_context) + search_for_og, og_raw_search_pattern = self.get_search_pattern(og_context) file_content_after = search_for_og.sub(replace_with, file_content_before) if file_content_before != file_content_after: @@ -147,25 +147,26 @@ def replace_version( if not dry_run: # pragma: no-coverage self.write_file_contents(file_content_after) - def get_search_pattern(self, context: MutableMapping) -> re.Pattern: + def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]: """Compile and return the regex if it is valid, otherwise return the string.""" # the default search pattern is escaped, so we can still use it in a regex - default = re.compile(re.escape(self.version_config.search.format(**context)), re.MULTILINE | re.DOTALL) - if self.no_regex: + raw_pattern = self.version_config.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) - return default + return default, raw_pattern re_context = {key: re.escape(str(value)) for key, value in context.items()} regex_pattern = self.version_config.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) - return search_for_re + return search_for_re, raw_pattern except re.error as e: logger.error("Invalid regex '%s' for file %s: %s.", default, self.path, e) - logger.debug("Searching for the default pattern: '%s'", default.pattern) - return default + logger.debug("Searching for the default pattern: '%s'", raw_pattern) + return default, raw_pattern def __str__(self) -> str: # pragma: no-coverage return self.path diff --git a/docsrc/reference/search-and-replace-config.md b/docsrc/reference/search-and-replace-config.md index 1358fef5..d4dd4ecc 100644 --- a/docsrc/reference/search-and-replace-config.md +++ b/docsrc/reference/search-and-replace-config.md @@ -2,7 +2,7 @@ Bump-my-version uses a combination of [template strings](https://docs.python.org/3/library/string.html#format-string-syntax) using a [formatting context](formatting-context.md) and regular expressions to search the configured files for the old or current version and replace the text with the new version. -Bump My Version falls back to using a simple string search if the search template is not a valid regular expression or if the `no-regex` flag is `True`. The search template is always rendered using the formatting context. The basic logic is: +Bump My Version defaults to using a simple string search. If the search template is not a valid regular expression or if the `no-regex` flag is `True`. The search template is always rendered using the formatting context. The basic logic is: 1. Escape the formatting context for use in a regular expression. 2. Render the search string using the escaped formatting context. diff --git a/tests/fixtures/basic_cfg_expected.txt b/tests/fixtures/basic_cfg_expected.txt index 0333e2b7..c302b7f3 100644 --- a/tests/fixtures/basic_cfg_expected.txt +++ b/tests/fixtures/basic_cfg_expected.txt @@ -6,8 +6,8 @@ 'files': [{'filename': 'setup.py', 'glob': None, 'ignore_missing_version': False, - 'no_regex': False, 'parse': '(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?', + 'regex': False, 'replace': '{new_version}', 'search': '{current_version}', 'serialize': ['{major}.{minor}.{patch}-{release}', @@ -15,8 +15,8 @@ {'filename': 'bumpversion/__init__.py', 'glob': None, 'ignore_missing_version': False, - 'no_regex': False, 'parse': '(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?', + 'regex': False, 'replace': '{new_version}', 'search': '{current_version}', 'serialize': ['{major}.{minor}.{patch}-{release}', @@ -24,8 +24,8 @@ {'filename': 'CHANGELOG.md', 'glob': None, 'ignore_missing_version': False, - 'no_regex': False, 'parse': '(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?', + 'regex': False, 'replace': '**unreleased**\n**v{new_version}**', 'search': '**unreleased**', 'serialize': ['{major}.{minor}.{patch}-{release}', @@ -33,7 +33,6 @@ 'ignore_missing_version': False, 'included_paths': [], 'message': 'Bump version: {current_version} → {new_version}', - 'no_regex': False, 'parse': '(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?', 'parts': {'major': {'first_value': None, 'independent': False, @@ -51,6 +50,7 @@ 'independent': False, 'optional_value': 'gamma', 'values': ['dev', 'gamma']}}, + 'regex': False, 'replace': '{new_version}', 'scm_info': {'branch_name': None, 'commit_sha': None, diff --git a/tests/fixtures/basic_cfg_expected.yaml b/tests/fixtures/basic_cfg_expected.yaml index 4f2ef704..917b89bf 100644 --- a/tests/fixtures/basic_cfg_expected.yaml +++ b/tests/fixtures/basic_cfg_expected.yaml @@ -8,8 +8,8 @@ files: - filename: "setup.py" glob: null ignore_missing_version: false - no_regex: false parse: "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?" + regex: false replace: "{new_version}" search: "{current_version}" serialize: @@ -18,8 +18,8 @@ files: - filename: "bumpversion/__init__.py" glob: null ignore_missing_version: false - no_regex: false parse: "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?" + regex: false replace: "{new_version}" search: "{current_version}" serialize: @@ -28,8 +28,8 @@ files: - filename: "CHANGELOG.md" glob: null ignore_missing_version: false - no_regex: false parse: "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?" + regex: false replace: "**unreleased**\n**v{new_version}**" search: "**unreleased**" serialize: @@ -39,7 +39,6 @@ ignore_missing_version: false included_paths: message: "Bump version: {current_version} → {new_version}" -no_regex: false parse: "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?" parts: major: @@ -64,6 +63,7 @@ parts: values: - "dev" - "gamma" +regex: false replace: "{new_version}" scm_info: branch_name: null diff --git a/tests/fixtures/basic_cfg_expected_full.json b/tests/fixtures/basic_cfg_expected_full.json index 51335519..0ebc34a9 100644 --- a/tests/fixtures/basic_cfg_expected_full.json +++ b/tests/fixtures/basic_cfg_expected_full.json @@ -9,8 +9,8 @@ "filename": "setup.py", "glob": null, "ignore_missing_version": false, - "no_regex": false, "parse": "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", + "regex": false, "replace": "{new_version}", "search": "{current_version}", "serialize": [ @@ -22,8 +22,8 @@ "filename": "bumpversion/__init__.py", "glob": null, "ignore_missing_version": false, - "no_regex": false, "parse": "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", + "regex": false, "replace": "{new_version}", "search": "{current_version}", "serialize": [ @@ -35,8 +35,8 @@ "filename": "CHANGELOG.md", "glob": null, "ignore_missing_version": false, - "no_regex": false, "parse": "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", + "regex": false, "replace": "**unreleased**\n**v{new_version}**", "search": "**unreleased**", "serialize": [ @@ -48,7 +48,6 @@ "ignore_missing_version": false, "included_paths": [], "message": "Bump version: {current_version} \u2192 {new_version}", - "no_regex": false, "parse": "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", "parts": { "major": { @@ -79,6 +78,7 @@ ] } }, + "regex": false, "replace": "{new_version}", "scm_info": { "branch_name": null, diff --git a/tests/test_cli.py b/tests/test_cli.py index 951654c4..2c831e04 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -180,7 +180,7 @@ def test_dirty_work_dir_raises_error(repo: str, scm_command: str, request): def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path): - """The --list option should list the configuration with new_version..""" + """The --list option should list the configuration with new_version.""" # Arrange config_path = tmp_path / "pyproject.toml" toml_path = fixtures_path / "basic_cfg.toml" @@ -206,7 +206,7 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path): "serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']", "search={current_version}", "replace={new_version}", - "no_regex=False", + "regex=False", "ignore_missing_version=False", "included_paths=[]", "tag=True", @@ -222,15 +222,15 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path): "{'filename': 'setup.py', 'glob': None, 'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', 'serialize': " "['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'], 'search': '{current_version}', " - "'replace': '{new_version}', 'no_regex': False, 'ignore_missing_version': False}, " + "'replace': '{new_version}', 'regex': False, 'ignore_missing_version': False}, " "{'filename': 'bumpversion/__init__.py', 'glob': None, 'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', 'serialize': " "['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'], 'search': '{current_version}', " - "'replace': '{new_version}', 'no_regex': False, 'ignore_missing_version': False}, " + "'replace': '{new_version}', 'regex': False, 'ignore_missing_version': False}, " "{'filename': 'CHANGELOG.md', 'glob': None, 'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', 'serialize': " "['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'], 'search': '**unreleased**', " - "'replace': '**unreleased**\\n**v{new_version}**', 'no_regex': False, 'ignore_missing_version': False}]" + "'replace': '**unreleased**\\n**v{new_version}**', 'regex': False, 'ignore_missing_version': False}]" ), } @@ -261,7 +261,7 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path): "serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']", "search={current_version}", "replace={new_version}", - "no_regex=False", + "regex=False", "ignore_missing_version=False", "included_paths=[]", "tag=True", @@ -277,15 +277,15 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path): "{'filename': 'setup.py', 'glob': None, 'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', 'serialize': " "['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'], 'search': '{current_version}', " - "'replace': '{new_version}', 'no_regex': False, 'ignore_missing_version': False}, " + "'replace': '{new_version}', 'regex': False, 'ignore_missing_version': False}, " "{'filename': 'bumpversion/__init__.py', 'glob': None, 'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', 'serialize': " "['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'], 'search': '{current_version}', " - "'replace': '{new_version}', 'no_regex': False, 'ignore_missing_version': False}, " + "'replace': '{new_version}', 'regex': False, 'ignore_missing_version': False}, " "{'filename': 'CHANGELOG.md', 'glob': None, 'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', 'serialize': " "['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'], 'search': '**unreleased**', " - "'replace': '**unreleased**\\n**v{new_version}**', 'no_regex': False, 'ignore_missing_version': False}]" + "'replace': '**unreleased**\\n**v{new_version}**', 'regex': False, 'ignore_missing_version': False}]" ), } @@ -538,7 +538,7 @@ def test_valid_regex_not_ignoring_regex(tmp_path: Path, caplog) -> None: "allow_dirty = true\n\n" "[[tool.bumpversion.files]]\n" 'filename = "VERSION"\n' - "no_regex = true\n" + "regex = false\n" f'search = "{search}"\n' f'replace = "{replace}"\n' ) diff --git a/tests/test_files.py b/tests/test_files.py index ff2b716b..f7344b38 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -363,6 +363,7 @@ def test_regex_search(tmp_path: Path) -> None: version_path.write_text("Release: 1234-56-78 '1.2.3'") overrides = { + "regex": True, "current_version": "1.2.3", "search": r"Release: \d{{4}}-\d{{2}}-\d{{2}} '{current_version}'", "replace": r"Release {now:%Y-%m-%d} '{new_version}'", @@ -387,6 +388,7 @@ def test_regex_search_with_escaped_chars(tmp_path: Path) -> None: version_path.write_text("## [Release] 1.2.3 1234-56-78") overrides = { + "regex": True, "current_version": "1.2.3", "search": r"## \[Release\] {current_version} \d{{4}}-\d{{2}}-\d{{2}}", "replace": r"## [Release] {new_version} {now:%Y-%m-%d}", @@ -411,6 +413,7 @@ def test_bad_regex_search(tmp_path: Path, caplog) -> None: version_path.write_text("Score: A+ ( '1.2.3'") overrides = { + "regex": True, "current_version": "1.2.3", "search": r"Score: A+ ( '{current_version}'", "replace": r"Score: A+ ( '{new_version}'",