diff --git a/packages/markitdown/src/markitdown/converters/_ipynb_converter.py b/packages/markitdown/src/markitdown/converters/_ipynb_converter.py index b15e77aa2..0933bdd9c 100644 --- a/packages/markitdown/src/markitdown/converters/_ipynb_converter.py +++ b/packages/markitdown/src/markitdown/converters/_ipynb_converter.py @@ -54,6 +54,16 @@ def convert( notebook_content = file_stream.read().decode(encoding=encoding) return self._convert(json.loads(notebook_content)) + def _get_source_lines(self, source: Any) -> list[str]: + """Normalize cell source representations (str, list, None) into a list of line strings.""" + if source is None: + return [] + if isinstance(source, str): + return source.splitlines(keepends=True) + if isinstance(source, list): + return [str(line) for line in source] + return [] + def _convert(self, notebook_content: dict) -> DocumentConverterResult: """Helper function that converts notebook JSON content to Markdown.""" try: @@ -62,7 +72,7 @@ def _convert(self, notebook_content: dict) -> DocumentConverterResult: for cell in notebook_content.get("cells", []): cell_type = cell.get("cell_type", "") - source_lines = cell.get("source", []) + source_lines = self._get_source_lines(cell.get("source")) if cell_type == "markdown": md_output.append("".join(source_lines)) @@ -83,7 +93,8 @@ def _convert(self, notebook_content: dict) -> DocumentConverterResult: md_text = "\n\n".join(md_output) # Check for title in notebook metadata - title = notebook_content.get("metadata", {}).get("title", title) + metadata = notebook_content.get("metadata") or {} + title = metadata.get("title", title) return DocumentConverterResult( markdown=md_text, diff --git a/packages/markitdown/tests/test_module_misc.py b/packages/markitdown/tests/test_module_misc.py index f966eee1e..a25f57fe8 100644 --- a/packages/markitdown/tests/test_module_misc.py +++ b/packages/markitdown/tests/test_module_misc.py @@ -587,6 +587,47 @@ def test_markitdown_llm() -> None: validate_strings(result, PPTX_TEST_STRINGS) +def test_ipynb_converter_string_and_none_source() -> None: + """Test IpynbConverter when cell source is represented as string, list, or None.""" + import json + + markitdown = MarkItDown() + nb_content = json.dumps( + { + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "markdown", + "source": "# String Title Heading\nThis cell source is a single string.", + }, + { + "cell_type": "code", + "source": "x = 42\nprint(x)", + }, + { + "cell_type": "markdown", + "source": None, + }, + { + "cell_type": "raw", + "source": ["raw line 1\n", "raw line 2"], + }, + ], + "metadata": None, + } + ).encode("utf-8") + + result = markitdown.convert_stream( + io.BytesIO(nb_content), stream_info=StreamInfo(extension=".ipynb") + ) + assert result.title == "String Title Heading" + assert "# String Title Heading" in result.markdown + assert "This cell source is a single string." in result.markdown + assert "```python\nx = 42\nprint(x)\n```" in result.markdown + assert "```\nraw line 1\nraw line 2\n```" in result.markdown + + if __name__ == "__main__": """Runs this file's tests from the command line.""" for test in [ @@ -602,6 +643,7 @@ def test_markitdown_llm() -> None: test_markitdown_exiftool, test_markitdown_llm_parameters, test_markitdown_llm, + test_ipynb_converter_string_and_none_source, ]: print(f"Running {test.__name__}...", end="") test()