pathier

 1import argparse
 2
 3import griddle
 4import noiftimer
 5import printbuddies
 6import younotyou
 7
 8from .pathier import Pathier, Pathish, Pathy
 9
10__all__ = ["Pathier", "Pathy", "Pathish"]
11
12
13@noiftimer.time_it()
14def sizeup():
15    """Print the sub-directories and their sizes of the current working directory."""
16    parser = argparse.ArgumentParser("sizeup")
17    parser.add_argument(
18        "-i",
19        "--ignore",
20        nargs="*",
21        default=[],
22        type=str,
23        help="Directory patterns to ignore.",
24    )
25    args = parser.parse_args()
26    matcher = younotyou.Matcher(exclude_patterns=args.ignore)
27    sizes: dict[str, int] = {}
28    folders = [
29        folder
30        for folder in Pathier.cwd().iterdir()
31        if folder.is_dir() and str(folder) in matcher
32    ]
33    print(f"Sizing up {len(folders)} directories...")
34    for folder in printbuddies.track(folders, "Scanning directories"):
35        try:
36            sizes[folder.name] = folder.size
37        except Exception as e:
38            pass
39    total_size = sum(sizes[folder] for folder in sizes)
40    size_list = [
41        (folder, Pathier.format_bytes(sizes[folder]))
42        for folder in sorted(list(sizes.keys()), key=lambda f: sizes[f], reverse=True)
43    ]
44    print(griddle.griddy(size_list, ["Dir", "Size"]))
45    print(f"Total size of '{Pathier.cwd()}': {Pathier.format_bytes(total_size)}")
46
47
48__version__ = "1.5.3"
class Pathier(pathlib.Path):
 17class Pathier(pathlib.Path):
 18    """Subclasses the standard library pathlib.Path class."""
 19
 20    def __new__(
 21        cls,
 22        *args: Self | str | pathlib.Path,
 23        **kwargs: Any,
 24    ) -> Self:
 25        if cls is Pathier:
 26            cls = WindowsPath if os.name == "nt" else PosixPath
 27        self = cls._from_parts(args)  # type: ignore
 28        if not self._flavour.is_supported:  # type: ignore
 29            raise NotImplementedError(
 30                "cannot instantiate %r on your system" % (cls.__name__,)
 31            )
 32        if "convert_backslashes" in kwargs:
 33            self.convert_backslashes = kwargs["convert_backslashes"]
 34        else:
 35            self.convert_backslashes = True
 36        return self  # type: ignore
 37
 38    @property
 39    def convert_backslashes(self) -> bool:
 40        """If True, when `self.__str__()`/`str(self)` is called, string representations will have double backslashes converted to a forward slash.
 41
 42        Only affects Windows paths."""
 43        try:
 44            return self._convert_backslashes
 45        except Exception as e:
 46            return True
 47
 48    @convert_backslashes.setter
 49    def convert_backslashes(self, should_convert: bool):
 50        self._convert_backslashes = should_convert
 51
 52    def __str__(self) -> str:
 53        path = super().__new__(pathlib.Path, self).__str__()  # type: ignore
 54        if self.convert_backslashes:
 55            path = path.replace("\\", "/")
 56        return path
 57
 58    # ===============================================stats===============================================
 59    @property
 60    def dob(self) -> datetime.datetime | None:
 61        """Returns the creation date of this file or directory as a `dateime.datetime` object."""
 62        return (
 63            datetime.datetime.fromtimestamp(self.stat().st_ctime)
 64            if self.exists()
 65            else None
 66        )
 67
 68    @property
 69    def age(self) -> float | None:
 70        """Returns the age in seconds of this file or directory."""
 71        return (
 72            (datetime.datetime.now() - self.dob).total_seconds() if self.dob else None
 73        )
 74
 75    @property
 76    def mod_date(self) -> datetime.datetime | None:
 77        """Returns the modification date of this file or directory as a `datetime.datetime` object."""
 78        return (
 79            datetime.datetime.fromtimestamp(self.stat().st_mtime)
 80            if self.exists()
 81            else None
 82        )
 83
 84    @property
 85    def mod_delta(self) -> float | None:
 86        """Returns how long ago in seconds this file or directory was modified."""
 87        return (
 88            (datetime.datetime.now() - self.mod_date).total_seconds()
 89            if self.mod_date
 90            else None
 91        )
 92
 93    @property
 94    def last_read_time(self) -> datetime.datetime | None:
 95        """Returns the last time this object made a call to `self.read_text()`, `self.read_bytes()`, or `self.open(mode="r"|"rb")`.
 96        Returns `None` if the file hasn't been read from.
 97
 98        Note: This property is only relative to the lifetime of this `Pathier` instance, not the file itself.
 99        i.e. This property will reset if you create a new `Pathier` object pointing to the same file.
100        """
101        return (
102            datetime.datetime.fromtimestamp(self._last_read_time)
103            if self._last_read_time
104            else None
105        )
106
107    @property
108    def modified_since_last_read(self) -> bool:
109        """Returns `True` if this file hasn't been read from or has been modified since the last time this object
110        made a call to `self.read_text()`, `self.read_bytes()`, or `self.open(mode="r"|"rb")`.
111
112        Note: This property is only relative to the lifetime of this `Pathier` instance, not the file itself.
113        i.e. This property will reset if you create a new `Pathier` object pointing to the same file.
114
115        #### Caveat:
116        May not be accurate if the file was modified within a couple of seconds of checking this property.
117        (For instance, on my machine `self.mod_date` is consistently 1-1.5s in the future from when `self.write_text()` was called according to `time.time()`.)
118        """
119        return (
120            False
121            if not self.mod_date
122            or not self.last_read_time
123            or self.mod_date < self.last_read_time
124            else True
125        )
126
127    @property
128    def size(self) -> int:
129        """Returns the size in bytes of this file or directory.
130
131        If this path doesn't exist, `0` will be returned."""
132        if not self.exists():
133            return 0
134        elif self.is_file():
135            return self.stat().st_size
136        elif self.is_dir():
137            return sum(file.stat().st_size for file in self.rglob("*.*"))
138        return 0
139
140    @property
141    def formatted_size(self) -> str:
142        """The size of this file or directory formatted with `self.format_bytes()`."""
143        return self.format_bytes(self.size)
144
145    @staticmethod
146    def format_bytes(size: int) -> str:
147        """Format `size` with common file size abbreviations and rounded to two decimal places.
148        >>> 1234 -> "1.23 kb" """
149        unit = "bytes"
150        for unit in ["bytes", "kb", "mb", "gb", "tb", "pb"]:
151            if unit != "bytes":
152                size *= 0.001  # type: ignore
153            if size < 1000 or unit == "pb":
154                break
155        return f"{round(size, 2)} {unit}"
156
157    def is_larger(self, path: Self) -> bool:
158        """Returns whether this file or folder is larger than the one pointed to by `path`."""
159        return self.size > path.size
160
161    def is_older(self, path: Self) -> bool | None:
162        """Returns whether this file or folder is older than the one pointed to by `path`.
163
164        Returns `None` if one or both paths don't exist."""
165        return self.dob < path.dob if self.dob and path.dob else None
166
167    def modified_more_recently(self, path: Self) -> bool | None:
168        """Returns whether this file or folder was modified more recently than the one pointed to by `path`.
169
170        Returns `None` if one or both paths don't exist."""
171        return (
172            self.mod_date > path.mod_date if self.mod_date and path.mod_date else None
173        )
174
175    # ===============================================navigation===============================================
176    def mkcwd(self):
177        """Make this path your current working directory."""
178        os.chdir(self)
179
180    @property
181    def in_PATH(self) -> bool:
182        """Return `True` if this path is in `sys.path`."""
183        return str(self) in sys.path
184
185    def add_to_PATH(self, index: int = 0):
186        """Insert this path into `sys.path` if it isn't already there.
187
188        #### :params:
189
190        `index`: The index of `sys.path` to insert this path at."""
191        path = str(self)
192        if not self.in_PATH:
193            sys.path.insert(index, path)
194
195    def append_to_PATH(self):
196        """Append this path to `sys.path` if it isn't already there."""
197        path = str(self)
198        if not self.in_PATH:
199            sys.path.append(path)
200
201    def remove_from_PATH(self):
202        """Remove this path from `sys.path` if it's in `sys.path`."""
203        if self.in_PATH:
204            sys.path.remove(str(self))
205
206    def moveup(self, name: str) -> Self:
207        """Return a new `Pathier` object that is a parent of this instance.
208
209        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
210        >>> p = Pathier("C:/some/directory/in/your/system")
211        >>> print(p.moveup("directory"))
212        >>> "C:/some/directory"
213        >>> print(p.moveup("yeet"))
214        >>> "Exception: yeet is not a parent of C:/some/directory/in/your/system" """
215        if name not in self.parts:
216            raise Exception(f"{name} is not a parent of {self}")
217        return self.__class__(*(self.parts[: self.parts.index(name) + 1]))
218
219    def __sub__(self, levels: int) -> Self:
220        """Return a new `Pathier` object moved up `levels` number of parents from the current path.
221        >>> p = Pathier("C:/some/directory/in/your/system")
222        >>> new_p = p - 3
223        >>> print(new_p)
224        >>> "C:/some/directory" """
225        path = self
226        for _ in range(levels):
227            path = path.parent
228        return path
229
230    def move_under(self, name: str) -> Self:
231        """Return a new `Pathier` object such that the stem is one level below the given folder `name`.
232
233        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
234        >>> p = Pathier("a/b/c/d/e/f/g")
235        >>> print(p.move_under("c"))
236        >>> 'a/b/c/d'"""
237        if name not in self.parts:
238            raise Exception(f"{name} is not a parent of {self}")
239        return self - (len(self.parts) - self.parts.index(name) - 2)
240
241    def separate(self, name: str, keep_name: bool = False) -> Self:
242        """Return a new `Pathier` object that is the relative child path after `name`.
243
244        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
245
246        #### :params:
247
248        `keep_name`: If `True`, the returned path will start with `name`.
249        >>> p = Pathier("a/b/c/d/e/f/g")
250        >>> print(p.separate("c"))
251        >>> 'd/e/f/g'
252        >>> print(p.separate("c", True))
253        >>> 'c/d/e/f/g'"""
254        if name not in self.parts:
255            raise Exception(f"{name} is not a parent of {self}")
256        if keep_name:
257            return self.__class__(*self.parts[self.parts.index(name) :])
258        return self.__class__(*self.parts[self.parts.index(name) + 1 :])
259
260    # ============================================write and read============================================
261    def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
262        """Create this directory.
263
264        Same as `Path().mkdir()` except `parents` and `exist_ok` default to `True` instead of `False`.
265        """
266        super().mkdir(mode, parents, exist_ok)
267
268    def touch(self, mode: int = 438, exist_ok: bool = True):
269        """Create file (and parents if necessary)."""
270        self.parent.mkdir()
271        super().touch(mode, exist_ok)
272
273    def open(  # type: ignore
274        self,
275        mode: str = "r",
276        buffering: int = -1,
277        encoding: str | None = None,
278        errors: str | None = None,
279        newline: str | None = None,
280    ) -> IO[Any]:
281        """
282        Open the file pointed by this path and return a file object, as
283        the built-in open() function does.
284        """
285        stream = super().open(mode, buffering, encoding, errors, newline)
286        if "r" in mode:
287            self._last_read_time = time.time()
288        return stream
289
290    def write_text(
291        self,
292        data: Any,
293        encoding: Any | None = None,
294        errors: Any | None = None,
295        newline: Any | None = None,
296        parents: bool = True,
297    ) -> int:
298        """Write data to file.
299
300        If a `TypeError` is raised, the function  will attempt to cast `data` to a `str` and try the write again.
301
302        If a `FileNotFoundError` is raised and `parents = True`, `self.parent` will be created.
303        """
304        write = functools.partial(
305            super().write_text,
306            encoding=encoding,
307            errors=errors,
308            newline=newline,
309        )
310        try:
311            return write(data)
312        except TypeError:
313            data = str(data)
314            return write(data)
315        except FileNotFoundError:
316            if parents:
317                self.parent.mkdir(parents=True)
318                return write(data)
319            else:
320                raise
321        except Exception as e:
322            raise
323
324    def write_bytes(self, data: Buffer, parents: bool = True) -> int:
325        """Write bytes to file.
326
327        #### :params:
328
329        `parents`: If `True` and the write operation fails with a `FileNotFoundError`,
330        make the parent directory and retry the write."""
331        try:
332            return super().write_bytes(data)
333        except FileNotFoundError:
334            if parents:
335                self.parent.mkdir(parents=True)
336                return super().write_bytes(data)
337            else:
338                raise
339        except Exception as e:
340            raise
341
342    def append(self, data: str, new_line: bool = True, encoding: Any | None = None):
343        """Append `data` to the file pointed to by this `Pathier` object.
344
345        #### :params:
346
347        `new_line`: If `True`, add `\\n` to `data`.
348
349        `encoding`: The file encoding to use."""
350        if new_line:
351            data += "\n"
352        with self.open("a", encoding=encoding) as file:
353            file.write(data)
354
355    def replace_strings(
356        self,
357        substitutions: Sequence[tuple[str, str]],
358        count: int = -1,
359        encoding: Any | None = None,
360    ):
361        """For each pair in `substitutions`, replace the first string with the second string.
362
363        #### :params:
364
365        `count`: Only replace this many occurences of each pair.
366        By default (`-1`), all occurences are replaced.
367
368        `encoding`: The file encoding to use.
369
370        e.g.
371        >>> path = Pathier("somefile.txt")
372        >>>
373        >>> path.replace([("hello", "yeet"), ("goodbye", "yeehaw")])
374        equivalent to
375        >>> path.write_text(path.read_text().replace("hello", "yeet").replace("goodbye", "yeehaw"))
376        """
377        text = self.read_text(encoding)
378        for sub in substitutions:
379            text = text.replace(sub[0], sub[1], count)
380        self.write_text(text, encoding=encoding)
381
382    def join(self, data: Sequence[str], encoding: Any | None = None, sep: str = "\n"):
383        """Write a list of strings, joined by `sep`, to the file pointed at by this instance.
384
385        Equivalent to `Pathier("somefile.txt").write_text(sep.join(data), encoding=encoding)`
386
387        #### :params:
388
389        `encoding`: The file encoding to use.
390
391        `sep`: The separator to use when joining `data`."""
392        self.write_text(sep.join(data), encoding=encoding)
393
394    def split(self, encoding: Any | None = None, keepends: bool = False) -> list[str]:
395        """Returns the content of the pointed at file as a list of strings, splitting at new line characters.
396
397        Equivalent to `Pathier("somefile.txt").read_text(encoding=encoding).splitlines()`
398
399        #### :params:
400
401        `encoding`: The file encoding to use.
402
403        `keepend`: If `True`, line breaks will be included in returned strings."""
404        return self.read_text(encoding=encoding).splitlines(keepends)
405
406    def json_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
407        """Load json file."""
408        return json.loads(self.read_text(encoding, errors))
409
410    def json_dumps(
411        self,
412        data: Any,
413        encoding: Any | None = None,
414        errors: Any | None = None,
415        newline: Any | None = None,
416        sort_keys: bool = False,
417        indent: Any | None = 2,
418        default: Any | None = str,
419        parents: bool = True,
420    ) -> Any:
421        """Dump `data` to json file."""
422        self.write_text(
423            json.dumps(data, indent=indent, default=default, sort_keys=sort_keys),
424            encoding,
425            errors,
426            newline,
427            parents,
428        )
429
430    def pickle_loads(self) -> Any:
431        """Load pickle file."""
432        return pickle.loads(self.read_bytes())
433
434    def pickle_dumps(self, data: Any):
435        """Dump `data` to pickle file."""
436        self.write_bytes(pickle.dumps(data))
437
438    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
439        """Load toml file."""
440        return tomlkit.loads(self.read_text(encoding, errors)).unwrap()
441
442    def toml_dumps(
443        self,
444        data: Any,
445        toml_encoders: Sequence[Callable[[Any], Any]] = [str],
446        encoding: Any | None = None,
447        errors: Any | None = None,
448        newline: Any | None = None,
449        sort_keys: bool = False,
450        parents: bool = True,
451    ):
452        """Dump `data` to toml file.
453
454        `toml_encoders` can be a list of functions to call when a value in `data` doesn't map to `tomlkit`'s built in types.
455        By default, anything that `tomlkit` can't convert will be cast to a string. Encoder order matters.
456        e.g. By default any `Pathier` object in `data` will be converted to a string."""
457        encoders: list[Callable[[Any], Any]] = []
458        for toml_encoder in toml_encoders:
459            encoder: Callable[[Any], Any] = lambda x: tomlkit.item(  # type:ignore
460                toml_encoder(x)
461            )
462            encoders.append(encoder)
463            tomlkit.register_encoder(encoder)
464        try:
465            self.write_text(
466                tomlkit.dumps(data, sort_keys),  # type:ignore
467                encoding,
468                errors,
469                newline,
470                parents,
471            )
472        except Exception as e:
473            raise e
474        finally:
475            for encoder in encoders:
476                tomlkit.unregister_encoder(encoder)
477
478    def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
479        """Load a json, toml, or pickle file based off this path's suffix."""
480        match self.suffix:
481            case ".json":
482                return self.json_loads(encoding, errors)
483            case ".toml":
484                return self.toml_loads(encoding, errors)
485            case ".pickle" | ".pkl":
486                return self.pickle_loads()
487            case _:
488                raise ValueError(
489                    f"No load function exists for file type `{self.suffix}`."
490                )
491
492    def dumps(
493        self,
494        data: Any,
495        encoding: Any | None = None,
496        errors: Any | None = None,
497        newline: Any | None = None,
498        sort_keys: bool = False,
499        indent: Any | None = None,
500        default: Any | None = str,
501        toml_encoders: Sequence[Callable[[Any], Any]] = [str],
502        parents: bool = True,
503    ):
504        """Dump `data` to a json or toml file based off this instance's suffix.
505
506        For toml files:
507        `toml_encoders` can be a list of functions to call when a value in `data` doesn't map to `tomlkit`'s built in types.
508        By default, anything that `tomlkit` can't convert will be cast to a string. Encoder order matters.
509        e.g. By default any `Pathier` object in `data` will be converted to a string."""
510        match self.suffix:
511            case ".json":
512                self.json_dumps(
513                    data, encoding, errors, newline, sort_keys, indent, default, parents
514                )
515            case ".toml":
516                self.toml_dumps(
517                    data, toml_encoders, encoding, errors, newline, sort_keys, parents
518                )
519            case ".pickle" | ".pkl":
520                self.pickle_dumps(data)
521            case _:
522                raise ValueError(
523                    f"No dump function exists for file type `{self.suffix}`."
524                )
525
526    def delete(self, missing_ok: bool = True):
527        """Delete the file or folder pointed to by this instance.
528
529        Uses `self.unlink()` if a file and uses `shutil.rmtree()` if a directory."""
530        if self.is_file():
531            self.unlink(missing_ok)
532        elif self.is_dir():
533            shutil.rmtree(self)
534
535    def copy(
536        self, new_path: Self | pathlib.Path | str, overwrite: bool = False
537    ) -> Self:
538        """Copy the path pointed to by this instance
539        to the instance pointed to by `new_path` using `shutil.copyfile`
540        or `shutil.copytree`.
541
542        Returns the new path.
543
544        #### :params:
545
546        `new_path`: The copy destination.
547
548        `overwrite`: If `True`, files already existing in `new_path` will be overwritten.
549        If `False`, only files that don't exist in `new_path` will be copied."""
550        dst = self.__class__(new_path)
551        if self.is_dir():
552            if overwrite or not dst.exists():
553                dst.mkdir()
554                shutil.copytree(self, dst, dirs_exist_ok=True)
555            else:
556                files = self.rglob("*.*")
557                for file in files:
558                    dst = dst.with_name(file.name)
559                    if not dst.exists():
560                        shutil.copyfile(file, dst)
561        elif self.is_file():
562            if overwrite or not dst.exists():
563                shutil.copyfile(self, dst)
564        return dst
565
566    def backup(self, timestamp: bool = False) -> Self | None:
567        """Create a copy of this file or directory with `_backup` appended to the path stem.
568        If the path to be backed up doesn't exist, `None` is returned.
569        Otherwise a `Pathier` object for the backup is returned.
570
571        #### :params:
572
573        `timestamp`: Add a timestamp to the backup name to prevent overriding previous backups.
574
575        >>> path = Pathier("some_file.txt")
576        >>> path.backup()
577        >>> list(path.iterdir())
578        >>> ['some_file.txt', 'some_file_backup.txt']
579        >>> path.backup(True)
580        >>> list(path.iterdir())
581        >>> ['some_file.txt', 'some_file_backup.txt', 'some_file_backup_04-28-2023-06_25_52_PM.txt']
582        """
583        if not self.exists():
584            return None
585        backup_stem = f"{self.stem}_backup"
586        if timestamp:
587            backup_stem = f"{backup_stem}_{datetime.datetime.now().strftime('%m-%d-%Y-%I_%M_%S_%p')}"
588        backup_path = self.with_stem(backup_stem)
589        self.copy(backup_path, True)
590        return backup_path
591
592    def execute(self, command: str = "", args: str = "") -> int:
593        """Make a call to `os.system` using the path pointed to by this Pathier object.
594
595        #### :params:
596
597        `command`: Program/command to precede the path with.
598
599        `args`: Any arguments that should come after the path.
600
601        :returns: The integer output of `os.system`.
602
603        e.g.
604        >>> path = Pathier("mydirectory") / "myscript.py"
605        then
606        >>> path.execute("py", "--iterations 10")
607        equivalent to
608        >>> os.system(f"py {path} --iterations 10")"""
609        return os.system(f"{command} {self} {args}")

Subclasses the standard library pathlib.Path class.

Pathier()
convert_backslashes: bool

If True, when self.__str__()/str(self) is called, string representations will have double backslashes converted to a forward slash.

Only affects Windows paths.

dob: datetime.datetime | None

Returns the creation date of this file or directory as a dateime.datetime object.

age: float | None

Returns the age in seconds of this file or directory.

mod_date: datetime.datetime | None

Returns the modification date of this file or directory as a datetime.datetime object.

mod_delta: float | None

Returns how long ago in seconds this file or directory was modified.

last_read_time: datetime.datetime | None

Returns the last time this object made a call to self.read_text(), self.read_bytes(), or self.open(mode="r"|"rb"). Returns None if the file hasn't been read from.

Note: This property is only relative to the lifetime of this Pathier instance, not the file itself. i.e. This property will reset if you create a new Pathier object pointing to the same file.

modified_since_last_read: bool

Returns True if this file hasn't been read from or has been modified since the last time this object made a call to self.read_text(), self.read_bytes(), or self.open(mode="r"|"rb").

Note: This property is only relative to the lifetime of this Pathier instance, not the file itself. i.e. This property will reset if you create a new Pathier object pointing to the same file.

Caveat:

May not be accurate if the file was modified within a couple of seconds of checking this property. (For instance, on my machine self.mod_date is consistently 1-1.5s in the future from when self.write_text() was called according to time.time().)

size: int

Returns the size in bytes of this file or directory.

If this path doesn't exist, 0 will be returned.

formatted_size: str

The size of this file or directory formatted with self.format_bytes().

@staticmethod
def format_bytes(size: int) -> str:
145    @staticmethod
146    def format_bytes(size: int) -> str:
147        """Format `size` with common file size abbreviations and rounded to two decimal places.
148        >>> 1234 -> "1.23 kb" """
149        unit = "bytes"
150        for unit in ["bytes", "kb", "mb", "gb", "tb", "pb"]:
151            if unit != "bytes":
152                size *= 0.001  # type: ignore
153            if size < 1000 or unit == "pb":
154                break
155        return f"{round(size, 2)} {unit}"

Format size with common file size abbreviations and rounded to two decimal places.

>>> 1234 -> "1.23 kb"
def is_larger(self, path: Self) -> bool:
157    def is_larger(self, path: Self) -> bool:
158        """Returns whether this file or folder is larger than the one pointed to by `path`."""
159        return self.size > path.size

Returns whether this file or folder is larger than the one pointed to by path.

def is_older(self, path: Self) -> bool | None:
161    def is_older(self, path: Self) -> bool | None:
162        """Returns whether this file or folder is older than the one pointed to by `path`.
163
164        Returns `None` if one or both paths don't exist."""
165        return self.dob < path.dob if self.dob and path.dob else None

Returns whether this file or folder is older than the one pointed to by path.

Returns None if one or both paths don't exist.

def modified_more_recently(self, path: Self) -> bool | None:
167    def modified_more_recently(self, path: Self) -> bool | None:
168        """Returns whether this file or folder was modified more recently than the one pointed to by `path`.
169
170        Returns `None` if one or both paths don't exist."""
171        return (
172            self.mod_date > path.mod_date if self.mod_date and path.mod_date else None
173        )

Returns whether this file or folder was modified more recently than the one pointed to by path.

Returns None if one or both paths don't exist.

def mkcwd(self):
176    def mkcwd(self):
177        """Make this path your current working directory."""
178        os.chdir(self)

Make this path your current working directory.

in_PATH: bool

Return True if this path is in sys.path.

def add_to_PATH(self, index: int = 0):
185    def add_to_PATH(self, index: int = 0):
186        """Insert this path into `sys.path` if it isn't already there.
187
188        #### :params:
189
190        `index`: The index of `sys.path` to insert this path at."""
191        path = str(self)
192        if not self.in_PATH:
193            sys.path.insert(index, path)

Insert this path into sys.path if it isn't already there.

:params:

index: The index of sys.path to insert this path at.

def append_to_PATH(self):
195    def append_to_PATH(self):
196        """Append this path to `sys.path` if it isn't already there."""
197        path = str(self)
198        if not self.in_PATH:
199            sys.path.append(path)

Append this path to sys.path if it isn't already there.

def remove_from_PATH(self):
201    def remove_from_PATH(self):
202        """Remove this path from `sys.path` if it's in `sys.path`."""
203        if self.in_PATH:
204            sys.path.remove(str(self))

Remove this path from sys.path if it's in sys.path.

def moveup(self, name: str) -> Self:
206    def moveup(self, name: str) -> Self:
207        """Return a new `Pathier` object that is a parent of this instance.
208
209        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
210        >>> p = Pathier("C:/some/directory/in/your/system")
211        >>> print(p.moveup("directory"))
212        >>> "C:/some/directory"
213        >>> print(p.moveup("yeet"))
214        >>> "Exception: yeet is not a parent of C:/some/directory/in/your/system" """
215        if name not in self.parts:
216            raise Exception(f"{name} is not a parent of {self}")
217        return self.__class__(*(self.parts[: self.parts.index(name) + 1]))

Return a new Pathier object that is a parent of this instance.

name is case-sensitive and raises an exception if it isn't in self.parts.

>>> p = Pathier("C:/some/directory/in/your/system")
>>> print(p.moveup("directory"))
>>> "C:/some/directory"
>>> print(p.moveup("yeet"))
>>> "Exception: yeet is not a parent of C:/some/directory/in/your/system"
def move_under(self, name: str) -> Self:
230    def move_under(self, name: str) -> Self:
231        """Return a new `Pathier` object such that the stem is one level below the given folder `name`.
232
233        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
234        >>> p = Pathier("a/b/c/d/e/f/g")
235        >>> print(p.move_under("c"))
236        >>> 'a/b/c/d'"""
237        if name not in self.parts:
238            raise Exception(f"{name} is not a parent of {self}")
239        return self - (len(self.parts) - self.parts.index(name) - 2)

Return a new Pathier object such that the stem is one level below the given folder name.

name is case-sensitive and raises an exception if it isn't in self.parts.

>>> p = Pathier("a/b/c/d/e/f/g")
>>> print(p.move_under("c"))
>>> 'a/b/c/d'
def separate(self, name: str, keep_name: bool = False) -> Self:
241    def separate(self, name: str, keep_name: bool = False) -> Self:
242        """Return a new `Pathier` object that is the relative child path after `name`.
243
244        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
245
246        #### :params:
247
248        `keep_name`: If `True`, the returned path will start with `name`.
249        >>> p = Pathier("a/b/c/d/e/f/g")
250        >>> print(p.separate("c"))
251        >>> 'd/e/f/g'
252        >>> print(p.separate("c", True))
253        >>> 'c/d/e/f/g'"""
254        if name not in self.parts:
255            raise Exception(f"{name} is not a parent of {self}")
256        if keep_name:
257            return self.__class__(*self.parts[self.parts.index(name) :])
258        return self.__class__(*self.parts[self.parts.index(name) + 1 :])

Return a new Pathier object that is the relative child path after name.

name is case-sensitive and raises an exception if it isn't in self.parts.

:params:

keep_name: If True, the returned path will start with name.

>>> p = Pathier("a/b/c/d/e/f/g")
>>> print(p.separate("c"))
>>> 'd/e/f/g'
>>> print(p.separate("c", True))
>>> 'c/d/e/f/g'
def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
261    def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
262        """Create this directory.
263
264        Same as `Path().mkdir()` except `parents` and `exist_ok` default to `True` instead of `False`.
265        """
266        super().mkdir(mode, parents, exist_ok)

Create this directory.

Same as Path().mkdir() except parents and exist_ok default to True instead of False.

def touch(self, mode: int = 438, exist_ok: bool = True):
268    def touch(self, mode: int = 438, exist_ok: bool = True):
269        """Create file (and parents if necessary)."""
270        self.parent.mkdir()
271        super().touch(mode, exist_ok)

Create file (and parents if necessary).

def open( self, mode: str = 'r', buffering: int = -1, encoding: str | None = None, errors: str | None = None, newline: str | None = None) -> IO[Any]:
273    def open(  # type: ignore
274        self,
275        mode: str = "r",
276        buffering: int = -1,
277        encoding: str | None = None,
278        errors: str | None = None,
279        newline: str | None = None,
280    ) -> IO[Any]:
281        """
282        Open the file pointed by this path and return a file object, as
283        the built-in open() function does.
284        """
285        stream = super().open(mode, buffering, encoding, errors, newline)
286        if "r" in mode:
287            self._last_read_time = time.time()
288        return stream

Open the file pointed by this path and return a file object, as the built-in open() function does.

def write_text( self, data: Any, encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, parents: bool = True) -> int:
290    def write_text(
291        self,
292        data: Any,
293        encoding: Any | None = None,
294        errors: Any | None = None,
295        newline: Any | None = None,
296        parents: bool = True,
297    ) -> int:
298        """Write data to file.
299
300        If a `TypeError` is raised, the function  will attempt to cast `data` to a `str` and try the write again.
301
302        If a `FileNotFoundError` is raised and `parents = True`, `self.parent` will be created.
303        """
304        write = functools.partial(
305            super().write_text,
306            encoding=encoding,
307            errors=errors,
308            newline=newline,
309        )
310        try:
311            return write(data)
312        except TypeError:
313            data = str(data)
314            return write(data)
315        except FileNotFoundError:
316            if parents:
317                self.parent.mkdir(parents=True)
318                return write(data)
319            else:
320                raise
321        except Exception as e:
322            raise

Write data to file.

If a TypeError is raised, the function will attempt to cast data to a str and try the write again.

If a FileNotFoundError is raised and parents = True, self.parent will be created.

def write_bytes(self, data: typing_extensions.Buffer, parents: bool = True) -> int:
324    def write_bytes(self, data: Buffer, parents: bool = True) -> int:
325        """Write bytes to file.
326
327        #### :params:
328
329        `parents`: If `True` and the write operation fails with a `FileNotFoundError`,
330        make the parent directory and retry the write."""
331        try:
332            return super().write_bytes(data)
333        except FileNotFoundError:
334            if parents:
335                self.parent.mkdir(parents=True)
336                return super().write_bytes(data)
337            else:
338                raise
339        except Exception as e:
340            raise

Write bytes to file.

:params:

parents: If True and the write operation fails with a FileNotFoundError, make the parent directory and retry the write.

def append( self, data: str, new_line: bool = True, encoding: typing.Any | None = None):
342    def append(self, data: str, new_line: bool = True, encoding: Any | None = None):
343        """Append `data` to the file pointed to by this `Pathier` object.
344
345        #### :params:
346
347        `new_line`: If `True`, add `\\n` to `data`.
348
349        `encoding`: The file encoding to use."""
350        if new_line:
351            data += "\n"
352        with self.open("a", encoding=encoding) as file:
353            file.write(data)

Append data to the file pointed to by this Pathier object.

:params:

new_line: If True, add \n to data.

encoding: The file encoding to use.

def replace_strings( self, substitutions: Sequence[tuple[str, str]], count: int = -1, encoding: typing.Any | None = None):
355    def replace_strings(
356        self,
357        substitutions: Sequence[tuple[str, str]],
358        count: int = -1,
359        encoding: Any | None = None,
360    ):
361        """For each pair in `substitutions`, replace the first string with the second string.
362
363        #### :params:
364
365        `count`: Only replace this many occurences of each pair.
366        By default (`-1`), all occurences are replaced.
367
368        `encoding`: The file encoding to use.
369
370        e.g.
371        >>> path = Pathier("somefile.txt")
372        >>>
373        >>> path.replace([("hello", "yeet"), ("goodbye", "yeehaw")])
374        equivalent to
375        >>> path.write_text(path.read_text().replace("hello", "yeet").replace("goodbye", "yeehaw"))
376        """
377        text = self.read_text(encoding)
378        for sub in substitutions:
379            text = text.replace(sub[0], sub[1], count)
380        self.write_text(text, encoding=encoding)

For each pair in substitutions, replace the first string with the second string.

:params:

count: Only replace this many occurences of each pair. By default (-1), all occurences are replaced.

encoding: The file encoding to use.

e.g.

>>> path = Pathier("somefile.txt")
>>>
>>> path.replace([("hello", "yeet"), ("goodbye", "yeehaw")])
equivalent to
>>> path.write_text(path.read_text().replace("hello", "yeet").replace("goodbye", "yeehaw"))
def join( self, data: Sequence[str], encoding: typing.Any | None = None, sep: str = '\n'):
382    def join(self, data: Sequence[str], encoding: Any | None = None, sep: str = "\n"):
383        """Write a list of strings, joined by `sep`, to the file pointed at by this instance.
384
385        Equivalent to `Pathier("somefile.txt").write_text(sep.join(data), encoding=encoding)`
386
387        #### :params:
388
389        `encoding`: The file encoding to use.
390
391        `sep`: The separator to use when joining `data`."""
392        self.write_text(sep.join(data), encoding=encoding)

Write a list of strings, joined by sep, to the file pointed at by this instance.

Equivalent to Pathier("somefile.txt").write_text(sep.join(data), encoding=encoding)

:params:

encoding: The file encoding to use.

sep: The separator to use when joining data.

def split( self, encoding: typing.Any | None = None, keepends: bool = False) -> list[str]:
394    def split(self, encoding: Any | None = None, keepends: bool = False) -> list[str]:
395        """Returns the content of the pointed at file as a list of strings, splitting at new line characters.
396
397        Equivalent to `Pathier("somefile.txt").read_text(encoding=encoding).splitlines()`
398
399        #### :params:
400
401        `encoding`: The file encoding to use.
402
403        `keepend`: If `True`, line breaks will be included in returned strings."""
404        return self.read_text(encoding=encoding).splitlines(keepends)

Returns the content of the pointed at file as a list of strings, splitting at new line characters.

Equivalent to Pathier("somefile.txt").read_text(encoding=encoding).splitlines()

:params:

encoding: The file encoding to use.

keepend: If True, line breaks will be included in returned strings.

def json_loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
406    def json_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
407        """Load json file."""
408        return json.loads(self.read_text(encoding, errors))

Load json file.

def json_dumps( self, data: Any, encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, sort_keys: bool = False, indent: typing.Any | None = 2, default: typing.Any | None = <class 'str'>, parents: bool = True) -> Any:
410    def json_dumps(
411        self,
412        data: Any,
413        encoding: Any | None = None,
414        errors: Any | None = None,
415        newline: Any | None = None,
416        sort_keys: bool = False,
417        indent: Any | None = 2,
418        default: Any | None = str,
419        parents: bool = True,
420    ) -> Any:
421        """Dump `data` to json file."""
422        self.write_text(
423            json.dumps(data, indent=indent, default=default, sort_keys=sort_keys),
424            encoding,
425            errors,
426            newline,
427            parents,
428        )

Dump data to json file.

def pickle_loads(self) -> Any:
430    def pickle_loads(self) -> Any:
431        """Load pickle file."""
432        return pickle.loads(self.read_bytes())

Load pickle file.

def pickle_dumps(self, data: Any):
434    def pickle_dumps(self, data: Any):
435        """Dump `data` to pickle file."""
436        self.write_bytes(pickle.dumps(data))

Dump data to pickle file.

def toml_loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
438    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
439        """Load toml file."""
440        return tomlkit.loads(self.read_text(encoding, errors)).unwrap()

Load toml file.

def toml_dumps( self, data: Any, toml_encoders: Sequence[Callable[[Any], Any]] = [<class 'str'>], encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, sort_keys: bool = False, parents: bool = True):
442    def toml_dumps(
443        self,
444        data: Any,
445        toml_encoders: Sequence[Callable[[Any], Any]] = [str],
446        encoding: Any | None = None,
447        errors: Any | None = None,
448        newline: Any | None = None,
449        sort_keys: bool = False,
450        parents: bool = True,
451    ):
452        """Dump `data` to toml file.
453
454        `toml_encoders` can be a list of functions to call when a value in `data` doesn't map to `tomlkit`'s built in types.
455        By default, anything that `tomlkit` can't convert will be cast to a string. Encoder order matters.
456        e.g. By default any `Pathier` object in `data` will be converted to a string."""
457        encoders: list[Callable[[Any], Any]] = []
458        for toml_encoder in toml_encoders:
459            encoder: Callable[[Any], Any] = lambda x: tomlkit.item(  # type:ignore
460                toml_encoder(x)
461            )
462            encoders.append(encoder)
463            tomlkit.register_encoder(encoder)
464        try:
465            self.write_text(
466                tomlkit.dumps(data, sort_keys),  # type:ignore
467                encoding,
468                errors,
469                newline,
470                parents,
471            )
472        except Exception as e:
473            raise e
474        finally:
475            for encoder in encoders:
476                tomlkit.unregister_encoder(encoder)

Dump data to toml file.

toml_encoders can be a list of functions to call when a value in data doesn't map to tomlkit's built in types. By default, anything that tomlkit can't convert will be cast to a string. Encoder order matters. e.g. By default any Pathier object in data will be converted to a string.

def loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
478    def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
479        """Load a json, toml, or pickle file based off this path's suffix."""
480        match self.suffix:
481            case ".json":
482                return self.json_loads(encoding, errors)
483            case ".toml":
484                return self.toml_loads(encoding, errors)
485            case ".pickle" | ".pkl":
486                return self.pickle_loads()
487            case _:
488                raise ValueError(
489                    f"No load function exists for file type `{self.suffix}`."
490                )

Load a json, toml, or pickle file based off this path's suffix.

def dumps( self, data: Any, encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, sort_keys: bool = False, indent: typing.Any | None = None, default: typing.Any | None = <class 'str'>, toml_encoders: Sequence[Callable[[Any], Any]] = [<class 'str'>], parents: bool = True):
492    def dumps(
493        self,
494        data: Any,
495        encoding: Any | None = None,
496        errors: Any | None = None,
497        newline: Any | None = None,
498        sort_keys: bool = False,
499        indent: Any | None = None,
500        default: Any | None = str,
501        toml_encoders: Sequence[Callable[[Any], Any]] = [str],
502        parents: bool = True,
503    ):
504        """Dump `data` to a json or toml file based off this instance's suffix.
505
506        For toml files:
507        `toml_encoders` can be a list of functions to call when a value in `data` doesn't map to `tomlkit`'s built in types.
508        By default, anything that `tomlkit` can't convert will be cast to a string. Encoder order matters.
509        e.g. By default any `Pathier` object in `data` will be converted to a string."""
510        match self.suffix:
511            case ".json":
512                self.json_dumps(
513                    data, encoding, errors, newline, sort_keys, indent, default, parents
514                )
515            case ".toml":
516                self.toml_dumps(
517                    data, toml_encoders, encoding, errors, newline, sort_keys, parents
518                )
519            case ".pickle" | ".pkl":
520                self.pickle_dumps(data)
521            case _:
522                raise ValueError(
523                    f"No dump function exists for file type `{self.suffix}`."
524                )

Dump data to a json or toml file based off this instance's suffix.

For toml files: toml_encoders can be a list of functions to call when a value in data doesn't map to tomlkit's built in types. By default, anything that tomlkit can't convert will be cast to a string. Encoder order matters. e.g. By default any Pathier object in data will be converted to a string.

def delete(self, missing_ok: bool = True):
526    def delete(self, missing_ok: bool = True):
527        """Delete the file or folder pointed to by this instance.
528
529        Uses `self.unlink()` if a file and uses `shutil.rmtree()` if a directory."""
530        if self.is_file():
531            self.unlink(missing_ok)
532        elif self.is_dir():
533            shutil.rmtree(self)

Delete the file or folder pointed to by this instance.

Uses self.unlink() if a file and uses shutil.rmtree() if a directory.

def copy( self, new_path: Union[Self, pathlib.Path, str], overwrite: bool = False) -> Self:
535    def copy(
536        self, new_path: Self | pathlib.Path | str, overwrite: bool = False
537    ) -> Self:
538        """Copy the path pointed to by this instance
539        to the instance pointed to by `new_path` using `shutil.copyfile`
540        or `shutil.copytree`.
541
542        Returns the new path.
543
544        #### :params:
545
546        `new_path`: The copy destination.
547
548        `overwrite`: If `True`, files already existing in `new_path` will be overwritten.
549        If `False`, only files that don't exist in `new_path` will be copied."""
550        dst = self.__class__(new_path)
551        if self.is_dir():
552            if overwrite or not dst.exists():
553                dst.mkdir()
554                shutil.copytree(self, dst, dirs_exist_ok=True)
555            else:
556                files = self.rglob("*.*")
557                for file in files:
558                    dst = dst.with_name(file.name)
559                    if not dst.exists():
560                        shutil.copyfile(file, dst)
561        elif self.is_file():
562            if overwrite or not dst.exists():
563                shutil.copyfile(self, dst)
564        return dst

Copy the path pointed to by this instance to the instance pointed to by new_path using shutil.copyfile or shutil.copytree.

Returns the new path.

:params:

new_path: The copy destination.

overwrite: If True, files already existing in new_path will be overwritten. If False, only files that don't exist in new_path will be copied.

def backup(self, timestamp: bool = False) -> Optional[Self]:
566    def backup(self, timestamp: bool = False) -> Self | None:
567        """Create a copy of this file or directory with `_backup` appended to the path stem.
568        If the path to be backed up doesn't exist, `None` is returned.
569        Otherwise a `Pathier` object for the backup is returned.
570
571        #### :params:
572
573        `timestamp`: Add a timestamp to the backup name to prevent overriding previous backups.
574
575        >>> path = Pathier("some_file.txt")
576        >>> path.backup()
577        >>> list(path.iterdir())
578        >>> ['some_file.txt', 'some_file_backup.txt']
579        >>> path.backup(True)
580        >>> list(path.iterdir())
581        >>> ['some_file.txt', 'some_file_backup.txt', 'some_file_backup_04-28-2023-06_25_52_PM.txt']
582        """
583        if not self.exists():
584            return None
585        backup_stem = f"{self.stem}_backup"
586        if timestamp:
587            backup_stem = f"{backup_stem}_{datetime.datetime.now().strftime('%m-%d-%Y-%I_%M_%S_%p')}"
588        backup_path = self.with_stem(backup_stem)
589        self.copy(backup_path, True)
590        return backup_path

Create a copy of this file or directory with _backup appended to the path stem. If the path to be backed up doesn't exist, None is returned. Otherwise a Pathier object for the backup is returned.

:params:

timestamp: Add a timestamp to the backup name to prevent overriding previous backups.

>>> path = Pathier("some_file.txt")
>>> path.backup()
>>> list(path.iterdir())
>>> ['some_file.txt', 'some_file_backup.txt']
>>> path.backup(True)
>>> list(path.iterdir())
>>> ['some_file.txt', 'some_file_backup.txt', 'some_file_backup_04-28-2023-06_25_52_PM.txt']
def execute(self, command: str = '', args: str = '') -> int:
592    def execute(self, command: str = "", args: str = "") -> int:
593        """Make a call to `os.system` using the path pointed to by this Pathier object.
594
595        #### :params:
596
597        `command`: Program/command to precede the path with.
598
599        `args`: Any arguments that should come after the path.
600
601        :returns: The integer output of `os.system`.
602
603        e.g.
604        >>> path = Pathier("mydirectory") / "myscript.py"
605        then
606        >>> path.execute("py", "--iterations 10")
607        equivalent to
608        >>> os.system(f"py {path} --iterations 10")"""
609        return os.system(f"{command} {self} {args}")

Make a call to os.system using the path pointed to by this Pathier object.

:params:

command: Program/command to precede the path with.

args: Any arguments that should come after the path.

:returns: The integer output of os.system.

e.g.

>>> path = Pathier("mydirectory") / "myscript.py"
then
>>> path.execute("py", "--iterations 10")
equivalent to
>>> os.system(f"py {path} --iterations 10")
Inherited Members
pathlib.Path
cwd
home
samefile
iterdir
glob
rglob
absolute
resolve
stat
owner
group
read_bytes
read_text
chmod
lchmod
rmdir
lstat
rename
replace
exists
is_dir
is_file
is_mount
is_block_device
is_char_device
is_fifo
is_socket
expanduser
pathlib.PurePath
as_posix
as_uri
drive
root
anchor
name
suffix
suffixes
stem
with_name
with_stem
with_suffix
relative_to
is_relative_to
parts
joinpath
parent
parents
is_absolute
is_reserved
match
Pathy = pathier.Pathier | pathlib.Path
Pathish = pathier.Pathier | pathlib.Path | str