You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
add_notebook_quotes decides what is a narrative docstring with a line-prefix
test, so a code string literal's closing delimiter — which usually sits at
column 0 — is mistaken for a docstring boundary. Every cell boundary after it is
inverted: the enclosing code cell becomes a hard SyntaxError, and the code that
follows is emitted as a markdown cell.
Confirmed live on origin/main (autohands/add_notebook_quotes.py:152). It is latent, not shipping — the only workspace file with the shape, autolens_workspace_test/gallery/gallery_build.py:42, sits outside scripts/
and iter_script_paths only walks scripts/, so it is never converted. Same
failure class as #211's opener bug, different trigger.
Splitting the converted reproducer the way ipynb-py-convert's py2nb does
(on the literal "\n\n# %%\n"):
Derive narrative-docstring boundaries from the parsed source instead of a line
prefix, so a string bound to a name can never be mistaken for a docstring.
Route both segmentation sites in add_notebook_quotes.py through that one
shared helper. navigator.py needs no change — it already delegates to add_notebook_quotes and reads the ''' delimiters back out, so it inherits
the fix.
Add regression tests for the string-literal shape, including the exact
reproducer and the real gallery_build.py shape.
Prove the change is behaviour-preserving by regenerating the artifact-bearing
workspaces and requiring a zero diff — no live script has the shape, so a
correct fix must change nothing.
Detailed implementation plan
Work Classification
Library — source + tests in a single organ repo. Ships via ship_library.
Affected Repositories
PyAutoHands (primary)
Branch Survey
Repository
Current Branch
Dirty?
./PyAutoHands
main
clean
worktree_list_claimed: nothing registered. worktree_check_conflict exit 0 —
no conflict. main is the only branch; no unregistered prior-session work.
New helper _narrative_docstring_ranges(lines) in autohands/add_notebook_quotes.py, returning 0-based (start, end) line
index pairs:
ast.parse("".join(lines)); walk module.body only.
Accept a node when it is ast.Expr wrapping ast.Constant of str, with col_offset == 0, and the source line at lineno - 1 begins with """ or '''. This reproduces the intended semantics of today's test (a column-0
bare triple-quoted expression statement) while excluding s = """…""",
indented function/class docstrings, and non-triple-quoted string statements.
SyntaxError → re-raise as ValueError quoting the offending line, so a
broken source script fails the build rather than producing a broken notebook.
start == end (single-line docstring) → ValueError naming the line. Zero
occurrences across the workspaces (see Reachability), so rejecting it is
cheaper and louder than emitting a one-line markdown cell.
add_notebook_quotes (:151-176) — the actual fix. Iterate enumerate(lines); replace the line.startswith('"""') test at :152 with
membership in the boundary set {start} ∪ {end} built from the helper.
All downstream cell-marker logic (:153-176) is untouched.
strip_env_declarations (:51) — hardening, not a fix. Verified not to be a live defect: the false "opener" match does fire, but every
non-__Env__ path emits the block verbatim (:81), so the misparse is
absorbed. It would only bite if an __Env__ header sat between a literal's
closer and the next column-0 delimiter — i.e. outside any docstring, which is
not a real shape. Migrated anyway so there is one segmentation source rather
than two prefix tests that can drift. close becomes the helper's end
instead of the forward scan at :55-58; the __Env__ header scan and the
merged/standalone/emptied branches (:61-83) are unchanged. The close == n unterminated branch becomes unreachable and is removed —
unterminated is now a parse error.
Docstring — update the "single shared strip layer" paragraph (:30-35)
to say the segmentation is AST-derived and that navigator.py inherits it
through add_notebook_quotes.
tests/test_add_notebook_quotes.py — add: the reproducer below, asserting [markdown, code] with the literal intact and no cell source containing # %%; the real gallery_build.py shape (CSS = """ … column-0 """ … def); a s = '''…''' single-quote variant; and a syntactically broken
source raising ValueError.
tests/test_strip_env_declarations.py — one case: an __Env__ block
followed by a code string literal, pinning that the literal survives the strip
untouched.
Reachability
Scanned all 1517 .py files across the four workspaces, their _test / _developer siblings and the three HowTo repos:
0 files fail ast.parse → the AST route is safe everywhere.
0 column-0 single-line docstrings → the one shape AST treats differently
from the current prefix test has no live occurrence.
1 column-0 triple-quote closer belonging to a code string: autolens_workspace_test/gallery/gallery_build.py:42 — outside scripts/,
never converted.
Validation
pytest PyAutoHands/tests/ green — in particular test_add_notebook_quotes.py, test_strip_env_declarations.py, test_check_navigator.py, test_generate_markdown.py (currently 26 passed on the first three).
Regenerating the six artifact-bearing workspaces produces a zero diff. This
is the real proof of the strip_env_declarations migration: a non-zero diff
means the AST predicate disagrees with the old prefix test somewhere.
navigator catalogues unchanged, for the same reason.
Trade-off — ast over tokenize.generate_tokens
Both give exact line spans. ast distinguishes "bare string expression
statement" from "string bound to a name" directly as a node type, where tokenize requires reconstructing that from surrounding NEWLINE/NL/INDENT
context. Same information, less hand-rolled state. Both require parseable source.
Key Files
autohands/add_notebook_quotes.py — the two segmentation sites and the new helper.
autohands/navigator.py — no change; _docstring_blocks inherits the fix.
A triple-quoted string literal assigned in code opens on a line that does not start at column 0 (s = """), so the opener is invisible to the test —
but its closing delimiter usually sits at column 0, and that line does
match. It flips is_in_quotes, and every cell boundary after it is inverted.
Reproduced:
"""__Intro__"""x=1s="""literal"""print(s)
converts to a final code cell whose source is
x=1s= """
literal# %%
'''
print(s)
— the literal's closing delimiter became a cell marker, and print(s) landed
inside a broken string. Same failure class as #211's opener bug, different trigger.
Reachability
One occurrence workspace-wide, found by scanning every .py in the four
workspaces, their _test / _developer siblings, and the three HowTo repos for
a column-0 triple-quote-closing line belonging to a code string:
gallery/ sits outside scripts/, and iter_script_paths only walks scripts/, so this file is never converted — the bug is latent, not
shipping. That is why it was left out of #211 rather than fixed there.
Fix
Replace the line-prefix test with real tokenization. tokenize.generate_tokens
yields STRING tokens with exact start/end line numbers, which distinguishes
a module/narrative docstring (a bare STRING expression statement) from a string
bound to a name. strip_env_declarations and navigator.py share this same
tokenizer-by-line-prefix assumption (add_notebook_quotes.py docstring: "the
single shared strip layer"), so all three should move together or the catalogue
and the notebooks will disagree.
Cheaper interim option, if tokenizing is judged too invasive: raise on a column-0
triple-quote line that closes a string the scanner never saw opened. Loud beats a
silently mangled cell, and matches the stray-# %% guard added in #214.
Validation
The reproducer above yields [markdown, code] with the literal intact inside
the code cell, and no cell source containing # %%.
The existing tests/test_add_notebook_quotes.py suite stays green.
Regenerating all six artifact-bearing workspaces produces a zero diff —
no live script has the shape, so a correct fix must change nothing.
navigator catalogues are unchanged for the same reason.
Overview
add_notebook_quotesdecides what is a narrative docstring with a line-prefixtest, so a code string literal's closing delimiter — which usually sits at
column 0 — is mistaken for a docstring boundary. Every cell boundary after it is
inverted: the enclosing code cell becomes a hard
SyntaxError, and the code thatfollows is emitted as a markdown cell.
Confirmed live on
origin/main(autohands/add_notebook_quotes.py:152). It islatent, not shipping — the only workspace file with the shape,
autolens_workspace_test/gallery/gallery_build.py:42, sits outsidescripts/and
iter_script_pathsonly walksscripts/, so it is never converted. Samefailure class as #211's opener bug, different trigger.
Splitting the converted reproducer the way
ipynb-py-convert'spy2nbdoes(on the literal
"\n\n# %%\n"):SyntaxError: unterminated triple-quoted string literalprint(s)— real code rendered as narrative prosePlan
prefix, so a string bound to a name can never be mistaken for a docstring.
add_notebook_quotes.pythrough that oneshared helper.
navigator.pyneeds no change — it already delegates toadd_notebook_quotesand reads the'''delimiters back out, so it inheritsthe fix.
unparseable source script, and a column-0 single-line docstring — rather than
silently emitting a mangled cell (the fix: split a docstring that follows code into its own notebook cell #214 stray-
# %%precedent).reproducer and the real
gallery_build.pyshape.workspaces and requiring a zero diff — no live script has the shape, so a
correct fix must change nothing.
Detailed implementation plan
Work Classification
Library — source + tests in a single organ repo. Ships via
ship_library.Affected Repositories
Branch Survey
worktree_list_claimed: nothing registered.worktree_check_conflictexit0—no conflict.
mainis the only branch; no unregistered prior-session work.Suggested branch:
feature/notebook-quotes-string-literalWorktree root:
~/Code/PyAutoLabs-wt/notebook-quotes-string-literal/Implementation Steps
New helper
_narrative_docstring_ranges(lines)inautohands/add_notebook_quotes.py, returning 0-based(start, end)lineindex pairs:
ast.parse("".join(lines)); walkmodule.bodyonly.ast.Exprwrappingast.Constantofstr, withcol_offset == 0, and the source line atlineno - 1begins with"""or'''. This reproduces the intended semantics of today's test (a column-0bare triple-quoted expression statement) while excluding
s = """…""",indented function/class docstrings, and non-triple-quoted string statements.
SyntaxError→ re-raise asValueErrorquoting the offending line, so abroken source script fails the build rather than producing a broken notebook.
start == end(single-line docstring) →ValueErrornaming the line. Zerooccurrences across the workspaces (see Reachability), so rejecting it is
cheaper and louder than emitting a one-line markdown cell.
add_notebook_quotes(:151-176) — the actual fix. Iterateenumerate(lines); replace theline.startswith('"""')test at:152withmembership in the boundary set
{start} ∪ {end}built from the helper.All downstream cell-marker logic (
:153-176) is untouched.strip_env_declarations(:51) — hardening, not a fix. Verifiednot to be a live defect: the false "opener" match does fire, but every
non-
__Env__path emits the block verbatim (:81), so the misparse isabsorbed. It would only bite if an
__Env__header sat between a literal'scloser and the next column-0 delimiter — i.e. outside any docstring, which is
not a real shape. Migrated anyway so there is one segmentation source rather
than two prefix tests that can drift.
closebecomes the helper'sendinstead of the forward scan at
:55-58; the__Env__header scan and themerged/standalone/emptied branches (
:61-83) are unchanged. Theclose == nunterminated branch becomes unreachable and is removed —unterminated is now a parse error.
Docstring — update the "single shared strip layer" paragraph (
:30-35)to say the segmentation is AST-derived and that
navigator.pyinherits itthrough
add_notebook_quotes.tests/test_add_notebook_quotes.py— add: the reproducer below, asserting[markdown, code]with the literal intact and no cell source containing# %%; the realgallery_build.pyshape (CSS = """… column-0"""…def); as = '''…'''single-quote variant; and a syntactically brokensource raising
ValueError.tests/test_strip_env_declarations.py— one case: an__Env__blockfollowed by a code string literal, pinning that the literal survives the strip
untouched.
Reachability
Scanned all 1517
.pyfiles across the four workspaces, their_test/_developersiblings and the three HowTo repos:ast.parse→ the AST route is safe everywhere.from the current prefix test has no live occurrence.
autolens_workspace_test/gallery/gallery_build.py:42— outsidescripts/,never converted.
Validation
pytest PyAutoHands/tests/green — in particulartest_add_notebook_quotes.py,test_strip_env_declarations.py,test_check_navigator.py,test_generate_markdown.py(currently26 passedon the first three).is the real proof of the
strip_env_declarationsmigration: a non-zero diffmeans the AST predicate disagrees with the old prefix test somewhere.
navigatorcatalogues unchanged, for the same reason.Trade-off —
astovertokenize.generate_tokensBoth give exact line spans.
astdistinguishes "bare string expressionstatement" from "string bound to a name" directly as a node type, where
tokenizerequires reconstructing that from surroundingNEWLINE/NL/INDENTcontext. Same information, less hand-rolled state. Both require parseable source.
Key Files
autohands/add_notebook_quotes.py— the two segmentation sites and the new helper.autohands/navigator.py— no change;_docstring_blocksinherits the fix.tests/test_add_notebook_quotes.py,tests/test_strip_env_declarations.py— regressions.Original Prompt
Click to expand starting prompt
add_notebook_quotesmistakes a code string literal's closing delimiter for a docstringType: bug
Target: hands
Repos:
Difficulty: small
Autonomy: safe
Priority: low
add_notebook_quotesdecides what is a narrative docstring with a line-prefixtest (
add_notebook_quotes.py):A triple-quoted string literal assigned in code opens on a line that does
not start at column 0 (
s = """), so the opener is invisible to the test —but its closing delimiter usually sits at column 0, and that line does
match. It flips
is_in_quotes, and every cell boundary after it is inverted.Reproduced:
converts to a final code cell whose source is
— the literal's closing delimiter became a cell marker, and
print(s)landedinside a broken string. Same failure class as
#211's opener bug, different trigger.
Reachability
One occurrence workspace-wide, found by scanning every
.pyin the fourworkspaces, their
_test/_developersiblings, and the three HowTo repos fora column-0 triple-quote-closing line belonging to a code string:
autolens_workspace_test/gallery/gallery_build.py:42gallery/sits outsidescripts/, anditer_script_pathsonly walksscripts/, so this file is never converted — the bug is latent, notshipping. That is why it was left out of #211 rather than fixed there.
Fix
Replace the line-prefix test with real tokenization.
tokenize.generate_tokensyields
STRINGtokens with exactstart/endline numbers, which distinguishesa module/narrative docstring (a bare
STRINGexpression statement) from a stringbound to a name.
strip_env_declarationsandnavigator.pyshare this sametokenizer-by-line-prefix assumption (
add_notebook_quotes.pydocstring: "thesingle shared strip layer"), so all three should move together or the catalogue
and the notebooks will disagree.
Cheaper interim option, if tokenizing is judged too invasive: raise on a column-0
triple-quote line that closes a string the scanner never saw opened. Loud beats a
silently mangled cell, and matches the stray-
# %%guard added in#214.
Validation
[markdown, code]with the literal intact insidethe code cell, and no cell source containing
# %%.tests/test_add_notebook_quotes.pysuite stays green.no live script has the shape, so a correct fix must change nothing.
navigatorcatalogues are unchanged for the same reason.Notes
(fix: notebook generation mangles a docstring that follows code; drop the Finish. hack #211 / fix: split a docstring that follows code into its own notebook cell #214).