CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/project/formatters/python-format.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { writeFile, readFile, unlink } from "fs";
7
import { file } from "tmp";
8
import { callback } from "awaiting";
9
import { spawn } from "child_process";
10
11
interface ParserOptions {
12
parser?: string;
13
tabWidth?: number;
14
useTabs?: boolean;
15
util?: string;
16
}
17
18
function close(proc, cb): void {
19
proc.on("close", (code) => cb(undefined, code));
20
}
21
22
// TODO: diversify this via options to support autopep8, black (requires python 3.6), and others...
23
24
function yapf(input_path) {
25
return spawn("yapf", ["-i", input_path]);
26
}
27
28
export async function python_format(
29
input: string,
30
options: ParserOptions,
31
logger: any
32
): Promise<string> {
33
// create input temp file
34
const input_path: string = await callback(file);
35
try {
36
await callback(writeFile, input_path, input);
37
38
// spawn the python formatter
39
const util = options.util || "yapf";
40
41
if (util !== "yapf") {
42
throw new Error(
43
"This project only supports 'yapf' for formatting Python"
44
);
45
}
46
47
const py_formatter = yapf(input_path);
48
49
py_formatter.on("error", (err) => {
50
// ATTN do not throw an error here, because this is triggered by the subprocess!
51
logger.debug(
52
`Formatting utility exited with error no ${(err as any).errno}`
53
);
54
});
55
56
// stdout/err capture
57
let stdout: string = "";
58
let stderr: string = "";
59
// read data as it is produced.
60
py_formatter.stdout.on("data", (data) => (stdout += data.toString()));
61
py_formatter.stderr.on("data", (data) => (stderr += data.toString()));
62
// wait for subprocess to close.
63
const code = await callback(close, py_formatter);
64
// only last line
65
// stdout = last_line(stdout);
66
if (code) {
67
if (code === -2) {
68
// ENOENT
69
throw new Error(`Formatting utility "${util}" is not installed`);
70
}
71
const err_msg = `Python formatter "${util}" exited with code ${code}:${
72
stdout.trim() ? "\n" + stdout.trim() : ""
73
}\n${stderr.trim()}\n${addContext(input, stderr)}'`;
74
logger.debug(`format python error: ${err_msg}`);
75
throw new Error(err_msg);
76
}
77
78
// all fine, we read from the temp file
79
const output: Buffer = await callback(readFile, input_path);
80
const s: string = output.toString("utf-8");
81
return s;
82
} finally {
83
unlink(input_path, () => {});
84
}
85
}
86
87
// This is designed to look like the context output by prettier.
88
export function addContext(input: string, stderr: string): string {
89
// the format of an error is
90
// yapf: a.py:2:27: EOL while scanning string literal
91
// and there is ABSOLUTELY NO WAY to get yapf to provide any context
92
// around the error. So we add it right here.
93
94
// Given that stderr looks like 'yapf: /tmp/tmp-35898eBshJwli6pIM.tmp:2:27: EOL while scanning string literal'
95
// figure out the line number (2 in this case), etc.
96
97
const pattern = /:([\d]+):/;
98
const match = stderr.match(pattern);
99
if (match != null && match?.[1] != null) {
100
const lineNum = parseInt(match?.[1] ?? "0");
101
102
// split input into lines so we can extract the relevant line
103
const lines = input.split("\n");
104
let n = Math.max(0, lineNum - 3);
105
const line = () => {
106
n += 1;
107
return n;
108
};
109
110
const before = lines
111
.slice(Math.max(0, lineNum - 3), lineNum - 1)
112
.map((x) => ` ${line()} | ${x}`)
113
.join("\n");
114
const at = `> ${line()} | ${lines[lineNum - 1]}`;
115
const after = lines
116
.slice(lineNum, lineNum + 2)
117
.map((x) => ` ${line()} | ${x}`)
118
.join("\n");
119
120
return `Error occurred at line ${lineNum}:
121
122
${before}
123
${at}
124
${after}`;
125
}
126
return "";
127
}
128
129