Skip to content

Commit

Permalink
feat: Allow expecting specific exit codes
Browse files Browse the repository at this point in the history
Issue #10: #10
  • Loading branch information
pawamoy committed Jan 27, 2023
1 parent 0940ca9 commit 620ec66
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 26 deletions.
12 changes: 7 additions & 5 deletions docs/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,12 @@ Example:

Code blocks execution can fail.
For example, your Python code may raise exceptions,
or your shell code may return a non-zero exit code.
or your shell code may return a non-zero exit code
(for shell commands that are expected to return non-zero,
see [Expecting a non-zero exit code](shell/#expecting-a-non-zero-exit-code)).

In these cases, the exception and traceback (Python),
or the standard error message (shell) will be rendered
or the current output (shell) will be rendered
instead of the result, and a warning will be logged.

Example of failing code:
Expand All @@ -214,7 +216,7 @@ assert 1 + 1 == 11
````

```text title="MkDocs output"
WARNING - markdown_exec: Execution of python code block exited with non-zero status
WARNING - markdown_exec: Execution of python code block exited with errors
```

```python title="Rendered traceback"
Expand All @@ -239,7 +241,7 @@ assert 1 + 1 == 11
````

```text title="MkDocs output"
WARNING - markdown_exec: Execution of python code block 'print hello' exited with non-zero status
WARNING - markdown_exec: Execution of python code block 'print hello' exited with errors
```

> TIP: **Titles act as IDs as well!**
Expand All @@ -254,7 +256,7 @@ WARNING - markdown_exec: Execution of python code block 'print hello' exited w
> ````
>
> ```text title="MkDocs output"
> WARNING - markdown_exec: Execution of python code block 'print world' exited with non-zero status
> WARNING - markdown_exec: Execution of python code block 'print world' exited with errors
> ```
## Literate Markdown
Expand Down
26 changes: 26 additions & 0 deletions docs/usage/shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,29 @@ $ mkdocs --help
echo Markdown is **cool**
```
````

## Expecting a non-zero exit code

You will sometimes want to run a command
that returns a non-zero exit code,
for example to show how errors look to your users.

You can tell Markdown Exec to expect
a particular exit code with the `returncode` option:

````md
```bash exec="true" returncode="1"
echo Not in the mood today
exit 1
```
````

In that case, the executed code won't be considered
to have failed, its output will be rendered normally,
and no warning will be logged in the MkDocs output,
allowing your strict builds to pass.

If the exit code is different than the one specified
with `returncode`, it will be considered a failure,
its output will be renderer anyway (stdout and stderr combined),
and a warning will be logged in the MkDocs output.
2 changes: 2 additions & 0 deletions src/markdown_exec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ def validator(
html_value = _to_bool(inputs.pop("html", "no"))
source_value = inputs.pop("source", "")
result_value = inputs.pop("result", "")
returncode_value = int(inputs.pop("returncode", "0"))
tabs_value = inputs.pop("tabs", "|".join(default_tabs))
tabs = tuple(_tabs_re.split(tabs_value, maxsplit=1))
options["id"] = id_value
options["html"] = html_value
options["source"] = source_value
options["result"] = result_value
options["returncode"] = returncode_value
options["tabs"] = tabs
options["extra"] = inputs
return True
Expand Down
22 changes: 19 additions & 3 deletions src/markdown_exec/formatters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@
default_tabs = ("Source", "Result")


class ExecutionError(Exception):
"""Exception raised for errors during execution of a code block.
Attributes:
message: The exception message.
returncode: The code returned by the execution of the code block.
"""

def __init__(self, message: str, returncode: int | None = None) -> None: # noqa: D107
super().__init__(message)
self.returncode = returncode


def base_format( # noqa: WPS231
*,
language: str,
Expand All @@ -26,6 +39,7 @@ def base_format( # noqa: WPS231
result: str = "",
tabs: tuple[str, str] = default_tabs,
id: str = "", # noqa: A002,VNE003
returncode: int = 0,
transform_source: Callable[[str], tuple[str, str]] | None = None,
**options: Any,
) -> Markup:
Expand All @@ -41,6 +55,7 @@ def base_format( # noqa: WPS231
result: If provided, use as language to format result in a code block.
tabs: Titles of tabs (if used).
id: An optional ID for the code block (useful when warning about errors).
returncode: The expected exit code.
transform_source: An optional callable that returns transformed versions of the source.
The input source is the one that is ran, the output source is the one that is
rendered (when the source option is enabled).
Expand All @@ -59,11 +74,12 @@ def base_format( # noqa: WPS231
source_output = code

try:
output = run(source_input, **extra)
except RuntimeError as error:
output = run(source_input, returncode=returncode, **extra)
except ExecutionError as error:
identifier = id or extra.get("title", "")
identifier = identifier and f"'{identifier}' "
logger.warning(f"Execution of {language} code block {identifier}exited with non-zero status")
exit_message = "errors" if error.returncode is None else f"unexpected code {error.returncode}"
logger.warning(f"Execution of {language} code block {identifier}exited with {exit_message}")
return markdown.convert(str(error))

if html:
Expand Down
18 changes: 11 additions & 7 deletions src/markdown_exec/formatters/bash.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@

import subprocess # noqa: S404

from markdown_exec.formatters.base import base_format
from markdown_exec.formatters.base import ExecutionError, base_format
from markdown_exec.rendering import code_block


def _run_bash(code: str, **extra: str) -> str:
try:
output = subprocess.check_output(["bash", "-c", code], stderr=subprocess.STDOUT).decode() # noqa: S603,S607
except subprocess.CalledProcessError as error:
raise RuntimeError(code_block("bash", error.output, **extra))
return output
def _run_bash(code: str, *, returncode: int = 0, **extra: str) -> str:
process = subprocess.run( # noqa: S603,S607
["bash", "-c", code],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
if process.returncode != returncode:
raise ExecutionError(code_block("sh", process.stdout, **extra), process.returncode)
return process.stdout


def _format_bash(**kwargs) -> str:
Expand Down
4 changes: 2 additions & 2 deletions src/markdown_exec/formatters/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from io import StringIO
from typing import Any

from markdown_exec.formatters.base import base_format
from markdown_exec.formatters.base import ExecutionError, base_format
from markdown_exec.rendering import code_block


Expand All @@ -27,7 +27,7 @@ def _run_python(code: str, **extra: str) -> str:
if frame.filename == "<string>":
frame.filename = "<executed code block>"
frame._line = code.split("\n")[frame.lineno - 1] # type: ignore[attr-defined,operator] # noqa: WPS437
raise RuntimeError(code_block("python", "".join(trace.format()), **extra))
raise ExecutionError(code_block("python", "".join(trace.format()), **extra))
return buffer.getvalue()


Expand Down
18 changes: 11 additions & 7 deletions src/markdown_exec/formatters/sh.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@

import subprocess # noqa: S404

from markdown_exec.formatters.base import base_format
from markdown_exec.formatters.base import ExecutionError, base_format
from markdown_exec.rendering import code_block


def _run_sh(code: str, **extra: str) -> str:
try:
output = subprocess.check_output(["sh", "-c", code], stderr=subprocess.STDOUT).decode() # noqa: S603,S607
except subprocess.CalledProcessError as error:
raise RuntimeError(code_block("sh", error.output, **extra))
return output
def _run_sh(code: str, *, returncode: int = 0, **extra: str) -> str:
process = subprocess.run( # noqa: S603,S607
["sh", "-c", code],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
if process.returncode != returncode:
raise ExecutionError(code_block("sh", process.stdout, **extra), process.returncode)
return process.stdout


def _format_sh(**kwargs) -> str:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_error_raised(md: Markdown, caplog) -> None:
assert "Traceback" in html
assert "ValueError" in html
assert "oh no!" in html
assert "Execution of python code block exited with non-zero status" in caplog.text
assert "Execution of python code block exited with errors" in caplog.text


def test_can_print_non_string_objects(md: Markdown) -> None:
Expand Down
23 changes: 22 additions & 1 deletion tests/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,25 @@ def test_error_raised(md: Markdown, caplog) -> None:
)
)
assert "error" in html
assert "Execution of sh code block exited with non-zero status" in caplog.text
assert "Execution of sh code block exited with unexpected code 2" in caplog.text


def test_return_code(md: Markdown, caplog) -> None:
"""Assert return code is used correctly.
Parameters:
md: A Markdown instance (fixture).
caplog: Pytest fixture to capture logs.
"""
html = md.convert(
dedent(
"""
```sh exec="yes" returncode="1"
echo Not in the mood
exit 1
```
"""
)
)
assert "Not in the mood" in html
assert "exited with" not in caplog.text

0 comments on commit 620ec66

Please sign in to comment.