pathier

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

Subclasses the standard library pathlib.Path class.

Pathier()
dob: datetime.datetime | None

Returns the creation date of this file or directory as a dateime.datetime object.

age: float | None

Returns the age in seconds of this file or directory.

mod_date: datetime.datetime | None

Returns the modification date of this file or directory as a datetime.datetime object.

mod_delta: float | None

Returns how long ago in seconds this file or directory was modified.

def size(self, format: bool = False) -> int | str | None:
62    def size(self, format: bool = False) -> int | str | None:
63        """Returns the size in bytes of this file or directory.
64        Returns None if this path doesn't exist.
65
66        :param format: If True, return value as a formatted string."""
67        if not self.exists():
68            return None
69        if self.is_file():
70            size = self.stat().st_size
71        if self.is_dir():
72            size = sum(file.stat().st_size for file in self.rglob("*.*"))
73        if format:
74            return self.format_size(size)
75        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.
@staticmethod
def format_size(size: int) -> str:
77    @staticmethod
78    def format_size(size: int) -> str:
79        """Format 'size' with common file size abbreviations
80        and rounded to two decimal places.
81        >>> 1234 -> "1.23 kb" """
82        for unit in ["bytes", "kb", "mb", "gb", "tb", "pb"]:
83            if unit != "bytes":
84                size *= 0.001
85            if size < 1000 or unit == "pb":
86                return f"{round(size, 2)} {unit}"

Format 'size' with common file size abbreviations and rounded to two decimal places.

>>> 1234 -> "1.23 kb"
def is_larger(self, path: Self) -> bool:
88    def is_larger(self, path: Self) -> bool:
89        """Returns whether this file or folder is larger than
90        the one pointed to by 'path'."""
91        return self.size() > path.size()

Returns whether this file or folder is larger than the one pointed to by 'path'.

def is_older(self, path: Self) -> bool:
93    def is_older(self, path: Self) -> bool:
94        """Returns whether this file or folder is older than
95        the one pointed to by 'path'."""
96        return self.dob < path.dob

Returns whether this file or folder is older than the one pointed to by 'path'.

def modified_more_recently(self, path: Self) -> bool:
 98    def modified_more_recently(self, path: Self) -> bool:
 99        """Returns whether this file or folder was modified
100        more recently than the one pointed to by 'path'."""
101        return self.mod_date > path.mod_date

Returns whether this file or folder was modified more recently than the one pointed to by 'path'.

def mkcwd(self):
104    def mkcwd(self):
105        """Make this path your current working directory."""
106        os.chdir(self)

Make this path your current working directory.

def moveup(self, name: str) -> Self:
108    def moveup(self, name: str) -> Self:
109        """Return a new Pathier object that is a parent of this instance.
110        'name' is case-sensitive and raises an exception if it isn't in self.parts.
111        >>> p = Pathier("C:\some\directory\in\your\system")
112        >>> print(p.moveup("directory"))
113        >>> "C:\some\directory"
114        >>> print(p.moveup("yeet"))
115        >>> "Exception: yeet is not a parent of C:\some\directory\in\your\system" """
116        if name not in self.parts:
117            raise Exception(f"{name} is not a parent of {self}")
118        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"
def move_under(self, name: str) -> Self:
131    def move_under(self, name: str) -> Self:
132        """Return a new Pathier object such that the stem
133        is one level below the folder 'name'.
134        'name' is case-sensitive and raises an exception if it isn't in self.parts.
135        >>> p = Pathier("a/b/c/d/e/f/g")
136        >>> print(p.move_under("c"))
137        >>> 'a/b/c/d'"""
138        if name not in self.parts:
139            raise Exception(f"{name} is not a parent of {self}")
140        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'
def separate(self, name: str, keep_name: bool = False) -> Self:
142    def separate(self, name: str, keep_name: bool = False) -> Self:
143        """Return a new Pathier object that is the
144        relative child path after 'name'.
145        'name' is case-sensitive and raises an exception if it isn't in self.parts.
146
147        :param keep_name: If True, the returned path will start with 'name'.
148        >>> p = Pathier("a/b/c/d/e/f/g")
149        >>> print(p.separate("c"))
150        >>> 'd/e/f/g'
151        >>> print(p.separate("c", True))
152        >>> 'c/d/e/f/g'"""
153        if name not in self.parts:
154            raise Exception(f"{name} is not a parent of {self}")
155        if keep_name:
156            return Pathier(*self.parts[self.parts.index(name) :])
157        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'
def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
160    def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
161        """Create this directory.
162        Same as Path().mkdir() except
163        'parents' and 'exist_ok' default
164        to True instead of False."""
165        super().mkdir(mode, parents, exist_ok)

Create this directory. Same as Path().mkdir() except 'parents' and 'exist_ok' default to True instead of False.

def touch(self):
167    def touch(self):
168        """Create file and parents if necessary."""
169        self.parent.mkdir()
170        super().touch()

Create file and parents if necessary.

def write_text( self, data: Any, encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, parents: bool = True):
172    def write_text(
173        self,
174        data: Any,
175        encoding: Any | None = None,
176        errors: Any | None = None,
177        newline: Any | None = None,
178        parents: bool = True,
179    ):
180        """Write data to file. If a TypeError is raised, the function
181        will attempt to case data to a str and try the write again.
182        If a FileNotFoundError is raised and parents = True,
183        self.parent will be created."""
184        write = functools.partial(
185            super().write_text,
186            encoding=encoding,
187            errors=errors,
188            newline=newline,
189        )
190        try:
191            write(data)
192        except TypeError:
193            data = str(data)
194            write(data)
195        except FileNotFoundError:
196            if parents:
197                self.parent.mkdir(parents=True)
198                write(data)
199            else:
200                raise
201        except Exception as e:
202            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.

def write_bytes(self, data: bytes, parents: bool = True):
204    def write_bytes(self, data: bytes, parents: bool = True):
205        """Write bytes to file.
206
207        :param parents: If True and the write operation fails
208        with a FileNotFoundError, make the parent directory
209        and retry the write."""
210        try:
211            super().write_bytes(data)
212        except FileNotFoundError:
213            if parents:
214                self.parent.mkdir(parents=True)
215                super().write_bytes(data)
216            else:
217                raise
218        except Exception as e:
219            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.
def json_loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
221    def json_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
222        """Load json file."""
223        return json.loads(self.read_text(encoding, errors))

Load json file.

def json_dumps( self, data: Any, encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, sort_keys: bool = False, indent: typing.Any | None = None, default: typing.Any | None = None, parents: bool = True) -> Any:
225    def json_dumps(
226        self,
227        data: Any,
228        encoding: Any | None = None,
229        errors: Any | None = None,
230        newline: Any | None = None,
231        sort_keys: bool = False,
232        indent: Any | None = None,
233        default: Any | None = None,
234        parents: bool = True,
235    ) -> Any:
236        """Dump data to json file."""
237        self.write_text(
238            json.dumps(data, indent=indent, default=default, sort_keys=sort_keys),
239            encoding,
240            errors,
241            newline,
242            parents,
243        )

Dump data to json file.

def toml_loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
245    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
246        """Load toml file."""
247        return tomlkit.loads(self.read_text(encoding, errors))

Load toml file.

def toml_dumps( self, data: Any, encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, sort_keys: bool = False, parents: bool = True):
249    def toml_dumps(
250        self,
251        data: Any,
252        encoding: Any | None = None,
253        errors: Any | None = None,
254        newline: Any | None = None,
255        sort_keys: bool = False,
256        parents: bool = True,
257    ):
258        """Dump data to toml file."""
259        self.write_text(
260            tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents
261        )

Dump data to toml file.

def loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
263    def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
264        """Load a json or toml file based off this instance's suffix."""
265        match self.suffix:
266            case ".json":
267                return self.json_loads(encoding, errors)
268            case ".toml":
269                return self.toml_loads(encoding, errors)

Load a json or toml file based off this instance's suffix.

def dumps( self, data: Any, encoding: typing.Any | None = None, errors: typing.Any | None = None, newline: typing.Any | None = None, sort_keys: bool = False, indent: typing.Any | None = None, default: typing.Any | None = None, parents: bool = True):
271    def dumps(
272        self,
273        data: Any,
274        encoding: Any | None = None,
275        errors: Any | None = None,
276        newline: Any | None = None,
277        sort_keys: bool = False,
278        indent: Any | None = None,
279        default: Any | None = None,
280        parents: bool = True,
281    ):
282        """Dump data to a json or toml file based off this instance's suffix."""
283        match self.suffix:
284            case ".json":
285                self.json_dumps(
286                    data, encoding, errors, newline, sort_keys, indent, default, parents
287                )
288            case ".toml":
289                self.toml_dumps(data, encoding, errors, newline, sort_keys, parents)

Dump data to a json or toml file based off this instance's suffix.

def delete(self, missing_ok: bool = True):
291    def delete(self, missing_ok: bool = True):
292        """Delete the file or folder pointed to by this instance.
293        Uses self.unlink() if a file and uses shutil.rmtree() if a directory."""
294        if self.is_file():
295            self.unlink(missing_ok)
296        elif self.is_dir():
297            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.

def copy( self, new_path: Union[Self, pathlib.Path, str], overwrite: bool = False) -> Self:
299    def copy(
300        self, new_path: Self | pathlib.Path | str, overwrite: bool = False
301    ) -> Self:
302        """Copy the path pointed to by this instance
303        to the instance pointed to by new_path using shutil.copyfile
304        or shutil.copytree. Returns the new path.
305
306        :param new_path: The copy destination.
307
308        :param overwrite: If True, files already existing in new_path
309        will be overwritten. If False, only files that don't exist in new_path
310        will be copied."""
311        new_path = Pathier(new_path)
312        if self.is_dir():
313            if overwrite or not new_path.exists():
314                shutil.copytree(self, new_path, dirs_exist_ok=True)
315            else:
316                files = self.rglob("*.*")
317                for file in files:
318                    dst = new_path.with_name(file.name)
319                    if not dst.exists():
320                        shutil.copyfile(file, dst)
321        elif self.is_file():
322            if overwrite or not new_path.exists():
323                shutil.copyfile(self, new_path)
324        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
chmod
lchmod
rmdir
lstat
rename
replace
exists
is_dir
is_file
is_mount
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