diff --git a/src/coretrace_python/frontend/parser.py b/src/coretrace_python/frontend/parser.py index 6469a78..94ba78b 100644 --- a/src/coretrace_python/frontend/parser.py +++ b/src/coretrace_python/frontend/parser.py @@ -1,6 +1,7 @@ from __future__ import annotations import ast +import warnings from coretrace_python.source import SourceFile @@ -11,7 +12,12 @@ class ParseError(Exception): def parse_source(source: str, filename: str = "") -> ast.Module: try: - return ast.parse(source, filename=filename) + with warnings.catch_warnings(): + # A warning raised while parsing (an invalid escape sequence: a SyntaxWarning + # from 3.12, a DeprecationWarning on 3.11) is about the analysed source, not + # the analyzer, and would reach its stderr once per process that parses (#241). + warnings.simplefilter("ignore") + return ast.parse(source, filename=filename) except SyntaxError as error: line = error.lineno or 0 column = error.offset or 0 diff --git a/tests/test_parse_warnings.py b/tests/test_parse_warnings.py new file mode 100644 index 0000000..0b8610d --- /dev/null +++ b/tests/test_parse_warnings.py @@ -0,0 +1,59 @@ +"""Acceptance tests for issue #241: a warning Python raises while parsing an analysed +source is about that source, not about the analyzer, and never reaches its stderr. + +An invalid escape sequence is a ``SyntaxWarning`` from Python 3.12 on and a +``DeprecationWarning`` on 3.11. With ``-j N`` the parent and a worker both parse every +module, so an unfiltered warning was printed twice. +""" + +from __future__ import annotations + +import subprocess +import sys +import warnings +from pathlib import Path + +import pytest + +from coretrace_python.frontend.parser import parse_source + +INVALID_ESCAPE = 'import re\n\nPATTERN = re.compile("\\d+")\n' + + +def test_parsing_raises_no_warning_about_the_analysed_source() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + tree = parse_source(INVALID_ESCAPE, "escapes.py") + + assert caught == [] + assert len(tree.body) == 2 + + +def test_parsing_leaves_the_warning_filters_of_the_process_alone() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + parse_source(INVALID_ESCAPE, "escapes.py") + warnings.warn("raised by the analyzer itself", SyntaxWarning, stacklevel=1) + + assert [str(warning.message) for warning in caught] == ["raised by the analyzer itself"] + + +@pytest.mark.parametrize("jobs", ["1", "2"]) +def test_check_prints_nothing_on_stderr_about_the_analysed_sources( + tmp_path: Path, jobs: str +) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "escapes.py").write_text(INVALID_ESCAPE, encoding="utf-8") + (root / "other.py").write_text("def run():\n return 1\n", encoding="utf-8") + + checked = subprocess.run( + [sys.executable, "-m", "coretrace_python", "--check", str(root), "-j", jobs], + capture_output=True, + text=True, + check=False, + ) + + assert checked.stderr == "" + assert checked.stdout == "no findings\ncoverage: 2/2 files, 2/2 functions\n" + assert checked.returncode == 0