Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an option to fail if there is an error parsing a document #367

Merged
merged 1 commit into from
Feb 24, 2025
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
59 changes: 36 additions & 23 deletions src/doccmd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,10 @@ def _run_args_against_docs(
use_pty: bool,
markup_language: MarkupLanguage,
) -> None:
"""
Run commands on the given file.
"""Run commands on the given file.

Raises:
_ParseError: The file could not be parsed.
"""
temporary_file_extension = _get_temporary_file_extension(
language=code_block_language,
Expand All @@ -414,11 +416,10 @@ def _run_args_against_docs(
charset_matches = charset_normalizer.from_bytes(sequences=content_bytes)
best_match = charset_matches.best()
if best_match is None:
no_encoding_message = click.style(
text="Could not detect encoding.",
fg="red",
raise _ParseError(
path=document_path,
reason="Could not detect encoding.",
)
raise click.ClickException(message=no_encoding_message)

encoding = best_match.encoding
newline_bytes = _detect_newline(content_bytes=content_bytes)
Expand Down Expand Up @@ -460,11 +461,7 @@ def _run_args_against_docs(
parsers: Sequence[Parser] = [*code_block_parsers, *skip_parsers]
sybil = Sybil(parsers=parsers, encoding=encoding)

try:
document = _parse_file(sybil=sybil, path=document_path)
except _ParseError as exc:
_log_error(message=str(object=exc))
return
document = _parse_file(sybil=sybil, path=document_path)

try:
_evaluate_document(document=document, args=args)
Expand Down Expand Up @@ -679,6 +676,16 @@ def _run_args_against_docs(
"Use forward slashes on all platforms."
),
)
@click.option(
"--fail-on-parse-error/--no-fail-on-parse-error",
"fail_on_parse_error",
default=False,
show_default=True,
type=bool,
help=(
"Whether to fail (with exit code 1) if a given file cannot be parsed."
),
)
@beartype
def main(
*,
Expand All @@ -696,6 +703,7 @@ def main(
markdown_suffixes: Sequence[str],
max_depth: int,
exclude_patterns: Sequence[str],
fail_on_parse_error: bool,
) -> None:
"""Run commands against code blocks in the given documentation files.

Expand Down Expand Up @@ -742,15 +750,20 @@ def main(
for file_path in file_paths:
for code_block_language in languages:
markup_language = suffix_map[file_path.suffix]
_run_args_against_docs(
args=args,
document_path=file_path,
code_block_language=code_block_language,
pad_temporary_file=pad_file,
verbose=verbose,
temporary_file_extension=temporary_file_extension,
temporary_file_name_prefix=temporary_file_name_prefix,
skip_markers=skip_markers,
use_pty=use_pty,
markup_language=markup_language,
)
try:
_run_args_against_docs(
args=args,
document_path=file_path,
code_block_language=code_block_language,
pad_temporary_file=pad_file,
verbose=verbose,
temporary_file_extension=temporary_file_extension,
temporary_file_name_prefix=temporary_file_name_prefix,
skip_markers=skip_markers,
use_pty=use_pty,
markup_language=markup_language,
)
except _ParseError as exc:
_log_error(message=str(object=exc))
if fail_on_parse_error:
sys.exit(1)
78 changes: 69 additions & 9 deletions tests/test_doccmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,26 @@ def test_not_utf_8_file_given(tmp_path: Path) -> None:
assert result.stderr == expected_stderr


def test_unknown_encoding(tmp_path: Path) -> None:
@pytest.mark.parametrize(
argnames=("fail_on_parse_error_options", "expected_exit_code"),
argvalues=[
([], 0),
(["--fail-on-parse-error"], 1),
],
)
def test_unknown_encoding(
tmp_path: Path,
fail_on_parse_error_options: Sequence[str],
expected_exit_code: int,
) -> None:
"""
An error is shown when a file cannot be decoded.
"""
runner = CliRunner(mix_stderr=False)
rst_file = tmp_path / "example.rst"
rst_file.write_bytes(data=Path(sys.executable).read_bytes())
arguments = [
*fail_on_parse_error_options,
"--language",
"python",
"--command",
Expand All @@ -197,8 +209,11 @@ def test_unknown_encoding(tmp_path: Path) -> None:
catch_exceptions=False,
color=True,
)
expected_stderr = f"Error: {fg.red}Could not detect encoding.{reset}\n"
assert result.exit_code != 0
expected_stderr = (
f"{fg.red}Could not parse {rst_file}: "
f"Could not detect encoding.{reset}\n"
)
assert result.exit_code == expected_exit_code
assert result.stdout == ""
assert result.stderr == expected_stderr

Expand Down Expand Up @@ -1015,7 +1030,18 @@ def test_default_skip_rst(tmp_path: Path) -> None:
assert result.stderr == ""


def test_skip_no_arguments(tmp_path: Path) -> None:
@pytest.mark.parametrize(
argnames=("fail_on_parse_error_options", "expected_exit_code"),
argvalues=[
([], 0),
(["--fail-on-parse-error"], 1),
],
)
def test_skip_no_arguments(
tmp_path: Path,
fail_on_parse_error_options: Sequence[str],
expected_exit_code: int,
) -> None:
"""
An error is shown if a skip is given with no arguments.
"""
Expand All @@ -1030,6 +1056,7 @@ def test_skip_no_arguments(tmp_path: Path) -> None:
"""
rst_file.write_text(data=content, encoding="utf-8")
arguments = [
*fail_on_parse_error_options,
"--no-pad-file",
"--language",
"python",
Expand All @@ -1043,7 +1070,10 @@ def test_skip_no_arguments(tmp_path: Path) -> None:
catch_exceptions=False,
color=True,
)
assert result.exit_code == 0, (result.stdout, result.stderr)
assert result.exit_code == expected_exit_code, (
result.stdout,
result.stderr,
)
expected_stderr = textwrap.dedent(
text=f"""\
{fg.red}Could not parse {rst_file}: missing arguments to skip doccmd[all]{reset}
Expand All @@ -1054,7 +1084,18 @@ def test_skip_no_arguments(tmp_path: Path) -> None:
assert result.stderr == expected_stderr


def test_skip_bad_arguments(tmp_path: Path) -> None:
@pytest.mark.parametrize(
argnames=("fail_on_parse_error_options", "expected_exit_code"),
argvalues=[
([], 0),
(["--fail-on-parse-error"], 1),
],
)
def test_skip_bad_arguments(
tmp_path: Path,
fail_on_parse_error_options: Sequence[str],
expected_exit_code: int,
) -> None:
"""
An error is shown if a skip is given with bad arguments.
"""
Expand All @@ -1069,6 +1110,7 @@ def test_skip_bad_arguments(tmp_path: Path) -> None:
"""
rst_file.write_text(data=content, encoding="utf-8")
arguments = [
*fail_on_parse_error_options,
"--no-pad-file",
"--language",
"python",
Expand All @@ -1082,7 +1124,10 @@ def test_skip_bad_arguments(tmp_path: Path) -> None:
catch_exceptions=False,
color=True,
)
assert result.exit_code == 0, (result.stdout, result.stderr)
assert result.exit_code == expected_exit_code, (
result.stdout,
result.stderr,
)
expected_stderr = textwrap.dedent(
text=f"""\
{fg.red}Could not parse {rst_file}: malformed arguments to skip doccmd[all]: '!!!'{reset}
Expand Down Expand Up @@ -2485,7 +2530,18 @@ def test_multiple_exclude_patterns(tmp_path: Path) -> None:
assert result.stderr == ""


def test_lexing_exception(tmp_path: Path) -> None:
@pytest.mark.parametrize(
argnames=("fail_on_parse_error_options", "expected_exit_code"),
argvalues=[
([], 0),
(["--fail-on-parse-error"], 1),
],
)
def test_lexing_exception(
tmp_path: Path,
fail_on_parse_error_options: Sequence[str],
expected_exit_code: int,
) -> None:
"""
Lexing exceptions are handled when an invalid source file is given.
"""
Expand All @@ -2498,6 +2554,7 @@ def test_lexing_exception(tmp_path: Path) -> None:
"""
source_file.write_text(data=invalid_content, encoding="utf-8")
arguments = [
*fail_on_parse_error_options,
"--language",
"python",
"--command",
Expand All @@ -2510,7 +2567,10 @@ def test_lexing_exception(tmp_path: Path) -> None:
catch_exceptions=False,
color=True,
)
assert result.exit_code == 0, (result.stdout, result.stderr)
assert result.exit_code == expected_exit_code, (
result.stdout,
result.stderr,
)
expected_stderr = textwrap.dedent(
text=f"""\
{fg.red}Could not parse {source_file}: Could not find end of ' <!-- code -->\\n', starting at line 1, column 1, looking for '(?:(?<=\\n) )?--+>' in {source_file}:
Expand Down
4 changes: 4 additions & 0 deletions tests/test_doccmd/test_help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,8 @@ Options:
in directories. This option can be used
multiple times. Use forward slashes on all
platforms.
--fail-on-parse-error / --no-fail-on-parse-error
Whether to fail (with exit code 1) if a given
file cannot be parsed. [default: no-fail-on-
parse-error]
--help Show this message and exit.