Skip to content

Commit 472311b

Browse files
committed
Added PathPlus and tests for it
1 parent 7192f31 commit 472311b

File tree

3 files changed

+1334
-18
lines changed

3 files changed

+1334
-18
lines changed

domdf_python_tools/paths.py

Lines changed: 179 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# Copytight © 2008 Ned Batchelder
2020
# Licensed under CC-BY-SA
2121
#
22-
# Parts of the docstrings based on the Python 3.8.2 Documentation
22+
# Parts of the docstrings and the PathPlus class based on the Python 3.8.2 Documentation
2323
# Licensed under the Python Software Foundation License Version 2.
2424
# Copyright © 2001-2020 Python Software Foundation. All rights reserved.
2525
# Copyright © 2000 BeOpen.com . All rights reserved.
@@ -47,13 +47,13 @@
4747
import pathlib
4848
import shutil
4949
import stat
50-
from typing import IO, Callable, Optional
50+
from typing import Any, IO, Callable, Optional
5151

5252
# this package
5353
from domdf_python_tools.typing import PathLike
5454

5555

56-
def append(var: str, filename: PathLike):
56+
def append(var: str, filename: PathLike, **kwargs) -> int:
5757
"""
5858
Append ``var`` to the file ``filename`` in the current directory.
5959
@@ -67,8 +67,8 @@ def append(var: str, filename: PathLike):
6767
:param filename: The file to append to
6868
"""
6969

70-
with open(os.path.join(os.getcwd(), filename), 'a') as f:
71-
f.write(var)
70+
with open(os.path.join(os.getcwd(), filename), 'a', **kwargs) as f:
71+
return f.write(var)
7272

7373

7474
def copytree(
@@ -106,12 +106,12 @@ def copytree(
106106
s = os.path.join(src, item)
107107
d = os.path.join(dst, item)
108108
if os.path.isdir(s):
109-
shutil.copytree(s, d, symlinks, ignore)
109+
return shutil.copytree(s, d, symlinks, ignore)
110110
else:
111-
shutil.copy2(s, d)
111+
return shutil.copy2(s, d)
112112

113113

114-
def delete(filename: PathLike):
114+
def delete(filename: PathLike, **kwargs):
115115
"""
116116
Delete the file in the current directory.
117117
@@ -124,12 +124,12 @@ def delete(filename: PathLike):
124124
:param filename: The file to delete
125125
"""
126126

127-
os.remove(os.path.join(os.getcwd(), filename))
127+
os.remove(os.path.join(os.getcwd(), filename), **kwargs)
128128

129129

130-
def maybe_make(directory: PathLike, mode=0o777, parents: bool = False, exist_ok: bool = False):
130+
def maybe_make(directory: PathLike, mode: int = 0o777, parents: bool = False, exist_ok: bool = False):
131131
"""
132-
Create a directory at this given path, but only if the directory does not already exist.
132+
Create a directory at the given path, but only if the directory does not already exist.
133133
134134
:param directory: Directory to create
135135
:param mode: Combined with the process’ umask value to determine the file mode and access flags
@@ -167,7 +167,7 @@ def parent_path(path: PathLike) -> pathlib.Path:
167167
return path.parent
168168

169169

170-
def read(filename: PathLike) -> str:
170+
def read(filename: PathLike, **kwargs) -> str:
171171
"""
172172
Read a file in the current directory (in text mode).
173173
@@ -185,7 +185,7 @@ def read(filename: PathLike) -> str:
185185

186186
# TODO: docstring
187187

188-
with open(os.path.join(os.getcwd(), filename)) as f:
188+
with open(os.path.join(os.getcwd(), filename), **kwargs) as f:
189189
return f.read()
190190

191191

@@ -223,7 +223,7 @@ def relpath(path: PathLike, relative_to: Optional[PathLike] = None) -> pathlib.P
223223
relpath2 = relpath
224224

225225

226-
def write(var: str, filename: PathLike) -> None:
226+
def write(var: str, filename: PathLike, **kwargs) -> None:
227227
"""
228228
Write a variable to file in the current directory.
229229
@@ -233,7 +233,7 @@ def write(var: str, filename: PathLike) -> None:
233233
:param filename: The file to write to
234234
"""
235235

236-
with open(os.path.join(os.getcwd(), filename), 'w') as f:
236+
with open(os.path.join(os.getcwd(), filename), 'w', **kwargs) as f:
237237
f.write(var)
238238

239239

@@ -271,3 +271,167 @@ def make_executable(filename: PathLike) -> None:
271271

272272
st = os.stat(str(filename))
273273
os.chmod(str(filename), st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
274+
275+
276+
class PathPlus(pathlib.Path):
277+
"""
278+
Subclass of :mod:`pathlib.Path` with additional methods and a default encoding of UTF-8.
279+
280+
Path represents a filesystem path but unlike PurePath, also offers
281+
methods to do system calls on path objects. Depending on your system,
282+
instantiating a Path will return either a PosixPath or a WindowsPath
283+
object. You can also instantiate a PosixPath or WindowsPath directly,
284+
but cannot instantiate a WindowsPath on a POSIX system or vice versa.
285+
"""
286+
287+
def __new__(cls, *args, **kwargs):
288+
if cls is PathPlus:
289+
cls = WindowsPathPlus if os.name == 'nt' else PosixPathPlus
290+
self = cls._from_parts(args, init=False)
291+
if not self._flavour.is_supported:
292+
raise NotImplementedError(f"cannot instantiate {cls.__name__!r} on your system")
293+
self._init()
294+
return self
295+
296+
def make_executable(self):
297+
"""
298+
Make the file executable.
299+
"""
300+
301+
make_executable(self)
302+
303+
def write_clean(
304+
self,
305+
string: str,
306+
encoding: Optional[str] = "UTF-8",
307+
errors: Optional[str] = None,
308+
):
309+
"""
310+
Open the file in text mode, write to it without trailing spaces, and close the file.
311+
312+
:param string:
313+
:type string: str
314+
:param encoding: The encoding to write to the file using. Default ``"UTF-8"``.
315+
:param errors:
316+
"""
317+
318+
with self.open("w", encoding=encoding, errors=errors) as fp:
319+
clean_writer(string, fp)
320+
321+
def maybe_make(
322+
self,
323+
mode: int = 0o777,
324+
parents: bool = False,
325+
exist_ok: bool = False,
326+
):
327+
"""
328+
Create a directory at this path, but only if the directory does not already exist.
329+
330+
:param mode: Combined with the process’ umask value to determine the file mode and access flags
331+
:type mode:
332+
:param parents: If :py:obj:`False` (the default), a missing parent raises a :class:`~python:FileNotFoundError`.
333+
If :py:obj:`True`, any missing parents of this path are created as needed; they are created with the
334+
default permissions without taking mode into account (mimicking the POSIX mkdir -p command).
335+
:type parents: bool, optional
336+
:param exist_ok: If :py:obj:`False` (the default), a :class:`~python:FileExistsError` is raised if the
337+
target directory already exists. If :py:obj:`True`, :class:`~python:FileExistsError` exceptions
338+
will be ignored (same behavior as the POSIX mkdir -p command), but only if the last path
339+
component is not an existing non-directory file.
340+
:type exist_ok: bool, optional
341+
"""
342+
343+
maybe_make(self, mode=mode, parents=parents, exist_ok=exist_ok)
344+
345+
def append_text(
346+
self,
347+
string: str,
348+
encoding: Optional[str] = "UTF-8",
349+
errors: Optional[str] = None,
350+
):
351+
"""
352+
Open the file in text mode, append the given string to it, and close the file.
353+
354+
:param string:
355+
:type string: str
356+
:param encoding: The encoding to write to the file using. Default ``"UTF-8"``.
357+
:param errors:
358+
"""
359+
360+
with self.open("a", encoding=encoding, errors=errors) as fp:
361+
fp.write(string)
362+
363+
def write_text(
364+
self,
365+
data: str,
366+
encoding: Optional[str] = "UTF-8",
367+
errors: Optional[str] = None,
368+
) -> int:
369+
"""
370+
Open the file in text mode, write to it, and close the file.
371+
372+
:param data:
373+
:type data: str
374+
:param encoding: The encoding to write to the file using. Default ``"UTF-8"``.
375+
:param errors:
376+
"""
377+
378+
return super().write_text(data, encoding=encoding, errors=errors)
379+
380+
def read_text(
381+
self,
382+
encoding: Optional[str] = "UTF-8",
383+
errors: Optional[str] = None,
384+
) -> str:
385+
"""
386+
Open the file in text mode, read it, and close the file.
387+
388+
:param encoding: The encoding to write to the file using. Default ``"UTF-8"``.
389+
:param errors:
390+
391+
:return: The content of the file.
392+
"""
393+
394+
return super().read_text(encoding=encoding, errors=errors)
395+
396+
def open(
397+
self,
398+
mode: str = "r",
399+
buffering: int = -1,
400+
encoding: Optional[str] = "UTF-8",
401+
errors: Optional[str] = None,
402+
newline: Optional[str] = None,
403+
) -> IO[Any]:
404+
405+
"""
406+
Open the file pointed by this path and return a file object, as
407+
the built-in open() function does.
408+
"""
409+
410+
if 'b' in mode:
411+
encoding = None
412+
return super().open(mode, buffering=buffering, encoding=encoding, errors=errors, newline=newline)
413+
414+
415+
class PosixPathPlus(PathPlus, pathlib.PurePosixPath):
416+
"""Path subclass for non-Windows systems.
417+
418+
On a POSIX system, instantiating a PathPlus object should return an instance of this class.
419+
"""
420+
__slots__ = ()
421+
422+
423+
class WindowsPathPlus(PathPlus, pathlib.PureWindowsPath):
424+
"""Path subclass for Windows systems.
425+
426+
On a Windows system, instantiating a PathPlus object should return an instance of this class.
427+
"""
428+
__slots__ = ()
429+
430+
def owner(self): # pragma: no cover
431+
raise NotImplementedError("Path.owner() is unsupported on this system")
432+
433+
def group(self): # pragma: no cover
434+
raise NotImplementedError("Path.group() is unsupported on this system")
435+
436+
def is_mount(self): # pragma: no cover
437+
raise NotImplementedError("Path.is_mount() is unsupported on this system")

0 commit comments

Comments
 (0)