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
80 changes: 80 additions & 0 deletions .github/scripts/check_tutorials_complete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Fail if any HowTo tutorial script looks truncated.

Several tutorial scripts were once cut off mid-generation (a long file was
re-emitted and the output was severed), losing all content below the cut and
leaving a script that ends part-way through — typically on a docstring that
promises a plot or code block which never appears. This check guards against
that regression recurring.

A tutorial is considered *complete* when it contains a recognised terminal
section marker (``__Wrap Up__`` or ``__Summary__``). A truncated script never
reaches its wrap-up, so the absence of the marker is a reliable signal that the
script lost content. Deliberate "not written yet" stub tutorials still carry a
``__Wrap Up__`` section, so they pass.

A second, cheaper guard flags any script whose final docstring block ends on a
colon (``:``) — the classic "a plot/code block follows" promise left dangling
by a mid-docstring cutoff.

Run from the repo root::

python scripts/check_tutorials_complete.py

Exit status is non-zero if any tutorial fails, listing each offender.
"""
from __future__ import annotations

import re
import sys
from pathlib import Path

TERMINAL_MARKERS = ("__wrap up__", "__summary__")


def final_docstring(text: str) -> str | None:
"""Return the last triple-quoted block if it sits at the end of the file."""
blocks = list(re.finditer(r'"""(.*?)"""', text, re.DOTALL))
if not blocks:
return None
last = blocks[-1]
if text[last.end():].strip() == "":
return last.group(1)
return None


def check(path: Path) -> str | None:
"""Return a failure reason for a truncated-looking tutorial, else None."""
text = path.read_text(encoding="utf-8")
if not any(marker in text.lower() for marker in TERMINAL_MARKERS):
return "no __Wrap Up__/__Summary__ terminal section (looks truncated)"
trailing = final_docstring(text)
if trailing is not None and trailing.strip().endswith(":"):
return "final docstring ends on ':' (dangling promise of a following block)"
return None


def main(root: str) -> int:
scripts_dir = Path(root) / "scripts"
files = sorted(scripts_dir.rglob("tutorial_*.py"))

failures = [(f, reason) for f in files if (reason := check(f)) is not None]

print(f"Checked {len(files)} tutorial scripts under {scripts_dir}.")
if failures:
print(f"\n{len(failures)} tutorial(s) look incomplete / truncated:\n")
for f, reason in failures:
print(f" [FAIL] {f.relative_to(root)} — {reason}")
print(
"\nEach tutorial must end with a `__Wrap Up__` (or `__Summary__`) "
"section. If a script is genuinely truncated, restore its lost "
"content; if it is complete, add the terminal section."
)
return 1

print("All tutorial scripts have a terminal section — none look truncated.")
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1] if len(sys.argv) > 1 else "."))
20 changes: 20 additions & 0 deletions .github/workflows/tutorials_complete.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Tutorials Complete

# Guards against tutorial scripts being truncated / cut off mid-generation (a
# long file re-emitted with the output severed, losing every section below the
# cut). Each tutorial_*.py must end with a `__Wrap Up__` (or `__Summary__`)
# terminal section; the check is pure-stdlib and needs no library install.
# See scripts/check_tutorials_complete.py.

on: [push, pull_request]

jobs:
tutorials-complete:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Check tutorials are not truncated
run: python .github/scripts/check_tutorials_complete.py .
4 changes: 2 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ AUTO-GENERATED by PyAutoBuild — do not edit by hand; regenerate with generate.
- [Tutorial 0: Visualization](scripts/chapter_1_introduction/tutorial_0_visualization.py): In this tutorial, we quickly cover visualization in **PyAutoLens** and make sure images display clearly in your Jupyter notebook and on your computer screen.
- Contents: Directories, Dataset, Subplots, Plot Customization, Overlays, Wrap Up
- [HowToLens: Introduction](scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py): A strong gravitational lens is a system where two (or more) galaxies align perfectly down our line of sight from Earth such that the foreground galaxy's mass curves space-time in on itself, such that the light of a background source galaxy is deflected and magnified. This means we can see the background source galaxy multiple times, as multiple arcs or rings, because multiple paths through the foreground galaxy's mass are taken by the source's light.
- Contents: Grids, Geometry, Light Profiles, One Dimension Projection
- Contents: Grids, Geometry, Light Profiles, One Dimension Projection, Galaxies, Units
- [Tutorial 2: Ray Tracing](scripts/chapter_1_introduction/tutorial_2_ray_tracing.py): Strong gravitational lensing occurs when the mass of a foreground galaxy (or galaxies) curves space-time around it, causing light rays from a background source to appear deflected.
- Contents: Grid, Mass Profiles
- Contents: Grid, Mass Profiles, Ray Tracing Grids, Ray Tracing Images, Galaxies, Tracer, Mappings
- [Tutorial 5: More Ray Tracing](scripts/chapter_1_introduction/tutorial_3_more_ray_tracing.py): We'll now reinforce the ideas that we learnt about ray-tracing in the previous tutorial and introduce the following new concepts:
- Contents: Initial Setup, Concise Code, Critical Curves, Caustics, Units, More Complexity, Multi Galaxy Ray Tracing, Wrap Up
- [Tutorial 4: Point Sources](scripts/chapter_1_introduction/tutorial_4_point_sources.py): This tutorial is not wrriten yet, but will explain how point source lensing works.
Expand Down
Loading
Loading