Skip to content

Commit

Permalink
feat: Allow using print in code blocks
Browse files Browse the repository at this point in the history
Markdown vs. HTML is specified
using the `html` option.

Also, removed useless "isolation".
  • Loading branch information
pawamoy committed May 1, 2022
1 parent 395f4c4 commit 7c124fd
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 89 deletions.
53 changes: 28 additions & 25 deletions docs/gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ as base64 encoded PNG data. Finally we output an HTML image with the base64 data
Using SVG is not possible here since Diagrams embeds actual, smaller PNG files
in the result, files which are not automatically added to the final site.

```python exec="true" show_source="tabbed-right" title="Diagrams"
```python exec="true" html="true" source="tabbed-right" title="Diagrams"
from base64 import b64encode
from contextlib import suppress
from diagrams import Diagram, setdiagram
Expand All @@ -27,7 +27,7 @@ with suppress(FileNotFoundError):
Pod("pod3")] << ReplicaSet("rs") << Deployment("dp") << HPA("hpa")
png = b64encode(diagram.dot.pipe(format="png")).decode()

output_html(f'<img src="data:image/png;base64, {png}"/>')
print(f'<img src="data:image/png;base64, {png}"/>')
```

## Python modules inter-dependencies
Expand All @@ -43,7 +43,7 @@ so the code is a bit convoluted, but you could make a function of it,
put it in an importable script/module, and reuse it cleanly in your executed
code blocks.

```python exec="true" show_source="tabbed-right" isolate="yes" title="pydeps module dependencies graph"
```python exec="true" html="true" source="tabbed-right" title="pydeps module dependencies graph"
from pydeps import cli, colors, py2depgraph, dot
from pydeps.pydeps import depgraph_to_dotsrc
from pydeps.target import Target
Expand All @@ -62,15 +62,15 @@ reference = "../reference"
modules = (
"markdown_exec",
"markdown_exec.python",
"markdown_exec.markdown_helpers",
"markdown_exec.rendering",
)
for module in modules:
svg_title = module.replace(".", "_")
title_tag = f"<title>{svg_title}</title>"
href = f"{reference}/{module.replace('.', '/')}/"
svg = svg.replace(title_tag, f'<a href="{href}"><title>{module}</title>')
svg = svg.replace("</text></g>", "</text></a></g>")
output_html(svg)
print(svg)
```

## Code snippets (SVG)
Expand All @@ -82,33 +82,36 @@ from somewhere else using the
or by reading it dynamically from Python.
We also prevent Rich from actually writing to the terminal.

```python exec="true" show_source="tabbed-right" title="Rich SVG code snippet"
```python exec="true" html="true" source="tabbed-right" title="Rich SVG code snippet"
import os
from rich.console import Console
from rich.padding import Padding
from rich.syntax import Syntax

code = """from contextlib import asynccontextmanager
import httpx
code = """
from contextlib import asynccontextmanager
import httpx
class BookClient(httpx.AsyncClient):
async def get_book(self, book_id: int) -> str:
response = await self.get(f"/books/{book_id}")
return response.text
class BookClient(httpx.AsyncClient):
async def get_book(self, book_id: int) -> str:
response = await self.get(f"/books/{book_id}")
return response.text
@asynccontextmanager
async def book_client(*args, **kwargs):
async with BookClient(*args, **kwargs) as client:
yield client
@asynccontextmanager
async def book_client(*args, **kwargs):
async with BookClient(*args, **kwargs) as client:
yield client
"""

with open(os.devnull, "w") as devnull:
console = Console(record=True, width=65, file=devnull, markup=False)
renderable = Syntax(code, "python", line_numbers=True, indent_guides=True, theme="material")
renderable = Syntax(code, "python", theme="material")
renderable = Padding(renderable, (0,), expand=False)
console.print(renderable, markup=False)
svg = console.export_svg()
output_html(svg)
svg = console.export_svg(title="async context manager")
print(svg)
```

## Python module output
Expand All @@ -117,7 +120,7 @@ This example uses Python's [`runpy`][runpy] module to run another
Python module. This other module's output is captured by temporarily
patching `sys.stdout` with a text buffer.

```python exec="true" show_source="tabbed-right" isolate="yes" title="runpy and script/module output"
```python exec="true" source="tabbed-right" title="runpy and script/module output"
import argparse
import sys
import warnings
Expand All @@ -136,7 +139,7 @@ output = sys.stdout.getvalue()
sys.stdout = old_stdout
sys.argv = old_argv

output_markdown(f"```\n{output}\n```")
print(f"```\n{output}\n```")
```

## Python CLI documentation
Expand All @@ -148,10 +151,10 @@ if you know the project is using [`argparse`][argparse] to build its command lin
interface, and if it exposes its parser, then you can get the help message
directly from the parser.

```python exec="true" show_source="tabbed-right" isolate="yes" title="argparse parser help message"
```python exec="true" source="tabbed-right" title="argparse parser help message"
from duty.cli import get_parser
parser = get_parser()
output_markdown(f"```\n{parser.format_help()}\n```")
print(f"```\n{parser.format_help()}\n```")
```

### Argparse parser documentation
Expand All @@ -160,7 +163,7 @@ In this example, we inspect the `argparse` parser to build better-looking
Markdown/HTML contents. We simply use the description and iterate on options,
but more complex stuff is possible of course.

```python exec="true" show_source="tabbed-right" isolate="yes" title="CLI help using argparse parser"
```python exec="true" source="tabbed-right" title="CLI help using argparse parser"
import argparse
from duty.cli import get_parser
parser = get_parser()
Expand All @@ -180,5 +183,5 @@ for action in parser._actions:
if action.default and action.default != argparse.SUPPRESS:
line += f"(default: {action.default})"
lines.append(line)
output_markdown("\n".join(lines))
print("\n".join(lines))
```
2 changes: 1 addition & 1 deletion scripts/gen_credits.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@ def get_deps(base_deps):
return jinja_env.from_string(template_text).render(**template_data)


output_markdown(get_credits())
print(get_credits())
8 changes: 4 additions & 4 deletions src/markdown_exec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ def validator(
exec_value = _to_bool(inputs.pop("exec", "no"))
if not exec_value:
return False
isolate_value = _to_bool(inputs.pop("isolate", "no"))
show_source_value = inputs.pop("show_source", "")
html_value = _to_bool(inputs.pop("html", "no"))
source_value = inputs.pop("source", "")
options["exec"] = exec_value
options["isolate"] = isolate_value
options["show_source"] = show_source_value
options["html"] = html_value
options["source"] = source_value
options["extra"] = inputs
return True

Expand Down
97 changes: 42 additions & 55 deletions src/markdown_exec/python.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,78 @@
"""Formatter and utils for executing Python code."""

from __future__ import annotations

import traceback
from textwrap import indent
from functools import partial
from io import StringIO
from typing import Any

from markdown.core import Markdown
from markupsafe import Markup

from markdown_exec.rendering import code_block, markdown, tabbed

md_copy = None


class MarkdownOutput(Exception): # noqa: N818
"""Exception to return Markdown."""


class HTMLOutput(Exception): # noqa: N818
"""Exception to return HTML."""


def output_markdown(text: str) -> None:
"""Output Markdown.
def buffer_print(buffer: StringIO, *text: str, end: str = "\n", **kwargs: Any) -> None:
"""Print Markdown.
Parameters:
text: The Markdown to convert and inject back in the page.
Raises:
MarkdownOutput: Our way of returning without 'return' or 'yield' keywords.
buffer: A string buffer to write into.
*text: The text to write into the buffer. Multiple strings accepted.
end: The string to write at the end.
**kwargs: Other keyword arguments passed to `print` are ignored.
"""
raise MarkdownOutput(text)


def output_html(text: str) -> None:
"""Output HTML.
Parameters:
text: The HTML to inject back in the page.
Raises:
HTMLOutput: Our way of returning without 'return' or 'yield' keywords.
"""
raise HTMLOutput(text)
buffer.write(" ".join(text) + end)


def exec_python( # noqa: WPS231
source: str,
code: str,
md: Markdown,
isolate: bool = False,
show_source: str = "",
html: bool,
source: str,
**options: Any,
) -> str:
"""Execute code and return HTML.
Parameters:
source: The code to execute.
code: The code to execute.
md: The Markdown instance.
isolate: Whether to run the code in isolation.
show_source: Whether to show source as well, and where.
html: Whether to inject output as HTML directly, without rendering.
source: Whether to show source as well, and where.
**options: Additional options passed from the formatter.
Returns:
HTML contents.
"""
markdown.mimic(md)

if isolate:
exec_source = f"def _function():\n{indent(source, prefix=' ' * 4)}\n_function()\n"
else:
exec_source = source
extra = options.get("extra", {})

buffer = StringIO()
exec_globals = {"print": partial(buffer_print, buffer)}

try:
exec(exec_source) # noqa: S102
except MarkdownOutput as raised_output:
output = str(raised_output)
except HTMLOutput as raised_output:
output = f'<div markdown="0">{str(raised_output)}</div>'
except Exception:
output = code_block("python", traceback.format_exc(), **extra)
if show_source:
source_block = code_block("python", source, **extra)
if show_source == "above":
exec(code, {}, exec_globals) # noqa: S102
except Exception as error:
trace = traceback.TracebackException.from_exception(error)
for frame in trace.stack:
if frame.filename == "<string>":
frame.filename = "<executed code block>"
frame._line = code.split("\n")[frame.lineno - 1] # type: ignore[attr-defined,operator] # noqa: WPS437
output = code_block("python", "".join(trace.format()), **extra)
else:
output = buffer.getvalue()
if html:
output = f'<div markdown="0">{str(output)}</div>'

if source:
source_block = code_block("python", code, **extra)
if source == "above":
output = source_block + "\n\n" + output
elif show_source == "below":
elif source == "below":
output = output + "\n\n" + source_block
elif show_source == "tabbed-left":
output = tabbed(("Source", source_block), ("Result", output))
elif show_source == "tabbed-right":
elif source == "tabbed-left":
output = (("Source", source_block), ("Result", output))
elif source == "tabbed-right":
output = tabbed(("Result", output), ("Source", source_block))

return markdown.convert(output)
8 changes: 4 additions & 4 deletions tests/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_output_markdown(md: Markdown) -> None:
dedent(
"""
```python exec="yes"
output_markdown("**Bold!**")
print("**Bold!**")
```
"""
)
Expand All @@ -32,10 +32,10 @@ def test_output_html(md: Markdown) -> None:
html = md.convert(
dedent(
"""
```python exec="yes"
output_html("**Bold!**")
```python exec="yes" html="yes"
print("**Bold!**")
```
"""
)
)
assert html == '<div markdown="0">**Bold!**</div>'
assert html == '<div markdown="0">**Bold!**\n</div>'

0 comments on commit 7c124fd

Please sign in to comment.