Coverage for src/markdown_exec/formatters/base.py: 95.16%
48 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-18 18:20 +0200
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-18 18:20 +0200
1"""Generic formatter for executing code."""
3from __future__ import annotations
5from textwrap import indent
6from typing import TYPE_CHECKING, Any, Callable
7from uuid import uuid4
9from markupsafe import Markup
11from markdown_exec.logger import get_logger
12from markdown_exec.rendering import MarkdownConverter, add_source, code_block
14if TYPE_CHECKING:
15 from markdown.core import Markdown
17logger = get_logger(__name__)
18default_tabs = ("Source", "Result")
21class ExecutionError(Exception):
22 """Exception raised for errors during execution of a code block.
24 Attributes:
25 message: The exception message.
26 returncode: The code returned by the execution of the code block.
27 """
29 def __init__(self, message: str, returncode: int | None = None) -> None: # noqa: D107
30 super().__init__(message)
31 self.returncode = returncode
34def _format_log_details(details: str, *, strip_fences: bool = False) -> str:
35 if strip_fences:
36 lines = details.split("\n")
37 if lines[0].startswith("```") and lines[-1].startswith("```"): 37 ↛ 39line 37 didn't jump to line 39, because the condition on line 37 was never false
38 details = "\n".join(lines[1:-1])
39 return indent(details, " " * 2)
42def base_format(
43 *,
44 language: str,
45 run: Callable,
46 code: str,
47 md: Markdown,
48 html: bool = False,
49 source: str = "",
50 result: str = "",
51 tabs: tuple[str, str] = default_tabs,
52 id: str = "", # noqa: A002
53 id_prefix: str | None = None,
54 returncode: int = 0,
55 transform_source: Callable[[str], tuple[str, str]] | None = None,
56 session: str | None = None,
57 update_toc: bool = True,
58 **options: Any,
59) -> Markup:
60 """Execute code and return HTML.
62 Parameters:
63 language: The code language.
64 run: Function that runs code and returns output.
65 code: The code to execute.
66 md: The Markdown instance.
67 html: Whether to inject output as HTML directly, without rendering.
68 source: Whether to show source as well, and where.
69 result: If provided, use as language to format result in a code block.
70 tabs: Titles of tabs (if used).
71 id: An optional ID for the code block (useful when warning about errors).
72 id_prefix: A string used to prefix HTML ids in the generated HTML.
73 returncode: The expected exit code.
74 transform_source: An optional callable that returns transformed versions of the source.
75 The input source is the one that is ran, the output source is the one that is
76 rendered (when the source option is enabled).
77 session: A session name, to persist state between executed code blocks.
78 update_toc: Whether to include generated headings
79 into the Markdown table of contents (toc extension).
80 **options: Additional options passed from the formatter.
82 Returns:
83 HTML contents.
84 """
85 markdown = MarkdownConverter(md, update_toc=update_toc)
86 extra = options.get("extra", {})
88 if transform_source:
89 source_input, source_output = transform_source(code)
90 else:
91 source_input = code
92 source_output = code
94 try:
95 output = run(source_input, returncode=returncode, session=session, id=id, **extra)
96 except ExecutionError as error:
97 identifier = id or extra.get("title", "")
98 identifier = identifier and f"'{identifier}' "
99 exit_message = "errors" if error.returncode is None else f"unexpected code {error.returncode}"
100 log_message = (
101 f"Execution of {language} code block {identifier}exited with {exit_message}\n\n"
102 f"Code block is:\n\n{_format_log_details(source_input)}\n\n"
103 f"Output is:\n\n{_format_log_details(str(error), strip_fences=True)}\n"
104 )
105 logger.warning(log_message)
106 return markdown.convert(str(error))
108 if html:
109 if source:
110 placeholder = str(uuid4())
111 wrapped_output = add_source(
112 source=source_output,
113 location=source,
114 output=placeholder,
115 language=language,
116 tabs=tabs,
117 **extra,
118 )
119 return markdown.convert(wrapped_output, stash={placeholder: output})
120 return Markup(output)
122 wrapped_output = output
123 if result and source != "console": 123 ↛ 124line 123 didn't jump to line 124, because the condition on line 123 was never true
124 wrapped_output = code_block(result, output)
125 if source:
126 wrapped_output = add_source(
127 source=source_output,
128 location=source,
129 output=wrapped_output,
130 language=language,
131 tabs=tabs,
132 result=result,
133 **extra,
134 )
135 prefix = id_prefix if id_prefix is not None else (f"{id}-" if id else None)
136 return markdown.convert(wrapped_output, id_prefix=prefix)