pathlibutil 0.0.5__tar.gz → 0.1.0__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.
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
@@ -0,0 +1,77 @@
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
+
@@ -0,0 +1,56 @@
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`
@@ -0,0 +1 @@
1
+ from pathlibutil.path import Path
@@ -0,0 +1,91 @@
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
@@ -0,0 +1,50 @@
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,17 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: pathlibutil
3
- Version: 0.0.5
4
- Summary: extend pathlib with some handy features
5
- Author: Christoph Dörrer
6
- Project-URL: Homepage, https://github.com/d-chris/pathutil
7
- Project-URL: Bug Tracker, https://github.com/d-chris/pathutil/issues
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.11
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
-
15
- # readme
16
-
17
- > [MIT License](LICENSE)
@@ -1,3 +0,0 @@
1
- # readme
2
-
3
- > [MIT License](LICENSE)
@@ -1,27 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "pathlibutil"
7
- version = "0.0.5"
8
- authors = [{ name = "Christoph Dörrer" }]
9
- description = "extend pathlib with some handy features"
10
- readme = "README.md"
11
- requires-python = ">=3.11"
12
- classifiers = [
13
- "Programming Language :: Python :: 3",
14
- "License :: OSI Approved :: MIT License",
15
- "Operating System :: OS Independent",
16
- ]
17
-
18
- [project.urls]
19
- "Homepage" = "https://github.com/d-chris/pathutil"
20
- "Bug Tracker" = "https://github.com/d-chris/pathutil/issues"
21
-
22
- [tool.pytest.ini_options]
23
- minversion = "6.0"
24
- pythonpath = "src"
25
- testpaths = "test"
26
- addopts = "--random-order -v -s"
27
- # addopts = "--cov=src --cov-report=html:test/report"
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
@@ -1,2 +0,0 @@
1
- from .pathlist import PathList
2
- from .pathutil import Path
@@ -1,69 +0,0 @@
1
- import functools
2
- import hashlib
3
-
4
- from .pathlist import PathList as _PathList
5
- from .pathutil import Path as _Path
6
-
7
-
8
- def cache(func):
9
- @functools.wraps(func)
10
- def cached(self, *args, **kwargs):
11
- try:
12
- lock = (self.mtime, self)
13
- except AttributeError:
14
- lock = self
15
-
16
- try:
17
- func_cache = self.__cache__[lock]
18
- except (AttributeError, KeyError):
19
- func_cache = dict()
20
- self.__cache__ = {lock: func_cache}
21
-
22
- try:
23
- args_cache = func_cache[func.__name__]
24
- except KeyError:
25
- args_cache = dict()
26
- self.__cache__[lock][func.__name__] = args_cache
27
-
28
- # key = args + tuple(sorted(kwargs.items()))
29
- key = args
30
- try:
31
- value = args_cache[key]
32
- except KeyError:
33
- value = func(self, *args, **kwargs)
34
- args_cache[key] = value
35
-
36
- return value
37
-
38
- return cached
39
-
40
-
41
- class Path(_Path):
42
-
43
- def cached(self, func: str = None) -> dict:
44
- try:
45
- cache, *_ = self.__cache__.values()
46
-
47
- if func:
48
- return cache[func]
49
- else:
50
- return cache
51
- except (AttributeError, KeyError) as e:
52
- return dict()
53
-
54
- @cache
55
- def _count(self, substr: str, /, *, size: int) -> int:
56
- return super()._count(substr, size=size)
57
-
58
- @cache
59
- def _file_digest(self, algorithm: str, /, *, _bufsize: int) -> 'hashlib._Hash':
60
- return super()._file_digest(algorithm, _bufsize=_bufsize)
61
-
62
-
63
- class PathList(_PathList):
64
- @staticmethod
65
- def Path(item):
66
- if isinstance(item, Path):
67
- return item
68
-
69
- return Path(item)
@@ -1,222 +0,0 @@
1
- import re
2
- from typing import Dict, Generator, Iterable, List, Self, Tuple
3
-
4
- from .pathlist import PathList
5
- from .pathutil import Path
6
-
7
-
8
- class HashList:
9
- def __init__(self, files: str, algorithm: str = None):
10
- self.files = PathList(files)
11
- self.algorithm = Path.algorithm(algorithm)
12
-
13
- @property
14
- def filedigest(self) -> Dict[Path, str]:
15
- try:
16
- return self._filedigest
17
- except AttributeError:
18
- self._filedigest = dict(zip(self.files, self.hexdigest))
19
-
20
- return self._filedigest
21
-
22
- @property
23
- def hexdigest(self) -> List[str]:
24
- try:
25
- return self._hexdigest
26
- except AttributeError:
27
- def digest(x: Path):
28
- try:
29
- return x.hexdigest(self.algorithm).upper()
30
- except (FileNotFoundError, PermissionError):
31
- return None
32
-
33
- self._hexdigest = self.files.apply(digest)
34
-
35
- return self._hexdigest
36
-
37
- def missing(self):
38
- for file, hash in self:
39
- if hash:
40
- continue
41
-
42
- yield file
43
-
44
- def __iter__(self):
45
- for item in self.filedigest.items():
46
- yield item
47
-
48
- def __getitem__(self, item: Path) -> str:
49
- return self.filedigest[item]
50
-
51
- def __len__(self):
52
- return len(self.filedigest)
53
-
54
- def __str__(self):
55
- return str(self.filedigest)
56
-
57
- def __repr__(self):
58
- return f"{self.__class__.__name__}({[str(f) for f in self.files]}, algorithm='{self.algorithm}')"
59
-
60
-
61
- class HashSum(HashList):
62
- def __init__(self, files: Iterable, hashfile: str, algorithm: str = None, comments: str = None, relative: bool = False):
63
-
64
- self.root = Path(hashfile)
65
-
66
- if not algorithm:
67
- algorithm = self.root.suffix
68
-
69
- super().__init__(files, algorithm)
70
-
71
- self.comments = comments
72
-
73
- self.save(self.root, relative=relative)
74
-
75
- def __repr__(self):
76
- files = [str(f) for f in self.files]
77
- comment = str('\n').join(self.comments)
78
- return f"{self.__class__.__name__}({files}, '{self.root}', algorithm='{self.algorithm}', comments='{comment}')"
79
-
80
- @staticmethod
81
- def strip_comments(comment: str) -> str:
82
- return comment.lstrip('# ')
83
-
84
- @classmethod
85
- def split_comments(cls, comments: str) -> List[str]:
86
- try:
87
- return [cls.strip_comments(line) for line in comments.split('\n')]
88
- except AttributeError:
89
- return list()
90
-
91
- @property
92
- def comments(self) -> List[str]:
93
- return self._comments
94
-
95
- @comments.setter
96
- def comments(self, comments: str):
97
- self._comments = self.split_comments(comments)
98
-
99
- def items(self) -> Tuple[Path, str]:
100
- for file, hash in self:
101
- yield file, hash
102
-
103
- def save(self, filename: str, comments: str = None, relative: bool = False) -> None:
104
- if not all(self.hexdigest):
105
- raise FileNotFoundError(list(self.missing()))
106
-
107
- self.root = Path(filename).resolve().with_suffix(
108
- self.algorithm, separator=True)
109
-
110
- if not comments:
111
- comments = self.comments
112
- else:
113
- comments = self.split_comments(comments)
114
-
115
- with self.root.open(mode='wt', encoding='utf-8') as f:
116
-
117
- if comments:
118
- for line in comments:
119
- f.write(f"# {line}\n")
120
-
121
- f.write('\n')
122
-
123
- for filename, hash in self.items():
124
- filename = filename.resolve()
125
-
126
- try:
127
- filename = filename.relative_to(
128
- self.root.parent, uptree=relative)
129
- except ValueError:
130
- pass
131
-
132
- f.write(f"{hash} *{filename}\n")
133
-
134
-
135
- class HashFile(HashSum):
136
- regex = re.compile(
137
- r'^(?P<hash>[0-9a-f]{8,}) \*(?P<file>.*?)$', re.IGNORECASE)
138
-
139
- def __init__(self, filename: str, algorithm: str = None):
140
- self._comments = list()
141
-
142
- files = list()
143
- self._hashes = list()
144
-
145
- self.root = Path(filename).resolve()
146
-
147
- if not algorithm:
148
- algorithm = self.root.suffix
149
-
150
- for line in self.root.iter_lines(encoding='utf-8'):
151
- if not line:
152
- continue
153
-
154
- if line.startswith('#'):
155
- self._comments.append(self.strip_comments(line))
156
- continue
157
-
158
- match = self.regex.match(line)
159
-
160
- try:
161
- file = Path(match.group('file'))
162
- hash = match.group('hash')
163
- except (AttributeError, KeyError):
164
- continue
165
-
166
- if not file.is_absolute():
167
- file = self.root.parent.joinpath(file).resolve()
168
-
169
- files.append(file)
170
- self._hashes.append(hash)
171
-
172
- super(HashSum, self).__init__(files, algorithm)
173
-
174
- def __repr__(self):
175
- return f"{self.__class__.__name__}('{self.root}', algorithm='{self.algorithm}')"
176
-
177
- @property
178
- def hashes(self) -> List[str]:
179
- return self._hashes
180
-
181
- @property
182
- def filedigest(self) -> Dict[Path, Tuple[str, str]]:
183
- try:
184
- return self._filedigest
185
- except AttributeError:
186
- self._filedigest = dict(
187
- zip(self.files, tuple(zip(self.hashes, self.hexdigest)))
188
- )
189
-
190
- return self._filedigest
191
-
192
- def __getitem__(self, item: Path) -> str:
193
- _, digest = self.filedigest[item]
194
-
195
- return digest
196
-
197
- def items(self) -> Tuple[Path, str]:
198
- for file, (_, digest) in self:
199
- yield file, digest
200
-
201
- def missing(self):
202
- for file, (_, digest) in self:
203
- if digest:
204
- continue
205
-
206
- yield file
207
-
208
- def match(self):
209
- for file, (hash, digest) in self:
210
- if not digest:
211
- continue
212
-
213
- if hash == digest:
214
- yield file
215
-
216
- def modified(self):
217
- for file, (hash, digest) in self:
218
- if not digest:
219
- continue
220
-
221
- if hash != digest:
222
- yield file