Repository: jaraco/path

Issue:
Align API with pathlib.Path
While this project has a long-established API, it's now at odds with the now more established conventions of the stdlib's [pathlib module](https://docs.python.org/3/library/pathlib.html).

Let's compare the usage of a `path.Path` object and a `pathlib.Path` object and determine if it would be possible for `path.Path` to implement the same interface as `pathlib.Path`.

For now, `path.Path` should continue to offer its current interface, but eventually, it should drop support for duplicated functionality.

Discussion:

Patch:
diff --git a/.coveragerc b/.coveragerc
index 6a34e66..0fd3ada 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,6 +2,7 @@
 omit =
 	# leading `*/` for pytest-dev/pytest-cov#456
 	*/.tox/*
+	path/py37compat.py
 
 [report]
 show_missing = True
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 7d6b455..246cd14 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -9,7 +9,8 @@ jobs:
         python:
         - 3.6
         - 3.9
-        - 3.10.0-alpha - 3.10.99
+        # disabled due to pywin32 availability mhammond/pywin32#1588
+        # - 3.10.0-alpha - 3.10.99
         platform: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.platform }}
     steps:
diff --git a/CHANGES.rst b/CHANGES.rst
index a2bc6c1..8f384eb 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,9 @@
+v16.1.0
+-------
+
+- #204: Improved test coverage across the package to 99%, fixing
+  bugs in uncovered code along the way.
+
 v16.0.0
 -------
 
diff --git a/README.rst b/README.rst
index 86f7df7..834ff5f 100644
--- a/README.rst
+++ b/README.rst
@@ -97,6 +97,12 @@ has several advantages over ``pathlib``:
   subclass the model do not need to be concerned with
   OS-specific nuances.
 
+This path project has the explicit aim to provide compatibility
+with ``pathlib`` objects where possible, such that a ``path.Path``
+object is a drop-in replacement for ``pathlib.Path*`` objects.
+This project welcomes contributions to improve that compatibility
+where it's lacking.
+
 Alternatives
 ============
 
diff --git a/path/__init__.py b/path/__init__.py
index 4ded584..b34ad95 100644
--- a/path/__init__.py
+++ b/path/__init__.py
@@ -57,10 +57,10 @@ __all__ = ['Path', 'TempDir']
 
 LINESEPS = ['\r\n', '\r', '\n']
 U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029']
-NEWLINE = re.compile('|'.join(LINESEPS))
 U_NEWLINE = re.compile('|'.join(U_LINESEPS))
-NL_END = re.compile(r'(?:{0})$'.format(NEWLINE.pattern))
-U_NL_END = re.compile(r'(?:{0})$'.format(U_NEWLINE.pattern))
+B_NEWLINE = re.compile('|'.join(LINESEPS).encode())
+B_NL_END = re.compile(f'(?:{B_NEWLINE.pattern.decode()})$'.encode())
+U_NL_END = re.compile(f'(?:{U_NEWLINE.pattern})$')
 
 
 class TreeWalkWarning(Warning):
@@ -182,14 +182,9 @@ class Path(str):
 
     # Adding a Path and a string yields a Path.
     def __add__(self, more):
-        try:
-            return self._next_class(super(Path, self).__add__(more))
-        except TypeError:  # Python bug
-            return NotImplemented
+        return self._next_class(super(Path, self).__add__(more))
 
     def __radd__(self, other):
-        if not isinstance(other, str):
-            return NotImplemented
         return self._next_class(other.__add__(self))
 
     # The / operator joins Paths.
@@ -228,9 +223,6 @@ class Path(str):
     def __exit__(self, *_):
         os.chdir(self._old_dir)
 
-    def __fspath__(self):
-        return self
-
     @classmethod
     def getcwd(cls):
         """Return the current working directory as a path object.
@@ -373,7 +365,7 @@ class Path(str):
         .. seealso:: :func:`os.path.splitdrive`
         """
         drive, rel = self.module.splitdrive(self)
-        return self._next_class(drive), rel
+        return self._next_class(drive), self._next_class(rel)
 
     def splitext(self):
         """Return two-tuple of ``.stripext()`` and ``.ext``.
@@ -398,19 +390,6 @@ class Path(str):
         """
         return self.splitext()[0]
 
-    def splitunc(self):
-        unc, rest = self.module.splitunc(self)
-        return self._next_class(unc), rest
-
-    @property
-    def uncshare(self):
-        """
-        The UNC mount point for this path.
-        This is empty for paths on local drives.
-        """
-        unc, r = self.module.splitunc(self)
-        return self._next_class(unc)
-
     @multimethod
     def joinpath(cls, first, *others):
         """
@@ -441,6 +420,10 @@ class Path(str):
         return list(self._parts())
 
     def parts(self):
+        """
+        >>> Path('/foo/bar/baz').parts()
+        (Path('/'), 'foo', 'bar', 'baz')
+        """
         return tuple(self._parts())
 
     def _parts(self):
@@ -576,6 +559,7 @@ class Path(str):
                 do_traverse = traverse()
             except Exception as exc:
                 errors(f"Unable to access '{child}': {exc}")
+                continue
 
             if do_traverse:
                 for item in child.walk(errors=errors, match=match):
@@ -785,7 +769,7 @@ class Path(str):
             text = text.encode(encoding or sys.getdefaultencoding(), errors)
         else:
             assert encoding is None
-            text = NEWLINE.sub(linesep, text)
+            text = B_NEWLINE.sub(linesep.encode(), text)
         self.write_bytes(text, append=append)
 
     def lines(self, encoding=None, errors='strict', retain=True):
@@ -851,7 +835,7 @@ class Path(str):
             for line in lines:
                 isUnicode = isinstance(line, str)
                 if linesep is not None:
-                    pattern = U_NL_END if isUnicode else NL_END
+                    pattern = U_NL_END if isUnicode else B_NL_END
                     line = pattern.sub('', line) + linesep
                 if isUnicode:
                     line = line.encode(encoding or sys.getdefaultencoding(), errors)
@@ -903,7 +887,12 @@ class Path(str):
     # bound. Playing it safe and wrapping them all in method calls.
 
     def isabs(self):
-        """.. seealso:: :func:`os.path.isabs`"""
+        """
+        >>> Path('.').isabs()
+        False
+
+        .. seealso:: :func:`os.path.isabs`
+        """
         return self.module.isabs(self)
 
     def exists(self):
@@ -923,14 +912,16 @@ class Path(str):
         return self.module.islink(self)
 
     def ismount(self):
-        """.. seealso:: :func:`os.path.ismount`"""
+        """
+        >>> Path('.').ismount()
+        False
+
+        .. seealso:: :func:`os.path.ismount`
+        """
         return self.module.ismount(self)
 
     def samefile(self, other):
         """.. seealso:: :func:`os.path.samefile`"""
-        if not hasattr(self.module, 'samefile'):
-            other = Path(other).realpath().normpath().normcase()
-            return self.realpath().normpath().normcase() == other
         return self.module.samefile(self, other)
 
     def getatime(self):
@@ -941,7 +932,11 @@ class Path(str):
         getatime,
         None,
         None,
-        """ Last access time of the file.
+        """
+        Last access time of the file.
+
+        >>> Path('.').atime > 0
+        True
 
         .. seealso:: :meth:`getatime`, :func:`os.path.getatime`
         """,
@@ -955,7 +950,8 @@ class Path(str):
         getmtime,
         None,
         None,
-        """ Last-modified time of the file.
+        """
+        Last modified time of the file.
 
         .. seealso:: :meth:`getmtime`, :func:`os.path.getmtime`
         """,
@@ -989,38 +985,45 @@ class Path(str):
         """,
     )
 
-    if hasattr(os, 'access'):
-
-        def access(self, mode):
-            """Return ``True`` if current user has access to this path.
+    def access(self, *args, **kwargs):
+        """
+        Return does the real user have access to this path.
 
-            mode - One of the constants :data:`os.F_OK`, :data:`os.R_OK`,
-            :data:`os.W_OK`, :data:`os.X_OK`
+        >>> Path('.').access(os.F_OK)
+        True
 
-            .. seealso:: :func:`os.access`
-            """
-            return os.access(self, mode)
+        .. seealso:: :func:`os.access`
+        """
+        return os.access(self, *args, **kwargs)
 
     def stat(self):
-        """Perform a ``stat()`` system call on this path.
+        """
+        Perform a ``stat()`` system call on this path.
+
+        >>> Path('.').stat()
+        os.stat_result(...)
 
         .. seealso:: :meth:`lstat`, :func:`os.stat`
         """
         return os.stat(self)
 
     def lstat(self):
-        """Like :meth:`stat`, but do not follow symbolic links.
+        """
+        Like :meth:`stat`, but do not follow symbolic links.
+
+        >>> Path('.').lstat() == Path('.').stat()
+        True
 
         .. seealso:: :meth:`stat`, :func:`os.lstat`
         """
         return os.lstat(self)
 
-    def __get_owner_windows(self):
-        """
+    def __get_owner_windows(self):  # pragma: nocover
+        r"""
         Return the name of the owner of this file or directory. Follow
         symbolic links.
 
-        Return a name of the form ``r'DOMAIN\\User Name'``; may be a group.
+        Return a name of the form ``DOMAIN\User Name``; may be a group.
 
         .. seealso:: :attr:`owner`
         """
@@ -1031,7 +1034,7 @@ class Path(str):
         account, domain, typecode = win32security.LookupAccountSid(None, sid)
         return domain + '\\' + account
 
-    def __get_owner_unix(self):
+    def __get_owner_unix(self):  # pragma: nocover
         """
         Return the name of the owner of this file or directory. Follow
         symbolic links.
@@ -1041,15 +1044,16 @@ class Path(str):
         st = self.stat()
         return pwd.getpwuid(st.st_uid).pw_name
 
-    def __get_owner_not_implemented(self):
+    def __get_owner_not_implemented(self):  # pragma: nocover
         raise NotImplementedError("Ownership not available on this platform.")
 
-    if 'win32security' in globals():
-        get_owner = __get_owner_windows
-    elif 'pwd' in globals():
-        get_owner = __get_owner_unix
-    else:
-        get_owner = __get_owner_not_implemented
+    get_owner = (
+        __get_owner_windows
+        if 'win32security' in globals()
+        else __get_owner_unix
+        if 'pwd' in globals()
+        else __get_owner_not_implemented
+    )
 
     owner = property(
         get_owner,
@@ -1078,12 +1082,12 @@ class Path(str):
     #
     # --- Modifying operations on files and directories
 
-    def utime(self, times):
+    def utime(self, *args, **kwargs):
         """Set the access and modified times of this file.
 
         .. seealso:: :func:`os.utime`
         """
-        os.utime(self, times)
+        os.utime(self, *args, **kwargs)
         return self
 
     def chmod(self, mode):
@@ -1099,22 +1103,23 @@ class Path(str):
         os.chmod(self, mode)
         return self
 
-    def chown(self, uid=-1, gid=-1):
-        """
-        Change the owner and group by names rather than the uid or gid numbers.
+    if hasattr(os, 'chown'):
 
-        .. seealso:: :func:`os.chown`
-        """
-        if hasattr(os, 'chown'):
-            if 'pwd' in globals() and isinstance(uid, str):
-                uid = pwd.getpwnam(uid).pw_uid
-            if 'grp' in globals() and isinstance(gid, str):
-                gid = grp.getgrnam(gid).gr_gid
-            os.chown(self, uid, gid)
-        else:
-            msg = "Ownership not available on this platform."
-            raise NotImplementedError(msg)
-        return self
+        def chown(self, uid=-1, gid=-1):
+            """
+            Change the owner and group by names or numbers.
+
+            .. seealso:: :func:`os.chown`
+            """
+
+            def resolve_uid(uid):
+                return uid if isinstance(uid, int) else pwd.getpwnam(uid).pw_uid
+
+            def resolve_gid(gid):
+                return gid if isinstance(gid, int) else grp.getgrnam(gid).gr_gid
+
+            os.chown(self, resolve_uid(uid), resolve_gid(gid))
+            return self
 
     def rename(self, new):
         """.. seealso:: :func:`os.rename`"""
@@ -1203,67 +1208,50 @@ class Path(str):
             self.unlink()
         return self
 
-    def unlink(self):
-        """.. seealso:: :func:`os.unlink`"""
-        os.unlink(self)
-        return self
-
-    def unlink_p(self):
-        """Like :meth:`unlink`, but does not raise an exception if the
-        file does not exist."""
-        self.remove_p()
-        return self
+    unlink = remove
+    unlink_p = remove_p
 
     # --- Links
 
-    if hasattr(os, 'link'):
-
-        def link(self, newpath):
-            """Create a hard link at `newpath`, pointing to this file.
-
-            .. seealso:: :func:`os.link`
-            """
-            os.link(self, newpath)
-            return self._next_class(newpath)
-
-    if hasattr(os, 'symlink'):
+    def link(self, newpath):
+        """Create a hard link at `newpath`, pointing to this file.
 
-        def symlink(self, newlink=None):
-            """Create a symbolic link at `newlink`, pointing here.
+        .. seealso:: :func:`os.link`
+        """
+        os.link(self, newpath)
+        return self._next_class(newpath)
 
-            If newlink is not supplied, the symbolic link will assume
-            the name self.basename(), creating the link in the cwd.
+    def symlink(self, newlink=None):
+        """Create a symbolic link at `newlink`, pointing here.
 
-            .. seealso:: :func:`os.symlink`
-            """
-            if newlink is None:
-                newlink = self.basename()
-            os.symlink(self, newlink)
-            return self._next_class(newlink)
+        If newlink is not supplied, the symbolic link will assume
+        the name self.basename(), creating the link in the cwd.
 
-    if hasattr(os, 'readlink'):
+        .. seealso:: :func:`os.symlink`
+        """
+        if newlink is None:
+            newlink = self.basename()
+        os.symlink(self, newlink)
+        return self._next_class(newlink)
 
-        def readlink(self):
-            """Return the path to which this symbolic link points.
+    def readlink(self):
+        """Return the path to which this symbolic link points.
 
-            The result may be an absolute or a relative path.
+        The result may be an absolute or a relative path.
 
-            .. seealso:: :meth:`readlinkabs`, :func:`os.readlink`
-            """
-            return self._next_class(os.readlink(self))
+        .. seealso:: :meth:`readlinkabs`, :func:`os.readlink`
+        """
+        return self._next_class(os.readlink(self))
 
-        def readlinkabs(self):
-            """Return the path to which this symbolic link points.
+    def readlinkabs(self):
+        """Return the path to which this symbolic link points.
 
-            The result is always an absolute path.
+        The result is always an absolute path.
 
-            .. seealso:: :meth:`readlink`, :func:`os.readlink`
-            """
-            p = self.readlink()
-            if p.isabs():
-                return p
-            else:
-                return (self.parent / p).abspath()
+        .. seealso:: :meth:`readlink`, :func:`os.readlink`
+        """
+        p = self.readlink()
+        return p if p.isabs() else (self.parent / p).abspath()
 
     # High-level functions from shutil
     # These functions will be bound to the instance such that
@@ -1344,15 +1332,15 @@ class Path(str):
 
     if hasattr(os, 'chroot'):
 
-        def chroot(self):
+        def chroot(self):  # pragma: nocover
             """.. seealso:: :func:`os.chroot`"""
             os.chroot(self)
 
     if hasattr(os, 'startfile'):
 
-        def startfile(self):
+        def startfile(self, *args, **kwargs):  # pragma: nocover
             """.. seealso:: :func:`os.startfile`"""
-            os.startfile(self)
+            os.startfile(self, *args, **kwargs)
             return self
 
     # in-place re-writing, courtesy of Martijn Pieters
@@ -1422,8 +1410,7 @@ class Path(str):
             )
         else:
             os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
-            if hasattr(os, 'O_BINARY'):
-                os_mode |= os.O_BINARY
+            os_mode |= getattr(os, 'O_BINARY', 0)
             fd = os.open(self, os_mode, perm)
             writable = io.open(
                 fd,
@@ -1638,14 +1625,10 @@ def _multi_permission_mask(mode):
     """
     Support multiple, comma-separated Unix chmod symbolic modes.
 
-    >>> _multi_permission_mask('a=r,u+w')(0) == 0o644
-    True
+    >>> oct(_multi_permission_mask('a=r,u+w')(0))
+    '0o644'
     """
-
-    def compose(f, g):
-        return lambda *args, **kwargs: g(f(*args, **kwargs))
-
-    return functools.reduce(compose, map(_permission_mask, mode.split(',')))
+    return compose(*map(_permission_mask, reversed(mode.split(','))))
 
 
 def _permission_mask(mode):
@@ -1674,6 +1657,10 @@ def _permission_mask(mode):
 
     >>> _permission_mask('g=')(0o157) == 0o107
     True
+
+    >>> _permission_mask('gobbledeegook')
+    Traceback (most recent call last):
+    ValueError: ('Unrecognized symbolic mode', 'gobbledeegook')
     """
     # parse the symbolic mode
     parsed = re.match('(?P<who>[ugoa]+)(?P<op>[-+=])(?P<what>[rwx]*)$', mode)
diff --git a/setup.cfg b/setup.cfg
index 39edd97..91b4535 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -47,6 +47,7 @@ testing =
 	# local
 	appdirs
 	packaging
+	pywin32; platform_system == "Windows"
 
 	# required for checkdocs on README.rst
 	pygments


Test patch:
diff --git a/test_path.py b/test_path.py
index e5f656b..f4c83ee 100644
--- a/test_path.py
+++ b/test_path.py
@@ -28,6 +28,7 @@ import datetime
 import subprocess
 import re
 import contextlib
+import stat
 
 import pytest
 
@@ -73,7 +74,7 @@ class TestBasics:
         cwd = Path(os.getcwd())
         assert boz.relpath() == cwd.relpathto(boz)
 
-        if os.name == 'nt':
+        if os.name == 'nt':  # pragma: nocover
             # Check relpath across drives.
             d = Path('D:\\')
             assert d.relpathto(boz) == boz
@@ -140,12 +141,6 @@ class TestBasics:
         assert isinstance(cwd, Path)
         assert cwd == os.getcwd()
 
-    @pytest.mark.skipif('not hasattr(os.path, "splitunc")')
-    def test_UNC(self):
-        p = Path(r'\\python1\share1\dir1\file1.txt')
-        assert p.uncshare == r'\\python1\share1'
-        assert p.splitunc() == os.path.splitunc(str(p))
-
     def test_explicit_module(self):
         """
         The user may specify an explicit path module to use.
@@ -196,6 +191,114 @@ class TestBasics:
         assert isinstance(res2, path_posix)
         assert res2 == 'foo/bar'
 
+    def test_radd_string(self):
+        res = 'foo' + Path('bar')
+        assert res == Path('foobar')
+
+    def test_fspath(self):
+        os.fspath(Path('foobar'))
+
+    def test_normpath(self):
+        assert Path('foo//bar').normpath() == os.path.normpath('foo//bar')
+
+    def test_expandvars(self, monkeypatch):
+        monkeypatch.setitem(os.environ, 'sub', 'value')
+        val = '$sub/$(sub)'
+        assert Path(val).expandvars() == os.path.expandvars(val)
+        assert 'value' in Path(val).expandvars()
+
+    def test_expand(self):
+        val = 'foobar'
+        expected = os.path.normpath(os.path.expanduser(os.path.expandvars(val)))
+        assert Path(val).expand() == expected
+
+    def test_splitdrive(self):
+        val = Path.using_module(ntpath)(r'C:\bar')
+        drive, rest = val.splitdrive()
+        assert drive == 'C:'
+        assert rest == r'\bar'
+        assert isinstance(rest, Path)
+
+    def test_relpathto(self):
+        source = Path.using_module(ntpath)(r'C:\foo')
+        dest = Path.using_module(ntpath)(r'D:\bar')
+        assert source.relpathto(dest) == dest
+
+    def test_walk_errors(self):
+        start = Path('/does-not-exist')
+        items = list(start.walk(errors='ignore'))
+        assert not items
+
+    def test_walk_child_error(self, tmpdir):
+        def simulate_access_denied(item):
+            if item.name == 'sub1':
+                raise OSError("Access denied")
+
+        p = Path(tmpdir)
+        (p / 'sub1').makedirs_p()
+        items = path.Traversal(simulate_access_denied)(p.walk(errors='ignore'))
+        assert list(items) == [p / 'sub1']
+
+    def test_read_md5(self, tmpdir):
+        target = Path(tmpdir) / 'some file'
+        target.write_text('quick brown fox and lazy dog')
+        assert target.read_md5() == b's\x15\rPOW\x7fYk\xa8\x8e\x00\x0b\xd7G\xf9'
+
+    def test_read_hexhash(self, tmpdir):
+        target = Path(tmpdir) / 'some file'
+        target.write_text('quick brown fox and lazy dog')
+        assert target.read_hexhash('md5') == '73150d504f577f596ba88e000bd747f9'
+
+    @pytest.mark.skipif("not hasattr(os, 'statvfs')")
+    def test_statvfs(self):
+        Path('.').statvfs()
+
+    @pytest.mark.skipif("not hasattr(os, 'pathconf')")
+    def test_pathconf(self):
+        assert isinstance(Path('.').pathconf(1), int)
+
+    def test_utime(self, tmpdir):
+        tmpfile = Path(tmpdir) / 'file'
+        tmpfile.touch()
+        new_time = (time.time() - 600,) * 2
+        assert Path(tmpfile).utime(new_time).stat().st_atime == new_time[0]
+
+    def test_chmod_str(self, tmpdir):
+        tmpfile = Path(tmpdir) / 'file'
+        tmpfile.touch()
+        tmpfile.chmod('o-r')
+        is_windows = platform.system() == 'Windows'
+        assert is_windows or not (tmpfile.stat().st_mode & stat.S_IROTH)
+
+    @pytest.mark.skipif("not hasattr(Path, 'chown')")
+    def test_chown(self, tmpdir):
+        tmpfile = Path(tmpdir) / 'file'
+        tmpfile.touch()
+        tmpfile.chown(os.getuid(), os.getgid())
+        import pwd
+
+        name = pwd.getpwuid(os.getuid()).pw_name
+        tmpfile.chown(name)
+
+    def test_renames(self, tmpdir):
+        tmpfile = Path(tmpdir) / 'file'
+        tmpfile.touch()
+        tmpfile.renames(Path(tmpdir) / 'foo' / 'alt')
+
+    def test_mkdir_p(self, tmpdir):
+        Path(tmpdir).mkdir_p()
+
+    def test_removedirs_p(self, tmpdir):
+        dir = Path(tmpdir) / 'somedir'
+        dir.mkdir()
+        (dir / 'file').touch()
+        (dir / 'sub').mkdir()
+        dir.removedirs_p()
+        assert dir.isdir()
+        assert (dir / 'file').isfile()
+        # TODO: shouldn't sub get removed?
+        # assert not (dir / 'sub').isdir()
+
 
 class TestReadWriteText:
     def test_read_write(self, tmpdir):
@@ -203,6 +306,7 @@ class TestReadWriteText:
         file.write_text('hello world')
         assert file.read_text() == 'hello world'
         assert file.read_bytes() == b'hello world'
+        file.write_text(b'hello world')
 
 
 class TestPerformance:
@@ -228,6 +332,36 @@ class TestPerformance:
         assert duration < limit
 
 
+class TestOwnership:
+    def test_get_owner(self):
+        Path('/').get_owner()
+
+
+class TestLinks:
+    def test_link(self, tmpdir):
+        target = Path(tmpdir) / 'target'
+        target.write_text('hello', encoding='utf-8')
+        link = target.link(Path(tmpdir) / 'link')
+        assert link.read_text() == 'hello'
+
+    def test_symlink_none(self, tmpdir):
+        root = Path(tmpdir)
+        with root:
+            file = (Path('dir').mkdir() / 'file').touch()
+            file.symlink()
+            assert Path('file').isfile()
+
+    def test_readlinkabs_passthrough(self, tmpdir):
+        link = Path(tmpdir) / 'link'
+        Path('foo').abspath().symlink(link)
+        link.readlinkabs() == Path('foo').abspath()
+
+    def test_readlinkabs_rendered(self, tmpdir):
+        link = Path(tmpdir) / 'link'
+        Path('foo').symlink(link)
+        link.readlinkabs() == Path(tmpdir) / 'foo'
+
+
 class TestSymbolicLinksWalk:
     def test_skip_symlinks(self, tmpdir):
         root = Path(tmpdir)
@@ -281,6 +415,22 @@ class TestSelfReturn:
         assert p == ret
 
 
+@pytest.mark.skipif("not hasattr(Path, 'chroot')")
+def test_chroot(monkeypatch):
+    results = []
+    monkeypatch.setattr(os, 'chroot', results.append)
+    Path().chroot()
+    assert results == ['']
+
+
+@pytest.mark.skipif("not hasattr(Path, 'startfile')")
+def test_startfile(monkeypatch):
+    results = []
+    monkeypatch.setattr(os, 'startfile', results.append)
+    Path().startfile()
+    assert results == ['']
+
+
 class TestScratchDir:
     """
     Tests that run in a temporary directory (does not test TempDir class)
@@ -338,7 +488,7 @@ class TestScratchDir:
         assert t2 <= f.mtime <= t3
         if hasattr(os.path, 'getctime'):
             ct2 = f.ctime
-            if os.name == 'nt':
+            if platform.system() == 'Windows':  # pragma: nocover
                 # On Windows, "ctime" is CREATION time
                 assert ct == ct2
                 assert ct2 < t2
@@ -400,7 +550,7 @@ class TestScratchDir:
         platform.system() != "Linux",
         reason="Only Linux allows writing invalid encodings",
     )
-    def test_listdir_other_encoding(self, tmpdir):
+    def test_listdir_other_encoding(self, tmpdir):  # pragma: nocover
         """
         Some filesystems allow non-character sequences in path names.
         ``.listdir`` should still function in this case.
@@ -505,10 +655,7 @@ class TestScratchDir:
         assert testFile.bytes() == testCopy2.bytes()
 
         # Make a link for the next test to use.
-        if hasattr(os, 'symlink'):
-            testFile.symlink(testLink)
-        else:
-            testFile.copy(testLink)  # fallback
+        testFile.symlink(testLink)
 
         # Test copying directory tree.
         testA.copytree(testC)
@@ -680,7 +827,6 @@ class TestScratchDir:
 
         assert i == len(txt) / size - 1
 
-    @pytest.mark.skipif(not hasattr(os.path, 'samefile'), reason="samefile not present")
     def test_samefile(self, tmpdir):
         f1 = (TempDir() / '1.txt').touch()
         f1.write_text('foo')
@@ -758,10 +904,7 @@ class TestMergeTree:
         with open(self.test_file, 'w') as f:
             f.write('x' * 10000)
 
-        if hasattr(os, 'symlink'):
-            self.test_file.symlink(self.test_link)
-        else:
-            self.test_file.copy(self.test_link)
+        self.test_file.symlink(self.test_link)
 
     def check_link(self):
         target = Path(self.subdir_b / self.test_link.name)
@@ -1070,6 +1213,11 @@ class TestInPlace:
         assert 'Lorem' not in data
         assert 'lazy dog' in data
 
+    def test_write_mode_invalid(self, tmpdir):
+        with pytest.raises(ValueError):
+            with (Path(tmpdir) / 'document').in_place(mode='w'):
+                pass
+
 
 class TestSpecialPaths:
     @pytest.fixture(autouse=True, scope='class')
@@ -1207,3 +1355,30 @@ def test_no_dependencies():
     """
     cmd = [sys.executable, '-S', '-c', 'import path']
     subprocess.check_call(cmd)
+
+
+class TestHandlers:
+    @staticmethod
+    def run_with_handler(handler):
+        try:
+            raise ValueError()
+        except Exception:
+            handler("Something unexpected happened")
+
+    def test_raise(self):
+        handler = path.Handlers._resolve('strict')
+        with pytest.raises(ValueError):
+            self.run_with_handler(handler)
+
+    def test_warn(self):
+        handler = path.Handlers._resolve('warn')
+        with pytest.warns(path.TreeWalkWarning):
+            self.run_with_handler(handler)
+
+    def test_ignore(self):
+        handler = path.Handlers._resolve('ignore')
+        self.run_with_handler(handler)
+
+    def test_invalid_handler(self):
+        with pytest.raises(ValueError):
+            path.Handlers._resolve('raise')