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.
- {pathlibutil-0.0.4 → 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.4/PKG-INFO +0 -17
- pathlibutil-0.0.4/README.md +0 -3
- pathlibutil-0.0.4/pyproject.toml +0 -27
- pathlibutil-0.0.4/setup.cfg +0 -4
- pathlibutil-0.0.4/src/pathlibutil/__init__.py +0 -4
- pathlibutil-0.0.4/src/pathlibutil/cached.py +0 -48
- pathlibutil-0.0.4/src/pathlibutil/hashsum.py +0 -114
- pathlibutil-0.0.4/src/pathlibutil/pathutil.py +0 -192
- pathlibutil-0.0.4/src/pathlibutil.egg-info/PKG-INFO +0 -17
- pathlibutil-0.0.4/src/pathlibutil.egg-info/SOURCES.txt +0 -13
- pathlibutil-0.0.4/src/pathlibutil.egg-info/dependency_links.txt +0 -1
- pathlibutil-0.0.4/src/pathlibutil.egg-info/top_level.txt +0 -1
- pathlibutil-0.0.4/test/test_hashsum.py +0 -15
- pathlibutil-0.0.4/test/test_pathutil.py +0 -275
|
@@ -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.4/PKG-INFO
DELETED
|
@@ -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)
|
pathlibutil-0.0.4/README.md
DELETED
pathlibutil-0.0.4/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.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"
|
pathlibutil-0.0.4/setup.cfg
DELETED
|
@@ -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
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pathlibutil
|
|
@@ -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
|