Skip to content

Read a notebook cell's source as text, not as characters - #2549

Open
Anish Mehta (anishmehta24) wants to merge 1 commit into
microsoft:mainfrom
anishmehta24:fix/ipynb-string-source-title
Open

Anish Mehta (anishmehta24) wants to merge 1 commit into
microsoft:mainfrom
anishmehta24:fix/ipynb-string-source-title

Conversation

@anishmehta24

Copy link
Copy Markdown

Motivation and Context

nbformat's multiline_string is a string or a list of lines, and both are valid. nbformat.validate accepts either, and nbformat.reads keeps whichever spelling the file used. The converter walked the raw value to find the title:

for line in source_lines:
    if line.startswith("# "):

With a list that iterates lines, which is what was intended. With a string it iterates characters — and no single character starts with "# " — so a notebook written that way comes out with no title at all:

notebook = {
    "nbformat": 4, "nbformat_minor": 5, "metadata": {},
    "cells": [{"cell_type": "markdown", "source": "# My Notebook\nintro text\n", "metadata": {}}],
}
IpynbConverter()._convert(notebook).title    # None

The same notebook written with "source": ["# My Notebook\n", "intro text\n"] gives "My Notebook".

The markdown body is unaffected either way, which is why this is easy to miss: "".join(...) over a string reassembles the string, so only the title is lost.

nbformat.writes normalises to lists, so notebooks saved by Jupyter itself aren't affected. Notebooks assembled as JSON and written directly — generated notebooks, tooling that builds cells programmatically and calls json.dump rather than nbformat.writes — are.

Description

Take the cell's text once, then split it for the heading scan:

source = cell.get("source", [])
source_text = source if isinstance(source, str) else "".join(source)

The body uses source_text directly, so its output is byte-identical to before for list sources.

The title scan now runs over source_text.splitlines(). Besides fixing the string case, that also picks up a heading in a list entry holding several lines (["intro\n# My Notebook\n"]), which the per-entry check couldn't see — same root cause, so I fixed it in the same place rather than leaving a second shape broken.

No behaviour changes for notebooks that already worked; the existing #hashtag title regression test still passes.

Tests

Three tests in packages/markitdown/tests/test_module_misc.py, next to the existing ipynb ones:

  • test_ipynb_string_source_is_read_as_text_not_characters — a string source now yields the title
  • test_ipynb_string_and_list_sources_agree — the two spellings of the same notebook produce identical title and markdown
  • test_ipynb_heading_below_the_first_line_of_a_source_entry — a heading after the first line of a list entry

All three fail on main. test_module_misc.py is 84 passed, 3 skipped, 1 failed — the failure is test_speech_transcription, which fails identically on a clean checkout here (it needs audio dependencies I don't have installed).

nbformat's `multiline_string` is a string *or* a list of lines. Both validate,
and `nbformat.reads` keeps whichever the file used. The converter walked the
raw value looking for a heading:

    for line in source_lines:
        if line.startswith("# "):

With a list that iterates lines, which is what was intended. With a string it
iterates characters, and no single character starts with "# ", so a notebook
written that way came out with no title at all. The markdown body was fine,
because "".join() over a string reassembles it.

Take the cell's text once, then split it for the heading scan. That also picks
up a heading in a list entry that holds several lines, which the per-entry
check could not see.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant