Skip to content

Commit f80ac5f

Browse files
committed
Backport some methods from Python 3.9's pathlib
1 parent 2fdcbaf commit f80ac5f

File tree

2 files changed

+134
-26
lines changed

2 files changed

+134
-26
lines changed

domdf_python_tools/paths.py

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
import pathlib
4747
import shutil
4848
import stat
49-
from typing import IO, Any, Callable, Iterable, List, Optional
49+
import sys
50+
from typing import IO, Any, Callable, Iterable, List, Optional, TypeVar, Union
5051

5152
# this package
5253
from domdf_python_tools.stringlist import StringList
@@ -67,9 +68,14 @@
6768
"PosixPathPlus",
6869
"WindowsPathPlus",
6970
"in_directory",
71+
"_P",
7072
]
7173

7274
newline_default = object()
75+
_P = TypeVar('_P', bound=pathlib.PurePath)
76+
"""
77+
.. versionadded:: 0.11.0
78+
"""
7379

7480

7581
def append(var: str, filename: PathLike, **kwargs) -> int:
@@ -589,6 +595,121 @@ def load_json(
589595
**kwargs,
590596
)
591597

598+
if sys.version_info < (3, 7):
599+
600+
def is_mount(self) -> bool:
601+
"""
602+
Check if this path is a POSIX mount point.
603+
604+
:rtype:
605+
606+
.. versionadded:: 0.3.8 for Python 3.7 and above
607+
.. versionadded:: 0.11.0 for Python 3.6
608+
"""
609+
610+
# Need to exist and be a dir
611+
if not self.exists() or not self.is_dir():
612+
return False
613+
614+
parent = pathlib.Path(self.parent)
615+
try:
616+
parent_dev = parent.stat().st_dev
617+
except OSError:
618+
return False
619+
620+
dev = self.stat().st_dev
621+
if dev != parent_dev:
622+
return True
623+
ino = self.stat().st_ino
624+
parent_ino = parent.stat().st_ino
625+
return ino == parent_ino
626+
627+
if sys.version_info < (3, 8):
628+
629+
def rename(self: _P, target: Union[str, pathlib.PurePath]) -> _P: # type: ignore
630+
"""
631+
Rename this path to the target path.
632+
633+
The target path may be absolute or relative. Relative paths are
634+
interpreted relative to the current working directory, *not* the
635+
directory of the Path object.
636+
637+
:param target:
638+
639+
:returns: The new Path instance pointing to the target path.
640+
641+
.. versionadded:: 0.3.8 for Python 3.8 and above
642+
.. versionadded:: 0.11.0 for Python 3.6 and Python 3.7
643+
"""
644+
645+
self._accessor.rename(self, target) # type: ignore
646+
return self.__class__(target)
647+
648+
def replace(self: _P, target: Union[str, pathlib.PurePath]) -> _P: # type: ignore
649+
"""
650+
Rename this path to the target path, overwriting if that path exists.
651+
652+
The target path may be absolute or relative. Relative paths are
653+
interpreted relative to the current working directory, *not* the
654+
directory of the Path object.
655+
656+
Returns the new Path instance pointing to the target path.
657+
658+
:param target:
659+
660+
:returns: The new Path instance pointing to the target path.
661+
662+
.. versionadded:: 0.3.8 for Python 3.8 and above
663+
.. versionadded:: 0.11.0 for Python 3.6 and Python 3.7
664+
"""
665+
666+
self._accessor.replace(self, target) # type: ignore
667+
return self.__class__(target)
668+
669+
def unlink(self, missing_ok: bool = False) -> None:
670+
"""
671+
Remove this file or link.
672+
673+
If the path is a directory, use :meth:`~domdf_python_tools.paths.PathPlus.rmdir()` instead.
674+
675+
.. versionadded:: 0.3.8 for Python 3.8 and above
676+
.. versionadded:: 0.11.0 for Python 3.6 and Python 3.7
677+
"""
678+
679+
try:
680+
self._accessor.unlink(self) # type: ignore
681+
except FileNotFoundError:
682+
if not missing_ok:
683+
raise
684+
685+
def link_to(self, target: Union[str, bytes, os.PathLike[str]]) -> None:
686+
"""
687+
Create a hard link pointing to a path named target.
688+
689+
:param target:
690+
691+
.. versionadded:: 0.3.8 for Python 3.8 and above
692+
.. versionadded:: 0.11.0 for Python 3.6 and Python 3.7
693+
"""
694+
695+
self._accessor.link_to(self, target) # type: ignore
696+
697+
if sys.version_info < (3, 9):
698+
699+
def __enter__(self):
700+
return self
701+
702+
def __exit__(self, t, v, tb):
703+
# https://bugs.python.org/issue39682
704+
# In previous versions of pathlib, this method marked this path as
705+
# closed; subsequent attempts to perform I/O would raise an IOError.
706+
# This functionality was never documented, and had the effect of
707+
# making Path objects mutable, contrary to PEP 428. In Python 3.9 the
708+
# _closed attribute was removed, and this method made a no-op.
709+
# This method and __enter__()/__exit__() should be deprecated and
710+
# removed in the future.
711+
pass
712+
592713

593714
class PosixPathPlus(PathPlus, pathlib.PurePosixPath):
594715
"""

tests/test_paths_stdlib.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -159,25 +159,15 @@ def test_with(self):
159159
with p:
160160
pass
161161

162-
if sys.version_info < (3, 9): # pragma: no cover (>=py39)
163-
# I/O operation on closed path
164-
self.assertRaises(ValueError, next, it)
165-
self.assertRaises(ValueError, next, it2)
166-
self.assertRaises(ValueError, p.open)
167-
self.assertRaises(ValueError, p.resolve)
168-
self.assertRaises(ValueError, p.absolute)
169-
self.assertRaises(ValueError, p.__enter__)
170-
171-
else: # pragma: no cover (<py39)
172-
# Using a path as a context manager is a no-op, thus the following
173-
# operations should still succeed after the context manage exits.
174-
next(it)
175-
next(it2)
176-
p.exists()
177-
p.resolve()
178-
p.absolute()
179-
with p:
180-
pass
162+
# Using a path as a context manager is a no-op, thus the following
163+
# operations should still succeed after the context manage exits.
164+
next(it)
165+
next(it2)
166+
p.exists()
167+
p.resolve()
168+
p.absolute()
169+
with p:
170+
pass
181171

182172
def test_chmod(self):
183173
p = PathPlus(BASE) / 'fileA'
@@ -239,8 +229,7 @@ def test_unlink(self):
239229
self.assertFileNotFound(p.stat)
240230
self.assertFileNotFound(p.unlink)
241231

242-
@min_version(3.9, "Requires Python 3.9 or higher")
243-
def test_unlink_missing_ok(self): # pragma: no cover (<py37)
232+
def test_unlink_missing_ok(self):
244233
p = PathPlus(BASE) / 'fileAAA'
245234
self.assertFileNotFound(p.unlink)
246235
p.unlink(missing_ok=True) # type: ignore
@@ -253,9 +242,8 @@ def test_rmdir(self):
253242
self.assertFileNotFound(p.stat)
254243
self.assertFileNotFound(p.unlink)
255244

256-
@min_version(3.9, "Requires Python 3.9 or higher")
257245
@unittest.skipUnless(hasattr(os, "link"), "os.link() is not present")
258-
def test_link_to(self): # pragma: no cover (<py37)
246+
def test_link_to(self):
259247
P = PathPlus(BASE)
260248
p = P / 'fileA'
261249
size = p.stat().st_size
@@ -547,9 +535,8 @@ def test_is_file(self):
547535
self.assertFalse((P / 'linkB').is_file())
548536
self.assertFalse((P / 'brokenLink').is_file())
549537

550-
@min_version(3.9, "Requires Python 3.9 or higher")
551538
@only_posix
552-
def test_is_mount(self): # pragma: no cover (<py37)
539+
def test_is_mount(self):
553540
P = PathPlus(BASE)
554541
R = PathPlus('/') # TODO: Work out Windows.
555542
self.assertFalse((P / 'fileA').is_mount()) # type: ignore

0 commit comments

Comments
 (0)