Repository: callowayproject/bump-my-version
Pull request #292 (2025-02-02 15:03:20)

Fix type hinting incompatibility in Python 3.9
Refactor to use Pathlike type alias for path representation

Unified path type handling across the codebase by introducing the `Pathlike` type alias (`Union[str, Path]`). This improves readability and consistency in path-related functions and methods, reducing redundancy. Updated corresponding type annotations, imports, and tests accordingly.


Patch:
diff --git a/bumpversion/config/__init__.py b/bumpversion/config/__init__.py
index f9645921..8ec98b44 100644
--- a/bumpversion/config/__init__.py
+++ b/bumpversion/config/__init__.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Union
+from typing import TYPE_CHECKING, Any, Optional
 
 from bumpversion.config.files import read_config_file
 from bumpversion.config.models import Config
@@ -10,7 +10,7 @@ from bumpversion.exceptions import ConfigurationError
 from bumpversion.ui import get_indented_logger
 
 if TYPE_CHECKING:  # pragma: no-coverage
-    from pathlib import Path
+    from bumpversion.utils import Pathlike
 
 logger = get_indented_logger(__name__)
 
@@ -54,7 +54,7 @@ def set_config_defaults(parsed_config: dict[str, Any], **overrides: Any) -> dict
     return config_dict
 
 
-def get_configuration(config_file: Union[str, Path, None] = None, **overrides: Any) -> Config:
+def get_configuration(config_file: Optional[Pathlike] = None, **overrides: Any) -> Config:
     """
     Return the configuration based on any configuration files and overrides.
 
diff --git a/bumpversion/config/files.py b/bumpversion/config/files.py
index b29a53e0..d4daf35d 100644
--- a/bumpversion/config/files.py
+++ b/bumpversion/config/files.py
@@ -3,13 +3,14 @@
 from __future__ import annotations
 
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Dict, MutableMapping, Union
+from typing import TYPE_CHECKING, Any, Dict, MutableMapping, Optional, Union
 
 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.utils import Pathlike
     from bumpversion.versioning.models import Version
 
 logger = get_indented_logger(__name__)
@@ -22,7 +23,7 @@ CONFIG_FILE_SEARCH_ORDER = (
 )
 
 
-def find_config_file(explicit_file: Union[str, Path, None] = None) -> Union[Path, None]:
+def find_config_file(explicit_file: Optional[Pathlike] = None) -> Union[Path, None]:
     """
     Find the configuration file, if it exists.
 
@@ -48,7 +49,7 @@ def find_config_file(explicit_file: Union[str, Path, None] = None) -> Union[Path
     )
 
 
-def read_config_file(config_file: Union[str, Path, None] = None) -> Dict[str, Any]:
+def read_config_file(config_file: Optional[Pathlike] = None) -> Dict[str, Any]:
     """
     Read the configuration file, if it exists.
 
diff --git a/bumpversion/scm/git.py b/bumpversion/scm/git.py
index 3b6cd282..e023aaf6 100644
--- a/bumpversion/scm/git.py
+++ b/bumpversion/scm/git.py
@@ -11,7 +11,7 @@ from typing import Any, ClassVar, MutableMapping, Optional
 from bumpversion.exceptions import DirtyWorkingDirectoryError
 from bumpversion.scm.models import LatestTagInfo, SCMConfig
 from bumpversion.ui import get_indented_logger
-from bumpversion.utils import format_and_raise_error, is_subpath, run_command
+from bumpversion.utils import Pathlike, format_and_raise_error, is_subpath, run_command
 
 logger = get_indented_logger(__name__)
 
@@ -60,7 +60,7 @@ class Git:
         self._latest_tag_info = LatestTagInfo(**info)
         return self._latest_tag_info
 
-    def add_path(self, path: str | Path) -> None:
+    def add_path(self, path: Pathlike) -> None:
         """Add a path to the VCS."""
         repository_root = self.latest_tag_info().repository_root
         if not (repository_root and is_subpath(repository_root, path)):
@@ -86,7 +86,7 @@ class Git:
         ):
             return []
 
-    def commit_and_tag(self, files: list[Path | str], context: MutableMapping, dry_run: bool = False) -> None:
+    def commit_and_tag(self, files: list[Pathlike], context: MutableMapping, dry_run: bool = False) -> None:
         """Commit and tag files to the repository using the configuration."""
         if dry_run:
             return
diff --git a/bumpversion/scm/hg.py b/bumpversion/scm/hg.py
index dc61e89c..7a9718b2 100644
--- a/bumpversion/scm/hg.py
+++ b/bumpversion/scm/hg.py
@@ -1,13 +1,12 @@
 """Mercurial source control management."""
 
 import subprocess
-from pathlib import Path
 from typing import ClassVar, MutableMapping, Optional
 
 from bumpversion.exceptions import DirtyWorkingDirectoryError, SignedTagsError
 from bumpversion.scm.models import LatestTagInfo, SCMConfig
 from bumpversion.ui import get_indented_logger
-from bumpversion.utils import run_command
+from bumpversion.utils import Pathlike, run_command
 
 logger = get_indented_logger(__name__)
 
@@ -51,11 +50,11 @@ class Mercurial:
         self._latest_tag_info = LatestTagInfo(**info)
         return self._latest_tag_info
 
-    def add_path(self, path: str | Path) -> None:
+    def add_path(self, path: Pathlike) -> None:
         """Add a path to the Source Control Management repository."""
         pass
 
-    def commit_and_tag(self, files: list[Path | str], context: MutableMapping, dry_run: bool = False) -> None:
+    def commit_and_tag(self, files: list[Pathlike], context: MutableMapping, dry_run: bool = False) -> None:
         """Commit and tag files to the repository using the configuration."""
 
     def tag(self, name: str, sign: bool = False, message: Optional[str] = None) -> None:
diff --git a/bumpversion/scm/models.py b/bumpversion/scm/models.py
index 616fd149..43083780 100644
--- a/bumpversion/scm/models.py
+++ b/bumpversion/scm/models.py
@@ -7,7 +7,7 @@ from typing import Any, MutableMapping, Optional, Protocol
 
 from bumpversion.config import Config
 from bumpversion.ui import get_indented_logger
-from bumpversion.utils import extract_regex_flags
+from bumpversion.utils import Pathlike, extract_regex_flags
 
 logger = get_indented_logger(__name__)
 
@@ -82,7 +82,7 @@ class SCMTool(Protocol):
         """Return the latest tag information."""
         ...
 
-    def add_path(self, path: str | Path) -> None:
+    def add_path(self, path: Pathlike) -> None:
         """Add a path to the pending commit."""
         ...
 
@@ -90,7 +90,7 @@ class SCMTool(Protocol):
         """Return all tags in the SCM."""
         ...
 
-    def commit_and_tag(self, files: list[Path | str], context: MutableMapping, dry_run: bool = False) -> None:
+    def commit_and_tag(self, files: list[Pathlike], context: MutableMapping, dry_run: bool = False) -> None:
         """Commit and tag files to the repository using the configuration."""
         ...
 
@@ -126,7 +126,7 @@ class DefaultSCMTool:
         """Return the latest tag information."""
         return LatestTagInfo()
 
-    def add_path(self, path: str | Path) -> None:
+    def add_path(self, path: Pathlike) -> None:
         """Add a path to the pending commit."""
         logger.debug("No source code management system configured. Skipping adding path '%s'.", path)
 
@@ -134,7 +134,7 @@ class DefaultSCMTool:
         """Return all tags in the SCM."""
         return []
 
-    def commit_and_tag(self, files: list[Path | str], context: MutableMapping, dry_run: bool = False) -> None:
+    def commit_and_tag(self, files: list[Pathlike], context: MutableMapping, dry_run: bool = False) -> None:
         """Pretend to commit and tag files to the repository using the configuration."""
         logger.debug("No source code management system configured. Skipping committing and tagging.")
 
@@ -200,7 +200,7 @@ class SCMInfo:
         for attr_name, value in asdict(latest_tag_info).items():
             setattr(self, attr_name, value)
 
-    def path_in_repo(self, path: Path | str) -> bool:
+    def path_in_repo(self, path: Pathlike) -> bool:
         """Return whether a path is inside this repository."""
         if self.repository_root is None:
             return True
@@ -208,12 +208,7 @@ class SCMInfo:
             return True
         return str(path).startswith(str(self.repository_root))
 
-    def commit_and_tag(
-        self,
-        files: list[str | Path],
-        context: MutableMapping,
-        dry_run: bool = False,
-    ) -> None:
+    def commit_and_tag(self, files: list[Pathlike], context: MutableMapping, dry_run: bool = False) -> None:
         """Commit the files to the source code management system."""
         logger.indent()
 
@@ -230,7 +225,7 @@ class SCMInfo:
         self.tool.commit_and_tag(files=files, context=context, dry_run=dry_run)
         logger.dedent()
 
-    def _commit(self, files: list[Path | str], context: MutableMapping, dry_run: bool = False) -> None:
+    def _commit(self, files: list[Pathlike], context: MutableMapping, dry_run: bool = False) -> None:
         """Commit the files to the source code management system."""
         do_commit = not dry_run
         logger.info("%s the commit", "Preparing" if do_commit else "Would prepare")
diff --git a/bumpversion/utils.py b/bumpversion/utils.py
index 1f124262..a5d8e9ca 100644
--- a/bumpversion/utils.py
+++ b/bumpversion/utils.py
@@ -14,6 +14,10 @@ from bumpversion.ui import get_indented_logger
 logger = get_indented_logger(__name__)
 
 
+Pathlike = Union[str, Path]
+"""A type alias for a path-like object."""
+
+
 def extract_regex_flags(regex_pattern: str) -> Tuple[str, str]:
     """
     Extract the regex flags from the regex pattern.
@@ -133,7 +137,7 @@ def run_command(command: list, env: Optional[dict] = None) -> CompletedProcess:
     return result
 
 
-def is_subpath(parent: Union[Path, str], path: Union[Path, str]) -> bool:
+def is_subpath(parent: Pathlike, path: Pathlike) -> bool:
     """Return whether a path is inside the parent."""
     normalized_parent = Path(parent)
     normalized_path = Path(path)


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 99bf77b0..cac766e7 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -8,6 +8,7 @@ import pytest
 from pytest import param
 
 from bumpversion import utils
+from bumpversion.utils import Pathlike
 
 DRIVE_PREFIX = "c:" if sys.platform == "win32" else ""
 
@@ -183,7 +184,7 @@ class TestExtractRegexFlags:
         param(Path(f"{DRIVE_PREFIX}/parent"), "relative/path", True, id="parent_as_path_path_as_string_4"),
     ],
 )
-def test_is_subpath(parent: Path | str, path: Path | str, expected: bool) -> None:
+def test_is_subpath(parent: Pathlike, path: Pathlike, expected: bool) -> None:
     """Test the is_subpath function."""
     # Act
     result = utils.is_subpath(parent, path)