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 mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
137        """Create this directory.
138        Same as Path().mkdir() except
139        'parents' and 'exist_ok' default
140        to True instead of False."""
141        super().mkdir(mode, parents, exist_ok)
142
143    def touch(self):
144        """Create file and parents if necessary."""
145        self.parent.mkdir()
146        super().touch()
147
148    def write_text(
149        self,
150        data: Any,
151        encoding: Any | None = None,
152        errors: Any | None = None,
153        newline: Any | None = None,
154        parents: bool = True,
155    ):
156        """Write data to file. If a TypeError is raised, the function
157        will attempt to case data to a str and try the write again.
158        If a FileNotFoundError is raised and parents = True,
159        self.parent will be created."""
160        write = functools.partial(
161            super().write_text,
162            encoding=encoding,
163            errors=errors,
164            newline=newline,
165        )
166        try:
167            write(data)
168        except TypeError:
169            data = str(data)
170            write(data)
171        except FileNotFoundError:
172            if parents:
173                self.parent.mkdir(parents=True)
174                write(data)
175            else:
176                raise
177        except Exception as e:
178            raise
179
180    def write_bytes(self, data: bytes, parents: bool = True):
181        """Write bytes to file.
182
183        :param parents: If True and the write operation fails
184        with a FileNotFoundError, make the parent directory
185        and retry the write."""
186        try:
187            super().write_bytes(data)
188        except FileNotFoundError:
189            if parents:
190                self.parent.mkdir(parents=True)
191                super().write_bytes(data)
192            else:
193                raise
194        except Exception as e:
195            raise
196
197    def json_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
198        """Load json file."""
199        return json.loads(self.read_text(encoding, errors))
200
201    def json_dumps(
202        self,
203        data: Any,
204        encoding: Any | None = None,
205        errors: Any | None = None,
206        newline: Any | None = None,
207        sort_keys: bool = False,
208        indent: Any | None = None,
209        default: Any | None = None,
210        parents: bool = True,
211    ) -> Any:
212        """Dump data to json file."""
213        self.write_text(
214            json.dumps(data, indent=indent, default=default, sort_keys=sort_keys),
215            encoding,
216            errors,
217            newline,
218            parents,
219        )
220
221    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
222        """Load toml file."""
223        return tomlkit.loads(self.read_text(encoding, errors))
224
225    def toml_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        parents: bool = True,
233    ):
234        """Dump data to toml file."""
235        self.write_text(
236            tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents
237        )
238
239    def loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
240        """Load a json or toml file based off this instance's suffix."""
241        match self.suffix:
242            case ".json":
243                return self.json_loads(encoding, errors)
244            case ".toml":
245                return self.toml_loads(encoding, errors)
246
247    def dumps(
248        self,
249        data: Any,
250        encoding: Any | None = None,
251        errors: Any | None = None,
252        newline: Any | None = None,
253        sort_keys: bool = False,
254        indent: Any | None = None,
255        default: Any | None = None,
256        parents: bool = True,
257    ):
258        """Dump data to a json or toml file based off this instance's suffix."""
259        match self.suffix:
260            case ".json":
261                self.json_dumps(
262                    data, encoding, errors, newline, sort_keys, indent, default, parents
263                )
264            case ".toml":
265                self.toml_dumps(data, encoding, errors, newline, sort_keys, parents)
266
267    def delete(self, missing_ok: bool = True):
268        """Delete the file or folder pointed to by this instance.
269        Uses self.unlink() if a file and uses shutil.rmtree() if a directory."""
270        if self.is_file():
271            self.unlink(missing_ok)
272        elif self.is_dir():
273            shutil.rmtree(self)
274
275    def copy(
276        self, new_path: Self | pathlib.Path | str, overwrite: bool = False
277    ) -> Self:
278        """Copy the path pointed to by this instance
279        to the instance pointed to by new_path using shutil.copyfile
280        or shutil.copytree. Returns the new path.
281
282        :param new_path: The copy destination.
283
284        :param overwrite: If True, files already existing in new_path
285        will be overwritten. If False, only files that don't exist in new_path
286        will be copied."""
287        new_path = Pathier(new_path)
288        if self.is_dir():
289            if overwrite or not new_path.exists():
290                shutil.copytree(self, new_path, dirs_exist_ok=True)
291            else:
292                files = self.rglob("*.*")
293                for file in files:
294                    dst = new_path.with_name(file.name)
295                    if not dst.exists():
296                        shutil.copyfile(file, dst)
297        elif self.is_file():
298            if overwrite or not new_path.exists():
299                shutil.copyfile(self, new_path)
300        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 mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
136    def mkdir(self, mode: int = 511, parents: bool = True, exist_ok: bool = True):
137        """Create this directory.
138        Same as Path().mkdir() except
139        'parents' and 'exist_ok' default
140        to True instead of False."""
141        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):
143    def touch(self):
144        """Create file and parents if necessary."""
145        self.parent.mkdir()
146        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):
148    def write_text(
149        self,
150        data: Any,
151        encoding: Any | None = None,
152        errors: Any | None = None,
153        newline: Any | None = None,
154        parents: bool = True,
155    ):
156        """Write data to file. If a TypeError is raised, the function
157        will attempt to case data to a str and try the write again.
158        If a FileNotFoundError is raised and parents = True,
159        self.parent will be created."""
160        write = functools.partial(
161            super().write_text,
162            encoding=encoding,
163            errors=errors,
164            newline=newline,
165        )
166        try:
167            write(data)
168        except TypeError:
169            data = str(data)
170            write(data)
171        except FileNotFoundError:
172            if parents:
173                self.parent.mkdir(parents=True)
174                write(data)
175            else:
176                raise
177        except Exception as e:
178            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):
180    def write_bytes(self, data: bytes, parents: bool = True):
181        """Write bytes to file.
182
183        :param parents: If True and the write operation fails
184        with a FileNotFoundError, make the parent directory
185        and retry the write."""
186        try:
187            super().write_bytes(data)
188        except FileNotFoundError:
189            if parents:
190                self.parent.mkdir(parents=True)
191                super().write_bytes(data)
192            else:
193                raise
194        except Exception as e:
195            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:
197    def json_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
198        """Load json file."""
199        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:
201    def json_dumps(
202        self,
203        data: Any,
204        encoding: Any | None = None,
205        errors: Any | None = None,
206        newline: Any | None = None,
207        sort_keys: bool = False,
208        indent: Any | None = None,
209        default: Any | None = None,
210        parents: bool = True,
211    ) -> Any:
212        """Dump data to json file."""
213        self.write_text(
214            json.dumps(data, indent=indent, default=default, sort_keys=sort_keys),
215            encoding,
216            errors,
217            newline,
218            parents,
219        )

Dump data to json file.

def toml_loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
221    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
222        """Load toml file."""
223        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):
225    def toml_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        parents: bool = True,
233    ):
234        """Dump data to toml file."""
235        self.write_text(
236            tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents
237        )

Dump data to toml file.

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