Coverage for pycommons / io / path.py: 98%
180 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-02 06:36 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-02 06:36 +0000
1"""
2The class `Path` for handling paths to files and directories.
4The instances of :class:`Path` identify file system paths.
5They are always fully canonicalized with all relative components resolved.
6They thus allow the clear and unique identification of files and directories.
7They also offer support for opening streams, creating paths to sub-folders,
8and so on.
10The first goal is to encapsulate the functionality of the :mod:`os.path`
11module into a single class.
12The second goal is to make sure that we do not run into any dodgy situation
13with paths pointing to security-sensitive locations or something due to
14strange `.` and `..` trickery.
15If you try to resolve a path inside a directory and the resulting canonical
16path is outside that directory, you get an error raised, for example.
17"""
19import codecs
20from io import TextIOBase
21from os import O_CREAT, O_EXCL, O_TRUNC, makedirs, scandir
22from os import close as osclose
23from os import open as osopen
24from os import remove as osremove
25from os.path import (
26 abspath,
27 commonpath,
28 dirname,
29 expanduser,
30 expandvars,
31 isdir,
32 isfile,
33 join,
34 normcase,
35 realpath,
36 relpath,
37)
38from os.path import basename as osbasename
39from os.path import exists as osexists
40from shutil import rmtree
41from typing import (
42 Any,
43 Callable,
44 Final,
45 Generator,
46 Iterable,
47 Iterator,
48 TextIO,
49 cast,
50)
52from pycommons.types import check_int_range, type_error
54#: the UTF-8 encoding
55UTF8: Final[str] = "utf-8-sig"
57#: The list of possible text encodings
58__ENCODINGS: Final[tuple[tuple[tuple[bytes, ...], str], ...]] = \
59 (((codecs.BOM_UTF8,), UTF8),
60 ((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE), "utf-32"),
61 ((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE), "utf-16"))
64def _get_text_encoding(filename: str) -> str:
65 r"""
66 Get the text encoding from a BOM if present.
68 If no encoding BOM can be found, we return the standard UTF-8 encoding.
69 Adapted from https://stackoverflow.com/questions/13590749.
71 :param filename: the filename
72 :returns: the encoding
74 >>> from tempfile import mkstemp
75 >>> from os import close as osxclose
76 >>> from os import remove as osremove
77 >>> (h, tf) = mkstemp()
78 >>> osxclose(h)
79 >>> with open(tf, "wb") as out:
80 ... out.write(b'\xef\xbb\xbf')
81 3
82 >>> _get_text_encoding(tf)
83 'utf-8-sig'
85 >>> with open(tf, "wb") as out:
86 ... out.write(b'\xff\xfe\x00\x00')
87 4
88 >>> _get_text_encoding(tf)
89 'utf-32'
91 >>> with open(tf, "wb") as out:
92 ... out.write(b'\x00\x00\xfe\xff')
93 4
94 >>> _get_text_encoding(tf)
95 'utf-32'
97 >>> with open(tf, "wb") as out:
98 ... out.write(b'\xff\xfe')
99 2
100 >>> _get_text_encoding(tf)
101 'utf-16'
103 >>> with open(tf, "wb") as out:
104 ... out.write(b'\xfe\xff')
105 2
106 >>> _get_text_encoding(tf)
107 'utf-16'
109 >>> with open(tf, "wb") as out:
110 ... out.write(b'\xaa\xf3')
111 2
112 >>> _get_text_encoding(tf)
113 'utf-8-sig'
115 >>> osremove(tf)
116 """
117 with open(filename, "rb") as f:
118 header = f.read(4) # Read just the first four bytes.
119 for boms, encoding in __ENCODINGS:
120 for bom in boms:
121 if header.find(bom) == 0:
122 return encoding
123 return UTF8
126class Path(str):
127 """
128 An immutable representation of a canonical path.
130 All instances of this class identify a fully-qualified path which does not
131 contain any relative parts (`"."` or `".."`), is fully expanded, and, if
132 the file system is case-insensitive, has the case normalized. A path is
133 also an instance of `str`, so it can be used wherever strings are required
134 and functions can be designed to accept `str` and receive `Path` instances
135 instead.
137 >>> try:
138 ... Path(1)
139 ... except TypeError as te:
140 ... print(te)
141 descriptor '__len__' requires a 'str' object but received a 'int'
143 >>> try:
144 ... Path(None)
145 ... except TypeError as te:
146 ... print(te)
147 descriptor '__len__' requires a 'str' object but received a 'NoneType'
149 >>> try:
150 ... Path("")
151 ... except ValueError as ve:
152 ... print(ve)
153 Path must not be empty.
155 >>> try:
156 ... Path(" ")
157 ... except ValueError as ve:
158 ... print(ve)
159 Path must not start or end with white space, but ' ' does.
161 >>> from os.path import dirname
162 >>> Path(dirname(realpath(__file__)) + '/..') == \
163dirname(dirname(realpath(__file__)))
164 True
166 >>> Path(dirname(realpath(__file__)) + "/.") == \
167dirname(realpath(__file__))
168 True
170 >>> Path(__file__) == realpath(__file__)
171 True
173 >>> from os import getcwd
174 >>> Path(".") == realpath(getcwd())
175 True
177 >>> from os import getcwd
178 >>> Path("..") == dirname(realpath(getcwd()))
179 True
181 >>> from os import getcwd
182 >>> Path("../.") == dirname(realpath(getcwd()))
183 True
185 >>> from os import getcwd
186 >>> Path("../1.txt") == \
187join(dirname(realpath(getcwd())), "1.txt")
188 True
190 >>> from os import getcwd
191 >>> Path("./1.txt") == join(realpath(getcwd()), "1.txt")
192 True
194 >>> from os.path import isabs
195 >>> isabs(Path(".."))
196 True
197 """
199 # see https://docs.astral.sh/ruff/rules/no-slots-in-str-subclass/
200 __slots__ = ()
202 def __new__(cls, value: Any): # noqa
203 """
204 Construct the path object by normalizing the path string.
206 :param value: the string value
207 :raises TypeError: if `value` is not a string
208 :raises ValueError: if `value` is not a proper path
210 >>> isinstance(Path("."), Path)
211 True
212 >>> isinstance(Path("."), str)
213 True
214 >>> isinstance(Path(".")[-2:], Path)
215 False
216 >>> isinstance(Path(".")[-2:], str)
217 True
218 >>> isinstance(Path(__file__).strip(), Path)
219 False
221 >>> isinstance(__file__, Path)
222 False
223 >>> isinstance(Path(__file__), Path)
224 True
225 >>> p = Path(__file__)
226 >>> Path(p) is p
227 True
229 >>> try:
230 ... Path(None)
231 ... except TypeError as te:
232 ... print(te)
233 descriptor '__len__' requires a 'str' object but received a 'NoneType'
235 >>> try:
236 ... Path(1)
237 ... except TypeError as te:
238 ... print(te)
239 descriptor '__len__' requires a 'str' object but received a 'int'
241 >>> try:
242 ... Path("")
243 ... except ValueError as ve:
244 ... print(ve)
245 Path must not be empty.
246 """
247 if isinstance(value, Path):
248 return cast("Path", value)
250 if str.__len__(value) <= 0:
251 raise ValueError("Path must not be empty.")
252 if str.strip(value) != value:
253 raise ValueError("Path must not start or end with white space, "
254 f"but {value!r} does.")
255 value = normcase(abspath(realpath(expanduser(expandvars(value)))))
256 if (str.__len__(value) <= 0) or (value in {".", ".."}): # impossible!
257 raise ValueError(f"Canonicalization cannot yield {value!r}.")
259 return super().__new__(cls, value)
261 def exists(self) -> bool:
262 """
263 Check if this path identifies an existing file or directory.
265 See also :meth:`~Path.is_file` and :meth:`~Path.is_dir`.
267 :returns: `True` if this path identifies an existing file, `False`
268 otherwise.
270 >>> Path(__file__).exists()
271 True
272 >>> from os.path import dirname
273 >>> Path(dirname(__file__)).exists()
274 True
275 >>> from tempfile import mkstemp
276 >>> from os import close as osxclose
277 >>> from os import remove as osremove
278 >>> (h, tf) = mkstemp()
279 >>> osxclose(h)
280 >>> p = Path(tf)
281 >>> p.exists()
282 True
283 >>> osremove(p)
284 >>> p.exists()
285 False
286 """
287 return osexists(self)
289 def is_file(self) -> bool:
290 """
291 Check if this path identifies an existing file.
293 See also :meth:`~enforce_file`, which raises an error if the `is_file`
294 is not `True`.
296 :returns: `True` if this path identifies an existing file, `False`
297 otherwise.
299 >>> Path(__file__).is_file()
300 True
301 >>> from os.path import dirname
302 >>> Path(dirname(__file__)).is_file()
303 False
304 """
305 return isfile(self)
307 def enforce_file(self) -> None:
308 """
309 Raise an error if the path does not reference an existing file.
311 This function uses :meth:`is_file` internally and raises a
312 `ValueError` if it returns `False`. It is therefore a shorthand
313 for situations where you want to have an error if a path does
314 not identify a file.
316 :raises ValueError: if this path does not reference an existing file
318 >>> Path(__file__).enforce_file() # nothing happens
319 >>> from os import getcwd
320 >>> try:
321 ... Path(getcwd()).enforce_file()
322 ... except ValueError as ve:
323 ... print(str(ve)[-25:])
324 does not identify a file.
325 """
326 if not self.is_file():
327 raise ValueError(f"Path {self!r} does not identify a file.")
329 def is_dir(self) -> bool:
330 """
331 Check if this path identifies an existing directory.
333 The method :meth:`~enforce_dir` also checks this, but raises an
334 exception if it is not `True`.
336 :returns: `True` if this path identifies an existing directory,
337 `False` otherwise.
339 >>> Path(__file__).is_dir()
340 False
341 >>> from os.path import dirname
342 >>> Path(dirname(__file__)).is_dir()
343 True
344 """
345 return isdir(self)
347 def enforce_dir(self) -> None:
348 """
349 Raise an error if the path does not reference an existing directory.
351 This function uses :meth:`is_dir` internally and raises a
352 `ValueError` if it returns `False`. It is therefore a shorthand
353 for situations where you want to have an error if a path does
354 not identify a directory.
356 :raises ValueError: if this path does not reference an existing
357 directory
359 >>> try:
360 ... Path(__file__).enforce_dir()
361 ... except ValueError as ve:
362 ... print(str(ve)[-30:])
363 does not identify a directory.
365 >>> from os import getcwd
366 >>> Path(getcwd()).enforce_dir() # nothing happens
367 """
368 if not self.is_dir():
369 raise ValueError(f"Path {self!r} does not identify a directory.")
371 def contains(self, other: str) -> bool:
372 r"""
373 Check whether this path is a directory and contains another path.
375 A file can never contain anything else. A directory contains itself as
376 well as any sub-directories, i.e., `a/b/` contains `a/b/` and `a/b/c`.
377 The function :meth:`~enforce_contains` throws an exception if the
378 path does not contain `other`.
380 :param other: the other path
381 :returns: `True` is this path contains the other path, `False` of not
383 >>> from os.path import dirname
384 >>> Path(dirname(__file__)).contains(__file__)
385 True
386 >>> Path(__file__).contains(__file__)
387 False
388 >>> Path(dirname(__file__)).contains(dirname(__file__))
389 True
390 >>> Path(__file__).contains(dirname(__file__))
391 False
392 >>> Path(join(dirname(__file__), "a")).contains(
393 ... join(dirname(__file__), "b"))
394 False
396 >>> try:
397 ... Path(dirname(__file__)).contains(1)
398 ... except TypeError as te:
399 ... print(te)
400 descriptor '__len__' requires a 'str' object but received a 'int'
402 >>> try:
403 ... Path(dirname(__file__)).contains(None)
404 ... except TypeError as te:
405 ... print(te)
406 descriptor '__len__' requires a 'str' object but received a 'NoneType'
408 >>> try:
409 ... Path(dirname(__file__)).contains("")
410 ... except ValueError as ve:
411 ... print(ve)
412 Path must not be empty.
414 >>> Path("E:\\").contains("C:\\")
415 False
416 """
417 if not self.is_dir():
418 return False
419 other_path: Final[Path] = Path(other)
420 try:
421 return commonpath([self]) == commonpath([self, other_path])
422 except ValueError: # this happens if paths are on different drives
423 return False # This cannot be reached on Linux systems.
425 def enforce_contains(self, other: str) -> None:
426 """
427 Raise an exception if this is not a directory containing another path.
429 The method :meth:`contains` checks whether this path is a directory
430 and contains the other path and returns the result of this check as a
431 `bool`. This function here raises an exception if that check fails.
433 :param other: the other path
434 :raises ValueError: if `other` is not a sub-path of this path.
436 >>> try:
437 ... Path(__file__).enforce_contains(__file__)
438 ... except ValueError as ve:
439 ... print(str(ve)[-25:])
440 not identify a directory.
442 >>> from os.path import dirname
443 >>> Path(dirname(__file__)).enforce_contains(__file__) # nothing
444 >>> try:
445 ... Path(join(dirname(__file__), "a")).enforce_contains(\
446Path(join(dirname(__file__), "b")))
447 ... except ValueError as ve:
448 ... print(str(ve)[-25:])
449 not identify a directory.
451 >>> Path(dirname(__file__)).enforce_contains(Path(join(dirname(\
452__file__), "b"))) # nothing happens
453 >>> try:
454 ... Path(dirname(__file__)).enforce_contains(dirname(\
455dirname(__file__)))
456 ... except ValueError as ve:
457 ... print(str(ve)[:4])
458 ... print("does not contain" in str(ve))
459 Path
460 True
461 """
462 self.enforce_dir()
463 if not self.contains(other):
464 raise ValueError(f"Path {self!r} does not contain {other!r}.")
466 def resolve_inside(self, relative_path: str) -> "Path":
467 """
468 Resolve a relative path to an absolute path inside this path.
470 Resolve the relative path inside this path. This path must identify
471 a directory. The relative path cannot contain anything that makes it
472 leave the directory, e.g., any `".."`. The paths are joined and then
473 it is enforced that this path must contain the result via
474 :meth:`enforce_contains` and otherwise an error is raised.
476 :param relative_path: the path to resolve
477 :returns: the resolved child path
478 :raises TypeError: If the `relative_path` is not a string.
479 :raises ValueError: If the `relative_path` would resolve to something
480 outside of this path and/or if it is empty.
482 >>> from os.path import dirname
483 >>> Path(dirname(__file__)).resolve_inside("a.txt")[-5:]
484 'a.txt'
486 >>> from os.path import basename
487 >>> Path(dirname(__file__)).resolve_inside(basename(__file__)) \
488== Path(__file__)
489 True
491 >>> try:
492 ... Path(dirname(__file__)).resolve_inside("..")
493 ... except ValueError as ve:
494 ... print("does not contain" in str(ve))
495 True
497 >>> try:
498 ... Path(__file__).resolve_inside("..")
499 ... except ValueError as ve:
500 ... print("does not identify a directory" in str(ve))
501 True
503 >>> try:
504 ... Path(dirname(__file__)).resolve_inside(None)
505 ... except TypeError as te:
506 ... print(te)
507 descriptor '__len__' requires a 'str' object but received a 'NoneType'
509 >>> try:
510 ... Path(dirname(__file__)).resolve_inside(2)
511 ... except TypeError as te:
512 ... print(te)
513 descriptor '__len__' requires a 'str' object but received a 'int'
515 >>> try:
516 ... Path(__file__).resolve_inside("")
517 ... except ValueError as ve:
518 ... print(ve)
519 Relative path must not be empty.
521 >>> try:
522 ... Path(__file__).resolve_inside(" ")
523 ... except ValueError as ve:
524 ... print(ve)
525 Relative path must not start or end with white space, but ' ' does.
526 """
527 if str.__len__(relative_path) == 0:
528 raise ValueError("Relative path must not be empty.")
529 if str.strip(relative_path) != relative_path:
530 raise ValueError("Relative path must not start or end with white "
531 f"space, but {relative_path!r} does.")
532 opath: Final[Path] = Path(join(self, relative_path))
533 self.enforce_contains(opath)
534 return opath
536 def ensure_file_exists(self) -> bool:
537 """
538 Atomically ensure that the file exists and create it otherwise.
540 While :meth:`is_file` checks if the path identifies an existing file
541 and :meth:`enforce_file` raises an error if it does not, this method
542 here creates the file if it does not exist. The method can only create
543 the file if the directory already exists.
545 :returns: `True` if the file already existed and
546 `False` if it was newly and atomically created.
547 :raises: ValueError if anything goes wrong during the file creation
549 >>> print(Path(__file__).ensure_file_exists())
550 True
552 >>> from os.path import dirname
553 >>> try:
554 ... Path(dirname(__file__)).ensure_file_exists()
555 ... print("??")
556 ... except ValueError as ve:
557 ... print("does not identify a file." in str(ve))
558 True
560 >>> try:
561 ... Path(join(join(dirname(__file__), "a"), "b"))\
562.ensure_file_exists()
563 ... print("??")
564 ... except ValueError as ve:
565 ... print("Error when trying to create file" in str(ve))
566 True
567 """
568 existed: bool = False
569 try:
570 osclose(osopen(self, O_CREAT | O_EXCL))
571 except FileExistsError:
572 existed = True
573 except Exception as err:
574 raise ValueError(
575 f"Error when trying to create file {self!r}.") from err
576 self.enforce_file()
577 return existed
579 def create_file_or_truncate(self) -> None:
580 """
581 Create the file identified by this path and truncate it if it exists.
583 :raises: ValueError if anything goes wrong during the file creation
585 >>> from tempfile import mkstemp
586 >>> from os import close as osxclose
587 >>> from os import remove as osremove
588 >>> (h, tf) = mkstemp()
589 >>> osxclose(h)
591 >>> pth = Path(tf)
592 >>> pth.write_all_str("test")
593 >>> print(pth.read_all_str())
594 test
595 <BLANKLINE>
597 >>> pth.create_file_or_truncate()
598 >>> pth.is_file()
599 True
601 >>> try:
602 ... pth.read_all_str()
603 ... except ValueError as ve:
604 ... print(str(ve)[-17:])
605 contains no text.
607 >>> osremove(pth)
608 >>> pth.is_file()
609 False
611 >>> pth.create_file_or_truncate()
612 >>> pth.is_file()
613 True
615 >>> osremove(pth)
617 >>> from os import makedirs as osmkdir
618 >>> from os import rmdir as osrmdir
619 >>> osmkdir(pth)
621 >>> try:
622 ... pth.create_file_or_truncate()
623 ... except ValueError as ve:
624 ... print(str(ve)[:35])
625 Error when truncating/creating file
627 >>> osrmdir(pth)
628 """
629 try:
630 osclose(osopen(self, O_CREAT | O_TRUNC))
631 except BaseException as err: # noqa
632 raise ValueError(
633 f"Error when truncating/creating file {self!r}.") from err
634 self.enforce_file()
636 def ensure_dir_exists(self) -> None:
637 """
638 Make sure that the directory exists, create it otherwise.
640 Method :meth:`is_dir` checks whether the path identifies an
641 existing directory, method :meth:`enforce_dir` raises an error if not,
642 and this method creates the directory if it does not exist.
644 :raises ValueError: if the directory did not exist and creation failed
646 >>> from os.path import dirname
647 >>> Path(dirname(__file__)).ensure_dir_exists() # nothing happens
649 >>> try:
650 ... Path(__file__).ensure_dir_exists()
651 ... except ValueError as ve:
652 ... print("does not identify a directory" in str(ve))
653 True
655 >>> try:
656 ... Path(join(__file__, "a")).ensure_dir_exists()
657 ... except ValueError as ve:
658 ... print("Error when trying to create directory" in str(ve))
659 True
661 >>> from tempfile import mkdtemp
662 >>> from os import rmdir as osrmdirx
663 >>> td = mkdtemp()
664 >>> Path(td).ensure_dir_exists()
665 >>> osrmdirx(td)
666 >>> Path(td).ensure_dir_exists()
667 >>> p = Path(td).resolve_inside("a")
668 >>> p.ensure_dir_exists()
669 >>> p2 = p.resolve_inside("b")
670 >>> p2.ensure_dir_exists()
671 >>> osrmdirx(p2)
672 >>> osrmdirx(p)
673 >>> osrmdirx(td)
674 >>> p2.ensure_dir_exists()
675 >>> osrmdirx(p2)
676 >>> osrmdirx(p)
677 >>> osrmdirx(td)
678 """
679 try:
680 makedirs(name=self, exist_ok=True)
681 except FileExistsError:
682 pass
683 except Exception as err:
684 raise ValueError(
685 f"Error when trying to create directory {self!r}.") from err
686 self.enforce_dir()
688 def ensure_parent_dir_exists(self) -> "Path":
689 """
690 Make sure that the parent directory exists, create it otherwise.
692 This path may identify a file or directory to be created that does not
693 yet exist. The parent directory of this path is ensured to exist,
694 i.e., if it already exists, nothing happens, but if it does not yet
695 exist, it is created. If the parent directory cannot be created, a
696 :class:`ValueError` is raised.
698 :returns: the parent dir
699 :raises ValueError: if the directory did not exist and creation failed
701 >>> from os.path import dirname
702 >>> _ = Path(__file__).ensure_parent_dir_exists() # nothing happens
704 >>> try:
705 ... _ = Path(join(__file__, "a")).ensure_parent_dir_exists()
706 ... except ValueError as ve:
707 ... print("does not identify a directory" in str(ve))
708 True
710 >>> from tempfile import mkdtemp
711 >>> from os import rmdir as osrmdirx
712 >>> td = mkdtemp()
713 >>> tf = Path(join(td, "xxx"))
714 >>> _ = tf.ensure_parent_dir_exists()
715 >>> osrmdirx(td)
716 >>> isdir(dirname(tf))
717 False
718 >>> _ = tf.ensure_parent_dir_exists()
719 >>> isdir(dirname(tf))
720 True
721 >>> osrmdirx(td)
723 >>> td = mkdtemp()
724 >>> isdir(td)
725 True
726 >>> td2 = join(td, "xxx")
727 >>> isdir(td2)
728 False
729 >>> tf = join(td2, "xxx")
730 >>> _ = Path(tf).ensure_parent_dir_exists()
731 >>> isdir(td2)
732 True
733 >>> osrmdirx(td2)
734 >>> osrmdirx(td)
736 >>> td = mkdtemp()
737 >>> isdir(td)
738 True
739 >>> td2 = join(td, "xxx")
740 >>> isdir(td2)
741 False
742 >>> td3 = join(td2, "xxx")
743 >>> isdir(td3)
744 False
745 >>> tf = join(td3, "xxx")
746 >>> _ = Path(tf).ensure_parent_dir_exists()
747 >>> isdir(td3)
748 True
749 >>> isdir(td2)
750 True
751 >>> osrmdirx(td3)
752 >>> osrmdirx(td2)
753 >>> osrmdirx(td)
754 """
755 pd: Final[Path] = Path(dirname(self))
756 Path.ensure_dir_exists(pd)
757 return pd
759 def open_for_read(self) -> TextIOBase:
760 r"""
761 Open this file for reading text.
763 The resulting text stream will automatically use the right encoding
764 and take any encoding error serious. If the path does not identify an
765 existing file, an exception is thrown.
767 :returns: the file open for reading
768 :raises ValueError: if the path does not identify a file
770 >>> with Path(__file__).open_for_read() as rd:
771 ... print(f"{len(rd.readline())}")
772 ... print(f"{rd.readline()!r}")
773 4
774 'The class `Path` for handling paths to files and directories.\n'
776 >>> from os.path import dirname
777 >>> try:
778 ... with Path(dirname(__file__)).open_for_read():
779 ... pass
780 ... except ValueError as ve:
781 ... print(str(ve)[-25:])
782 does not identify a file.
783 """
784 self.enforce_file()
785 return cast("TextIOBase", open( # noqa: SIM115
786 self, encoding=_get_text_encoding(self), errors="strict"))
788 def read_all_str(self) -> str:
789 r"""
790 Read a file as a single string.
792 Read the complete contents of a file as a single string. If the file
793 is empty, an exception will be raised. No modification is applied to
794 the text that is read.
796 :returns: the single string of text
797 :raises ValueError: if the path does not identify a file or if the
798 file it identifies is empty
800 >>> Path(__file__).read_all_str()[4:30]
801 'The class `Path` for handl'
803 >>> from os.path import dirname
804 >>> try:
805 ... Path(dirname(__file__)).read_all_str()
806 ... except ValueError as ve:
807 ... print(str(ve)[-25:])
808 does not identify a file.
810 >>> from tempfile import mkstemp
811 >>> from os import remove as osremovex
812 >>> h, p = mkstemp(text=True)
813 >>> osclose(h)
814 >>> try:
815 ... Path(p).read_all_str()
816 ... except ValueError as ve:
817 ... print(str(ve)[-19:])
818 ' contains no text.
820 >>> with open(p, "wt") as tx:
821 ... tx.write("aa\n")
822 ... tx.write(" bb ")
823 3
824 6
825 >>> Path(p).read_all_str()
826 'aa\n bb '
827 >>> osremovex(p)
828 """
829 with self.open_for_read() as reader:
830 res: Final[str] = reader.read()
831 if str.__len__(res) <= 0:
832 raise ValueError(f"File {self!r} contains no text.")
833 return res
835 def open_for_write(self) -> TextIOBase:
836 """
837 Open the file for writing UTF-8 encoded text.
839 If the path cannot be opened for writing, some error will be raised.
841 :returns: the text io wrapper for writing
842 :raises ValueError: if the path does not identify a file or such a
843 file cannot be created
845 >>> from tempfile import mkstemp
846 >>> from os import remove as osremovex
847 >>> h, p = mkstemp(text=True)
848 >>> osclose(h)
849 >>> with Path(p).open_for_write() as wd:
850 ... wd.write("1234")
851 4
852 >>> Path(p).read_all_str()
853 '1234'
854 >>> osremovex(p)
856 >>> from os.path import dirname
857 >>> try:
858 ... with Path(dirname(__file__)).open_for_write() as wd:
859 ... pass
860 ... except ValueError as ve:
861 ... print("does not identify a file." in str(ve))
862 True
863 """
864 self.ensure_file_exists()
865 return cast("TextIOBase", open( # noqa: SIM115
866 self, mode="w", encoding="utf-8", errors="strict"))
868 def write_all_str(self, contents: str) -> None:
869 r"""
870 Write the given string to the file.
872 The string `contents` is written to a file. If it does not end
873 with `\n`, then `\n` will automatically be appended. No other changes
874 are applied to `contents`. `contents` must be a `str` and it must not
875 be empty.
877 :param contents: the contents to write
878 :raises TypeError: if the contents are not a string or an `Iterable`
879 of strings
880 :raises ValueError: if the path is not a file or it cannot be opened
881 as a file or the `contents` are an empty string
883 >>> from tempfile import mkstemp
884 >>> from os import remove as osremovex
885 >>> h, p = mkstemp(text=True)
886 >>> osclose(h)
888 >>> try:
889 ... Path(p).write_all_str(None)
890 ... except TypeError as te:
891 ... print(str(te))
892 descriptor '__len__' requires a 'str' object but received a 'NoneType'
894 >>> try:
895 ... Path(p).write_all_str(["a"])
896 ... except TypeError as te:
897 ... print(str(te))
898 descriptor '__len__' requires a 'str' object but received a 'list'
900 >>> Path(p).write_all_str("\na\nb")
901 >>> Path(p).read_all_str()
902 '\na\nb\n'
904 >>> Path(p).write_all_str(" \na\n b ")
905 >>> Path(p).read_all_str()
906 ' \na\n b \n'
908 >>> try:
909 ... Path(p).write_all_str("")
910 ... except ValueError as ve:
911 ... print(str(ve)[:34])
912 Cannot write empty content to file
914 >>> osremovex(p)
915 >>> from os.path import dirname
916 >>> try:
917 ... Path(dirname(__file__)).write_all_str("a")
918 ... except ValueError as ve:
919 ... print("does not identify a file." in str(ve))
920 True
921 """
922 if str.__len__(contents) <= 0:
923 raise ValueError(f"Cannot write empty content to file {self!r}.")
924 with self.open_for_write() as writer:
925 writer.write(contents)
926 if contents[-1] != "\n":
927 writer.write("\n")
929 def relative_to(self, base_path: str) -> str:
930 """
931 Compute a relative path of this path towards the given base path.
933 :param base_path: the string
934 :returns: a relative path
935 :raises ValueError: if this path is not inside `base_path` or the
936 relativization result is otherwise invalid
938 >>> from os.path import dirname
939 >>> f = file_path(__file__)
940 >>> d1 = directory_path(dirname(f))
941 >>> d2 = directory_path(dirname(d1))
942 >>> d3 = directory_path(dirname(d2))
943 >>> f.relative_to(d1)
944 'path.py'
945 >>> f.relative_to(d2)
946 'io/path.py'
947 >>> f.relative_to(d3)
948 'pycommons/io/path.py'
949 >>> d1.relative_to(d3)
950 'pycommons/io'
951 >>> d1.relative_to(d1)
952 '.'
954 >>> try:
955 ... d1.relative_to(f)
956 ... except ValueError as ve:
957 ... print(str(ve)[-30:])
958 does not identify a directory.
960 >>> try:
961 ... d2.relative_to(d1)
962 ... except ValueError as ve:
963 ... print(str(ve)[-21:])
964 pycommons/pycommons'.
965 """
966 opath: Final[Path] = Path(base_path)
967 opath.enforce_contains(self)
968 rv: Final[str] = relpath(self, opath)
969 if (str.__len__(rv) == 0) or (str.strip(rv) is not rv):
970 raise ValueError( # close to impossible
971 f"Invalid relative path {rv!r} resulting from relativizing "
972 f"{self!r} to {base_path!r}={opath!r}.")
973 return rv
975 def up(self, levels: int = 1) -> "Path":
976 """
977 Go up the directory tree for a given number of times.
979 Get a `Path` identifying the containing directory, or its containing
980 directory, depending on the number of `levels` specified.
982 :param levels: the number levels to go up: `1` for getting the
983 directly containing directory, `2` for the next higher directory,
984 and so on.
985 :returns: the resulting path
987 >>> f = file_path(__file__)
988 >>> print(f.up()[-13:])
989 /pycommons/io
990 >>> print(f.up(1)[-13:])
991 /pycommons/io
992 >>> print(f.up(2)[-10:])
993 /pycommons
995 >>> try:
996 ... f.up(0)
997 ... except ValueError as ve:
998 ... print(ve)
999 levels=0 is invalid, must be in 1..255.
1001 >>> try:
1002 ... f.up(None)
1003 ... except TypeError as te:
1004 ... print(te)
1005 levels should be an instance of int but is None.
1007 >>> try:
1008 ... f.up('x')
1009 ... except TypeError as te:
1010 ... print(te)
1011 levels should be an instance of int but is str, namely 'x'.
1013 >>> try:
1014 ... f.up(255)
1015 ... except ValueError as ve:
1016 ... print(str(ve)[:70])
1017 Cannot go up from directory '/' anymore when going up for 255 levels f
1018 """
1019 s: str = self
1020 for _ in range(check_int_range(levels, "levels", 1, 255)):
1021 old: str = s
1022 s = dirname(s)
1023 if (str.__len__(s) == 0) or (s == old):
1024 raise ValueError(
1025 f"Cannot go up from directory {old!r} anymore when going "
1026 f"up for {levels} levels from {self!r}.")
1027 return directory_path(s)
1029 def basename(self) -> str:
1030 """
1031 Get the name of the file or directory identified by this path.
1033 :returns: the name of the file or directory
1035 >>> file_path(__file__).basename()
1036 'path.py'
1037 >>> file_path(__file__).up(2).basename()
1038 'pycommons'
1040 >>> try:
1041 ... Path("/").basename()
1042 ... except ValueError as ve:
1043 ... print(ve)
1044 Invalid basename '' of path '/'.
1045 """
1046 s: Final[str] = osbasename(self)
1047 if str.__len__(s) <= 0:
1048 raise ValueError(f"Invalid basename {s!r} of path {self!r}.")
1049 return s
1051 def list_dir(self, files: bool = True,
1052 directories: bool = True) -> Iterator["Path"]:
1053 """
1054 List the files and/or sub-directories in this directory.
1056 :returns: an iterable with the fully-qualified paths
1058 >>> from tempfile import mkstemp, mkdtemp
1059 >>> from os import close as osxclose
1061 >>> dir1 = Path(mkdtemp())
1062 >>> dir2 = Path(mkdtemp(dir=dir1))
1063 >>> dir3 = Path(mkdtemp(dir=dir1))
1064 >>> (h, tf1) = mkstemp(dir=dir1)
1065 >>> osclose(h)
1066 >>> (h, tf2) = mkstemp(dir=dir1)
1067 >>> osclose(h)
1068 >>> file1 = Path(tf1)
1069 >>> file2 = Path(tf2)
1071 >>> set(dir1.list_dir()) == {dir2, dir3, file1, file2}
1072 True
1074 >>> set(dir1.list_dir(files=False)) == {dir2, dir3}
1075 True
1077 >>> set(dir1.list_dir(directories=False)) == {file1, file2}
1078 True
1080 >>> try:
1081 ... dir1.list_dir(None)
1082 ... except TypeError as te:
1083 ... print(te)
1084 files should be an instance of bool but is None.
1086 >>> try:
1087 ... dir1.list_dir(1)
1088 ... except TypeError as te:
1089 ... print(te)
1090 files should be an instance of bool but is int, namely 1.
1092 >>> try:
1093 ... dir1.list_dir(True, None)
1094 ... except TypeError as te:
1095 ... print(te)
1096 directories should be an instance of bool but is None.
1098 >>> try:
1099 ... dir1.list_dir(True, 1)
1100 ... except TypeError as te:
1101 ... print(te)
1102 directories should be an instance of bool but is int, namely 1.
1104 >>> try:
1105 ... dir1.list_dir(False, False)
1106 ... except ValueError as ve:
1107 ... print(ve)
1108 files and directories cannot both be False.
1110 >>> delete_path(dir1)
1111 """
1112 if not isinstance(files, bool):
1113 raise type_error(files, "files", bool)
1114 if not isinstance(directories, bool):
1115 raise type_error(directories, "directories", bool)
1116 if not (files or directories):
1117 raise ValueError("files and directories cannot both be False.")
1118 self.enforce_dir()
1119 return map(self.resolve_inside, (
1120 f.name for f in scandir(self) if (
1121 directories and f.is_dir(follow_symlinks=False)) or (
1122 files and f.is_file(follow_symlinks=False))))
1125def file_path(pathstr: str) -> "Path":
1126 """
1127 Get a path identifying an existing file.
1129 This is a shorthand for creating a :class:`~Path` and then invoking
1130 :meth:`~Path.enforce_file`.
1132 :param pathstr: the path
1133 :returns: the file
1135 >>> file_path(__file__)[-20:]
1136 'pycommons/io/path.py'
1138 >>> from os.path import dirname
1139 >>> try:
1140 ... file_path(dirname(__file__))
1141 ... except ValueError as ve:
1142 ... print("does not identify a file." in str(ve))
1143 True
1144 """
1145 fi: Final[Path] = Path(pathstr)
1146 fi.enforce_file()
1147 return fi
1150def directory_path(pathstr: str) -> "Path":
1151 """
1152 Get a path identifying an existing directory.
1154 This is a shorthand for creating a :class:`~Path` and then invoking
1155 :meth:`~Path.enforce_dir`.
1157 :param pathstr: the path
1158 :returns: the file
1160 >>> from os.path import dirname
1161 >>> directory_path(dirname(__file__))[-12:]
1162 'pycommons/io'
1164 >>> try:
1165 ... directory_path(__file__)
1166 ... except ValueError as ve:
1167 ... print("does not identify a directory." in str(ve))
1168 True
1169 """
1170 fi: Final[Path] = Path(pathstr)
1171 fi.enforce_dir()
1172 return fi
1175#: the ends-with check
1176__ENDSWITH: Final[Callable[[str, str], bool]] = cast(
1177 "Callable[[str, str], bool]", str.endswith)
1180def line_writer(output: TextIO | TextIOBase) -> Callable[[str], None]:
1181 r"""
1182 Create a line-writing :class:`typing.Callable` from an output stream.
1184 This function takes any string passed to it and writes it to the
1185 :class:`typing.TextIO` instance. If the string does not end in `"\n"`,
1186 it then writes `"\n"` as well to terminate the line. If something that
1187 is not a :class:`str` is passed in, it will throw a :class:`TypeError`.
1189 Notice that :meth:`~io.TextIOBase.write` and
1190 :meth:`~io.IOBase.writelines` of class :class:`io.TextIOBase` do not
1191 terminate lines that are written
1192 with a `"\n"`. This means that, unless you manually make sure that all
1193 lines are terminated by `"\n"`, they get written as a single line instead
1194 of multiple lines. To solve this issue conveniently, we provide the
1195 functions :func:`line_writer`, which wraps the
1196 :meth:`~io.TextIOBase.write` into another function, which automatically
1197 terminates all strings passed to it with `"\n"` unless they already end in
1198 `"\n"`, and :func:`write_lines`, which iterates over a sequence of strings
1199 and writes each of them to a given :class:`typing.TextIO` and automatically
1200 adds the `"\n"` terminator to each of them if necessary.
1202 :param output: the output stream
1203 :returns: an instance of :class:`typing.Callable` that will write each
1204 string it receives as a properly terminated line to the output
1205 stream.
1206 :raises TypeError: if `output` is not an instance of
1207 :class:`io.TextIOBase`.
1209 >>> from tempfile import mkstemp
1210 >>> from os import close as osclose
1211 >>> from os import remove as osremove
1212 >>> (h, tf) = mkstemp()
1213 >>> osclose(h)
1215 >>> with open(tf, "wt") as out:
1216 ... w = line_writer(out)
1217 ... w("123")
1218 >>> with open(tf, "rt") as inp:
1219 ... print(list(inp))
1220 ['123\n']
1222 >>> with open(tf, "wt") as out:
1223 ... w = line_writer(out)
1224 ... w("")
1225 >>> with open(tf, "rt") as inp:
1226 ... print(list(inp))
1227 ['\n']
1229 >>> with open(tf, "wt") as out:
1230 ... w = line_writer(out)
1231 ... w("123\n")
1232 >>> with open(tf, "rt") as inp:
1233 ... print(list(inp))
1234 ['123\n']
1236 >>> with open(tf, "wt") as out:
1237 ... w = line_writer(out)
1238 ... w("\n")
1239 >>> with open(tf, "rt") as inp:
1240 ... print(list(inp))
1241 ['\n']
1243 >>> with open(tf, "wt") as out:
1244 ... w = line_writer(out)
1245 ... w("123")
1246 ... w("456")
1247 >>> with open(tf, "rt") as inp:
1248 ... print(list(inp))
1249 ['123\n', '456\n']
1251 >>> with open(tf, "wt") as out:
1252 ... w = line_writer(out)
1253 ... w("123 ")
1254 ... w("")
1255 ... w(" 456")
1256 >>> with open(tf, "rt") as inp:
1257 ... print(list(inp))
1258 ['123 \n', '\n', ' 456\n']
1260 >>> with open(tf, "wt") as out:
1261 ... w = line_writer(out)
1262 ... w("123 \n")
1263 ... w("\n")
1264 ... w(" 456")
1265 >>> with open(tf, "rt") as inp:
1266 ... print(list(inp))
1267 ['123 \n', '\n', ' 456\n']
1269 >>> try:
1270 ... with open(tf, "wt") as out:
1271 ... w = line_writer(out)
1272 ... w("123 ")
1273 ... w(None)
1274 ... except TypeError as te:
1275 ... print(str(te)[:-10])
1276 descriptor 'endswith' for 'str' objects doesn't apply to a 'NoneTy
1278 >>> try:
1279 ... with open(tf, "wt") as out:
1280 ... w = line_writer(out)
1281 ... w("123 ")
1282 ... w(2)
1283 ... except TypeError as te:
1284 ... print(te)
1285 descriptor 'endswith' for 'str' objects doesn't apply to a 'int' object
1287 >>> osremove(tf)
1289 >>> try:
1290 ... line_writer(1)
1291 ... except TypeError as te:
1292 ... print(te)
1293 output should be an instance of io.TextIOBase but is int, namely 1.
1295 >>> try:
1296 ... line_writer(None)
1297 ... except TypeError as te:
1298 ... print(te)
1299 output should be an instance of io.TextIOBase but is None.
1300 """
1301 if not isinstance(output, TextIOBase):
1302 raise type_error(output, "output", TextIOBase)
1304 def __call(s: str, __w: Callable[[str], Any] = output.write) -> None:
1305 b: Final[bool] = __ENDSWITH(s, "\n")
1306 __w(s)
1307 if not b:
1308 __w("\n")
1310 return cast("Callable[[str], None]", __call)
1313def __line_iterator(lines: Iterable[str]) -> Generator[str, None, None]:
1314 r"""
1315 Iterate over the given lines, adding newlines where needed.
1317 :param lines: the lines
1318 :returns: the generator
1320 >>> list(__line_iterator([]))
1321 []
1323 >>> list(__line_iterator(['a']))
1324 ['a', '\n']
1326 >>> list(__line_iterator(['a', 'b']))
1327 ['a', '\n', 'b', '\n']
1329 >>> list(__line_iterator(['a\n']))
1330 ['a\n']
1332 >>> list(__line_iterator(['a\n', 'b']))
1333 ['a\n', 'b', '\n']
1335 >>> list(__line_iterator(['a', 'b\n']))
1336 ['a', '\n', 'b\n']
1338 >>> list(__line_iterator(['a\n', 'b\n']))
1339 ['a\n', 'b\n']
1341 >>> try:
1342 ... list(__line_iterator(["a", 1]))
1343 ... except TypeError as te:
1344 ... print(te)
1345 descriptor 'endswith' for 'str' objects doesn't apply to a 'int' object
1346 """
1347 for line in lines:
1348 b: bool = __ENDSWITH(line, "\n")
1349 yield line
1350 if not b:
1351 yield "\n"
1354def write_lines(lines: Iterable[str], output: TextIO | TextIOBase) -> None:
1355 r"""
1356 Write all the lines in the given :class:`typing.Iterable` to the output.
1358 This function takes care of properly terminating lines using `"\n"` when
1359 writing them to an output and also performs type-checking.
1361 Notice that :meth:`~io.TextIOBase.write` and
1362 :meth:`~io.IOBase.writelines` of class :class:`io.TextIOBase` do not
1363 terminate lines that are written with a `"\n"`. This means that, unless
1364 you manually make sure that all lines are terminated by `"\n"`, they get
1365 written as a single line instead of multiple lines. To solve this issue
1366 conveniently, we provide the functions :func:`line_writer`, which wraps
1367 the :meth:`~io.TextIOBase.write` into another function, which
1368 automatically terminates all strings passed to it with `"\n"` unless they
1369 already end in `"\n"`, and :func:`write_lines`, which iterates over a
1370 sequence of strings and writes each of them to a given
1371 :class:`typing.TextIO` and automatically adds the `"\n"` terminator to
1372 each of them if necessary.
1374 :param lines: the lines
1375 :param output: the output
1376 :raises TypeError: If anything is of the wrong type.
1378 >>> from io import StringIO
1380 >>> with StringIO() as sio:
1381 ... write_lines(("123", "456"), sio)
1382 ... print(sio.getvalue())
1383 123
1384 456
1385 <BLANKLINE>
1387 >>> from io import StringIO
1388 >>> with StringIO() as sio:
1389 ... write_lines(("123\n", "456"), sio)
1390 ... print(sio.getvalue())
1391 123
1392 456
1393 <BLANKLINE>
1395 >>> from io import StringIO
1396 >>> with StringIO() as sio:
1397 ... write_lines(("123\n", "456\n"), sio)
1398 ... print(sio.getvalue())
1399 123
1400 456
1401 <BLANKLINE>
1403 >>> with StringIO() as sio:
1404 ... write_lines(["123"], sio)
1405 ... print(sio.getvalue())
1406 123
1407 <BLANKLINE>
1409 >>> with StringIO() as sio:
1410 ... write_lines(["123\n"], sio)
1411 ... print(sio.getvalue())
1412 123
1413 <BLANKLINE>
1415 >>> with StringIO() as sio:
1416 ... write_lines("123", sio)
1417 ... print(sio.getvalue())
1418 1
1419 2
1420 3
1421 <BLANKLINE>
1423 >>> with StringIO() as sio:
1424 ... write_lines((sss for sss in ["123", "abc"]), sio)
1425 ... print(sio.getvalue())
1426 123
1427 abc
1428 <BLANKLINE>
1430 >>> with StringIO() as sio:
1431 ... write_lines("", sio)
1432 ... print(sio.getvalue())
1433 <BLANKLINE>
1435 >>> from tempfile import mkstemp
1436 >>> from os import close as osclose
1437 >>> from os import remove as osremove
1438 >>> (h, tf) = mkstemp()
1439 >>> osclose(h)
1441 >>> with open(tf, "wt") as out:
1442 ... write_lines(["123"], out)
1443 >>> with open(tf, "rt") as inp:
1444 ... print(list(inp))
1445 ['123\n']
1447 >>> with open(tf, "wt") as out:
1448 ... write_lines([""], out)
1449 >>> with open(tf, "rt") as inp:
1450 ... print(repr(inp.read()))
1451 '\n'
1453 >>> with open(tf, "wt") as out:
1454 ... write_lines(["\n"], out)
1455 >>> with open(tf, "rt") as inp:
1456 ... print(repr(inp.read()))
1457 '\n'
1459 >>> with open(tf, "wt") as out:
1460 ... write_lines([" \n"], out)
1461 >>> with open(tf, "rt") as inp:
1462 ... print(repr(inp.read()))
1463 ' \n'
1465 >>> osremove(tf)
1467 >>> with StringIO() as sio:
1468 ... write_lines(["\n"], sio)
1469 ... print(repr(sio.getvalue()))
1470 '\n'
1472 >>> with StringIO() as sio:
1473 ... write_lines([""], sio)
1474 ... print(repr(sio.getvalue()))
1475 '\n'
1477 >>> sio = StringIO()
1478 >>> try:
1479 ... write_lines(None, sio)
1480 ... except TypeError as te:
1481 ... print(te)
1482 lines should be an instance of typing.Iterable but is None.
1484 >>> sio = StringIO()
1485 >>> try:
1486 ... write_lines(123, sio)
1487 ... except TypeError as te:
1488 ... print(te)
1489 lines should be an instance of typing.Iterable but is int, namely 123.
1491 >>> sio = StringIO()
1492 >>> try:
1493 ... write_lines([1, "sdf"], sio)
1494 ... except TypeError as te:
1495 ... print(te)
1496 descriptor 'endswith' for 'str' objects doesn't apply to a 'int' object
1498 >>> sio = StringIO()
1499 >>> try:
1500 ... write_lines(["sdf", 1], sio)
1501 ... except TypeError as te:
1502 ... print(te)
1503 descriptor 'endswith' for 'str' objects doesn't apply to a 'int' object
1504 >>> print(repr(sio.getvalue()))
1505 'sdf\n'
1507 >>> try:
1508 ... write_lines("x", None)
1509 ... except TypeError as te:
1510 ... print(te)
1511 output should be an instance of io.TextIOBase but is None.
1513 >>> try:
1514 ... write_lines("x", 1)
1515 ... except TypeError as te:
1516 ... print(te)
1517 output should be an instance of io.TextIOBase but is int, namely 1.
1519 >>> try:
1520 ... write_lines(2, 1)
1521 ... except TypeError as te:
1522 ... print(te)
1523 lines should be an instance of typing.Iterable but is int, namely 2.
1524 """
1525 if not isinstance(lines, Iterable):
1526 raise type_error(lines, "lines", Iterable)
1527 if not isinstance(output, TextIOBase):
1528 raise type_error(output, "output", TextIOBase)
1529 output.writelines(__line_iterator(lines))
1532def delete_path(path: str) -> None:
1533 """
1534 Delete a path, completely, and recursively.
1536 This is intentionally inserted as an additional function and not a member
1537 of the :class:`Path` in order make the deletion more explicit and to avoid
1538 any form of accidental deleting. This function will not raise an error if
1539 the file deletion fails.
1541 :param path: The path to be deleted
1542 :raises ValueError: if `path` does not refer to an existing file or
1543 directory
1544 :raises TypeError: if `path` is not a string
1546 >>> from tempfile import mkstemp, mkdtemp
1547 >>> from os import close as osxclose
1549 >>> (h, tf) = mkstemp()
1550 >>> isfile(tf)
1551 True
1552 >>> delete_path(tf)
1553 >>> isfile(tf)
1554 False
1556 >>> try:
1557 ... delete_path(tf)
1558 ... except ValueError as ve:
1559 ... print(str(ve).endswith("is neither file nor directory."))
1560 True
1562 >>> td = mkdtemp()
1563 >>> isdir(td)
1564 True
1565 >>> delete_path(td)
1566 >>> isdir(td)
1567 False
1569 >>> try:
1570 ... delete_path(tf)
1571 ... except ValueError as ve:
1572 ... print(str(ve).endswith("is neither file nor directory."))
1573 True
1574 """
1575 p: Final[Path] = Path(path)
1576 if isfile(p):
1577 osremove(p)
1578 elif isdir(p):
1579 rmtree(p, ignore_errors=True)
1580 else:
1581 raise ValueError(f"{path!r} is neither file nor directory.")