/
niceSOFT
/
python3-pytest
Обзор
Документация
Войти
/
niceSOFT
/
python3-pytest
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
9.1.0
testing/python/raises.py
515 строк
19 KB
Fridayworks
Fix -v hint in raises match diff not working (#14226)
19 май 2026, 22:45
Не верифицирован
19 май 2026, 22:45
543b11f
Код
Авторство
О чём код?
# mypy: allow-untyped-defs from __future__ import annotations import io import re import sys from _pytest.outcomes import Failed from _pytest.pytester import Pytester from _pytest.warning_types import PytestWarning import pytest def wrap_escape(s: str) -> str: return "^" + re.escape(s) + "$" class TestRaises: def test_check_callable(self) -> None: with pytest.raises(TypeError, match=r".* must be callable"): pytest.raises(RuntimeError, "int('qwe')") # type: ignore[call-overload] def test_raises(self): with pytest.raises(ValueError) as excinfo: int("qwe") assert "invalid literal" in str(excinfo.value) def test_raises_function(self): with pytest.raises(ValueError) as excinfo: int("hello") assert "invalid literal" in str(excinfo.value) def test_raises_does_not_allow_none(self): with pytest.raises( ValueError, match=wrap_escape("You must specify at least one parameter to match on."), ): # We're testing that this invalid usage gives a helpful error, # so we can ignore Mypy telling us that None is invalid. pytest.raises(expected_exception=None) # type: ignore # it's unclear if this message is helpful, and if it is, should it trigger more # liberally? Usually you'd get a TypeError here def test_raises_false_and_arg(self): with pytest.raises( ValueError, match=wrap_escape( "Expected an exception type or a tuple of exception types, but got `False`. " "Raising exceptions is already understood as failing the test, so you don't need " "any special code to say 'this should never raise an exception'." ), ): pytest.raises(False, int) # type: ignore[call-overload] def test_raises_does_not_allow_empty_tuple(self): with pytest.raises( ValueError, match=wrap_escape("You must specify at least one parameter to match on."), ): pytest.raises(expected_exception=()) def test_raises_callable_no_exception(self) -> None: class A: def __call__(self): pass try: pytest.raises(ValueError, A()) except pytest.fail.Exception: pass def test_raises_falsey_type_error(self) -> None: with pytest.raises(TypeError): with pytest.raises(AssertionError, match=0): # type: ignore[call-overload] raise AssertionError("ohai") def test_raises_repr_inflight(self): """Ensure repr() on an exception info inside a pytest.raises with block works (#4386)""" class E(Exception): pass with pytest.raises(E) as excinfo: # this test prints the inflight uninitialized object # using repr and str as well as pprint to demonstrate # it works print(str(excinfo)) print(repr(excinfo)) import pprint pprint.pprint(excinfo) raise E() def test_raises_as_contextmanager(self, pytester: Pytester) -> None: pytester.makepyfile( """ import pytest import _pytest._code def test_simple(): with pytest.raises(ZeroDivisionError) as excinfo: assert isinstance(excinfo, _pytest._code.ExceptionInfo) 1/0 print(excinfo) assert excinfo.type == ZeroDivisionError assert isinstance(excinfo.value, ZeroDivisionError) def test_noraise(): with pytest.raises(pytest.raises.Exception): with pytest.raises(ValueError): int() def test_raise_wrong_exception_passes_by(): with pytest.raises(ZeroDivisionError): with pytest.raises(ValueError): 1/0 """ ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) def test_does_not_raise(self, pytester: Pytester) -> None: pytester.makepyfile( """ from contextlib import nullcontext as does_not_raise import pytest @pytest.mark.parametrize('example_input,expectation', [ (3, does_not_raise()), (2, does_not_raise()), (1, does_not_raise()), (0, pytest.raises(ZeroDivisionError)), ]) def test_division(example_input, expectation): '''Test how much I know division.''' with expectation: assert (6 / example_input) is not None """ ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*4 passed*"]) def test_does_not_raise_does_raise(self, pytester: Pytester) -> None: pytester.makepyfile( """ from contextlib import nullcontext as does_not_raise import pytest @pytest.mark.parametrize('example_input,expectation', [ (0, does_not_raise()), (1, pytest.raises(ZeroDivisionError)), ]) def test_division(example_input, expectation): '''Test how much I know division.''' with expectation: assert (6 / example_input) is not None """ ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) def test_raises_with_invalid_regex(self, pytester: Pytester) -> None: pytester.makepyfile( """ import pytest def test_invalid_regex(): with pytest.raises(ValueError, match="invalid regex character ["): raise ValueError() """ ) result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*Invalid regex pattern provided to 'match': unterminated character set at position 24*", ] ) result.stdout.no_fnmatch_line("*Traceback*") result.stdout.no_fnmatch_line("*File*") result.stdout.no_fnmatch_line("*line*") def test_raises_match_failure_suppresses_exception_context( self, pytester: Pytester ) -> None: pytester.makepyfile( """ import pytest def test_raises_match_failure(): with pytest.raises(ValueError, match="expected"): raise ValueError("actual") """ ) result = pytester.runpytest("--tb=short") assert result.ret == 1 result.stdout.fnmatch_lines( [ "*E*AssertionError: Regex pattern did not match.*", ] ) result.stdout.no_fnmatch_line("*ValueError: actual") result.stdout.no_fnmatch_line( "*The above exception was the direct cause of the following exception:*" ) result.stdout.no_fnmatch_line( "*During handling of the above exception, another exception occurred:*" ) def test_noclass(self) -> None: with pytest.raises(TypeError): with pytest.raises("wrong"): # type: ignore[call-overload] ... # pragma: no cover def test_invalid_arguments_to_raises(self) -> None: with pytest.raises(TypeError, match="unknown"): with pytest.raises(TypeError, unknown="bogus"): # type: ignore[call-overload] raise ValueError() def test_tuple(self): with pytest.raises((KeyError, ValueError)): raise KeyError("oops") def test_no_raise_message(self) -> None: try: with pytest.raises(ValueError): int("0") except pytest.fail.Exception as e: assert e.msg == "DID NOT RAISE ValueError" else: assert False, "Expected pytest.raises.Exception" try: with pytest.raises(ValueError): pass except pytest.fail.Exception as e: assert e.msg == "DID NOT RAISE ValueError" else: assert False, "Expected pytest.raises.Exception" @pytest.mark.parametrize( "method", ["function", "function_match", "with", "with_raisesexc", "with_group"] ) def test_raises_cyclic_reference(self, method): """Ensure pytest.raises does not leave a reference cycle (#1965).""" import gc class T: def __call__(self): raise ValueError t = T() refcount = len(gc.get_referrers(t)) if method == "function": pytest.raises(ValueError, t) elif method == "function_match": pytest.raises(ValueError, t).match("^$") elif method == "with": with pytest.raises(ValueError): t() elif method == "with_raisesexc": with pytest.RaisesExc(ValueError): t() elif method == "with_group": with pytest.RaisesGroup(ValueError, allow_unwrapped=True): t() else: # pragma: no cover raise AssertionError("bad parametrization") # ensure both forms of pytest.raises don't leave exceptions in sys.exc_info() assert sys.exc_info() == (None, None, None) assert refcount == len(gc.get_referrers(t)) def test_raises_match(self) -> None: msg = r"with base \d+" with pytest.raises(ValueError, match=msg): int("asdf") msg = "with base 10" with pytest.raises(ValueError, match=msg): int("asdf") msg = "with base 16" expr = ( "Regex pattern did not match.\n" f" Expected regex: {msg!r}\n" f" Actual message: \"invalid literal for int() with base 10: 'asdf'\"" ) with pytest.raises(AssertionError, match="^" + re.escape(expr) + "$"): with pytest.raises(ValueError, match=msg): int("asdf", base=10) # "match" without context manager. pytest.raises(ValueError, int, "asdf").match("invalid literal") with pytest.raises(AssertionError) as excinfo: pytest.raises(ValueError, int, "asdf").match(msg) assert str(excinfo.value) == expr pytest.raises(TypeError, int, match="invalid") # type: ignore[call-overload] def tfunc(match): raise ValueError(f"match={match}") pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf") pytest.raises(ValueError, tfunc, match="").match("match=") # empty string matches everything, which is probably not what the user wants with pytest.warns( PytestWarning, match=wrap_escape( "matching against an empty string will *always* pass. If you want to check for an empty message you " "need to pass '^$'. If you don't want to match you should pass `None` or leave out the parameter." ), ): pytest.raises(match="") def test_match_failure_string_quoting(self): with pytest.raises(AssertionError) as excinfo: with pytest.raises(AssertionError, match="'foo"): raise AssertionError("'bar") (msg,) = excinfo.value.args assert ( msg == '''Regex pattern did not match.\n Expected regex: "'foo"\n Actual message: "'bar"''' ) def test_match_failure_exact_string_message(self): message = "Oh here is a message with (42) numbers in parameters" with pytest.raises(AssertionError) as excinfo: with pytest.raises(AssertionError, match=message): raise AssertionError(message) (msg,) = excinfo.value.args assert msg == ( "Regex pattern did not match.\n" " Expected regex: 'Oh here is a message with (42) numbers in parameters'\n" " Actual message: 'Oh here is a message with (42) numbers in parameters'\n" " Did you mean to `re.escape()` the regex?" ) def test_raises_match_wrong_type(self): """Raising an exception with the wrong type and match= given. pytest should throw the unexpected exception - the pattern match is not really relevant if we got a different exception. """ with pytest.raises( ValueError, match=wrap_escape("invalid literal for int() with base 10: 'asdf'"), ): with pytest.raises(IndexError, match="nomatch"): int("asdf") def test_raises_exception_looks_iterable(self): class Meta(type): def __getitem__(self, item): return 1 / 0 # pragma: no cover def __len__(self): return 1 # pragma: no cover class ClassLooksIterableException(Exception, metaclass=Meta): pass with pytest.raises( Failed, match=r"DID NOT RAISE ClassLooksIterableException", ): with pytest.raises(ClassLooksIterableException): ... # pragma: no cover def test_raises_with_raising_dunder_class(self) -> None: """Test current behavior with regard to exceptions via __class__ (#4284).""" class CrappyClass(Exception): # Type ignored because it's bypassed intentionally. @property # type: ignore def __class__(self): assert False, "via __class__" with pytest.raises(AssertionError) as excinfo: with pytest.raises(CrappyClass()): # type: ignore[call-overload] pass assert "via __class__" in excinfo.value.args[0] def test_raises_context_manager_with_kwargs(self): with pytest.raises(expected_exception=ValueError): raise ValueError with pytest.raises( TypeError, match=wrap_escape( "Unexpected keyword arguments passed to pytest.raises: foo\n" "Use context-manager form instead?" ), ): with pytest.raises(OSError, foo="bar"): # type: ignore[call-overload] pass def test_expected_exception_is_not_a_baseexception(self) -> None: with pytest.raises( TypeError, match=wrap_escape("Expected a BaseException type, but got 'str'"), ): with pytest.raises("hello"): # type: ignore[call-overload] pass # pragma: no cover class NotAnException: pass with pytest.raises( ValueError, match=wrap_escape( "Expected a BaseException type, but got 'NotAnException'" ), ): with pytest.raises(NotAnException): # type: ignore[type-var] pass # pragma: no cover with pytest.raises( TypeError, match=wrap_escape("Expected a BaseException type, but got 'str'"), ): with pytest.raises(("hello", NotAnException)): # type: ignore[arg-type] pass # pragma: no cover def test_issue_11872(self) -> None: """Regression test for #11872. urllib.error.HTTPError on some Python 3.10/11 minor releases raises KeyError instead of AttributeError on invalid attribute access. https://github.com/python/cpython/issues/98778 """ from email.message import Message from urllib.error import HTTPError with pytest.raises(HTTPError, match="Not Found") as exc_info: raise HTTPError( code=404, msg="Not Found", fp=io.BytesIO(), hdrs=Message(), url="" ) exc_info.value.close() # avoid a resource warning def test_raises_match_compiled_regex(self) -> None: """Test that compiled regex patterns work with pytest.raises.""" # Test with a compiled pattern that matches pattern = re.compile(r"with base \d+") with pytest.raises(ValueError, match=pattern): int("asdf") # Test with a compiled pattern that doesn't match pattern_nomatch = re.compile(r"with base 16") expr = ( "Regex pattern did not match.\n" f" Expected regex: {pattern_nomatch.pattern!r}\n" f" Actual message: \"invalid literal for int() with base 10: 'asdf'\"" ) with pytest.raises(AssertionError, match="^" + re.escape(expr) + "$"): with pytest.raises(ValueError, match=pattern_nomatch): int("asdf", base=10) # Test compiled pattern with flags pattern_with_flags = re.compile(r"INVALID LITERAL", re.IGNORECASE) with pytest.raises(ValueError, match=pattern_with_flags): int("asdf") def test_pipe_is_treated_as_regex_metacharacter(self) -> None: """| (pipe) must be recognized as a regex metacharacter.""" from _pytest.raises import is_fully_escaped from _pytest.raises import unescape assert not is_fully_escaped("foo|bar") assert is_fully_escaped(r"foo\|bar") assert unescape(r"foo\|bar") == "foo|bar" def test_consecutive_backslashes_in_escape_check(self) -> None: """Consecutive backslashes escape each other, leaving the metachar unescaped.""" from _pytest.raises import is_fully_escaped # r"\." -> one backslash escapes the dot -> fully escaped assert is_fully_escaped(r"\.") # r"\\." -> two backslashes: the first escapes the second, dot is unescaped assert not is_fully_escaped(r"\\.") # r"\\\." -> three backslashes: pair escapes pair, last escapes dot -> fully escaped assert is_fully_escaped(r"\\\.") # Same idea with pipe metachar # "\\\\|" is the string \\| (2 backslashes + pipe): even count, pipe is unescaped assert not is_fully_escaped("\\\\|") # r"\\\\|" is the string \\\\| (4 backslashes + pipe): even count, pipe is unescaped assert not is_fully_escaped(r"\\\\|") def test_raises_match_verbose_diff(self, pytester: Pytester) -> None: """Test that -v flag shows full diff in raises match failure (#14214).""" pytester.makepyfile( """ import re import pytest def test_raises_v_hint(): prefix = "A" * 60 expected = prefix + " expected_ending" actual = prefix + " actual_ending" with pytest.raises( ValueError, match=f"^{re.escape(expected)}$" ): raise ValueError(actual) """ ) # Without -v: should show "Skipping ... identical leading characters" result = pytester.runpytest() result.stdout.fnmatch_lines(["*Skipping*identical leading*"]) # With -v: should NOT show "Skipping" — full diff shown result_v = pytester.runpytest("-v") assert "Skipping" not in result_v.stdout.str()