pathlibutil 0.0.4__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.4
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.4"
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,4 +0,0 @@
1
- from .pathutil import Path
2
- from .hashsum import hashsum, hashcheck, hashparse
3
-
4
- __all__ = ['Path', 'hashsum', 'hashcheck', 'hashparse']
@@ -1,48 +0,0 @@
1
- from . import Path
2
- import functools
3
- from typing import Optional, Union, Callable
4
- import hashlib
5
-
6
-
7
- def cache(func):
8
- @functools.wraps(func)
9
- def cached(self, *args, **kwargs):
10
- try:
11
- lock = (self.mtime, self)
12
- except AttributeError:
13
- lock = self
14
-
15
- try:
16
- func_cache = self.__cache__[lock]
17
- except (AttributeError, KeyError):
18
- func_cache = dict()
19
- self.__cache__ = {lock: func_cache}
20
-
21
- try:
22
- args_cache = func_cache[func.__name__]
23
- except KeyError:
24
- args_cache = dict()
25
- self.__cache__[lock][func.__name__] = args_cache
26
-
27
- # key = args + tuple(sorted(kwargs.items()))
28
- key = args
29
- try:
30
- value = args_cache[key]
31
- except KeyError:
32
- value = func(self, *args, **kwargs)
33
- args_cache[key] = value
34
-
35
- return value
36
-
37
- return cached
38
-
39
-
40
- class Path(Path):
41
-
42
- @cache
43
- def _count(self, substr: str, /, *, size: int) -> int:
44
- return super()._count(substr, size=size)
45
-
46
- @cache
47
- def _file_digest(self, algorithm: str, /, *, _bufsize: int) -> 'hashlib._Hash':
48
- return super()._file_digest(algorithm, _bufsize=_bufsize)
@@ -1,114 +0,0 @@
1
- from . import Path
2
- from typing import Iterable, Tuple, Generator, Dict, List, Union
3
- import os
4
- import re
5
-
6
- import concurrent.futures as cf
7
-
8
-
9
- def hashsum(
10
- hashfile: str,
11
- files: Iterable,
12
- *,
13
- header: str = None,
14
- algorithm: str = None,
15
- size: int = None
16
- ) -> Path:
17
- hashfile = Path(hashfile)
18
-
19
- if algorithm is None:
20
- algorithm = hashfile.suffix.lstrip('.')
21
- if algorithm not in hashfile.algorithms_available:
22
- raise ValueError('unknown suffix or specify algorithm')
23
- else:
24
- hashfile = hashfile.with_suffix(f".{algorithm}")
25
-
26
- with hashfile.open(mode='wt', encoding='utf-8') as f:
27
- if header != None:
28
- f.writelines(
29
- [f"# {line}\n" for line in header.split('\n') if line]
30
- )
31
-
32
- if f.seek(0, os.SEEK_END) > 0:
33
- f.write('\n')
34
-
35
- dest = hashfile.resolve().parent
36
-
37
- kwargs = {
38
- 'algorithm': algorithm,
39
- 'size': size
40
- }
41
-
42
- def calc_hashes(file: Path, dest: Path, **kwargs) -> str:
43
- file = file.resolve()
44
-
45
- if file.is_relative_to(dest):
46
- filename = file.relative_to(dest)
47
- else:
48
- filename = file
49
-
50
- return f"{file.hexdigest(**kwargs)} *{filename}"
51
-
52
- with cf.ThreadPoolExecutor() as exec:
53
- results = [
54
- exec.submit(calc_hashes, file, dest, **kwargs)
55
- for file in files
56
- ]
57
-
58
- for result in cf.as_completed(results):
59
- f.write(f"{result.result()}\n")
60
-
61
- return hashfile
62
-
63
-
64
- def hashparse(
65
- hashfile: str,
66
- ) -> Generator[Tuple[str, Path], None, None]:
67
-
68
- hashfile = Path(hashfile)
69
-
70
- root = hashfile.resolve().parent
71
-
72
- regex = re.compile(
73
- r'^(?P<hash>[a-f0-9]{8,}) \*(?P<filename>.*?)$',
74
- re.IGNORECASE
75
- )
76
-
77
- for match in map(lambda line: regex.match(line.strip()), hashfile.iter_lines(encoding='utf-8')):
78
- if match is None:
79
- continue
80
-
81
- try:
82
- hash, filename = match.group('hash'), Path(match.group('filename'))
83
- except IndexError:
84
- continue
85
-
86
- if not filename.is_absolute():
87
- filename = root.joinpath(filename)
88
-
89
- yield (hash, filename)
90
-
91
-
92
- def hashcheck(
93
- hashfile: str,
94
- algorithm: str = None,
95
- *,
96
- size: int = None
97
- ) -> Dict[Union[bool, None], List[Path]]:
98
-
99
- kwargs = {
100
- 'algorithm': algorithm,
101
- 'size': size
102
- }
103
-
104
- result = {True: [], False: [], None: []}
105
-
106
- for hash, file in hashparse(hashfile):
107
- try:
108
- key = bool(file.verify(hash, **kwargs))
109
- except (FileNotFoundError, PermissionError) as e:
110
- result[None].append(file)
111
- else:
112
- result[key].append(file)
113
-
114
- return result
@@ -1,192 +0,0 @@
1
- import pathlib
2
- import hashlib
3
- import os
4
- import shutil
5
- import distutils.file_util as dfutil
6
- from typing import Tuple, Union, Callable, Optional, Any
7
-
8
-
9
- class Path(pathlib.Path):
10
- _flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour
11
-
12
- _digest_length = {
13
- 'shake_128': 128,
14
- 'shake_256': 256
15
- }
16
-
17
- _digest_default = 'md5'
18
-
19
- _digest_chunk = 2**20
20
-
21
- @property
22
- def default_digest(self) -> str:
23
- return self._digest_default
24
-
25
- def iter_lines(self, encoding: str = None) -> str:
26
- ''' read the content of a file line by line without the line-ending char '''
27
- with super().open(mode='rt', encoding=encoding) as f:
28
- while True:
29
- line = f.readline()
30
-
31
- if line:
32
- yield line.rstrip('\n')
33
- else:
34
- break
35
-
36
- def iter_bytes(self, size: int = None) -> bytes:
37
- ''' return a chunk of bytes '''
38
- if not size:
39
- size = self._digest_chunk
40
-
41
- with super().open(mode='rb') as f:
42
- while True:
43
- chunk = f.read(size)
44
-
45
- if chunk:
46
- yield chunk
47
- else:
48
- break
49
-
50
- def hexdigest(self, algorithm: str = None, *, size: int = None, length: int = None) -> str:
51
- ''' calculate a hashsum using an algorithm '''
52
- h = self.digest(algorithm, size=size)
53
-
54
- if h.digest_size != 0:
55
- kwargs = dict()
56
- else:
57
- if length:
58
- kwargs = {'length': length}
59
- else:
60
- try:
61
- key = self.algorithm(algorithm)
62
- kwargs = {'length': self._digest_length[key]}
63
- except KeyError:
64
- raise TypeError(
65
- "hexdigest() missing required argument 'length'")
66
-
67
- if kwargs['length'] < 0:
68
- raise ValueError(
69
- "hexdigest() required argument 'length' to be a positive integer")
70
-
71
- return h.hexdigest(**kwargs)
72
-
73
- def digest(self, algorithm: str = None, *, size: int = None) -> 'hashlib._Hash':
74
- ''' digest of the binary file-content '''
75
- if not size or size < 0:
76
- size = self._digest_chunk
77
-
78
- if not algorithm:
79
- algorithm = self._digest_default
80
-
81
- return self._file_digest(self.algorithm(algorithm), _bufsize=size)
82
-
83
- def _file_digest(self, algorithm: str, /, *, _bufsize: int) -> 'hashlib._Hash':
84
- digest = (lambda: hashlib.new(algorithm))
85
-
86
- with self.open(mode='rb') as f:
87
- h = hashlib.file_digest(f, digest, _bufsize=_bufsize)
88
-
89
- return h
90
-
91
- @property
92
- def algorithms_available(self) -> set[str]:
93
- ''' names of available hash algorithms '''
94
- return hashlib.algorithms_available
95
-
96
- def algorithm(self, value: Union[str, Any]) -> Union[str, Any]:
97
- ''' converts file suffix into a valid algorithm string '''
98
- try:
99
- return value.strip().lstrip('.').lower()
100
- except AttributeError:
101
- return self._digest_default
102
-
103
- def eol_count(self, eol: str = None, size: int = None) -> int:
104
- ''' return the number of end-of-line characters'''
105
- try:
106
- substr = eol.encode()
107
- except AttributeError as e:
108
- substr = '\n'.encode()
109
-
110
- if not size:
111
- size = self._digest_chunk
112
-
113
- return self._count(substr, size=size)
114
-
115
- def _count(self, substr: str, /, *, size: int) -> int:
116
- return sum(chunk.count(substr) for chunk in self.iter_bytes(size))
117
-
118
- def copy(self, dst: Union[str, 'Path'], *, parents: bool = True, **kwargs) -> Tuple['Path', int]:
119
- ''' copies self into a new destination, check distutils.file_util::copy_file for kwargs '''
120
-
121
- if parents is True:
122
- Path(dst).mkdir(parents=True, exist_ok=True)
123
-
124
- destination, result = dfutil.copy_file(self, dst, **kwargs)
125
-
126
- return (Path(destination), result)
127
-
128
- def move(self, dst: Union[str, 'Path'], *, parents: bool = True, prune: bool = True, **kwargs) -> Tuple['Path', int]:
129
- ''' moves self into a new destination '''
130
-
131
- destination, result = self.copy(dst, parents=parents, **kwargs)
132
-
133
- if result:
134
- prune = False if not prune else 'try'
135
- self.unlink(missing_ok=True, prune=prune)
136
-
137
- return (Path(destination), result)
138
-
139
- def rmdir(self, *, recursive=False, **kwargs):
140
- ''' deletes a directory with all files, check shutil::rmtree for kwargs '''
141
-
142
- if not recursive:
143
- super().rmdir()
144
- else:
145
- shutil.rmtree(self, **kwargs)
146
-
147
- def unlink(self, missing_ok: bool = False, *, prune: Union[bool, str] = False):
148
- ''' deletes a file and prune an empty directory '''
149
- super().unlink(missing_ok)
150
-
151
- if prune:
152
- try:
153
- self.parent.rmdir(recursive=False)
154
- except OSError as e:
155
- if str(prune).casefold() != 'try':
156
- raise
157
-
158
- def touch(self, mode=0o666, exist_ok=True, *, parents: bool = False):
159
- ''' creates a file and parent directories '''
160
- if parents is True:
161
- self.parent.mkdir(parents=True, exist_ok=True)
162
-
163
- super().touch(mode=mode, exist_ok=exist_ok)
164
-
165
- @property
166
- def mtime(self) -> int:
167
- ''' time of the last modification in nanoseconds '''
168
- return self.stat().st_mtime_ns
169
-
170
- def verify(self, hexdigest: str, algorithm: Optional[str] = None, *, size: Optional[int] = None) -> Union[str, None]:
171
- ''' verify if file has the correct hash '''
172
- hexdigest = hexdigest.strip().lower()
173
- digest_size = int(len(hexdigest) / 2)
174
-
175
- if algorithm:
176
- result = self.hexdigest(algorithm, length=digest_size, size=size)
177
- return algorithm if result == hexdigest else None
178
-
179
- for algorithm in self.algorithms_available:
180
- h = hashlib.new(algorithm)
181
-
182
- if h.digest_size not in (digest_size, 0):
183
- continue
184
-
185
- result = self.hexdigest(algorithm, length=digest_size, size=size)
186
-
187
- if result == hexdigest:
188
- break
189
- else:
190
- return None
191
-
192
- return algorithm
@@ -1,17 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: pathlibutil
3
- Version: 0.0.4
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,13 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- src/pathlibutil/__init__.py
5
- src/pathlibutil/cached.py
6
- src/pathlibutil/hashsum.py
7
- src/pathlibutil/pathutil.py
8
- src/pathlibutil.egg-info/PKG-INFO
9
- src/pathlibutil.egg-info/SOURCES.txt
10
- src/pathlibutil.egg-info/dependency_links.txt
11
- src/pathlibutil.egg-info/top_level.txt
12
- test/test_hashsum.py
13
- test/test_pathutil.py
@@ -1 +0,0 @@
1
- pathlibutil
@@ -1,15 +0,0 @@
1
- import pytest
2
-
3
- from pathlibutil import *
4
-
5
-
6
- def test_hashsum():
7
- pass
8
-
9
-
10
- def test_hashcheck():
11
- pass
12
-
13
-
14
- def test_hashparse():
15
- pass
@@ -1,275 +0,0 @@
1
- import pytest
2
- import hashlib
3
- import inspect
4
- import pathlib
5
- import subprocess
6
- import time
7
-
8
- from pathlibutil import Path
9
-
10
- CONTENT = 'foo\nbar!\n'
11
- SEC = 0.02
12
-
13
-
14
- @pytest.fixture()
15
- def tmp_file(tmp_path: pathlib.Path) -> str:
16
- ''' returns a filename to a temporary testfile'''
17
- txt = tmp_path / 'test_file.txt'
18
-
19
- txt.write_text(CONTENT, encoding='utf-8', newline='')
20
- return str(txt)
21
-
22
-
23
- @pytest.fixture()
24
- def dst_path(tmp_path: pathlib.Path) -> str:
25
- dest = tmp_path / 'destination'
26
-
27
- return str(dest)
28
-
29
-
30
- def test_eol_count(tmp_file):
31
- p = Path(tmp_file)
32
- assert p.eol_count() == 2
33
- assert p.eol_count(eol='\n') == 2
34
- assert p.eol_count(eol='\r') == 0
35
-
36
-
37
- def test_verify(tmp_file):
38
- p = Path(tmp_file)
39
-
40
- my_bytes = pathlib.Path(tmp_file).read_bytes()
41
- shake_128 = hashlib.new('shake_128', my_bytes).hexdigest(128)
42
- sha256 = hashlib.new('sha256', my_bytes).hexdigest()
43
-
44
- assert p.verify(sha256) == 'sha256'
45
- assert p.verify(sha256, algorithm='sha256') != None
46
- assert p.verify(sha256[:14]) == None
47
- assert p.verify(sha256[:14], algorithm='sha256') == None
48
- assert p.verify(sha256, algorithm='sha1') == None
49
- assert p.verify(sha256, algorithm=None, size=10) == 'sha256'
50
-
51
- assert p.verify(shake_128) != None
52
- assert p.verify(shake_128, algorithm='shake_128') == 'shake_128'
53
- assert p.verify(shake_128[:32]) != None
54
- assert p.verify(shake_128[:32], algorithm='shake_128') == 'shake_128'
55
-
56
-
57
- def test_hexdigest(tmp_file):
58
- p = Path(tmp_file)
59
-
60
- my_bytes = pathlib.Path(tmp_file).read_bytes()
61
- md5 = hashlib.new('md5', my_bytes).hexdigest()
62
- sha1 = hashlib.new('sha1', my_bytes).hexdigest()
63
-
64
- assert p.hexdigest() == md5
65
- assert p.hexdigest(p.default_digest) == md5
66
- assert p.hexdigest(algorithm='md5', size=4) == md5
67
- assert p.hexdigest(algorithm='sha1') == sha1
68
-
69
- with pytest.raises(ValueError):
70
- p.hexdigest(algorithm='fubar')
71
-
72
- with pytest.raises(TypeError):
73
- p.hexdigest(size='fubar')
74
-
75
- # test on a directory
76
- with pytest.raises(PermissionError):
77
- p.parent.hexdigest()
78
-
79
- # test none existing file
80
- p.unlink()
81
- with pytest.raises(FileNotFoundError):
82
- p.hexdigest()
83
-
84
-
85
- def test_shake(tmp_file):
86
- p = Path(tmp_file)
87
-
88
- assert len(p.hexdigest('shake_128')) == 128*2
89
-
90
- length = 10
91
-
92
- assert len(p.hexdigest('shake_128', length=length)) == length * 2
93
-
94
- with pytest.raises(TypeError):
95
- p.hexdigest('shake_128', length)
96
-
97
- with pytest.raises(ValueError):
98
- p.hexdigest('shake_256', length=-1)
99
-
100
-
101
- def test_digest(tmp_file):
102
- p = Path(tmp_file)
103
-
104
- my_bytes = pathlib.Path(tmp_file).read_bytes()
105
- md5 = hashlib.new('md5', my_bytes)
106
-
107
- assert p.digest('md5').digest() == md5.digest()
108
-
109
-
110
- def test_available_algorithm():
111
- p = Path()
112
-
113
- assert isinstance(p.algorithms_available, set)
114
-
115
- for a in p.algorithms_available:
116
- assert a in hashlib.algorithms_available
117
-
118
-
119
- def test_iter_lines(tmp_file):
120
- with pytest.raises(FileNotFoundError):
121
- for line in Path('file_not_available.txt').iter_lines():
122
- pass
123
-
124
- my_generator = Path(tmp_file).iter_lines()
125
-
126
- assert inspect.isgenerator(my_generator)
127
- assert list(my_generator) == str(CONTENT).splitlines()
128
-
129
-
130
- def test_iter_bytes(tmp_file):
131
- with pytest.raises(FileNotFoundError):
132
- for chunk in Path('file_not_available.txt').iter_bytes():
133
- pass
134
-
135
- my_generator = Path(tmp_file).iter_bytes()
136
-
137
- assert inspect.isgenerator(my_generator)
138
- assert list(my_generator)[0] == str(CONTENT).encode()
139
-
140
-
141
- def test_copy(tmp_file, dst_path):
142
- src = Path(tmp_file)
143
-
144
- result = src.copy(dst_path, parents=True)
145
-
146
- assert isinstance(result, tuple)
147
-
148
- dst, copied = result
149
-
150
- assert copied == True
151
- assert pathlib.Path(src).is_file() == True
152
- assert dst == pathlib.Path(dst_path).joinpath(pathlib.Path(tmp_file).name)
153
-
154
-
155
- def test_move(tmp_file, dst_path):
156
- src = Path(tmp_file)
157
-
158
- result = src.move(dst_path)
159
-
160
- assert isinstance(result, tuple)
161
-
162
- _, moved = result
163
-
164
- assert moved == True
165
- assert pathlib.Path(src).is_file() == False
166
-
167
-
168
- def test_unlink_prune(tmp_file):
169
- src = Path(tmp_file)
170
-
171
- src.unlink(prune=True)
172
- assert src.is_file() == False
173
- assert src.parent.exists() == False
174
-
175
-
176
- def test_unlink(tmp_file):
177
- src = Path(tmp_file)
178
-
179
- src.unlink()
180
- assert src.is_file() == False
181
- assert src.parent.exists() == True
182
-
183
-
184
- def test_unlink_missing(tmp_path):
185
- src = Path(tmp_path) / 'subdir' / 'file_not_found.txt'
186
-
187
- with pytest.raises(FileNotFoundError):
188
- src.unlink()
189
-
190
- src.parent.mkdir()
191
-
192
- src.unlink(missing_ok=True)
193
- assert src.parent.is_dir() == True
194
-
195
- src2 = Path(tmp_path) / 'subdir' / 'not_empty.txt'
196
- src2.touch()
197
-
198
- with pytest.raises(OSError):
199
- src.unlink(missing_ok=True, prune=True)
200
- assert src.parent.is_dir() == True
201
-
202
- src.unlink(missing_ok=True, prune='try')
203
- assert src.parent.is_dir() == True
204
-
205
- src2.unlink()
206
- src.unlink(missing_ok=True, prune=True)
207
- assert src.parent.is_dir() == False
208
-
209
-
210
- def test_rmdir_isfile(tmp_file):
211
- src = Path(tmp_file)
212
-
213
- with pytest.raises(NotADirectoryError):
214
- src.rmdir()
215
-
216
- with pytest.raises(NotADirectoryError):
217
- src.rmdir(recursive=True)
218
-
219
- assert src.exists() == True
220
-
221
-
222
- def test_rmdir_isdir(dst_path):
223
- dst = Path(dst_path)
224
- dst.mkdir()
225
- file = dst.joinpath('tmp.txt')
226
- file.touch()
227
-
228
- assert dst.is_dir() == True
229
- assert file.is_file() == True
230
- dst.rmdir(recursive=True)
231
- assert file.exists() == False
232
- assert dst.exists() == False
233
-
234
-
235
- def test_touch(tmp_path):
236
- src = Path(tmp_path) / 'file_not_found.txt'
237
-
238
- src.touch()
239
- assert src.is_file() == True
240
-
241
- src2 = Path(tmp_path) / 'subdir' / 'file_not_found.txt'
242
-
243
- with pytest.raises(FileNotFoundError):
244
- src2.touch()
245
-
246
- assert src2.exists() == False
247
-
248
- src2.touch(parents=True)
249
- assert src2.parent.is_dir() == True
250
- assert src2.is_file() == True
251
-
252
-
253
- def test_mtime(tmp_file, tmp_path):
254
- src = Path(tmp_file)
255
-
256
- start = src.mtime
257
-
258
- assert isinstance(start, int)
259
-
260
- with src.open(mode='at') as f:
261
- f.write('hallo welt')
262
- time.sleep(SEC)
263
-
264
- end = src.mtime
265
- assert (end - start) >= (SEC * 1e9)
266
-
267
- assert (src.mtime - end) == 0
268
-
269
- src2 = Path(tmp_path) / 'subdir_not_exists'
270
-
271
- with pytest.raises(FileNotFoundError):
272
- _ = src2.mtime
273
-
274
- src2.mkdir()
275
- assert src2.mtime > 0