diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 9b0e9305..d7d7318c 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -4,6 +4,7 @@ import argparse import contextlib +import inspect import io import logging import os @@ -297,6 +298,27 @@ def generate_quality_report( return reporter.total_percent_covered() +def _call_reporter_factory(factory_fn, reports, options): + """ + Call a plugin's ``diff_cover_report_quality`` implementation. + + Plugins are free to declare only the arguments they need (including none + at all, as documented in the README), so pass only what the function + actually accepts. + """ + available = {"reports": reports, "options": options} + try: + parameters = inspect.signature(factory_fn).parameters + except (TypeError, ValueError): # pragma: no cover - builtins/C callables + return factory_fn(**available) + + if any(p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters.values()): + return factory_fn(**available) + + kwargs = {name: value for name, value in available.items() if name in parameters} + return factory_fn(**kwargs) + + def main(argv=None, directory=None): """ Main entry point for the tool, script installed via pyproject.toml @@ -366,8 +388,8 @@ def main(argv=None, directory=None): reporter = QualityReporter(driver, input_reports, user_options) elif reporter_factory_fn: - reporter = reporter_factory_fn( - reports=input_reports, options=user_options + reporter = _call_reporter_factory( + reporter_factory_fn, input_reports, user_options ) percent_passing = generate_quality_report( diff --git a/diff_cover/hookspecs.py b/diff_cover/hookspecs.py index 3a79cc66..bfb8d079 100644 --- a/diff_cover/hookspecs.py +++ b/diff_cover/hookspecs.py @@ -4,9 +4,13 @@ @hookspec -def diff_cover_report_quality(): +def diff_cover_report_quality(reports, options): """ Return a 2-part tuple: - Quality plugin name - Object that implements the BaseViolationReporter protocol + + ``reports`` is the list of open pre-generated report file handles and + ``options`` the user options string; both are passed by ``diff-quality``. + A plugin may declare either or neither argument. """ diff --git a/tests/test_diff_quality_main.py b/tests/test_diff_quality_main.py index 98f11af4..57e9b63b 100644 --- a/tests/test_diff_quality_main.py +++ b/tests/test_diff_quality_main.py @@ -2,8 +2,10 @@ """Test for diff_cover.diff_quality - main""" +import pluggy import pytest +from diff_cover import hookspecs from diff_cover.diff_quality_tool import main, parse_quality_args @@ -147,3 +149,33 @@ def _run_main(report, argv): quality_reporter = report.call_args[0][0] assert quality_reporter.driver.name == "pylint" assert quality_reporter.options == "--foobar" + + +def test_plugin_may_declare_hook_arguments(): + """A plugin declaring reports/options must validate against the hookspec.""" + hookimpl = pluggy.HookimplMarker("diff_cover") + + class Plugin: + @hookimpl + def diff_cover_report_quality(self, reports, options): + return (reports, options) + + plugin_manager = pluggy.PluginManager("diff_cover") + plugin_manager.add_hookspecs(hookspecs) + plugin_manager.register(Plugin(), name="myplugin") + + +@pytest.mark.parametrize( + "factory,expected", + [ + (lambda: "none", "none"), + (lambda options: options, "--foobar"), + (lambda reports: reports, ["report"]), + (lambda reports, options: (reports, options), (["report"], "--foobar")), + (lambda **kwargs: kwargs, {"reports": ["report"], "options": "--foobar"}), + ], +) +def test_call_reporter_factory_passes_declared_arguments(factory, expected): + from diff_cover.diff_quality_tool import _call_reporter_factory + + assert _call_reporter_factory(factory, ["report"], "--foobar") == expected