From 0e7aa406f71568cfbf9fc6a5c8e26e8ac647f1f9 Mon Sep 17 00:00:00 2001 From: colasriev <156931839+colasriev@users.noreply.github.com> Date: Thu, 22 May 2025 13:44:36 +0200 Subject: [PATCH 1/7] Use PEP518 compliant pyproject format Parameters should be under the [tool.coverage-threshold] key. If not found, fall back to [coverage-threshold]. --- coverage_threshold/cli/read_config.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/coverage_threshold/cli/read_config.py b/coverage_threshold/cli/read_config.py index 24c9a53..7c06ceb 100644 --- a/coverage_threshold/cli/read_config.py +++ b/coverage_threshold/cli/read_config.py @@ -9,11 +9,16 @@ def read_config(config_file_name: Optional[str]) -> Config: DEFAULT_FILENAME = "./pyproject.toml" if config_file_name is not None: - return Config.parse(toml.load(config_file_name)["coverage-threshold"]) + if not os.path.isfile(config_file_name): + raise FileNotFoundError(f"Config file {config_file_name} not found") else: - if os.path.isfile(DEFAULT_FILENAME): - return Config.parse( - toml.load(DEFAULT_FILENAME).get("coverage-threshold", {}) - ) - else: - return Config() + config_file_name = DEFAULT_FILENAME + if os.path.isfile(config_file_name): + try: + # PEP 518 compliant version + return Config.parse(toml.load(config_file_name)["tool"]["coverage-threshold"]) + except KeyError: + # Legacy version + return Config.parse(toml.load(config_file_name).get("coverage-threshold", {})) + else: + return Config() From 1106d6cf09a072f237bb142d7c5b78c3b97de9af Mon Sep 17 00:00:00 2001 From: DeanWay Date: Sat, 24 May 2025 09:36:33 -0400 Subject: [PATCH 2/7] fix lint --- coverage_threshold/cli/read_config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/coverage_threshold/cli/read_config.py b/coverage_threshold/cli/read_config.py index 7c06ceb..a17001f 100644 --- a/coverage_threshold/cli/read_config.py +++ b/coverage_threshold/cli/read_config.py @@ -16,9 +16,13 @@ def read_config(config_file_name: Optional[str]) -> Config: if os.path.isfile(config_file_name): try: # PEP 518 compliant version - return Config.parse(toml.load(config_file_name)["tool"]["coverage-threshold"]) + return Config.parse( + toml.load(config_file_name)["tool"]["coverage-threshold"] + ) except KeyError: # Legacy version - return Config.parse(toml.load(config_file_name).get("coverage-threshold", {})) + return Config.parse( + toml.load(config_file_name).get("coverage-threshold", {}) + ) else: return Config() From ef71fa2f9dfc49ee72409b0831c286db1c39044a Mon Sep 17 00:00:00 2001 From: DeanWay Date: Sat, 24 May 2025 09:41:44 -0400 Subject: [PATCH 3/7] add integration test for old pyproject.toml key --- example_project/legacy.pyproject.toml | 2 ++ example_project/pyproject.toml | 2 +- tests/integration/test_cli.py | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 example_project/legacy.pyproject.toml diff --git a/example_project/legacy.pyproject.toml b/example_project/legacy.pyproject.toml new file mode 100644 index 0000000..b59c6db --- /dev/null +++ b/example_project/legacy.pyproject.toml @@ -0,0 +1,2 @@ +[coverage-threshold] +line_coverage_min = 75.0 diff --git a/example_project/pyproject.toml b/example_project/pyproject.toml index b59c6db..9bb6a5c 100644 --- a/example_project/pyproject.toml +++ b/example_project/pyproject.toml @@ -1,2 +1,2 @@ -[coverage-threshold] +[tool.coverage-threshold] line_coverage_min = 75.0 diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index cd391f8..e6c02a9 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -29,6 +29,21 @@ def test_cli_runs_successfully_on_example_project() -> None: assert process.stdout == SUCCESS_MESSAGE.encode("utf-8") +# backwards compatibilty for before this project became compliant with pep518 +def test_cli_runs_successfully_on_example_project_with_legacy_pyproject_format() -> ( + None +): + process = subprocess.run( + ["coverage-threshold", "--config", "legacy.pyproject.toml"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=EXAMPLE_PROJECT_PATH, + ) + assert process.returncode == 0 + assert process.stderr == b"" + assert process.stdout == SUCCESS_MESSAGE.encode("utf-8") + + def test_cli_fails() -> None: process = subprocess.run( ["coverage-threshold", "--line-coverage-min", "100.0"], From fa989f463cd246a426588dbe51d2047e80dd6944 Mon Sep 17 00:00:00 2001 From: DeanWay Date: Sat, 24 May 2025 09:49:13 -0400 Subject: [PATCH 4/7] refactor to prevent KeyError catch block from potentially being triggered by toml.load or Config.parse --- coverage_threshold/cli/read_config.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/coverage_threshold/cli/read_config.py b/coverage_threshold/cli/read_config.py index a17001f..b1803f6 100644 --- a/coverage_threshold/cli/read_config.py +++ b/coverage_threshold/cli/read_config.py @@ -14,15 +14,13 @@ def read_config(config_file_name: Optional[str]) -> Config: else: config_file_name = DEFAULT_FILENAME if os.path.isfile(config_file_name): + toml_dict = toml.load(config_file_name) try: # PEP 518 compliant version - return Config.parse( - toml.load(config_file_name)["tool"]["coverage-threshold"] - ) + config_dict = toml_dict["tool"]["coverage-threshold"] except KeyError: # Legacy version - return Config.parse( - toml.load(config_file_name).get("coverage-threshold", {}) - ) + config_dict = toml_dict.get("coverage-threshold", {}) + return Config.parse(config_dict) else: return Config() From 6c650a48a88080144aec7b40637ae4893f60c269 Mon Sep 17 00:00:00 2001 From: DeanWay Date: Sat, 24 May 2025 10:10:46 -0400 Subject: [PATCH 5/7] move read_config testing out of example project, into unit tests --- tests/integration/test_cli.py | 15 -------- tests/unit/config/__init__.py | 0 tests/unit/config/complex.pyproject.toml | 20 ++++++++++ .../unit/config}/legacy.pyproject.toml | 0 tests/unit/config/pyproject.toml | 2 + tests/unit/config/test_read_config.py | 37 +++++++++++++++++++ 6 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 tests/unit/config/__init__.py create mode 100644 tests/unit/config/complex.pyproject.toml rename {example_project => tests/unit/config}/legacy.pyproject.toml (100%) create mode 100644 tests/unit/config/pyproject.toml create mode 100644 tests/unit/config/test_read_config.py diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index e6c02a9..cd391f8 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -29,21 +29,6 @@ def test_cli_runs_successfully_on_example_project() -> None: assert process.stdout == SUCCESS_MESSAGE.encode("utf-8") -# backwards compatibilty for before this project became compliant with pep518 -def test_cli_runs_successfully_on_example_project_with_legacy_pyproject_format() -> ( - None -): - process = subprocess.run( - ["coverage-threshold", "--config", "legacy.pyproject.toml"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=EXAMPLE_PROJECT_PATH, - ) - assert process.returncode == 0 - assert process.stderr == b"" - assert process.stdout == SUCCESS_MESSAGE.encode("utf-8") - - def test_cli_fails() -> None: process = subprocess.run( ["coverage-threshold", "--line-coverage-min", "100.0"], diff --git a/tests/unit/config/__init__.py b/tests/unit/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/config/complex.pyproject.toml b/tests/unit/config/complex.pyproject.toml new file mode 100644 index 0000000..7d1f290 --- /dev/null +++ b/tests/unit/config/complex.pyproject.toml @@ -0,0 +1,20 @@ +[tool.coverage-threshold] +line_coverage_min = 95 +file_line_coverage_min = 95 +branch_coverage_min = 50 + +[tool.coverage-threshold.modules."src/cli/"] +file_line_coverage_min = 40 + +[tool.coverage-threshold.modules."src/cli/my_command.py"] +file_line_coverage_min = 100 + +[tool.coverage-threshold.modules."src/lib/"] +file_line_coverage_min = 100 +file_branch_coverage_min = 100 + +[tool.coverage-threshold.modules."src/model/"] +file_line_coverage_min = 100 + +[tool.coverage-threshold.modules."src/__main__.py"] +file_line_coverage_min = 0 diff --git a/example_project/legacy.pyproject.toml b/tests/unit/config/legacy.pyproject.toml similarity index 100% rename from example_project/legacy.pyproject.toml rename to tests/unit/config/legacy.pyproject.toml diff --git a/tests/unit/config/pyproject.toml b/tests/unit/config/pyproject.toml new file mode 100644 index 0000000..9bb6a5c --- /dev/null +++ b/tests/unit/config/pyproject.toml @@ -0,0 +1,2 @@ +[tool.coverage-threshold] +line_coverage_min = 75.0 diff --git a/tests/unit/config/test_read_config.py b/tests/unit/config/test_read_config.py new file mode 100644 index 0000000..707b541 --- /dev/null +++ b/tests/unit/config/test_read_config.py @@ -0,0 +1,37 @@ +import os +from decimal import Decimal + +from coverage_threshold.cli.read_config import read_config + + +def test_read_config_parses_default_pyproject_format(): + config = read_config(path_to_test_config("pyproject.toml")) + assert config.line_coverage_min == Decimal("75.0") + assert config.modules is None + + +# backwards compatibilty for before this project became compliant with pep518 +def test_read_config_parses_legacy_pyproject_format(): + config = read_config(path_to_test_config("legacy.pyproject.toml")) + assert config.line_coverage_min == Decimal("75.0") + assert config.modules is None + + +def test_read_config_parses_complex_pyproject_format(): + config = read_config(path_to_test_config("complex.pyproject.toml")) + assert config.line_coverage_min == Decimal("95.0") + assert config.file_line_coverage_min == Decimal("95.0") + assert config.branch_coverage_min == Decimal("50.0") + assert config.modules is not None + assert config.modules["src/cli/"].file_line_coverage_min == Decimal("40.0") + assert config.modules["src/cli/my_command.py"].file_line_coverage_min == Decimal( + "100" + ) + assert config.modules["src/lib/"].file_line_coverage_min == Decimal("100") + assert config.modules["src/lib/"].file_branch_coverage_min == Decimal("100") + assert config.modules["src/model/"].file_line_coverage_min == Decimal("100") + assert config.modules["src/__main__.py"].file_line_coverage_min == Decimal("0") + + +def path_to_test_config(filename: str) -> str: + return os.path.join(os.path.dirname(__file__), filename) From c366bd437a7ee8d955381b45b0f414b6b3119e44 Mon Sep 17 00:00:00 2001 From: DeanWay Date: Sat, 24 May 2025 10:11:15 -0400 Subject: [PATCH 6/7] update readme and pyproject.toml to use pep518 format --- README.md | 30 ++++++++++++++++-------------- pyproject.toml | 14 +++++++------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3c7bae5..374f691 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ A command line tool for checking coverage reports against configurable coverage minimums. Currently built for use around python's [coverage](https://pypi.org/project/coverage/) - ### Installation + `pip install coverage-threshold` also recommended: @@ -12,7 +12,9 @@ also recommended: `pip install coverage` ### Usage + Typical execution: + ```bash coverage run -m pytest tests/ # or any test runner here coverage json @@ -52,34 +54,34 @@ optional arguments: --config CONFIG path to config file (default: ./pyproject.toml) ``` - ### Config the current expected config file format is [toml](https://toml.io/en/) the default config file used is `pyproject.toml` but and alternative path can be specified with `--config` example config: + ```toml -[coverage-threshold] +[tool.coverage-threshold] line_coverage_min = 95 file_line_coverage_min = 95 branch_coverage_min = 50 - [coverage-threshold.modules."src/cli/"] - file_line_coverage_min = 40 +[tool.coverage-threshold.modules."src/cli/"] +file_line_coverage_min = 40 - [coverage-threshold.modules."src/cli/my_command.py"] - file_line_coverage_min = 100 +[tool.coverage-threshold.modules."src/cli/my_command.py"] +file_line_coverage_min = 100 - [coverage-threshold.modules."src/lib/"] - file_line_coverage_min = 100 - file_branch_coverage_min = 100 +[tool.coverage-threshold.modules."src/lib/"] +file_line_coverage_min = 100 +file_branch_coverage_min = 100 - [coverage-threshold.modules."src/model/"] - file_line_coverage_min = 100 +[tool.coverage-threshold.modules."src/model/"] +file_line_coverage_min = 100 - [coverage-threshold.modules."src/__main__.py"] - file_line_coverage_min = 0 +[tool.coverage-threshold.modules."src/__main__.py"] +file_line_coverage_min = 0 ``` Each string key in `config.modules` is treated as a path prefix, where the longest matching prefix is used to configure the coverage thresholds for each file diff --git a/pyproject.toml b/pyproject.toml index d864e1f..1c17ff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,15 +6,15 @@ build-backend = "setuptools.build_meta" profile = "black" src_paths = ["coverage_threshold", "tests"] -[coverage-threshold] +[tool.coverage-threshold] line_coverage_min = 0 file_line_coverage_min = 100 file_branch_coverage_min = 100 - [coverage-threshold.modules."coverage_threshold/cli/"] - file_line_coverage_min = 0 - file_branch_coverage_min = 0 +[tool.coverage-threshold.modules."coverage_threshold/cli/"] +file_line_coverage_min = 0 +file_branch_coverage_min = 0 - [coverage-threshold.modules."coverage_threshold/__main__.py"] - file_line_coverage_min = 0 - file_branch_coverage_min = 0 +[tool.coverage-threshold.modules."coverage_threshold/__main__.py"] +file_line_coverage_min = 0 +file_branch_coverage_min = 0 From 3b62bcef465b04856308a15a78dbd3e196b56578 Mon Sep 17 00:00:00 2001 From: DeanWay Date: Sat, 24 May 2025 10:11:58 -0400 Subject: [PATCH 7/7] add missing return types --- tests/unit/config/test_read_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/config/test_read_config.py b/tests/unit/config/test_read_config.py index 707b541..1985fea 100644 --- a/tests/unit/config/test_read_config.py +++ b/tests/unit/config/test_read_config.py @@ -4,20 +4,20 @@ from coverage_threshold.cli.read_config import read_config -def test_read_config_parses_default_pyproject_format(): +def test_read_config_parses_default_pyproject_format() -> None: config = read_config(path_to_test_config("pyproject.toml")) assert config.line_coverage_min == Decimal("75.0") assert config.modules is None # backwards compatibilty for before this project became compliant with pep518 -def test_read_config_parses_legacy_pyproject_format(): +def test_read_config_parses_legacy_pyproject_format() -> None: config = read_config(path_to_test_config("legacy.pyproject.toml")) assert config.line_coverage_min == Decimal("75.0") assert config.modules is None -def test_read_config_parses_complex_pyproject_format(): +def test_read_config_parses_complex_pyproject_format() -> None: config = read_config(path_to_test_config("complex.pyproject.toml")) assert config.line_coverage_min == Decimal("95.0") assert config.file_line_coverage_min == Decimal("95.0")