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

1"""Generic formatter for executing code.""" 

2 

3from __future__ import annotations 

4 

5from textwrap import indent 

6from typing import TYPE_CHECKING, Any, Callable 

7from uuid import uuid4 

8 

9from markupsafe import Markup 

10 

11from markdown_exec.logger import get_logger 

12from markdown_exec.rendering import MarkdownConverter, add_source, code_block 

13 

14if TYPE_CHECKING: 

15 from markdown.core import Markdown 

16 

17logger = get_logger(__name__) 

18default_tabs = ("Source", "Result") 

19 

20 

21class ExecutionError(Exception): 

22 """Exception raised for errors during execution of a code block. 

23 

24 Attributes: 

25 message: The exception message. 

26 returncode: The code returned by the execution of the code block. 

27 """ 

28 

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

30 super().__init__(message) 

31 self.returncode = returncode 

32 

33 

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) 

40 

41 

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. 

61 

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. 

81 

82 Returns: 

83 HTML contents. 

84 """ 

85 markdown = MarkdownConverter(md, update_toc=update_toc) 

86 extra = options.get("extra", {}) 

87 

88 if transform_source: 

89 source_input, source_output = transform_source(code) 

90 else: 

91 source_input = code 

92 source_output = code 

93 

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)) 

107 

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) 

121 

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)