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