markdown-exec 1.8.2__tar.gz → 1.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/PKG-INFO +8 -3
  2. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/README.md +6 -2
  3. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/pyproject.toml +10 -1
  4. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/__init__.py +7 -1
  5. markdown_exec-1.9.0/src/markdown_exec/formatters/_exec_python.py +8 -0
  6. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/base.py +53 -3
  7. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/pyodide.py +11 -2
  8. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/python.py +3 -3
  9. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/logger.py +20 -7
  10. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/mkdocs_plugin.py +10 -0
  11. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/processors.py +17 -12
  12. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/pyodide.css +1 -0
  13. markdown_exec-1.8.2/tests/__init__.py +0 -7
  14. markdown_exec-1.8.2/tests/conftest.py +0 -28
  15. markdown_exec-1.8.2/tests/test_base_formatter.py +0 -60
  16. markdown_exec-1.8.2/tests/test_converter.py +0 -76
  17. markdown_exec-1.8.2/tests/test_python.py +0 -205
  18. markdown_exec-1.8.2/tests/test_shell.py +0 -87
  19. markdown_exec-1.8.2/tests/test_toc.py +0 -89
  20. markdown_exec-1.8.2/tests/test_tree.py +0 -25
  21. markdown_exec-1.8.2/tests/test_validator.py +0 -38
  22. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/LICENSE +0 -0
  23. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/ansi.css +0 -0
  24. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/debug.py +0 -0
  25. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/__init__.py +0 -0
  26. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/bash.py +0 -0
  27. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/console.py +0 -0
  28. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/markdown.py +0 -0
  29. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/pycon.py +0 -0
  30. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/sh.py +0 -0
  31. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/formatters/tree.py +0 -0
  32. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/py.typed +0 -0
  33. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/pyodide.js +0 -0
  34. {markdown_exec-1.8.2 → markdown_exec-1.9.0}/src/markdown_exec/rendering.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: markdown-exec
3
- Version: 1.8.2
3
+ Version: 1.9.0
4
4
  Summary: Utilities to execute code blocks in Markdown files.
5
5
  Keywords: markdown,python,exec,shell,bash,mkdocs
6
6
  Author-Email: =?utf-8?q?Timoth=C3=A9e_Mazzucotelli?= <dev@pawamoy.fr>
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: Documentation
19
20
  Classifier: Topic :: Software Development
20
21
  Classifier: Topic :: Utilities
@@ -36,9 +37,9 @@ Description-Content-Type: text/markdown
36
37
  # Markdown Exec
37
38
 
38
39
  [![ci](https://github.com/pawamoy/markdown-exec/workflows/ci/badge.svg)](https://github.com/pawamoy/markdown-exec/actions?query=workflow%3Aci)
39
- [![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://pawamoy.github.io/markdown-exec/)
40
+ [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://pawamoy.github.io/markdown-exec/)
40
41
  [![pypi version](https://img.shields.io/pypi/v/markdown-exec.svg)](https://pypi.org/project/markdown-exec/)
41
- [![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/markdown-exec)
42
+ [![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/markdown-exec)
42
43
  [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#markdown-exec:gitter.im)
43
44
 
44
45
  Utilities to execute code blocks in Markdown files.
@@ -113,6 +114,10 @@ markdown_extensions:
113
114
  plugins:
114
115
  - search
115
116
  - markdown-exec
117
+
118
+ # SuperFences must still be enabled!
119
+ markdown_extensions:
120
+ - pymdownx.superfences
116
121
  ```
117
122
 
118
123
  We do recommend enabling Markdown Exec with the MkDocs plugin
@@ -1,9 +1,9 @@
1
1
  # Markdown Exec
2
2
 
3
3
  [![ci](https://github.com/pawamoy/markdown-exec/workflows/ci/badge.svg)](https://github.com/pawamoy/markdown-exec/actions?query=workflow%3Aci)
4
- [![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://pawamoy.github.io/markdown-exec/)
4
+ [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://pawamoy.github.io/markdown-exec/)
5
5
  [![pypi version](https://img.shields.io/pypi/v/markdown-exec.svg)](https://pypi.org/project/markdown-exec/)
6
- [![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/markdown-exec)
6
+ [![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/markdown-exec)
7
7
  [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#markdown-exec:gitter.im)
8
8
 
9
9
  Utilities to execute code blocks in Markdown files.
@@ -78,6 +78,10 @@ markdown_extensions:
78
78
  plugins:
79
79
  - search
80
80
  - markdown-exec
81
+
82
+ # SuperFences must still be enabled!
83
+ markdown_extensions:
84
+ - pymdownx.superfences
81
85
  ```
82
86
 
83
87
  We do recommend enabling Markdown Exec with the MkDocs plugin
@@ -32,6 +32,7 @@ classifiers = [
32
32
  "Programming Language :: Python :: 3.10",
33
33
  "Programming Language :: Python :: 3.11",
34
34
  "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
35
36
  "Topic :: Documentation",
36
37
  "Topic :: Software Development",
37
38
  "Topic :: Utilities",
@@ -40,7 +41,7 @@ classifiers = [
40
41
  dependencies = [
41
42
  "pymdown-extensions>=9",
42
43
  ]
43
- version = "1.8.2"
44
+ version = "1.9.0"
44
45
 
45
46
  [project.license]
46
47
  text = "ISC"
@@ -69,3 +70,11 @@ source = "scm"
69
70
  [tool.pdm.build]
70
71
  package-dir = "src"
71
72
  editable-backend = "editables"
73
+ source-includes = [
74
+ "share",
75
+ ]
76
+
77
+ [tool.pdm.build.wheel-data]
78
+ data = [
79
+ { path = "share/**/*", relative-to = "." },
80
+ ]
@@ -8,6 +8,7 @@ Utilities to execute code blocks in Markdown files.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import os
11
12
  import re
12
13
  from typing import TYPE_CHECKING, Any
13
14
 
@@ -26,6 +27,7 @@ from markdown_exec.formatters.tree import _format_tree
26
27
 
27
28
  __all__: list[str] = ["formatter", "validator"]
28
29
 
30
+ MARKDOWN_EXEC_AUTO = [lang.strip() for lang in os.getenv("MARKDOWN_EXEC_AUTO", "").split(",")]
29
31
 
30
32
  formatters = {
31
33
  "bash": _format_bash,
@@ -63,7 +65,7 @@ def validator(
63
65
  Returns:
64
66
  Success or not.
65
67
  """
66
- exec_value = _to_bool(inputs.pop("exec", "no"))
68
+ exec_value = language in MARKDOWN_EXEC_AUTO or _to_bool(inputs.pop("exec", "no"))
67
69
  if language not in {"tree", "pyodide"} and not exec_value:
68
70
  return False
69
71
  id_value = inputs.pop("id", "")
@@ -76,6 +78,8 @@ def validator(
76
78
  update_toc_value = _to_bool(inputs.pop("updatetoc", "yes"))
77
79
  tabs_value = inputs.pop("tabs", "|".join(default_tabs))
78
80
  tabs = tuple(_tabs_re.split(tabs_value, maxsplit=1))
81
+ workdir_value = inputs.pop("workdir", None)
82
+ width_value = int(inputs.pop("width", "0"))
79
83
  options["id"] = id_value
80
84
  options["id_prefix"] = id_prefix_value
81
85
  options["html"] = html_value
@@ -85,6 +89,8 @@ def validator(
85
89
  options["session"] = session_value
86
90
  options["update_toc"] = update_toc_value
87
91
  options["tabs"] = tabs
92
+ options["workdir"] = workdir_value
93
+ options["width"] = width_value
88
94
  options["extra"] = inputs
89
95
  return True
90
96
 
@@ -0,0 +1,8 @@
1
+ """Special module without future annotations for executing Python code."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ def exec_python(code: str, filename: str, exec_globals: Optional[Dict[str, Any]] = None) -> None:
7
+ compiled = compile(code, filename=filename, mode="exec")
8
+ exec(compiled, exec_globals) # noqa: S102
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
6
+ from contextlib import contextmanager
5
7
  from textwrap import indent
6
- from typing import TYPE_CHECKING, Any, Callable
8
+ from typing import TYPE_CHECKING, Any, Callable, Iterator
7
9
  from uuid import uuid4
8
10
 
9
11
  from markupsafe import Markup
@@ -18,6 +20,47 @@ logger = get_logger(__name__)
18
20
  default_tabs = ("Source", "Result")
19
21
 
20
22
 
23
+ @contextmanager
24
+ def working_directory(path: str | None = None) -> Iterator[None]:
25
+ """Change the working directory for the duration of the context.
26
+
27
+ Parameters:
28
+ path: The path to change the working directory to.
29
+ """
30
+ if path:
31
+ old_cwd = os.getcwd()
32
+ os.chdir(path)
33
+ try:
34
+ yield
35
+ finally:
36
+ os.chdir(old_cwd)
37
+ else:
38
+ yield
39
+
40
+
41
+ @contextmanager
42
+ def console_width(width: int | None = None) -> Iterator[None]:
43
+ """Set the console width for the duration of the context.
44
+
45
+ The console width is set using the `COLUMNS` environment variable.
46
+
47
+ Parameters:
48
+ width: The width to set the console to.
49
+ """
50
+ if width:
51
+ old_width = os.environ.get("COLUMNS", None)
52
+ os.environ["COLUMNS"] = str(width)
53
+ try:
54
+ yield
55
+ finally:
56
+ if old_width is None:
57
+ del os.environ["COLUMNS"]
58
+ else:
59
+ os.environ["COLUMNS"] = old_width
60
+ else:
61
+ yield
62
+
63
+
21
64
  class ExecutionError(Exception):
22
65
  """Exception raised for errors during execution of a code block.
23
66
 
@@ -55,6 +98,8 @@ def base_format(
55
98
  transform_source: Callable[[str], tuple[str, str]] | None = None,
56
99
  session: str | None = None,
57
100
  update_toc: bool = True,
101
+ workdir: str | None = None,
102
+ width: int | None = None,
58
103
  **options: Any,
59
104
  ) -> Markup:
60
105
  """Execute code and return HTML.
@@ -77,6 +122,7 @@ def base_format(
77
122
  session: A session name, to persist state between executed code blocks.
78
123
  update_toc: Whether to include generated headings
79
124
  into the Markdown table of contents (toc extension).
125
+ workdir: The working directory to use for the execution.
80
126
  **options: Additional options passed from the formatter.
81
127
 
82
128
  Returns:
@@ -92,7 +138,8 @@ def base_format(
92
138
  source_output = code
93
139
 
94
140
  try:
95
- output = run(source_input, returncode=returncode, session=session, id=id, **extra)
141
+ with working_directory(workdir), console_width(width):
142
+ output = run(source_input, returncode=returncode, session=session, id=id, **extra)
96
143
  except ExecutionError as error:
97
144
  identifier = id or extra.get("title", "")
98
145
  identifier = identifier and f"'{identifier}' "
@@ -105,9 +152,12 @@ def base_format(
105
152
  logger.warning(log_message)
106
153
  return markdown.convert(str(error))
107
154
 
155
+ if not output:
156
+ return Markup()
157
+
108
158
  if html:
109
159
  if source:
110
- placeholder = str(uuid4())
160
+ placeholder = f'<div class="{uuid4()}"></div>'
111
161
  wrapped_output = add_source(
112
162
  source=source_output,
113
163
  location=source,
@@ -7,16 +7,21 @@ from typing import TYPE_CHECKING, Any
7
7
  if TYPE_CHECKING:
8
8
  from markdown import Markdown
9
9
 
10
+ # All Ace.js themes listed here:
11
+ # https://github.com/ajaxorg/ace/tree/master/src/theme
12
+
10
13
  play_emoji = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7Z"></path></svg>'
11
14
  clear_emoji = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.14 3c-.51 0-1.02.2-1.41.59L2.59 14.73c-.78.77-.78 2.04 0 2.83L5.03 20h7.66l8.72-8.73c.79-.77.79-2.04 0-2.83l-4.85-4.85c-.39-.39-.91-.59-1.42-.59M17 18l-2 2h7v-2"></path></svg>'
12
15
 
13
- template = """
16
+ assets = """
14
17
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.16.0/ace.js"></script>
15
18
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
16
19
  <script type="text/javascript" src="https://cdn.jsdelivr.net/pyodide/v0.23.0/full/pyodide.js"></script>
17
20
  <link title="light" rel="alternate stylesheet" href="https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow.min.css" disabled="disabled">
18
21
  <link title="dark" rel="alternate stylesheet" href="https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow-night-blue.min.css" disabled="disabled">
22
+ """
19
23
 
24
+ template = """
20
25
  <div class="pyodide">
21
26
  <div class="pyodide-editor-bar">
22
27
  <span class="pyodide-bar-item">Editor (session: %(session)s)</span><span id="%(id_prefix)srun" title="Run: press Ctrl-Enter" class="pyodide-bar-item pyodide-clickable"><span class="twemoji">%(play_emoji)s</span> Run</span>
@@ -43,6 +48,7 @@ def _format_pyodide(code: str, md: Markdown, session: str, extra: dict, **option
43
48
  _counter += 1
44
49
  install = extra.pop("install", "")
45
50
  install = install.split(",") if install else []
51
+ exclude_assets = extra.pop("assets", "1").lower() in {"0", "false", "no", "off"}
46
52
  theme = extra.pop("theme", "tomorrow,tomorrow_night")
47
53
  if "," not in theme:
48
54
  theme = f"{theme},{theme}"
@@ -57,4 +63,7 @@ def _format_pyodide(code: str, md: Markdown, session: str, extra: dict, **option
57
63
  "play_emoji": play_emoji,
58
64
  "clear_emoji": clear_emoji,
59
65
  }
60
- return template % data
66
+ rendered = template % data
67
+ if exclude_assets:
68
+ return rendered
69
+ return assets + rendered
@@ -11,6 +11,7 @@ from io import StringIO
11
11
  from types import ModuleType
12
12
  from typing import Any
13
13
 
14
+ from markdown_exec.formatters._exec_python import exec_python
14
15
  from markdown_exec.formatters.base import ExecutionError, base_format
15
16
  from markdown_exec.rendering import code_block
16
17
 
@@ -67,9 +68,8 @@ def _run_python(
67
68
  exec_globals["print"] = partial(_buffer_print, buffer)
68
69
 
69
70
  try:
70
- compiled = compile(code, filename=code_block_id, mode="exec")
71
- exec(compiled, exec_globals) # noqa: S102
72
- except Exception as error: # noqa: BLE001
71
+ exec_python(code, code_block_id, exec_globals)
72
+ except Exception as error:
73
73
  trace = traceback.TracebackException.from_exception(error)
74
74
  for frame in trace.stack:
75
75
  if frame.filename.startswith("<code block: "):
@@ -39,22 +39,35 @@ class _Logger:
39
39
  _default_logger: Any = logging.getLogger
40
40
  _instances: ClassVar[dict[str, _Logger]] = {}
41
41
 
42
+ # See same code in Griffe project.
42
43
  def __init__(self, name: str) -> None:
43
- # default logger that can be patched by third-party
44
+ # Default logger that can be patched by third-party.
44
45
  self._logger = self.__class__._default_logger(name)
45
- # register instance
46
- self._instances[name] = self
47
46
 
48
47
  def __getattr__(self, name: str) -> Any:
49
- # forward everything to the logger
48
+ # Forward everything to the logger.
50
49
  return getattr(self._logger, name)
51
50
 
51
+ @classmethod
52
+ def get(cls, name: str) -> _Logger:
53
+ """Get a logger instance.
54
+
55
+ Parameters:
56
+ name: The logger name.
57
+
58
+ Returns:
59
+ The logger instance.
60
+ """
61
+ if name not in cls._instances:
62
+ cls._instances[name] = cls(name)
63
+ return cls._instances[name]
64
+
52
65
  @classmethod
53
66
  def _patch_loggers(cls, get_logger_func: Callable) -> None:
54
- # patch current instances
67
+ # Patch current instances.
55
68
  for name, instance in cls._instances.items():
56
69
  instance._logger = get_logger_func(name)
57
- # future instances will be patched as well
70
+ # Future instances will be patched as well.
58
71
  cls._default_logger = get_logger_func
59
72
 
60
73
 
@@ -67,7 +80,7 @@ def get_logger(name: str) -> _Logger:
67
80
  Returns:
68
81
  The logger.
69
82
  """
70
- return _Logger(name)
83
+ return _Logger.get(name)
71
84
 
72
85
 
73
86
  def patch_loggers(get_logger_func: Callable[[str], Any]) -> None:
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, MutableMapping
9
9
 
10
10
  from mkdocs.config import config_options
11
11
  from mkdocs.config.base import Config
12
+ from mkdocs.exceptions import PluginError
12
13
  from mkdocs.plugins import BasePlugin
13
14
  from mkdocs.utils import write_file
14
15
 
@@ -76,6 +77,15 @@ class MarkdownExecPlugin(BasePlugin[MarkdownExecPluginConfig]):
76
77
  Returns:
77
78
  The modified config.
78
79
  """
80
+ if "pymdownx.superfences" not in config["markdown_extensions"]:
81
+ message = "The 'markdown-exec' plugin requires the 'pymdownx.superfences' Markdown extension to work."
82
+ raise PluginError(message)
83
+ if self.config.ansi in ("required", True) and not ansi_ok:
84
+ raise PluginError(
85
+ "The configuration for the 'markdown-exec' plugin requires "
86
+ "that it is installed with the 'ansi' extra. "
87
+ "Install it with 'pip install markdown-exec[ansi]'.",
88
+ )
79
89
  self.mkdocs_config_dir = os.getenv("MKDOCS_CONFIG_DIR")
80
90
  os.environ["MKDOCS_CONFIG_DIR"] = os.path.dirname(config["config_file_path"])
81
91
  self.languages = self.config.languages
@@ -92,9 +92,9 @@ class InsertHeadings(Treeprocessor):
92
92
  if match:
93
93
  counter = int(match.group(1))
94
94
  markup: Markup = self.md.htmlStash.rawHtmlBlocks[counter] # type: ignore[assignment]
95
- if markup in self.headings:
95
+ if headings := self.headings.get(markup):
96
96
  div = Element("div", {"class": "markdown-exec"})
97
- div.extend(self.headings[markup])
97
+ div.extend(headings)
98
98
  el.append(div)
99
99
 
100
100
 
@@ -104,15 +104,20 @@ class RemoveHeadings(Treeprocessor):
104
104
  name = "markdown_exec_remove_headings"
105
105
 
106
106
  def run(self, root: Element) -> None: # noqa: D102
107
+ self._remove_duplicated_headings(root)
108
+
109
+ def _remove_duplicated_headings(self, parent: Element) -> None:
107
110
  carry_text = ""
108
- for el in reversed(root): # Reversed mainly for the ability to mutate during iteration.
109
- for subel in reversed(el):
110
- if subel.tag == "div" and subel.get("class") == "markdown-exec":
111
- # Delete the duplicated headings along with their container, but keep the text (i.e. the actual HTML).
112
- carry_text = (subel.text or "") + carry_text
113
- el.remove(subel)
114
- elif carry_text:
115
- subel.tail = (subel.tail or "") + carry_text
111
+ for el in reversed(parent): # Reversed mainly for the ability to mutate during iteration.
112
+ if el.tag == "div" and el.get("class") == "markdown-exec":
113
+ # Delete the duplicated headings along with their container, but keep the text (i.e. the actual HTML).
114
+ carry_text = (el.text or "") + carry_text
115
+ parent.remove(el)
116
+ else:
117
+ if carry_text:
118
+ el.tail = (el.tail or "") + carry_text
116
119
  carry_text = ""
117
- if carry_text:
118
- el.text = (el.text or "") + carry_text
120
+ self._remove_duplicated_headings(el)
121
+
122
+ if carry_text:
123
+ parent.text = (parent.text or "") + carry_text
@@ -40,6 +40,7 @@ html[data-theme="dark"] {
40
40
  .pyodide-output {
41
41
  width: 100%;
42
42
  margin-bottom: -15px;
43
+ min-height: 46px;
43
44
  max-height: 400px
44
45
  }
45
46
 
@@ -1,7 +0,0 @@
1
- """Tests suite for `markdown_exec`."""
2
-
3
- from pathlib import Path
4
-
5
- TESTS_DIR = Path(__file__).parent
6
- TMP_DIR = TESTS_DIR / "tmp"
7
- FIXTURES_DIR = TESTS_DIR / "fixtures"
@@ -1,28 +0,0 @@
1
- """Configuration for the pytest test suite."""
2
-
3
- import pytest
4
- from markdown import Markdown
5
-
6
- from markdown_exec import formatter, formatters, validator
7
-
8
-
9
- @pytest.fixture()
10
- def md() -> Markdown:
11
- """Return a Markdown instance.
12
-
13
- Returns:
14
- Markdown instance.
15
- """
16
- fences = [
17
- {
18
- "name": language,
19
- "class": language,
20
- "validator": validator,
21
- "format": formatter,
22
- }
23
- for language in formatters
24
- ]
25
- return Markdown(
26
- extensions=["pymdownx.superfences"],
27
- extension_configs={"pymdownx.superfences": {"custom_fences": fences}},
28
- )
@@ -1,60 +0,0 @@
1
- """Tests for the base formatter."""
2
-
3
- import pytest
4
- from markdown import Markdown
5
-
6
- from markdown_exec.formatters.base import base_format
7
-
8
-
9
- def test_no_p_around_html(md: Markdown) -> None:
10
- """Assert HTML isn't wrapped in a `p` tag.
11
-
12
- Parameters:
13
- md: A Markdown instance (fixture).
14
- """
15
- code = "<pre><code>hello</code></pre>"
16
- html = base_format(
17
- language="whatever",
18
- run=lambda code, **_: code,
19
- code=code,
20
- md=md,
21
- html=True,
22
- )
23
- assert html == code
24
-
25
-
26
- @pytest.mark.parametrize("html", [True, False])
27
- def test_render_source(md: Markdown, html: bool) -> None:
28
- """Assert source is rendered.
29
-
30
- Parameters:
31
- md: A Markdown instance (fixture).
32
- html: Whether output is HTML or not.
33
- """
34
- markup = base_format(
35
- language="python",
36
- run=lambda code, **_: code,
37
- code="hello",
38
- md=md,
39
- html=html,
40
- source="tabbed-left",
41
- )
42
- assert "Source" in markup
43
-
44
-
45
- def test_render_console_plus_ansi_result(md: Markdown) -> None:
46
- """Assert we can render source as console style with `ansi` highlight.
47
-
48
- Parameters:
49
- md: A Markdown instance (fixture).
50
- """
51
- markup = base_format(
52
- language="bash",
53
- run=lambda code, **_: code,
54
- code="echo -e '\033[31mhello'",
55
- md=md,
56
- html=False,
57
- source="console",
58
- result="ansi",
59
- )
60
- assert "<code>ansi" in markup
@@ -1,76 +0,0 @@
1
- """Tests for the Markdown converter."""
2
-
3
- from __future__ import annotations
4
-
5
- import re
6
- from textwrap import dedent
7
- from typing import TYPE_CHECKING
8
-
9
- import pytest
10
- from markdown.extensions.toc import TocExtension
11
-
12
- from markdown_exec.rendering import MarkdownConfig, markdown_config
13
-
14
- if TYPE_CHECKING:
15
- from markdown import Markdown
16
-
17
-
18
- def test_rendering_nested_blocks(md: Markdown) -> None:
19
- """Assert nested blocks are properly handled.
20
-
21
- Parameters:
22
- md: A Markdown instance (fixture).
23
- """
24
- html = md.convert(
25
- dedent(
26
- """
27
- ````md exec="1"
28
- ```python exec="1"
29
- print("**Bold!**")
30
- ```
31
- ````
32
- """,
33
- ),
34
- )
35
- assert html == "<p><strong>Bold!</strong></p>"
36
-
37
-
38
- def test_instantiating_config_singleton() -> None:
39
- """Assert that the Markdown config instances act as a singleton."""
40
- assert MarkdownConfig() is markdown_config
41
- markdown_config.save([], {})
42
- markdown_config.reset()
43
-
44
-
45
- @pytest.mark.parametrize(
46
- ("id", "id_prefix", "expected"),
47
- [
48
- ("", None, 'id="exec-\\d+--heading"'),
49
- ("", "", 'id="heading"'),
50
- ("", "some-prefix-", 'id="some-prefix-heading"'),
51
- ("some-id", None, 'id="some-id-heading"'),
52
- ("some-id", "", 'id="heading"'),
53
- ("some-id", "some-prefix-", 'id="some-prefix-heading"'),
54
- ],
55
- )
56
- def test_prefixing_headings(md: Markdown, id: str, id_prefix: str | None, expected: str) -> None: # noqa: A002
57
- """Assert that we prefix headings as specified.
58
-
59
- Parameters:
60
- md: A Markdown instance (fixture).
61
- id: The code block id.
62
- id_prefix: The code block id prefix.
63
- expected: The id we expect to find in the HTML.
64
- """
65
- TocExtension().extendMarkdown(md)
66
- prefix = f'idprefix="{id_prefix}"' if id_prefix is not None else ""
67
- html = md.convert(
68
- dedent(
69
- f"""
70
- ```python exec="1" id="{id}" {prefix}
71
- print("# HEADING")
72
- ```
73
- """,
74
- ),
75
- )
76
- assert re.search(expected, html)
@@ -1,205 +0,0 @@
1
- """Tests for the Python formatters."""
2
-
3
- from __future__ import annotations
4
-
5
- from textwrap import dedent
6
- from typing import TYPE_CHECKING
7
-
8
- if TYPE_CHECKING:
9
- import pytest
10
- from markdown import Markdown
11
-
12
-
13
- def test_output_markdown(md: Markdown) -> None:
14
- """Assert Markdown is converted to HTML.
15
-
16
- Parameters:
17
- md: A Markdown instance (fixture).
18
- """
19
- html = md.convert(
20
- dedent(
21
- """
22
- ```python exec="yes"
23
- print("**Bold!**")
24
- ```
25
- """,
26
- ),
27
- )
28
- assert html == "<p><strong>Bold!</strong></p>"
29
-
30
-
31
- def test_output_html(md: Markdown) -> None:
32
- """Assert HTML is injected as is.
33
-
34
- Parameters:
35
- md: A Markdown instance (fixture).
36
- """
37
- html = md.convert(
38
- dedent(
39
- """
40
- ```python exec="yes" html="yes"
41
- print("**Bold!**")
42
- ```
43
- """,
44
- ),
45
- )
46
- assert html == "<p>**Bold!**\n</p>"
47
-
48
-
49
- def test_error_raised(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
50
- """Assert errors properly log a warning and return a formatted traceback.
51
-
52
- Parameters:
53
- md: A Markdown instance (fixture).
54
- caplog: Pytest fixture to capture logs.
55
- """
56
- html = md.convert(
57
- dedent(
58
- """
59
- ```python exec="yes"
60
- raise ValueError("oh no!")
61
- ```
62
- """,
63
- ),
64
- )
65
- assert "Traceback" in html
66
- assert "ValueError" in html
67
- assert "oh no!" in html
68
- assert "Execution of python code block exited with errors" in caplog.text
69
-
70
-
71
- def test_can_print_non_string_objects(md: Markdown) -> None:
72
- """Assert we can print non-string objects.
73
-
74
- Parameters:
75
- md: A Markdown instance (fixture).
76
- """
77
- html = md.convert(
78
- dedent(
79
- """
80
- ```python exec="yes"
81
- class NonString:
82
- def __str__(self):
83
- return "string"
84
-
85
- nonstring = NonString()
86
- print(nonstring, nonstring)
87
- ```
88
- """,
89
- ),
90
- )
91
- assert "Traceback" not in html
92
-
93
-
94
- def test_sessions(md: Markdown) -> None:
95
- """Assert sessions can be reused.
96
-
97
- Parameters:
98
- md: A Markdown instance (fixture).
99
- """
100
- html = md.convert(
101
- dedent(
102
- """
103
- ```python exec="1" session="a"
104
- a = 1
105
- ```
106
-
107
- ```pycon exec="1" session="b"
108
- >>> b = 2
109
- ```
110
-
111
- ```pycon exec="1" session="a"
112
- >>> print(f"a = {a}")
113
- >>> try:
114
- ... print(b)
115
- ... except NameError:
116
- ... print("ok")
117
- ... else:
118
- ... print("ko")
119
- ```
120
-
121
- ```python exec="1" session="b"
122
- print(f"b = {b}")
123
- try:
124
- print(a)
125
- except NameError:
126
- print("ok")
127
- else:
128
- print("ko")
129
- ```
130
- """,
131
- ),
132
- )
133
- assert "a = 1" in html
134
- assert "b = 2" in html
135
- assert "ok" in html
136
- assert "ko" not in html
137
-
138
-
139
- def test_reporting_errors_in_sessions(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
140
- """Assert errors and source lines are correctly reported across sessions.
141
-
142
- Parameters:
143
- md: A Markdown instance (fixture).
144
- caplog: Pytest fixture to capture logs.
145
- """
146
- html = md.convert(
147
- dedent(
148
- """
149
- ```python exec="1" session="a"
150
- def fraise():
151
- raise RuntimeError("strawberry")
152
- ```
153
-
154
- ```python exec="1" session="a"
155
- print("hello")
156
- fraise()
157
- ```
158
- """,
159
- ),
160
- )
161
- assert "Traceback" in html
162
- assert "strawberry" in html
163
- assert "fraise()" in caplog.text
164
- assert 'raise RuntimeError("strawberry")' in caplog.text
165
-
166
-
167
- def test_removing_output_from_pycon_code(md: Markdown) -> None:
168
- """Assert output lines are removed from pycon snippets.
169
-
170
- Parameters:
171
- md: A Markdown instance (fixture).
172
- """
173
- html = md.convert(
174
- dedent(
175
- """
176
- ```pycon exec="1" source="console"
177
- >>> print("ok")
178
- ko
179
- ```
180
- """,
181
- ),
182
- )
183
- assert "ok" in html
184
- assert "ko" not in html
185
-
186
-
187
- def test_functions_have_a_module_attribute(md: Markdown) -> None:
188
- """Assert functions have a `__module__` attribute.
189
-
190
- Parameters:
191
- md: A Markdown instance (fixture).
192
- """
193
- html = md.convert(
194
- dedent(
195
- """
196
- ```python exec="1"
197
- def func():
198
- pass
199
-
200
- print(f"`{func.__module__}`")
201
- ```
202
- """,
203
- ),
204
- )
205
- assert "_code_block_n" in html
@@ -1,87 +0,0 @@
1
- """Tests for the shell formatters."""
2
-
3
- from __future__ import annotations
4
-
5
- from textwrap import dedent
6
- from typing import TYPE_CHECKING
7
-
8
- if TYPE_CHECKING:
9
- import pytest
10
- from markdown import Markdown
11
-
12
-
13
- def test_output_markdown(md: Markdown) -> None:
14
- """Assert Markdown is converted to HTML.
15
-
16
- Parameters:
17
- md: A Markdown instance (fixture).
18
- """
19
- html = md.convert(
20
- dedent(
21
- """
22
- ```sh exec="yes"
23
- echo "**Bold!**"
24
- ```
25
- """,
26
- ),
27
- )
28
- assert html == "<p><strong>Bold!</strong></p>"
29
-
30
-
31
- def test_output_html(md: Markdown) -> None:
32
- """Assert HTML is injected as is.
33
-
34
- Parameters:
35
- md: A Markdown instance (fixture).
36
- """
37
- html = md.convert(
38
- dedent(
39
- """
40
- ```sh exec="yes" html="yes"
41
- echo "**Bold!**"
42
- ```
43
- """,
44
- ),
45
- )
46
- assert html == "<p>**Bold!**\n</p>"
47
-
48
-
49
- def test_error_raised(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
50
- """Assert errors properly log a warning and return a formatted traceback.
51
-
52
- Parameters:
53
- md: A Markdown instance (fixture).
54
- caplog: Pytest fixture to capture logs.
55
- """
56
- html = md.convert(
57
- dedent(
58
- """
59
- ```sh exec="yes"
60
- echo("wrong syntax")
61
- ```
62
- """,
63
- ),
64
- )
65
- assert "error" in html
66
- assert "Execution of sh code block exited with unexpected code 2" in caplog.text
67
-
68
-
69
- def test_return_code(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
70
- """Assert return code is used correctly.
71
-
72
- Parameters:
73
- md: A Markdown instance (fixture).
74
- caplog: Pytest fixture to capture logs.
75
- """
76
- html = md.convert(
77
- dedent(
78
- """
79
- ```sh exec="yes" returncode="1"
80
- echo Not in the mood
81
- exit 1
82
- ```
83
- """,
84
- ),
85
- )
86
- assert "Not in the mood" in html
87
- assert "exited with" not in caplog.text
@@ -1,89 +0,0 @@
1
- """Tests for the logic updating the table of contents."""
2
-
3
- from __future__ import annotations
4
-
5
- from textwrap import dedent
6
- from typing import TYPE_CHECKING
7
-
8
- from markdown.extensions.toc import TocExtension
9
-
10
- if TYPE_CHECKING:
11
- from markdown import Markdown
12
-
13
-
14
- def test_updating_toc(md: Markdown) -> None:
15
- """Assert ToC is updated with generated headings.
16
-
17
- Parameters:
18
- md: A Markdown instance (fixture).
19
- """
20
- TocExtension().extendMarkdown(md)
21
- html = md.convert(
22
- dedent(
23
- """
24
- ```python exec="yes"
25
- print("# big heading")
26
- ```
27
- """,
28
- ),
29
- )
30
- assert "<h1" in html
31
- assert "big-heading" in md.toc # type: ignore[attr-defined]
32
-
33
-
34
- def test_not_updating_toc(md: Markdown) -> None:
35
- """Assert ToC is not updated with generated headings.
36
-
37
- Parameters:
38
- md: A Markdown instance (fixture).
39
- """
40
- TocExtension().extendMarkdown(md)
41
- html = md.convert(
42
- dedent(
43
- """
44
- ```python exec="yes" updatetoc="no"
45
- print("# big heading")
46
- ```
47
- """,
48
- ),
49
- )
50
- assert "<h1" in html
51
- assert "big-heading" not in md.toc # type: ignore[attr-defined]
52
-
53
-
54
- def test_both_updating_and_not_updating_toc(md: Markdown) -> None:
55
- """Assert ToC is not updated with generated headings.
56
-
57
- Parameters:
58
- md: A Markdown instance (fixture).
59
- """
60
- TocExtension().extendMarkdown(md)
61
- html = md.convert(
62
- dedent(
63
- """
64
- ```python exec="yes" updatetoc="no"
65
- print("# big heading")
66
- ```
67
-
68
- ```python exec="yes" updatetoc="yes"
69
- print("## medium heading")
70
- ```
71
-
72
- ```python exec="yes" updatetoc="no"
73
- print("### small heading")
74
- ```
75
-
76
- ```python exec="yes" updatetoc="yes"
77
- print("#### tiny heading")
78
- ```
79
- """,
80
- ),
81
- )
82
- assert "<h1" in html
83
- assert "<h2" in html
84
- assert "<h3" in html
85
- assert "<h4" in html
86
- assert "big-heading" not in md.toc # type: ignore[attr-defined]
87
- assert "medium-heading" in md.toc # type: ignore[attr-defined]
88
- assert "small-heading" not in md.toc # type: ignore[attr-defined]
89
- assert "tiny-heading" in md.toc # type: ignore[attr-defined]
@@ -1,25 +0,0 @@
1
- """Tests for the shell formatters."""
2
-
3
- from textwrap import dedent
4
-
5
- from markdown import Markdown
6
-
7
-
8
- def test_output_markdown(md: Markdown) -> None:
9
- """Assert we can highlight lines in the output.
10
-
11
- Parameters:
12
- md: A Markdown instance (fixture).
13
- """
14
- html = md.convert(
15
- dedent(
16
- """
17
- ```tree hl_lines="2"
18
- 1
19
- 2
20
- 3
21
- ```
22
- """,
23
- ),
24
- )
25
- assert '<span class="hll">' in html
@@ -1,38 +0,0 @@
1
- """Tests for the `validator` function."""
2
-
3
- import pytest
4
- from markdown.core import Markdown
5
-
6
- from markdown_exec import validator
7
-
8
-
9
- @pytest.mark.parametrize(
10
- ("exec_value", "expected"),
11
- [
12
- ("yes", True),
13
- ("YES", True),
14
- ("on", True),
15
- ("ON", True),
16
- ("whynot", True),
17
- ("true", True),
18
- ("TRUE", True),
19
- ("1", True),
20
- ("-1", True),
21
- ("0", False),
22
- ("no", False),
23
- ("NO", False),
24
- ("off", False),
25
- ("OFF", False),
26
- ("false", False),
27
- ("FALSE", False),
28
- ],
29
- )
30
- def test_validate(md: Markdown, exec_value: str, expected: bool) -> None:
31
- """Assert the validator returns True or False given inputs.
32
-
33
- Parameters:
34
- md: A Markdown instance.
35
- exec_value: The exec option value, passed from the code block.
36
- expected: Expected validation result.
37
- """
38
- assert validator("whatever", inputs={"exec": exec_value}, options={}, attrs={}, md=md) is expected
File without changes