Skip to content

Commit

Permalink
Add Jupyter notebook renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Apr 19, 2022
1 parent 358ee81 commit 9bc9c4b
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 0 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ If your terminal supports hyperlinks, you can add `--hyperlinks` or `-y` which w
rich README.md --hyperlinks
```

## Jupyter notebook

You can request Jupyter notebook rendering by adding the `--ipynb` switch. If the file ends with `.ipynb` Jupyter notebook will be auto-detected.

```
rich notebook.ipynb
```

All options that apply to syntax highlighting can be applied to code cells, and all options that apply to Markdown can be
applied to Markdown cells.

## JSON

You can request JSON pretty formatting and highlighting with the `--json` or `-j` switches. If the file ends with `.json` then JSON will be auto-detected.
Expand Down
114 changes: 114 additions & 0 deletions src/rich_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
RULE = 6
INSPECT = 7
CSV = 8
IPYNB = 9


def on_error(message: str, error: Optional[Exception] = None, code=-1) -> NoReturn:
Expand Down Expand Up @@ -250,6 +251,7 @@ class OptionHighlighter(RegexHighlighter):
@click.option("--markdown", "-m", is_flag=True, help="Display as [u]markdown[/u].")
@click.option("--rst", is_flag=True, help="Display [u]restructured text[/u].")
@click.option("--csv", is_flag=True, help="Display [u]CSV[/u] as a table.")
@click.option("--ipynb", is_flag=True, help="Display [u]Jupyter notebook[/u].")
@click.option("--syntax", is_flag=True, help="[u]Syntax[/u] highlighting.")
@click.option("--inspect", is_flag=True, help="[u]Inspect[/u] a python object.")
@click.option(
Expand Down Expand Up @@ -396,6 +398,7 @@ def main(
markdown: bool = False,
rst: bool = False,
csv: bool = False,
ipynb: bool = False,
inspect: bool = True,
emoji: bool = False,
left: bool = False,
Expand Down Expand Up @@ -478,6 +481,8 @@ def print_usage() -> None:
resource_format = CSV
elif rst:
resource_format = RST
elif ipynb:
resource_format = IPYNB

if resource_format == AUTO and "." in resource:
import os.path
Expand All @@ -503,6 +508,8 @@ def print_usage() -> None:
resource_format = CSV
elif ext == ".rst":
resource_format = RST
elif ext == ".ipynb":
resource_format = IPYNB

if resource_format == AUTO:
resource_format = SYNTAX
Expand Down Expand Up @@ -591,6 +598,20 @@ def print_usage() -> None:

renderable = render_csv(resource, head, tail, title, caption)

elif resource_format == IPYNB:

renderable = render_ipynb(
resource,
theme,
hyperlinks,
lexer,
head,
tail,
line_numbers,
guides,
no_wrap,
)

else:
if not resource:
print_usage()
Expand Down Expand Up @@ -778,6 +799,99 @@ def render_csv(
return table


def render_ipynb(
resource: str,
theme: str,
hyperlinks: bool,
lexer: str,
head: Optional[int],
tail: Optional[int],
line_numbers: bool,
guides: bool,
no_wrap: bool,
) -> RenderableType:
"""Render resource as Jupyter notebook.
Args:
resource (str): Resource string.
theme (str): Syntax theme for code cells.
hyperlinks (bool): Whether to render hyperlinks in Markdown cells.
lexer (str): Lexer for code cell syntax highlighting (if no language set in notebook).
head (int): Display first `head` lines of each cell.
tail (int): Display last `tail` lines of each cell.
line_numbers (bool): Enable line number in code cells.
guides (bool): Enable indentation guides in code cell syntax highlighting.
no_wrap (bool): Don't word wrap syntax highlighted cells.
Returns:
RenderableType: Notebook as Markdown renderable.
"""
import json
from rich.syntax import Syntax
from rich.console import Group
from rich.panel import Panel
from .markdown import Markdown

notebook_str, _ = read_resource(resource, None)
notebook_dict = json.loads(notebook_str)
lexer = lexer or notebook_dict.get("metadata", {}).get("kernelspec", {}).get(
"language", ""
)

renderable: RenderableType
new_line = True
cells: List[RenderableType] = []
for cell in notebook_dict["cells"]:
if new_line:
cells.append("")
if "execution_count" in cell:
execution_count = cell["execution_count"] or " "
cells.append(f"[green]In [[#66ff00]{execution_count}[/#66ff00]]:[/green]")
source = "".join(cell["source"])
if cell["cell_type"] == "code":
num_lines = len(source.splitlines())
line_range = _line_range(head, tail, num_lines)
renderable = Panel(
Syntax(
source,
lexer,
theme=theme,
line_numbers=line_numbers,
indent_guides=guides,
word_wrap=not no_wrap,
line_range=line_range,
)
)
elif cell["cell_type"] == "markdown":
renderable = Markdown(source, code_theme=theme, hyperlinks=hyperlinks)
else:
renderable = Text(source)
new_line = True
cells.append(renderable)
for output in cell.get("outputs", []):
output_type = output["output_type"]
if output_type == "stream":
renderable = "".join(output["text"])
new_line = False
elif output_type == "error":
renderable = Text("\n".join(output["traceback"]).rstrip())
new_line = True
elif output_type == "execute_result":
execution_count = output.get("execution_count", " ") or " "
renderable = (
f"[red]Out[[#ee4b2b]{execution_count}[/#ee4b2b]]:[/red]\n"
+ "\n".join(output["data"].get("text/plain", ""))
)
new_line = True
else:
continue
cells.append(renderable)

renderable = Group(*cells)

return renderable


def _line_range(
head: Optional[int], tail: Optional[int], num_lines: int
) -> Optional[Tuple[int, int]]:
Expand Down
131 changes: 131 additions & 0 deletions test_data/notebook.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9705e233-47a3-48a9-9331-0c8be3bcfece",
"metadata": {},
"source": [
"# Jupyter Notebook\n",
"## Introduction"
]
},
{
"cell_type": "markdown",
"id": "e76f77cc-5fdd-40cb-bf5e-ce72707a511b",
"metadata": {},
"source": [
"This is a *Markdown* cell."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "6733a29a-d919-4285-b8fe-7c0406c52b5d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"This is a code cell.\n"
]
}
],
"source": [
"print(\"This is a code cell.\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "cb1b6061-1a48-4267-8fb3-f246e851a15f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Code cells can have outputs.\n"
]
},
{
"data": {
"text/plain": [
"3"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"print(\"Code cells can have outputs.\")\n",
"1 + 2"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "8be0183b-6a01-4334-bcd2-1ed624934dc8",
"metadata": {},
"outputs": [
{
"ename": "SyntaxError",
"evalue": "unterminated string literal (detected at line 1) (289343476.py, line 1)",
"output_type": "error",
"traceback": [
"\u001b[0;36m Input \u001b[0;32mIn [3]\u001b[0;36m\u001b[0m\n\u001b[0;31m print(\"Code cell execution can result in a syntax error)\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m unterminated string literal (detected at line 1)\n"
]
}
],
"source": [
"print(\"Code cell execution can result in a syntax error)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "1b2741fe-1522-4bdf-8fc6-fd252b8ac245",
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'a' is not defined",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"Input \u001b[0;32mIn [4]\u001b[0m, in \u001b[0;36m<cell line: 2>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# Traceback are also supported\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43ma\u001b[49m\n",
"\u001b[0;31mNameError\u001b[0m: name 'a' is not defined"
]
}
],
"source": [
"# Traceback are also supported\n",
"a"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

0 comments on commit 9bc9c4b

Please sign in to comment.