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

Dump data to json file.

def toml_loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
238    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
239        """Load toml file."""
240        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):
242    def toml_dumps(
243        self,
244        data: Any,
245        encoding: Any | None = None,
246        errors: Any | None = None,
247        newline: Any | None = None,
248        sort_keys: bool = False,
249        parents: bool = True,
250    ):
251        """Dump data to toml file."""
252        self.write_text(
253            tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents
254        )

Dump data to toml file.

def loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
256    def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
257        """Load a json or toml file based off this instance's suffix."""
258        match self.suffix:
259            case ".json":
260                return self.json_loads(encoding, errors)
261            case ".toml":
262                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):
264    def dumps(
265        self,
266        data: Any,
267        encoding: Any | None = None,
268        errors: Any | None = None,
269        newline: Any | None = None,
270        sort_keys: bool = False,
271        indent: Any | None = None,
272        default: Any | None = None,
273        parents: bool = True,
274    ):
275        """Dump data to a json or toml file based off this instance's suffix."""
276        match self.suffix:
277            case ".json":
278                self.json_dumps(
279                    data, encoding, errors, newline, sort_keys, indent, default, parents
280                )
281            case ".toml":
282                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):
284    def delete(self, missing_ok: bool = True):
285        """Delete the file or folder pointed to by this instance.
286        Uses self.unlink() if a file and uses shutil.rmtree() if a directory."""
287        if self.is_file():
288            self.unlink(missing_ok)
289        elif self.is_dir():
290            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:
292    def copy(
293        self, new_path: Self | pathlib.Path | str, overwrite: bool = False
294    ) -> Self:
295        """Copy the path pointed to by this instance
296        to the instance pointed to by new_path using shutil.copyfile
297        or shutil.copytree. Returns the new path.
298
299        :param new_path: The copy destination.
300
301        :param overwrite: If True, files already existing in new_path
302        will be overwritten. If False, only files that don't exist in new_path
303        will be copied."""
304        new_path = Pathier(new_path)
305        if self.is_dir():
306            if overwrite or not new_path.exists():
307                shutil.copytree(self, new_path, dirs_exist_ok=True)
308            else:
309                files = self.rglob("*.*")
310                for file in files:
311                    dst = new_path.with_name(file.name)
312                    if not dst.exists():
313                        shutil.copyfile(file, dst)
314        elif self.is_file():
315            if overwrite or not new_path.exists():
316                shutil.copyfile(self, new_path)
317        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