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