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

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

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

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

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

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

Create file (and parents if necessary).

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

Dump data to json file.

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

Load toml file.

def toml_dumps( self, data: Any, encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, sort_keys: bool = False, parents: bool = True):
422    def toml_dumps(
423        self,
424        data: Any,
425        encoding: Any | None = None,
426        errors: Any | None = None,
427        newline: Any | None = None,
428        sort_keys: bool = False,
429        parents: bool = True,
430    ):
431        """Dump `data` to toml file."""
432        self.write_text(
433            tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents
434        )

Dump data to toml file.

def loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
436    def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
437        """Load a json or toml file based off this instance's suffix."""
438        match self.suffix:
439            case ".json":
440                return self.json_loads(encoding, errors)
441            case ".toml":
442                return self.toml_loads(encoding, errors)

Load a json or toml file based off this instance'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 = None, parents: bool = True):
444    def dumps(
445        self,
446        data: Any,
447        encoding: Any | None = None,
448        errors: Any | None = None,
449        newline: Any | None = None,
450        sort_keys: bool = False,
451        indent: Any | None = None,
452        default: Any | None = None,
453        parents: bool = True,
454    ):
455        """Dump `data` to a json or toml file based off this instance's suffix."""
456        match self.suffix:
457            case ".json":
458                self.json_dumps(
459                    data, encoding, errors, newline, sort_keys, indent, default, parents
460                )
461            case ".toml":
462                self.toml_dumps(data, encoding, errors, newline, sort_keys, parents)

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

def delete(self, missing_ok: bool = True):
464    def delete(self, missing_ok: bool = True):
465        """Delete the file or folder pointed to by this instance.
466
467        Uses `self.unlink()` if a file and uses `shutil.rmtree()` if a directory."""
468        if self.is_file():
469            self.unlink(missing_ok)
470        elif self.is_dir():
471            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:
473    def copy(
474        self, new_path: Self | pathlib.Path | str, overwrite: bool = False
475    ) -> Self:
476        """Copy the path pointed to by this instance
477        to the instance pointed to by `new_path` using `shutil.copyfile`
478        or `shutil.copytree`.
479
480        Returns the new path.
481
482        #### :params:
483
484        `new_path`: The copy destination.
485
486        `overwrite`: If `True`, files already existing in `new_path` will be overwritten.
487        If `False`, only files that don't exist in `new_path` will be copied."""
488        dst = self.__class__(new_path)
489        if self.is_dir():
490            if overwrite or not dst.exists():
491                dst.mkdir()
492                shutil.copytree(self, dst, dirs_exist_ok=True)
493            else:
494                files = self.rglob("*.*")
495                for file in files:
496                    dst = dst.with_name(file.name)
497                    if not dst.exists():
498                        shutil.copyfile(file, dst)
499        elif self.is_file():
500            if overwrite or not dst.exists():
501                shutil.copyfile(self, dst)
502        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]:
504    def backup(self, timestamp: bool = False) -> Self | None:
505        """Create a copy of this file or directory with `_backup` appended to the path stem.
506        If the path to be backed up doesn't exist, `None` is returned.
507        Otherwise a `Pathier` object for the backup is returned.
508
509        #### :params:
510
511        `timestamp`: Add a timestamp to the backup name to prevent overriding previous backups.
512
513        >>> path = Pathier("some_file.txt")
514        >>> path.backup()
515        >>> list(path.iterdir())
516        >>> ['some_file.txt', 'some_file_backup.txt']
517        >>> path.backup(True)
518        >>> list(path.iterdir())
519        >>> ['some_file.txt', 'some_file_backup.txt', 'some_file_backup_04-28-2023-06_25_52_PM.txt']
520        """
521        if not self.exists():
522            return None
523        backup_stem = f"{self.stem}_backup"
524        if timestamp:
525            backup_stem = f"{backup_stem}_{datetime.datetime.now().strftime('%m-%d-%Y-%I_%M_%S_%p')}"
526        backup_path = self.with_stem(backup_stem)
527        self.copy(backup_path, True)
528        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:
530    def execute(self, command: str = "", args: str = "") -> int:
531        """Make a call to `os.system` using the path pointed to by this Pathier object.
532
533        #### :params:
534
535        `command`: Program/command to precede the path with.
536
537        `args`: Any arguments that should come after the path.
538
539        :returns: The integer output of `os.system`.
540
541        e.g.
542        >>> path = Pathier("mydirectory") / "myscript.py"
543        then
544        >>> path.execute("py", "--iterations 10")
545        equivalent to
546        >>> os.system(f"py {path} --iterations 10")"""
547        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