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

feat(python): use returncode for python exec #80

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,13 @@ grep extra_css README.md && exit 2
```
````

You can also expect a type of Exception in Python:

````md
```python exec="1" source="tabbed-left" exception="ValueError"
raise ValueError
```
````

See [usage](https://pawamoy.github.io/markdown-exec/usage/) for more details,
and the [gallery](https://pawamoy.github.io/markdown-exec/gallery/) for more examples!
8 changes: 6 additions & 2 deletions docs/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ linking to their related documentation:
- [`idprefix`](#html-ids): Change or remove the prefix in front of HTML ids/hrefs.
- [`result`](#wrap-result-in-a-code-block): Choose the syntax highlight of your code block output.
- [`returncode`](shell.md#expecting-a-non-zero-exit-code): Tell what return code is expected (shell code).
- [`exception`](python.md#expecting-an-exception): Tell what exception code is expected (python code).
- [`session`](#sessions): Execute code blocks within a named session, reusing previously defined variables, etc..
- [`source`](#render-the-source-code-as-well): Render the source as well as the output.
- [`tabs`](#change-the-titles-of-tabs): When rendering the source using tabs, choose the tabs titles.
Expand Down Expand Up @@ -335,8 +336,11 @@ 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
(for shell commands that are expected to return non-zero,
see [Expecting a non-zero exit code](shell.md#expecting-a-non-zero-exit-code)).
For shell commands that are expected to return non-zero,
see [Expecting a non-zero exit code](shell.md#expecting-a-non-zero-exit-code).

For python blocks that are expected to raise an exception
see [Expecting an Exception](python.md#expecting-an-exception).

In these cases, the exception and traceback (Python),
or the current output (shell) will be rendered
Expand Down
29 changes: 28 additions & 1 deletion docs/usage/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,31 @@ as well as their output:
```pycon exec="1" source="console"
--8<-- "usage/multiple.pycon"
```
````
````

## Expecting an `Exception`

You will sometimes want to run Python code that you
expect to raise an `Exception`.
For example to show how errors look to your users.

You can tell Markdown Exec to expect
a particular `Exception` code with the `exception` option:

````md
```python exec="true" exception="ValueError"
print("This will run")
raise ValueError("This is a value error")
print("This will not run")
```
````

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 `exception` code is different than the one specified
with `exception=`, it will be considered a failure,
only the traceback will be renderer
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 @@ -74,6 +74,7 @@ def validator(
source_value = inputs.pop("source", "")
result_value = inputs.pop("result", "")
returncode_value = int(inputs.pop("returncode", "0"))
exception_value = inputs.pop("exception", None)
session_value = inputs.pop("session", "")
update_toc_value = _to_bool(inputs.pop("updatetoc", "yes"))
tabs_value = inputs.pop("tabs", "|".join(default_tabs))
Expand All @@ -86,6 +87,7 @@ def validator(
options["source"] = source_value
options["result"] = result_value
options["returncode"] = returncode_value
options["exception"] = exception_value
options["session"] = session_value
options["update_toc"] = update_toc_value
options["tabs"] = tabs
Expand Down
22 changes: 18 additions & 4 deletions src/markdown_exec/formatters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ class ExecutionError(Exception):
returncode: The code returned by the execution of the code block.
"""

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


def _format_log_details(details: str, *, strip_fences: bool = False) -> str:
Expand All @@ -97,6 +98,7 @@ def base_format(
id: str = "", # noqa: A002
id_prefix: str | None = None,
returncode: int = 0,
exception: str | None = None,
transform_source: Callable[[str], tuple[str, str]] | None = None,
session: str | None = None,
update_toc: bool = True,
Expand All @@ -117,7 +119,8 @@ def base_format(
tabs: Titles of tabs (if used).
id: An optional ID for the code block (useful when warning about errors).
id_prefix: A string used to prefix HTML ids in the generated HTML.
returncode: The expected exit code.
returncode: The expected exit code. shell only
exception: The expected Exception raised. python or pycon only
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 @@ -141,11 +144,22 @@ def base_format(

try:
with working_directory(workdir), console_width(width):
output = run(source_input, returncode=returncode, session=session, id=id, **extra)
if language in ("python", "pycon"):
output = run(source_input, exception=exception, session=session, id=id, **extra)
else:
output = run(source_input, returncode=returncode, session=session, id=id, **extra)
except ExecutionError as error:
identifier = id or extra.get("title", "")
identifier = identifier and f"'{identifier}' "
exit_message = "errors" if error.returncode is None else f"unexpected code {error.returncode}"
if error.returncode is not None:
exit_message = f"returncode {error.returncode} expected {returncode}"
elif error.exception is not None:
exit_message = f"{error.exception}"
if exception is not None:
exit_message += f" expected {exception}"
else:
exit_message = "errors"

log_message = (
f"Execution of {language} code block {identifier}exited with {exit_message}\n\n"
f"Code block is:\n\n{_format_log_details(source_input)}\n\n"
Expand Down
7 changes: 5 additions & 2 deletions src/markdown_exec/formatters/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def _code_block_id(

def _run_python(
code: str,
returncode: int | None = None, # noqa: ARG001
exception: str | None = None,
session: str | None = None,
id: str | None = None, # noqa: A002
**extra: str,
Expand Down Expand Up @@ -77,7 +77,10 @@ def _run_python(
frame._lines = _code_blocks[frame.filename][frame.lineno - 1] # type: ignore[attr-defined,operator]
else:
frame._line = _code_blocks[frame.filename][frame.lineno - 1] # type: ignore[attr-defined,operator]
raise ExecutionError(code_block("python", "".join(trace.format()), **extra)) from error
if exception is not None and exception in str(error.__class__):
_buffer_print(buffer, "".join(trace.format()))
else:
raise ExecutionError(code_block("python", "".join(trace.format())), exception=str(type(error), **extra)) from error
return buffer.getvalue()


Expand Down
37 changes: 36 additions & 1 deletion tests/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_error_raised(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
assert "Traceback" in html
assert "ValueError" in html
assert "oh no!" in html
assert "Execution of python code block exited with errors" in caplog.text
assert "Execution of python code block exited with <class 'ValueError'>" in caplog.text


def test_can_print_non_string_objects(md: Markdown) -> None:
Expand Down Expand Up @@ -229,3 +229,38 @@ def f(x: Int) -> None:
)
assert "<code>Int</code>" not in html
assert re.search(r"class '_code_block_n\d+_\.Int'", html)


def test_exception_code(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
"""Assert return code is used correctly.

Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
```python exec="yes" exception="ValueError"
print("blah blah blah")
raise ValueError
```
""",
),
)
assert "blah blah blah" in html
assert "ValueError" in html
assert "exited with" not in caplog.text

html = md.convert(
dedent(
"""
```python exec="yes" exception="TypeError"
print("blah blah blah")
raise ValueError
```
""",
),
)
assert "blah blah blah" not in html
assert "ValueError" in html
assert "<class 'ValueError'> expected TypeError" in caplog.text
2 changes: 1 addition & 1 deletion tests/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_error_raised(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
),
)
assert "error" in html
assert "Execution of sh code block exited with unexpected code 2" in caplog.text
assert "Execution of sh code block exited with returncode 2 expected 0" in caplog.text


def test_return_code(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
Expand Down