Skip to content

Commit

Permalink
Add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsewell committed Mar 31, 2020
1 parent 6ddaf3e commit be45c7e
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 30 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["myst_parser", "myst_nb", "sphinx_togglebutton", "sphinx_copybutton"]
extensions = ["myst_nb", "sphinx_togglebutton", "sphinx_copybutton"]

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
Expand Down
88 changes: 76 additions & 12 deletions docs/use/markdown.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,91 @@
# Notebooks in markdown
---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: '0.8'
jupytext_version: 1.4.1+dev
kernelspec:
display_name: Python 3
language: python
name: python3
---
# Notebooks as Markdown

MyST-NB also provides functionality for notebooks within markdown. This lets you
include and control notebook-like behavior from with your markdown content.
MyST-NB also provides functionality for writing notebooks in a text-based format,
utilising the [MyST Markdown format](https://jupytext.readthedocs.io/en/latest/formats.html#myst-markdown) outlined in [jupytext](https://jupytext.readthedocs.io).
These files have a 1-to-1 mapping with the notebook, so can be opened as notebooks
in Jupyter Notebook and Jupyter Lab (with jupytext installed), and are also integrated
directly into the {ref}`Execution and Caching <execute/cache>` machinery!

The primary way to accomplish this is with the `{jupyter-execute}` directive. The content
of this directive should be runnable code in Jupyter. For example, the following
code:
They are distinguished from standard Markdown files by adding this top matter to the first line of you file (or the relevant `kernelspec` for your code):

````
```{jupyter-execute}
```md
---
jupytext:
text_representation:
format_name: myst
kernelspec:
display_name: Python 3
name: python3
---
```

```{tip}
You can also create the file from an existing notebook: `jupytext notebook.ipynb --to myst`
```

The following syntax can then be used to define a code cell:

````md
```{code-cell} ipython3
a = "This is some"
b = "Python code!"
print(f"{a} {b}")
```
````

Yields the following:
Yielding the following:

```{jupyter-execute}
```{code-cell} ipython3
a = "This is some"
b = "Python code!"
print(f"{a} {b}")
```

Currently, this uses [Jupyter-Sphinx](https://jupyter-sphinx.readthedocs.io/)
under the hood for execution and rendering.
The same metadata tags can be used as you would in a normal notebook,
for example those discussed in {ref}`use/hiding/code`:

````md
```{code-cell} ipython3
:tags: [hide_output]

for i in range(20):
print("Millhouse did not test cootie positive")
```
````

Yields the following:

```{code-cell} ipython3
:tags: [hide_output]
for i in range(20):
print("Millhouse did not test cootie positive")
```

and `raises-exception` means our code will execute without halting the kernel:

````md
```{code-cell} ipython3
:tags: [raises-exception]

raise ValueError("oopsie!")
```
````

```{code-cell} ipython3
:tags: [raises-exception]
raise ValueError("oopsie!")
```
1 change: 1 addition & 0 deletions myst_nb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def setup(app):
"""Initialize Sphinx extension."""
# Allow parsing ipynb files
app.add_source_suffix(".ipynb", "myst-nb")
app.add_source_suffix(".md", "myst-nb")
app.add_source_parser(NotebookParser)
app.setup_extension("sphinx_togglebutton")

Expand Down
7 changes: 4 additions & 3 deletions myst_nb/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from jupyter_cache import get_cache
from jupyter_cache.executors import load_executor

from .converter import path_to_notebook
from .converter import path_to_notebook, is_myst_file

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -96,8 +96,9 @@ def _stage_and_execute(env, exec_docnames, path_cache):

for nb in exec_docnames:
source_path = env.doc2path(nb)
stage_record = cache_base.stage_notebook_file(source_path)
pk_list.append(stage_record.pk)
if is_myst_file(source_path):
stage_record = cache_base.stage_notebook_file(source_path)
pk_list.append(stage_record.pk)

# can leverage parallel execution implemented in jupyter-cache here
execute_staged_nb(cache_base, pk_list or None)
Expand Down
38 changes: 25 additions & 13 deletions myst_nb/converter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from os.path import splitext
import os
from pathlib import Path

# import jupytext
Expand All @@ -12,40 +12,52 @@

def string_to_notebook(inputstring, env):
"""de-serialize a notebook or text-based representation"""
extension = splitext(env.doc2path(env.docname))[1]
extension = os.path.splitext(env.doc2path(env.docname))[1]
if extension == ".ipynb":
return nbf.reads(inputstring, nbf.NO_CONVERT)
elif is_myst_notebook(inputstring):
elif is_myst_notebook(inputstring.splitlines(keepends=True)):
# return jupytext.reads(inputstring, fmt="myst")
return myst_to_notebook(inputstring)
return None


def path_to_notebook(path):
extension = splitext(path)[1]
extension = os.path.splitext(path)[1]
if extension == ".ipynb":
return nbf.read(path, nbf.NO_CONVERT)
else:
return myst_to_notebook(Path(path).read_text())


def is_myst_notebook(inputstring):
def is_myst_file(path):
extension = os.path.splitext(path)[1]
if extension == ".ipynb":
return True
if not os.path.exists(path):
return False

with open(path) as handle:
# here we use an iterator, so that only the required lines are read
is_myst = is_myst_notebook((line for line in handle))

return is_myst


def is_myst_notebook(line_iter):
"""Is the text file a MyST based notebook representation?"""
# we need to distinguish between markdown representing notebooks
# and standard notebooks.
# Therefore, for now we require that, at a mimimum we can find some top matter
# containing the jupytext format_name
lines = inputstring.splitlines()
if (not lines) or (not lines[0].startswith("---")):
return False

yaml_lines = []
for line in lines[1:]:
if line.startswith("---") or line.startswith("..."):
for i, line in enumerate(line_iter):
if i == 0 and not line.startswith("---"):
return False
if i != 0 and (line.startswith("---") or line.startswith("...")):
break
yaml_lines.append(line)
yaml_lines.append(line.rstrip() + "\n")

front_matter = yaml.safe_load("\n".join(yaml_lines))
front_matter = yaml.safe_load("".join(yaml_lines))
if (
front_matter.get("jupytext", {})
.get("text_representation", {})
Expand Down
1 change: 1 addition & 0 deletions tests/notebooks/basic_unrun.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ kernelspec:
display_name: Python 3
language: python
name: python3
author: Chris
---

# a title
Expand Down
2 changes: 1 addition & 1 deletion tests/test_text_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ def test_basic_run(nb_run, file_regression, check_nbs):
nb_run.build()
# print(nb_run.status())
assert nb_run.warnings() == ""
# assert nb_run.app.env.metadata == {"basic_run": {"test_name": "notebook1"}}
assert nb_run.app.env.metadata == {"basic_unrun": {"author": "Chris"}}
file_regression.check(nb_run.get_nb(), check_fn=check_nbs, extension=".ipynb")
file_regression.check(nb_run.get_doctree().pformat(), extension=".xml")

0 comments on commit be45c7e

Please sign in to comment.