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