Coverage for pycommons / io / path.py: 98%
180 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-24 03:11 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-24 03:11 +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.
18>>> Path("/tmp/x/../1.txt")
19'/tmp/1.txt'
20"""
22import codecs
23from io import TextIOBase
24from os import O_CREAT, O_EXCL, O_TRUNC, makedirs, scandir
25from os import close as osclose
26from os import open as osopen
27from os import remove as osremove
28from os.path import (
29 abspath,
30 commonpath,
31 dirname,
32 expanduser,
33 expandvars,
34 isdir,
35 isfile,
36 join,
37 normcase,
38 realpath,
39 relpath,
40)
41from os.path import basename as osbasename
42from os.path import exists as osexists
43from shutil import rmtree
44from typing import (
45 Any,
46 Callable,
47 Final,
48 Generator,
49 Iterable,
50 Iterator,
51 TextIO,
52 cast,
53)
55from pycommons.types import check_int_range, type_error
57#: the UTF-8 encoding
58UTF8: Final[str] = "utf-8-sig"
60#: The list of possible text encodings
61__ENCODINGS: Final[tuple[tuple[tuple[bytes, ...], str], ...]] = \
62 (((codecs.BOM_UTF8,), UTF8),
63 ((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE), "utf-32"),
64 ((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE), "utf-16"))
67def _get_text_encoding(filename: str) -> str:
68 r"""
69 Get the text encoding from a BOM if present.
71 If no encoding BOM can be found, we return the standard UTF-8 encoding.
72 Adapted from https://stackoverflow.com/questions/13590749.
74 :param filename: the filename
75 :returns: the encoding
77 >>> from tempfile import mkstemp
78 >>> from os import close as osxclose
79 >>> from os import remove as osremove
80 >>> (h, tf) = mkstemp()
81 >>> osxclose(h)
82 >>> with open(tf, "wb") as out:
83 ... out.write(b'\xef\xbb\xbf')
84 3
85 >>> _get_text_encoding(tf)
86 'utf-8-sig'
88 >>> with open(tf, "wb") as out:
89 ... out.write(b'\xff\xfe\x00\x00')
90 4
91 >>> _get_text_encoding(tf)
92 'utf-32'
94 >>> with open(tf, "wb") as out:
95 ... out.write(b'\x00\x00\xfe\xff')
96 4
97 >>> _get_text_encoding(tf)
98 'utf-32'
100 >>> with open(tf, "wb") as out:
101 ... out.write(b'\xff\xfe')
102 2
103 >>> _get_text_encoding(tf)
104 'utf-16'
106 >>> with open(tf, "wb") as out:
107 ... out.write(b'\xfe\xff')
108 2
109 >>> _get_text_encoding(tf)
110 'utf-16'
112 >>> with open(tf, "wb") as out:
113 ... out.write(b'\xaa\xf3')
114 2
115 >>> _get_text_encoding(tf)
116 'utf-8-sig'
118 >>> osremove(tf)
119 """
120 with open(filename, "rb") as f:
121 header = f.read(4) # Read just the first four bytes.
122 for boms, encoding in __ENCODINGS:
123 for bom in boms:
124 if header.find(bom) == 0:
125 return encoding
126 return UTF8
129class Path(str):
130 """
131 An immutable representation of a canonical path.
133 All instances of this class identify a fully-qualified path which does not
134 contain any relative parts (`"."` or `".."`), is fully expanded, and, if
135 the file system is case-insensitive, has the case normalized. A path is
136 also an instance of `str`, so it can be used wherever strings are required
137 and functions can be designed to accept `str` and receive `Path` instances
138 instead.
140 >>> try:
141 ... Path(1)
142 ... except TypeError as te:
143 ... print(te)
144 descriptor '__len__' requires a 'str' object but received a 'int'
146 >>> try:
147 ... Path(None)
148 ... except TypeError as te:
149 ... print(te)
150 descriptor '__len__' requires a 'str' object but received a 'NoneType'
152 >>> try:
153 ... Path("")
154 ... except ValueError as ve:
155 ... print(ve)
156 Path must not be empty.
158 >>> try:
159 ... Path(" ")
160 ... except ValueError as ve:
161 ... print(ve)
162 Path must not start or end with white space, but ' ' does.
164 >>> from os.path import dirname
165 >>> Path(dirname(realpath(__file__)) + '/..') == \
166dirname(dirname(realpath(__file__)))
167 True
169 >>> Path(dirname(realpath(__file__)) + "/.") == \
170dirname(realpath(__file__))
171 True
173 >>> Path(__file__) == realpath(__file__)
174 True
176 >>> from os import getcwd
177 >>> Path(".") == realpath(getcwd())
178 True
180 >>> from os import getcwd
181 >>> Path("..") == dirname(realpath(getcwd()))
182 True
184 >>> from os import getcwd
185 >>> Path("../.") == dirname(realpath(getcwd()))
186 True
188 >>> from os import getcwd
189 >>> Path("../1.txt") == \
190join(dirname(realpath(getcwd())), "1.txt")
191 True
193 >>> from os import getcwd
194 >>> Path("./1.txt") == join(realpath(getcwd()), "1.txt")
195 True
197 >>> from os.path import isabs
198 >>> isabs(Path(".."))
199 True
200 """
202 # see https://docs.astral.sh/ruff/rules/no-slots-in-str-subclass/
203 __slots__ = ()
205 def __new__(cls, value: Any): # noqa
206 """
207 Construct the path object by normalizing the path string.
209 :param value: the string value
210 :raises TypeError: if `value` is not a string
211 :raises ValueError: if `value` is not a proper path
213 >>> isinstance(Path("."), Path)
214 True
215 >>> isinstance(Path("."), str)
216 True
217 >>> isinstance(Path(".")[-2:], Path)
218 False
219 >>> isinstance(Path(".")[-2:], str)
220 True
221 >>> isinstance(Path(__file__).strip(), Path)
222 False
224 >>> isinstance(__file__, Path)
225 False
226 >>> isinstance(Path(__file__), Path)
227 True
228 >>> p = Path(__file__)
229 >>> Path(p) is p
230 True
232 >>> try:
233 ... Path(None)
234 ... except TypeError as te:
235 ... print(te)
236 descriptor '__len__' requires a 'str' object but received a 'NoneType'
238 >>> try:
239 ... Path(1)
240 ... except TypeError as te:
241 ... print(te)
242 descriptor '__len__' requires a 'str' object but received a 'int'
244 >>> try:
245 ... Path("")
246 ... except ValueError as ve:
247 ... print(ve)
248 Path must not be empty.
249 """
250 if isinstance(value, Path):
251 return cast("Path", value)
253 if str.__len__(value) <= 0:
254 raise ValueError("Path must not be empty.")
255 if str.strip(value) != value:
256 raise ValueError("Path must not start or end with white space, "
257 f"but {value!r} does.")
258 value = normcase(abspath(realpath(expanduser(expandvars(value)))))
259 if (str.__len__(value) <= 0) or (value in {".", ".."}): # impossible!
260 raise ValueError(f"Canonicalization cannot yield {value!r}.")
262 return super().__new__(cls, value)
264 def exists(self) -> bool:
265 """
266 Check if this path identifies an existing file or directory.
268 See also :meth:`~Path.is_file` and :meth:`~Path.is_dir`.
270 :returns: `True` if this path identifies an existing file, `False`
271 otherwise.
273 >>> Path(__file__).exists()
274 True
275 >>> from os.path import dirname
276 >>> Path(dirname(__file__)).exists()
277 True
278 >>> from tempfile import mkstemp
279 >>> from os import close as osxclose
280 >>> from os import remove as osremove
281 >>> (h, tf) = mkstemp()
282 >>> osxclose(h)
283 >>> p = Path(tf)
284 >>> p.exists()
285 True
286 >>> osremove(p)
287 >>> p.exists()
288 False
289 """
290 return osexists(self)
292 def is_file(self) -> bool:
293 """
294 Check if this path identifies an existing file.
296 See also :meth:`~enforce_file`, which raises an error if the `is_file`
297 is not `True`.
299 :returns: `True` if this path identifies an existing file, `False`
300 otherwise.
302 >>> Path(__file__).is_file()
303 True
304 >>> from os.path import dirname
305 >>> Path(dirname(__file__)).is_file()
306 False
307 """
308 return isfile(self)
310 def enforce_file(self) -> None:
311 """
312 Raise an error if the path does not reference an existing file.
314 This function uses :meth:`is_file` internally and raises a
315 `ValueError` if it returns `False`. It is therefore a shorthand
316 for situations where you want to have an error if a path does
317 not identify a file.
319 :raises ValueError: if this path does not reference an existing file
321 >>> Path(__file__).enforce_file() # nothing happens
322 >>> from os import getcwd
323 >>> try:
324 ... Path(getcwd()).enforce_file()
325 ... except ValueError as ve:
326 ... print(str(ve)[-25:])
327 does not identify a file.
328 """
329 if not self.is_file():
330 raise ValueError(f"Path {self!r} does not identify a file.")
332 def is_dir(self) -> bool:
333 """
334 Check if this path identifies an existing directory.
336 The method :meth:`~enforce_dir` also checks this, but raises an
337 exception if it is not `True`.
339 :returns: `True` if this path identifies an existing directory,
340 `False` otherwise.
342 >>> Path(__file__).is_dir()
343 False
344 >>> from os.path import dirname
345 >>> Path(dirname(__file__)).is_dir()
346 True
347 """
348 return isdir(self)
350 def enforce_dir(self) -> None:
351 """
352 Raise an error if the path does not reference an existing directory.
354 This function uses :meth:`is_dir` internally and raises a
355 `ValueError` if it returns `False`. It is therefore a shorthand
356 for situations where you want to have an error if a path does
357 not identify a directory.
359 :raises ValueError: if this path does not reference an existing
360 directory
362 >>> try:
363 ... Path(__file__).enforce_dir()
364 ... except ValueError as ve:
365 ... print(str(ve)[-30:])
366 does not identify a directory.
368 >>> from os import getcwd
369 >>> Path(getcwd()).enforce_dir() # nothing happens
370 """
371 if not self.is_dir():
372 raise ValueError(f"Path {self!r} does not identify a directory.")
374 def contains(self, other: str) -> bool:
375 r"""
376 Check whether this path is a directory and contains another path.
378 A file can never contain anything else. A directory contains itself as
379 well as any sub-directories, i.e., `a/b/` contains `a/b/` and `a/b/c`.
380 The function :meth:`~enforce_contains` throws an exception if the
381 path does not contain `other`.
383 :param other: the other path
384 :returns: `True` is this path contains the other path, `False` of not
386 >>> from os.path import dirname
387 >>> Path(dirname(__file__)).contains(__file__)
388 True
389 >>> Path(__file__).contains(__file__)
390 False
391 >>> Path(dirname(__file__)).contains(dirname(__file__))
392 True
393 >>> Path(__file__).contains(dirname(__file__))
394 False
395 >>> Path(join(dirname(__file__), "a")).contains(
396 ... join(dirname(__file__), "b"))
397 False
399 >>> try:
400 ... Path(dirname(__file__)).contains(1)
401 ... except TypeError as te:
402 ... print(te)
403 descriptor '__len__' requires a 'str' object but received a 'int'
405 >>> try:
406 ... Path(dirname(__file__)).contains(None)
407 ... except TypeError as te:
408 ... print(te)
409 descriptor '__len__' requires a 'str' object but received a 'NoneType'
411 >>> try:
412 ... Path(dirname(__file__)).contains("")
413 ... except ValueError as ve:
414 ... print(ve)
415 Path must not be empty.
417 >>> Path("E:\\").contains("C:\\")
418 False
419 """
420 if not self.is_dir():
421 return False
422 other_path: Final[Path] = Path(other)
423 try:
424 return commonpath([self]) == commonpath([self, other_path])
425 except ValueError: # this happens if paths are on different drives
426 return False # This cannot be reached on Linux systems.
428 def enforce_contains(self, other: str) -> None:
429 """
430 Raise an exception if this is not a directory containing another path.
432 The method :meth:`contains` checks whether this path is a directory
433 and contains the other path and returns the result of this check as a
434 `bool`. This function here raises an exception if that check fails.
436 :param other: the other path
437 :raises ValueError: if `other` is not a sub-path of this path.
439 >>> try:
440 ... Path(__file__).enforce_contains(__file__)
441 ... except ValueError as ve:
442 ... print(str(ve)[-25:])
443 not identify a directory.
445 >>> from os.path import dirname
446 >>> Path(dirname(__file__)).enforce_contains(__file__) # nothing
447 >>> try:
448 ... Path(join(dirname(__file__), "a")).enforce_contains(\
449Path(join(dirname(__file__), "b")))
450 ... except ValueError as ve:
451 ... print(str(ve)[-25:])
452 not identify a directory.
454 >>> Path(dirname(__file__)).enforce_contains(Path(join(dirname(\
455__file__), "b"))) # nothing happens
456 >>> try:
457 ... Path(dirname(__file__)).enforce_contains(dirname(\
458dirname(__file__)))
459 ... except ValueError as ve:
460 ... print(str(ve)[:4])
461 ... print("does not contain" in str(ve))
462 Path
463 True
464 """
465 self.enforce_dir()
466 if not self.contains(other):
467 raise ValueError(f"Path {self!r} does not contain {other!r}.")
469 def resolve_inside(self, relative_path: str) -> "Path":
470 """
471 Resolve a relative path to an absolute path inside this path.
473 Resolve the relative path inside this path. This path must identify
474 a directory. The relative path cannot contain anything that makes it
475 leave the directory, e.g., any `".."`. The paths are joined and then
476 it is enforced that this path must contain the result via
477 :meth:`enforce_contains` and otherwise an error is raised.
479 :param relative_path: the path to resolve
480 :returns: the resolved child path
481 :raises TypeError: If the `relative_path` is not a string.
482 :raises ValueError: If the `relative_path` would resolve to something
483 outside of this path and/or if it is empty.
485 >>> from os.path import dirname
486 >>> Path(dirname(__file__)).resolve_inside("a.txt")[-5:]
487 'a.txt'
489 >>> from os.path import basename
490 >>> Path(dirname(__file__)).resolve_inside(basename(__file__)) \
491== Path(__file__)
492 True
494 >>> try:
495 ... Path(dirname(__file__)).resolve_inside("..")
496 ... except ValueError as ve:
497 ... print("does not contain" in str(ve))
498 True
500 >>> try:
501 ... Path(__file__).resolve_inside("..")
502 ... except ValueError as ve:
503 ... print("does not identify a directory" in str(ve))
504 True
506 >>> try:
507 ... Path(dirname(__file__)).resolve_inside(None)
508 ... except TypeError as te:
509 ... print(te)
510 descriptor '__len__' requires a 'str' object but received a 'NoneType'
512 >>> try:
513 ... Path(dirname(__file__)).resolve_inside(2)
514 ... except TypeError as te:
515 ... print(te)
516 descriptor '__len__' requires a 'str' object but received a 'int'
518 >>> try:
519 ... Path(__file__).resolve_inside("")
520 ... except ValueError as ve:
521 ... print(ve)
522 Relative path must not be empty.
524 >>> try:
525 ... Path(__file__).resolve_inside(" ")
526 ... except ValueError as ve:
527 ... print(ve)
528 Relative path must not start or end with white space, but ' ' does.
529 """
530 if str.__len__(relative_path) == 0:
531 raise ValueError("Relative path must not be empty.")
532 if str.strip(relative_path) != relative_path:
533 raise ValueError("Relative path must not start or end with white "
534 f"space, but {relative_path!r} does.")
535 opath: Final[Path] = Path(join(self, relative_path))
536 self.enforce_contains(opath)
537 return opath
539 def ensure_file_exists(self) -> bool:
540 """
541 Atomically ensure that the file exists and create it otherwise.
543 While :meth:`is_file` checks if the path identifies an existing file
544 and :meth:`enforce_file` raises an error if it does not, this method
545 here creates the file if it does not exist. The method can only create
546 the file if the directory already exists.
548 :returns: `True` if the file already existed and
549 `False` if it was newly and atomically created.
550 :raises: ValueError if anything goes wrong during the file creation
552 >>> print(Path(__file__).ensure_file_exists())
553 True
555 >>> from os.path import dirname
556 >>> try:
557 ... Path(dirname(__file__)).ensure_file_exists()
558 ... print("??")
559 ... except ValueError as ve:
560 ... print("does not identify a file." in str(ve))
561 True
563 >>> try:
564 ... Path(join(join(dirname(__file__), "a"), "b"))\
565.ensure_file_exists()
566 ... print("??")
567 ... except ValueError as ve:
568 ... print("Error when trying to create file" in str(ve))
569 True
570 """
571 existed: bool = False
572 try:
573 osclose(osopen(self, O_CREAT | O_EXCL))
574 except FileExistsError:
575 existed = True
576 except Exception as err:
577 raise ValueError(
578 f"Error when trying to create file {self!r}.") from err
579 self.enforce_file()
580 return existed
582 def create_file_or_truncate(self) -> None:
583 """
584 Create the file identified by this path and truncate it if it exists.
586 :raises: ValueError if anything goes wrong during the file creation
588 >>> from tempfile import mkstemp
589 >>> from os import close as osxclose
590 >>> from os import remove as osremove
591 >>> (h, tf) = mkstemp()
592 >>> osxclose(h)
594 >>> pth = Path(tf)
595 >>> pth.write_all_str("test")
596 >>> print(pth.read_all_str())
597 test
598 <BLANKLINE>
600 >>> pth.create_file_or_truncate()
601 >>> pth.is_file()
602 True
604 >>> try:
605 ... pth.read_all_str()
606 ... except ValueError as ve:
607 ... print(str(ve)[-17:])
608 contains no text.
610 >>> osremove(pth)
611 >>> pth.is_file()
612 False
614 >>> pth.create_file_or_truncate()
615 >>> pth.is_file()
616 True
618 >>> osremove(pth)
620 >>> from os import makedirs as osmkdir
621 >>> from os import rmdir as osrmdir
622 >>> osmkdir(pth)
624 >>> try:
625 ... pth.create_file_or_truncate()
626 ... except ValueError as ve:
627 ... print(str(ve)[:35])
628 Error when truncating/creating file
630 >>> osrmdir(pth)
631 """
632 try:
633 osclose(osopen(self, O_CREAT | O_TRUNC))
634 except BaseException as err: # noqa
635 raise ValueError(
636 f"Error when truncating/creating file {self!r}.") from err
637 self.enforce_file()
639 def ensure_dir_exists(self) -> None:
640 """
641 Make sure that the directory exists, create it otherwise.
643 Method :meth:`is_dir` checks whether the path identifies an
644 existing directory, method :meth:`enforce_dir` raises an error if not,
645 and this method creates the directory if it does not exist.
647 :raises ValueError: if the directory did not exist and creation failed
649 >>> from os.path import dirname
650 >>> Path(dirname(__file__)).ensure_dir_exists() # nothing happens
652 >>> try:
653 ... Path(__file__).ensure_dir_exists()
654 ... except ValueError as ve:
655 ... print("does not identify a directory" in str(ve))
656 True
658 >>> try:
659 ... Path(join(__file__, "a")).ensure_dir_exists()
660 ... except ValueError as ve:
661 ... print("Error when trying to create directory" in str(ve))
662 True
664 >>> from tempfile import mkdtemp
665 >>> from os import rmdir as osrmdirx
666 >>> td = mkdtemp()
667 >>> Path(td).ensure_dir_exists()
668 >>> osrmdirx(td)
669 >>> Path(td).ensure_dir_exists()
670 >>> p = Path(td).resolve_inside("a")
671 >>> p.ensure_dir_exists()
672 >>> p2 = p.resolve_inside("b")
673 >>> p2.ensure_dir_exists()
674 >>> osrmdirx(p2)
675 >>> osrmdirx(p)
676 >>> osrmdirx(td)
677 >>> p2.ensure_dir_exists()
678 >>> osrmdirx(p2)
679 >>> osrmdirx(p)
680 >>> osrmdirx(td)
681 """
682 try:
683 makedirs(name=self, exist_ok=True)
684 except FileExistsError:
685 pass
686 except Exception as err:
687 raise ValueError(
688 f"Error when trying to create directory {self!r}.") from err
689 self.enforce_dir()
691 def ensure_parent_dir_exists(self) -> "Path":
692 """
693 Make sure that the parent directory exists, create it otherwise.
695 This path may identify a file or directory to be created that does not
696 yet exist. The parent directory of this path is ensured to exist,
697 i.e., if it already exists, nothing happens, but if it does not yet
698 exist, it is created. If the parent directory cannot be created, a
699 :class:`ValueError` is raised.
701 :returns: the parent dir
702 :raises ValueError: if the directory did not exist and creation failed
704 >>> from os.path import dirname
705 >>> _ = Path(__file__).ensure_parent_dir_exists() # nothing happens
707 >>> try:
708 ... _ = Path(join(__file__, "a")).ensure_parent_dir_exists()
709 ... except ValueError as ve:
710 ... print("does not identify a directory" in str(ve))
711 True
713 >>> from tempfile import mkdtemp
714 >>> from os import rmdir as osrmdirx
715 >>> td = mkdtemp()
716 >>> tf = Path(join(td, "xxx"))
717 >>> _ = tf.ensure_parent_dir_exists()
718 >>> osrmdirx(td)
719 >>> isdir(dirname(tf))
720 False
721 >>> _ = tf.ensure_parent_dir_exists()
722 >>> isdir(dirname(tf))
723 True
724 >>> osrmdirx(td)
726 >>> td = mkdtemp()
727 >>> isdir(td)
728 True
729 >>> td2 = join(td, "xxx")
730 >>> isdir(td2)
731 False
732 >>> tf = join(td2, "xxx")
733 >>> _ = Path(tf).ensure_parent_dir_exists()
734 >>> isdir(td2)
735 True
736 >>> osrmdirx(td2)
737 >>> osrmdirx(td)
739 >>> td = mkdtemp()
740 >>> isdir(td)
741 True
742 >>> td2 = join(td, "xxx")
743 >>> isdir(td2)
744 False
745 >>> td3 = join(td2, "xxx")
746 >>> isdir(td3)
747 False
748 >>> tf = join(td3, "xxx")
749 >>> _ = Path(tf).ensure_parent_dir_exists()
750 >>> isdir(td3)
751 True
752 >>> isdir(td2)
753 True
754 >>> osrmdirx(td3)
755 >>> osrmdirx(td2)
756 >>> osrmdirx(td)
757 """
758 pd: Final[Path] = Path(dirname(self))
759 Path.ensure_dir_exists(pd)
760 return pd
762 def open_for_read(self) -> TextIOBase:
763 r"""
764 Open this file for reading text.
766 The resulting text stream will automatically use the right encoding
767 and take any encoding error serious. If the path does not identify an
768 existing file, an exception is thrown.
770 :returns: the file open for reading
771 :raises ValueError: if the path does not identify a file
773 >>> with Path(__file__).open_for_read() as rd:
774 ... print(f"{len(rd.readline())}")
775 ... print(f"{rd.readline()!r}")
776 4
777 'The class `Path` for handling paths to files and directories.\n'
779 >>> from os.path import dirname
780 >>> try:
781 ... with Path(dirname(__file__)).open_for_read():
782 ... pass
783 ... except ValueError as ve:
784 ... print(str(ve)[-25:])
785 does not identify a file.
786 """
787 self.enforce_file()
788 return cast("TextIOBase", open( # noqa: SIM115
789 self, encoding=_get_text_encoding(self), errors="strict"))
791 def read_all_str(self) -> str:
792 r"""
793 Read a file as a single string.
795 Read the complete contents of a file as a single string. If the file
796 is empty, an exception will be raised. No modification is applied to
797 the text that is read.
799 :returns: the single string of text
800 :raises ValueError: if the path does not identify a file or if the
801 file it identifies is empty
803 >>> Path(__file__).read_all_str()[4:30]
804 'The class `Path` for handl'
806 >>> from os.path import dirname
807 >>> try:
808 ... Path(dirname(__file__)).read_all_str()
809 ... except ValueError as ve:
810 ... print(str(ve)[-25:])
811 does not identify a file.
813 >>> from tempfile import mkstemp
814 >>> from os import remove as osremovex
815 >>> h, p = mkstemp(text=True)
816 >>> osclose(h)
817 >>> try:
818 ... Path(p).read_all_str()
819 ... except ValueError as ve:
820 ... print(str(ve)[-19:])
821 ' contains no text.
823 >>> with open(p, "wt") as tx:
824 ... tx.write("aa\n")
825 ... tx.write(" bb ")
826 3
827 6
828 >>> Path(p).read_all_str()
829 'aa\n bb '
830 >>> osremovex(p)
831 """
832 with self.open_for_read() as reader:
833 res: Final[str] = reader.read()
834 if str.__len__(res) <= 0:
835 raise ValueError(f"File {self!r} contains no text.")
836 return res
838 def open_for_write(self) -> TextIOBase:
839 """
840 Open the file for writing UTF-8 encoded text.
842 If the path cannot be opened for writing, some error will be raised.
844 :returns: the text io wrapper for writing
845 :raises ValueError: if the path does not identify a file or such a
846 file cannot be created
848 >>> from tempfile import mkstemp
849 >>> from os import remove as osremovex
850 >>> h, p = mkstemp(text=True)
851 >>> osclose(h)
852 >>> with Path(p).open_for_write() as wd:
853 ... wd.write("1234")
854 4
855 >>> Path(p).read_all_str()
856 '1234'
857 >>> osremovex(p)
859 >>> from os.path import dirname
860 >>> try:
861 ... with Path(dirname(__file__)).open_for_write() as wd:
862 ... pass
863 ... except ValueError as ve:
864 ... print("does not identify a file." in str(ve))
865 True
866 """
867 self.ensure_file_exists()
868 return cast("TextIOBase", open( # noqa: SIM115
869 self, mode="w", encoding="utf-8", errors="strict"))
871 def write_all_str(self, contents: str) -> None:
872 r"""
873 Write the given string to the file.
875 The string `contents` is written to a file. If it does not end
876 with `\n`, then `\n` will automatically be appended. No other changes
877 are applied to `contents`. `contents` must be a `str` and it must not
878 be empty.
880 :param contents: the contents to write
881 :raises TypeError: if the contents are not a string or an `Iterable`
882 of strings
883 :raises ValueError: if the path is not a file or it cannot be opened
884 as a file or the `contents` are an empty string
886 >>> from tempfile import mkstemp
887 >>> from os import remove as osremovex
888 >>> h, p = mkstemp(text=True)
889 >>> osclose(h)
891 >>> try:
892 ... Path(p).write_all_str(None)
893 ... except TypeError as te:
894 ... print(str(te))
895 descriptor '__len__' requires a 'str' object but received a 'NoneType'
897 >>> try:
898 ... Path(p).write_all_str(["a"])
899 ... except TypeError as te:
900 ... print(str(te))
901 descriptor '__len__' requires a 'str' object but received a 'list'
903 >>> Path(p).write_all_str("\na\nb")
904 >>> Path(p).read_all_str()
905 '\na\nb\n'
907 >>> Path(p).write_all_str(" \na\n b ")
908 >>> Path(p).read_all_str()
909 ' \na\n b \n'
911 >>> try:
912 ... Path(p).write_all_str("")
913 ... except ValueError as ve:
914 ... print(str(ve)[:34])
915 Cannot write empty content to file
917 >>> osremovex(p)
918 >>> from os.path import dirname
919 >>> try:
920 ... Path(dirname(__file__)).write_all_str("a")
921 ... except ValueError as ve:
922 ... print("does not identify a file." in str(ve))
923 True
924 """
925 if str.__len__(contents) <= 0:
926 raise ValueError(f"Cannot write empty content to file {self!r}.")
927 with self.open_for_write() as writer:
928 writer.write(contents)
929 if contents[-1] != "\n":
930 writer.write("\n")
932 def relative_to(self, base_path: str) -> str:
933 """
934 Compute a relative path of this path towards the given base path.
936 :param base_path: the string
937 :returns: a relative path
938 :raises ValueError: if this path is not inside `base_path` or the
939 relativization result is otherwise invalid
941 >>> from os.path import dirname
942 >>> f = file_path(__file__)
943 >>> d1 = directory_path(dirname(f))
944 >>> d2 = directory_path(dirname(d1))
945 >>> d3 = directory_path(dirname(d2))
946 >>> f.relative_to(d1)
947 'path.py'
948 >>> f.relative_to(d2)
949 'io/path.py'
950 >>> f.relative_to(d3)
951 'pycommons/io/path.py'
952 >>> d1.relative_to(d3)
953 'pycommons/io'
954 >>> d1.relative_to(d1)
955 '.'
957 >>> try:
958 ... d1.relative_to(f)
959 ... except ValueError as ve:
960 ... print(str(ve)[-30:])
961 does not identify a directory.
963 >>> try:
964 ... d2.relative_to(d1)
965 ... except ValueError as ve:
966 ... print(str(ve)[-21:])
967 pycommons/pycommons'.
968 """
969 opath: Final[Path] = Path(base_path)
970 opath.enforce_contains(self)
971 rv: Final[str] = relpath(self, opath)
972 if (str.__len__(rv) == 0) or (str.strip(rv) is not rv):
973 raise ValueError( # close to impossible
974 f"Invalid relative path {rv!r} resulting from relativizing "
975 f"{self!r} to {base_path!r}={opath!r}.")
976 return rv
978 def up(self, levels: int = 1) -> "Path":
979 """
980 Go up the directory tree for a given number of times.
982 Get a `Path` identifying the containing directory, or its containing
983 directory, depending on the number of `levels` specified.
985 :param levels: the number levels to go up: `1` for getting the
986 directly containing directory, `2` for the next higher directory,
987 and so on.
988 :returns: the resulting path
990 >>> f = file_path(__file__)
991 >>> print(f.up()[-13:])
992 /pycommons/io
993 >>> print(f.up(1)[-13:])
994 /pycommons/io
995 >>> print(f.up(2)[-10:])
996 /pycommons
998 >>> try:
999 ... f.up(0)
1000 ... except ValueError as ve:
1001 ... print(ve)
1002 levels=0 is invalid, must be in 1..255.
1004 >>> try:
1005 ... f.up(None)
1006 ... except TypeError as te:
1007 ... print(te)
1008 levels should be an instance of int but is None.
1010 >>> try:
1011 ... f.up('x')
1012 ... except TypeError as te:
1013 ... print(te)
1014 levels should be an instance of int but is str, namely 'x'.
1016 >>> try:
1017 ... f.up(255)
1018 ... except ValueError as ve:
1019 ... print(str(ve)[:70])
1020 Cannot go up from directory '/' anymore when going up for 255 levels f
1021 """
1022 s: str = self
1023 for _ in range(check_int_range(levels, "levels", 1, 255)):
1024 old: str = s
1025 s = dirname(s)
1026 if (str.__len__(s) == 0) or (s == old):
1027 raise ValueError(
1028 f"Cannot go up from directory {old!r} anymore when going "
1029 f"up for {levels} levels from {self!r}.")
1030 return directory_path(s)
1032 def basename(self) -> str:
1033 """
1034 Get the name of the file or directory identified by this path.
1036 :returns: the name of the file or directory
1038 >>> file_path(__file__).basename()
1039 'path.py'
1040 >>> file_path(__file__).up(2).basename()
1041 'pycommons'
1043 >>> try:
1044 ... Path("/").basename()
1045 ... except ValueError as ve:
1046 ... print(ve)
1047 Invalid basename '' of path '/'.
1048 """
1049 s: Final[str] = osbasename(self)
1050 if str.__len__(s) <= 0:
1051 raise ValueError(f"Invalid basename {s!r} of path {self!r}.")
1052 return s
1054 def list_dir(self, files: bool = True,
1055 directories: bool = True) -> Iterator["Path"]:
1056 """
1057 List the files and/or sub-directories in this directory.
1059 :returns: an iterable with the fully-qualified paths
1061 >>> from tempfile import mkstemp, mkdtemp
1062 >>> from os import close as osxclose
1064 >>> dir1 = Path(mkdtemp())
1065 >>> dir2 = Path(mkdtemp(dir=dir1))
1066 >>> dir3 = Path(mkdtemp(dir=dir1))
1067 >>> (h, tf1) = mkstemp(dir=dir1)
1068 >>> osclose(h)
1069 >>> (h, tf2) = mkstemp(dir=dir1)
1070 >>> osclose(h)
1071 >>> file1 = Path(tf1)
1072 >>> file2 = Path(tf2)
1074 >>> set(dir1.list_dir()) == {dir2, dir3, file1, file2}
1075 True
1077 >>> set(dir1.list_dir(files=False)) == {dir2, dir3}
1078 True
1080 >>> set(dir1.list_dir(directories=False)) == {file1, file2}
1081 True
1083 >>> try:
1084 ... dir1.list_dir(None)
1085 ... except TypeError as te:
1086 ... print(te)
1087 files should be an instance of bool but is None.
1089 >>> try:
1090 ... dir1.list_dir(1)
1091 ... except TypeError as te:
1092 ... print(te)
1093 files should be an instance of bool but is int, namely 1.
1095 >>> try:
1096 ... dir1.list_dir(True, None)
1097 ... except TypeError as te:
1098 ... print(te)
1099 directories should be an instance of bool but is None.
1101 >>> try:
1102 ... dir1.list_dir(True, 1)
1103 ... except TypeError as te:
1104 ... print(te)
1105 directories should be an instance of bool but is int, namely 1.
1107 >>> try:
1108 ... dir1.list_dir(False, False)
1109 ... except ValueError as ve:
1110 ... print(ve)
1111 files and directories cannot both be False.
1113 >>> delete_path(dir1)
1114 """
1115 if not isinstance(files, bool):
1116 raise type_error(files, "files", bool)
1117 if not isinstance(directories, bool):
1118 raise type_error(directories, "directories", bool)
1119 if not (files or directories):
1120 raise ValueError("files and directories cannot both be False.")
1121 self.enforce_dir()
1122 return map(self.resolve_inside, (
1123 f.name for f in scandir(self) if (
1124 directories and f.is_dir(follow_symlinks=False)) or (
1125 files and f.is_file(follow_symlinks=False))))
1128def file_path(pathstr: str) -> "Path":
1129 """
1130 Get a path identifying an existing file.
1132 This is a shorthand for creating a :class:`~Path` and then invoking
1133 :meth:`~Path.enforce_file`.
1135 :param pathstr: the path
1136 :returns: the file
1138 >>> file_path(__file__)[-20:]
1139 'pycommons/io/path.py'
1141 >>> from os.path import dirname
1142 >>> try:
1143 ... file_path(dirname(__file__))
1144 ... except ValueError as ve:
1145 ... print("does not identify a file." in str(ve))
1146 True
1147 """
1148 fi: Final[Path] = Path(pathstr)
1149 fi.enforce_file()
1150 return fi
1153def directory_path(pathstr: str) -> "Path":
1154 """
1155 Get a path identifying an existing directory.
1157 This is a shorthand for creating a :class:`~Path` and then invoking
1158 :meth:`~Path.enforce_dir`.
1160 :param pathstr: the path
1161 :returns: the file
1163 >>> from os.path import dirname
1164 >>> directory_path(dirname(__file__))[-12:]
1165 'pycommons/io'
1167 >>> try:
1168 ... directory_path(__file__)
1169 ... except ValueError as ve:
1170 ... print("does not identify a directory." in str(ve))
1171 True
1172 """
1173 fi: Final[Path] = Path(pathstr)
1174 fi.enforce_dir()
1175 return fi
1178#: the ends-with check
1179__ENDSWITH: Final[Callable[[str, str], bool]] = cast(
1180 "Callable[[str, str], bool]", str.endswith)
1183def line_writer(output: TextIO | TextIOBase) -> Callable[[str], None]:
1184 r"""
1185 Create a line-writing :class:`typing.Callable` from an output stream.
1187 This function takes any string passed to it and writes it to the
1188 :class:`typing.TextIO` instance. If the string does not end in `"\n"`,
1189 it then writes `"\n"` as well to terminate the line. If something that
1190 is not a :class:`str` is passed in, it will throw a :class:`TypeError`.
1192 Notice that :meth:`~io.TextIOBase.write` and
1193 :meth:`~io.IOBase.writelines` of class :class:`io.TextIOBase` do not
1194 terminate lines that are written
1195 with a `"\n"`. This means that, unless you manually make sure that all
1196 lines are terminated by `"\n"`, they get written as a single line instead
1197 of multiple lines. To solve this issue conveniently, we provide the
1198 functions :func:`line_writer`, which wraps the
1199 :meth:`~io.TextIOBase.write` into another function, which automatically
1200 terminates all strings passed to it with `"\n"` unless they already end in
1201 `"\n"`, and :func:`write_lines`, which iterates over a sequence of strings
1202 and writes each of them to a given :class:`typing.TextIO` and automatically
1203 adds the `"\n"` terminator to each of them if necessary.
1205 :param output: the output stream
1206 :returns: an instance of :class:`typing.Callable` that will write each
1207 string it receives as a properly terminated line to the output
1208 stream.
1209 :raises TypeError: if `output` is not an instance of
1210 :class:`io.TextIOBase`.
1212 >>> from tempfile import mkstemp
1213 >>> from os import close as osclose
1214 >>> from os import remove as osremove
1215 >>> (h, tf) = mkstemp()
1216 >>> osclose(h)
1218 >>> with open(tf, "wt") as out:
1219 ... w = line_writer(out)
1220 ... w("123")
1221 >>> with open(tf, "rt") as inp:
1222 ... print(list(inp))
1223 ['123\n']
1225 >>> with open(tf, "wt") as out:
1226 ... w = line_writer(out)
1227 ... w("")
1228 >>> with open(tf, "rt") as inp:
1229 ... print(list(inp))
1230 ['\n']
1232 >>> with open(tf, "wt") as out:
1233 ... w = line_writer(out)
1234 ... w("123\n")
1235 >>> with open(tf, "rt") as inp:
1236 ... print(list(inp))
1237 ['123\n']
1239 >>> with open(tf, "wt") as out:
1240 ... w = line_writer(out)
1241 ... w("\n")
1242 >>> with open(tf, "rt") as inp:
1243 ... print(list(inp))
1244 ['\n']
1246 >>> with open(tf, "wt") as out:
1247 ... w = line_writer(out)
1248 ... w("123")
1249 ... w("456")
1250 >>> with open(tf, "rt") as inp:
1251 ... print(list(inp))
1252 ['123\n', '456\n']
1254 >>> with open(tf, "wt") as out:
1255 ... w = line_writer(out)
1256 ... w("123 ")
1257 ... w("")
1258 ... w(" 456")
1259 >>> with open(tf, "rt") as inp:
1260 ... print(list(inp))
1261 ['123 \n', '\n', ' 456\n']
1263 >>> with open(tf, "wt") as out:
1264 ... w = line_writer(out)
1265 ... w("123 \n")
1266 ... w("\n")
1267 ... w(" 456")
1268 >>> with open(tf, "rt") as inp:
1269 ... print(list(inp))
1270 ['123 \n', '\n', ' 456\n']
1272 >>> try:
1273 ... with open(tf, "wt") as out:
1274 ... w = line_writer(out)
1275 ... w("123 ")
1276 ... w(None)
1277 ... except TypeError as te:
1278 ... print(str(te)[:-10])
1279 descriptor 'endswith' for 'str' objects doesn't apply to a 'NoneTy
1281 >>> try:
1282 ... with open(tf, "wt") as out:
1283 ... w = line_writer(out)
1284 ... w("123 ")
1285 ... w(2)
1286 ... except TypeError as te:
1287 ... print(te)
1288 descriptor 'endswith' for 'str' objects doesn't apply to a 'int' object
1290 >>> osremove(tf)
1292 >>> try:
1293 ... line_writer(1)
1294 ... except TypeError as te:
1295 ... print(te)
1296 output should be an instance of io.TextIOBase but is int, namely 1.
1298 >>> try:
1299 ... line_writer(None)
1300 ... except TypeError as te:
1301 ... print(te)
1302 output should be an instance of io.TextIOBase but is None.
1303 """
1304 if not isinstance(output, TextIOBase):
1305 raise type_error(output, "output", TextIOBase)
1307 def __call(s: str, __w: Callable[[str], Any] = output.write) -> None:
1308 b: Final[bool] = __ENDSWITH(s, "\n")
1309 __w(s)
1310 if not b:
1311 __w("\n")
1313 return cast("Callable[[str], None]", __call)
1316def __line_iterator(lines: Iterable[str]) -> Generator[str, None, None]:
1317 r"""
1318 Iterate over the given lines, adding newlines where needed.
1320 :param lines: the lines
1321 :returns: the generator
1323 >>> list(__line_iterator([]))
1324 []
1326 >>> list(__line_iterator(['a']))
1327 ['a', '\n']
1329 >>> list(__line_iterator(['a', 'b']))
1330 ['a', '\n', 'b', '\n']
1332 >>> list(__line_iterator(['a\n']))
1333 ['a\n']
1335 >>> list(__line_iterator(['a\n', 'b']))
1336 ['a\n', 'b', '\n']
1338 >>> list(__line_iterator(['a', 'b\n']))
1339 ['a', '\n', 'b\n']
1341 >>> list(__line_iterator(['a\n', 'b\n']))
1342 ['a\n', 'b\n']
1344 >>> try:
1345 ... list(__line_iterator(["a", 1]))
1346 ... except TypeError as te:
1347 ... print(te)
1348 descriptor 'endswith' for 'str' objects doesn't apply to a 'int' object
1349 """
1350 for line in lines:
1351 b: bool = __ENDSWITH(line, "\n")
1352 yield line
1353 if not b:
1354 yield "\n"
1357def write_lines(lines: Iterable[str], output: TextIO | TextIOBase) -> None:
1358 r"""
1359 Write all the lines in the given :class:`typing.Iterable` to the output.
1361 This function takes care of properly terminating lines using `"\n"` when
1362 writing them to an output and also performs type-checking.
1364 Notice that :meth:`~io.TextIOBase.write` and
1365 :meth:`~io.IOBase.writelines` of class :class:`io.TextIOBase` do not
1366 terminate lines that are written with a `"\n"`. This means that, unless
1367 you manually make sure that all lines are terminated by `"\n"`, they get
1368 written as a single line instead of multiple lines. To solve this issue
1369 conveniently, we provide the functions :func:`line_writer`, which wraps
1370 the :meth:`~io.TextIOBase.write` into another function, which
1371 automatically terminates all strings passed to it with `"\n"` unless they
1372 already end in `"\n"`, and :func:`write_lines`, which iterates over a
1373 sequence of strings and writes each of them to a given
1374 :class:`typing.TextIO` and automatically adds the `"\n"` terminator to
1375 each of them if necessary.
1377 :param lines: the lines
1378 :param output: the output
1379 :raises TypeError: If anything is of the wrong type.
1381 >>> from io import StringIO
1383 >>> with StringIO() as sio:
1384 ... write_lines(("123", "456"), sio)
1385 ... print(sio.getvalue())
1386 123
1387 456
1388 <BLANKLINE>
1390 >>> from io import StringIO
1391 >>> with StringIO() as sio:
1392 ... write_lines(("123\n", "456"), sio)
1393 ... print(sio.getvalue())
1394 123
1395 456
1396 <BLANKLINE>
1398 >>> from io import StringIO
1399 >>> with StringIO() as sio:
1400 ... write_lines(("123\n", "456\n"), sio)
1401 ... print(sio.getvalue())
1402 123
1403 456
1404 <BLANKLINE>
1406 >>> with StringIO() as sio:
1407 ... write_lines(["123"], sio)
1408 ... print(sio.getvalue())
1409 123
1410 <BLANKLINE>
1412 >>> with StringIO() as sio:
1413 ... write_lines(["123\n"], sio)
1414 ... print(sio.getvalue())
1415 123
1416 <BLANKLINE>
1418 >>> with StringIO() as sio:
1419 ... write_lines("123", sio)
1420 ... print(sio.getvalue())
1421 1
1422 2
1423 3
1424 <BLANKLINE>
1426 >>> with StringIO() as sio:
1427 ... write_lines((sss for sss in ["123", "abc"]), sio)
1428 ... print(sio.getvalue())
1429 123
1430 abc
1431 <BLANKLINE>
1433 >>> with StringIO() as sio:
1434 ... write_lines("", sio)
1435 ... print(sio.getvalue())
1436 <BLANKLINE>
1438 >>> from tempfile import mkstemp
1439 >>> from os import close as osclose
1440 >>> from os import remove as osremove
1441 >>> (h, tf) = mkstemp()
1442 >>> osclose(h)
1444 >>> with open(tf, "wt") as out:
1445 ... write_lines(["123"], out)
1446 >>> with open(tf, "rt") as inp:
1447 ... print(list(inp))
1448 ['123\n']
1450 >>> with open(tf, "wt") as out:
1451 ... write_lines([""], out)
1452 >>> with open(tf, "rt") as inp:
1453 ... print(repr(inp.read()))
1454 '\n'
1456 >>> with open(tf, "wt") as out:
1457 ... write_lines(["\n"], out)
1458 >>> with open(tf, "rt") as inp:
1459 ... print(repr(inp.read()))
1460 '\n'
1462 >>> with open(tf, "wt") as out:
1463 ... write_lines([" \n"], out)
1464 >>> with open(tf, "rt") as inp:
1465 ... print(repr(inp.read()))
1466 ' \n'
1468 >>> osremove(tf)
1470 >>> with StringIO() as sio:
1471 ... write_lines(["\n"], sio)
1472 ... print(repr(sio.getvalue()))
1473 '\n'
1475 >>> with StringIO() as sio:
1476 ... write_lines([""], sio)
1477 ... print(repr(sio.getvalue()))
1478 '\n'
1480 >>> sio = StringIO()
1481 >>> try:
1482 ... write_lines(None, sio)
1483 ... except TypeError as te:
1484 ... print(te)
1485 lines should be an instance of typing.Iterable but is None.
1487 >>> sio = StringIO()
1488 >>> try:
1489 ... write_lines(123, sio)
1490 ... except TypeError as te:
1491 ... print(te)
1492 lines should be an instance of typing.Iterable but is int, namely 123.
1494 >>> sio = StringIO()
1495 >>> try:
1496 ... write_lines([1, "sdf"], sio)
1497 ... except TypeError as te:
1498 ... print(te)
1499 descriptor 'endswith' for 'str' objects doesn't apply to a 'int' object
1501 >>> sio = StringIO()
1502 >>> try:
1503 ... write_lines(["sdf", 1], sio)
1504 ... except TypeError as te:
1505 ... print(te)
1506 descriptor 'endswith' for 'str' objects doesn't apply to a 'int' object
1507 >>> print(repr(sio.getvalue()))
1508 'sdf\n'
1510 >>> try:
1511 ... write_lines("x", None)
1512 ... except TypeError as te:
1513 ... print(te)
1514 output should be an instance of io.TextIOBase but is None.
1516 >>> try:
1517 ... write_lines("x", 1)
1518 ... except TypeError as te:
1519 ... print(te)
1520 output should be an instance of io.TextIOBase but is int, namely 1.
1522 >>> try:
1523 ... write_lines(2, 1)
1524 ... except TypeError as te:
1525 ... print(te)
1526 lines should be an instance of typing.Iterable but is int, namely 2.
1527 """
1528 if not isinstance(lines, Iterable):
1529 raise type_error(lines, "lines", Iterable)
1530 if not isinstance(output, TextIOBase):
1531 raise type_error(output, "output", TextIOBase)
1532 output.writelines(__line_iterator(lines))
1535def delete_path(path: str) -> None:
1536 """
1537 Delete a path, completely, and recursively.
1539 This is intentionally inserted as an additional function and not a member
1540 of the :class:`Path` in order make the deletion more explicit and to avoid
1541 any form of accidental deleting. This function will not raise an error if
1542 the file deletion fails.
1544 :param path: The path to be deleted
1545 :raises ValueError: if `path` does not refer to an existing file or
1546 directory
1547 :raises TypeError: if `path` is not a string
1549 >>> from tempfile import mkstemp, mkdtemp
1550 >>> from os import close as osxclose
1552 >>> (h, tf) = mkstemp()
1553 >>> isfile(tf)
1554 True
1555 >>> delete_path(tf)
1556 >>> isfile(tf)
1557 False
1559 >>> try:
1560 ... delete_path(tf)
1561 ... except ValueError as ve:
1562 ... print(str(ve).endswith("is neither file nor directory."))
1563 True
1565 >>> td = mkdtemp()
1566 >>> isdir(td)
1567 True
1568 >>> delete_path(td)
1569 >>> isdir(td)
1570 False
1572 >>> try:
1573 ... delete_path(tf)
1574 ... except ValueError as ve:
1575 ... print(str(ve).endswith("is neither file nor directory."))
1576 True
1577 """
1578 p: Final[Path] = Path(path)
1579 if isfile(p):
1580 osremove(p)
1581 elif isdir(p):
1582 rmtree(p, ignore_errors=True)
1583 else:
1584 raise ValueError(f"{path!r} is neither file nor directory.")