cli.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. """nbclient cli."""
  2. from __future__ import annotations
  3. import logging
  4. import sys
  5. import typing
  6. from pathlib import Path
  7. from textwrap import dedent
  8. import nbformat
  9. from jupyter_core.application import JupyterApp
  10. from traitlets import Bool, Integer, List, Unicode, default
  11. from traitlets.config import catch_config_error
  12. from nbclient import __version__
  13. from .client import NotebookClient
  14. # mypy: disable-error-code="no-untyped-call"
  15. nbclient_aliases: dict[str, str] = {
  16. "timeout": "NbClientApp.timeout",
  17. "startup_timeout": "NbClientApp.startup_timeout",
  18. "kernel_name": "NbClientApp.kernel_name",
  19. "output": "NbClientApp.output_base",
  20. }
  21. nbclient_flags: dict[str, typing.Any] = {
  22. "allow-errors": (
  23. {
  24. "NbClientApp": {
  25. "allow_errors": True,
  26. },
  27. },
  28. "Errors are ignored and execution is continued until the end of the notebook.",
  29. ),
  30. "inplace": (
  31. {
  32. "NbClientApp": {
  33. "inplace": True,
  34. },
  35. },
  36. "Overwrite input notebook with executed results.",
  37. ),
  38. }
  39. class NbClientApp(JupyterApp):
  40. """
  41. An application used to execute notebook files (``*.ipynb``)
  42. """
  43. version = Unicode(__version__)
  44. name = "jupyter-execute"
  45. aliases = nbclient_aliases
  46. flags = nbclient_flags
  47. description = "An application used to execute notebook files (*.ipynb)"
  48. notebooks = List(Unicode(), help="Path of notebooks to convert").tag(config=True)
  49. timeout = Integer(
  50. None,
  51. allow_none=True,
  52. help=dedent(
  53. """
  54. The time to wait (in seconds) for output from executions.
  55. If a cell execution takes longer, a TimeoutError is raised.
  56. ``-1`` will disable the timeout.
  57. """
  58. ),
  59. ).tag(config=True)
  60. startup_timeout = Integer(
  61. 60,
  62. help=dedent(
  63. """
  64. The time to wait (in seconds) for the kernel to start.
  65. If kernel startup takes longer, a RuntimeError is
  66. raised.
  67. """
  68. ),
  69. ).tag(config=True)
  70. allow_errors = Bool(
  71. False,
  72. help=dedent(
  73. """
  74. When a cell raises an error the default behavior is that
  75. execution is stopped and a :py:class:`nbclient.exceptions.CellExecutionError`
  76. is raised.
  77. If this flag is provided, errors are ignored and execution
  78. is continued until the end of the notebook.
  79. """
  80. ),
  81. ).tag(config=True)
  82. skip_cells_with_tag = Unicode(
  83. "skip-execution",
  84. help=dedent(
  85. """
  86. Name of the cell tag to use to denote a cell that should be skipped.
  87. """
  88. ),
  89. ).tag(config=True)
  90. kernel_name = Unicode(
  91. "",
  92. help=dedent(
  93. """
  94. Name of kernel to use to execute the cells.
  95. If not set, use the kernel_spec embedded in the notebook.
  96. """
  97. ),
  98. ).tag(config=True)
  99. inplace = Bool(
  100. False,
  101. help=dedent(
  102. """
  103. Default is execute notebook without writing the newly executed notebook.
  104. If this flag is provided, the newly generated notebook will
  105. overwrite the input notebook.
  106. """
  107. ),
  108. ).tag(config=True)
  109. output_base = Unicode(
  110. None,
  111. allow_none=True,
  112. help=dedent(
  113. """
  114. Write executed notebook to this file base name.
  115. Supports pattern replacements ``'{notebook_name}'``,
  116. the name of the input notebook file without extension.
  117. Note that output is always relative to the parent directory of the
  118. input notebook.
  119. """
  120. ),
  121. ).tag(config=True)
  122. @default("log_level")
  123. def _log_level_default(self) -> int:
  124. return logging.INFO
  125. @catch_config_error
  126. def initialize(self, argv: list[str] | None = None) -> None:
  127. """Initialize the app."""
  128. super().initialize(argv)
  129. # Get notebooks to run
  130. self.notebooks = self.get_notebooks()
  131. # If there are none, throw an error
  132. if not self.notebooks:
  133. sys.exit(-1)
  134. # If output, must have single notebook
  135. if len(self.notebooks) > 1 and self.output_base is not None:
  136. if "{notebook_name}" not in self.output_base:
  137. msg = (
  138. "If passing multiple notebooks with `--output=output` option, "
  139. "output string must contain {notebook_name}"
  140. )
  141. raise ValueError(msg)
  142. # Loop and run them one by one
  143. for path in self.notebooks:
  144. self.run_notebook(path)
  145. def get_notebooks(self) -> list[str]:
  146. """Get the notebooks for the app."""
  147. # If notebooks were provided from the command line, use those
  148. if self.extra_args:
  149. notebooks = self.extra_args
  150. # If not, look to the class attribute
  151. else:
  152. notebooks = self.notebooks
  153. # Return what we got.
  154. return notebooks
  155. def run_notebook(self, notebook_path: str) -> None:
  156. """Run a notebook by path."""
  157. # Log it
  158. self.log.info(f"Executing {notebook_path}")
  159. input_path = Path(notebook_path).with_suffix(".ipynb")
  160. # Get its parent directory so we can add it to the $PATH
  161. path = input_path.parent.absolute()
  162. # Optional output of executed notebook
  163. if self.inplace:
  164. output_path = input_path
  165. elif self.output_base:
  166. output_path = input_path.parent.joinpath(
  167. self.output_base.format(notebook_name=input_path.with_suffix("").name)
  168. ).with_suffix(".ipynb")
  169. else:
  170. output_path = None
  171. if output_path and not output_path.parent.is_dir():
  172. msg = f"Cannot write to directory={output_path.parent} that does not exist"
  173. raise ValueError(msg)
  174. # Open up the notebook we're going to run
  175. with input_path.open() as f:
  176. nb = nbformat.read(f, as_version=4)
  177. # Configure nbclient to run the notebook
  178. client = NotebookClient(
  179. nb,
  180. timeout=self.timeout,
  181. startup_timeout=self.startup_timeout,
  182. skip_cells_with_tag=self.skip_cells_with_tag,
  183. allow_errors=self.allow_errors,
  184. kernel_name=self.kernel_name,
  185. resources={"metadata": {"path": path}},
  186. )
  187. # Run it
  188. client.execute()
  189. # Save it
  190. if output_path:
  191. self.log.info(f"Save executed results to {output_path}")
  192. nbformat.write(nb, output_path)
  193. main = NbClientApp.launch_instance