diff --git a/docs/_scripts/notebook_convert.py b/docs/_scripts/notebook_convert.py index 07655bc78..842daa9c8 100644 --- a/docs/_scripts/notebook_convert.py +++ b/docs/_scripts/notebook_convert.py @@ -1,4 +1,6 @@ import argparse +import ast +import glob import os import re from pathlib import Path @@ -7,7 +9,22 @@ from typing import Literal, Optional import nbformat from nbconvert.exporters import MarkdownExporter from nbconvert.preprocessors import Preprocessor -import glob + + +def _uses_input(source: str) -> bool: + """Parse the source code to determine if it uses the input() function.""" + try: + tree = ast.parse(source) + except SyntaxError: + # If there's a syntax error, assume input() might be present to be safe. + return False + + for node in ast.walk(tree): + if isinstance(node, ast.Call): + # Check if the function called is named 'input' + if isinstance(node.func, ast.Name) and node.func.id == "input": + return True + return False class EscapePreprocessor(Preprocessor): @@ -87,9 +104,11 @@ class EscapePreprocessor(Preprocessor): elif cell.cell_type == "code": # Determine if the cell has bash or cell magic - if cell.source.startswith("%") or cell.source.startswith("!"): - # update metadata to denote that it's not a python cell - cell.metadata["language_info"] = {"name": "unknown"} + source = cell.source + is_exec = not ( + source.startswith("%") or source.startswith("!") or _uses_input(source) + ) + cell.metadata["exec"] = is_exec # Remove noqa comments cell.source = re.sub(r"#\s*noqa.*$", "", cell.source, flags=re.MULTILINE) diff --git a/docs/_scripts/notebook_convert_templates/md_executable/index.md.j2 b/docs/_scripts/notebook_convert_templates/md_executable/index.md.j2 index 736e678e0..1d081f3e7 100644 --- a/docs/_scripts/notebook_convert_templates/md_executable/index.md.j2 +++ b/docs/_scripts/notebook_convert_templates/md_executable/index.md.j2 @@ -6,7 +6,7 @@ {%- if 'magics_language' in cell.metadata -%} {{ cell.metadata.magics_language}} {%- elif 'name' in nb.metadata.get('language_info', {}) -%} - {{ nb.metadata.language_info.name }} exec="on" source="above" session="1" result="ansi" + {{ nb.metadata.language_info.name }}{% if cell.metadata.exec|default(false) %} exec="on" source="above" session="1" result="ansi"{% endif %} {%- endif %} {{ cell.source}} ``` diff --git a/docs/tests/unit_tests/test_notebook_conversion.py b/docs/tests/unit_tests/test_notebook_conversion.py new file mode 100644 index 000000000..554cb38e3 --- /dev/null +++ b/docs/tests/unit_tests/test_notebook_conversion.py @@ -0,0 +1,59 @@ +import nbformat + +from _scripts.notebook_convert import md_executable + + +EXPECTED_OUTPUT = """\ +```python exec="on" source="above" session="1" result="ansi" +print("Hello, world!") +``` +""" + + +def test_convert_normal_code_block() -> None: + notebook = nbformat.v4.new_notebook() + notebook.metadata.language_info = {"name": "python", "version": "3.11"} + notebook.cells.append(nbformat.v4.new_code_cell('print("Hello, world!")')) + markdown, _ = md_executable.from_notebook_node(notebook) + assert markdown == EXPECTED_OUTPUT + + +# We treat cell magic as a non-executable code block. +CELL_MAGIC_INPUT = """\ +%%capture +print("Hello, world!")\ +""" + +CELL_MAGIC_OUTPUT = """\ +```python +%%capture +print("Hello, world!") +``` +""" + + +def test_convert_cell_magic() -> None: + notebook = nbformat.v4.new_notebook() + notebook.metadata.language_info = {"name": "python", "version": "3.11"} + notebook.cells.append(nbformat.v4.new_code_cell(CELL_MAGIC_INPUT)) + markdown, _ = md_executable.from_notebook_node(notebook) + assert markdown == CELL_MAGIC_OUTPUT + + +STDIN_INPUT = """\ +input("Enter your name: ")\ +""" + +STDIN_OUTPUT = """\ +```python +input("Enter your name: ") +``` +""" + + +def test_convert_input_cell() -> None: + notebook = nbformat.v4.new_notebook() + notebook.metadata.language_info = {"name": "python", "version": "3.11"} + notebook.cells.append(nbformat.v4.new_code_cell(STDIN_INPUT)) + markdown, _ = md_executable.from_notebook_node(notebook) + assert markdown == STDIN_OUTPUT