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.
- package/README.md +45 -1
- package/cli.cjs +319 -56
- package/package.json +1 -1
- 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
|
|
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)));
|
|
@@ -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
|
-
|
|
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((
|
|
1636
|
-
watch = setInterval(() => stopRequested() &&
|
|
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
|
|
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),
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
2036
|
+
const inner = path5.join(folder, entry.name);
|
|
1806
2037
|
const group = {};
|
|
1807
|
-
for (const file of await
|
|
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
|
|
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
|
-
`${
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
1956
|
-
var
|
|
2191
|
+
var fs7 = __toESM(require("node:fs"));
|
|
2192
|
+
var path7 = __toESM(require("node:path"));
|
|
1957
2193
|
|
|
1958
2194
|
// src/desktop/pyodideRuntime.ts
|
|
1959
|
-
var
|
|
1960
|
-
var
|
|
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
|
-
|
|
2044
|
-
return this.call("
|
|
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) =>
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
2393
|
+
return path7.join(cacheRoot(), `pyodide-${version}`);
|
|
2141
2394
|
}
|
|
2142
2395
|
function pyodideDirectory() {
|
|
2143
2396
|
try {
|
|
2144
|
-
return [
|
|
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((
|
|
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
|
|
2198
|
-
var
|
|
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 =
|
|
2589
|
+
const target = path8.join(dir, `image-${this.imageCount}.svg`);
|
|
2337
2590
|
try {
|
|
2338
|
-
|
|
2339
|
-
|
|
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,
|
|
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 !
|
|
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
|
|
2555
|
-
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
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.
|
|
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": {
|