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.
- {pathlibutil-0.1.1 → pathlibutil-0.1.2}/PKG-INFO +79 -3
- {pathlibutil-0.1.1 → pathlibutil-0.1.2}/README.md +74 -0
- pathlibutil-0.1.2/pathlibutil/path.py +298 -0
- {pathlibutil-0.1.1 → pathlibutil-0.1.2}/pyproject.toml +11 -21
- pathlibutil-0.1.1/pathlibutil/path.py +0 -155
- {pathlibutil-0.1.1 → pathlibutil-0.1.2}/LICENSE +0 -0
- {pathlibutil-0.1.1 → pathlibutil-0.1.2}/pathlibutil/__init__.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pathlibutil
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
5
|
-
Home-page: https://
|
|
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
|
[](https://pypi.org/project/pathlibutil/)
|
|
27
29
|
[](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
|
|
28
30
|
[](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
|
|
31
|
+
[](https://d-chris.github.io/pathlibutil)
|
|
32
|
+
[](https://github.com/d-chris/pathlibutil)
|
|
33
|
+
[](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
|
[](https://pypi.org/project/pathlibutil/)
|
|
6
6
|
[](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
|
|
7
7
|
[](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
|
|
8
|
+
[](https://d-chris.github.io/pathlibutil)
|
|
9
|
+
[](https://github.com/d-chris/pathlibutil)
|
|
10
|
+
[](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.
|
|
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
|
|
File without changes
|