Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion autohands/add_notebook_quotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@
"""

import ast
import re

from typing import Iterable, List, Tuple

from sys import argv


# A column-0 triple-quote docstring opener, with the optional raw-string prefix.
# The prefix is load-bearing for LaTeX-carrying tutorial prose: without ``r``,
# ``\theta`` in a docstring is a TAB followed by ``heta``. A raw opener is the
# same cell boundary as a plain one — the converter replaces this line with
# ``'''`` when it emits the cell, so the prefix never reaches the notebook.
_TRIPLE_DELIM_OPENER_RE = re.compile(r"^[rR]?(?:\"\"\"|''')")


def _narrative_docstring_ranges(lines: List[str]) -> List[Tuple[int, int]]:
"""Locate the narrative docstring blocks of a script, by parsing it.

Expand Down Expand Up @@ -64,9 +73,13 @@ def _narrative_docstring_ranges(lines: List[str]) -> List[Tuple[int, int]]:

start = node.lineno - 1
end = node.end_lineno - 1
if not (lines[start].startswith('"""') or lines[start].startswith("'''")):
if not _TRIPLE_DELIM_OPENER_RE.match(lines[start]):
# A column-0 string statement written with a single-quote delimiter
# was never a cell boundary; leave it as code, as it always was.
# An ``r``/``R`` prefix does not change that: a raw narrative
# docstring is the same cell boundary as a plain one, and the
# converter replaces this opener line when it emits the cell,
# so the prefix never reaches the generated notebook.
continue
if start == end:
raise ValueError(
Expand Down
9 changes: 7 additions & 2 deletions autohands/env_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,13 @@
# raise on it (``add_notebook_quotes.strip_env_declarations`` also strips such
# lines defensively when generating notebooks/markdown).
_ENV_DECLARATION_RE = re.compile(r"^# ENV:(?P<tokens>.*)$")
# A bare triple-quote delimiter alone on its line (opens/closes a docstring).
_DOCSTRING_DELIM_RE = re.compile(r"^(?:\"\"\"|''')\s*$")
# A bare triple-quote delimiter alone on its line (opens/closes a docstring),
# with the optional raw-string prefix an opener may carry. Missing the ``r``
# form would be silent and worse than a crash: the scan walks past the opener,
# matches the block's CLOSER as an opener instead, and every docstring parity
# after it inverts — so an ``__Env__`` section lower down is read as if it were
# outside a docstring and its ``ENV:`` declaration is lost without a word.
_DOCSTRING_DELIM_RE = re.compile(r"^[rR]?(?:\"\"\"|''')\s*$")
# The ``__Env__`` docstring header — the section marker, optional trailing text
# (e.g. a ``(Developer Only)`` note).
_ENV_SECTION_HEADER_RE = re.compile(r"^__Env__\b")
Expand Down
40 changes: 40 additions & 0 deletions tests/test_add_notebook_quotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,43 @@ def test_indented_closing_delimiter_still_closes_the_block(tmp_path, monkeypatch

assert [cell["cell_type"] for cell in notebook["cells"]] == ["markdown", "code"]
assert "x = 1" in "".join(notebook["cells"][1]["source"])


def test_raw_string_docstring_is_a_cell_boundary():
r"""A raw narrative docstring converts identically to a plain one.

LaTeX-carrying tutorial prose has to be raw — in a plain docstring
``\theta`` is a TAB followed by ``heta`` — but the opener test read
``lines[start].startswith('\"\"\"')``, which an ``r\"\"\"`` line fails. The
block was then dropped as a boundary and the prose shipped as a *code* cell
containing a bare string literal.

Asserted on the converted source rather than the notebook: the converter
replaces the opener line outright when it emits the cell, so a
byte-identical conversion is the tighter statement of "the prefix does not
leak into the generated artefact".
"""
plain = '"""\n' "__Intro__\n" "prose $\\theta_E$\n" '"""\n' "\n" "x = 1\n"
raw = "r" + plain

converted = "".join(add_notebook_quotes(_lines(raw)))

assert converted == "".join(add_notebook_quotes(_lines(plain)))
assert "# %%" in converted
assert 'r"""' not in converted and "r'''" not in converted


def test_every_raw_prefix_and_delimiter_form_is_a_cell_boundary():
"""``r``/``R`` against both triple-quote delimiters, all six forms."""
baseline = None

for prefix in ("", "r", "R"):
for delim in ('"""', "'''"):
script = f"{prefix}{delim}\n__Intro__\nprose\n{delim}\n\nx = 1\n"
converted = "".join(add_notebook_quotes(_lines(script)))

assert "# %%" in converted, (prefix, delim)
if baseline is None:
baseline = converted
else:
assert converted == baseline, (prefix, delim)
72 changes: 72 additions & 0 deletions tests/test_env_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,75 @@ def test_unknown_token_raises_in_resolver(tmp_path):
p = _write_script(tmp_path, "imaging/x.py", '"""\n__Env__\n\nENV: bogus\n"""\n')
with pytest.raises(ValueError, match="unknown env declaration token"):
apply_profile({}, p, {"defaults": {}})


# --- Raw-string (`r"""`) docstring openers -------------------------------------
# The workspace tutorial scripts carry LaTeX in their narrative docstrings, so
# those docstrings have to be raw (in a plain docstring `\theta` is a TAB
# followed by `heta`). `_DOCSTRING_DELIM_RE` matched a bare delimiter only, so
# an `r"""` opener was walked past and the block's CLOSER matched as an opener
# instead — parity inverted for the rest of the file and a later `__Env__`
# section was read as if it sat outside a docstring. Silent: no raise, just a
# lost `ENV:` declaration and a silently rerouted smoke profile.


def test_read_declaration_survives_an_earlier_raw_docstring(tmp_path):
# The parity case a single-block test would miss: the RAW docstring is not
# the one carrying the declaration. Before the fix this returned None.
body = (
'r"""\n'
"Tutorial prose with LaTeX $\\theta_E$ and $\\frac{1}{2}$.\n"
'"""\n'
"\n"
"import autolens as al\n"
"code()\n"
"\n"
'"""\n'
"Wrap Up\n"
"-------\n"
"\n"
"__Env__\n"
"\n"
"ENV: full_datasets\n"
'"""\n'
)
p = _write_script(tmp_path, "imaging/x.py", body)
assert read_env_declaration(p) == ["full_datasets"]


def test_read_declaration_in_a_raw_docstring_section(tmp_path):
# The declaration inside the raw block itself.
body = (
"import autolens as al\n"
"\n"
'r"""\n'
"Closing prose with $\\theta_E$.\n"
"\n"
"__Env__\n"
"\n"
"ENV: jax full_datasets\n"
'"""\n'
)
p = _write_script(tmp_path, "imaging/x.py", body)
assert read_env_declaration(p) == ["jax", "full_datasets"]


def test_read_declaration_raw_prefix_forms_all_parse(tmp_path):
# `r`/`R` against both delimiters, each identical to the unprefixed form.
for index, prefix in enumerate(("", "r", "R")):
for delim in ('"""', "'''"):
body = (
f"{prefix}{delim}\n"
"Prose with $\\theta_E$.\n"
f"{delim}\n"
"\n"
"code()\n"
"\n"
f"{delim}\n"
"__Env__\n"
"\n"
"ENV: jax\n"
f"{delim}\n"
)
p = _write_script(tmp_path, f"imaging/x{index}{len(delim)}{delim[0]}.py", body)
assert read_env_declaration(p) == ["jax"], (prefix, delim)
46 changes: 46 additions & 0 deletions tests/test_strip_env_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,49 @@ def test_strip_leaves_a_code_string_literal_untouched():
assert "print(s)" in stripped
assert "__Later__" in stripped
assert "y = 2" in stripped


RAW_MERGED_SCRIPT = (
'r"""\n'
"Imaging Example\n"
"===============\n"
"\n"
"The Einstein radius $\\theta_E$ and the term $\\frac{1}{2}$ need a raw\n"
"docstring: unprefixed, $\\theta$ is a TAB followed by `heta`.\n"
'"""\n'
"import autolens as al\n"
"\n"
"al.do_something()\n"
"\n"
'r"""\n'
"Wrap Up\n"
"-------\n"
"\n"
"Closing prose with $\\theta_E$.\n"
"\n"
"__Env__ (Developer Only)\n"
"\n"
"Not user documentation: this section configures the test harness.\n"
"\n"
"ENV: full_datasets\n"
'"""\n'
)


def test_strip_removes_env_section_from_a_raw_docstring():
r"""A raw (`r\"\"\"`) block strips exactly as a plain one does.

The workspace tutorial scripts carry LaTeX in their narrative docstrings, so
those docstrings must be raw. `_narrative_docstring_ranges` did not see an
`r\"\"\"` opener as a block boundary, so the section inside it was invisible
to the strip and the developer-only `ENV:` line leaked into the generated
notebook and markdown.
"""
out = "".join(strip_env_declarations(_lines(RAW_MERGED_SCRIPT)))

_assert_no_env_leak(out)
# The user-facing prose and the LaTeX survive untouched.
assert "Imaging Example" in out
assert "Wrap Up" in out
assert "$\\theta_E$" in out
assert "al.do_something()" in out
Loading