pathier

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

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):
172    def mkcwd(self):
173        """Make this path your current working directory."""
174        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):
181    def add_to_PATH(self, index: int = 0):
182        """Insert this path into `sys.path` if it isn't already there.
183
184        #### :params:
185
186        `index`: The index of `sys.path` to insert this path at."""
187        path = str(self)
188        if not self.in_PATH:
189            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):
191    def append_to_PATH(self):
192        """Append this path to `sys.path` if it isn't already there."""
193        path = str(self)
194        if not self.in_PATH:
195            sys.path.append(path)

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

def remove_from_PATH(self):
197    def remove_from_PATH(self):
198        """Remove this path from `sys.path` if it's in `sys.path`."""
199        if self.in_PATH:
200            sys.path.remove(str(self))

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

def moveup(self, name: str) -> Self:
202    def moveup(self, name: str) -> Self:
203        """Return a new `Pathier` object that is a parent of this instance.
204
205        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
206        >>> p = Pathier("C:/some/directory/in/your/system")
207        >>> print(p.moveup("directory"))
208        >>> "C:/some/directory"
209        >>> print(p.moveup("yeet"))
210        >>> "Exception: yeet is not a parent of C:/some/directory/in/your/system" """
211        if name not in self.parts:
212            raise Exception(f"{name} is not a parent of {self}")
213        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:
226    def move_under(self, name: str) -> Self:
227        """Return a new `Pathier` object such that the stem is one level below the given folder `name`.
228
229        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
230        >>> p = Pathier("a/b/c/d/e/f/g")
231        >>> print(p.move_under("c"))
232        >>> 'a/b/c/d'"""
233        if name not in self.parts:
234            raise Exception(f"{name} is not a parent of {self}")
235        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:
237    def separate(self, name: str, keep_name: bool = False) -> Self:
238        """Return a new `Pathier` object that is the relative child path after `name`.
239
240        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
241
242        #### :params:
243
244        `keep_name`: If `True`, the returned path will start with `name`.
245        >>> p = Pathier("a/b/c/d/e/f/g")
246        >>> print(p.separate("c"))
247        >>> 'd/e/f/g'
248        >>> print(p.separate("c", True))
249        >>> 'c/d/e/f/g'"""
250        if name not in self.parts:
251            raise Exception(f"{name} is not a parent of {self}")
252        if keep_name:
253            return self.__class__(*self.parts[self.parts.index(name) :])
254        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):
257    def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
258        """Create this directory.
259
260        Same as `Path().mkdir()` except `parents` and `exist_ok` default to `True` instead of `False`.
261        """
262        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):
264    def touch(self):
265        """Create file (and parents if necessary)."""
266        self.parent.mkdir()
267        super().touch()

Create file (and parents if necessary).

def open( self, mode='r', buffering=-1, encoding=None, errors=None, newline=None):
269    def open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None):
270        """
271        Open the file pointed by this path and return a file object, as
272        the built-in open() function does.
273        """
274        stream = super().open(mode, buffering, encoding, errors, newline)
275        if "r" in mode:
276            self._last_read_time = time.time()
277        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):
279    def write_text(
280        self,
281        data: Any,
282        encoding: Any | None = None,
283        errors: Any | None = None,
284        newline: Any | None = None,
285        parents: bool = True,
286    ):
287        """Write data to file.
288
289        If a `TypeError` is raised, the function  will attempt to cast `data` to a `str` and try the write again.
290
291        If a `FileNotFoundError` is raised and `parents = True`, `self.parent` will be created.
292        """
293        write = functools.partial(
294            super().write_text,
295            encoding=encoding,
296            errors=errors,
297            newline=newline,
298        )
299        try:
300            write(data)
301        except TypeError:
302            data = str(data)
303            write(data)
304        except FileNotFoundError:
305            if parents:
306                self.parent.mkdir(parents=True)
307                write(data)
308            else:
309                raise
310        except Exception as e:
311            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: bytes, parents: bool = True):
313    def write_bytes(self, data: bytes, parents: bool = True):
314        """Write bytes to file.
315
316        #### :params:
317
318        `parents`: If `True` and the write operation fails with a `FileNotFoundError`,
319        make the parent directory and retry the write."""
320        try:
321            super().write_bytes(data)
322        except FileNotFoundError:
323            if parents:
324                self.parent.mkdir(parents=True)
325                super().write_bytes(data)
326            else:
327                raise
328        except Exception as e:
329            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):
331    def append(self, data: str, new_line: bool = True, encoding: Any | None = None):
332        """Append `data` to the file pointed to by this `Pathier` object.
333
334        #### :params:
335
336        `new_line`: If `True`, add `\\n` to `data`.
337
338        `encoding`: The file encoding to use."""
339        if new_line:
340            data += "\n"
341        with self.open("a", encoding=encoding) as file:
342            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: list[tuple[str, str]], count: int = -1, encoding: typing.Any | None = None):
344    def replace_strings(
345        self,
346        substitutions: list[tuple[str, str]],
347        count: int = -1,
348        encoding: Any | None = None,
349    ):
350        """For each pair in `substitutions`, replace the first string with the second string.
351
352        #### :params:
353
354        `count`: Only replace this many occurences of each pair.
355        By default (`-1`), all occurences are replaced.
356
357        `encoding`: The file encoding to use.
358
359        e.g.
360        >>> path = Pathier("somefile.txt")
361        >>>
362        >>> path.replace([("hello", "yeet"), ("goodbye", "yeehaw")])
363        equivalent to
364        >>> path.write_text(path.read_text().replace("hello", "yeet").replace("goodbye", "yeehaw"))
365        """
366        text = self.read_text(encoding)
367        for sub in substitutions:
368            text = text.replace(sub[0], sub[1], count)
369        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: list[str], encoding: typing.Any | None = None, sep: str = '\n'):
371    def join(self, data: list[str], encoding: Any | None = None, sep: str = "\n"):
372        """Write a list of strings, joined by `sep`, to the file pointed at by this instance.
373
374        Equivalent to `Pathier("somefile.txt").write_text(sep.join(data), encoding=encoding)`
375
376        #### :params:
377
378        `encoding`: The file encoding to use.
379
380        `sep`: The separator to use when joining `data`."""
381        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]:
383    def split(self, encoding: Any | None = None, keepends: bool = False) -> list[str]:
384        """Returns the content of the pointed at file as a list of strings, splitting at new line characters.
385
386        Equivalent to `Pathier("somefile.txt").read_text(encoding=encoding).splitlines()`
387
388        #### :params:
389
390        `encoding`: The file encoding to use.
391
392        `keepend`: If `True`, line breaks will be included in returned strings."""
393        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:
395    def json_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
396        """Load json file."""
397        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:
399    def json_dumps(
400        self,
401        data: Any,
402        encoding: Any | None = None,
403        errors: Any | None = None,
404        newline: Any | None = None,
405        sort_keys: bool = False,
406        indent: Any | None = 2,
407        default: Any | None = str,
408        parents: bool = True,
409    ) -> Any:
410        """Dump `data` to json file."""
411        self.write_text(
412            json.dumps(data, indent=indent, default=default, sort_keys=sort_keys),
413            encoding,
414            errors,
415            newline,
416            parents,
417        )

Dump data to json file.

def pickle_loads(self) -> Any:
419    def pickle_loads(self) -> Any:
420        """Load pickle file."""
421        return pickle.loads(self.read_bytes())

Load pickle file.

def pickle_dumps(self, data: Any):
423    def pickle_dumps(self, data: Any):
424        """Dump `data` to pickle file."""
425        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:
427    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
428        """Load toml file."""
429        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):
431    def toml_dumps(
432        self,
433        data: Any,
434        toml_encoders: Sequence[Callable[[Any], Any]] = [str],
435        encoding: Any | None = None,
436        errors: Any | None = None,
437        newline: Any | None = None,
438        sort_keys: bool = False,
439        parents: bool = True,
440    ):
441        """Dump `data` to toml file.
442
443        `toml_encoders` can be a list of functions to call when a value in `data` doesn't map to `tomlkit`'s built in types.
444        By default, anything that `tomlkit` can't convert will be cast to a string. Encoder order matters.
445        e.g. By default any `Pathier` object in `data` will be converted to a string."""
446        encoders = []
447        for toml_encoder in toml_encoders:
448            encoder = lambda x: tomlkit.item(toml_encoder(x))
449            encoders.append(encoder)
450            tomlkit.register_encoder(encoder)
451        try:
452            self.write_text(
453                tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents
454            )
455        except Exception as e:
456            raise e
457        finally:
458            for encoder in encoders:
459                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:
461    def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
462        """Load a json, toml, or pickle file based off this path's suffix."""
463        match self.suffix:
464            case ".json":
465                return self.json_loads(encoding, errors)
466            case ".toml":
467                return self.toml_loads(encoding, errors)
468            case ".pickle" | ".pkl":
469                return self.pickle_loads()

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):
471    def dumps(
472        self,
473        data: Any,
474        encoding: Any | None = None,
475        errors: Any | None = None,
476        newline: Any | None = None,
477        sort_keys: bool = False,
478        indent: Any | None = None,
479        default: Any | None = str,
480        toml_encoders: Sequence[Callable[[Any], Any]] = [str],
481        parents: bool = True,
482    ):
483        """Dump `data` to a json or toml file based off this instance's suffix.
484
485        For toml files:
486        `toml_encoders` can be a list of functions to call when a value in `data` doesn't map to `tomlkit`'s built in types.
487        By default, anything that `tomlkit` can't convert will be cast to a string. Encoder order matters.
488        e.g. By default any `Pathier` object in `data` will be converted to a string."""
489        match self.suffix:
490            case ".json":
491                self.json_dumps(
492                    data, encoding, errors, newline, sort_keys, indent, default, parents
493                )
494            case ".toml":
495                self.toml_dumps(
496                    data, toml_encoders, encoding, errors, newline, sort_keys, parents
497                )
498            case ".pickle" | ".pkl":
499                self.pickle_dumps(data)

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):
501    def delete(self, missing_ok: bool = True):
502        """Delete the file or folder pointed to by this instance.
503
504        Uses `self.unlink()` if a file and uses `shutil.rmtree()` if a directory."""
505        if self.is_file():
506            self.unlink(missing_ok)
507        elif self.is_dir():
508            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:
510    def copy(
511        self, new_path: Self | pathlib.Path | str, overwrite: bool = False
512    ) -> Self:
513        """Copy the path pointed to by this instance
514        to the instance pointed to by `new_path` using `shutil.copyfile`
515        or `shutil.copytree`.
516
517        Returns the new path.
518
519        #### :params:
520
521        `new_path`: The copy destination.
522
523        `overwrite`: If `True`, files already existing in `new_path` will be overwritten.
524        If `False`, only files that don't exist in `new_path` will be copied."""
525        dst = self.__class__(new_path)
526        if self.is_dir():
527            if overwrite or not dst.exists():
528                dst.mkdir()
529                shutil.copytree(self, dst, dirs_exist_ok=True)
530            else:
531                files = self.rglob("*.*")
532                for file in files:
533                    dst = dst.with_name(file.name)
534                    if not dst.exists():
535                        shutil.copyfile(file, dst)
536        elif self.is_file():
537            if overwrite or not dst.exists():
538                shutil.copyfile(self, dst)
539        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]:
541    def backup(self, timestamp: bool = False) -> Self | None:
542        """Create a copy of this file or directory with `_backup` appended to the path stem.
543        If the path to be backed up doesn't exist, `None` is returned.
544        Otherwise a `Pathier` object for the backup is returned.
545
546        #### :params:
547
548        `timestamp`: Add a timestamp to the backup name to prevent overriding previous backups.
549
550        >>> path = Pathier("some_file.txt")
551        >>> path.backup()
552        >>> list(path.iterdir())
553        >>> ['some_file.txt', 'some_file_backup.txt']
554        >>> path.backup(True)
555        >>> list(path.iterdir())
556        >>> ['some_file.txt', 'some_file_backup.txt', 'some_file_backup_04-28-2023-06_25_52_PM.txt']
557        """
558        if not self.exists():
559            return None
560        backup_stem = f"{self.stem}_backup"
561        if timestamp:
562            backup_stem = f"{backup_stem}_{datetime.datetime.now().strftime('%m-%d-%Y-%I_%M_%S_%p')}"
563        backup_path = self.with_stem(backup_stem)
564        self.copy(backup_path, True)
565        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:
567    def execute(self, command: str = "", args: str = "") -> int:
568        """Make a call to `os.system` using the path pointed to by this Pathier object.
569
570        #### :params:
571
572        `command`: Program/command to precede the path with.
573
574        `args`: Any arguments that should come after the path.
575
576        :returns: The integer output of `os.system`.
577
578        e.g.
579        >>> path = Pathier("mydirectory") / "myscript.py"
580        then
581        >>> path.execute("py", "--iterations 10")
582        equivalent to
583        >>> os.system(f"py {path} --iterations 10")"""
584        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