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

Issue:
Glob pattern with dry-run on permitted mismatch causes indented output 
* bump-my-version version: 0.20.2
* Python version: 3.10.13
* Operating System: Ubuntu 20.04 LTS

### Description

When a glob pattern is used to match multiple files, and that files without any matched version search pattern are permitted, the dry run output becomes more deeply nested on each new unmatched case.

Sample config: 
```

❯ bump-my-version bump major --dry-run --verbose

Starting BumpVersion 0.20.2

Reading configuration

  Reading config file: /home/francis/dev/stac/mlm-extension/pyproject.toml

  Specified version (0.0.0) does not match last tagged version (1.0.0-beta.3)

  Parsing current version '0.0.0'

  Attempting to increment part 'major'

    Values are now: major=1, minor=0, patch=0

  New version will be '1.0.0'

Dry run active, won't touch any files.

[.... skipping a few entries that are rendered correctly ...]

  File CONTRIBUTING.md: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

    File README_DLM_LEGACY.md: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

      File CHANGELOG.md: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

        File README.md: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

          Found 'https://crim\-ca\.github\.io/mlm\-extension/v0\.0\.0/schema\.json' at line 6: https://crim-ca.github.io/mlm-extension/v0.0.0/schema.json

          Would change file README.md:

            *** before README.md

            --- after README.md

            ***************

            *** 3,9 ****

              [![hackmd-github-sync-badge](https://hackmd.io/lekSD_RVRiquNHRloXRzeg/badge)](https://hackmd.io/lekSD_RVRiquNHRloXRzeg?both)

              - **Title:** Machine Learning Model Extension

            ! - **Identifier:** [https://crim-ca.github.io/mlm-extension/v0.0.0/schema.json](https://crim-ca.github.io/mlm-extension/v0.0.0/schema.json)

              - **Field Name Prefix:** mlm

              - **Scope:** Collection, Item, Asset, Links

              - **Extension Maturity Classification:** Proposal

            --- 3,9 ----

              [![hackmd-github-sync-badge](https://hackmd.io/lekSD_RVRiquNHRloXRzeg/badge)](https://hackmd.io/lekSD_RVRiquNHRloXRzeg?both)

              - **Title:** Machine Learning Model Extension

            ! - **Identifier:** [https://crim-ca.github.io/mlm-extension/v1.0.0/schema.json](https://crim-ca.github.io/mlm-extension/v1.0.0/schema.json)

              - **Field Name Prefix:** mlm

              - **Scope:** Collection, Item, Asset, Links

              - **Extension Maturity Classification:** Proposal

        File README_STAC_MODEL.md: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

          File best-practices.md: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

            Found 'https://crim\-ca\.github\.io/mlm\-extension/v0\.0\.0/schema\.json' at line 148: https://crim-ca.github.io/mlm-extension/v0.0.0/schema.json

            Would change file best-practices.md:

              *** before best-practices.md

              --- after best-practices.md

              ***************

              *** 145,151 ****

                {

                  "id": "stac-item-model",

                  "stac_extensions": [

              !     "https://crim-ca.github.io/mlm-extension/v0.0.0/schema.json",

                    "https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json"

                  ],

                  "assets": {

              --- 145,151 ----

                {

                  "id": "stac-item-model",

                  "stac_extensions": [

              !     "https://crim-ca.github.io/mlm-extension/v1.0.0/schema.json",

                    "https://stac-extensions.github.io/ml-aoi/v0.2.0/schema.json"

                  ],

                  "assets": {

              ***************

              *** 216,222 ****

                ```json

                {

                  "stac_extensions": [

              !     "https://crim-ca.github.io/mlm-extension/v0.0.0/schema.json",

                    "https://stac-extensions.github.io/file/v2.1.0/schema.json"

                  ],

                  "assets": {

              --- 216,222 ----

                ```json

                {

                  "stac_extensions": [

              !     "https://crim-ca.github.io/mlm-extension/v1.0.0/schema.json",

                    "https://stac-extensions.github.io/file/v2.1.0/schema.json"

                  ],

                  "assets": {

          File docker/README.md: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

            File tests/test_schema.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

              File tests/conftest.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                File stac_model/__init__.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                  File stac_model/schema.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                    Found 'https://crim\-ca\.github\.io/mlm\-extension/v0\.0\.0/schema\.json' at line 40: https://crim-ca.github.io/mlm-extension/v0.0.0/schema.json

                    Would change file stac_model/schema.py:

                      *** before stac_model/schema.py

                      --- after stac_model/schema.py

                      ***************

                      *** 37,43 ****

                        )

                        SchemaName = Literal["mlm"]

                      ! SCHEMA_URI: str = "https://crim-ca.github.io/mlm-extension/v0.0.0/schema.json"

                        PREFIX = f"{get_args(SchemaName)[0]}:"

                      --- 37,43 ----

                        )

                        SchemaName = Literal["mlm"]

                      ! SCHEMA_URI: str = "https://crim-ca.github.io/mlm-extension/v1.0.0/schema.json"

                        PREFIX = f"{get_args(SchemaName)[0]}:"

                  File stac_model/runtime.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                    File stac_model/__main__.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                      File stac_model/input.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                        File stac_model/base.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                          File stac_model/examples.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                            File stac_model/output.py: replace `https://crim-ca.github.io/mlm-extension/v{current_version}/schema.json` with `https://crim-ca.github.io/mlm-extension/v{new_version}/schema.json`

                              Processing config file: /home/francis/dev/stac/mlm-extension/pyproject.toml

                                Found '0\.0\.0' at line 1: 0.0.0

                                Would change file /home/francis/dev/stac/mlm-extension/pyproject.toml:tool.bumpversion.current_version:

                                  *** before /home/francis/dev/stac/mlm-extension/pyproject.toml:tool.bumpversion.current_version

                                  --- after /home/francis/dev/stac/mlm-extension/pyproject.toml:tool.bumpversion.current_version

                                  ***************

                                  *** 1 ****

                                  ! 0.0.0

                                  --- 1 ----

                                  ! 1.0.0

                              Would prepare Git commit

                                Would add changes in file 'README_STAC_MODEL.md' to Git

                                Would add changes in file 'stac_model/base.py' to Git

                                Would add changes in file 'examples/item_raster_bands.json' to Git

                                Would add changes in file 'best-practices.md' to Git

                                Would add changes in file 'examples/collection.json' to Git

                                Would add changes in file 'tests/test_schema.py' to Git

                                Would add changes in file 'json-schema/schema.json' to Git

                                Would add changes in file 'docker/README.md' to Git

                                Would add changes in file 'CHANGELOG.md' to Git

                                Would add changes in file 'tests/conftest.py' to Git

                                Would add changes in file 'README_DLM_LEGACY.md' to Git

                                Would add changes in file 'examples/item_multi_io.json' to Git

                                Would add changes in file 'package.json' to Git

                                Would add changes in file 'stac_model/schema.py' to Git

                                Would add changes in file 'CONTRIBUTING.md' to Git

                                Would add changes in file 'stac_model/output.py' to Git

                                Would add changes in file 'stac_model/__init__.py' to Git

                                Would add changes in file 'examples/item_eo_bands.json' to Git

                                Would add changes in file 'stac_model/input.py' to Git

                                Would add changes in file '/home/francis/dev/stac/mlm-extension/pyproject.toml' to Git

                                Would add changes in file 'stac_model/runtime.py' to Git

                                Would add changes in file 'stac_model/__main__.py' to Git

                                Would add changes in file 'README.md' to Git

                                Would add changes in file 'stac_model/examples.py' to Git

                                Would add changes in file 'examples/item_basic.json' to Git

                                Would commit to Git with message 'Bump version: 0.0.0 → 1.0.0'

                              Would tag 'v1.0.0' with message 'Bump version: 0.0.0 → 1.0.0' in Git and not signing

                              Done.                    
```


Pull request:
Fixed the indentation problem
- Added a dedent when a file does not match the change pattern.
- Fixes #181

Patch:
diff --git a/bumpversion/files.py b/bumpversion/files.py
index a39a234c..4376cf77 100644
--- a/bumpversion/files.py
+++ b/bumpversion/files.py
@@ -183,15 +183,9 @@ class ConfiguredFile:
                 logger.dedent()
                 return
             raise FileNotFoundError(f"File not found: '{self.file_change.filename}'")  # pragma: no-coverage
-        logger.debug("Serializing the current version")
-        logger.indent()
-        context["current_version"] = self.version_config.serialize(current_version, context)
-        logger.dedent()
+        context["current_version"] = self._get_serialized_version("current_version", current_version, context)
         if new_version:
-            logger.debug("Serializing the new version")
-            logger.indent()
-            context["new_version"] = self.version_config.serialize(new_version, context)
-            logger.dedent()
+            context["new_version"] = self._get_serialized_version("new_version", new_version, context)
         else:
             logger.debug("No new version, using current version as new version")
             context["new_version"] = context["current_version"]
@@ -200,6 +194,7 @@ class ConfiguredFile:
         replace_with = self.version_config.replace.format(**context)
 
         if not self._contains_change_pattern(search_for, raw_search_pattern, current_version, context):
+            logger.dedent()
             return
 
         file_content_before = self.get_file_contents()
@@ -217,6 +212,14 @@ class ConfiguredFile:
         if not dry_run:  # pragma: no-coverage
             self.write_file_contents(file_content_after)
 
+    def _get_serialized_version(self, context_key: str, version: Version, context: MutableMapping) -> str:
+        """Get the serialized version."""
+        logger.debug("Serializing the %s", context_key.replace("_", " "))
+        logger.indent()
+        serialized_version = self.version_config.serialize(version, context)
+        logger.dedent()
+        return serialized_version
+
     def __str__(self) -> str:  # pragma: no-coverage
         return self.file_change.filename
 


Test patch:
diff --git a/tests/test_files.py b/tests/test_files.py
index f6bf33fd..ec0576b1 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -1,5 +1,6 @@
 """File processing tests."""
 
+import logging
 import os
 import shutil
 from datetime import datetime, timezone
@@ -15,10 +16,139 @@ from bumpversion import exceptions, files, config, bump
 from bumpversion.config.models import FileChange
 from bumpversion.context import get_context
 from bumpversion.exceptions import VersionNotFoundError
+from bumpversion.ui import setup_logging, get_indented_logger
 from bumpversion.version_part import VersionConfig
 from tests.conftest import get_config_data, inside_dir
 
 
+class TestConfiguredFile:
+    """Tests of the ConfiguredFile class."""
+
+    class TestGetFileContents:
+        """Tests of the get_file_content method."""
+
+        def test_returns_contents_of_existing_file(self, tmp_path: Path) -> None:
+            """The get_file_content method returns the contents of an existing file."""
+            # Arrange
+            filepath = tmp_path / "file.md"
+            expected_data = "My current version is 1.2.3"
+            filepath.write_text(expected_data, encoding="utf-8")
+            overrides = {
+                "current_version": "1.2.3",
+                "files": [{"filename": str(filepath)}],
+            }
+            conf, version_config, current_version = get_config_data(overrides)
+            assert len(conf.files_to_modify) == 1
+            assert conf.files_to_modify[0].filename == str(filepath)
+
+            # Act
+            configured_file = files.ConfiguredFile(conf.files_to_modify[0], version_config)
+
+            # Assert
+            assert configured_file.get_file_contents() == expected_data
+
+        def test_raises_exception_if_file_not_found(self, tmp_path: Path) -> None:
+            """The get_file_content method raises an exception if the file does not exist."""
+            filepath = tmp_path / "file.md"
+            overrides = {
+                "current_version": "1.2.3",
+                "files": [{"filename": str(filepath)}],
+            }
+            conf, version_config, current_version = get_config_data(overrides)
+            assert len(conf.files_to_modify) == 1
+            assert conf.files_to_modify[0].filename == str(filepath)
+            configured_file = files.ConfiguredFile(conf.files_to_modify[0], version_config)
+
+            # Act
+            with pytest.raises(FileNotFoundError):
+                configured_file.get_file_contents()
+
+    class TestMakeFileChange:
+        """Tests of the make_file_change function."""
+
+        def test_raises_exception_if_file_not_found(self, tmp_path: Path) -> None:
+            """The make_file_change method raises an exception if the file does not exist."""
+            # Assemble
+            filepath = tmp_path / "file.md"
+            overrides = {
+                "current_version": "1.2.3",
+                "files": [{"filename": str(filepath)}],
+            }
+            conf, version_config, current_version = get_config_data(overrides)
+            configured_file = files.ConfiguredFile(conf.files_to_modify[0], version_config)
+            new_version = current_version.bump("patch")
+            ctx = get_context(conf)
+
+            # Act
+            with pytest.raises(FileNotFoundError):
+                configured_file.make_file_change(current_version, new_version, ctx, dry_run=True)
+
+        def test_logs_missing_file_when_ignore_missing_file_set(self, tmp_path: Path, caplog) -> None:
+            """The make_file_change method logs missing file when ignore_missing_file set to True."""
+            # Assemble
+            setup_logging(1)
+            caplog.set_level(logging.INFO)
+            logger = get_indented_logger(__name__)
+            logger.reset()
+            filepath = tmp_path / "file.md"
+            overrides = {
+                "current_version": "1.2.3",
+                "files": [{"filename": str(filepath), "ignore_missing_file": True}],
+            }
+            conf, version_config, current_version = get_config_data(overrides)
+            configured_file = files.ConfiguredFile(conf.files_to_modify[0], version_config)
+            new_version = current_version.bump("patch")
+            ctx = get_context(conf)
+
+            # Act
+            configured_file.make_file_change(current_version, new_version, ctx, dry_run=True)
+
+            # Assert
+            logs = caplog.messages
+            assert logs[0] == "Reading configuration"
+            assert logs[1] == "  Configuration file not found: missing."
+            assert logs[2] == f"\nFile {filepath}: replace `{{current_version}}` with `{{new_version}}`"
+            assert logs[3] == "  File not found, but ignoring"
+
+        def test_dedents_properly_when_file_does_not_contain_pattern(self, fixtures_path: Path, caplog) -> None:
+            """The make_file_change method dedents the logging context when it does not contain a pattern."""
+            # Assemble
+            setup_logging(1)
+            caplog.set_level(logging.INFO)
+            logger = get_indented_logger(__name__)
+            logger.reset()
+            globs = fixtures_path / "glob" / "**/*.txt"
+            overrides = {
+                "current_version": "1.2.3",
+                "files": [{"glob": str(globs), "ignore_missing_file": True, "ignore_missing_version": True}],
+            }
+            conf, version_config, current_version = get_config_data(overrides)
+            configured_file1 = files.ConfiguredFile(conf.files_to_modify[0], version_config)
+            configured_file2 = files.ConfiguredFile(conf.files_to_modify[1], version_config)
+            configured_file3 = files.ConfiguredFile(conf.files_to_modify[2], version_config)
+            new_version = current_version.bump("patch")
+            ctx = get_context(conf)
+
+            # Act
+            configured_file1.make_file_change(current_version, new_version, ctx, dry_run=True)
+            configured_file2.make_file_change(current_version, new_version, ctx, dry_run=True)
+            configured_file3.make_file_change(current_version, new_version, ctx, dry_run=True)
+
+            # Assert
+            logs = caplog.messages
+            assert logs[0] == "Reading configuration"
+            assert logs[1] == "  Configuration file not found: missing."
+            assert logs[2] == (
+                f"\nFile {configured_file1.file_change.filename}: replace `{{current_version}}` with `{{new_version}}`"
+            )
+            assert logs[3] == (
+                f"\nFile {configured_file2.file_change.filename}: replace `{{current_version}}` with `{{new_version}}`"
+            )
+            assert logs[4] == (
+                f"\nFile {configured_file3.file_change.filename}: replace `{{current_version}}` with `{{new_version}}`"
+            )
+
+
 def test_single_file_processed_twice(tmp_path: Path):
     """
     Verify that a single file "file2" can be processed twice.