pll-python 0.9.0 → 0.9.2
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.
- package/README.md +45 -1
- package/cli.cjs +310 -55
- package/package.json +1 -1
- package/worker.cjs +17 -12
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
|
|
28
|
-
var
|
|
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/
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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((
|
|
686
|
-
this.pending.set(id, { resolve:
|
|
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)));
|
|
@@ -1003,6 +1004,128 @@ function timedOutProblem(stuck) {
|
|
|
1003
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.");
|
|
1004
1005
|
}
|
|
1005
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
|
+
|
|
1006
1129
|
// src/run/packages.ts
|
|
1007
1130
|
var MAY_NEED_PACKAGES_RE = /\bimport\b|__import__|\.\s*to_pandas\s*\(/;
|
|
1008
1131
|
function needsPackages(code, siblings = []) {
|
|
@@ -1244,7 +1367,14 @@ async function runFilePlan(runtime, host, plan) {
|
|
|
1244
1367
|
if (stopped(host, STOPPED.beforeStart)) {
|
|
1245
1368
|
return "stopped";
|
|
1246
1369
|
}
|
|
1247
|
-
|
|
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);
|
|
1248
1378
|
if (complete !== null && stopped(host, STOPPED.checking)) {
|
|
1249
1379
|
return "stopped";
|
|
1250
1380
|
}
|
|
@@ -1635,8 +1765,8 @@ function processStatus(code) {
|
|
|
1635
1765
|
}
|
|
1636
1766
|
async function startPython(runtime, stopRequested) {
|
|
1637
1767
|
let watch;
|
|
1638
|
-
const stop = new Promise((
|
|
1639
|
-
watch = setInterval(() => stopRequested() &&
|
|
1768
|
+
const stop = new Promise((resolve5) => {
|
|
1769
|
+
watch = setInterval(() => stopRequested() && resolve5("stopped"), 50);
|
|
1640
1770
|
});
|
|
1641
1771
|
try {
|
|
1642
1772
|
return await Promise.race([
|
|
@@ -1711,7 +1841,105 @@ function cannotRead(file, err) {
|
|
|
1711
1841
|
}
|
|
1712
1842
|
}
|
|
1713
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
|
+
|
|
1714
1940
|
// src/cli/examplar.ts
|
|
1941
|
+
var fs5 = __toESM(require("node:fs/promises"));
|
|
1942
|
+
var path5 = __toESM(require("node:path"));
|
|
1715
1943
|
var EXAMPLAR_USAGE = `pll examplar - author Examplar bundles
|
|
1716
1944
|
|
|
1717
1945
|
pll examplar build <dir> [-o bundle.json] [--verify tests.py]
|
|
@@ -1743,7 +1971,7 @@ bundles with this tool rather than a local python.
|
|
|
1743
1971
|
wrong fails on everything.
|
|
1744
1972
|
`;
|
|
1745
1973
|
var EXAMPLAR_USAGE_HINT = "Usage: pll examplar build <dir> [-o bundle.json] [--verify tests.py]. `pll examplar --help` says more.";
|
|
1746
|
-
var
|
|
1974
|
+
var BUILD_OPTIONS2 = {
|
|
1747
1975
|
out: { type: "string", short: "o", needs: "a file" },
|
|
1748
1976
|
verify: { type: "string", needs: "a test file" },
|
|
1749
1977
|
help: { type: "boolean", short: "h" }
|
|
@@ -1758,7 +1986,7 @@ function parseExamplarArgs(argv) {
|
|
|
1758
1986
|
error: argv.length === 0 ? "examplar needs a subcommand" : `unknown examplar subcommand ${argv[0]}`
|
|
1759
1987
|
};
|
|
1760
1988
|
}
|
|
1761
|
-
const line = parseCommandLine(argv.slice(1),
|
|
1989
|
+
const line = parseCommandLine(argv.slice(1), BUILD_OPTIONS2);
|
|
1762
1990
|
if ("error" in line) {
|
|
1763
1991
|
return { help: false, error: line.error };
|
|
1764
1992
|
}
|
|
@@ -1778,17 +2006,17 @@ function parseExamplarArgs(argv) {
|
|
|
1778
2006
|
}
|
|
1779
2007
|
async function readDir(folder) {
|
|
1780
2008
|
try {
|
|
1781
|
-
return await
|
|
2009
|
+
return await fs5.readdir(folder, { withFileTypes: true });
|
|
1782
2010
|
} catch {
|
|
1783
2011
|
throw new Error(`${folder} does not exist. See \`pll examplar --help\`.`);
|
|
1784
2012
|
}
|
|
1785
2013
|
}
|
|
1786
2014
|
async function readWheats(dir) {
|
|
1787
|
-
const folder =
|
|
2015
|
+
const folder = path5.join(dir, "wheats");
|
|
1788
2016
|
const out = {};
|
|
1789
2017
|
for (const entry of await readDir(folder)) {
|
|
1790
2018
|
if (!entry.isFile() || !entry.name.endsWith(".py")) continue;
|
|
1791
|
-
out[entry.name.slice(0, -3)] = await
|
|
2019
|
+
out[entry.name.slice(0, -3)] = await fs5.readFile(path5.join(folder, entry.name), "utf8");
|
|
1792
2020
|
}
|
|
1793
2021
|
if (Object.keys(out).length === 0) {
|
|
1794
2022
|
throw new Error(`${folder} has no .py files in it.`);
|
|
@@ -1796,7 +2024,7 @@ async function readWheats(dir) {
|
|
|
1796
2024
|
return out;
|
|
1797
2025
|
}
|
|
1798
2026
|
async function readChaffs(dir) {
|
|
1799
|
-
const folder =
|
|
2027
|
+
const folder = path5.join(dir, "chaffs");
|
|
1800
2028
|
const out = {};
|
|
1801
2029
|
const loose = [];
|
|
1802
2030
|
for (const entry of await readDir(folder)) {
|
|
@@ -1805,11 +2033,11 @@ async function readChaffs(dir) {
|
|
|
1805
2033
|
continue;
|
|
1806
2034
|
}
|
|
1807
2035
|
if (!entry.isDirectory()) continue;
|
|
1808
|
-
const inner =
|
|
2036
|
+
const inner = path5.join(folder, entry.name);
|
|
1809
2037
|
const group = {};
|
|
1810
|
-
for (const file of await
|
|
2038
|
+
for (const file of await fs5.readdir(inner, { withFileTypes: true })) {
|
|
1811
2039
|
if (!file.isFile() || !file.name.endsWith(".py")) continue;
|
|
1812
|
-
group[file.name.slice(0, -3)] = await
|
|
2040
|
+
group[file.name.slice(0, -3)] = await fs5.readFile(path5.join(inner, file.name), "utf8");
|
|
1813
2041
|
}
|
|
1814
2042
|
if (Object.keys(group).length > 0) {
|
|
1815
2043
|
out[entry.name] = group;
|
|
@@ -1817,7 +2045,7 @@ async function readChaffs(dir) {
|
|
|
1817
2045
|
}
|
|
1818
2046
|
if (loose.length > 0) {
|
|
1819
2047
|
throw new Error(
|
|
1820
|
-
`${
|
|
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]}\`.`
|
|
1821
2049
|
);
|
|
1822
2050
|
}
|
|
1823
2051
|
if (Object.keys(out).length === 0) {
|
|
@@ -1837,7 +2065,7 @@ ${EXAMPLAR_USAGE_HINT}
|
|
|
1837
2065
|
`);
|
|
1838
2066
|
return EXIT.usage;
|
|
1839
2067
|
}
|
|
1840
|
-
const dir =
|
|
2068
|
+
const dir = path5.resolve(args.dir);
|
|
1841
2069
|
let sources;
|
|
1842
2070
|
try {
|
|
1843
2071
|
sources = { wheats: await readWheats(dir), chaffs: await readChaffs(dir) };
|
|
@@ -1864,7 +2092,7 @@ ${EXAMPLAR_USAGE_HINT}
|
|
|
1864
2092
|
if (args.out === void 0) {
|
|
1865
2093
|
process.stdout.write(json + "\n");
|
|
1866
2094
|
} else {
|
|
1867
|
-
await
|
|
2095
|
+
await fs5.writeFile(path5.resolve(args.out), json + "\n", "utf8");
|
|
1868
2096
|
view.note(`wrote ${args.out} (${json.length} bytes)`);
|
|
1869
2097
|
}
|
|
1870
2098
|
return EXIT.ok;
|
|
@@ -1875,7 +2103,7 @@ function caught(impl) {
|
|
|
1875
2103
|
async function verify(runtime, view, bundle, testsPath) {
|
|
1876
2104
|
let testSource;
|
|
1877
2105
|
try {
|
|
1878
|
-
testSource = await
|
|
2106
|
+
testSource = await fs5.readFile(path5.resolve(testsPath), "utf8");
|
|
1879
2107
|
} catch (err) {
|
|
1880
2108
|
view.problem(`pll: cannot read ${testsPath}: ${errorText(err)}`);
|
|
1881
2109
|
return EXIT.usage;
|
|
@@ -1893,14 +2121,14 @@ async function verify(runtime, view, bundle, testsPath) {
|
|
|
1893
2121
|
const result = await runtime.examplarRun(
|
|
1894
2122
|
testSource,
|
|
1895
2123
|
JSON.stringify(bundle),
|
|
1896
|
-
|
|
2124
|
+
path5.basename(testsPath)
|
|
1897
2125
|
);
|
|
1898
2126
|
if (!result.ok) {
|
|
1899
2127
|
view.problem(`pll: ${result.error ?? "the bundle could not be run"}`);
|
|
1900
2128
|
return EXIT.usage;
|
|
1901
2129
|
}
|
|
1902
2130
|
let bad = 0;
|
|
1903
|
-
view.problem(`verifying with ${
|
|
2131
|
+
view.problem(`verifying with ${path5.basename(testsPath)}:`);
|
|
1904
2132
|
if (result.timedOut) {
|
|
1905
2133
|
const { kind, id, targets, test, seconds } = result.timedOut;
|
|
1906
2134
|
const where = kind === "wheat" ? `wheat ${id}` : `chaff ${targets}/${id}`;
|
|
@@ -1960,12 +2188,12 @@ async function verify(runtime, view, bundle, testsPath) {
|
|
|
1960
2188
|
}
|
|
1961
2189
|
|
|
1962
2190
|
// src/cli/runtime.ts
|
|
1963
|
-
var
|
|
1964
|
-
var
|
|
2191
|
+
var fs7 = __toESM(require("node:fs"));
|
|
2192
|
+
var path7 = __toESM(require("node:path"));
|
|
1965
2193
|
|
|
1966
2194
|
// src/desktop/pyodideRuntime.ts
|
|
1967
|
-
var
|
|
1968
|
-
var
|
|
2195
|
+
var fs6 = __toESM(require("node:fs"));
|
|
2196
|
+
var path6 = __toESM(require("node:path"));
|
|
1969
2197
|
var import_node_worker_threads = require("node:worker_threads");
|
|
1970
2198
|
|
|
1971
2199
|
// src/run/python.ts
|
|
@@ -2046,10 +2274,27 @@ var Python = class extends WorkerConnection {
|
|
|
2046
2274
|
/**
|
|
2047
2275
|
* Run a student's tests against every implementation in a bundle.
|
|
2048
2276
|
* `fileName` is the student's file, whose own errors are told from the
|
|
2049
|
-
* 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.
|
|
2050
2295
|
*/
|
|
2051
|
-
|
|
2052
|
-
return this.call("
|
|
2296
|
+
bundleLoad(sessionKey, entries) {
|
|
2297
|
+
return this.call("_pll_bundle_load", [sessionKey, entries]);
|
|
2053
2298
|
}
|
|
2054
2299
|
/**
|
|
2055
2300
|
* Apply one event to a running reactor and get the frame it produced.
|
|
@@ -2097,7 +2342,7 @@ function deliverRunResult(result, onEvent, fileName) {
|
|
|
2097
2342
|
function desktopPython(paths) {
|
|
2098
2343
|
return new Python({
|
|
2099
2344
|
indexUrl() {
|
|
2100
|
-
const found = paths.indexUrlCandidates.find((dir) =>
|
|
2345
|
+
const found = paths.indexUrlCandidates.find((dir) => fs6.existsSync(path6.join(dir, "pyodide.asm.wasm")));
|
|
2101
2346
|
if (!found) {
|
|
2102
2347
|
throw new Error(paths.missingAssetsHint);
|
|
2103
2348
|
}
|
|
@@ -2122,7 +2367,7 @@ function desktopPython(paths) {
|
|
|
2122
2367
|
function createCliRuntime() {
|
|
2123
2368
|
return desktopPython({
|
|
2124
2369
|
indexUrlCandidates: pyodideDirectory(),
|
|
2125
|
-
workerPath:
|
|
2370
|
+
workerPath: path7.join(__dirname, "worker.cjs"),
|
|
2126
2371
|
missingAssetsHint: "Could not find Pyodide's assets. Reinstall pll-python so its `pyodide` dependency is present.",
|
|
2127
2372
|
// Ample for a first start on a slow machine; an autograder waits no
|
|
2128
2373
|
// longer than that for a Pyodide that will never start.
|
|
@@ -2132,7 +2377,7 @@ function createCliRuntime() {
|
|
|
2132
2377
|
}
|
|
2133
2378
|
function writable(dir) {
|
|
2134
2379
|
try {
|
|
2135
|
-
|
|
2380
|
+
fs7.accessSync(dir, fs7.constants.W_OK);
|
|
2136
2381
|
return true;
|
|
2137
2382
|
} catch {
|
|
2138
2383
|
return false;
|
|
@@ -2141,15 +2386,15 @@ function writable(dir) {
|
|
|
2141
2386
|
function userPackageCache(indexUrl) {
|
|
2142
2387
|
let version = "unknown";
|
|
2143
2388
|
try {
|
|
2144
|
-
const manifest =
|
|
2389
|
+
const manifest = fs7.readFileSync(path7.join(indexUrl, "package.json"), "utf8");
|
|
2145
2390
|
version = JSON.parse(manifest).version;
|
|
2146
2391
|
} catch {
|
|
2147
2392
|
}
|
|
2148
|
-
return
|
|
2393
|
+
return path7.join(cacheRoot(), `pyodide-${version}`);
|
|
2149
2394
|
}
|
|
2150
2395
|
function pyodideDirectory() {
|
|
2151
2396
|
try {
|
|
2152
|
-
return [
|
|
2397
|
+
return [path7.dirname(require.resolve("pyodide"))];
|
|
2153
2398
|
} catch {
|
|
2154
2399
|
return [];
|
|
2155
2400
|
}
|
|
@@ -2187,7 +2432,7 @@ function createStdinReader() {
|
|
|
2187
2432
|
return Promise.resolve(null);
|
|
2188
2433
|
}
|
|
2189
2434
|
start();
|
|
2190
|
-
return new Promise((
|
|
2435
|
+
return new Promise((resolve5) => waiting.push(resolve5));
|
|
2191
2436
|
},
|
|
2192
2437
|
close() {
|
|
2193
2438
|
ended = true;
|
|
@@ -2202,8 +2447,8 @@ function createStdinReader() {
|
|
|
2202
2447
|
}
|
|
2203
2448
|
|
|
2204
2449
|
// src/cli/view.ts
|
|
2205
|
-
var
|
|
2206
|
-
var
|
|
2450
|
+
var fs8 = __toESM(require("node:fs"));
|
|
2451
|
+
var path8 = __toESM(require("node:path"));
|
|
2207
2452
|
var ESC = String.fromCharCode(27);
|
|
2208
2453
|
var CliView = class {
|
|
2209
2454
|
constructor(opts) {
|
|
@@ -2341,10 +2586,10 @@ var CliView = class {
|
|
|
2341
2586
|
);
|
|
2342
2587
|
return;
|
|
2343
2588
|
}
|
|
2344
|
-
const target =
|
|
2589
|
+
const target = path8.join(dir, `image-${this.imageCount}.svg`);
|
|
2345
2590
|
try {
|
|
2346
|
-
|
|
2347
|
-
|
|
2591
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
2592
|
+
fs8.writeFileSync(target, svg, "utf8");
|
|
2348
2593
|
this.note(this.dim(`[image ${width}x${height} -> ${target}]`));
|
|
2349
2594
|
} catch (err) {
|
|
2350
2595
|
this.problem(`Could not write ${target}: ${errorText(err)}`);
|
|
@@ -2462,6 +2707,8 @@ same everywhere.
|
|
|
2462
2707
|
|
|
2463
2708
|
Subcommands
|
|
2464
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\`
|
|
2465
2712
|
|
|
2466
2713
|
Options
|
|
2467
2714
|
--no-tests do not run the file's \`test_*\` functions, which
|
|
@@ -2480,7 +2727,8 @@ everything PLL says about the run goes to stderr.
|
|
|
2480
2727
|
Exit codes
|
|
2481
2728
|
0 ran, and any tests passed
|
|
2482
2729
|
1 the program raised, or was stopped
|
|
2483
|
-
2 level checks found problems,
|
|
2730
|
+
2 level checks found problems, or a bundle could not be loaded,
|
|
2731
|
+
so it was not run
|
|
2484
2732
|
3 a test failed
|
|
2485
2733
|
64 bad usage, or Python could not start
|
|
2486
2734
|
130 a second Ctrl+C gave up waiting for it to stop
|
|
@@ -2553,17 +2801,24 @@ function parseArgs2(argv) {
|
|
|
2553
2801
|
}
|
|
2554
2802
|
function isFile(at) {
|
|
2555
2803
|
try {
|
|
2556
|
-
return !
|
|
2804
|
+
return !fs9.statSync(at).isDirectory();
|
|
2557
2805
|
} catch {
|
|
2558
2806
|
return false;
|
|
2559
2807
|
}
|
|
2560
2808
|
}
|
|
2809
|
+
var SUBCOMMANDS = {
|
|
2810
|
+
examplar: runExamplar,
|
|
2811
|
+
bundle: runBundle
|
|
2812
|
+
};
|
|
2561
2813
|
async function main(argv) {
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2814
|
+
for (const [name, run] of Object.entries(SUBCOMMANDS)) {
|
|
2815
|
+
const at = subcommandIndex(argv, name, OPTIONS);
|
|
2816
|
+
if (at === -1) {
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2564
2819
|
const leading = parseCommandLine(argv.slice(0, at), LEADING_OPTIONS);
|
|
2565
2820
|
if ("error" in leading) {
|
|
2566
|
-
process.stderr.write(`pll: ${leading.error} before
|
|
2821
|
+
process.stderr.write(`pll: ${leading.error} before ${name}
|
|
2567
2822
|
${USAGE_HINT}
|
|
2568
2823
|
`);
|
|
2569
2824
|
return EXIT.usage;
|
|
@@ -2575,7 +2830,7 @@ ${USAGE_HINT}
|
|
|
2575
2830
|
const runtime2 = createCliRuntime();
|
|
2576
2831
|
sayPackageNotes(runtime2, view2);
|
|
2577
2832
|
try {
|
|
2578
|
-
return await
|
|
2833
|
+
return await run(runtime2, view2, argv.slice(at + 1));
|
|
2579
2834
|
} catch (err) {
|
|
2580
2835
|
view2.problem(`pll: ${errorText(err)}`);
|
|
2581
2836
|
return EXIT.usage;
|
|
@@ -2589,7 +2844,7 @@ ${USAGE_HINT}
|
|
|
2589
2844
|
return EXIT.ok;
|
|
2590
2845
|
}
|
|
2591
2846
|
if (args.version) {
|
|
2592
|
-
process.stdout.write("0.9.
|
|
2847
|
+
process.stdout.write("0.9.2\n");
|
|
2593
2848
|
return EXIT.ok;
|
|
2594
2849
|
}
|
|
2595
2850
|
if (args.error !== void 0) {
|
|
@@ -2629,7 +2884,7 @@ ${USAGE_HINT}
|
|
|
2629
2884
|
process.stderr.on("error", onBrokenPipe);
|
|
2630
2885
|
try {
|
|
2631
2886
|
return await runFile(runtime, view, {
|
|
2632
|
-
file:
|
|
2887
|
+
file: path9.resolve(args.file),
|
|
2633
2888
|
runTests: args.runTests,
|
|
2634
2889
|
stopRequested: () => interrupted
|
|
2635
2890
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pll-python",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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": {
|