2
0
Эх сурвалжийг харах

Add support for os.PathLike objects throughout the API

Functions that accept file paths now support pathlib.Path objects in addition to
strings and bytes. This includes repository operations, configuration file
handling, ignore file processing, and all major entry points.

Fixes #1074
Jelmer Vernooij 1 сар өмнө
parent
commit
32e73e511e

+ 6 - 0
NEWS

@@ -5,6 +5,12 @@
    when trying to process pack data from repositories that were cloned externally.
    (Jelmer Vernooij, #1179)
 
+ * Add support for ``os.PathLike`` objects throughout the API. Functions that
+   accept file paths now support ``pathlib.Path`` objects in addition to
+   strings and bytes. This includes repository operations, configuration file
+   handling, ignore file processing, and all major entry points.
+   (Jelmer Vernooij, #1074)
+
  * Add support for ``format`` argument to ``Repo.init()`` and ``Repo.init_bare()``
    to specify repository format version (0 or 1). This allows creating repositories
    with different format versions by setting the ``core.repositoryformatversion``

+ 12 - 6
dulwich/config.py

@@ -643,18 +643,22 @@ class ConfigFile(ConfigDict):
         return ret
 
     @classmethod
-    def from_path(cls, path: str) -> "ConfigFile":
+    def from_path(cls, path: Union[str, os.PathLike]) -> "ConfigFile":
         """Read configuration from a file on disk."""
         with GitFile(path, "rb") as f:
             ret = cls.from_file(f)
-            ret.path = path
+            ret.path = os.fspath(path)
             return ret
 
-    def write_to_path(self, path: Optional[str] = None) -> None:
+    def write_to_path(self, path: Optional[Union[str, os.PathLike]] = None) -> None:
         """Write configuration to a file on disk."""
         if path is None:
-            path = self.path
-        with GitFile(path, "wb") as f:
+            if self.path is None:
+                raise ValueError("No path specified and no default path available")
+            path_to_use: Union[str, os.PathLike] = self.path
+        else:
+            path_to_use = path
+        with GitFile(path_to_use, "wb") as f:
             self.write_to_file(f)
 
     def write_to_file(self, f: BinaryIO) -> None:
@@ -809,7 +813,9 @@ class StackedConfig(Config):
                     yield section
 
 
-def read_submodules(path: str) -> Iterator[tuple[bytes, bytes, bytes]]:
+def read_submodules(
+    path: Union[str, os.PathLike],
+) -> Iterator[tuple[bytes, bytes, bytes]]:
     """Read a .gitmodules file."""
     cfg = ConfigFile.from_path(path)
     return parse_submodules(cfg)

+ 10 - 5
dulwich/file.py

@@ -24,7 +24,7 @@
 import os
 import sys
 import warnings
-from typing import ClassVar
+from typing import ClassVar, Union
 
 
 def ensure_dir_exists(dirname) -> None:
@@ -68,7 +68,9 @@ def _fancy_rename(oldname, newname) -> None:
     os.remove(tmpfile)
 
 
-def GitFile(filename, mode="rb", bufsize=-1, mask=0o644):
+def GitFile(
+    filename: Union[str, bytes, os.PathLike], mode="rb", bufsize=-1, mask=0o644
+):
     """Create a file object that obeys the git file locking protocol.
 
     Returns: a builtin file object or a _GitFile object
@@ -140,10 +142,13 @@ class _GitFile:
         "writelines",
     }
 
-    def __init__(self, filename, mode, bufsize, mask) -> None:
-        self._filename = filename
+    def __init__(
+        self, filename: Union[str, bytes, os.PathLike], mode, bufsize, mask
+    ) -> None:
+        # Convert PathLike to str/bytes for our internal use
+        self._filename: Union[str, bytes] = os.fspath(filename)
         if isinstance(self._filename, bytes):
-            self._lockfilename = self._filename + b".lock"
+            self._lockfilename: Union[str, bytes] = self._filename + b".lock"
         else:
             self._lockfilename = self._filename + ".lock"
         try:

+ 3 - 1
dulwich/ignore.py

@@ -421,7 +421,9 @@ class IgnoreFilter:
         return _check_parent_exclusion(path, matching_patterns)
 
     @classmethod
-    def from_path(cls, path, ignorecase: bool = False) -> "IgnoreFilter":
+    def from_path(
+        cls, path: Union[str, os.PathLike], ignorecase: bool = False
+    ) -> "IgnoreFilter":
         with open(path, "rb") as f:
             return cls(read_ignore_patterns(f), ignorecase, path=path)
 

+ 2 - 2
dulwich/index.py

@@ -798,7 +798,7 @@ class Index:
 
     def __init__(
         self,
-        filename: Union[bytes, str],
+        filename: Union[bytes, str, os.PathLike],
         read=True,
         skip_hash: bool = False,
         version: Optional[int] = None,
@@ -811,7 +811,7 @@ class Index:
           skip_hash: Whether to skip SHA1 hash when writing (for manyfiles feature)
           version: Index format version to use (None = auto-detect from file or use default)
         """
-        self._filename = filename
+        self._filename = os.fspath(filename)
         # TODO(jelmer): Store the version returned by read_index
         self._version = version
         self._skip_hash = skip_hash

+ 7 - 3
dulwich/object_store.py

@@ -35,6 +35,7 @@ from typing import (
     Callable,
     Optional,
     Protocol,
+    Union,
     cast,
 )
 
@@ -750,7 +751,10 @@ class DiskObjectStore(PackBasedObjectStore):
     """Git-style object store that exists on disk."""
 
     def __init__(
-        self, path, loose_compression_level=-1, pack_compression_level=-1
+        self,
+        path: Union[str, os.PathLike],
+        loose_compression_level=-1,
+        pack_compression_level=-1,
     ) -> None:
         """Open an object store.
 
@@ -773,7 +777,7 @@ class DiskObjectStore(PackBasedObjectStore):
         return f"<{self.__class__.__name__}({self.path!r})>"
 
     @classmethod
-    def from_config(cls, path, config):
+    def from_config(cls, path: Union[str, os.PathLike], config):
         try:
             default_compression_level = int(
                 config.get((b"core",), b"compression").decode()
@@ -1046,7 +1050,7 @@ class DiskObjectStore(PackBasedObjectStore):
             )
 
     @classmethod
-    def init(cls, path):
+    def init(cls, path: Union[str, os.PathLike]):
         try:
             os.mkdir(path)
         except FileExistsError:

+ 11 - 6
dulwich/pack.py

@@ -363,7 +363,7 @@ def iter_sha1(iter):
     return sha.hexdigest().encode("ascii")
 
 
-def load_pack_index(path):
+def load_pack_index(path: Union[str, os.PathLike]):
     """Load an index file by path.
 
     Args:
@@ -396,7 +396,7 @@ def _load_file_contents(f, size=None):
     return contents, size
 
 
-def load_pack_index_file(path, f):
+def load_pack_index_file(path: Union[str, os.PathLike], f):
     """Load an index file from a file-like object.
 
     Args:
@@ -404,15 +404,20 @@ def load_pack_index_file(path, f):
       f: File-like object
     Returns: A PackIndex loaded from the given file
     """
+    # Ensure path is a string for PackIndex classes
+    path_str = os.fspath(path)
+    if isinstance(path_str, bytes):
+        path_str = os.fsdecode(path_str)
+
     contents, size = _load_file_contents(f)
     if contents[:4] == b"\377tOc":
         version = struct.unpack(b">L", contents[4:8])[0]
         if version == 2:
-            return PackIndex2(path, file=f, contents=contents, size=size)
+            return PackIndex2(path_str, file=f, contents=contents, size=size)
         else:
             raise KeyError(f"Unknown pack index format {version}")
     else:
-        return PackIndex1(path, file=f, contents=contents, size=size)
+        return PackIndex1(path_str, file=f, contents=contents, size=size)
 
 
 def bisect_find_sha(start, end, sha, unpack_name):
@@ -1191,7 +1196,7 @@ class PackData:
     position.  It will all just throw a zlib or KeyError.
     """
 
-    def __init__(self, filename, file=None, size=None) -> None:
+    def __init__(self, filename: Union[str, os.PathLike], file=None, size=None) -> None:
         """Create a PackData object representing the pack in the given filename.
 
         The file must exist and stay readable until the object is disposed of.
@@ -1225,7 +1230,7 @@ class PackData:
         return cls(str(file), file=file, size=size)
 
     @classmethod
-    def from_path(cls, path):
+    def from_path(cls, path: Union[str, os.PathLike]):
         return cls(filename=path)
 
     def close(self) -> None:

+ 7 - 5
dulwich/porcelain.py

@@ -262,7 +262,7 @@ def get_user_timezones():
     return author_timezone, commit_timezone
 
 
-def open_repo(path_or_repo):
+def open_repo(path_or_repo: Union[str, os.PathLike, BaseRepo]):
     """Open an argument that can be a repository or a path for a repository."""
     if isinstance(path_or_repo, BaseRepo):
         return path_or_repo
@@ -275,7 +275,7 @@ def _noop_context_manager(obj):
     yield obj
 
 
-def open_repo_closing(path_or_repo):
+def open_repo_closing(path_or_repo: Union[str, os.PathLike, BaseRepo]):
     """Open an argument that can be a repository or a path for a repository.
     returns a context manager that will close the repo on exit if the argument
     is a path, else does nothing if the argument is a repo.
@@ -477,7 +477,9 @@ def commit_tree(repo, tree, message=None, author=None, committer=None):
         )
 
 
-def init(path=".", *, bare=False, symlinks: Optional[bool] = None):
+def init(
+    path: Union[str, os.PathLike] = ".", *, bare=False, symlinks: Optional[bool] = None
+):
     """Create a new git repository.
 
     Args:
@@ -497,7 +499,7 @@ def init(path=".", *, bare=False, symlinks: Optional[bool] = None):
 
 def clone(
     source,
-    target=None,
+    target: Optional[Union[str, os.PathLike]] = None,
     bare=False,
     checkout=None,
     errstream=default_bytes_err_stream,
@@ -582,7 +584,7 @@ def clone(
     )
 
 
-def add(repo=".", paths=None):
+def add(repo: Union[str, os.PathLike, BaseRepo] = ".", paths=None):
     """Add files to the staging area.
 
     Args:

+ 12 - 9
dulwich/refs.py

@@ -26,7 +26,7 @@ import os
 import warnings
 from collections.abc import Iterator
 from contextlib import suppress
-from typing import Any, Optional
+from typing import Any, Optional, Union
 
 from .errors import PackedRefsException, RefFormatError
 from .file import GitFile, ensure_dir_exists
@@ -611,16 +611,19 @@ class InfoRefsContainer(RefsContainer):
 class DiskRefsContainer(RefsContainer):
     """Refs container that reads refs from disk."""
 
-    def __init__(self, path, worktree_path=None, logger=None) -> None:
+    def __init__(
+        self,
+        path: Union[str, bytes, os.PathLike],
+        worktree_path: Optional[Union[str, bytes, os.PathLike]] = None,
+        logger=None,
+    ) -> None:
         super().__init__(logger=logger)
-        if getattr(path, "encode", None) is not None:
-            path = os.fsencode(path)
-        self.path = path
+        # Convert path-like objects to strings, then to bytes for Git compatibility
+        self.path = os.fsencode(os.fspath(path))
         if worktree_path is None:
-            worktree_path = path
-        if getattr(worktree_path, "encode", None) is not None:
-            worktree_path = os.fsencode(worktree_path)
-        self.worktree_path = worktree_path
+            self.worktree_path = self.path
+        else:
+            self.worktree_path = os.fsencode(os.fspath(worktree_path))
         self._packed_refs = None
         self._peeled_refs = None
 

+ 30 - 6
dulwich/repo.py

@@ -1151,7 +1151,7 @@ class Repo(BaseRepo):
 
     def __init__(
         self,
-        root: str,
+        root: Union[str, bytes, os.PathLike],
         object_store: Optional[PackBasedObjectStore] = None,
         bare: Optional[bool] = None,
     ) -> None:
@@ -1163,6 +1163,9 @@ class Repo(BaseRepo):
             repository's default object store
           bare: True if this is a bare repository.
         """
+        root = os.fspath(root)
+        if isinstance(root, bytes):
+            root = os.fsdecode(root)
         hidden_path = os.path.join(root, CONTROLDIR)
         if bare is None:
             if os.path.isfile(hidden_path) or os.path.isdir(
@@ -1736,8 +1739,8 @@ class Repo(BaseRepo):
     @classmethod
     def _init_maybe_bare(
         cls,
-        path,
-        controldir,
+        path: Union[str, bytes, os.PathLike],
+        controldir: Union[str, bytes, os.PathLike],
         bare,
         object_store=None,
         config=None,
@@ -1745,6 +1748,12 @@ class Repo(BaseRepo):
         symlinks: Optional[bool] = None,
         format: Optional[int] = None,
     ):
+        path = os.fspath(path)
+        if isinstance(path, bytes):
+            path = os.fsdecode(path)
+        controldir = os.fspath(controldir)
+        if isinstance(controldir, bytes):
+            controldir = os.fsdecode(controldir)
         for d in BASE_DIRECTORIES:
             os.mkdir(os.path.join(controldir, *d))
         if object_store is None:
@@ -1766,7 +1775,7 @@ class Repo(BaseRepo):
     @classmethod
     def init(
         cls,
-        path: str,
+        path: Union[str, bytes, os.PathLike],
         *,
         mkdir: bool = False,
         config=None,
@@ -1782,6 +1791,9 @@ class Repo(BaseRepo):
           format: Repository format version (defaults to 0)
         Returns: `Repo` instance
         """
+        path = os.fspath(path)
+        if isinstance(path, bytes):
+            path = os.fsdecode(path)
         if mkdir:
             os.mkdir(path)
         controldir = os.path.join(path, CONTROLDIR)
@@ -1798,7 +1810,13 @@ class Repo(BaseRepo):
         )
 
     @classmethod
-    def _init_new_working_directory(cls, path, main_repo, identifier=None, mkdir=False):
+    def _init_new_working_directory(
+        cls,
+        path: Union[str, bytes, os.PathLike],
+        main_repo,
+        identifier=None,
+        mkdir=False,
+    ):
         """Create a new working directory linked to a repository.
 
         Args:
@@ -1808,6 +1826,9 @@ class Repo(BaseRepo):
           mkdir: Whether to create the directory
         Returns: `Repo` instance
         """
+        path = os.fspath(path)
+        if isinstance(path, bytes):
+            path = os.fsdecode(path)
         if mkdir:
             os.mkdir(path)
         if identifier is None:
@@ -1838,7 +1859,7 @@ class Repo(BaseRepo):
     @classmethod
     def init_bare(
         cls,
-        path,
+        path: Union[str, bytes, os.PathLike],
         *,
         mkdir=False,
         object_store=None,
@@ -1855,6 +1876,9 @@ class Repo(BaseRepo):
           format: Repository format version (defaults to 0)
         Returns: a `Repo` instance
         """
+        path = os.fspath(path)
+        if isinstance(path, bytes):
+            path = os.fsdecode(path)
         if mkdir:
             os.mkdir(path)
         return cls._init_maybe_bare(

+ 41 - 0
tests/test_config.py

@@ -316,6 +316,47 @@ who\"
             cf2.get((b"remote", b"origin"), b"fetch"),
         )
 
+    def test_from_path_pathlib(self) -> None:
+        import tempfile
+        from pathlib import Path
+
+        # Create a temporary config file
+        with tempfile.NamedTemporaryFile(mode="w", suffix=".config", delete=False) as f:
+            f.write("[core]\n    filemode = true\n")
+            temp_path = f.name
+
+        try:
+            # Test with pathlib.Path
+            path_obj = Path(temp_path)
+            cf = ConfigFile.from_path(path_obj)
+            self.assertEqual(cf.get((b"core",), b"filemode"), b"true")
+        finally:
+            # Clean up
+            os.unlink(temp_path)
+
+    def test_write_to_path_pathlib(self) -> None:
+        import tempfile
+        from pathlib import Path
+
+        # Create a config
+        cf = ConfigFile()
+        cf.set((b"user",), b"name", b"Test User")
+
+        # Write to pathlib.Path
+        with tempfile.NamedTemporaryFile(suffix=".config", delete=False) as f:
+            temp_path = f.name
+
+        try:
+            path_obj = Path(temp_path)
+            cf.write_to_path(path_obj)
+
+            # Read it back
+            cf2 = ConfigFile.from_path(path_obj)
+            self.assertEqual(cf2.get((b"user",), b"name"), b"Test User")
+        finally:
+            # Clean up
+            os.unlink(temp_path)
+
 
 class ConfigDictTests(TestCase):
     def test_get_set(self) -> None:

+ 24 - 0
tests/test_ignore.py

@@ -193,6 +193,30 @@ class IgnoreFilterTests(TestCase):
         self.assertTrue(filter.is_ignored("foo"))
         self.assertTrue(filter.is_ignored("foo[bar]"))
 
+    def test_from_path_pathlib(self) -> None:
+        import tempfile
+        from pathlib import Path
+
+        # Create a temporary .gitignore file
+        with tempfile.NamedTemporaryFile(
+            mode="w", suffix=".gitignore", delete=False
+        ) as f:
+            f.write("*.pyc\n__pycache__/\n")
+            temp_path = f.name
+
+        try:
+            # Test with pathlib.Path
+            path_obj = Path(temp_path)
+            ignore_filter = IgnoreFilter.from_path(path_obj)
+
+            # Test that it loaded the patterns correctly
+            self.assertTrue(ignore_filter.is_ignored("test.pyc"))
+            self.assertTrue(ignore_filter.is_ignored("__pycache__/"))
+            self.assertFalse(ignore_filter.is_ignored("test.py"))
+        finally:
+            # Clean up
+            os.unlink(temp_path)
+
 
 class IgnoreFilterStackTests(TestCase):
     def test_stack_first(self) -> None:

+ 35 - 0
tests/test_index.py

@@ -127,6 +127,41 @@ class SimpleIndexTestCase(IndexTestCase):
         self.assertEqual(b"bla", newname)
         self.assertEqual(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", newsha)
 
+    def test_index_pathlib(self) -> None:
+        import tempfile
+        from pathlib import Path
+
+        # Create a temporary index file
+        with tempfile.NamedTemporaryFile(suffix=".index", delete=False) as f:
+            temp_path = f.name
+
+        try:
+            # Test creating Index with pathlib.Path
+            path_obj = Path(temp_path)
+            index = Index(path_obj, read=False)
+            self.assertEqual(str(path_obj), index.path)
+
+            # Add an entry and write
+            index[b"test"] = IndexEntry(
+                ctime=(0, 0),
+                mtime=(0, 0),
+                dev=0,
+                ino=0,
+                mode=33188,
+                uid=0,
+                gid=0,
+                size=0,
+                sha=b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
+            )
+            index.write()
+
+            # Read it back with pathlib.Path
+            index2 = Index(path_obj)
+            self.assertIn(b"test", index2)
+        finally:
+            # Clean up
+            os.unlink(temp_path)
+
 
 class SimpleIndexWriterTestCase(IndexTestCase):
     def setUp(self) -> None:

+ 46 - 0
tests/test_porcelain.py

@@ -883,6 +883,28 @@ class CloneTests(PorcelainTestCase):
         ) as r:
             self.assertEqual(c3.id, r.refs[b"HEAD"])
 
+    def test_clone_pathlib(self) -> None:
+        from pathlib import Path
+
+        f1_1 = make_object(Blob, data=b"f1")
+        commit_spec = [[1]]
+        trees = {1: [(b"f1", f1_1)]}
+
+        c1 = build_commit_graph(self.repo.object_store, commit_spec, trees)[0]
+        self.repo.refs[b"refs/heads/master"] = c1.id
+
+        target_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, target_dir)
+        target_path = Path(target_dir) / "clone_repo"
+
+        errstream = BytesIO()
+        r = porcelain.clone(
+            self.repo.path, target_path, checkout=False, errstream=errstream
+        )
+        self.addCleanup(r.close)
+        self.assertEqual(r.path, str(target_path))
+        self.assertTrue(os.path.exists(str(target_path)))
+
 
 class InitTests(TestCase):
     def test_non_bare(self) -> None:
@@ -895,6 +917,30 @@ class InitTests(TestCase):
         self.addCleanup(shutil.rmtree, repo_dir)
         porcelain.init(repo_dir, bare=True)
 
+    def test_init_pathlib(self) -> None:
+        from pathlib import Path
+
+        repo_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, repo_dir)
+        repo_path = Path(repo_dir)
+
+        # Test non-bare repo with pathlib
+        repo = porcelain.init(repo_path)
+        self.assertTrue(os.path.exists(os.path.join(repo_dir, ".git")))
+        repo.close()
+
+    def test_init_bare_pathlib(self) -> None:
+        from pathlib import Path
+
+        repo_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, repo_dir)
+        repo_path = Path(repo_dir)
+
+        # Test bare repo with pathlib
+        repo = porcelain.init(repo_path, bare=True)
+        self.assertTrue(os.path.exists(os.path.join(repo_dir, "refs")))
+        repo.close()
+
 
 class AddTests(PorcelainTestCase):
     def test_add_default_paths(self) -> None:

+ 43 - 0
tests/test_refs.py

@@ -757,6 +757,49 @@ _TEST_REFS_SERIALIZED = (
 )
 
 
+class DiskRefsContainerPathlibTests(TestCase):
+    def test_pathlib_init(self) -> None:
+        from pathlib import Path
+
+        from dulwich.refs import DiskRefsContainer
+
+        # Create a temporary directory
+        temp_dir = tempfile.mkdtemp()
+        self.addCleanup(os.rmdir, temp_dir)
+
+        # Test with pathlib.Path
+        path_obj = Path(temp_dir)
+        refs = DiskRefsContainer(path_obj)
+        self.assertEqual(refs.path, temp_dir.encode())
+
+        # Test refpath with pathlib initialized container
+        ref_path = refs.refpath(b"HEAD")
+        self.assertTrue(isinstance(ref_path, bytes))
+        self.assertEqual(ref_path, os.path.join(temp_dir.encode(), b"HEAD"))
+
+    def test_pathlib_worktree_path(self) -> None:
+        from pathlib import Path
+
+        from dulwich.refs import DiskRefsContainer
+
+        # Create temporary directories
+        temp_dir = tempfile.mkdtemp()
+        worktree_dir = tempfile.mkdtemp()
+        self.addCleanup(os.rmdir, temp_dir)
+        self.addCleanup(os.rmdir, worktree_dir)
+
+        # Test with pathlib.Path for both paths
+        path_obj = Path(temp_dir)
+        worktree_obj = Path(worktree_dir)
+        refs = DiskRefsContainer(path_obj, worktree_path=worktree_obj)
+        self.assertEqual(refs.path, temp_dir.encode())
+        self.assertEqual(refs.worktree_path, worktree_dir.encode())
+
+        # Test refpath returns worktree path for HEAD
+        ref_path = refs.refpath(b"HEAD")
+        self.assertEqual(ref_path, os.path.join(worktree_dir.encode(), b"HEAD"))
+
+
 class InfoRefsContainerTests(TestCase):
     def test_invalid_refname(self) -> None:
         text = _TEST_REFS_SERIALIZED + b"00" * 20 + b"\trefs/stash\n"

+ 49 - 0
tests/test_repository.py

@@ -122,6 +122,55 @@ class CreateRepositoryTests(TestCase):
         self.assertEqual(target_dir, repo._controldir)
         self._check_repo_contents(repo, True)
 
+    def test_create_disk_bare_pathlib(self) -> None:
+        from pathlib import Path
+
+        tmp_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        repo_path = Path(tmp_dir)
+        repo = Repo.init_bare(repo_path)
+        self.assertEqual(tmp_dir, repo._controldir)
+        self._check_repo_contents(repo, True)
+        # Test that refpath works with pathlib
+        ref_path = repo.refs.refpath(b"refs/heads/master")
+        self.assertTrue(isinstance(ref_path, bytes))
+        self.assertEqual(ref_path, os.path.join(tmp_dir.encode(), b"refs/heads/master"))
+
+    def test_create_disk_non_bare_pathlib(self) -> None:
+        from pathlib import Path
+
+        tmp_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        repo_path = Path(tmp_dir)
+        repo = Repo.init(repo_path)
+        self.assertEqual(os.path.join(tmp_dir, ".git"), repo._controldir)
+        self._check_repo_contents(repo, False)
+
+    def test_open_repo_pathlib(self) -> None:
+        from pathlib import Path
+
+        tmp_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        # First create a repo
+        repo = Repo.init_bare(tmp_dir)
+        repo.close()
+        # Now open it with pathlib
+        repo_path = Path(tmp_dir)
+        repo2 = Repo(repo_path)
+        self.assertEqual(tmp_dir, repo2._controldir)
+        self.assertTrue(repo2.bare)
+        repo2.close()
+
+    def test_create_disk_bare_mkdir_pathlib(self) -> None:
+        from pathlib import Path
+
+        tmp_dir = tempfile.mkdtemp()
+        target_path = Path(tmp_dir) / "target"
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        repo = Repo.init_bare(target_path, mkdir=True)
+        self.assertEqual(str(target_path), repo._controldir)
+        self._check_repo_contents(repo, True)
+
 
 class MemoryRepoTests(TestCase):
     def test_set_description(self) -> None: