pll-python 0.1.2 → 0.2.0

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 (3) hide show
  1. package/cli.cjs +89 -23
  2. package/package.json +1 -1
  3. package/worker.cjs +24 -781
package/cli.cjs CHANGED
@@ -252,7 +252,17 @@ function userTracebackFrames(traceback) {
252
252
  }
253
253
  return frames;
254
254
  }
255
- function parsePythonError(rawTraceback) {
255
+ function sourceIndent(source, lineNumber) {
256
+ if (!source) {
257
+ return 0;
258
+ }
259
+ const line = source.split(/\r?\n/)[lineNumber - 1];
260
+ if (line === void 0) {
261
+ return 0;
262
+ }
263
+ return line.length - line.trimStart().length;
264
+ }
265
+ function parsePythonError(rawTraceback, source) {
256
266
  const traceback = rawTraceback.trimEnd();
257
267
  const lines = traceback.split(/\r?\n/);
258
268
  let errorType = "Error";
@@ -286,10 +296,11 @@ function parsePythonError(rawTraceback) {
286
296
  if (lineNumber !== null) {
287
297
  for (let i = 0; i < lines.length - 1; i++) {
288
298
  if (/^\s*\^+\s*$/.test(lines[i + 1] ?? "")) {
289
- const caretLine = lines[i + 1];
290
- const caretIdx = caretLine.indexOf("^");
299
+ const sourceLine = lines[i];
300
+ const caretIdx = lines[i + 1].indexOf("^");
291
301
  if (caretIdx >= 0) {
292
- column = caretIdx;
302
+ const echoed = sourceLine.length - sourceLine.trimStart().length;
303
+ column = Math.max(0, caretIdx - echoed) + sourceIndent(source, lineNumber);
293
304
  }
294
305
  }
295
306
  }
@@ -689,11 +700,11 @@ function findRuntimeFinding(source, fileName, level, parsedError) {
689
700
  // src/common/analyzers/runtimeFinding.ts
690
701
  function findingForErrorEvent(event, source, fileName, level) {
691
702
  const traceback = event.traceback || `${event.errorType}: ${event.message}`;
692
- const parsed = parsePythonError(traceback);
693
- if (parsed.lineNumber === null && event.lineNumber !== null) {
703
+ const parsed = parsePythonError(traceback, source);
704
+ if (parsed.lineNumber === null && event.lineNumber != null) {
694
705
  parsed.lineNumber = event.lineNumber;
695
706
  }
696
- if (parsed.column === null && event.column !== null) {
707
+ if (parsed.column === null && event.column != null) {
697
708
  parsed.column = event.column;
698
709
  }
699
710
  if (parsed.fileName === null && event.fileName) {
@@ -704,6 +715,11 @@ function findingForErrorEvent(event, source, fileName, level) {
704
715
 
705
716
  // src/common/pyodideRunner.ts
706
717
  var ANY_IMPORT_RE = /(^|\n)[ \t]*(import|from)[ \t]+\S/;
718
+ var PANDAS_METHOD_RE = /\.\s*to_pandas\s*\(/;
719
+ var NEEDS_PACKAGES_RE_LIST = [PANDAS_METHOD_RE];
720
+ function needsPackages(code) {
721
+ return ANY_IMPORT_RE.test(code) || NEEDS_PACKAGES_RE_LIST.some((re) => re.test(code));
722
+ }
707
723
 
708
724
  // src/cli/files.ts
709
725
  var fs = __toESM(require("node:fs/promises"));
@@ -721,8 +737,17 @@ var MOUNT_EXTENSIONS = /* @__PURE__ */ new Set([
721
737
  ".md",
722
738
  ".dat",
723
739
  ".xml",
724
- ".py"
740
+ ".py",
741
+ // Pictures, for `load_image`. `.svg` is not in BINARY_EXTENSIONS below
742
+ // because it is text, and mounting it as text is lossless.
743
+ ".svg",
744
+ ".png",
745
+ ".jpg",
746
+ ".jpeg",
747
+ ".gif",
748
+ ".webp"
725
749
  ]);
750
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
726
751
  var WRITEBACK_EXTENSIONS = /* @__PURE__ */ new Set([
727
752
  ".csv",
728
753
  ".txt",
@@ -757,9 +782,15 @@ function isMountableName(name) {
757
782
  function isWritebackName(name) {
758
783
  return isSafeBasename(name) && WRITEBACK_EXTENSIONS.has(extensionOf(name));
759
784
  }
785
+ function isBinaryMountName(name) {
786
+ return BINARY_EXTENSIONS.has(extensionOf(name));
787
+ }
760
788
  function utf8ByteLength(text) {
761
789
  return new TextEncoder().encode(text).byteLength;
762
790
  }
791
+ function contentsByteLength(contents) {
792
+ return typeof contents === "string" ? utf8ByteLength(contents) : contents.byteLength;
793
+ }
763
794
  function decodeMountContents(data) {
764
795
  if (data.byteLength > MAX_FILE_BYTES) {
765
796
  return void 0;
@@ -781,27 +812,33 @@ function selectMountableFiles(candidates) {
781
812
  if (!isMountableName(candidate.name)) {
782
813
  continue;
783
814
  }
784
- let text;
785
- if (typeof candidate.contents === "string") {
815
+ let contents;
816
+ if (isBinaryMountName(candidate.name)) {
817
+ const raw = typeof candidate.contents === "string" ? new TextEncoder().encode(candidate.contents) : candidate.contents;
818
+ if (raw.byteLength > MAX_FILE_BYTES) {
819
+ continue;
820
+ }
821
+ contents = raw;
822
+ } else if (typeof candidate.contents === "string") {
786
823
  if (utf8ByteLength(candidate.contents) > MAX_FILE_BYTES) {
787
824
  continue;
788
825
  }
789
826
  if (candidate.contents.includes("\0")) {
790
827
  continue;
791
828
  }
792
- text = candidate.contents;
829
+ contents = candidate.contents;
793
830
  } else {
794
831
  const decoded = decodeMountContents(candidate.contents);
795
832
  if (decoded === void 0) {
796
833
  continue;
797
834
  }
798
- text = decoded;
835
+ contents = decoded;
799
836
  }
800
- const size = utf8ByteLength(text);
837
+ const size = contentsByteLength(contents);
801
838
  if (total + size > MAX_TOTAL_BYTES) {
802
839
  continue;
803
840
  }
804
- selected.push({ name: candidate.name, contents: text });
841
+ selected.push({ name: candidate.name, contents });
805
842
  total += size;
806
843
  }
807
844
  return selected;
@@ -816,6 +853,9 @@ function selectWritebackFiles(files) {
816
853
  if (!isWritebackName(file.name)) {
817
854
  continue;
818
855
  }
856
+ if (typeof file.contents !== "string") {
857
+ continue;
858
+ }
819
859
  const size = utf8ByteLength(file.contents);
820
860
  if (size > MAX_FILE_BYTES || total + size > MAX_TOTAL_BYTES) {
821
861
  continue;
@@ -855,6 +895,9 @@ async function writeBackSiblingFiles(scriptPath, files) {
855
895
  const written = [];
856
896
  for (const file of selectWritebackFiles(files)) {
857
897
  try {
898
+ if (typeof file.contents !== "string") {
899
+ continue;
900
+ }
858
901
  await fs.writeFile(path.join(folder, file.name), file.contents, "utf8");
859
902
  written.push(file.name);
860
903
  } catch {
@@ -934,7 +977,7 @@ async function staticChecks(runtime, view, source, fileName, level) {
934
977
  return true;
935
978
  }
936
979
  async function ensurePackages(runtime, view, source) {
937
- if (!ANY_IMPORT_RE.test(source)) {
980
+ if (!needsPackages(source)) {
938
981
  return;
939
982
  }
940
983
  try {
@@ -1302,18 +1345,33 @@ function asPlain(value) {
1302
1345
  }
1303
1346
  return value;
1304
1347
  }
1305
- function adaptTestCase(raw) {
1348
+ function friendlyTypeCheckMessage(message, level) {
1349
+ const marker = message.indexOf("TypeCheckError:");
1350
+ if (marker < 0) {
1351
+ return message;
1352
+ }
1353
+ const detail = message.slice(marker + "TypeCheckError:".length).trim();
1354
+ if (!detail) {
1355
+ return message;
1356
+ }
1357
+ const explanation = explainTypeCheckError(detail, null, level);
1358
+ return [explanation.headline, ...explanation.howToFix.map((line) => `- ${line}`)].join(
1359
+ "\n"
1360
+ );
1361
+ }
1362
+ function adaptTestCase(raw, level) {
1306
1363
  const row = asPlain(raw) ?? {};
1307
1364
  const line = row.line_number;
1365
+ const message = row.message == null ? null : String(row.message);
1308
1366
  return {
1309
1367
  name: String(row.name ?? ""),
1310
1368
  outcome: String(row.outcome ?? "failed"),
1311
1369
  lineNumber: typeof line === "number" ? line : null,
1312
- message: row.message == null ? null : String(row.message),
1370
+ message: message === null ? null : friendlyTypeCheckMessage(message, level),
1313
1371
  stdout: row.stdout == null ? null : String(row.stdout)
1314
1372
  };
1315
1373
  }
1316
- function deliverTestResult(result, onEvent, fileName) {
1374
+ function deliverTestResult(result, onEvent, fileName, level) {
1317
1375
  if (result.internal_error && result.error_type) {
1318
1376
  onEvent({
1319
1377
  kind: "error",
@@ -1327,7 +1385,7 @@ function deliverTestResult(result, onEvent, fileName) {
1327
1385
  onEvent({ kind: "done" });
1328
1386
  return;
1329
1387
  }
1330
- const tests = Array.isArray(result.tests) ? result.tests.map(adaptTestCase) : [];
1388
+ const tests = Array.isArray(result.tests) ? result.tests.map((row) => adaptTestCase(row, level)) : [];
1331
1389
  onEvent({
1332
1390
  kind: "testReport",
1333
1391
  fileName,
@@ -1470,7 +1528,7 @@ var WorkerPythonRuntime = class {
1470
1528
  },
1471
1529
  "testResult"
1472
1530
  );
1473
- deliverTestResult(result, onEvent, request.fileName);
1531
+ deliverTestResult(result, onEvent, request.fileName, request.level);
1474
1532
  }
1475
1533
  async staticAnalyze(request) {
1476
1534
  await this.initialize();
@@ -1690,11 +1748,12 @@ function findingLocation(finding) {
1690
1748
  if (finding.fileName === "<repl>" || finding.fileName === "<input>") {
1691
1749
  return null;
1692
1750
  }
1693
- const label = finding.column !== null ? `${finding.fileName}:${finding.lineNumber}:${finding.column + 1}` : `${finding.fileName}:${finding.lineNumber}`;
1751
+ const column = typeof finding.column === "number" && Number.isFinite(finding.column) ? finding.column : null;
1752
+ const label = column !== null ? `${finding.fileName}:${finding.lineNumber}:${column + 1}` : `${finding.fileName}:${finding.lineNumber}`;
1694
1753
  return {
1695
1754
  fileName: finding.fileName,
1696
1755
  line: finding.lineNumber,
1697
- column: finding.column,
1756
+ column,
1698
1757
  label
1699
1758
  };
1700
1759
  }
@@ -1845,6 +1904,13 @@ var CliView = class {
1845
1904
  this.problem(" " + line);
1846
1905
  }
1847
1906
  }
1907
+ const printed = (test.stdout ?? "").replace(/\s+$/, "");
1908
+ if (printed) {
1909
+ this.problem(this.dim(" output:"));
1910
+ for (const line of printed.split("\n")) {
1911
+ this.problem(this.dim(" " + line));
1912
+ }
1913
+ }
1848
1914
  }
1849
1915
  }
1850
1916
  };
@@ -1965,7 +2031,7 @@ async function main(argv) {
1965
2031
  return EXIT.ok;
1966
2032
  }
1967
2033
  if (args.version) {
1968
- process.stdout.write("0.1.2\n");
2034
+ process.stdout.write("0.2.0\n");
1969
2035
  return EXIT.ok;
1970
2036
  }
1971
2037
  if (args.error !== void 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pll-python",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
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": {