From 5d02be91db9244a44eb06cd5822ab507f0be7008 Mon Sep 17 00:00:00 2001 From: Dominic Davis-Foster Date: Sun, 16 Jan 2022 16:46:17 +0000 Subject: [PATCH 1/2] Exit with status code 4 if syntax errors are encountered when checking code blocks. --- doc-source/changelog.rst | 8 ++ snippet_fmt/__init__.py | 43 ++++++- snippet_fmt/__main__.py | 2 +- snippet_fmt/formatters.py | 2 +- tests/test_snippet_fmt.py | 120 +++++++++++++++++- tests/test_snippet_fmt_/test_retval.rst | 8 ++ tests/test_snippet_fmt_/test_retval.yml | 16 +++ .../test_retval_syntaxerror.rst | 7 + .../test_retval_syntaxerror.yml | 26 ++++ 9 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 tests/test_snippet_fmt_/test_retval.rst create mode 100644 tests/test_snippet_fmt_/test_retval.yml create mode 100644 tests/test_snippet_fmt_/test_retval_syntaxerror.rst create mode 100644 tests/test_snippet_fmt_/test_retval_syntaxerror.yml diff --git a/doc-source/changelog.rst b/doc-source/changelog.rst index f398354..68d2502 100644 --- a/doc-source/changelog.rst +++ b/doc-source/changelog.rst @@ -2,6 +2,14 @@ Changelog =============== +v0.2.0 +---------- + +* CLI -- Now exits with code ``4`` if syntax errors are encountered when checking code blocks. +* :meth:`RSTReformatter.run ` -- Now returns ``4`` if an error occurred, and ``1`` if the file was changed. + +.. note:: A Return code of ``5`` indicates a combination of a syntax error and the file being reformatted. + v0.1.4 ---------- diff --git a/snippet_fmt/__init__.py b/snippet_fmt/__init__.py index d0e76cc..631133a 100644 --- a/snippet_fmt/__init__.py +++ b/snippet_fmt/__init__.py @@ -34,6 +34,7 @@ import contextlib import re import textwrap +import traceback from typing import Dict, Iterator, List, Match, NamedTuple, Optional # 3rd party @@ -110,11 +111,17 @@ def __init__(self, filename: PathLike, config: SnippetFmtConfigDict): } self.load_extra_formatters() - def run(self) -> bool: + def run(self) -> int: """ Run the reformatter. - :return: Whether the file was changed. + :return: Whether the file was changed or any errors occurred. + + .. versionchanged:: 0.2.0 + + Now returns ``4`` if an error occurred, and integers instead of booleans for other results. + + .. note:: A Return code of ``5`` indicates a combination of a syntax error and the file being reformatted. """ content = StringList(self._unformatted_source) @@ -135,11 +142,37 @@ def run(self) -> bool: self._reformatted_source = pattern.sub(self.process_match, str(content)) + ret = 0 + for error in self.errors: + ret |= 4 + lineno = self._unformatted_source[:error.offset].count('\n') + 1 click.echo(f"{self.filename}:{lineno}: {error.exc.__class__.__name__}: {error.exc}", err=True) - return self._reformatted_source != self._unformatted_source + if isinstance(error.exc, SyntaxError): + tbe = traceback.TracebackException( + error.exc.__class__, + error.exc, + None, # type: ignore[arg-type] + ) + + if tbe.text is not None: + click.echo(f' {tbe.text.strip()}', err=True) + if tbe.offset is not None: + caretspace = tbe.text.rstrip('\n') + print(repr(caretspace), tbe.offset) + offset = min(len(caretspace), tbe.offset) - 1 + print(offset) + caretspace = caretspace[:offset].lstrip() + # non-space whitespace (likes tabs) must be kept for alignment + print(repr(caretspace)) + click.echo(f" {''.join(((c.isspace() and c or ' ') for c in caretspace))}^", err=True) + + if self._reformatted_source != self._unformatted_source: + ret |= 1 + + return ret def process_match(self, match: Match[str]) -> str: """ @@ -164,7 +197,7 @@ def process_match(self, match: Match[str]) -> str: code = textwrap.dedent(match["code"]) with self._collect_error(match): - with syntaxerror_for_file(self.filename): + with syntaxerror_for_file("snippet.py"): code = formatter(code, **lang_config) code = textwrap.indent(code, match["indent"] + match["body_indent"]) @@ -247,7 +280,7 @@ def reformat_file( ret = r.run() - if ret: + if ret & 1 == 1: click.echo(r.get_diff(), color=resolve_color_default(colour)) r.to_file() diff --git a/snippet_fmt/__main__.py b/snippet_fmt/__main__.py index 2c3e613..4659901 100644 --- a/snippet_fmt/__main__.py +++ b/snippet_fmt/__main__.py @@ -113,7 +113,7 @@ def main( with handle_tracebacks(show_traceback, cls=SyntaxTracebackHandler): ret_for_file = r.run() - if ret_for_file: + if ret_for_file & 1 == 1: if verbose: click.echo(f"Reformatting {path}") if show_diff: diff --git a/snippet_fmt/formatters.py b/snippet_fmt/formatters.py index 55c6039..6d53cdc 100644 --- a/snippet_fmt/formatters.py +++ b/snippet_fmt/formatters.py @@ -107,7 +107,7 @@ def format_python(code: str, **config) -> str: r.run() return r.to_string() else: - ast.parse(code) + ast.parse(code, filename="snippet.py") return code diff --git a/tests/test_snippet_fmt.py b/tests/test_snippet_fmt.py index 8e3646e..38d0117 100644 --- a/tests/test_snippet_fmt.py +++ b/tests/test_snippet_fmt.py @@ -1,12 +1,12 @@ # stdlib -import re +import sys from typing import Dict, List, Union, no_type_check # 3rd party import dom_toml import pytest from _pytest.capture import CaptureResult -from coincidence import AdvancedDataRegressionFixture, AdvancedFileRegressionFixture +from coincidence import AdvancedDataRegressionFixture, AdvancedFileRegressionFixture, min_version from coincidence.params import param from consolekit.terminal_colours import strip_ansi from consolekit.testing import CliRunner, Result @@ -209,3 +209,119 @@ def check_out( } advanced_data_regression.check(data_dict) + + +def test_retval( + tmp_pathplus: PathPlus, + advanced_file_regression: AdvancedFileRegressionFixture, + advanced_data_regression: AdvancedDataRegressionFixture, + capsys, + ): + + (tmp_pathplus / "formate.toml").write_text((source_dir / "example_formate.toml").read_text()) + + config: SnippetFmtConfigDict = {"languages": {"python": {"reformat": True}}, "directives": ["code-block"]} + + (tmp_pathplus / "demo.rst").write_lines([ + ".. code-block:: python", + '', + '\tprint (\t"hello world"', + "\t)", + '', + '', + ".. code-block:: python3", + '', + "\tdef for while if", + '', + ]) + + with in_directory(tmp_pathplus): + assert reformat_file(tmp_pathplus / "demo.rst", config) == 1 + advanced_file_regression.check_file(tmp_pathplus / "demo.rst") + + result = capsys.readouterr() + stdout = result.out + stderr = result.err + + advanced_data_regression.check({ + "out": strip_ansi(stdout.replace(tmp_pathplus.as_posix(), "...")).split('\n'), + "err": strip_ansi(stderr.replace(tmp_pathplus.as_posix(), "...")).split('\n'), + }) + + with in_directory(tmp_pathplus): + assert reformat_file(tmp_pathplus / "demo.rst", config) == 0 + advanced_file_regression.check_file(tmp_pathplus / "demo.rst") + assert capsys.readouterr().out == '' + + +def test_retval_syntaxerror( + tmp_pathplus: PathPlus, + advanced_file_regression: AdvancedFileRegressionFixture, + advanced_data_regression: AdvancedDataRegressionFixture, + capsys, + ): + + (tmp_pathplus / "formate.toml").write_text((source_dir / "example_formate.toml").read_text()) + + config: SnippetFmtConfigDict = {"languages": {"python3": {"reformat": False}, }, "directives": ["code-block"]} + + content = [ + ".. code-block:: python3", + '', + "\tdef for while if", + '', + ".. code-block:: python3", + '', + '\tprint (\t"hello world"', + "\t)", + '', + ] + + (tmp_pathplus / "demo.rst").write_lines(content) + + data = {} + + with in_directory(tmp_pathplus): + assert reformat_file(tmp_pathplus / "demo.rst", config) == 4 + assert (tmp_pathplus / "demo.rst").read_lines() == content + + result = capsys.readouterr() + stdout = result.out + stderr = result.err + + data["error"] = { + "out": strip_ansi(stdout.replace(tmp_pathplus.as_posix(), "...")).split('\n'), + "err": strip_ansi(stderr.replace(tmp_pathplus.as_posix(), "...")).split('\n'), + } + + config["languages"]["python3"]["reformat"] = True + + with in_directory(tmp_pathplus): + assert reformat_file(tmp_pathplus / "demo.rst", config) == 5 + advanced_file_regression.check_file(tmp_pathplus / "demo.rst") + + result = capsys.readouterr() + stdout = result.out + stderr = result.err + + data["reformat"] = { + "out": strip_ansi(stdout.replace(tmp_pathplus.as_posix(), "...")).split('\n'), + "err": strip_ansi(stderr.replace(tmp_pathplus.as_posix(), "...")).split('\n'), + } + + if sys.version_info >= (3, 8): + # Offsets are wrong on earlier versions. + advanced_data_regression.check(data) + + with in_directory(tmp_pathplus): + assert reformat_file(tmp_pathplus / "demo.rst", config) == 4 + advanced_file_regression.check_file(tmp_pathplus / "demo.rst") + + result = capsys.readouterr() + stdout = result.out + stderr = result.err + + assert data["error"] == { + "out": strip_ansi(stdout.replace(tmp_pathplus.as_posix(), "...")).split('\n'), + "err": strip_ansi(stderr.replace(tmp_pathplus.as_posix(), "...")).split('\n'), + } diff --git a/tests/test_snippet_fmt_/test_retval.rst b/tests/test_snippet_fmt_/test_retval.rst new file mode 100644 index 0000000..f265c50 --- /dev/null +++ b/tests/test_snippet_fmt_/test_retval.rst @@ -0,0 +1,8 @@ +.. code-block:: python + + print("hello world") + + +.. code-block:: python3 + + def for while if diff --git a/tests/test_snippet_fmt_/test_retval.yml b/tests/test_snippet_fmt_/test_retval.yml new file mode 100644 index 0000000..767d165 --- /dev/null +++ b/tests/test_snippet_fmt_/test_retval.yml @@ -0,0 +1,16 @@ +err: +- '' +out: +- "--- .../demo.rst\t(original)" +- "+++ .../demo.rst\t(reformatted)" +- '@@ -1,7 +1,6 @@' +- ' .. code-block:: python' +- '' +- "-\tprint (\t\"hello world\"" +- "-\t)" +- "+\tprint(\"hello world\")" +- '' +- '' +- ' .. code-block:: python3' +- '' +- '' diff --git a/tests/test_snippet_fmt_/test_retval_syntaxerror.rst b/tests/test_snippet_fmt_/test_retval_syntaxerror.rst new file mode 100644 index 0000000..cf4fe71 --- /dev/null +++ b/tests/test_snippet_fmt_/test_retval_syntaxerror.rst @@ -0,0 +1,7 @@ +.. code-block:: python3 + + def for while if + +.. code-block:: python3 + + print("hello world") diff --git a/tests/test_snippet_fmt_/test_retval_syntaxerror.yml b/tests/test_snippet_fmt_/test_retval_syntaxerror.yml new file mode 100644 index 0000000..dc0d75f --- /dev/null +++ b/tests/test_snippet_fmt_/test_retval_syntaxerror.yml @@ -0,0 +1,26 @@ +error: + err: + - '.../demo.rst:1: SyntaxError: invalid syntax (snippet.py, line 1)' + - ' def for while if' + - ' ^' + - '' + out: + - '' +reformat: + err: + - '.../demo.rst:1: SyntaxError: invalid syntax (snippet.py, line 1)' + - ' def for while if' + - ' ^' + - '' + out: + - "--- .../demo.rst\t(original)" + - "+++ .../demo.rst\t(reformatted)" + - '@@ -4,6 +4,5 @@' + - '' + - ' .. code-block:: python3' + - '' + - "-\tprint (\t\"hello world\"" + - "-\t)" + - "+\tprint(\"hello world\")" + - '' + - '' From 6601f862add4e243b27fa153cbaef91422561982 Mon Sep 17 00:00:00 2001 From: Dominic Davis-Foster Date: Sun, 16 Jan 2022 19:17:23 +0000 Subject: [PATCH 2/2] Comment out debug prints --- snippet_fmt/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/snippet_fmt/__init__.py b/snippet_fmt/__init__.py index 631133a..79f3ce4 100644 --- a/snippet_fmt/__init__.py +++ b/snippet_fmt/__init__.py @@ -161,12 +161,12 @@ def run(self) -> int: click.echo(f' {tbe.text.strip()}', err=True) if tbe.offset is not None: caretspace = tbe.text.rstrip('\n') - print(repr(caretspace), tbe.offset) + # print(repr(caretspace), tbe.offset) offset = min(len(caretspace), tbe.offset) - 1 - print(offset) + # print(offset) caretspace = caretspace[:offset].lstrip() # non-space whitespace (likes tabs) must be kept for alignment - print(repr(caretspace)) + # print(repr(caretspace)) click.echo(f" {''.join(((c.isspace() and c or ' ') for c in caretspace))}^", err=True) if self._reformatted_source != self._unformatted_source: