diff --git a/docs/use/myst-notebooks.md b/docs/use/myst-notebooks.md index 1292771..ec60971 100644 --- a/docs/use/myst-notebooks.md +++ b/docs/use/myst-notebooks.md @@ -53,6 +53,22 @@ If **both** an `.ipynb` and a `.md` file exist in your book's folders, then the `.md` file will take precedence! ``` +### The Jupyter Book MyST CLI + +Jupyter Book has a small CLI to provide common functionality for manipulating and +creating MyST markdown files that synchronize with Jupytext. To add Jupytext syntax +to a markdown file (that will tell Jupytext it is a MyST markdown file), run the +following command: + +```bash +jupyter-book myst init mymarkdownfile.md --kernel kernelname +``` + +If you do not specify `--kernel`, then the default kernel will be used *if there is +only one available*. If there are multiple kernels available, you must specify one +manually. + + ## Structure of MyST notebooks Let's take a look at the structure that Jupytext creates, which you may also use diff --git a/jupyter_book/commands/__init__.py b/jupyter_book/commands/__init__.py index 7fa9179..55e291a 100644 --- a/jupyter_book/commands/__init__.py +++ b/jupyter_book/commands/__init__.py @@ -10,7 +10,7 @@ from ..sphinx import build_sphinx from ..toc import build_toc from ..pdf import html_to_pdf -from ..utils import _message_box, _error +from ..utils import _message_box, _error, init_myst_file @click.group() @@ -91,7 +91,7 @@ def build(path_book, path_output, config, toc, warningiserror, build): if exc: _error( "There was an error in building your book. " - "Look above for the error message.", + "Look above for the error message." ) else: # Builder-specific options @@ -221,3 +221,21 @@ def toc(path, filename_split_char, skip_text, output_folder): output_file.write_text(out_yaml) _message_box(f"Table of Contents written to {output_file}") + + +@main.group() +def myst(): + """Manipulate MyST markdown files.""" + pass + + +@myst.command() +@click.argument("path", nargs=-1, type=click.Path(exists=True, dir_okay=False)) +@click.option( + "--kernel", help="The name of the Jupyter kernel to attach to this markdown file." +) +def init(path, kernel): + """Add Jupytext metadata for your markdown file(s), with optional Kernel name. + """ + for ipath in path: + init_myst_file(ipath, kernel, verbose=True) diff --git a/jupyter_book/tests/test_utils.py b/jupyter_book/tests/test_utils.py new file mode 100644 index 0000000..b463878 --- /dev/null +++ b/jupyter_book/tests/test_utils.py @@ -0,0 +1,28 @@ +from pathlib import Path +from jupyter_book.utils import init_myst_file +import pytest + + +def test_myst_init(tmpdir): + """Test adding myst metadata to text files.""" + path = Path(tmpdir).joinpath("tmp.md").absolute() + text = "TEST" + with open(path, "w") as ff: + ff.write(text) + init_myst_file(path, kernel="python3") + + # Make sure it runs properly. Default kernel should be python3 + new_text = path.read_text() + assert "format_name: myst" in new_text + assert "TEST" == new_text.strip().split("\n")[-1] + assert "name: python3" in new_text + + # Non-existent kernel + with pytest.raises(Exception) as err: + init_myst_file(path, kernel="blah") + assert "Did not find kernel: blah" in str(err) + + # Missing file + with pytest.raises(Exception) as err: + init_myst_file(path.joinpath("MISSING"), kernel="python3") + assert "Markdown file not found:" in str(err) diff --git a/jupyter_book/utils.py b/jupyter_book/utils.py index b00cfa7..923a601 100644 --- a/jupyter_book/utils.py +++ b/jupyter_book/utils.py @@ -1,5 +1,6 @@ from pathlib import Path from textwrap import dedent +from jupyter_client.kernelspec import find_kernel_specs SUPPORTED_FILE_SUFFIXES = [".ipynb", ".md", ".markdown", ".myst", ".Rmd", ".py"] @@ -62,3 +63,52 @@ def _error(msg, kind=None): kind = ValueError box = _message_box(msg, color="red", doprint=False) raise kind(box) + + +############################################################################## +# MyST + Jupytext + + +def init_myst_file(path, kernel, verbose=True): + """Initialize a file with a Jupytext header that marks it as MyST markdown. + + Parameters + ---------- + path : string + A path to a markdown file to be initialized for Jupytext + kernel : string + A kernel name to add to the markdown file. See a list of kernel names with + `jupyter kernelspec list`. + """ + try: + from jupytext.cli import jupytext + except ImportError: + raise ImportError( + "In order to use myst markdown features, " "please install jupytext first." + ) + if not Path(path).exists(): + raise FileNotFoundError(f"Markdown file not found: {path}") + + kernels = list(find_kernel_specs().keys()) + kernels_text = "\n".join(kernels) + if kernel is None: + if len(kernels) > 1: + _error( + "There are multiple kernel options, so you must give one manually." + " with `--kernel`\nPlease specify one of the following kernels.\n\n" + f"{kernels_text}" + ) + else: + kernel = kernels[0] + + if kernel not in kernels: + raise ValueError( + f"Did not find kernel: {kernel}\nPlease specify one of the " + f"installed kernels:\n\n{kernels_text}" + ) + + args = (str(path), "-q", "--set-kernel", kernel, "--set-formats", "myst") + jupytext(args) + + if verbose: + print(f"Initialized file: {path}\nWith kernel: {kernel}") diff --git a/setup.py b/setup.py index 2acf5ba..338272b 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ "beautifulsoup4", "matplotlib", "pytest-regressions", + "jupytext", ] + doc_reqs setup( name="jupyter-book",