diff --git a/.github/workflows/python_analysis.yml b/.github/workflows/python_analysis.yml new file mode 100644 index 0000000..21853ca --- /dev/null +++ b/.github/workflows/python_analysis.yml @@ -0,0 +1,43 @@ +name: Python analysis + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - develop + - main + - release/** + - feature/** + - hotfix/** + push: + branches: + - develop + - main + - release/** + - feature/** + - hotfix/** + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + call-workflow-static-analysis: + name: Static analysis + uses: MiraGeoscience/CI-tools/.github/workflows/reusable-python-static_analysis.yml@main + with: + package-manager: 'poetry' + app-name: 'mirageoscience' + python-version: '3.10' + call-workflow-pytest: + name: Pytest + uses: MiraGeoscience/CI-tools/.github/workflows/reusable-python-pytest.yml@main + with: + package-manager: 'poetry' + python-versions: '["3.10", "3.11", "3.12"]' + os: '["ubuntu-latest", "windows-latest"]' + cache-number: 1 + codecov-reference-python-version: '3.10' + codecov-reference-os: '["windows-latest"]' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 82f9275..517bc82 100644 --- a/.gitignore +++ b/.gitignore @@ -140,6 +140,11 @@ venv.bak/ # mkdocs documentation /site +#pycharm +/.idea/* +!/.idea/scopes/ +!/.idea/copyright/ + # mypy .mypy_cache/ .dmypy.json diff --git a/.idea/copyright/MiraGeoscience.xml b/.idea/copyright/MiraGeoscience.xml new file mode 100644 index 0000000..a2b44a9 --- /dev/null +++ b/.idea/copyright/MiraGeoscience.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..080d950 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/scopes/sources.xml b/.idea/scopes/sources.xml new file mode 100644 index 0000000..c0b2e4f --- /dev/null +++ b/.idea/scopes/sources.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f5df53b..fc27712 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_language_version: python: python3 exclude: ^docs/(conf.py|_ext/) -default_stages: [commit,push] +default_stages: [pre-commit,pre-push] fail_fast: false ci: @@ -16,12 +16,12 @@ repos: - id: poetry-check args: [--lock] - repo: https://github.com/hadialqattan/pycln - rev: v2.4.0 + rev: v2.5.0 hooks: - id: pycln args: [ --config=pyproject.toml ] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.1 + rev: v0.9.1 hooks: - id: ruff args: @@ -30,25 +30,35 @@ repos: # - --unsafe-fixes - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.1 + rev: v1.14.1 hooks: - id: mypy additional_dependencies: [ tomli, # to read config from pyproject.toml ] - repo: https://github.com/codingjoe/relint - rev: 3.1.1 + rev: 3.3.1 hooks: - id: relint args: [-W] # to fail on warnings - repo: https://github.com/MiraGeoscience/pre-commit-hooks - rev: v1.0.0 + rev: v1.0.2 hooks: - id: check-copyright files: (^LICENSE|^README(|-dev).rst|\.py|\.pyi)$ exclude: (^\.|^docs/) - id: prepare-commit-msg - id: check-commit-msg +- repo: local + hooks: + - id: pylint + name: pylint + entry: poetry run pylint + language: system + require_serial: true # pylint does its own parallelism + types: [text] + types_or: [python, pyi] + exclude: ^(devtools|docs)/ - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: @@ -56,7 +66,7 @@ repos: exclude: (\.lock|\.ipynb|^THIRD_PARTY_SOFTWARE\.rst)$ entry: codespell -I .codespellignore - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace exclude: \.mdj$ diff --git a/LICENSE b/LICENSE index 8148138..42da102 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Mira Geoscience +Copyright (c) 2024-2025 Mira Geoscience Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index 9ca7380..9a1e45e 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,8 @@ Usage Example of ``.pre-commit-config.yamnl``: .. code:: yaml -repos: + + repos: - repo: http://github.com/MiraGeoscience/pre-commit-hooks rev: hooks: @@ -63,4 +64,4 @@ SOFTWARE. Copyright ^^^^^^^^^ -Copyright (c) 2024 Mira Geoscience Ltd. +Copyright (c) 2024-2025 Mira Geoscience Ltd. diff --git a/mirageoscience/__init__.py b/mirageoscience/__init__.py new file mode 100644 index 0000000..6c33ff4 --- /dev/null +++ b/mirageoscience/__init__.py @@ -0,0 +1,8 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2024-2025 Mira Geoscience Ltd. ' +# ' +# This file is part of mirageoscience.pre-commit-hooks package. ' +# ' +# mirageoscience.pre-commit-hooks is distributed under the terms and conditions ' +# of the MIT License (see LICENSE file at the root of this source code package). ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' diff --git a/mirageoscience/hooks/__init__.py b/mirageoscience/hooks/__init__.py index 55b0f78..9a2b362 100644 --- a/mirageoscience/hooks/__init__.py +++ b/mirageoscience/hooks/__init__.py @@ -1,9 +1,10 @@ -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of mirageoscience.pre-commit-hooks package. ' -# ' -# All rights reserved. ' -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2024-2025 Mira Geoscience Ltd. ' +# ' +# This file is part of mirageoscience.pre-commit-hooks package. ' +# ' +# mirageoscience.pre-commit-hooks is distributed under the terms and conditions ' +# of the MIT License (see LICENSE file at the root of this source code package). ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -__version__ = "1.0.2" +__version__ = "1.1.0" diff --git a/mirageoscience/hooks/check_copyright.py b/mirageoscience/hooks/check_copyright.py index 865d46a..1ab15e7 100644 --- a/mirageoscience/hooks/check_copyright.py +++ b/mirageoscience/hooks/check_copyright.py @@ -1,51 +1,62 @@ #!/usr/bin/env python3 -# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of mirageoscience.pre-commit-hooks package. ' -# ' -# mirageoscience_pre_commit_hooks is distributed under the terms and conditions of ' -# the MIT License (see LICENSE file at the root of this source code package). ' -# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2024-2025 Mira Geoscience Ltd. ' +# ' +# This file is part of mirageoscience.pre-commit-hooks package. ' +# ' +# mirageoscience.pre-commit-hooks is distributed under the terms and conditions ' +# of the MIT License (see LICENSE file at the root of this source code package). ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' from __future__ import annotations +import argparse import re import sys from datetime import date +from pathlib import Path -def main(args=None): +MAX_TOP_LINES = 10 +_FULL_SCAN_FILE_NAMES = ["README.rst", "README-dev.rst", "package.rst"] + + +def check_files( + files: list[str] | None = None, full_scan_files: list[str] | None = None +) -> bool: """Checks for valid copyright statements in given files. This function scans the specified files for copyright notices and reports any files that either lack a copyright statement or have an invalid year. Args: - args (list, optional): A list of filenames to be checked. Defaults to + files (list, optional): A list of filenames to be checked. Defaults to `sys.argv[1:]` if not provided. + full_scan_files (list, optional): A list of filenames to be scanned + entirely, instead of checking only the top lines. - Raises: - SystemExit: Exits the program with an exit code of 1 if any - files have missing or invalid copyright statements. + Returns: + bool: True if all files have valid copyright statements, + False otherwise. """ current_year = date.today().year copyright_re = re.compile( rf"\bcopyright \(c\) (:?\d{{4}}-|)\b{current_year}\b", re.IGNORECASE ) - files = sys.argv[1:] - max_lines = 10 + if full_scan_files is None: + full_scan_files = [] + if files is None: + files = sys.argv[1:] + file_paths = [Path(f) for f in files] report_files = [] - for f in files: + for f in file_paths: with open(f, encoding="utf-8") as file: count = 0 has_dated_copyright = False for line in file: count += 1 - if count >= max_lines and not ( - f.endswith("README.rst") or f.endswith("README-dev.rst") or f.endswith("package.rst") - ): + if count >= MAX_TOP_LINES and f.name not in full_scan_files: break if re.search(copyright_re, line): has_dated_copyright = True @@ -54,15 +65,46 @@ def main(args=None): if not has_dated_copyright: report_files.append(f) - if len(report_files) > 0: - for f in report_files: - sys.stderr.write(f"{f}: No copyright or invalid year\n") - exit(1) + if len(report_files) == 0: + return True + for f in report_files: + sys.stderr.write(f"{f}: No copyright or invalid year\n") + return False -# readonly CURRENT_YEAR=$(date +"%Y") +def main(): + """Parses command line arguments and calls the `check_files` function. + + Raises: + SystemExit: If check_files returns False. + """ + + parser = argparse.ArgumentParser() + parser.add_argument("files", nargs="+", help="list of files to scan") + parser.add_argument( + "--full-scan-files", + type=lambda s: s.split(","), + help=( + "Comma-separated list of names for files to scan entirely, " + f"instead of checking only the top {MAX_TOP_LINES} lines" + ), + metavar="FILE1,FILE2,...", + default=[], + required=False, + ) + + args = parser.parse_args() + if not check_files(args.files, _FULL_SCAN_FILE_NAMES + args.full_scan_files): + sys.exit(1) + + +# Note: a simpler bash script for this task would be: +# ---------------------------- +# readonly CURRENT_YEAR=$(date +"%Y") +# # if ! grep -e "Copyright (c) .*$CURRENT_YEAR" $(head -10 $f) 2>&1 1>/dev/null; then # echo "File '$f' has no copyright or an invalid year" # exit 1 # fi +# ---------------------------- diff --git a/mirageoscience/hooks/git_message_hook.py b/mirageoscience/hooks/git_message_hook.py index 21e2b64..d2809e1 100644 --- a/mirageoscience/hooks/git_message_hook.py +++ b/mirageoscience/hooks/git_message_hook.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 -# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of mirageoscience.pre-commit-hooks package. ' -# ' -# mirageoscience_pre_commit_hooks is distributed under the terms and conditions of ' -# the MIT License (see LICENSE file at the root of this source code package). ' -# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2024-2025 Mira Geoscience Ltd. ' +# ' +# This file is part of mirageoscience.pre-commit-hooks package. ' +# ' +# mirageoscience.pre-commit-hooks is distributed under the terms and conditions ' +# of the MIT License (see LICENSE file at the root of this source code package). ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' """Some Git pre-commit hooks implementations.""" @@ -43,36 +43,35 @@ def get(): match = re.match(JiraPattern.get(), text.strip()) return match.group(1) if match else "" + def get_message_prefix_bang(line: str) -> str: """Capture the standard commit message prefix, if any, such as 'fixup!', 'amend!', - etc. + etc. + + :return: the standard commit message prefix if found, else empty string. + """ - :return: the standard commit message prefix if found, else empty string. - """ class BangPattern: """Internal class that encapsulates the regular expression for the Bnag pattern, making sure it gets compiled only once.""" - __pattern = re.compile( - r"(\w*!\s)" - ) + __pattern = re.compile(r"(\w*!\s)") @staticmethod def get(): """:return: the compiled regular expression for the JIRA pattern""" return BangPattern.__pattern + # use re.match() rather than re.search() to enforce pattern at the beginning match = re.match(BangPattern.get(), line.strip()) return match.group(1) if match else "" + def get_branch_name() -> str | None: """:return: the name of the current branch""" git_proc = subprocess.run( - shlex.split("git branch --list"), - stdout=subprocess.PIPE, - text=True, - check=False + shlex.split("git branch --list"), stdout=subprocess.PIPE, text=True, check=False ) if git_proc.returncode != 0: @@ -169,7 +168,7 @@ def check_commit_message(filepath: str) -> tuple[bool, str]: def check_commit_msg(filepath: str) -> None: - """To be used a the Git commit-msg hook. + """To be used as the Git commit-msg hook. Exit with non-0 status if the commit message is deemed invalid. """ @@ -184,7 +183,7 @@ def check_commit_msg(filepath: str) -> None: def prepare_commit_msg(filepath: str, source: str | None = None) -> None: - """To be used a the Git prepare-commit-msg hook. + """To be used as the Git prepare-commit-msg hook. Will add the JIRA ID found in the branch name in case it is missing from the commit message. @@ -202,11 +201,7 @@ def prepare_commit_msg(filepath: str, source: str | None = None) -> None: return prefix_bang = "" - with open( - filepath, - "r+", - encoding="utf-8" - ) as message_file: + with open(filepath, "r+", encoding="utf-8") as message_file: message_has_jira_id = False message_lines = message_file.readlines() for line_index, line_content in enumerate(message_lines): @@ -214,7 +209,7 @@ def prepare_commit_msg(filepath: str, source: str | None = None) -> None: # test only the first non-comment line line_content = line_content.strip() prefix_bang = get_message_prefix_bang(line_content) - line_content = line_content[len(prefix_bang):].strip() + line_content = line_content[len(prefix_bang) :].strip() message_jira_id = get_jira_id(line_content) if not message_jira_id: message_lines[line_index] = ( @@ -231,7 +226,7 @@ def prepare_commit_msg(filepath: str, source: str | None = None) -> None: message_file.write("".join(message_lines)) -def main(args=None): +def main(): parser = argparse.ArgumentParser() parser.add_argument("msg_file", help="the message file") group = parser.add_mutually_exclusive_group(required=True) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..912dfba --- /dev/null +++ b/poetry.lock @@ -0,0 +1,393 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "astroid" +version = "3.3.8" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, + {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[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.6.10" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "dill" +version = "0.3.9" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[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 = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +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.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pylint" +version = "3.3.3" +description = "python code static checker" +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"}, + {file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"}, +] + +[package.dependencies] +astroid = ">=3.3.8,<=3.4.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, +] +isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pylint-pytest" +version = "1.1.7" +description = "A Pylint plugin to suppress pytest-related false positives." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pylint-pytest-1.1.7.tar.gz", hash = "sha256:7a38be02c014eb6d98791eb978e79ed292f1904d3a518289c6d7ac4fb4122e98"}, + {file = "pylint_pytest-1.1.7-py3-none-any.whl", hash = "sha256:5d687a2f4b17e85654fc2a8f04944761efb11cb15dc46d008f420c377b149151"}, +] + +[package.dependencies] +pylint = ">=2" +pytest = ">=4.6" + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "639afa119cab7b225655e4ffcac453e792d133646aaa3bc9d967cbc72a96d97d" diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..dbe7a79 --- /dev/null +++ b/pylintrc @@ -0,0 +1,618 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on=F,E,W + +# Specify a score threshold to be exceeded before program exits with error. +#fail-under= + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. The default value ignores Emacs file +# locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins=pylint_pytest + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.9 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + missing-module-docstring, + missing-function-docstring, + missing-class-docstring, + fixme + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + x, + y, + z, + ex, + Run, + id, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=6 + +# Maximum number of attributes for a class (see R0902). +max-attributes=15 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=6 + +# Maximum number of branch for function / method body. +max-branches=13 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=65 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException, + builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/pyproject.toml b/pyproject.toml index 7f0f125..00d8ca1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "mirageoscience.pre-commit-hooks" -version = "1.0.2" +version = "1.1.0" license = "MIT" description = "" @@ -24,6 +24,7 @@ python = "^3.10" [tool.poetry.group.dev.dependencies] Pygments = "*" pylint = "*" +pylint-pytest = "*" pytest = "*" pytest-mock = "*" pytest-cov = "*" @@ -76,19 +77,13 @@ show_error_context = true show_column_numbers = true check_untyped_defs = true -plugins = [ - "numpy.typing.mypy_plugin" -] - [tool.pytest.ini_options] #addopts = [tool.coverage.run] branch = true -source = ["my_app"] -omit = [ - "my_app/commands/hello_world.py" -] +source = ["mirageoscience"] +omit = [] [tool.coverage.report] exclude_lines = [ @@ -98,7 +93,7 @@ exclude_lines = [ "pragma: no cover" ] -fail_under = 80 +fail_under = 60 [tool.coverage.html] skip_empty = true diff --git a/tests/__init__.py b/tests/__init__.py index 2ec2c6f..6c33ff4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,7 +1,8 @@ -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of mirageoscience.pre-commit-hooks package. ' -# ' -# All rights reserved. ' -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' \ No newline at end of file +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2024-2025 Mira Geoscience Ltd. ' +# ' +# This file is part of mirageoscience.pre-commit-hooks package. ' +# ' +# mirageoscience.pre-commit-hooks is distributed under the terms and conditions ' +# of the MIT License (see LICENSE file at the root of this source code package). ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' diff --git a/tests/check_copyright_test.py b/tests/check_copyright_test.py new file mode 100644 index 0000000..7d95c78 --- /dev/null +++ b/tests/check_copyright_test.py @@ -0,0 +1,129 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2025 Mira Geoscience Ltd. ' +# ' +# This file is part of mirageoscience.pre-commit-hooks package. ' +# ' +# mirageoscience.pre-commit-hooks is distributed under the terms and conditions ' +# of the MIT License (see LICENSE file at the root of this source code package). ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +from __future__ import annotations + +import sys +from datetime import date +from pathlib import Path +from unittest import mock + +import pytest + +from mirageoscience.hooks.check_copyright import _FULL_SCAN_FILE_NAMES, check_files +from mirageoscience.hooks.check_copyright import main as check_copyright_main + + +def test_valid_copyright(tmp_path: Path): + current_year = date.today().year + file_content = f"\n\n\n# Copyright (c) {current_year}\n" + test_file = tmp_path / "test_current_year.py" + test_file.write_text(file_content, encoding="utf-8") + assert check_files([str(test_file)]) + + +def test_old_copyright_date(tmp_path: Path): + current_year = date.today().year + file_content = f"\n\n\n# Copyright (c) {current_year - 1}\n" + test_file = tmp_path / "test_old_year.py" + test_file.write_text(file_content, encoding="utf-8") + assert not check_files([str(test_file)]) + + +def test_missing_copyright(tmp_path: Path): + test_file = tmp_path / "test_missing.py" + test_file.write_text("No statement here", encoding="utf-8") + assert not check_files([str(test_file)]) + + +@pytest.mark.parametrize("year_is_current", [True, False]) +def test_full_scan_custom_files(tmp_path: Path, year_is_current: bool): + statement_year = date.today().year + if not year_is_current: + statement_year -= 1 + file_content = "\n Not Here" * 100 + f"# Copyright (c) {statement_year}\n" + file_names = ["something.else", "another.thing"] + test_files = [] + for f in file_names: + test_file = tmp_path / f + test_file.write_text(file_content, encoding="utf-8") + test_files.append(str(test_file)) + assert year_is_current == check_files(test_files, full_scan_files=file_names) + + +def test_copyright_not_found_further_down(tmp_path: Path): + current_year = date.today().year + file_content = "\n Not Here" * 100 + f"# Copyright (c) {current_year}\n" + file_name = "something.else" + test_file = tmp_path / file_name + test_file.write_text(file_content, encoding="utf-8") + assert not check_files([str(test_file)]) + + +@pytest.mark.parametrize("outdated_file_position", [0, 1, 2]) +def test_multiple_good_files(tmp_path: Path, outdated_file_position: int): + current_year = date.today().year + good_file_content = f"# Copyright (c) {current_year}\n" + outdated_file_content = f"# Copyright (c) {current_year - 1}\n" + test_file_good1 = tmp_path / "test_good1.py" + test_file_good1.write_text(good_file_content, encoding="utf-8") + test_file_good2 = tmp_path / "test_good2.py" + test_file_good2.write_text(good_file_content, encoding="utf-8") + test_file_outdated = tmp_path / "test_outdated.py" + test_file_outdated.write_text(outdated_file_content, encoding="utf-8") + test_files = [ + str(test_file_good1), + str(test_file_good2), + ] + test_files.insert(outdated_file_position, str(test_file_outdated)) + assert not check_files(test_files) + + +def test_main_passes_args_to_check_files(): + test_args = ["script_name", "file1.py", "file2.py"] + with mock.patch.object(sys, "argv", test_args): + with mock.patch( + "mirageoscience.hooks.check_copyright.check_files" + ) as mock_check_files: + mock_check_files.return_value = True + check_copyright_main() + mock_check_files.assert_called_once_with( + ["file1.py", "file2.py"], + ["README.rst", "README-dev.rst", "package.rst"], + ) + + +def test_main_exits_with_error(): + test_args = ["script_name", "file1.py"] + with mock.patch.object(sys, "argv", test_args): + with mock.patch( + "mirageoscience.hooks.check_copyright.check_files" + ) as mock_check_files: + mock_check_files.return_value = False + with pytest.raises(SystemExit) as e: + check_copyright_main() + assert e.value.code == 1 + + +def test_main_with_full_scan_files(): + test_args = [ + "script_name", + "file1.py", + "--full-scan-files", + "full_file1.py,full_file2.py", + ] + with mock.patch.object(sys, "argv", test_args): + with mock.patch( + "mirageoscience.hooks.check_copyright.check_files" + ) as mock_check_files: + mock_check_files.return_value = True + check_copyright_main() + mock_check_files.assert_called_once_with( + ["file1.py"], [*_FULL_SCAN_FILE_NAMES, "full_file1.py", "full_file2.py"] + ) diff --git a/tests/git_message_hook.py b/tests/git_message_hook.py deleted file mode 100644 index d167431..0000000 --- a/tests/git_message_hook.py +++ /dev/null @@ -1,98 +0,0 @@ -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of mirageoscience.pre-commit-hooks package. ' -# ' -# All rights reserved. ' -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -from __future__ import annotations -import pytest - -from mirageoscience.hooks.git_message_hook import * - - -@pytest.fixture -def mock_get_branch_name(mocker): - def _mock_get_branch_name(branch_name): - mocker.patch('mirageoscience.hooks.git_message_hook.get_branch_name', - return_value=branch_name) - return get_jira_id(branch_name) - return _mock_get_branch_name - - -def test_get_jira_id(): - text = "[GEOPY-1233] Git commit message" - assert get_jira_id(text) == "GEOPY-1233" - - -def test_get_message_prefix_bang_with_bang(): - """Tests if get_message_prefix_bang can extract the prefix bang from a line.""" - line = "fixup! This is a fix" - expected_prefix_bang = "fixup! " - actual_prefix_bang = get_message_prefix_bang(line) - assert actual_prefix_bang == expected_prefix_bang - - -def test_get_message_prefix_bang_no_bang(): - """Tests if get_message_prefix_bang returns empty string for a line without bang.""" - line = "This is a commit message" - expected_prefix_bang = "" - actual_prefix_bang = get_message_prefix_bang(line) - assert actual_prefix_bang == expected_prefix_bang - - -def test_check_commit_message_valid_with_message_jira(mock_get_branch_name): - """Test avec identifiant JIRA dans le message de commit""" - branch_name = "feature_branch" - mock_get_branch_name(branch_name) - message_content = "GEOPY-123 Fix a bug xx" - filepath = "test_commit_message.txt" - with open(filepath, "w") as f: - f.write(message_content) - - is_valid, error_message = check_commit_message(filepath) - assert is_valid - assert error_message == "" - - -def test_check_commit_message_invalid_no_jira(mock_get_branch_name): - """Test without JIRA id in the branch name or message content""" - branch_name = "feature_branch" - mock_get_branch_name(branch_name) - message_content = "Fix a bug" - filepath = "test_commit_message.txt" - with open(filepath, "w") as f: - f.write(message_content) - - is_valid, error_message = check_commit_message(filepath) - assert not is_valid - assert error_message == "Either the branch name or the commit message must start with a JIRA ID." - - -def test_check_commit_message_invalid_different_jira(mock_get_branch_name): - """Test with different JIRA id in the branch name and in the message content""" - branch_name = "GEOPY-123_fix_bug" - mock_get_branch_name(branch_name) - message_content = "GI-456 Fix a bug" - filepath = "test_commit_message.txt" - with open(filepath, "w") as f: - f.write(message_content) - - is_valid, error_message = check_commit_message(filepath) - assert not is_valid - assert error_message.startswith("Different JIRA ID in commit message") - - -def test_check_commit_message_invalid_short_message(mock_get_branch_name): - """Test with a too short message content""" - branch_name = "GEOPY-123_fix_bug" - mock_get_branch_name(branch_name) - message_content = "Fix" - filepath = "test_commit_message.txt" - with open(filepath, "w") as f: - f.write(message_content) - - is_valid, error_message = check_commit_message(filepath) - assert not is_valid - assert error_message.startswith("First line of commit message must be at least") diff --git a/tests/git_message_hook_test.py b/tests/git_message_hook_test.py new file mode 100644 index 0000000..383b252 --- /dev/null +++ b/tests/git_message_hook_test.py @@ -0,0 +1,149 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2024-2025 Mira Geoscience Ltd. ' +# ' +# This file is part of mirageoscience.pre-commit-hooks package. ' +# ' +# mirageoscience.pre-commit-hooks is distributed under the terms and conditions ' +# of the MIT License (see LICENSE file at the root of this source code package). ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +from __future__ import annotations + +import sys +from pathlib import Path +from unittest import mock + +import pytest + +from mirageoscience.hooks.git_message_hook import ( + check_commit_message, + get_jira_id, + get_message_prefix_bang, +) +from mirageoscience.hooks.git_message_hook import ( + main as git_message_hook_main, +) + + +@pytest.fixture +def mock_get_branch_name(mocker): + def _mock_get_branch_name(branch_name): + mocker.patch( + "mirageoscience.hooks.git_message_hook.get_branch_name", + return_value=branch_name, + ) + return get_jira_id(branch_name) + + return _mock_get_branch_name + + +def test_get_jira_id(): + text = "[GEOPY-1233] Git commit message" + assert get_jira_id(text) == "GEOPY-1233" + + +def test_get_message_prefix_bang_with_bang(): + """Tests if get_message_prefix_bang can extract the prefix bang from a line.""" + line = "fixup! This is a fix" + expected_prefix_bang = "fixup! " + actual_prefix_bang = get_message_prefix_bang(line) + assert actual_prefix_bang == expected_prefix_bang + + +def test_get_message_prefix_bang_no_bang(): + """Tests if get_message_prefix_bang returns empty string for a line without bang.""" + line = "This is a commit message" + expected_prefix_bang = "" + actual_prefix_bang = get_message_prefix_bang(line) + assert actual_prefix_bang == expected_prefix_bang + + +def test_check_commit_message_valid_with_message_jira( + mock_get_branch_name, tmp_path: Path +): + """Test avec identifiant JIRA dans le message de commit""" + branch_name = "feature_branch" + mock_get_branch_name(branch_name) + message_content = "GEOPY-123 Fix a bug xx" + filepath = tmp_path / "test_commit_message.txt" + filepath.write_text(message_content, encoding="utf-8") + + is_valid, error_message = check_commit_message(str(filepath)) + assert is_valid + assert error_message == "" + + +def test_check_commit_message_invalid_no_jira(mock_get_branch_name, tmp_path: Path): + """Test without JIRA id in the branch name or message content""" + branch_name = "feature_branch" + mock_get_branch_name(branch_name) + message_content = "Fix a bug" + filepath = tmp_path / "test_commit_message.txt" + filepath.write_text(message_content, encoding="utf-8") + + is_valid, error_message = check_commit_message(str(filepath)) + assert not is_valid + assert ( + error_message + == "Either the branch name or the commit message must start with a JIRA ID." + ) + + +def test_check_commit_message_invalid_different_jira( + mock_get_branch_name, tmp_path: Path +): + """Test with different JIRA id in the branch name and in the message content""" + branch_name = "GEOPY-123_fix_bug" + mock_get_branch_name(branch_name) + message_content = "GI-456 Fix a bug" + filepath = tmp_path / "test_commit_message.txt" + filepath.write_text(message_content, encoding="utf-8") + + is_valid, error_message = check_commit_message(str(filepath)) + assert not is_valid + assert error_message.startswith("Different JIRA ID in commit message") + + +def test_check_commit_message_invalid_short_message( + mock_get_branch_name, tmp_path: Path +): + """Test with a too short message content""" + branch_name = "GEOPY-123_fix_bug" + mock_get_branch_name(branch_name) + message_content = "Fix" + filepath = tmp_path / "test_commit_message.txt" + filepath.write_text(message_content, encoding="utf-8") + + is_valid, error_message = check_commit_message(str(filepath)) + assert not is_valid + assert error_message.startswith("First line of commit message must be at least") + + +def test_main_calls_prepare_commit_msg(): + test_args = ["script_name", "--prepare", "msg_file"] + with mock.patch.object(sys, "argv", test_args): + with mock.patch( + "mirageoscience.hooks.git_message_hook.prepare_commit_msg" + ) as mock_prepare_commit_msg: + git_message_hook_main() + mock_prepare_commit_msg.assert_called_once_with("msg_file", *[]) + + +def test_main_calls_check_commit_msg(): + test_args = ["script_name", "--check", "msg_file"] + with mock.patch.object(sys, "argv", test_args): + with mock.patch( + "mirageoscience.hooks.git_message_hook.check_commit_msg" + ) as mock_check_commit_msg: + git_message_hook_main() + mock_check_commit_msg.assert_called_once_with("msg_file") + + +def test_main_with_remaining_args(): + test_args = ["script_name", "--prepare", "msg_file", "arg1", "arg2"] + with mock.patch.object(sys, "argv", test_args): + with mock.patch( + "mirageoscience.hooks.git_message_hook.prepare_commit_msg" + ) as mock_prepare_commit_msg: + git_message_hook_main() + mock_prepare_commit_msg.assert_called_once_with("msg_file", "arg1", "arg2") diff --git a/tests/test_commit_message.txt b/tests/test_commit_message.txt deleted file mode 100644 index 9f9636b..0000000 --- a/tests/test_commit_message.txt +++ /dev/null @@ -1 +0,0 @@ -Fix \ No newline at end of file diff --git a/tests/version_test.py b/tests/version_test.py new file mode 100644 index 0000000..3878ffa --- /dev/null +++ b/tests/version_test.py @@ -0,0 +1,35 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2024-2025 Mira Geoscience Ltd. ' +# ' +# This file is part of mirageoscience.pre-commit-hooks package. ' +# ' +# mirageoscience.pre-commit-hooks is distributed under the terms and conditions ' +# of the MIT License (see LICENSE file at the root of this source code package). ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +from __future__ import annotations + +from pathlib import Path + +import tomli as toml +from packaging.version import Version + +from mirageoscience import hooks as mira_hooks + + +def get_pyproject_version(): + path = Path(__file__).resolve().parents[1] / "pyproject.toml" + + with open(str(path), encoding="utf-8") as file: + pyproject = toml.loads(file.read()) + + return pyproject["tool"]["poetry"]["version"] + + +def test_version_is_consistent(): + assert mira_hooks.__version__ == get_pyproject_version() + + +def test_version_is_pep440(): + version = Version(mira_hooks.__version__) + assert version is not None