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