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