Repository: callowayproject/bump-my-version (python)

Issue:
Global `regex` configuration not inherited in sub-sections
* bump-my-version version: 0.10.0
* Python version: 3.11.15
* Operating System: macOS

### Description

The `no_regex` parameter from the top-level section of a configuration file is not inherited in sub-sections.

### What I Did

Here is a `changelog.md` file containing:
```md
# Changelog

## {gh}`4.7.1 (unreleased) <compare/v4.7.0...main>`

- Rely on `bump-my-version` to update metadata.
```

With a `pyproject.toml` file containing:
```toml
[tool.bumpversion]
current_version = "4.7.1"
no_regex = true

[[tool.bumpversion.files]]
filename = "./changelog.md"
search = "{{gh}}`{current_version} (unreleased)"
replace = "{{gh}}`{new_version} (unreleased)"
```

Invoking the CLI, I get:
```shell-session
$ bump-my-version bump --dry-run --verbose patch
Starting BumpVersion 0.10.0                                                                                                                                                                                         
Reading config file pyproject.toml:                                                                                                                                                                                 
Parsing version '4.7.1' using regexp '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)'                                                                                                                               
Parsed the following values: major=4, minor=7, patch=1                                                                                                                                                              
Attempting to increment part 'patch'                                                                                                                                                                                
Values are now: major=4, minor=7, patch=2                                                                                                                                                                           
New version will be '4.7.2'                                                                                                                                                                                         
Dry run active, won't touch any files.                                                                                                                                                                              
Asserting files ./changelog.md contain the version string...                                                                                                                                                        
Found 're.compile('4\\.7\\.1')' in ./changelog.md at line 3: 4.7.1                                                                                                                                                  
Would not change file ./changelog.md                                                                                                                                                                                
Would write to config file pyproject.toml:                                                                                                                                                                          
*** before pyproject.toml                                                                                                                                                                                           
--- after pyproject.toml                                                                                                                                                                                            
***************                                                                                                                                                                                                     
*** 1,5 ****                                                                                                                                                                                                        
  [tool.bumpversion]                                                                                                                                                                                                
! current_version = "4.7.1"                                                                                                                                                                                         
  no_regex = true                                                                                                                                                                                                   
                                                                                                                                                                                                                    
  [[tool.bumpversion.files]]                                                                                                                                                                                        
--- 1,5 ----                                                                                                                                                                                                        
  [tool.bumpversion]                                                                                                                                                                                                
! current_version = "4.7.2"                                                                                                                                                                                         
  no_regex = true                                                                                                                                                                                                   
                                                                                                                                                                                                                    
  [[tool.bumpversion.files]]   
```

Here you can see the `./changelog.md` file is left untouched. But I expect instead to have its version updated.

### Solution

To fix this behaviour, I had to duplicate the `no_regex = true` parameter from the top-level `[tool.bumpversion]` section to the `[[tool.bumpversion.files]]`.

So with the following `pyproject.toml` file:
```toml
[tool.bumpversion]
current_version = "4.7.1"
no_regex = true

[[tool.bumpversion.files]]
filename = "./changelog.md"
no_regex = true
search = "{{gh}}`{current_version} (unreleased)"
replace = "{{gh}}`{new_version} (unreleased)"
```

I now get the behavior I was looking for:
```shell-session
$ bump-my-version bump --dry-run --verbose patch
Starting BumpVersion 0.10.0                                                                                                                                                                                         
Reading config file pyproject.toml:                                                                                                                                                                                 
Parsing version '4.7.1' using regexp '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)'                                                                                                                               
Parsed the following values: major=4, minor=7, patch=1                                                                                                                                                              
Attempting to increment part 'patch'                                                                                                                                                                                
Values are now: major=4, minor=7, patch=2                                                                                                                                                                           
New version will be '4.7.2'                                                                                                                                                                                         
Dry run active, won't touch any files.                                                                                                                                                                              
Asserting files ./changelog.md contain the version string...                                                                                                                                                        
Found 're.compile('\\{gh\\}`4\\.7\\.1\\ \\(unreleased\\)', re.MULTILINE|re.DOTALL)' in ./changelog.md at line 3: {gh}`4.7.1 (unreleased)                                                                            
Would change file ./changelog.md:                                                                                                                                                                                   
*** before ./changelog.md                                                                                                                                                                                           
--- after ./changelog.md                                                                                                                                                                                            
***************                                                                                                                                                                                                     
*** 1,5 ****                                                                                                                                                                                                        
  # Changelog                                                                                                                                                                                                       
                                                                                                                                                                                                                    
! ## {gh}`4.7.1 (unreleased) <compare/v4.7.0...main>`                                                                                                                                                               
                                                                                                                                                                                                                    
  - Rely on `bump-my-version` to update metadata.                                                                                                                                                                   
--- 1,5 ----                                                                                                                                                                                                        
  # Changelog                                                                                                                                                                                                       
                                                                                                                                                                                                                    
! ## {gh}`4.7.2 (unreleased) <compare/v4.7.0...main>`                                                                                                                                                               
                                                                                                                                                                                                                    
  - Rely on `bump-my-version` to update metadata.                                                                                                                                                                   
Would write to config file pyproject.toml:                                                                                                                                                                          
*** before pyproject.toml                                                                                                                                                                                           
--- after pyproject.toml                                                                                                                                                                                            
***************                                                                                                                                                                                                     
*** 1,5 ****                                                                                                                                                                                                        
  [tool.bumpversion]                                                                                                                                                                                                
! current_version = "4.7.1"                                                                                                                                                                                         
  no_regex = true                                                                                                                                                                                                   
                                                                                                                                                                                                                    
  [[tool.bumpversion.files]]                                                                                                                                                                                        
--- 1,5 ----                                                                                                                                                                                                        
  [tool.bumpversion]                                                                                                                                                                                                
! current_version = "4.7.2"                                                                                                                                                                                         
  no_regex = true                                                                                                                                                                                                   
                                                                                                                                                                                                                    
  [[tool.bumpversion.files]]                                                                                                                                                                                        
```

### Discussion

This demonstrate that I was expecting the `no_regex` parameter from the top-level to be inherited by default in the sub-sections.

I think this is the behavior that should be implemented. In the same vein as #59 (in which we discuss defaulting to `no-regex` mode), I think this behavior will reduce surprises for newcomers, make configuration files more readable, and increase the overall user-friendlyness of bump-my-version.


Pull request:
Fixes global regex value
Fixes #64

Changed default regex CLI value to None.

The default value of False was overriding other values.

Patch:
diff --git a/bumpversion/cli.py b/bumpversion/cli.py
index 7b9cc86e..bcb08d26 100644
--- a/bumpversion/cli.py
+++ b/bumpversion/cli.py
@@ -141,7 +141,7 @@ click.rich_click.OPTION_GROUPS = {
 )
 @click.option(
     "--regex/--no-regex",
-    default=False,
+    default=None,
     envvar="BUMPVERSION_REGEX",
     help="Treat the search parameter as a regular expression or explicitly do not treat it as a regular expression.",
 )
@@ -232,7 +232,7 @@ def bump(
     serialize: Optional[List[str]],
     search: Optional[str],
     replace: Optional[str],
-    regex: bool,
+    regex: Optional[bool],
     no_configured_files: bool,
     ignore_missing_version: bool,
     dry_run: bool,
diff --git a/bumpversion/files.py b/bumpversion/files.py
index efccdd3a..a5f8c1e1 100644
--- a/bumpversion/files.py
+++ b/bumpversion/files.py
@@ -1,4 +1,5 @@
 """Methods for changing files."""
+import os.path
 import re
 from copy import deepcopy
 from difflib import context_diff
@@ -94,7 +95,18 @@ class ConfiguredFile:
         self._newlines: Optional[str] = None
 
     def get_file_contents(self) -> str:
-        """Return the contents of the file."""
+        """
+        Return the contents of the file.
+
+        Raises:
+            FileNotFoundError: if the file doesn't exist
+
+        Returns:
+            The contents of the file
+        """
+        if not os.path.exists(self.file_change.filename):
+            raise FileNotFoundError(f"File not found: '{self.file_change.filename}'")
+
         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
diff --git a/bumpversion/version_part.py b/bumpversion/version_part.py
index 6058fd59..13430d35 100644
--- a/bumpversion/version_part.py
+++ b/bumpversion/version_part.py
@@ -150,6 +150,18 @@ class VersionConfig:
         self.search = search
         self.replace = replace
 
+    def __repr__(self) -> str:
+        return f"<bumpversion.VersionConfig:{self.parse_regex.pattern}:{self.serialize_formats}>"
+
+    def __eq__(self, other: Any) -> bool:
+        return (
+            self.parse_regex.pattern == other.parse_regex.pattern
+            and self.serialize_formats == other.serialize_formats
+            and self.part_configs == other.part_configs
+            and self.search == other.search
+            and self.replace == other.replace
+        )
+
     @property
     def order(self) -> List[str]:
         """


Test patch:
diff --git a/tests/fixtures/basic_cfg.toml b/tests/fixtures/basic_cfg.toml
index a1b14936..7808ae95 100644
--- a/tests/fixtures/basic_cfg.toml
+++ b/tests/fixtures/basic_cfg.toml
@@ -41,6 +41,6 @@ replace = """**unreleased**
 [tool.bumpversion.parts.release]
 optional_value = "gamma"
 values =[
-	"dev",
-	"gamma",
+    "dev",
+    "gamma",
 ]
diff --git a/tests/fixtures/regex_test_config.toml b/tests/fixtures/regex_test_config.toml
new file mode 100644
index 00000000..46eeca1a
--- /dev/null
+++ b/tests/fixtures/regex_test_config.toml
@@ -0,0 +1,8 @@
+[tool.bumpversion]
+current_version = "4.7.1"
+regex = true
+
+[[tool.bumpversion.files]]
+filename = "./citation.cff"
+search = "date-released: \\d{{4}}-\\d{{2}}-\\d{{2}}"
+replace = "date-released: {utcnow:%Y-%m-%d}"
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 14314cac..638d7df4 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -2,6 +2,7 @@
 import shutil
 import subprocess
 import traceback
+from datetime import datetime
 from pathlib import Path
 
 import pytest
@@ -45,6 +46,32 @@ def test_bump_no_configured_files(mocker, tmp_path):
     assert len(call_args[2].files) == 0
 
 
+def test_bump_nested_regex(tmp_path: Path, fixtures_path: Path, caplog):
+    """
+    Arrange/Act: Run the `bump` subcommand with --no-configured-files.
+
+    Assert: There is no configured files specified to modify
+    """
+    cff_path = tmp_path / "citation.cff"
+    cff_path.write_text("cff-version: 1.2.0\ndate-released: 2023-09-19\n")
+    content = fixtures_path.joinpath("regex_test_config.toml").read_text()
+    config_path = tmp_path / ".bumpversion.toml"
+    config_path.write_text(content)
+
+    runner: CliRunner = CliRunner()
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["bump", "-vv", "patch"])
+
+    if result.exit_code != 0:
+        print(result.output)
+        print(caplog.text)
+
+    assert result.exit_code == 0
+
+    now = datetime.now().isoformat()[:10]
+    assert cff_path.read_text() == f"cff-version: 1.2.0\ndate-released: {now}\n"
+
+
 def test_bump_legacy(mocker, tmp_path):
     """
     Arrange/Act: Run the `bump` subcommand with --no-configured-files.