pathlibutil 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pathlibutil
3
- Version: 0.1.1
4
- Summary:
5
- Home-page: https://github.com/d-chris/pathlibutil
3
+ Version: 0.1.2
4
+ Summary: inherits from pathlib.Path with methods for hashing, copying, deleting and more
5
+ Home-page: https://d-chris.github.io
6
6
  License: MIT
7
7
  Keywords: pathlib,hashlib,shutil
8
8
  Author: Christoph Dörrer
@@ -15,6 +15,8 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Provides-Extra: 7z
19
+ Requires-Dist: py7zr (>=0.20.2,<0.21.0) ; extra == "7z"
18
20
  Project-URL: Documentation, https://d-chris.github.io/pathlibutil
19
21
  Project-URL: Repository, https://github.com/d-chris/pathlibutil
20
22
  Description-Content-Type: text/markdown
@@ -26,6 +28,10 @@ Description-Content-Type: text/markdown
26
28
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/)
27
29
  [![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
28
30
  [![GitHub Workflow Test)](https://img.shields.io/github/actions/workflow/status/d-chris/pathlibutil/pytest.yml?logo=github&label=pytest)](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
31
+ [![Website](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil&up_message=pdoc&logo=github&label=documentation)](https://d-chris.github.io/pathlibutil)
32
+ [![GitHub tag (with filter)](https://img.shields.io/github/v/tag/d-chris/pathlibutil?logo=github&label=github)](https://github.com/d-chris/pathlibutil)
33
+ [![Coverage](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil%2Fhtmlcov&up_message=available&down_message=missing&logo=codecov&label=coverage)](https://d-chris.github.io/pathlibutil/htmlcov)
34
+
29
35
 
30
36
  ---
31
37
 
@@ -39,6 +45,7 @@ Description-Content-Type: text/markdown
39
45
  - `Path().copy()` copy a file or directory to a new path destination
40
46
  - `Path().delete()` delete a file or directory-tree
41
47
  - `Path().move()` move a file or directory to a new path destination
48
+ - `Path().make_archive()` create and `Path().unpack_archive()` an archive from a file or directory
42
49
 
43
50
  ## Installation
44
51
 
@@ -62,6 +69,8 @@ Read a file and print its content and some file information to stdout.
62
69
  > `Path().read_lines()`
63
70
 
64
71
  ```python
72
+ from pathlib import Path
73
+
65
74
  readme = Path('README.md')
66
75
 
67
76
  print('File content'.center(80, '='))
@@ -78,6 +87,8 @@ Write a file with md5 checksums of all python files in the pathlibutil-directory
78
87
  > `Path().hexdigest()`
79
88
 
80
89
  ```python
90
+ from pathlib import Path
91
+
81
92
  file = Path('pathlibutil.md5')
82
93
 
83
94
  algorithm = file.suffix[1:]
@@ -99,6 +110,8 @@ Read a file with md5 checksums and verify them.
99
110
  > `Path().verify()`, `Path.default_hash` and `contextmanager`
100
111
 
101
112
  ```python
113
+ from pathlib import Path
114
+
102
115
  file = Path('pathlibutil.md5')
103
116
 
104
117
  Path.default_hash = file.suffix[1:]
@@ -122,3 +135,66 @@ with file.parent as cwd:
122
135
  print(f'{tag.ljust(len(digest), ".")} *{filename}')
123
136
  ```
124
137
 
138
+ ## Example 4
139
+
140
+ Search all pycache directories and free the memory.
141
+ > `Path().delete()` and `Path().size()`
142
+
143
+ ```python
144
+ from pathlib import Path
145
+
146
+ mem = 0
147
+ i = 0
148
+
149
+ for i, cache in enumerate(Path('.').rglob('*/__pycache__/'), start=1):
150
+ cache_size = cache.size()
151
+ try:
152
+ cache.delete(recursive=True)
153
+ except OSError:
154
+ print(f'Failed to delete {cache}')
155
+ else:
156
+ mem += cache_size
157
+
158
+ print(f'{i} cache directories deleted, {mem / 2**20:.2f} MB freed.')
159
+ ```
160
+
161
+ ## Example 5
162
+
163
+ Inherit from `pathlibutil.Path` to register new a archive format.
164
+ Specify a `name` as keyword argument in the new subclass, which has to be the suffix of the archives.
165
+ Implement a classmethod `_register_archive_format()` to register new archive formats.
166
+ > `Path().make_archive()` and `Path().move()`
167
+
168
+ ```python
169
+ import shutil
170
+ import pathlibutil
171
+
172
+ class RegisterRarFormat(pathlibutil.Path, name='rar'):
173
+ @classmethod
174
+ def _register_archive_format(cls):
175
+ """
176
+ implement new register functions for given `name`
177
+ """
178
+ try:
179
+ from pyunpack import Archive
180
+ except ModuleNotFoundError:
181
+ raise ModuleNotFoundError('pip install pyunpack')
182
+ else:
183
+ shutil.register_archive_format(
184
+ 'rar', Archive, description='rar archive'
185
+ )
186
+ shutil.register_unpack_format(
187
+ 'rar', ['.rar'], Archive
188
+ )
189
+
190
+ file = pathlibutil.Path('README.md')
191
+
192
+ print(f"available archive formats: {file.archive_formats}")
193
+
194
+ archive = file.make_archive('README.rar')
195
+
196
+ backup = archive.move('./backup/')
197
+
198
+ print(f'rar archive created: {archive.name} and moved to: {backup.parent}')
199
+ ```
200
+
@@ -5,6 +5,10 @@
5
5
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/)
6
6
  [![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
7
7
  [![GitHub Workflow Test)](https://img.shields.io/github/actions/workflow/status/d-chris/pathlibutil/pytest.yml?logo=github&label=pytest)](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
8
+ [![Website](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil&up_message=pdoc&logo=github&label=documentation)](https://d-chris.github.io/pathlibutil)
9
+ [![GitHub tag (with filter)](https://img.shields.io/github/v/tag/d-chris/pathlibutil?logo=github&label=github)](https://github.com/d-chris/pathlibutil)
10
+ [![Coverage](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil%2Fhtmlcov&up_message=available&down_message=missing&logo=codecov&label=coverage)](https://d-chris.github.io/pathlibutil/htmlcov)
11
+
8
12
 
9
13
  ---
10
14
 
@@ -18,6 +22,7 @@
18
22
  - `Path().copy()` copy a file or directory to a new path destination
19
23
  - `Path().delete()` delete a file or directory-tree
20
24
  - `Path().move()` move a file or directory to a new path destination
25
+ - `Path().make_archive()` create and `Path().unpack_archive()` an archive from a file or directory
21
26
 
22
27
  ## Installation
23
28
 
@@ -41,6 +46,8 @@ Read a file and print its content and some file information to stdout.
41
46
  > `Path().read_lines()`
42
47
 
43
48
  ```python
49
+ from pathlib import Path
50
+
44
51
  readme = Path('README.md')
45
52
 
46
53
  print('File content'.center(80, '='))
@@ -57,6 +64,8 @@ Write a file with md5 checksums of all python files in the pathlibutil-directory
57
64
  > `Path().hexdigest()`
58
65
 
59
66
  ```python
67
+ from pathlib import Path
68
+
60
69
  file = Path('pathlibutil.md5')
61
70
 
62
71
  algorithm = file.suffix[1:]
@@ -78,6 +87,8 @@ Read a file with md5 checksums and verify them.
78
87
  > `Path().verify()`, `Path.default_hash` and `contextmanager`
79
88
 
80
89
  ```python
90
+ from pathlib import Path
91
+
81
92
  file = Path('pathlibutil.md5')
82
93
 
83
94
  Path.default_hash = file.suffix[1:]
@@ -100,3 +111,66 @@ with file.parent as cwd:
100
111
 
101
112
  print(f'{tag.ljust(len(digest), ".")} *{filename}')
102
113
  ```
114
+
115
+ ## Example 4
116
+
117
+ Search all pycache directories and free the memory.
118
+ > `Path().delete()` and `Path().size()`
119
+
120
+ ```python
121
+ from pathlib import Path
122
+
123
+ mem = 0
124
+ i = 0
125
+
126
+ for i, cache in enumerate(Path('.').rglob('*/__pycache__/'), start=1):
127
+ cache_size = cache.size()
128
+ try:
129
+ cache.delete(recursive=True)
130
+ except OSError:
131
+ print(f'Failed to delete {cache}')
132
+ else:
133
+ mem += cache_size
134
+
135
+ print(f'{i} cache directories deleted, {mem / 2**20:.2f} MB freed.')
136
+ ```
137
+
138
+ ## Example 5
139
+
140
+ Inherit from `pathlibutil.Path` to register new a archive format.
141
+ Specify a `name` as keyword argument in the new subclass, which has to be the suffix of the archives.
142
+ Implement a classmethod `_register_archive_format()` to register new archive formats.
143
+ > `Path().make_archive()` and `Path().move()`
144
+
145
+ ```python
146
+ import shutil
147
+ import pathlibutil
148
+
149
+ class RegisterRarFormat(pathlibutil.Path, name='rar'):
150
+ @classmethod
151
+ def _register_archive_format(cls):
152
+ """
153
+ implement new register functions for given `name`
154
+ """
155
+ try:
156
+ from pyunpack import Archive
157
+ except ModuleNotFoundError:
158
+ raise ModuleNotFoundError('pip install pyunpack')
159
+ else:
160
+ shutil.register_archive_format(
161
+ 'rar', Archive, description='rar archive'
162
+ )
163
+ shutil.register_unpack_format(
164
+ 'rar', ['.rar'], Archive
165
+ )
166
+
167
+ file = pathlibutil.Path('README.md')
168
+
169
+ print(f"available archive formats: {file.archive_formats}")
170
+
171
+ archive = file.make_archive('README.rar')
172
+
173
+ backup = archive.move('./backup/')
174
+
175
+ print(f'rar archive created: {archive.name} and moved to: {backup.parent}')
176
+ ```
@@ -0,0 +1,298 @@
1
+ import errno
2
+ import hashlib
3
+ import itertools
4
+ import os
5
+ import pathlib
6
+ import shutil
7
+ import sys
8
+ from typing import Dict, Generator, List, Set
9
+
10
+
11
+ class Path(pathlib.Path):
12
+ """Path inherites from `pathlib.Path` and adds some methods to built-in python functions"""
13
+
14
+ _archive_formats: Dict[str, callable] = {}
15
+ """dict holding function to register shutil archive formats"""
16
+
17
+ default_hash = 'md5'
18
+ """default hash algorithm for the class when no algorithm is specified for `hexdigest()` and `verify()`"""
19
+
20
+ if sys.version_info < (3, 12):
21
+ _flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour
22
+
23
+ def __init_subclass__(cls, name, **kwargs) -> None:
24
+ """register archive formats from subclasses"""
25
+
26
+ super().__init_subclass__(**kwargs)
27
+
28
+ try:
29
+ cls._archive_formats[name] = getattr(
30
+ cls, '_register_archive_format'
31
+ )
32
+ except AttributeError:
33
+ pass
34
+
35
+ @property
36
+ def algorithms_available(self) -> Set[str]:
37
+ """
38
+ Set of available algorithms that can be passed to `hexdigest()` and `verify()` method.
39
+ """
40
+ return hashlib.algorithms_available
41
+
42
+ def hexdigest(self, algorithm: str = None, /, **kwargs) -> str:
43
+ """
44
+ Returns the hexdigest of the file using the named algorithm (default: `default_hash`).
45
+ """
46
+ try:
47
+ args = (kwargs.pop('length'),)
48
+ except KeyError:
49
+ args = ()
50
+
51
+ return hashlib.new(
52
+ name=algorithm or self.default_hash,
53
+ data=self.read_bytes(),
54
+ ).hexdigest(*args)
55
+
56
+ def verify(self, hexdigest: str, algorithm: str = None, *, strict: bool = True, **kwargs) -> bool:
57
+ """
58
+ Verifies the hash of the file using the named algorithm (default: `default_hash`).
59
+ """
60
+ _hash = self.hexdigest(algorithm, **kwargs)
61
+
62
+ if strict:
63
+ return _hash == hexdigest
64
+
65
+ if len(hexdigest) < 7:
66
+ raise ValueError('hashdigest must be at least 7 characters long')
67
+
68
+ for a, b in zip(_hash, hexdigest):
69
+ if a != b.lower():
70
+ return False
71
+
72
+ return True
73
+
74
+ def __enter__(self) -> 'Path':
75
+ """
76
+ Contextmanager to changes the current working directory.
77
+ """
78
+ cwd = os.getcwd()
79
+
80
+ try:
81
+ os.chdir(self)
82
+ except Exception as e:
83
+ raise e
84
+ else:
85
+ self.__stack = cwd
86
+
87
+ return self
88
+
89
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
90
+ """
91
+ Restore previous working directory.
92
+ """
93
+ try:
94
+ os.chdir(self.__stack)
95
+ finally:
96
+ del self.__stack
97
+
98
+ def read_lines(self, **kwargs) -> Generator[str, None, None]:
99
+ """
100
+ Iterates over all lines of the file until EOF is reached.
101
+ """
102
+ with self.open(**kwargs) as f:
103
+ yield from iter(f.readline, '')
104
+
105
+ def size(self, **kwargs) -> int:
106
+ """
107
+ Returns the size in bytes of a file or directory.
108
+ """
109
+ if self.is_dir():
110
+ return sum([p.size(**kwargs) for p in self.iterdir()])
111
+
112
+ return self.stat(**kwargs).st_size
113
+
114
+ def copy(self, dst: str, exist_ok: bool = True, **kwargs) -> 'Path':
115
+ """
116
+ Copies the file or directory to a destination path.
117
+ """
118
+ try:
119
+ _path = shutil.copytree(
120
+ self,
121
+ dst,
122
+ dirs_exist_ok=exist_ok,
123
+ **kwargs
124
+ )
125
+ except NotADirectoryError:
126
+ dst = Path(dst, self.name)
127
+
128
+ if not exist_ok and dst.exists():
129
+ raise FileExistsError(f'{dst} already exists')
130
+
131
+ dst.parent.mkdir(parents=True, exist_ok=True)
132
+
133
+ _path = shutil.copy2(
134
+ self,
135
+ dst,
136
+ **kwargs
137
+ )
138
+
139
+ return Path(_path)
140
+
141
+ def delete(self, *, recursive: bool = False, missing_ok: bool = False, **kwargs) -> None:
142
+ """
143
+ Deletes the file or directory.
144
+ """
145
+ try:
146
+ self.rmdir()
147
+ except NotADirectoryError:
148
+ self.unlink(missing_ok)
149
+ except FileNotFoundError as e:
150
+ if not missing_ok:
151
+ raise e
152
+ except OSError as e:
153
+ if not recursive or e.errno != errno.ENOTEMPTY:
154
+ raise e
155
+
156
+ shutil.rmtree(self, **kwargs)
157
+
158
+ def move(self, dst: str) -> 'Path':
159
+ """
160
+ Moves the file or directory to a destination path.
161
+ """
162
+ src = self.resolve(strict=True)
163
+ dst = Path(dst).resolve()
164
+ dst.mkdir(parents=True, exist_ok=True)
165
+
166
+ try:
167
+ _path = shutil.move(str(src), str(dst))
168
+ except shutil.Error as e:
169
+ raise OSError(e)
170
+
171
+ return Path(_path)
172
+
173
+ @staticmethod
174
+ def _find_archive_format(filname: 'Path') -> str:
175
+ """
176
+ Searches for a file the correct archive format.
177
+ """
178
+ ext = "".join(filname.suffixes)
179
+
180
+ for name, extensions, _ in shutil.get_unpack_formats():
181
+ if ext in extensions:
182
+ return name
183
+
184
+ return "".join(ext.split('.'))
185
+
186
+ @classmethod
187
+ def _register_format(cls, format: str) -> None:
188
+ """
189
+ Registers a archive format from `Path._register_<format>_format`.
190
+ """
191
+ try:
192
+ register_format = cls._archive_formats[format]
193
+ except KeyError:
194
+ raise ValueError(f"unknown archive format: '{format}'")
195
+ else:
196
+ register_format()
197
+
198
+ def make_archive(self, archivename: str, **kwargs) -> 'Path':
199
+ """
200
+ Creates an archive file (eg. zip) and returns the path to the archive.
201
+ """
202
+ _self = self.resolve(strict=True)
203
+ _archive = Path(archivename).resolve()
204
+ _format = kwargs.pop('format', self._find_archive_format(_archive))
205
+
206
+ _ = kwargs.pop('root_dir', None)
207
+ _ = kwargs.pop('base_dir', None)
208
+
209
+ for _ in range(2):
210
+ try:
211
+ _archive = shutil.make_archive(
212
+ base_name=_archive.parent.joinpath(_archive.stem),
213
+ format=_format,
214
+ root_dir=_self.parent,
215
+ base_dir=_self.relative_to(_self.parent),
216
+ **kwargs
217
+ )
218
+
219
+ return Path(_archive)
220
+ except ValueError:
221
+ self._register_format(_format)
222
+
223
+ def unpack_archive(self, extract_dir: str, **kwargs) -> 'Path':
224
+ """
225
+ Unpacks an archive file (eg. zip) and returns the path to the extracted files.
226
+ """
227
+
228
+ _format = kwargs.pop('format', self._find_archive_format(self))
229
+
230
+ for _ in range(2):
231
+ try:
232
+ shutil.unpack_archive(
233
+ self.resolve(strict=True),
234
+ extract_dir,
235
+ format=_format,
236
+ **kwargs
237
+ )
238
+
239
+ return Path(extract_dir)
240
+ except ValueError:
241
+ self._register_format(_format)
242
+
243
+ @property
244
+ def archive_formats(self) -> List[str]:
245
+ """
246
+ Returns a list of supported archive formats.
247
+ """
248
+ formats = itertools.chain(
249
+ self._archive_formats.keys(),
250
+ [name for name, _ in shutil.get_archive_formats()]
251
+ )
252
+
253
+ return list(formats)
254
+
255
+
256
+ class Register7zFormat(Path, name='7z'):
257
+ """
258
+ Register 7z archive format using `__init_subclass__` hook.
259
+
260
+ To register a new archive format create a subclass of `Path` and implement a `_register_archive_format()` method.
261
+
262
+ Example:
263
+ ```python
264
+ class Register7zArchive(Path, name='7z'):
265
+ @classmethod
266
+ def _register_archive_format(cls):
267
+
268
+ try:
269
+ from py7zr import pack_7zarchive, unpack_7zarchive
270
+ except ModuleNotFoundError:
271
+ raise ModuleNotFoundError('pip install pathlibutil[7z]')
272
+ else:
273
+ shutil.register_archive_format(
274
+ '7z', pack_7zarchive, description='7zip archive'
275
+ )
276
+ shutil.register_unpack_format(
277
+ '7z', ['.7z'], unpack_7zarchive
278
+ )
279
+ ```
280
+ """
281
+
282
+ @classmethod
283
+ def _register_archive_format(cls):
284
+ """
285
+ function to register 7z archive format
286
+ """
287
+
288
+ try:
289
+ from py7zr import pack_7zarchive, unpack_7zarchive
290
+ except ModuleNotFoundError:
291
+ raise ModuleNotFoundError('pip install pathlibutil[7z]')
292
+ else:
293
+ shutil.register_archive_format(
294
+ '7z', pack_7zarchive, description='7zip archive'
295
+ )
296
+ shutil.register_unpack_format(
297
+ '7z', ['.7z'], unpack_7zarchive
298
+ )
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "pathlibutil"
3
- version = "v0.1.1"
4
- description = ""
3
+ version = "v0.1.2"
4
+ description = "inherits from pathlib.Path with methods for hashing, copying, deleting and more"
5
5
  authors = ["Christoph Dörrer <d-chris@web.de>"]
6
6
  readme = "README.md"
7
7
  license = "MIT"
@@ -14,12 +14,17 @@ classifiers = [
14
14
  "License :: OSI Approved :: MIT License",
15
15
  ]
16
16
  keywords = ["pathlib", "hashlib", "shutil"]
17
+ homepage = "https://d-chris.github.io"
17
18
  repository = "https://github.com/d-chris/pathlibutil"
18
19
  documentation = "https://d-chris.github.io/pathlibutil"
19
20
  include = ["LICENSE"]
20
21
 
21
22
  [tool.poetry.dependencies]
22
23
  python = ">=3.8,<3.13"
24
+ py7zr = { version = "^0.20.2", optional = true }
25
+
26
+ [tool.poetry.extras]
27
+ 7z = ["py7zr"]
23
28
 
24
29
  [tool.poetry.group.dev.dependencies]
25
30
  pytest = "^7.4.3"
@@ -28,10 +33,9 @@ pytest-random-order = "^1.1.0"
28
33
  pytest-cov = "^4.1.0"
29
34
  pytest-mock = "^3.12.0"
30
35
 
31
-
32
36
  [tool.poetry.group.doc.dependencies]
33
- jinja2 = "^3.1.2"
34
37
  pdoc = "^14.3.0"
38
+ click = "^8.1.7"
35
39
 
36
40
  [[tool.poetry.source]]
37
41
  name = "PyPI"
@@ -46,28 +50,14 @@ priority = "explicit"
46
50
  requires = ["poetry-core"]
47
51
  build-backend = "poetry.core.masonry.api"
48
52
 
49
- [tool.tox]
50
- legacy_tox_ini = """
51
- [tox]
52
- envlist = py38,py39,py310,py311,py312
53
-
54
- [testenv]
55
- deps =
56
- pytest
57
- pytest-random-order
58
- pytest-mock
59
- pytest-cov
60
- commands = pytest --random-order
61
- """
62
-
63
53
  [tool.pytest.ini_options]
64
54
  minversion = "6.0"
65
55
  testpaths = "tests"
66
56
  addopts = [
67
57
  "--random-order",
68
58
  "--color=yes",
69
- "--cov=pathlibutil",
70
- "--cov-report=term-missing:skip-covered",
71
- "--cov-append",
59
+ # "--cov=pathlibutil",
60
+ # "--cov-report=term-missing:skip-covered",
61
+ # "--cov-append",
72
62
  # "--cov-report=html",
73
63
  ]
@@ -1,155 +0,0 @@
1
- import errno
2
- import hashlib
3
- import os
4
- import pathlib
5
- import shutil
6
- import sys
7
- from typing import Generator, Set
8
-
9
-
10
- class Path(pathlib.Path):
11
- """Path inherites from `pathlib.Path` and adds some methods to built-in python functions"""
12
-
13
- default_hash = 'md5'
14
- """default hash algorithm for the class when no algorithm is specified for `hexdigest()` and `verify()`"""
15
-
16
- if sys.version_info < (3, 12):
17
- _flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour
18
-
19
- @property
20
- def algorithms_available(self) -> Set[str]:
21
- """
22
- Set of available algorithms that can be passed to `hexdigest()` and `verify()` method.
23
- """
24
- return hashlib.algorithms_available
25
-
26
- def hexdigest(self, algorithm: str = None, /, **kwargs) -> str:
27
- """
28
- Returns the hexdigest of the file using the named algorithm (default: `default_hash`).
29
- """
30
- try:
31
- args = (kwargs.pop('length'),)
32
- except KeyError:
33
- args = ()
34
-
35
- return hashlib.new(
36
- name=algorithm or self.default_hash,
37
- data=self.read_bytes(),
38
- ).hexdigest(*args)
39
-
40
- def verify(self, hexdigest: str, algorithm: str = None, *, strict: bool = True, **kwargs) -> bool:
41
- """
42
- Verifies the hash of the file using the named algorithm (default: `default_hash`).
43
- """
44
- _hash = self.hexdigest(algorithm, **kwargs)
45
-
46
- if strict:
47
- return _hash == hexdigest
48
-
49
- if len(hexdigest) < 7:
50
- raise ValueError('hashdigest must be at least 7 characters long')
51
-
52
- for a, b in zip(_hash, hexdigest):
53
- if a != b.lower():
54
- return False
55
-
56
- return True
57
-
58
- def __enter__(self) -> 'Path':
59
- """
60
- Contextmanager to changes the current working directory.
61
- """
62
- cwd = os.getcwd()
63
-
64
- try:
65
- os.chdir(self)
66
- except Exception as e:
67
- raise e
68
- else:
69
- self.__stack = cwd
70
-
71
- return self
72
-
73
- def __exit__(self, exc_type, exc_val, exc_tb) -> None:
74
- """
75
- Restore previous working directory.
76
- """
77
- try:
78
- os.chdir(self.__stack)
79
- finally:
80
- del self.__stack
81
-
82
- def read_lines(self, **kwargs) -> Generator[str, None, None]:
83
- """
84
- Iterates over all lines of the file until EOF is reached.
85
- """
86
- with self.open(**kwargs) as f:
87
- yield from iter(f.readline, '')
88
-
89
- def size(self, **kwargs) -> int:
90
- """
91
- Returns the size in bytes of a file or directory.
92
- """
93
- if self.is_dir():
94
- return sum([p.size(**kwargs) for p in self.iterdir()])
95
-
96
- return self.stat(**kwargs).st_size
97
-
98
- def copy(self, dst: str, exist_ok: bool = True, **kwargs) -> 'Path':
99
- """
100
- Copies the file or directory to a destination path.
101
- """
102
- try:
103
- _path = shutil.copytree(
104
- self,
105
- dst,
106
- dirs_exist_ok=exist_ok,
107
- **kwargs
108
- )
109
- except NotADirectoryError:
110
- dst = Path(dst, self.name)
111
-
112
- if not exist_ok and dst.exists():
113
- raise FileExistsError(f'{dst} already exists')
114
-
115
- dst.parent.mkdir(parents=True, exist_ok=True)
116
-
117
- _path = shutil.copy2(
118
- self,
119
- dst,
120
- **kwargs
121
- )
122
-
123
- return Path(_path)
124
-
125
- def delete(self, *, recursive: bool = False, missing_ok: bool = False, **kwargs) -> None:
126
- """
127
- Deletes the file or directory.
128
- """
129
- try:
130
- self.rmdir()
131
- except NotADirectoryError:
132
- self.unlink(missing_ok)
133
- except FileNotFoundError as e:
134
- if not missing_ok:
135
- raise e
136
- except OSError as e:
137
- if not recursive or e.errno != errno.ENOTEMPTY:
138
- raise e
139
-
140
- shutil.rmtree(self, **kwargs)
141
-
142
- def move(self, dst: str) -> 'Path':
143
- """
144
- Moves the file or directory to a destination path.
145
- """
146
- src = self.resolve(strict=True)
147
- dst = Path(dst).resolve()
148
- dst.mkdir(parents=True, exist_ok=True)
149
-
150
- try:
151
- _path = shutil.move(str(src), str(dst))
152
- except shutil.Error as e:
153
- raise OSError(e)
154
-
155
- return Path(_path)
File without changes