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