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