From 26f554f3f8a0790e99ebbb1119be647c1587d384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20D=C3=B6rrer?= Date: Sun, 31 Dec 2023 10:53:06 +0100 Subject: [PATCH] rewritten pathlibutil with pytest --- .gitignore | 162 +++++++++++++++++++ LICENSE | 21 +++ README.md | 56 +++++++ examples/example1.py | 25 +++ examples/example2.py | 25 +++ examples/example3.py | 44 +++++ pathlibutil/__init__.py | 1 + pathlibutil/path.py | 91 +++++++++++ poetry.lock | 347 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 50 ++++++ tests/__init__.py | 0 tests/test_path.py | 228 ++++++++++++++++++++++++++ 12 files changed, 1050 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/example1.py create mode 100644 examples/example2.py create mode 100644 examples/example3.py create mode 100644 pathlibutil/__init__.py create mode 100644 pathlibutil/path.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/test_path.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a93118a --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +.vscode + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3ec9789 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Christoph Dörrer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..890e40b --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# pathlibutil + +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pathlibutil)](https://pypi.org/project/pathlibutil/) +[![PyPI](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/) +[![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](./LICENSE) +[![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) + +--- + +`pathlibutil.Path` inherits from `pathlib.Path` with some useful built-in python functions. + +- `Path().hexdigest()` to calculate and `Path().verify()` for verification of hexdigest from a file +- `Path.default_hash` to configurate default hash algorithm for `Path` class (default: *'md5'*) +- `Path().size()` to get size in bytes of a file or directory +- `Path().read_lines()` to yield over all lines from a file until EOF +- `contextmanager` to change current working directory with `with` statement + +## Installation + +```bash +pip install pathlibutil +``` + +## Usage + +```python +from pathlibutil import Path + +readme = Path('README.md') + +print(f'File size: {readme.size()} Bytes') +print(f'File sha1: {readme.hexdigest("sha1")}') + +print('-- File content --') +for line in readme.read_lines(encoding='utf-8'): + print(line, end='') +print('-- EOF --') + +with readme.parent as cwd: + print(f'Current working directory: {cwd}') + +# Change default hash algorithm from md5 to sha1 +Path.default_hash = 'sha1' + +print(f'File verification: {readme.verify("add3f48fded5e0829a8e3e025e44c2891542c58e")}') +``` + +## Examples + +1. [Read file line by line to stdout](./example/example1.py) + > `Path().read_lines()` +2. [Write calculated hash to file](./example/example2.py) + > `Path().hexdigest()` +3. [Read hashes from file for verification](./example/example3.py) + > `Path().verify()` and `contextmanager` diff --git a/examples/example1.py b/examples/example1.py new file mode 100644 index 0000000..fdf0f8d --- /dev/null +++ b/examples/example1.py @@ -0,0 +1,25 @@ +try: + import sys + sys.path.insert(0, '../pathlibutil') +finally: + from pathlibutil import Path + + +def main(): + """Example 1: Read a file and print its content and some file information to stdout.""" + + readme = Path('README.md') + + print(f'File size: {readme.size()} Bytes') + print(f'File sha1: {readme.hexdigest("sha1")}') + + print('File content'.center(80, '=')) + + for line in readme.read_lines(encoding='utf-8'): + print(line, end='') + + print('EOF'.center(80, '=')) + + +if __name__ == '__main__': + main() diff --git a/examples/example2.py b/examples/example2.py new file mode 100644 index 0000000..0c48435 --- /dev/null +++ b/examples/example2.py @@ -0,0 +1,25 @@ +try: + import sys + sys.path.insert(0, '../pathlibutil') +finally: + from pathlibutil import Path + + +def main(): + """Example 2: Write a file with md5 checksums of all python files in the pathlibutil-directory.""" + + file = Path('pathlibutil.md5') + + with file.open('w') as f: + f.write( + '# MD5 checksums generated with pathlibutil (https://pypi.org/project/pathlibutil/)\n\n') + + i = 0 + for i, filename in enumerate(Path('./pathlibutil').glob('*.py'), start=1): + f.write(f'{filename.hexdigest()} *{filename}\n') + + print(f'\nwritten: {i:>5} {file.default_hash}-hashes to: {file}') + + +if __name__ == '__main__': + main() diff --git a/examples/example3.py b/examples/example3.py new file mode 100644 index 0000000..4fb2e77 --- /dev/null +++ b/examples/example3.py @@ -0,0 +1,44 @@ +try: + import sys + sys.path.insert(0, '../pathlibutil') +finally: + from pathlibutil import Path + + +def main(): + """Example 3: Read a file with md5 checksums and verify them.""" + + file = Path('pathlibutil.md5') + + def no_comment(line: str) -> bool: + return not line.startswith('#') + + with file.parent as cwd: + miss = 0 + ok = 0 + fail = 0 + + for line in filter(no_comment, file.read_lines()): + try: + digest, filename = line.strip().split(' *') + verification = Path(filename).verify(digest, 'md5') + except ValueError as split_failed: + continue + except FileNotFoundError as verify_failed: + tag = 'missing' + miss += 1 + else: + if verification: + tag = 'ok' + ok += 1 + else: + tag = 'fail' + fail += 1 + + print(f'{tag.ljust(len(digest), ".")} *{filename}') + + print(f'\nok: {ok:<5} fail: {fail:<5} missing: {miss}') + + +if __name__ == '__main__': + main() diff --git a/pathlibutil/__init__.py b/pathlibutil/__init__.py new file mode 100644 index 0000000..c040afc --- /dev/null +++ b/pathlibutil/__init__.py @@ -0,0 +1 @@ +from pathlibutil.path import Path diff --git a/pathlibutil/path.py b/pathlibutil/path.py new file mode 100644 index 0000000..b4d43e8 --- /dev/null +++ b/pathlibutil/path.py @@ -0,0 +1,91 @@ +import hashlib +import os +import pathlib +import sys +from typing import Generator, Set + + +class Path(pathlib.Path): + default_hash = 'md5' + + if sys.version_info < (3, 12): + _flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour + + @property + def algorithms_available(self) -> Set[str]: + """ + Set of available algorithms that can be passed to hexdigest() method. + """ + return hashlib.algorithms_available + + def hexdigest(self, algorithm: str = None, /, **kwargs) -> str: + """ + Returns the hex digest of the file using the named algorithm (default: md5). + """ + try: + args = (kwargs.pop('length'),) + except KeyError: + args = () + + return hashlib.new( + name=algorithm or self.default_hash, + data=self.read_bytes(), + ).hexdigest(*args) + + def verify(self, hashdigest: str, algorithm: str = None, *, strict: bool = True, **kwargs) -> bool: + """ + Verifies the hash of the file using the named algorithm (default: md5). + """ + _hash = self.hexdigest(algorithm, **kwargs) + + if strict: + return _hash == hashdigest + + if len(hashdigest) < 7: + raise ValueError('hashdigest must be at least 7 characters long') + + for a, b in zip(_hash, hashdigest): + if a != b.lower(): + return False + + return True + + def __enter__(self) -> 'Path': + """ + Contextmanager to changes the current working directory. + """ + cwd = os.getcwd() + + try: + os.chdir(self) + except Exception as e: + raise e + else: + self.__stack = cwd + + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + """ + Restore previous working directory. + """ + try: + os.chdir(self.__stack) + finally: + del self.__stack + + def read_lines(self, **kwargs) -> Generator[str, None, None]: + """ + Iterates over all lines of the file until EOF is reached. + """ + with self.open(**kwargs) as f: + yield from iter(f.readline, '') + + def size(self, **kwargs) -> int: + """ + Returns the size in bytes of a file or directory. + """ + if self.is_dir(): + return sum([p.size(**kwargs) for p in self.iterdir()]) + + return self.stat(**kwargs).st_size diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..54509bf --- /dev/null +++ b/poetry.lock @@ -0,0 +1,347 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "cachetools" +version = "5.3.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.4.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, + {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, + {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, + {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, + {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, + {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, + {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, + {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, + {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, + {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, + {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, + {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, + {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyproject-api" +version = "1.6.1" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, + {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, +] + +[package.dependencies] +packaging = ">=23.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.12.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-random-order" +version = "1.1.0" +description = "Randomise the order in which pytest tests are run with some control over the randomness" +optional = false +python-versions = ">=3.5.0" +files = [ + {file = "pytest-random-order-1.1.0.tar.gz", hash = "sha256:dbe6debb9353a7af984cc9eddbeb3577dd4dbbcc1529a79e3d21f68ed9b45605"}, + {file = "pytest_random_order-1.1.0-py3-none-any.whl", hash = "sha256:6cb1e59ab0f798bb0c3488c11ae0c70d7d3340306a466d28b28ccd8ef8c20b7e"}, +] + +[package.dependencies] +pytest = ">=3.0.0" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tox" +version = "4.11.4" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tox-4.11.4-py3-none-any.whl", hash = "sha256:2adb83d68f27116812b69aa36676a8d6a52249cb0d173649de0e7d0c2e3e7229"}, + {file = "tox-4.11.4.tar.gz", hash = "sha256:73a7240778fabf305aeb05ab8ea26e575e042ab5a18d71d0ed13e343a51d6ce1"}, +] + +[package.dependencies] +cachetools = ">=5.3.1" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.12.3" +packaging = ">=23.1" +platformdirs = ">=3.10" +pluggy = ">=1.3" +pyproject-api = ">=1.6.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +virtualenv = ">=20.24.3" + +[package.extras] +docs = ["furo (>=2023.8.19)", "sphinx (>=7.2.4)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.24)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=1)", "diff-cover (>=7.7)", "distlib (>=0.3.7)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.18)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.12)", "wheel (>=0.41.2)"] + +[[package]] +name = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8,<3.13" +content-hash = "4c046c4ee46ce9231356d6937d78514a2c94f7f8607f92db0d0d2063b9c82cbd" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ad19a27 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[tool.poetry] +name = "pathlibutil" +version = "0.1.0" +description = "" +authors = ["Christoph Dörrer "] +readme = "README.md" +license = "MIT" +classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", +] +keywords = ["pathlib", "hashlib"] +repository = "https://github.com/d-chris/pathlibutil" + +[tool.poetry.dependencies] +python = ">=3.8,<3.13" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +tox = "^4.11.4" +pytest-random-order = "^1.1.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.12.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py38,py39,py310,py311,py312 + +[testenv] +deps = + pytest + pytest-random-order + pytest-mock + pytest-cov +commands = pytest --random-order +""" + +[tool.pytest.ini_options] +minversion = "6.0" +testpaths = "tests" +addopts = "--random-order --cov=pathlibutil --cov-report=term-missing:skip-covered --color=yes" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_path.py b/tests/test_path.py new file mode 100644 index 0000000..921399b --- /dev/null +++ b/tests/test_path.py @@ -0,0 +1,228 @@ +import hashlib +import os +import pathlib +import random +from types import GeneratorType +from unittest.mock import Mock + +import pytest + +from pathlibutil import Path + + +@pytest.fixture(scope='function') +def cls() -> Path: + """return the same class for all test function""" + + hash = Path.default_hash + yield Path + Path.default_hash = hash + + +@pytest.fixture(scope='function') +def file(cls) -> Path: + """new instance of Path for each test-function""" + return cls(__file__) + + +@pytest.fixture(scope='function') +def algorithm() -> str: + """random hash algorithm for one test without shake""" + return random.choice( + [ + a for a in hashlib.algorithms_available + if not a.startswith('shake') + ] + ) + + +@pytest.fixture(scope='function') +def shake() -> str: + """random shake algorithm for one test""" + return random.choice( + [ + a for a in hashlib.algorithms_available + if a.startswith('shake') + ] + ) + + +@pytest.fixture(scope='function') +def length() -> int: + """random length between 8 and 16 for shake algorithms""" + return random.randint(8, 16) + + +@pytest.fixture(autouse=True) +def mock_hexdigest(mocker) -> Mock: + mock_hashlib_new = mocker.patch('hashlib.new') + + mock = Mock() + mock.hexdigest.return_value = '0123456789abcdef' + + mock_hashlib_new.return_value = mock + + yield mock + + +def test_hexdigest_default(file: Path): + assert hasattr(Path, 'hexdigest') + + p = file.hexdigest() + + assert isinstance(p, str) + assert p == hashlib.new('md5', open(__file__, 'rb').read()).hexdigest() + + +def test_hexdigest_hash(file: Path, algorithm: str): + hash = hashlib.new(algorithm, open(__file__, 'rb',).read()).hexdigest() + + file.default_hash = algorithm + + p = file.hexdigest() + + assert p == hash + + +def test_hexdigest_random(file: Path, algorithm: str): + + p = file.hexdigest(algorithm) + + assert p == hashlib.new(algorithm, open(__file__, 'rb').read()).hexdigest() + + +def test_hexdigest_length(file: Path, shake: str, length: int): + + with pytest.raises(TypeError): + _ = file.hexdigest(shake, length) + + p = file.hexdigest(shake, length=length) + + assert p == hashlib.new( + shake, + open(__file__, 'rb').read() + ).hexdigest(length) + + +def test_algorithms_available(file: Path): + assert hasattr(Path, 'algorithms_available') + + p = file.algorithms_available + + assert isinstance(p, set) + assert hashlib.algorithms_available == p + + +def test_read_lines(file: Path): + assert hasattr(Path, 'read_lines') + + p = file.read_lines() + + assert isinstance(p, GeneratorType) + assert "".join(p) == open(__file__).read() + + +def test_default_hash(file: Path): + assert hasattr(Path, 'default_hash') + + assert file.default_hash == 'md5' + + +def test_context_manager(tmp_path: pathlib.Path): + assert hasattr(Path, '__enter__') + assert hasattr(Path, '__exit__') + + with Path(tmp_path) as p: + assert isinstance(p, Path) + assert str(p) == str(tmp_path) + assert str(os.getcwd()) == str(tmp_path) + + +def test_context_manager_raises(tmp_path: pathlib.Path): + + with pytest.raises(FileNotFoundError): + with Path(tmp_path / 'nonexistent'): + pass + + file = tmp_path / 'file.txt' + file.touch() + + with pytest.raises(NotADirectoryError): + with Path(file): + pass + + +def test_default_hash_classvar(cls: Path, algorithm: str): + assert cls.default_hash == 'md5' + + p = cls(__file__) + + cls.default_hash = algorithm + + assert p.default_hash == algorithm + assert p.hexdigest() == hashlib.new( + algorithm, open(__file__, 'rb').read()).hexdigest() + + +def test_size(file: Path, tmp_path: pathlib.Path): + assert hasattr(Path, 'size') + + assert file.size() == os.stat(__file__).st_size + + +def test_size_dir(tmp_path: pathlib.Path): + assert Path(tmp_path).size() == 0 + + tmp_path.joinpath('tempdir').mkdir() + assert Path(tmp_path).size() == 0 + + assert tmp_path.joinpath('file.txt').write_text( + 'ipsum lorem') == Path(tmp_path).size() + + +def test_size_raises(tmp_path: pathlib.Path): + + with pytest.raises(FileNotFoundError): + _ = Path(tmp_path / 'nonexistent').size() + + +def test_verify(file: Path, algorithm: str): + assert hasattr(Path, 'verify') + + hashsum = hashlib.new(algorithm, open(__file__, 'rb').read()).hexdigest() + + assert file.verify(hashsum, algorithm) == True + assert file.verify(hashsum, algorithm=algorithm, strict=True) == True + + assert file.verify(hashsum.upper(), algorithm) == False + assert file.verify(hashsum[:len(hashsum) - 1], algorithm) == False + + with pytest.raises(ValueError): + _ = file.verify('', strict=False) + + with pytest.raises(TypeError): + _ = file.verify(None, strict=False) + + +def test_verify_classvar(file: Path): + hashsum = hashlib.new('md5', open(__file__, 'rb').read()).hexdigest() + + assert file.verify(hashsum) == True + assert file.verify(hashsum[:8], strict=False) == True + + assert file.verify(hashsum[:4]+hashsum[:4], strict=False) == False + + +def test_verify_strict(file: Path, algorithm: str): + + hashsum = hashlib.new(algorithm, open(__file__, 'rb').read()).hexdigest() + + with pytest.raises(TypeError): + _ = file.verify(hashsum, algorithm, False) + + assert file.verify(hashsum.upper(), algorithm, strict=False) == True + + assert file.verify(hashsum[:7], algorithm, strict=False) == True + + with pytest.raises(ValueError): + _ = file.verify(hashsum[:6], strict=False)