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

Dump data to json file.

def toml_loads( self, encoding: typing.Any | None = None, errors: typing.Any | None = None) -> Any:
210    def toml_loads(self, encoding: Any | None = None, errors: Any | None = None) -> Any:
211        """Load toml file."""
212        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):
214    def toml_dumps(
215        self,
216        data: Any,
217        encoding: Any | None = None,
218        errors: Any | None = None,
219        newline: Any | None = None,
220        sort_keys: bool = False,
221        parents: bool = True,
222    ):
223        """Dump data to toml file."""
224        self.write_text(
225            tomlkit.dumps(data, sort_keys), encoding, errors, newline, parents
226        )

Dump data to toml file.

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