pll-python 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +45 -1
  2. package/cli.cjs +319 -56
  3. package/package.json +1 -1
  4. package/worker.cjs +22 -17
package/cli.cjs CHANGED
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli/main.ts
27
- var fs8 = __toESM(require("node:fs"));
28
- var path8 = __toESM(require("node:path"));
27
+ var fs9 = __toESM(require("node:fs"));
28
+ var path9 = __toESM(require("node:path"));
29
29
 
30
30
  // src/cli/args.ts
31
31
  var import_node_util = require("node:util");
@@ -93,7 +93,7 @@ function reworded(err, options, argv) {
93
93
  return message;
94
94
  }
95
95
 
96
- // src/cli/examplar.ts
96
+ // src/cli/bundle.ts
97
97
  var fs4 = __toESM(require("node:fs/promises"));
98
98
  var path4 = __toESM(require("node:path"));
99
99
 
@@ -396,7 +396,8 @@ function validateBundleUrl(url) {
396
396
  }
397
397
  return `\`${url}\` must be an https address.`;
398
398
  }
399
- async function loadBundle(url, store, fetchImpl = globalThis.fetch) {
399
+ var EXAMPLAR_BUNDLE = { noun: "an Examplar bundle", looksRight: looksLikeBundle };
400
+ async function loadBundle(url, store, fetchImpl = globalThis.fetch, kind = EXAMPLAR_BUNDLE) {
400
401
  const cached = await store.read(url).catch(() => void 0);
401
402
  let response;
402
403
  try {
@@ -424,8 +425,8 @@ async function loadBundle(url, store, fetchImpl = globalThis.fetch) {
424
425
  error: `${url} is ${json.length} bytes, past the ${MAX_BUNDLE_BYTES}-byte limit`
425
426
  };
426
427
  }
427
- if (!looksLikeBundle(json)) {
428
- return cached ? { json: cached.json, fromCache: true, note: `${url} did not return a bundle; using the cached copy` } : { fromCache: false, error: `${url} did not return an Examplar bundle` };
428
+ if (!kind.looksRight(json)) {
429
+ return cached ? { json: cached.json, fromCache: true, note: `${url} did not return a bundle; using the cached copy` } : { fromCache: false, error: `${url} did not return ${kind.noun}` };
429
430
  }
430
431
  const etag = response.headers.get("etag") ?? void 0;
431
432
  try {
@@ -682,8 +683,8 @@ var WorkerConnection = class {
682
683
  return Promise.reject(new Error(NOT_RUNNING));
683
684
  }
684
685
  const id = this.nextId++;
685
- return new Promise((resolve4, reject) => {
686
- this.pending.set(id, { resolve: resolve4, reject, live });
686
+ return new Promise((resolve5, reject) => {
687
+ this.pending.set(id, { resolve: resolve5, reject, live });
687
688
  worker.post({ id, type, ...fields });
688
689
  });
689
690
  }
@@ -799,7 +800,7 @@ function concatBytes(a, b) {
799
800
  }
800
801
 
801
802
  // src/run/examplar.ts
802
- async function runExamplarStep(runtime, bundles, host, code, fileName) {
803
+ async function runExamplarStep(runtime, bundles, host, code, fileName, sessionKey) {
803
804
  const bundle = await fetchBundle(bundles, host, code);
804
805
  if (bundle === null) {
805
806
  return null;
@@ -827,7 +828,7 @@ async function runExamplarStep(runtime, bundles, host, code, fileName) {
827
828
  }
828
829
  let result;
829
830
  try {
830
- result = await runtime.examplarRun(code, bundle.json, fileName);
831
+ result = await runtime.examplarRun(code, bundle.json, fileName, sessionKey);
831
832
  } catch (err) {
832
833
  if (!(err instanceof StoppedError)) {
833
834
  host.examplarCard(failedCard(bundle, errorText(err)));
@@ -975,11 +976,14 @@ function cardBody(card) {
975
976
  body.push({
976
977
  kind: "line",
977
978
  tone: "warn",
978
- text: `${BUGGY}caught ${card.caught} of ${card.total} - missed ${card.missed.join(", ")}.`
979
+ text: `${BUGGY}caught ${card.caught} of ${card.total} - missed ${card.missed.map(chaffLabel).join(", ")}.`
979
980
  });
980
981
  }
981
982
  return body;
982
983
  }
984
+ function chaffLabel(id) {
985
+ return /^\d+$/.test(id) ? `#${id}` : id;
986
+ }
983
987
  var UNMOUNTED_HINT = "Your tests are checked on their own, so files next to your program are not available while that happens.";
984
988
  function isFileAccessError(message) {
985
989
  return /^(FileNotFoundError|IsADirectoryError|NotADirectoryError|PermissionError|ModuleNotFoundError|ImportError):/.test(
@@ -1000,6 +1004,128 @@ function timedOutProblem(stuck) {
1000
1004
  return `\`${stuck.test}\` ran for more than ${stuck.seconds} seconds against ${against}, so the check stopped. ` + (stuck.kind === "wheat" ? "A test that runs that long is usually stuck in a loop." : "That implementation may never finish on what this test gives it; tell your course staff.");
1001
1005
  }
1002
1006
 
1007
+ // src/run/bundles.ts
1008
+ var DIRECTIVE_RE2 = /^#\s*bundle\s+(\S+)\s+(\S+:\/\/\S*)\s*$/;
1009
+ var NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1010
+ var KEYWORDS = /* @__PURE__ */ new Set([
1011
+ "False",
1012
+ "None",
1013
+ "True",
1014
+ "and",
1015
+ "as",
1016
+ "assert",
1017
+ "async",
1018
+ "await",
1019
+ "break",
1020
+ "class",
1021
+ "continue",
1022
+ "def",
1023
+ "del",
1024
+ "elif",
1025
+ "else",
1026
+ "except",
1027
+ "finally",
1028
+ "for",
1029
+ "from",
1030
+ "global",
1031
+ "if",
1032
+ "import",
1033
+ "in",
1034
+ "is",
1035
+ "lambda",
1036
+ "nonlocal",
1037
+ "not",
1038
+ "or",
1039
+ "pass",
1040
+ "raise",
1041
+ "return",
1042
+ "try",
1043
+ "while",
1044
+ "with",
1045
+ "yield"
1046
+ ]);
1047
+ var MODULE_BUNDLE = {
1048
+ noun: "a bundle",
1049
+ looksRight(json) {
1050
+ try {
1051
+ const bundle = JSON.parse(json);
1052
+ return typeof bundle === "object" && bundle !== null && typeof bundle.bundle === "number" && typeof bundle.pyc === "string";
1053
+ } catch {
1054
+ return false;
1055
+ }
1056
+ }
1057
+ };
1058
+ function parseBundleDirectives(source) {
1059
+ const bundles = [];
1060
+ const problems = [];
1061
+ const lines = source.split(/\r?\n/);
1062
+ for (let i = 0; i < lines.length; i++) {
1063
+ const match = DIRECTIVE_RE2.exec(lines[i].trim());
1064
+ if (match === null) {
1065
+ continue;
1066
+ }
1067
+ const [, name, url] = match;
1068
+ const line = i + 1;
1069
+ if (!NAME_RE.test(name) || KEYWORDS.has(name)) {
1070
+ problems.push({
1071
+ line,
1072
+ message: `\`${name}\` can't name a bundle: a name is letters, digits and \`_\`, not starting with a digit, and not one of Python's keywords.`
1073
+ });
1074
+ continue;
1075
+ }
1076
+ const earlier = bundles.find((bundle) => bundle.name === name);
1077
+ if (earlier !== void 0) {
1078
+ problems.push({
1079
+ line,
1080
+ message: `line ${earlier.line} already names a bundle \`${name}\`. Give each bundle its own name.`
1081
+ });
1082
+ continue;
1083
+ }
1084
+ const problem = validateBundleUrl(url);
1085
+ if (problem !== null) {
1086
+ problems.push({ line, message: problem });
1087
+ continue;
1088
+ }
1089
+ bundles.push({ name, url, line });
1090
+ }
1091
+ return { bundles, problems };
1092
+ }
1093
+ async function runBundleStep(runtime, store, host, code, sessionKey) {
1094
+ const { bundles, problems } = parseBundleDirectives(code);
1095
+ if (problems.length > 0) {
1096
+ for (const problem of problems) {
1097
+ host.say(`Line ${problem.line}: ${problem.message}`, "problem");
1098
+ }
1099
+ return "failed";
1100
+ }
1101
+ if (bundles.length === 0) {
1102
+ return "none";
1103
+ }
1104
+ host.status("Loading bundles...");
1105
+ const entries = [];
1106
+ for (const bundle of bundles) {
1107
+ const load = await loadBundle(bundle.url, store, void 0, MODULE_BUNDLE);
1108
+ if (load.note) {
1109
+ host.say(`The bundle \`${bundle.name}\`: ${load.note}`, "note");
1110
+ }
1111
+ if (load.json === void 0) {
1112
+ host.say(`Line ${bundle.line}: could not load the bundle \`${bundle.name}\`: ${load.error}.`, "problem");
1113
+ return "failed";
1114
+ }
1115
+ entries.push({ name: bundle.name, json: load.json });
1116
+ }
1117
+ const result = await runtime.bundleLoad(sessionKey, JSON.stringify(entries));
1118
+ if (!result.ok) {
1119
+ const at = bundles.find((bundle) => bundle.name === result.name);
1120
+ host.say(
1121
+ `Line ${at?.line ?? "?"}: the bundle \`${result.name ?? "?"}\` could not be loaded: ${result.error ?? "it is broken"}.`,
1122
+ "problem"
1123
+ );
1124
+ return "failed";
1125
+ }
1126
+ return "loaded";
1127
+ }
1128
+
1003
1129
  // src/run/packages.ts
1004
1130
  var MAY_NEED_PACKAGES_RE = /\bimport\b|__import__|\.\s*to_pandas\s*\(/;
1005
1131
  function needsPackages(code, siblings = []) {
@@ -1241,7 +1367,14 @@ async function runFilePlan(runtime, host, plan) {
1241
1367
  if (stopped(host, STOPPED.beforeStart)) {
1242
1368
  return "stopped";
1243
1369
  }
1244
- const complete = await runExamplarStep(runtime, plan.bundles, host, code, fileName);
1370
+ if (await runBundleStep(runtime, plan.bundles, host, code, sessionKey) === "failed") {
1371
+ host.say("The file was not run.", "problem");
1372
+ return "blocked";
1373
+ }
1374
+ if (stopped(host, STOPPED.beforeStart)) {
1375
+ return "stopped";
1376
+ }
1377
+ const complete = await runExamplarStep(runtime, plan.bundles, host, code, fileName, sessionKey);
1245
1378
  if (complete !== null && stopped(host, STOPPED.checking)) {
1246
1379
  return "stopped";
1247
1380
  }
@@ -1632,8 +1765,8 @@ function processStatus(code) {
1632
1765
  }
1633
1766
  async function startPython(runtime, stopRequested) {
1634
1767
  let watch;
1635
- const stop = new Promise((resolve4) => {
1636
- watch = setInterval(() => stopRequested() && resolve4("stopped"), 50);
1768
+ const stop = new Promise((resolve5) => {
1769
+ watch = setInterval(() => stopRequested() && resolve5("stopped"), 50);
1637
1770
  });
1638
1771
  try {
1639
1772
  return await Promise.race([
@@ -1708,7 +1841,105 @@ function cannotRead(file, err) {
1708
1841
  }
1709
1842
  }
1710
1843
 
1844
+ // src/cli/bundle.ts
1845
+ var BUNDLE_USAGE = `pll bundle - author bundles
1846
+
1847
+ pll bundle build <file.py> [-o bundle.json]
1848
+
1849
+ A bundle is a module a program names with a line of its own,
1850
+
1851
+ #bundle <name> https://example.edu/<file>.json
1852
+
1853
+ and then uses by that name, with no import: functions to call whose code
1854
+ the student doesn't see. Put the bundle where that address finds it.
1855
+
1856
+ <file.py> is the module, with the \`#level\` line it was written at: its code
1857
+ starts with that level's names (\`dataclass\`, \`Callable\`) wherever it is
1858
+ used. Its top-level \`test_\` functions, and an \`import pytest\`, are left out
1859
+ of the bundle; everything else is in it, docstrings too.
1860
+
1861
+ Only bytecode is written to the bundle, so the source stays in your
1862
+ repository. It is compiled by the Pyodide this package pins, so it always
1863
+ matches the interpreter that will run it.
1864
+
1865
+ -o, --out FILE write here instead of stdout
1866
+ `;
1867
+ var BUNDLE_USAGE_HINT = "Usage: pll bundle build <file.py> [-o bundle.json]. `pll bundle --help` says more.";
1868
+ var BUILD_OPTIONS = {
1869
+ out: { type: "string", short: "o", needs: "a file" },
1870
+ help: { type: "boolean", short: "h" }
1871
+ };
1872
+ function parseBundleArgs(argv) {
1873
+ if (argv[0] === "-h" || argv[0] === "--help") {
1874
+ return { help: true };
1875
+ }
1876
+ if (argv[0] !== "build") {
1877
+ return {
1878
+ help: false,
1879
+ error: argv.length === 0 ? "bundle needs a subcommand" : `unknown bundle subcommand ${argv[0]}`
1880
+ };
1881
+ }
1882
+ const line = parseCommandLine(argv.slice(1), BUILD_OPTIONS);
1883
+ if ("error" in line) {
1884
+ return { help: false, error: line.error };
1885
+ }
1886
+ const { values, positionals } = line;
1887
+ const args = {
1888
+ file: positionals[0],
1889
+ out: values.out,
1890
+ help: values.help === true
1891
+ };
1892
+ if (positionals.length > 1) {
1893
+ args.error = `unexpected extra argument ${positionals[1]}`;
1894
+ } else if (!args.help && args.file === void 0) {
1895
+ args.error = "no file given";
1896
+ } else if (args.file !== void 0 && !args.file.endsWith(".py")) {
1897
+ args.error = `${args.file} is not a .py file`;
1898
+ }
1899
+ return args;
1900
+ }
1901
+ async function runBundle(runtime, view, argv) {
1902
+ const args = parseBundleArgs(argv);
1903
+ if (args.help) {
1904
+ process.stdout.write(BUNDLE_USAGE);
1905
+ return EXIT.ok;
1906
+ }
1907
+ if (args.error !== void 0) {
1908
+ process.stderr.write(`pll: ${args.error}
1909
+ ${BUNDLE_USAGE_HINT}
1910
+ `);
1911
+ return EXIT.usage;
1912
+ }
1913
+ const file = path4.resolve(args.file);
1914
+ let source;
1915
+ try {
1916
+ source = await fs4.readFile(file, "utf8");
1917
+ } catch (err) {
1918
+ view.problem(`pll: cannot read ${args.file}: ${errorText(err)}`);
1919
+ return EXIT.usage;
1920
+ }
1921
+ const built = await runtime.bundleBuild(source, path4.basename(file));
1922
+ if (!built.ok || !built.bundle) {
1923
+ view.problem(`pll: ${built.error ?? "could not build the bundle"}`);
1924
+ return EXIT.usage;
1925
+ }
1926
+ const bundle = built.bundle;
1927
+ view.note(
1928
+ `built for Python ${bundle.built.python}, at #level ${bundle.level}: defines ${bundle.defines.join(", ") || "nothing"}`
1929
+ );
1930
+ const json = JSON.stringify(bundle);
1931
+ if (args.out === void 0) {
1932
+ process.stdout.write(json + "\n");
1933
+ } else {
1934
+ await fs4.writeFile(path4.resolve(args.out), json + "\n", "utf8");
1935
+ view.note(`wrote ${args.out} (${json.length} bytes)`);
1936
+ }
1937
+ return EXIT.ok;
1938
+ }
1939
+
1711
1940
  // src/cli/examplar.ts
1941
+ var fs5 = __toESM(require("node:fs/promises"));
1942
+ var path5 = __toESM(require("node:path"));
1712
1943
  var EXAMPLAR_USAGE = `pll examplar - author Examplar bundles
1713
1944
 
1714
1945
  pll examplar build <dir> [-o bundle.json] [--verify tests.py]
@@ -1740,7 +1971,7 @@ bundles with this tool rather than a local python.
1740
1971
  wrong fails on everything.
1741
1972
  `;
1742
1973
  var EXAMPLAR_USAGE_HINT = "Usage: pll examplar build <dir> [-o bundle.json] [--verify tests.py]. `pll examplar --help` says more.";
1743
- var BUILD_OPTIONS = {
1974
+ var BUILD_OPTIONS2 = {
1744
1975
  out: { type: "string", short: "o", needs: "a file" },
1745
1976
  verify: { type: "string", needs: "a test file" },
1746
1977
  help: { type: "boolean", short: "h" }
@@ -1755,7 +1986,7 @@ function parseExamplarArgs(argv) {
1755
1986
  error: argv.length === 0 ? "examplar needs a subcommand" : `unknown examplar subcommand ${argv[0]}`
1756
1987
  };
1757
1988
  }
1758
- const line = parseCommandLine(argv.slice(1), BUILD_OPTIONS);
1989
+ const line = parseCommandLine(argv.slice(1), BUILD_OPTIONS2);
1759
1990
  if ("error" in line) {
1760
1991
  return { help: false, error: line.error };
1761
1992
  }
@@ -1775,17 +2006,17 @@ function parseExamplarArgs(argv) {
1775
2006
  }
1776
2007
  async function readDir(folder) {
1777
2008
  try {
1778
- return await fs4.readdir(folder, { withFileTypes: true });
2009
+ return await fs5.readdir(folder, { withFileTypes: true });
1779
2010
  } catch {
1780
2011
  throw new Error(`${folder} does not exist. See \`pll examplar --help\`.`);
1781
2012
  }
1782
2013
  }
1783
2014
  async function readWheats(dir) {
1784
- const folder = path4.join(dir, "wheats");
2015
+ const folder = path5.join(dir, "wheats");
1785
2016
  const out = {};
1786
2017
  for (const entry of await readDir(folder)) {
1787
2018
  if (!entry.isFile() || !entry.name.endsWith(".py")) continue;
1788
- out[entry.name.slice(0, -3)] = await fs4.readFile(path4.join(folder, entry.name), "utf8");
2019
+ out[entry.name.slice(0, -3)] = await fs5.readFile(path5.join(folder, entry.name), "utf8");
1789
2020
  }
1790
2021
  if (Object.keys(out).length === 0) {
1791
2022
  throw new Error(`${folder} has no .py files in it.`);
@@ -1793,7 +2024,7 @@ async function readWheats(dir) {
1793
2024
  return out;
1794
2025
  }
1795
2026
  async function readChaffs(dir) {
1796
- const folder = path4.join(dir, "chaffs");
2027
+ const folder = path5.join(dir, "chaffs");
1797
2028
  const out = {};
1798
2029
  const loose = [];
1799
2030
  for (const entry of await readDir(folder)) {
@@ -1802,11 +2033,11 @@ async function readChaffs(dir) {
1802
2033
  continue;
1803
2034
  }
1804
2035
  if (!entry.isDirectory()) continue;
1805
- const inner = path4.join(folder, entry.name);
2036
+ const inner = path5.join(folder, entry.name);
1806
2037
  const group = {};
1807
- for (const file of await fs4.readdir(inner, { withFileTypes: true })) {
2038
+ for (const file of await fs5.readdir(inner, { withFileTypes: true })) {
1808
2039
  if (!file.isFile() || !file.name.endsWith(".py")) continue;
1809
- group[file.name.slice(0, -3)] = await fs4.readFile(path4.join(inner, file.name), "utf8");
2040
+ group[file.name.slice(0, -3)] = await fs5.readFile(path5.join(inner, file.name), "utf8");
1810
2041
  }
1811
2042
  if (Object.keys(group).length > 0) {
1812
2043
  out[entry.name] = group;
@@ -1814,7 +2045,7 @@ async function readChaffs(dir) {
1814
2045
  }
1815
2046
  if (loose.length > 0) {
1816
2047
  throw new Error(
1817
- `${path4.join(folder, loose[0])} is not inside a function's directory. Chaffs go in \`chaffs/<function>/\`, named after the function they break - move it to \`chaffs/<function>/${loose[0]}\`.`
2048
+ `${path5.join(folder, loose[0])} is not inside a function's directory. Chaffs go in \`chaffs/<function>/\`, named after the function they break - move it to \`chaffs/<function>/${loose[0]}\`.`
1818
2049
  );
1819
2050
  }
1820
2051
  if (Object.keys(out).length === 0) {
@@ -1834,7 +2065,7 @@ ${EXAMPLAR_USAGE_HINT}
1834
2065
  `);
1835
2066
  return EXIT.usage;
1836
2067
  }
1837
- const dir = path4.resolve(args.dir);
2068
+ const dir = path5.resolve(args.dir);
1838
2069
  let sources;
1839
2070
  try {
1840
2071
  sources = { wheats: await readWheats(dir), chaffs: await readChaffs(dir) };
@@ -1861,7 +2092,7 @@ ${EXAMPLAR_USAGE_HINT}
1861
2092
  if (args.out === void 0) {
1862
2093
  process.stdout.write(json + "\n");
1863
2094
  } else {
1864
- await fs4.writeFile(path4.resolve(args.out), json + "\n", "utf8");
2095
+ await fs5.writeFile(path5.resolve(args.out), json + "\n", "utf8");
1865
2096
  view.note(`wrote ${args.out} (${json.length} bytes)`);
1866
2097
  }
1867
2098
  return EXIT.ok;
@@ -1872,7 +2103,7 @@ function caught(impl) {
1872
2103
  async function verify(runtime, view, bundle, testsPath) {
1873
2104
  let testSource;
1874
2105
  try {
1875
- testSource = await fs4.readFile(path4.resolve(testsPath), "utf8");
2106
+ testSource = await fs5.readFile(path5.resolve(testsPath), "utf8");
1876
2107
  } catch (err) {
1877
2108
  view.problem(`pll: cannot read ${testsPath}: ${errorText(err)}`);
1878
2109
  return EXIT.usage;
@@ -1882,17 +2113,22 @@ async function verify(runtime, view, bundle, testsPath) {
1882
2113
  } catch {
1883
2114
  view.note("could not load pytest; assertion messages will be terse.");
1884
2115
  }
2116
+ try {
2117
+ await runtime.ensurePackages(testSource);
2118
+ } catch (err) {
2119
+ view.note(`could not load libraries (${errorText(err)}); imports may fail.`);
2120
+ }
1885
2121
  const result = await runtime.examplarRun(
1886
2122
  testSource,
1887
2123
  JSON.stringify(bundle),
1888
- path4.basename(testsPath)
2124
+ path5.basename(testsPath)
1889
2125
  );
1890
2126
  if (!result.ok) {
1891
2127
  view.problem(`pll: ${result.error ?? "the bundle could not be run"}`);
1892
2128
  return EXIT.usage;
1893
2129
  }
1894
2130
  let bad = 0;
1895
- view.problem(`verifying with ${path4.basename(testsPath)}:`);
2131
+ view.problem(`verifying with ${path5.basename(testsPath)}:`);
1896
2132
  if (result.timedOut) {
1897
2133
  const { kind, id, targets, test, seconds } = result.timedOut;
1898
2134
  const where = kind === "wheat" ? `wheat ${id}` : `chaff ${targets}/${id}`;
@@ -1952,12 +2188,12 @@ async function verify(runtime, view, bundle, testsPath) {
1952
2188
  }
1953
2189
 
1954
2190
  // src/cli/runtime.ts
1955
- var fs6 = __toESM(require("node:fs"));
1956
- var path6 = __toESM(require("node:path"));
2191
+ var fs7 = __toESM(require("node:fs"));
2192
+ var path7 = __toESM(require("node:path"));
1957
2193
 
1958
2194
  // src/desktop/pyodideRuntime.ts
1959
- var fs5 = __toESM(require("node:fs"));
1960
- var path5 = __toESM(require("node:path"));
2195
+ var fs6 = __toESM(require("node:fs"));
2196
+ var path6 = __toESM(require("node:path"));
1961
2197
  var import_node_worker_threads = require("node:worker_threads");
1962
2198
 
1963
2199
  // src/run/python.ts
@@ -2038,10 +2274,27 @@ var Python = class extends WorkerConnection {
2038
2274
  /**
2039
2275
  * Run a student's tests against every implementation in a bundle.
2040
2276
  * `fileName` is the student's file, whose own errors are told from the
2041
- * implementation's.
2277
+ * implementation's; its definitions are given the modules its `#bundle`
2278
+ * lines name, loaded for `sessionKey`'s run (`bundleLoad`).
2279
+ */
2280
+ examplarRun(testSource, bundle, fileName, sessionKey = "") {
2281
+ return this.call("_pll_examplar_run", [testSource, bundle, fileName, sessionKey]);
2282
+ }
2283
+ /**
2284
+ * Compile a module into a bundle, for `pll bundle build`. Authoring only;
2285
+ * the bytecode is this interpreter's, as `examplarBuild`'s is.
2286
+ */
2287
+ bundleBuild(source, fileName) {
2288
+ return this.call("_pll_bundle_build", [source, fileName]);
2289
+ }
2290
+ /**
2291
+ * Make the modules a file's `#bundle` lines name, which `sessionKey`'s
2292
+ * next file run gives its program. `entries` is the JSON of
2293
+ * `[{name, json}]`, each `json` a fetched bundle. Their top levels run
2294
+ * here.
2042
2295
  */
2043
- examplarRun(testSource, bundle, fileName) {
2044
- return this.call("_pll_examplar_run", [testSource, bundle, fileName]);
2296
+ bundleLoad(sessionKey, entries) {
2297
+ return this.call("_pll_bundle_load", [sessionKey, entries]);
2045
2298
  }
2046
2299
  /**
2047
2300
  * Apply one event to a running reactor and get the frame it produced.
@@ -2089,7 +2342,7 @@ function deliverRunResult(result, onEvent, fileName) {
2089
2342
  function desktopPython(paths) {
2090
2343
  return new Python({
2091
2344
  indexUrl() {
2092
- const found = paths.indexUrlCandidates.find((dir) => fs5.existsSync(path5.join(dir, "pyodide.asm.wasm")));
2345
+ const found = paths.indexUrlCandidates.find((dir) => fs6.existsSync(path6.join(dir, "pyodide.asm.wasm")));
2093
2346
  if (!found) {
2094
2347
  throw new Error(paths.missingAssetsHint);
2095
2348
  }
@@ -2114,7 +2367,7 @@ function desktopPython(paths) {
2114
2367
  function createCliRuntime() {
2115
2368
  return desktopPython({
2116
2369
  indexUrlCandidates: pyodideDirectory(),
2117
- workerPath: path6.join(__dirname, "worker.cjs"),
2370
+ workerPath: path7.join(__dirname, "worker.cjs"),
2118
2371
  missingAssetsHint: "Could not find Pyodide's assets. Reinstall pll-python so its `pyodide` dependency is present.",
2119
2372
  // Ample for a first start on a slow machine; an autograder waits no
2120
2373
  // longer than that for a Pyodide that will never start.
@@ -2124,7 +2377,7 @@ function createCliRuntime() {
2124
2377
  }
2125
2378
  function writable(dir) {
2126
2379
  try {
2127
- fs6.accessSync(dir, fs6.constants.W_OK);
2380
+ fs7.accessSync(dir, fs7.constants.W_OK);
2128
2381
  return true;
2129
2382
  } catch {
2130
2383
  return false;
@@ -2133,15 +2386,15 @@ function writable(dir) {
2133
2386
  function userPackageCache(indexUrl) {
2134
2387
  let version = "unknown";
2135
2388
  try {
2136
- const manifest = fs6.readFileSync(path6.join(indexUrl, "package.json"), "utf8");
2389
+ const manifest = fs7.readFileSync(path7.join(indexUrl, "package.json"), "utf8");
2137
2390
  version = JSON.parse(manifest).version;
2138
2391
  } catch {
2139
2392
  }
2140
- return path6.join(cacheRoot(), `pyodide-${version}`);
2393
+ return path7.join(cacheRoot(), `pyodide-${version}`);
2141
2394
  }
2142
2395
  function pyodideDirectory() {
2143
2396
  try {
2144
- return [path6.dirname(require.resolve("pyodide"))];
2397
+ return [path7.dirname(require.resolve("pyodide"))];
2145
2398
  } catch {
2146
2399
  return [];
2147
2400
  }
@@ -2179,7 +2432,7 @@ function createStdinReader() {
2179
2432
  return Promise.resolve(null);
2180
2433
  }
2181
2434
  start();
2182
- return new Promise((resolve4) => waiting.push(resolve4));
2435
+ return new Promise((resolve5) => waiting.push(resolve5));
2183
2436
  },
2184
2437
  close() {
2185
2438
  ended = true;
@@ -2194,8 +2447,8 @@ function createStdinReader() {
2194
2447
  }
2195
2448
 
2196
2449
  // src/cli/view.ts
2197
- var fs7 = __toESM(require("node:fs"));
2198
- var path7 = __toESM(require("node:path"));
2450
+ var fs8 = __toESM(require("node:fs"));
2451
+ var path8 = __toESM(require("node:path"));
2199
2452
  var ESC = String.fromCharCode(27);
2200
2453
  var CliView = class {
2201
2454
  constructor(opts) {
@@ -2333,10 +2586,10 @@ var CliView = class {
2333
2586
  );
2334
2587
  return;
2335
2588
  }
2336
- const target = path7.join(dir, `image-${this.imageCount}.svg`);
2589
+ const target = path8.join(dir, `image-${this.imageCount}.svg`);
2337
2590
  try {
2338
- fs7.mkdirSync(dir, { recursive: true });
2339
- fs7.writeFileSync(target, svg, "utf8");
2591
+ fs8.mkdirSync(dir, { recursive: true });
2592
+ fs8.writeFileSync(target, svg, "utf8");
2340
2593
  this.note(this.dim(`[image ${width}x${height} -> ${target}]`));
2341
2594
  } catch (err) {
2342
2595
  this.problem(`Could not write ${target}: ${errorText(err)}`);
@@ -2454,6 +2707,8 @@ same everywhere.
2454
2707
 
2455
2708
  Subcommands
2456
2709
  examplar build author an Examplar bundle; see \`pll examplar --help\`
2710
+ bundle build compile a module a \`#bundle\` line names; see
2711
+ \`pll bundle --help\`
2457
2712
 
2458
2713
  Options
2459
2714
  --no-tests do not run the file's \`test_*\` functions, which
@@ -2472,7 +2727,8 @@ everything PLL says about the run goes to stderr.
2472
2727
  Exit codes
2473
2728
  0 ran, and any tests passed
2474
2729
  1 the program raised, or was stopped
2475
- 2 level checks found problems, so it was not run
2730
+ 2 level checks found problems, or a bundle could not be loaded,
2731
+ so it was not run
2476
2732
  3 a test failed
2477
2733
  64 bad usage, or Python could not start
2478
2734
  130 a second Ctrl+C gave up waiting for it to stop
@@ -2545,17 +2801,24 @@ function parseArgs2(argv) {
2545
2801
  }
2546
2802
  function isFile(at) {
2547
2803
  try {
2548
- return !fs8.statSync(at).isDirectory();
2804
+ return !fs9.statSync(at).isDirectory();
2549
2805
  } catch {
2550
2806
  return false;
2551
2807
  }
2552
2808
  }
2809
+ var SUBCOMMANDS = {
2810
+ examplar: runExamplar,
2811
+ bundle: runBundle
2812
+ };
2553
2813
  async function main(argv) {
2554
- const at = subcommandIndex(argv, "examplar", OPTIONS);
2555
- if (at !== -1) {
2814
+ for (const [name, run] of Object.entries(SUBCOMMANDS)) {
2815
+ const at = subcommandIndex(argv, name, OPTIONS);
2816
+ if (at === -1) {
2817
+ continue;
2818
+ }
2556
2819
  const leading = parseCommandLine(argv.slice(0, at), LEADING_OPTIONS);
2557
2820
  if ("error" in leading) {
2558
- process.stderr.write(`pll: ${leading.error} before examplar
2821
+ process.stderr.write(`pll: ${leading.error} before ${name}
2559
2822
  ${USAGE_HINT}
2560
2823
  `);
2561
2824
  return EXIT.usage;
@@ -2567,7 +2830,7 @@ ${USAGE_HINT}
2567
2830
  const runtime2 = createCliRuntime();
2568
2831
  sayPackageNotes(runtime2, view2);
2569
2832
  try {
2570
- return await runExamplar(runtime2, view2, argv.slice(at + 1));
2833
+ return await run(runtime2, view2, argv.slice(at + 1));
2571
2834
  } catch (err) {
2572
2835
  view2.problem(`pll: ${errorText(err)}`);
2573
2836
  return EXIT.usage;
@@ -2581,7 +2844,7 @@ ${USAGE_HINT}
2581
2844
  return EXIT.ok;
2582
2845
  }
2583
2846
  if (args.version) {
2584
- process.stdout.write("0.8.1\n");
2847
+ process.stdout.write("0.9.1\n");
2585
2848
  return EXIT.ok;
2586
2849
  }
2587
2850
  if (args.error !== void 0) {
@@ -2621,7 +2884,7 @@ ${USAGE_HINT}
2621
2884
  process.stderr.on("error", onBrokenPipe);
2622
2885
  try {
2623
2886
  return await runFile(runtime, view, {
2624
- file: path8.resolve(args.file),
2887
+ file: path9.resolve(args.file),
2625
2888
  runTests: args.runTests,
2626
2889
  stopRequested: () => interrupted
2627
2890
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pll-python",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Run Python with PLL's language levels from the command line: level checks, in-file tests, friendly errors and the built-in image and table libraries, all on Pyodide. No Python install needed.",
5
5
  "license": "MIT",
6
6
  "repository": {