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

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

def modified_more_recently(self, path: Self) -> bool:
150    def modified_more_recently(self, path: Self) -> bool:
151        """Returns whether this file or folder was modified more recently than the one pointed to by `path`."""
152        return self.mod_date > path.mod_date

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

def mkcwd(self):
155    def mkcwd(self):
156        """Make this path your current working directory."""
157        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):
164    def add_to_PATH(self, index: int = 0):
165        """Insert this path into `sys.path` if it isn't already there.
166
167        #### :params:
168
169        `index`: The index of `sys.path` to insert this path at."""
170        path = str(self)
171        if not self.in_PATH:
172            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):
174    def append_to_PATH(self):
175        """Append this path to `sys.path` if it isn't already there."""
176        path = str(self)
177        if not self.in_PATH:
178            sys.path.append(path)

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

def remove_from_PATH(self):
180    def remove_from_PATH(self):
181        """Remove this path from `sys.path` if it's in `sys.path`."""
182        if self.in_PATH:
183            sys.path.remove(str(self))

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

def moveup(self, name: str) -> Self:
185    def moveup(self, name: str) -> Self:
186        """Return a new `Pathier` object that is a parent of this instance.
187
188        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
189        >>> p = Pathier("C:\some\directory\in\your\system")
190        >>> print(p.moveup("directory"))
191        >>> "C:\some\directory"
192        >>> print(p.moveup("yeet"))
193        >>> "Exception: yeet is not a parent of C:\some\directory\in\your\system" """
194        if name not in self.parts:
195            raise Exception(f"{name} is not a parent of {self}")
196        return Pathier(*(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:
209    def move_under(self, name: str) -> Self:
210        """Return a new `Pathier` object such that the stem is one level below the given folder `name`.
211
212        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
213        >>> p = Pathier("a/b/c/d/e/f/g")
214        >>> print(p.move_under("c"))
215        >>> 'a/b/c/d'"""
216        if name not in self.parts:
217            raise Exception(f"{name} is not a parent of {self}")
218        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:
220    def separate(self, name: str, keep_name: bool = False) -> Self:
221        """Return a new `Pathier` object that is the relative child path after `name`.
222
223        `name` is case-sensitive and raises an exception if it isn't in `self.parts`.
224
225        #### :params:
226
227        `keep_name`: If `True`, the returned path will start with `name`.
228        >>> p = Pathier("a/b/c/d/e/f/g")
229        >>> print(p.separate("c"))
230        >>> 'd/e/f/g'
231        >>> print(p.separate("c", True))
232        >>> 'c/d/e/f/g'"""
233        if name not in self.parts:
234            raise Exception(f"{name} is not a parent of {self}")
235        if keep_name:
236            return Pathier(*self.parts[self.parts.index(name) :])
237        return Pathier(*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):
240    def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
241        """Create this directory.
242
243        Same as `Path().mkdir()` except `parents` and `exist_ok` default to `True` instead of `False`.
244        """
245        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):
247    def touch(self):
248        """Create file (and parents if necessary)."""
249        self.parent.mkdir()
250        super().touch()

Create file (and parents if necessary).

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

Dump data to json file.

def toml_loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
402    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
403        """Load toml file."""
404        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):
406    def toml_dumps(
407        self,
408        data: Any,
409        encoding: Any | None = None,
410        errors: Any | None = None,
411        newline: Any | None = None,
412        sort_keys: bool = False,
413        parents: bool = True,
414    ):
415        """Dump `data` to toml file."""
416        self.write_text(
417            tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents
418        )

Dump data to toml file.

def loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
420    def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
421        """Load a json or toml file based off this instance's suffix."""
422        match self.suffix:
423            case ".json":
424                return self.json_loads(encoding, errors)
425            case ".toml":
426                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):
428    def dumps(
429        self,
430        data: Any,
431        encoding: Any | None = None,
432        errors: Any | None = None,
433        newline: Any | None = None,
434        sort_keys: bool = False,
435        indent: Any | None = None,
436        default: Any | None = None,
437        parents: bool = True,
438    ):
439        """Dump `data` to a json or toml file based off this instance's suffix."""
440        match self.suffix:
441            case ".json":
442                self.json_dumps(
443                    data, encoding, errors, newline, sort_keys, indent, default, parents
444                )
445            case ".toml":
446                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):
448    def delete(self, missing_ok: bool = True):
449        """Delete the file or folder pointed to by this instance.
450
451        Uses `self.unlink()` if a file and uses `shutil.rmtree()` if a directory."""
452        if self.is_file():
453            self.unlink(missing_ok)
454        elif self.is_dir():
455            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:
457    def copy(
458        self, new_path: Self | pathlib.Path | str, overwrite: bool = False
459    ) -> Self:
460        """Copy the path pointed to by this instance
461        to the instance pointed to by `new_path` using `shutil.copyfile`
462        or `shutil.copytree`.
463
464        Returns the new path.
465
466        #### :params:
467
468        `new_path`: The copy destination.
469
470        `overwrite`: If `True`, files already existing in `new_path` will be overwritten.
471        If `False`, only files that don't exist in `new_path` will be copied."""
472        new_path = Pathier(new_path)
473        if self.is_dir():
474            if overwrite or not new_path.exists():
475                new_path.mkdir()
476                shutil.copytree(self, new_path, dirs_exist_ok=True)
477            else:
478                files = self.rglob("*.*")
479                for file in files:
480                    dst = new_path.with_name(file.name)
481                    if not dst.exists():
482                        shutil.copyfile(file, dst)
483        elif self.is_file():
484            if overwrite or not new_path.exists():
485                shutil.copyfile(self, new_path)
486        return new_path

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]:
488    def backup(self, timestamp: bool = False) -> Self | None:
489        """Create a copy of this file or directory with `_backup` appended to the path stem.
490        If the path to be backed up doesn't exist, `None` is returned.
491        Otherwise a `Pathier` object for the backup is returned.
492
493        #### :params:
494
495        `timestamp`: Add a timestamp to the backup name to prevent overriding previous backups.
496
497        >>> path = Pathier("some_file.txt")
498        >>> path.backup()
499        >>> list(path.iterdir())
500        >>> ['some_file.txt', 'some_file_backup.txt']
501        >>> path.backup(True)
502        >>> list(path.iterdir())
503        >>> ['some_file.txt', 'some_file_backup.txt', 'some_file_backup_04-28-2023-06_25_52_PM.txt']
504        """
505        if not self.exists():
506            return None
507        backup_stem = f"{self.stem}_backup"
508        if timestamp:
509            backup_stem = f"{backup_stem}_{datetime.datetime.now().strftime('%m-%d-%Y-%I_%M_%S_%p')}"
510        backup_path = self.with_stem(backup_stem)
511        self.copy(backup_path, True)
512        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:
514    def execute(self, command: str = "", args: str = "") -> int:
515        """Make a call to `os.system` using the path pointed to by this Pathier object.
516
517        #### :params:
518
519        `command`: Program/command to precede the path with.
520
521        `args`: Any arguments that should come after the path.
522
523        :returns: The integer output of `os.system`.
524
525        e.g.
526        >>> path = Pathier("mydirectory") / "myscript.py"
527        then
528        >>> path.execute("py", "--iterations 10")
529        equivalent to
530        >>> os.system(f"py {path} --iterations 10")"""
531        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