Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/coretrace_python/frontend/parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import ast
import warnings

from coretrace_python.source import SourceFile

Expand All @@ -11,7 +12,12 @@ class ParseError(Exception):

def parse_source(source: str, filename: str = "<unknown>") -> 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
Expand Down
59 changes: 59 additions & 0 deletions tests/test_parse_warnings.py
Original file line number Diff line number Diff line change
@@ -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
Loading