Skip to content

About

LiaScript macros for the glpython editor and visualizer

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

84 Commits

Folders and files

Repository files navigation

glpython for LiaScript

Run and visualize Python directly in a LiaScript course with the browser-based glpython editor.

The macros use LiaScript's native editor as the source of truth and a separately hosted iframe for execution and visualization. Python runs locally in the learner's browser through Pyodide; the current source is sent for each action through an origin-checked, versioned handshake.

Import

Add the template to the header of a LiaScript course:

import: https://raw.githubusercontent.com/glpython-edu/glpython-liascript/main/README.md

Editor and visualizer

Attach @GLPython.eval to a Python code block:

import sys

for i in range(5):
    print("Hello", "World #" + str(i))

sys.version

@GLPython.eval

LiaScript owns the visible editor and preserves its source through its course and PWA state. The ▶ Run button below the block runs the current source through glpython: it triggers LiaScript's own run action, whose </> button is hidden, so LiaScript still saves edits and keeps its code history (the version arrows stay). Tests and Visualize go through LiaScript's run action too, so they store the block in the same way - a new version only if the code changed - and then test or visualize it instead of running it: Python runs once. The glpython runtime provides output and the step-by-step visualizer without mounting a second editor. It does not reserve space for turtle graphics.

The macro makes the block's editor indent with 4 spaces: Tab inserts spaces, not LiaScript's default tab character, which would stop Python with a TabError in a block indented with spaces. It also switches off the completion as one types and the snippets LiaScript turns on since version 2.1.1, so Enter is always a new line and Tab never expands a word (if + Enter became a whole if statement, also in a docstring); Ctrl+Space still completes a name on request. A block's own data-tabsize or data-ace-options (e.g. <!-- data-ace-options="useSoftTabs=false" --> or enableLiveAutocompletion=true) still wins.

State and navigation

The fenced source belongs to LiaScript. Edits therefore survive slide navigation and leaving the course followed by browser Back in the same way as the official Pyodide macro. A hard reload returns to the Markdown source baseline, which also matches the official macro's behavior.

Each macro has an isolated runtime iframe. If LiaScript recreates that iframe, the macro reattaches a clean worker and reloads the current LiaScript source; Python globals, output, turtle state, and visualization position are intentionally not serialized. Reset also terminates the current worker and creates a clean one. The post-run REPL below the output uses this per-block worker lifecycle: Run keeps the program's namespace while the worker survives, and Reset discards it without changing LiaScript's source.

When the iframe is ready, the macro sends it the block's source - for @GLPython.visual that starts the visualization. A block inside a fragment ({{1}}) gets its editor only when the fragment appears, and LiaScript fills the editor a moment later; so while the block's code is still empty, the macro waits for it, at most 2 seconds (a block may be empty on purpose). Run, Tests and the REPL don't wait.

Turtle graphics

Use @GLPython.turtle for a graphics-focused layout:

from gturtle import *

makeTurtle()
for _ in range(4):
    forward(100)
    right(90)

@GLPython.turtle

Visualization only

Use @GLPython.visual when learners should explore a prepared program without editing it. The fenced source is hidden and the visualization starts automatically. The visualizer keeps its step buttons, scrubber, box/substitution switch, options menu (animated steps), and synchronized output.

total = 0
for value in range(1, 5):
    total += value
print(total)

@GLPython.visual

Several files

Put consecutive code blocks with a file name in their header before the macro; LiaScript shows them as one project, and glpython loads all of them, so one file can import another. Start a file's name with - to show it collapsed (and + to show it open):

def doppelt(x):
    return 2 * x
from helpers import doppelt

print(doppelt(21))

@GLPython.eval

The last file runs. ▶ Run runs it, and for several files it gets a ▾ menu that chooses another file to run; Visualize and Tests follow that choice. Only the file that runs is visualized step by step; imported files run as a black box.

Shared files that live elsewhere, e.g. a parser used in several lessons, are loaded by URL: list them in the macro's argument, separated by spaces and together with box/substitution if needed. The file name of the URL is the module name, e.g. for from parser import parse:

@GLPython.eval(https://raw.githubusercontent.com/<user>/<repo>/main/materials/00-auswerter/parser.py)
@GLPython.eval(box https://…/parser.py https://…/auswerter.py)

A working example, loading Python's own colorsys.py from GitHub:

from colorsys import rgb_to_hsv

print(rgb_to_hsv(1, 0, 0))

@GLPython.eval(https://raw.githubusercontent.com/python/cpython/main/Lib/colorsys.py)

Such a library is listed below the buttons with View file to read it. If it can't be loaded (e.g. HTTP 404, no network, or a server without CORS), the list says so and the program runs without it, so Python reports the missing module. Raw GitHub URLs work; URLs must not contain parentheses.

Tests

The Tests button runs the examples written as doctests in the docstrings of the file that would run (with several files: the chosen one), as python -m doctest does. It shows how many examples passed and each failing example in doctest's own words (Expected: / Got:), with a button to visualize that example step by step. An example that only printed the expected value while the function returned None passes, as in doctest, with a hint that it probably should return instead.

def quadrat(x):
    """
    Gibt das Quadrat von x zurück.

    >>> quadrat(3)
    9
    >>> quadrat(-2)
    4
    """
    return x * x

@GLPython.eval

Box or substitution

The visualizer shows a call to a function whose body is a single return either as a box or replaced by its return expression (substitution, the default), and learners can switch with the toggle in its toolbar. To preset the mode for one block, pass box or substitution (or, with the feature tree, tree) to @GLPython.eval or @GLPython.visual:

def quadrat(x):
    return x * x

quadrat(7)

@GLPython.visual(box)

Without an argument the block uses the mode the learner last chose, and substitution at first.

Buttons and profiles

Below each block sits one row of buttons, left to right ▶ Run · Tests · Visualize · Reset · Copy IDE link ▾ («Ausführen · Tests · Visualisieren · Zurücksetzen · IDE-Link kopieren ▾» in German courses), framed together with the output panel. Every button has a text label and the classes glp-button plus one per function (glp-button--run, glp-button--tests, glp-button--visualize, glp-button--reset, glp-button--ide); ▶ Run also has glp-button--primary, its ▾ menu glp-button--run-menu, and Copy IDE link's ▾ menu glp-button--ide-menu. The row has glp-buttons, so a course can restyle all of it with @style.

While Python is busy (a program, a REPL input or Tests that run for more than a moment, e.g. while True:), ▶ Run becomes ■ Stop («■ Stoppen») in the same place and width: it restarts Python, the code stays, and the console notes it. This needs an embed of protocol version 4; with an older embed ▶ Run stays as it was.

The block has glpython's own look (the course design: white, light grey code areas, ink accent, the system's fonts) without any @style: ▶ Run is filled, the other buttons are outlined, and the block stays light in LiaScript's dark mode, like LiaScript's own code editor. The macro adds its rules once per page as the first stylesheet, so any @style rule overrides them, and every value is a --glp-* variable (see glpython-editor's README) that a course may set on the page for the button row. The frame is the rule .glpython-embed { border: … }. To draw one frame around LiaScript's code block, its history row and the glpython block together:

.lia-code--block:has(+ p.lia-paragraph:empty + .glpython-embed) {
  border: 1px solid var(--glp-border, #D1D5DB); border-bottom: none;
  border-radius: var(--glp-radius, 4px) var(--glp-radius, 4px) 0 0;
  margin-bottom: 0; padding: 8px 8px 0;
  background: var(--glp-bg, #FFFFFF);
}
p.lia-paragraph:empty:has(+ .glpython-embed) { display: none; }
.lia-code--block + p.lia-paragraph:empty + .glpython-embed {
  border: 1px solid var(--glp-border, #D1D5DB); border-top: none;
  border-radius: 0 0 var(--glp-radius, 4px) var(--glp-radius, 4px);
  margin-bottom: 20px; padding: 0 8px 8px;
}

This depends on LiaScript's internal classes and should be checked after LiaScript updates.

Copy IDE link («IDE-Link kopieren») is the hand-in: it copies a link to a copy of the block's current code in the glpython IDE (https://glpython-edu.github.io/) and confirms with «Link kopiert ✓», or, where the browser doesn't allow copying, shows the link in a field to copy by hand. Its ▾ menu has Open in IDE («In IDE öffnen»), which opens the same link in a new tab; its tooltip says that changes there don't come back to the course. The link is an IDE share link: every file of the block and the file that runs, and the block's libraries by their URL - the IDE fetches them, so a block with a library keeps a short link (an Etappe 4 hand-in measured 1,980 characters in the IDE's old form and 1,185 in the compact one). Libraries are read-only in the IDE (a lock, no editing, renaming or deleting), as they are here. A block with features= passes them on: the IDE then shows the same parts (▶, Tests, Visualisieren, the console's input line, the → chain export, coverage, listtree, «Baum»; ide and reset mean nothing there), as the starting state of that IDE tab - its ⋮ «Optionen» can switch each part, e.g. for a teacher looking at a hand-in. A block without features= passes nothing, and the IDE shows what it always does. A link longer than 2,083 characters - OneNote, for one, cuts links there - is still copied, with a note to choose «In IDE öffnen» and download the project there for a hand-in. A link longer than GitHub Pages accepts (8192 characters of path and query, a program of roughly 5,000 to 30,000 characters) carries the project after # instead of ?. The link is the IDE's compact form (?p=: raw deflate in base64url, about 40 % shorter than before), for which the macro loads fflate 0.8.3 from jsDelivr; where that can't load, it writes the IDE's older form (?project=, with lz-string 1.5.0), which the IDE reads as well. Until one of them has loaded, the button stays disabled.

features= in the macro's argument shows only the listed parts: run, tests, visualize, reset, repl (the input line below the output), export (the visualizer's «→-Kette kopieren») and ide (Copy IDE link ▾). Without it, everything shows. Hidden parts are not rendered at all. An unknown name shows a short warning below the buttons.

coverage is the one name that isn't part of «everything»: only a block that lists it gets it. After Tests, when all examples pass, the report then lists the lines of the block's functions that no example executed, each with its code («Von keinem Beispiel ausgeführt: Zeile 11: return "ungenügend"»), or, when every line ran, a neutral note that this doesn't show whether the examples check the edge cases. Coverage finds untested code, not wrong code: return note > 4 is fully covered by 5.5 and 3.5. Only the file that runs counts, not libraries or other files, and only lines in function bodies (not def lines, docstrings or code outside functions). Nothing is marked in the block's code editor; the report gives the line and its code.

listtree isn't part of «everything» either. It draws a list whose first element is a string, such as ["+", 2, ["*", 3, 4]], also as a tree: in the visualizer's variables (a wide tree behind «Baum zeigen»), in the full value's window, and in the console, where such a value gets a «Baum» button next to «Visualisieren». The visualizer's ⋮ menu can switch the trees off («Listen als Baum zeigen»). Each tree is described by the list's own text for screen readers.

tree isn't part of «everything» either. It gives the visualizer a third mode, Baum, next to «Kasten» and «Ersetzung»: an expression is shown as its tree while Python evaluates it. One step replaces the expression by its tree, then each step of «Ersetzung» turns a subtree into its value, and one step puts the value back in the line - x = 2 + 3 * 4 takes 5 steps instead of 3. A function with a single return <expression> is replaced by that expression's tree; any other call keeps its box below the tree, which says why («notenschnitt hat lokale Variablen – der Aufruf wird als Kasten gezeigt.», also in «Ersetzung»). Labels are Python's (+, quadrat, [ ]), with no parentheses. Without tree there's no «Baum» button. To start a block in «Baum», pass tree too: @GLPython.visual(tree features=visualize,tree). large makes the code 20px (line height 1.6) and the visualizer at least 60% of the window, e.g. for the projector in presentation mode.

While stepping through a visualization, the block keeps one height, so the course page doesn't move: the visualizer is as tall as the tallest step from the start, and the console as tall as the output at the last step. The visualizer is at most 80% of the course window; a taller step (e.g. a deep recursion in «Kasten») scrolls inside it, following the current step.

A course builds its own profiles from these options as macros in its header, passing @0 on so a block can add more options (a mode or library URLs):

@Code.erkunden: @GLPython.eval(features=run,repl,visualize,ide @0)
@Code.rezept:   @GLPython.eval(features=run,repl,visualize,tests,ide @0)
@Code.rezept3:  @GLPython.eval(features=run,repl,visualize,tests,ide,coverage @0)
@Code.pruefung: @GLPython.eval(features=run @0)
@Code.demo:     @GLPython.visual(large @0)

All options can be combined in any order, separated by spaces; LiaScript's commas between macro arguments are fine. examples/profiles.md shows each of these profiles.

Language

The buttons and the embed follow the course's language: header: German for de (including regional variants such as de-CH), English for en. For a language glpython doesn't support (e.g. fr), they follow the browser's language instead, and English if that isn't supported either. LiaScript reports a course without a language: header as English, so such a course is shown in English. Python's own values and error messages are never translated.

Hosting and compatibility

The template currently targets:

https://glpython-edu.github.io/glpython-editor/

The embed protocol is versioned independently of the course template. Synchronous Python input() depends on cross-origin isolation in the complete top-level LiaScript embedding context. The iframe reports that capability during its handshake. LiaScript's player isn't isolated, so there input() doesn't work: the program stops with RuntimeError: input() is not available here and a hint below it. If a snippet uses input() where the capability is unavailable, the macro also says that such programs run in the glpython IDE, whose console asks for the input, with an «In IDE öffnen» button when the block has the ide feature (else only that input() doesn't work here). Editing, normal execution, visualization, and turtle graphics remain available.

Development

Open this file through the LiaScript LiveEditor to exercise the real macro environment:

https://liascript.github.io/LiveEditor/?/show/file/https://raw.githubusercontent.com/glpython-edu/glpython-liascript/main/README.md

node scripts/check-macro.mjs checks the syntax of the template's <script> blocks with node --check. npm run check:ready (after npm ci; it needs Chrome) runs the @onload code on a test page with stand-ins for LiaScript's editor and the embed, and checks the first program sent on "ready": with text that arrives after "ready", with an empty block, with several files. CI runs both on every pull request and on main, since a merge to main reaches every course.

npm run check:player checks the same with the real LiaScript player, the live embed and a small course (scripts/player/course.md: a visualization in a fragment, visited several times so the embed comes from the cache). It needs those sites, so it isn't part of CI; run it before a release.

For local embed development, change embedOrigin and embedUrl in the template header to the origin printed by npm run dev:embed in glpython-editor.

Lizenz / License

MIT – see LICENSE. The macro includes no third-party code; it loads the glpython embed (MIT), fflate and lz-string (both MIT, for the IDE links) when a course runs. The checks install both as devDependencies.

About

LiaScript macros for the glpython editor and visualizer

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages