Merge pull request #9223 from nicoddemus/better-filter-warnings-messages
This commit is contained in:
commit
61e506a63f
|
@ -0,0 +1,4 @@
|
||||||
|
Improved error messages when parsing warning filters.
|
||||||
|
|
||||||
|
Previously pytest would show an internal traceback, which besides ugly sometimes would hide the cause
|
||||||
|
of the problem (for example an ``ImportError`` while importing a specific warning type).
|
|
@ -13,9 +13,11 @@ import types
|
||||||
import warnings
|
import warnings
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
@ -1612,17 +1614,54 @@ def parse_warning_filter(
|
||||||
) -> Tuple[str, str, Type[Warning], str, int]:
|
) -> Tuple[str, str, Type[Warning], str, int]:
|
||||||
"""Parse a warnings filter string.
|
"""Parse a warnings filter string.
|
||||||
|
|
||||||
This is copied from warnings._setoption, but does not apply the filter,
|
This is copied from warnings._setoption with the following changes:
|
||||||
only parses it, and makes the escaping optional.
|
|
||||||
|
* Does not apply the filter.
|
||||||
|
* Escaping is optional.
|
||||||
|
* Raises UsageError so we get nice error messages on failure.
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
error_template = dedent(
|
||||||
|
f"""\
|
||||||
|
while parsing the following warning configuration:
|
||||||
|
|
||||||
|
{arg}
|
||||||
|
|
||||||
|
This error occurred:
|
||||||
|
|
||||||
|
{{error}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
parts = arg.split(":")
|
parts = arg.split(":")
|
||||||
if len(parts) > 5:
|
if len(parts) > 5:
|
||||||
raise warnings._OptionError(f"too many fields (max 5): {arg!r}")
|
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}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
raise UsageError(error_template.format(error=error))
|
||||||
|
|
||||||
while len(parts) < 5:
|
while len(parts) < 5:
|
||||||
parts.append("")
|
parts.append("")
|
||||||
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
|
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
|
||||||
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
|
try:
|
||||||
category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined]
|
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||||
|
except warnings._OptionError as e:
|
||||||
|
raise UsageError(error_template.format(error=str(e)))
|
||||||
|
try:
|
||||||
|
category: Type[Warning] = _resolve_warning_category(category_)
|
||||||
|
except Exception:
|
||||||
|
exc_info = ExceptionInfo.from_current()
|
||||||
|
exception_text = exc_info.getrepr(style="native")
|
||||||
|
raise UsageError(error_template.format(error=exception_text))
|
||||||
if message and escape:
|
if message and escape:
|
||||||
message = re.escape(message)
|
message = re.escape(message)
|
||||||
if module and escape:
|
if module and escape:
|
||||||
|
@ -1631,14 +1670,38 @@ def parse_warning_filter(
|
||||||
try:
|
try:
|
||||||
lineno = int(lineno_)
|
lineno = int(lineno_)
|
||||||
if lineno < 0:
|
if lineno < 0:
|
||||||
raise ValueError
|
raise ValueError("number is negative")
|
||||||
except (ValueError, OverflowError) as e:
|
except ValueError as e:
|
||||||
raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e
|
raise UsageError(
|
||||||
|
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
lineno = 0
|
lineno = 0
|
||||||
return action, message, category, module, lineno
|
return action, message, category, module, lineno
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_warning_category(category: str) -> Type[Warning]:
|
||||||
|
"""
|
||||||
|
Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
|
||||||
|
propagate so we can get access to their tracebacks (#9218).
|
||||||
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
if not category:
|
||||||
|
return Warning
|
||||||
|
|
||||||
|
if "." not in category:
|
||||||
|
import builtins as m
|
||||||
|
|
||||||
|
klass = category
|
||||||
|
else:
|
||||||
|
module, _, klass = category.rpartition(".")
|
||||||
|
m = __import__(module, None, None, [klass])
|
||||||
|
cat = getattr(m, klass)
|
||||||
|
if not issubclass(cat, Warning):
|
||||||
|
raise UsageError(f"{cat} is not a Warning subclass")
|
||||||
|
return cast(Type[Warning], cat)
|
||||||
|
|
||||||
|
|
||||||
def apply_warning_filters(
|
def apply_warning_filters(
|
||||||
config_filters: Iterable[str], cmdline_filters: Iterable[str]
|
config_filters: Iterable[str], cmdline_filters: Iterable[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -2042,11 +2042,25 @@ def test_parse_warning_filter(
|
||||||
assert parse_warning_filter(arg, escape=escape) == expected
|
assert parse_warning_filter(arg, escape=escape) == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("arg", [":" * 5, "::::-1", "::::not-a-number"])
|
@pytest.mark.parametrize(
|
||||||
|
"arg",
|
||||||
|
[
|
||||||
|
# Too much parts.
|
||||||
|
":" * 5,
|
||||||
|
# Invalid action.
|
||||||
|
"FOO::",
|
||||||
|
# ImportError when importing the warning class.
|
||||||
|
"::test_parse_warning_filter_failure.NonExistentClass::",
|
||||||
|
# Class is not a Warning subclass.
|
||||||
|
"::list::",
|
||||||
|
# Negative line number.
|
||||||
|
"::::-1",
|
||||||
|
# Not a line number.
|
||||||
|
"::::not-a-number",
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_parse_warning_filter_failure(arg: str) -> None:
|
def test_parse_warning_filter_failure(arg: str) -> None:
|
||||||
import warnings
|
with pytest.raises(pytest.UsageError):
|
||||||
|
|
||||||
with pytest.raises(warnings._OptionError):
|
|
||||||
parse_warning_filter(arg, escape=True)
|
parse_warning_filter(arg, escape=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue