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: dict[str, int] = {} 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 size_list = [ 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(size_list, ["Dir", "Size"])) 26 print(f"Total size of '{Pathier.cwd()}': {Pathier.format_bytes(total_size)}") 27 28 29__version__ = "1.5.1"
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: list[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: list[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: list[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: list[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