pathlibutil 0.1.0__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,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2022 Christoph Dörrer
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Christoph Dörrer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.1
2
+ Name: 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
+ License: MIT
7
+ Keywords: pathlib,hashlib,shutil
8
+ Author: Christoph Dörrer
9
+ Author-email: d-chris@web.de
10
+ Requires-Python: >=3.8,<3.13
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Provides-Extra: 7z
19
+ Requires-Dist: py7zr (>=0.20.2,<0.21.0) ; extra == "7z"
20
+ Project-URL: Documentation, https://d-chris.github.io/pathlibutil
21
+ Project-URL: Repository, https://github.com/d-chris/pathlibutil
22
+ Description-Content-Type: text/markdown
23
+
24
+ # pathlibutil
25
+
26
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pathlibutil)](https://pypi.org/project/pathlibutil/)
27
+ [![PyPI](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/)
28
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/)
29
+ [![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
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
+
35
+
36
+ ---
37
+
38
+ `pathlibutil.Path` inherits from `pathlib.Path` with some useful built-in python functions.
39
+
40
+ - `Path().hexdigest()` to calculate and `Path().verify()` for verification of hexdigest from a file
41
+ - `Path.default_hash` to configurate default hash algorithm for `Path` class (default: *'md5'*)
42
+ - `Path().size()` to get size in bytes of a file or directory
43
+ - `Path().read_lines()` to yield over all lines from a file until EOF
44
+ - `contextmanager` to change current working directory with `with` statement
45
+ - `Path().copy()` copy a file or directory to a new path destination
46
+ - `Path().delete()` delete a file or directory-tree
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
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install pathlibutil
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ```python
59
+ from pathlibutil import Path
60
+
61
+ readme = Path('README.md')
62
+
63
+ print(f'File size: {readme.size()} Bytes')
64
+ ```
65
+
66
+ ## Example 1
67
+
68
+ Read a file and print its content and some file information to stdout.
69
+ > `Path().read_lines()`
70
+
71
+ ```python
72
+ from pathlib import Path
73
+
74
+ readme = Path('README.md')
75
+
76
+ print('File content'.center(80, '='))
77
+
78
+ for line in readme.read_lines(encoding='utf-8'):
79
+ print(line, end='')
80
+
81
+ print('EOF'.center(80, '='))
82
+ ```
83
+
84
+ ## Example 2
85
+
86
+ Write a file with md5 checksums of all python files in the pathlibutil-directory.
87
+ > `Path().hexdigest()`
88
+
89
+ ```python
90
+ from pathlib import Path
91
+
92
+ file = Path('pathlibutil.md5')
93
+
94
+ algorithm = file.suffix[1:]
95
+
96
+ with file.open('w') as f:
97
+ f.write(
98
+ f'# {algorithm} checksums generated with pathlibutil (https://pypi.org/project/pathlibutil/)\n\n')
99
+
100
+ i = 0
101
+ for i, filename in enumerate(Path('./pathlibutil').glob('*.py'), start=1):
102
+ f.write(f'{filename.hexdigest(algorithm)} *{filename}\n')
103
+
104
+ print(f'\nwritten: {i:>5} {algorithm}-hashes to: {file}')
105
+ ```
106
+
107
+ ## Example 3
108
+
109
+ Read a file with md5 checksums and verify them.
110
+ > `Path().verify()`, `Path.default_hash` and `contextmanager`
111
+
112
+ ```python
113
+ from pathlib import Path
114
+
115
+ file = Path('pathlibutil.md5')
116
+
117
+ Path.default_hash = file.suffix[1:]
118
+
119
+ def no_comment(line: str) -> bool:
120
+ return not line.startswith('#')
121
+
122
+ with file.parent as cwd:
123
+
124
+ for line in filter(no_comment, file.read_lines()):
125
+ try:
126
+ digest, filename = line.strip().split(' *')
127
+ verification = Path(filename).verify(digest)
128
+ except ValueError as split_failed:
129
+ continue
130
+ except FileNotFoundError as verify_failed:
131
+ tag = 'missing'
132
+ else:
133
+ tag = 'ok' if verification else 'fail
134
+
135
+ print(f'{tag.ljust(len(digest), ".")} *{filename}')
136
+ ```
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
+
@@ -0,0 +1,176 @@
1
+ # pathlibutil
2
+
3
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pathlibutil)](https://pypi.org/project/pathlibutil/)
4
+ [![PyPI](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/)
5
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/)
6
+ [![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
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
+
12
+
13
+ ---
14
+
15
+ `pathlibutil.Path` inherits from `pathlib.Path` with some useful built-in python functions.
16
+
17
+ - `Path().hexdigest()` to calculate and `Path().verify()` for verification of hexdigest from a file
18
+ - `Path.default_hash` to configurate default hash algorithm for `Path` class (default: *'md5'*)
19
+ - `Path().size()` to get size in bytes of a file or directory
20
+ - `Path().read_lines()` to yield over all lines from a file until EOF
21
+ - `contextmanager` to change current working directory with `with` statement
22
+ - `Path().copy()` copy a file or directory to a new path destination
23
+ - `Path().delete()` delete a file or directory-tree
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
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install pathlibutil
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ from pathlibutil import Path
37
+
38
+ readme = Path('README.md')
39
+
40
+ print(f'File size: {readme.size()} Bytes')
41
+ ```
42
+
43
+ ## Example 1
44
+
45
+ Read a file and print its content and some file information to stdout.
46
+ > `Path().read_lines()`
47
+
48
+ ```python
49
+ from pathlib import Path
50
+
51
+ readme = Path('README.md')
52
+
53
+ print('File content'.center(80, '='))
54
+
55
+ for line in readme.read_lines(encoding='utf-8'):
56
+ print(line, end='')
57
+
58
+ print('EOF'.center(80, '='))
59
+ ```
60
+
61
+ ## Example 2
62
+
63
+ Write a file with md5 checksums of all python files in the pathlibutil-directory.
64
+ > `Path().hexdigest()`
65
+
66
+ ```python
67
+ from pathlib import Path
68
+
69
+ file = Path('pathlibutil.md5')
70
+
71
+ algorithm = file.suffix[1:]
72
+
73
+ with file.open('w') as f:
74
+ f.write(
75
+ f'# {algorithm} checksums generated with pathlibutil (https://pypi.org/project/pathlibutil/)\n\n')
76
+
77
+ i = 0
78
+ for i, filename in enumerate(Path('./pathlibutil').glob('*.py'), start=1):
79
+ f.write(f'{filename.hexdigest(algorithm)} *{filename}\n')
80
+
81
+ print(f'\nwritten: {i:>5} {algorithm}-hashes to: {file}')
82
+ ```
83
+
84
+ ## Example 3
85
+
86
+ Read a file with md5 checksums and verify them.
87
+ > `Path().verify()`, `Path.default_hash` and `contextmanager`
88
+
89
+ ```python
90
+ from pathlib import Path
91
+
92
+ file = Path('pathlibutil.md5')
93
+
94
+ Path.default_hash = file.suffix[1:]
95
+
96
+ def no_comment(line: str) -> bool:
97
+ return not line.startswith('#')
98
+
99
+ with file.parent as cwd:
100
+
101
+ for line in filter(no_comment, file.read_lines()):
102
+ try:
103
+ digest, filename = line.strip().split(' *')
104
+ verification = Path(filename).verify(digest)
105
+ except ValueError as split_failed:
106
+ continue
107
+ except FileNotFoundError as verify_failed:
108
+ tag = 'missing'
109
+ else:
110
+ tag = 'ok' if verification else 'fail
111
+
112
+ print(f'{tag.ljust(len(digest), ".")} *{filename}')
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,5 @@
1
+ """
2
+ .. include:: ../README.md
3
+ """
4
+
5
+ from pathlibutil.path import Path
@@ -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,50 +1,63 @@
1
- [tool.poetry]
2
- name = "pathlibutil"
3
- version = "0.1.0"
4
- description = ""
5
- authors = ["Christoph Dörrer <d-chris@web.de>"]
6
- readme = "README.md"
7
- license = "MIT"
8
- classifiers = [
9
- "Programming Language :: Python :: 3.8",
10
- "Programming Language :: Python :: 3.9",
11
- "Programming Language :: Python :: 3.10",
12
- "Programming Language :: Python :: 3.11",
13
- "Programming Language :: Python :: 3.12",
14
- "License :: OSI Approved :: MIT License",
15
- ]
16
- keywords = ["pathlib", "hashlib"]
17
- repository = "https://github.com/d-chris/pathlibutil"
18
-
19
- [tool.poetry.dependencies]
20
- python = ">=3.8,<3.13"
21
-
22
- [tool.poetry.group.dev.dependencies]
23
- pytest = "^7.4.3"
24
- tox = "^4.11.4"
25
- pytest-random-order = "^1.1.0"
26
- pytest-cov = "^4.1.0"
27
- pytest-mock = "^3.12.0"
28
-
29
- [build-system]
30
- requires = ["poetry-core"]
31
- build-backend = "poetry.core.masonry.api"
32
-
33
- [tool.tox]
34
- legacy_tox_ini = """
35
- [tox]
36
- envlist = py38,py39,py310,py311,py312
37
-
38
- [testenv]
39
- deps =
40
- pytest
41
- pytest-random-order
42
- pytest-mock
43
- pytest-cov
44
- commands = pytest --random-order
45
- """
46
-
47
- [tool.pytest.ini_options]
48
- minversion = "6.0"
49
- testpaths = "tests"
50
- addopts = "--random-order --cov=pathlibutil --cov-report=term-missing:skip-covered --color=yes"
1
+ [tool.poetry]
2
+ name = "pathlibutil"
3
+ version = "v0.1.2"
4
+ description = "inherits from pathlib.Path with methods for hashing, copying, deleting and more"
5
+ authors = ["Christoph Dörrer <d-chris@web.de>"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ classifiers = [
9
+ "Programming Language :: Python :: 3.8",
10
+ "Programming Language :: Python :: 3.9",
11
+ "Programming Language :: Python :: 3.10",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "License :: OSI Approved :: MIT License",
15
+ ]
16
+ keywords = ["pathlib", "hashlib", "shutil"]
17
+ homepage = "https://d-chris.github.io"
18
+ repository = "https://github.com/d-chris/pathlibutil"
19
+ documentation = "https://d-chris.github.io/pathlibutil"
20
+ include = ["LICENSE"]
21
+
22
+ [tool.poetry.dependencies]
23
+ python = ">=3.8,<3.13"
24
+ py7zr = { version = "^0.20.2", optional = true }
25
+
26
+ [tool.poetry.extras]
27
+ 7z = ["py7zr"]
28
+
29
+ [tool.poetry.group.dev.dependencies]
30
+ pytest = "^7.4.3"
31
+ tox = "^4.11.4"
32
+ pytest-random-order = "^1.1.0"
33
+ pytest-cov = "^4.1.0"
34
+ pytest-mock = "^3.12.0"
35
+
36
+ [tool.poetry.group.doc.dependencies]
37
+ pdoc = "^14.3.0"
38
+ click = "^8.1.7"
39
+
40
+ [[tool.poetry.source]]
41
+ name = "PyPI"
42
+ priority = "primary"
43
+
44
+ [[tool.poetry.source]]
45
+ name = "testpypi"
46
+ url = "https://test.pypi.org/legacy/"
47
+ priority = "explicit"
48
+
49
+ [build-system]
50
+ requires = ["poetry-core"]
51
+ build-backend = "poetry.core.masonry.api"
52
+
53
+ [tool.pytest.ini_options]
54
+ minversion = "6.0"
55
+ testpaths = "tests"
56
+ addopts = [
57
+ "--random-order",
58
+ "--color=yes",
59
+ # "--cov=pathlibutil",
60
+ # "--cov-report=term-missing:skip-covered",
61
+ # "--cov-append",
62
+ # "--cov-report=html",
63
+ ]
@@ -1,77 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: pathlibutil
3
- Version: 0.1.0
4
- Summary:
5
- Home-page: https://github.com/d-chris/pathlibutil
6
- License: MIT
7
- Keywords: pathlib,hashlib
8
- Author: Christoph Dörrer
9
- Author-email: d-chris@web.de
10
- Requires-Python: >=3.8,<3.13
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Project-URL: Repository, https://github.com/d-chris/pathlibutil
19
- Description-Content-Type: text/markdown
20
-
21
- # pathlibutil
22
-
23
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pathlibutil)](https://pypi.org/project/pathlibutil/)
24
- [![PyPI](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/)
25
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/)
26
- [![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](./LICENSE)
27
- [![GitHub Workflow Test)](https://img.shields.io/github/actions/workflow/status/d-chris/pathlibutil/pytest.yml?logo=github&label=test)](https://github.com/d-chris/pathlibutil)
28
-
29
- ---
30
-
31
- `pathlibutil.Path` inherits from `pathlib.Path` with some useful built-in python functions.
32
-
33
- - `Path().hexdigest()` to calculate and `Path().verify()` for verification of hexdigest from a file
34
- - `Path.default_hash` to configurate default hash algorithm for `Path` class (default: *'md5'*)
35
- - `Path().size()` to get size in bytes of a file or directory
36
- - `Path().read_lines()` to yield over all lines from a file until EOF
37
- - `contextmanager` to change current working directory with `with` statement
38
-
39
- ## Installation
40
-
41
- ```bash
42
- pip install pathlibutil
43
- ```
44
-
45
- ## Usage
46
-
47
- ```python
48
- from pathlibutil import Path
49
-
50
- readme = Path('README.md')
51
-
52
- print(f'File size: {readme.size()} Bytes')
53
- print(f'File sha1: {readme.hexdigest("sha1")}')
54
-
55
- print('-- File content --')
56
- for line in readme.read_lines(encoding='utf-8'):
57
- print(line, end='')
58
- print('-- EOF --')
59
-
60
- with readme.parent as cwd:
61
- print(f'Current working directory: {cwd}')
62
-
63
- # Change default hash algorithm from md5 to sha1
64
- Path.default_hash = 'sha1'
65
-
66
- print(f'File verification: {readme.verify("add3f48fded5e0829a8e3e025e44c2891542c58e")}')
67
- ```
68
-
69
- ## Examples
70
-
71
- 1. [Read file line by line to stdout](./examples/example1.py)
72
- > `Path().read_lines()`
73
- 2. [Write calculated hash to file](./examples/example2.py)
74
- > `Path().hexdigest()`
75
- 3. [Read hashes from file for verification](./examples/example3.py)
76
- > `Path().verify()` and `contextmanager`
77
-
@@ -1,56 +0,0 @@
1
- # pathlibutil
2
-
3
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pathlibutil)](https://pypi.org/project/pathlibutil/)
4
- [![PyPI](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/)
5
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/)
6
- [![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](./LICENSE)
7
- [![GitHub Workflow Test)](https://img.shields.io/github/actions/workflow/status/d-chris/pathlibutil/pytest.yml?logo=github&label=test)](https://github.com/d-chris/pathlibutil)
8
-
9
- ---
10
-
11
- `pathlibutil.Path` inherits from `pathlib.Path` with some useful built-in python functions.
12
-
13
- - `Path().hexdigest()` to calculate and `Path().verify()` for verification of hexdigest from a file
14
- - `Path.default_hash` to configurate default hash algorithm for `Path` class (default: *'md5'*)
15
- - `Path().size()` to get size in bytes of a file or directory
16
- - `Path().read_lines()` to yield over all lines from a file until EOF
17
- - `contextmanager` to change current working directory with `with` statement
18
-
19
- ## Installation
20
-
21
- ```bash
22
- pip install pathlibutil
23
- ```
24
-
25
- ## Usage
26
-
27
- ```python
28
- from pathlibutil import Path
29
-
30
- readme = Path('README.md')
31
-
32
- print(f'File size: {readme.size()} Bytes')
33
- print(f'File sha1: {readme.hexdigest("sha1")}')
34
-
35
- print('-- File content --')
36
- for line in readme.read_lines(encoding='utf-8'):
37
- print(line, end='')
38
- print('-- EOF --')
39
-
40
- with readme.parent as cwd:
41
- print(f'Current working directory: {cwd}')
42
-
43
- # Change default hash algorithm from md5 to sha1
44
- Path.default_hash = 'sha1'
45
-
46
- print(f'File verification: {readme.verify("add3f48fded5e0829a8e3e025e44c2891542c58e")}')
47
- ```
48
-
49
- ## Examples
50
-
51
- 1. [Read file line by line to stdout](./examples/example1.py)
52
- > `Path().read_lines()`
53
- 2. [Write calculated hash to file](./examples/example2.py)
54
- > `Path().hexdigest()`
55
- 3. [Read hashes from file for verification](./examples/example3.py)
56
- > `Path().verify()` and `contextmanager`
@@ -1 +0,0 @@
1
- from pathlibutil.path import Path
@@ -1,91 +0,0 @@
1
- import hashlib
2
- import os
3
- import pathlib
4
- import sys
5
- from typing import Generator, Set
6
-
7
-
8
- class Path(pathlib.Path):
9
- default_hash = 'md5'
10
-
11
- if sys.version_info < (3, 12):
12
- _flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour
13
-
14
- @property
15
- def algorithms_available(self) -> Set[str]:
16
- """
17
- Set of available algorithms that can be passed to hexdigest() method.
18
- """
19
- return hashlib.algorithms_available
20
-
21
- def hexdigest(self, algorithm: str = None, /, **kwargs) -> str:
22
- """
23
- Returns the hex digest of the file using the named algorithm (default: md5).
24
- """
25
- try:
26
- args = (kwargs.pop('length'),)
27
- except KeyError:
28
- args = ()
29
-
30
- return hashlib.new(
31
- name=algorithm or self.default_hash,
32
- data=self.read_bytes(),
33
- ).hexdigest(*args)
34
-
35
- def verify(self, hashdigest: str, algorithm: str = None, *, strict: bool = True, **kwargs) -> bool:
36
- """
37
- Verifies the hash of the file using the named algorithm (default: md5).
38
- """
39
- _hash = self.hexdigest(algorithm, **kwargs)
40
-
41
- if strict:
42
- return _hash == hashdigest
43
-
44
- if len(hashdigest) < 7:
45
- raise ValueError('hashdigest must be at least 7 characters long')
46
-
47
- for a, b in zip(_hash, hashdigest):
48
- if a != b.lower():
49
- return False
50
-
51
- return True
52
-
53
- def __enter__(self) -> 'Path':
54
- """
55
- Contextmanager to changes the current working directory.
56
- """
57
- cwd = os.getcwd()
58
-
59
- try:
60
- os.chdir(self)
61
- except Exception as e:
62
- raise e
63
- else:
64
- self.__stack = cwd
65
-
66
- return self
67
-
68
- def __exit__(self, exc_type, exc_val, exc_tb) -> None:
69
- """
70
- Restore previous working directory.
71
- """
72
- try:
73
- os.chdir(self.__stack)
74
- finally:
75
- del self.__stack
76
-
77
- def read_lines(self, **kwargs) -> Generator[str, None, None]:
78
- """
79
- Iterates over all lines of the file until EOF is reached.
80
- """
81
- with self.open(**kwargs) as f:
82
- yield from iter(f.readline, '')
83
-
84
- def size(self, **kwargs) -> int:
85
- """
86
- Returns the size in bytes of a file or directory.
87
- """
88
- if self.is_dir():
89
- return sum([p.size(**kwargs) for p in self.iterdir()])
90
-
91
- return self.stat(**kwargs).st_size