diff --git a/AUTHORS b/AUTHORS index 96dbc1c12..27f70daca 100644 --- a/AUTHORS +++ b/AUTHORS @@ -93,6 +93,7 @@ Christopher Dignam Christopher Gilling Claire Cecil Claudio Madotto +Clément M.T. Robert CrazyMerlyn Cristian Vera Cyrus Maden diff --git a/changelog/11879.bugfix.rst b/changelog/11879.bugfix.rst new file mode 100644 index 000000000..70b6cce72 --- /dev/null +++ b/changelog/11879.bugfix.rst @@ -0,0 +1 @@ +Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 0288d7a54..25e943bc1 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -698,10 +698,21 @@ class ExceptionInfo(Generic[E]): return fmt.repr_excinfo(self) def _stringify_exception(self, exc: BaseException) -> str: + try: + notes = getattr(exc, "__notes__", []) + except KeyError: + # Workaround for https://github.com/python/cpython/issues/98778 on + # Python <= 3.9, and some 3.10 and 3.11 patch versions. + HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ()) + if sys.version_info[:2] <= (3, 11) and isinstance(exc, HTTPError): + notes = [] + else: + raise + return "\n".join( [ str(exc), - *getattr(exc, "__notes__", []), + *notes, ] ) diff --git a/testing/python/raises.py b/testing/python/raises.py index 3dcec31eb..f1e8fb1fb 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -301,3 +301,16 @@ class TestRaises: with pytest.raises(("hello", NotAnException)): # type: ignore[arg-type] pass # pragma: no cover assert "must be a BaseException type, not str" in str(excinfo.value) + + def test_issue_11872(self) -> None: + """Regression test for #11872. + + urllib.error.HTTPError on Python<=3.9 raises KeyError instead of + AttributeError on invalid attribute access. + + https://github.com/python/cpython/issues/98778 + """ + from urllib.error import HTTPError + + with pytest.raises(HTTPError, match="Not Found"): + raise HTTPError(code=404, msg="Not Found", fp=None, hdrs=None, url="") # type: ignore [arg-type]