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.
- {pathlibutil-0.0.5 → pathlibutil-0.1.0}/LICENSE +1 -1
- pathlibutil-0.1.0/PKG-INFO +77 -0
- pathlibutil-0.1.0/README.md +56 -0
- pathlibutil-0.1.0/pathlibutil/__init__.py +1 -0
- pathlibutil-0.1.0/pathlibutil/path.py +91 -0
- pathlibutil-0.1.0/pyproject.toml +50 -0
- pathlibutil-0.0.5/PKG-INFO +0 -17
- pathlibutil-0.0.5/README.md +0 -3
- pathlibutil-0.0.5/pyproject.toml +0 -27
- pathlibutil-0.0.5/setup.cfg +0 -4
- pathlibutil-0.0.5/src/pathlibutil/__init__.py +0 -2
- pathlibutil-0.0.5/src/pathlibutil/cached.py +0 -69
- pathlibutil-0.0.5/src/pathlibutil/hashing.py +0 -222
- pathlibutil-0.0.5/src/pathlibutil/job.py +0 -114
- pathlibutil-0.0.5/src/pathlibutil/pathlist.py +0 -48
- pathlibutil-0.0.5/src/pathlibutil/pathutil.py +0 -282
- pathlibutil-0.0.5/src/pathlibutil.egg-info/PKG-INFO +0 -17
- pathlibutil-0.0.5/src/pathlibutil.egg-info/SOURCES.txt +0 -16
- pathlibutil-0.0.5/src/pathlibutil.egg-info/dependency_links.txt +0 -1
- pathlibutil-0.0.5/src/pathlibutil.egg-info/top_level.txt +0 -1
- pathlibutil-0.0.5/test/test_hashsum.py +0 -7
- pathlibutil-0.0.5/test/test_pathlist.py +0 -40
- pathlibutil-0.0.5/test/test_pathutil.py +0 -473
|
@@ -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
|
+
[](https://pypi.org/project/pathlibutil/)
|
|
24
|
+
[](https://pypi.org/project/pathlibutil/)
|
|
25
|
+
[](https://pypi.org/project/pathlibutil/)
|
|
26
|
+
[](./LICENSE)
|
|
27
|
+
[](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
|
+
[](https://pypi.org/project/pathlibutil/)
|
|
4
|
+
[](https://pypi.org/project/pathlibutil/)
|
|
5
|
+
[](https://pypi.org/project/pathlibutil/)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](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"
|
pathlibutil-0.0.5/PKG-INFO
DELETED
|
@@ -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)
|
pathlibutil-0.0.5/README.md
DELETED
pathlibutil-0.0.5/pyproject.toml
DELETED
|
@@ -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"
|
pathlibutil-0.0.5/setup.cfg
DELETED
|
@@ -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
|