pathier
15class Pathier(pathlib.Path): 16 """Subclasses the standard library pathlib.Path class.""" 17 18 def __new__(cls, *args, **kwargs): 19 if cls is Pathier: 20 cls = WindowsPath if os.name == "nt" else PosixPath 21 self = cls._from_parts(args) 22 if not self._flavour.is_supported: 23 raise NotImplementedError( 24 "cannot instantiate %r on your system" % (cls.__name__,) 25 ) 26 return self 27 28 # ===============================================stats=============================================== 29 @property 30 def dob(self) -> datetime.datetime | None: 31 """Returns the creation date of this file 32 or directory as a dateime.datetime object.""" 33 if self.exists(): 34 return datetime.datetime.fromtimestamp(self.stat().st_ctime) 35 else: 36 return None 37 38 @property 39 def age(self) -> float | None: 40 """Returns the age in seconds of this file or directory.""" 41 if self.exists(): 42 return (datetime.datetime.now() - self.dob).total_seconds() 43 else: 44 return None 45 46 @property 47 def mod_date(self) -> datetime.datetime | None: 48 """Returns the modification date of this file 49 or directory as a datetime.datetime object.""" 50 if self.exists(): 51 return datetime.datetime.fromtimestamp(self.stat().st_mtime) 52 else: 53 return None 54 55 @property 56 def mod_delta(self) -> float | None: 57 """Returns how long ago in seconds this file 58 or directory was modified.""" 59 if self.exists(): 60 return (datetime.datetime.now() - self.mod_date).total_seconds() 61 else: 62 return None 63 64 def size(self, format: bool = False) -> int | str | None: 65 """Returns the size in bytes of this file or directory. 66 Returns None if this path doesn't exist. 67 68 :param format: If True, return value as a formatted string.""" 69 if not self.exists(): 70 return None 71 if self.is_file(): 72 size = self.stat().st_size 73 if self.is_dir(): 74 size = sum(file.stat().st_size for file in self.rglob("*.*")) 75 if format: 76 return self.format_size(size) 77 return size 78 79 @staticmethod 80 def format_size(size: int) -> str: 81 """Format 'size' with common file size abbreviations 82 and rounded to two decimal places. 83 >>> 1234 -> "1.23 kb" """ 84 for unit in ["bytes", "kb", "mb", "gb", "tb", "pb"]: 85 if unit != "bytes": 86 size *= 0.001 87 if size < 1000 or unit == "pb": 88 return f"{round(size, 2)} {unit}" 89 90 def is_larger(self, path: Self) -> bool: 91 """Returns whether this file or folder is larger than 92 the one pointed to by 'path'.""" 93 return self.size() > path.size() 94 95 def is_older(self, path: Self) -> bool: 96 """Returns whether this file or folder is older than 97 the one pointed to by 'path'.""" 98 return self.dob < path.dob 99 100 def modified_more_recently(self, path: Self) -> bool: 101 """Returns whether this file or folder was modified 102 more recently than the one pointed to by 'path'.""" 103 return self.mod_date > path.mod_date 104 105 # ===============================================navigation=============================================== 106 def mkcwd(self): 107 """Make this path your current working directory.""" 108 os.chdir(self) 109 110 @property 111 def in_PATH(self) -> bool: 112 """Return True if this 113 path is in sys.path.""" 114 return str(self) in sys.path 115 116 def add_to_PATH(self, index: int = 0): 117 """Insert this path into sys.path 118 if it isn't already there. 119 120 :param index: The index of sys.path 121 to insert this path at.""" 122 path = str(self) 123 if not self.in_PATH: 124 sys.path.insert(index, path) 125 126 def append_to_PATH(self): 127 """Append this path to sys.path 128 if it isn't already there.""" 129 path = str(self) 130 if not self.in_PATH: 131 sys.path.append(path) 132 133 def remove_from_PATH(self): 134 """Remove this path from sys.path 135 if it's in sys.path.""" 136 if self.in_PATH: 137 sys.path.remove(str(self)) 138 139 def moveup(self, name: str) -> Self: 140 """Return a new Pathier object that is a parent of this instance. 141 'name' is case-sensitive and raises an exception if it isn't in self.parts. 142 >>> p = Pathier("C:\some\directory\in\your\system") 143 >>> print(p.moveup("directory")) 144 >>> "C:\some\directory" 145 >>> print(p.moveup("yeet")) 146 >>> "Exception: yeet is not a parent of C:\some\directory\in\your\system" """ 147 if name not in self.parts: 148 raise Exception(f"{name} is not a parent of {self}") 149 return Pathier(*(self.parts[: self.parts.index(name) + 1])) 150 151 def __sub__(self, levels: int) -> Self: 152 """Return a new Pathier object moved up 'levels' number of parents from the current path. 153 >>> p = Pathier("C:\some\directory\in\your\system") 154 >>> new_p = p - 3 155 >>> print(new_p) 156 >>> "C:\some\directory" """ 157 path = self 158 for _ in range(levels): 159 path = path.parent 160 return path 161 162 def move_under(self, name: str) -> Self: 163 """Return a new Pathier object such that the stem 164 is one level below the folder 'name'. 165 'name' is case-sensitive and raises an exception if it isn't in self.parts. 166 >>> p = Pathier("a/b/c/d/e/f/g") 167 >>> print(p.move_under("c")) 168 >>> 'a/b/c/d'""" 169 if name not in self.parts: 170 raise Exception(f"{name} is not a parent of {self}") 171 return self - (len(self.parts) - self.parts.index(name) - 2) 172 173 def separate(self, name: str, keep_name: bool = False) -> Self: 174 """Return a new Pathier object that is the 175 relative child path after 'name'. 176 'name' is case-sensitive and raises an exception if it isn't in self.parts. 177 178 :param keep_name: If True, the returned path will start with 'name'. 179 >>> p = Pathier("a/b/c/d/e/f/g") 180 >>> print(p.separate("c")) 181 >>> 'd/e/f/g' 182 >>> print(p.separate("c", True)) 183 >>> 'c/d/e/f/g'""" 184 if name not in self.parts: 185 raise Exception(f"{name} is not a parent of {self}") 186 if keep_name: 187 return Pathier(*self.parts[self.parts.index(name) :]) 188 return Pathier(*self.parts[self.parts.index(name) + 1 :]) 189 190 # ============================================write and read============================================ 191 def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True): 192 """Create this directory. 193 Same as Path().mkdir() except 194 'parents' and 'exist_ok' default 195 to True instead of False.""" 196 super().mkdir(mode, parents, exist_ok) 197 198 def touch(self): 199 """Create file and parents if necessary.""" 200 self.parent.mkdir() 201 super().touch() 202 203 def write_text( 204 self, 205 data: Any, 206 encoding: Any | None = None, 207 errors: Any | None = None, 208 newline: Any | None = None, 209 parents: bool = True, 210 ): 211 """Write data to file. If a TypeError is raised, the function 212 will attempt to case data to a str and try the write again. 213 If a FileNotFoundError is raised and parents = True, 214 self.parent will be created.""" 215 write = functools.partial( 216 super().write_text, 217 encoding=encoding, 218 errors=errors, 219 newline=newline, 220 ) 221 try: 222 write(data) 223 except TypeError: 224 data = str(data) 225 write(data) 226 except FileNotFoundError: 227 if parents: 228 self.parent.mkdir(parents=True) 229 write(data) 230 else: 231 raise 232 except Exception as e: 233 raise 234 235 def write_bytes(self, data: bytes, parents: bool = True): 236 """Write bytes to file. 237 238 :param parents: If True and the write operation fails 239 with a FileNotFoundError, make the parent directory 240 and retry the write.""" 241 try: 242 super().write_bytes(data) 243 except FileNotFoundError: 244 if parents: 245 self.parent.mkdir(parents=True) 246 super().write_bytes(data) 247 else: 248 raise 249 except Exception as e: 250 raise 251 252 def json_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any: 253 """Load json file.""" 254 return json.loads(self.read_text(encoding, errors)) 255 256 def json_dumps( 257 self, 258 data: Any, 259 encoding: Any | None = None, 260 errors: Any | None = None, 261 newline: Any | None = None, 262 sort_keys: bool = False, 263 indent: Any | None = None, 264 default: Any | None = None, 265 parents: bool = True, 266 ) -> Any: 267 """Dump data to json file.""" 268 self.write_text( 269 json.dumps(data, indent=indent, default=default, sort_keys=sort_keys), 270 encoding, 271 errors, 272 newline, 273 parents, 274 ) 275 276 def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any: 277 """Load toml file.""" 278 return tomlkit.loads(self.read_text(encoding, errors)) 279 280 def toml_dumps( 281 self, 282 data: Any, 283 encoding: Any | None = None, 284 errors: Any | None = None, 285 newline: Any | None = None, 286 sort_keys: bool = False, 287 parents: bool = True, 288 ): 289 """Dump data to toml file.""" 290 self.write_text( 291 tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents 292 ) 293 294 def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any: 295 """Load a json or toml file based off this instance's suffix.""" 296 match self.suffix: 297 case ".json": 298 return self.json_loads(encoding, errors) 299 case ".toml": 300 return self.toml_loads(encoding, errors) 301 302 def dumps( 303 self, 304 data: Any, 305 encoding: Any | None = None, 306 errors: Any | None = None, 307 newline: Any | None = None, 308 sort_keys: bool = False, 309 indent: Any | None = None, 310 default: Any | None = None, 311 parents: bool = True, 312 ): 313 """Dump data to a json or toml file based off this instance's suffix.""" 314 match self.suffix: 315 case ".json": 316 self.json_dumps( 317 data, encoding, errors, newline, sort_keys, indent, default, parents 318 ) 319 case ".toml": 320 self.toml_dumps(data, encoding, errors, newline, sort_keys, parents) 321 322 def delete(self, missing_ok: bool = True): 323 """Delete the file or folder pointed to by this instance. 324 Uses self.unlink() if a file and uses shutil.rmtree() if a directory.""" 325 if self.is_file(): 326 self.unlink(missing_ok) 327 elif self.is_dir(): 328 shutil.rmtree(self) 329 330 def copy( 331 self, new_path: Self | pathlib.Path | str, overwrite: bool = False 332 ) -> Self: 333 """Copy the path pointed to by this instance 334 to the instance pointed to by new_path using shutil.copyfile 335 or shutil.copytree. Returns the new path. 336 337 :param new_path: The copy destination. 338 339 :param overwrite: If True, files already existing in new_path 340 will be overwritten. If False, only files that don't exist in new_path 341 will be copied.""" 342 new_path = Pathier(new_path) 343 if self.is_dir(): 344 if overwrite or not new_path.exists(): 345 shutil.copytree(self, new_path, dirs_exist_ok=True) 346 else: 347 files = self.rglob("*.*") 348 for file in files: 349 dst = new_path.with_name(file.name) 350 if not dst.exists(): 351 shutil.copyfile(file, dst) 352 elif self.is_file(): 353 if overwrite or not new_path.exists(): 354 shutil.copyfile(self, new_path) 355 return new_path
Subclasses the standard library pathlib.Path class.
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.
64 def size(self, format: bool = False) -> int | str | None: 65 """Returns the size in bytes of this file or directory. 66 Returns None if this path doesn't exist. 67 68 :param format: If True, return value as a formatted string.""" 69 if not self.exists(): 70 return None 71 if self.is_file(): 72 size = self.stat().st_size 73 if self.is_dir(): 74 size = sum(file.stat().st_size for file in self.rglob("*.*")) 75 if format: 76 return self.format_size(size) 77 return size
Returns the size in bytes of this file or directory. Returns None if this path doesn't exist.
Parameters
- format: If True, return value as a formatted string.
79 @staticmethod 80 def format_size(size: int) -> str: 81 """Format 'size' with common file size abbreviations 82 and rounded to two decimal places. 83 >>> 1234 -> "1.23 kb" """ 84 for unit in ["bytes", "kb", "mb", "gb", "tb", "pb"]: 85 if unit != "bytes": 86 size *= 0.001 87 if size < 1000 or unit == "pb": 88 return f"{round(size, 2)} {unit}"
Format 'size' with common file size abbreviations and rounded to two decimal places.
>>> 1234 -> "1.23 kb"
90 def is_larger(self, path: Self) -> bool: 91 """Returns whether this file or folder is larger than 92 the one pointed to by 'path'.""" 93 return self.size() > path.size()
Returns whether this file or folder is larger than the one pointed to by 'path'.
95 def is_older(self, path: Self) -> bool: 96 """Returns whether this file or folder is older than 97 the one pointed to by 'path'.""" 98 return self.dob < path.dob
Returns whether this file or folder is older than the one pointed to by 'path'.
100 def modified_more_recently(self, path: Self) -> bool: 101 """Returns whether this file or folder was modified 102 more recently than the one pointed to by 'path'.""" 103 return self.mod_date > path.mod_date
Returns whether this file or folder was modified more recently than the one pointed to by 'path'.
116 def add_to_PATH(self, index: int = 0): 117 """Insert this path into sys.path 118 if it isn't already there. 119 120 :param index: The index of sys.path 121 to insert this path at.""" 122 path = str(self) 123 if not self.in_PATH: 124 sys.path.insert(index, path)
Insert this path into sys.path if it isn't already there.
Parameters
- index: The index of sys.path to insert this path at.
126 def append_to_PATH(self): 127 """Append this path to sys.path 128 if it isn't already there.""" 129 path = str(self) 130 if not self.in_PATH: 131 sys.path.append(path)
Append this path to sys.path if it isn't already there.
133 def remove_from_PATH(self): 134 """Remove this path from sys.path 135 if it's in sys.path.""" 136 if self.in_PATH: 137 sys.path.remove(str(self))
Remove this path from sys.path if it's in sys.path.
139 def moveup(self, name: str) -> Self: 140 """Return a new Pathier object that is a parent of this instance. 141 'name' is case-sensitive and raises an exception if it isn't in self.parts. 142 >>> p = Pathier("C:\some\directory\in\your\system") 143 >>> print(p.moveup("directory")) 144 >>> "C:\some\directory" 145 >>> print(p.moveup("yeet")) 146 >>> "Exception: yeet is not a parent of C:\some\directory\in\your\system" """ 147 if name not in self.parts: 148 raise Exception(f"{name} is not a parent of {self}") 149 return Pathier(*(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"
162 def move_under(self, name: str) -> Self: 163 """Return a new Pathier object such that the stem 164 is one level below the folder 'name'. 165 'name' is case-sensitive and raises an exception if it isn't in self.parts. 166 >>> p = Pathier("a/b/c/d/e/f/g") 167 >>> print(p.move_under("c")) 168 >>> 'a/b/c/d'""" 169 if name not in self.parts: 170 raise Exception(f"{name} is not a parent of {self}") 171 return self - (len(self.parts) - self.parts.index(name) - 2)
Return a new Pathier object such that the stem is one level below the folder 'name'. 'name' is case-sensitive and raises an exception if it isn't in self.parts.
>>> p = Pathier("a/b/c/d/e/f/g")
>>> print(p.move_under("c"))
>>> 'a/b/c/d'
173 def separate(self, name: str, keep_name: bool = False) -> Self: 174 """Return a new Pathier object that is the 175 relative child path after 'name'. 176 'name' is case-sensitive and raises an exception if it isn't in self.parts. 177 178 :param keep_name: If True, the returned path will start with 'name'. 179 >>> p = Pathier("a/b/c/d/e/f/g") 180 >>> print(p.separate("c")) 181 >>> 'd/e/f/g' 182 >>> print(p.separate("c", True)) 183 >>> 'c/d/e/f/g'""" 184 if name not in self.parts: 185 raise Exception(f"{name} is not a parent of {self}") 186 if keep_name: 187 return Pathier(*self.parts[self.parts.index(name) :]) 188 return Pathier(*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.
Parameters
- 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'
191 def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True): 192 """Create this directory. 193 Same as Path().mkdir() except 194 'parents' and 'exist_ok' default 195 to True instead of False.""" 196 super().mkdir(mode, parents, exist_ok)
Create this directory. Same as Path().mkdir() except 'parents' and 'exist_ok' default to True instead of False.
198 def touch(self): 199 """Create file and parents if necessary.""" 200 self.parent.mkdir() 201 super().touch()
Create file and parents if necessary.
203 def write_text( 204 self, 205 data: Any, 206 encoding: Any | None = None, 207 errors: Any | None = None, 208 newline: Any | None = None, 209 parents: bool = True, 210 ): 211 """Write data to file. If a TypeError is raised, the function 212 will attempt to case data to a str and try the write again. 213 If a FileNotFoundError is raised and parents = True, 214 self.parent will be created.""" 215 write = functools.partial( 216 super().write_text, 217 encoding=encoding, 218 errors=errors, 219 newline=newline, 220 ) 221 try: 222 write(data) 223 except TypeError: 224 data = str(data) 225 write(data) 226 except FileNotFoundError: 227 if parents: 228 self.parent.mkdir(parents=True) 229 write(data) 230 else: 231 raise 232 except Exception as e: 233 raise
Write data to file. If a TypeError is raised, the function will attempt to case data to a str and try the write again. If a FileNotFoundError is raised and parents = True, self.parent will be created.
235 def write_bytes(self, data: bytes, parents: bool = True): 236 """Write bytes to file. 237 238 :param parents: If True and the write operation fails 239 with a FileNotFoundError, make the parent directory 240 and retry the write.""" 241 try: 242 super().write_bytes(data) 243 except FileNotFoundError: 244 if parents: 245 self.parent.mkdir(parents=True) 246 super().write_bytes(data) 247 else: 248 raise 249 except Exception as e: 250 raise
Write bytes to file.
Parameters
- parents: If True and the write operation fails with a FileNotFoundError, make the parent directory and retry the write.
252 def json_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any: 253 """Load json file.""" 254 return json.loads(self.read_text(encoding, errors))
Load json file.
256 def json_dumps( 257 self, 258 data: Any, 259 encoding: Any | None = None, 260 errors: Any | None = None, 261 newline: Any | None = None, 262 sort_keys: bool = False, 263 indent: Any | None = None, 264 default: Any | None = None, 265 parents: bool = True, 266 ) -> Any: 267 """Dump data to json file.""" 268 self.write_text( 269 json.dumps(data, indent=indent, default=default, sort_keys=sort_keys), 270 encoding, 271 errors, 272 newline, 273 parents, 274 )
Dump data to json file.
276 def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any: 277 """Load toml file.""" 278 return tomlkit.loads(self.read_text(encoding, errors))
Load toml file.
280 def toml_dumps( 281 self, 282 data: Any, 283 encoding: Any | None = None, 284 errors: Any | None = None, 285 newline: Any | None = None, 286 sort_keys: bool = False, 287 parents: bool = True, 288 ): 289 """Dump data to toml file.""" 290 self.write_text( 291 tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents 292 )
Dump data to toml file.
294 def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any: 295 """Load a json or toml file based off this instance's suffix.""" 296 match self.suffix: 297 case ".json": 298 return self.json_loads(encoding, errors) 299 case ".toml": 300 return self.toml_loads(encoding, errors)
Load a json or toml file based off this instance's suffix.
302 def dumps( 303 self, 304 data: Any, 305 encoding: Any | None = None, 306 errors: Any | None = None, 307 newline: Any | None = None, 308 sort_keys: bool = False, 309 indent: Any | None = None, 310 default: Any | None = None, 311 parents: bool = True, 312 ): 313 """Dump data to a json or toml file based off this instance's suffix.""" 314 match self.suffix: 315 case ".json": 316 self.json_dumps( 317 data, encoding, errors, newline, sort_keys, indent, default, parents 318 ) 319 case ".toml": 320 self.toml_dumps(data, encoding, errors, newline, sort_keys, parents)
Dump data to a json or toml file based off this instance's suffix.
322 def delete(self, missing_ok: bool = True): 323 """Delete the file or folder pointed to by this instance. 324 Uses self.unlink() if a file and uses shutil.rmtree() if a directory.""" 325 if self.is_file(): 326 self.unlink(missing_ok) 327 elif self.is_dir(): 328 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.
330 def copy( 331 self, new_path: Self | pathlib.Path | str, overwrite: bool = False 332 ) -> Self: 333 """Copy the path pointed to by this instance 334 to the instance pointed to by new_path using shutil.copyfile 335 or shutil.copytree. Returns the new path. 336 337 :param new_path: The copy destination. 338 339 :param overwrite: If True, files already existing in new_path 340 will be overwritten. If False, only files that don't exist in new_path 341 will be copied.""" 342 new_path = Pathier(new_path) 343 if self.is_dir(): 344 if overwrite or not new_path.exists(): 345 shutil.copytree(self, new_path, dirs_exist_ok=True) 346 else: 347 files = self.rglob("*.*") 348 for file in files: 349 dst = new_path.with_name(file.name) 350 if not dst.exists(): 351 shutil.copyfile(file, dst) 352 elif self.is_file(): 353 if overwrite or not new_path.exists(): 354 shutil.copyfile(self, new_path) 355 return new_path
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.
Parameters
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.
Inherited Members
- pathlib.Path
- cwd
- home
- samefile
- iterdir
- glob
- rglob
- absolute
- resolve
- stat
- owner
- group
- open
- 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