diff --git a/changelog/8096.improvement.rst b/changelog/8096.improvement.rst new file mode 100644 index 00000000000..9b66794b093 --- /dev/null +++ b/changelog/8096.improvement.rst @@ -0,0 +1 @@ +Clearer error message for invalid ``-W`` filters, listing valid actions and the ``-Wait`` pitfall. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c7bd3e1afab..e4a699c6562 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -2320,6 +2320,11 @@ def _strtobool(val: str) -> bool: raise ValueError(f"invalid truth value {val!r}") +WARNING_FILTER_DOC_URL = ( + "https://docs.python.org/3/library/warnings.html#describing-warning-filters" +) + + @lru_cache(maxsize=50) def parse_warning_filter( arg: str, *, escape: bool @@ -2331,6 +2336,8 @@ def parse_warning_filter( * Does not apply the filter. * Escaping is optional. * Raises UsageError so we get nice error messages on failure. + * Invalid actions name the valid choices, and hint at the `-W`/`-Wait` + short-option pitfall when relevant. """ __tracebackhide__ = True error_template = dedent( @@ -2347,16 +2354,13 @@ def parse_warning_filter( parts = arg.split(":") if len(parts) > 5: - doc_url = ( - "https://docs.python.org/3/library/warnings.html#describing-warning-filters" - ) error = dedent( f"""\ Too many fields ({len(parts)}), expected at most 5 separated by colons: action:message:category:module:line - For more information please consult: {doc_url} + For more information please consult: {WARNING_FILTER_DOC_URL} """ ) raise UsageError(error_template.format(error=error)) @@ -2367,7 +2371,16 @@ def parse_warning_filter( try: action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined] except warnings._OptionError as e: - raise UsageError(error_template.format(error=str(e))) from None + hint = ( + " (choose from: default, error, ignore, always, all, module, once)." + f" See {WARNING_FILTER_DOC_URL}" + ) + if action_.lower() == "ait": + hint = ( + " Note that '-W' takes a value, so '-Wait' is parsed as '-W ait'." + + hint + ) + raise UsageError(error_template.format(error=f"{e}{hint}")) from None try: category: type[Warning] = _resolve_warning_category(category_) except ImportError: diff --git a/testing/test_config.py b/testing/test_config.py index dad1653e299..23c97b2e0e2 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -3112,6 +3112,21 @@ def test_parse_warning_filter_failure(arg: str) -> None: parse_warning_filter(arg, escape=True) +@pytest.mark.parametrize( + "arg, expect_wait_hint", + [("ait", True), ("AIT", True), ("FOO", False)], +) +def test_parse_warning_filter_invalid_action_hint( + arg: str, expect_wait_hint: bool +) -> None: + """Invalid -W actions show valid choices; the -Wait pitfall hint is scoped to that case.""" + with pytest.raises( + pytest.UsageError, match=r"invalid action.*choose from" + ) as exc_info: + parse_warning_filter(arg, escape=True) + assert ("-Wait" in str(exc_info.value)) == expect_wait_hint + + class TestDebugOptions: def test_without_debug_does_not_write_log(self, pytester: Pytester) -> None: result = pytester.runpytest() diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 0344cb453d8..3728bb844d5 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -563,6 +563,23 @@ def test_invalid_regex_in_filterwarning(self, pytester: Pytester) -> None: ) +def test_dash_w_shows_wait_hint_on_usage_error(pytester: Pytester) -> None: + """`-Wait` is parsed as `-W ait`; the hint should point at this.""" + result = pytester.runpytest("-Wait") + assert result.ret == pytest.ExitCode.USAGE_ERROR + result.stderr.fnmatch_lines( + [ + "ERROR: while parsing the following warning configuration:", + "", + " ait", + "", + "This error occurred:", + "", + "invalid action: 'ait'*-Wait*choose from*", + ] + ) + + @pytest.mark.skip("not relevant until pytest 10.0") @pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> None: