Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/project/formatters/python-format.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { writeFile, readFile, unlink } from "fs";6import { file } from "tmp";7import { callback } from "awaiting";8import { spawn } from "child_process";910interface ParserOptions {11parser?: string;12tabWidth?: number;13useTabs?: boolean;14util?: string;15}1617function close(proc, cb): void {18proc.on("close", (code) => cb(undefined, code));19}2021// TODO: diversify this via options to support autopep8, black (requires python 3.6), and others...2223function yapf(input_path) {24return spawn("yapf", ["-i", input_path]);25}2627export async function python_format(28input: string,29options: ParserOptions,30logger: any31): Promise<string> {32// create input temp file33const input_path: string = await callback(file);34try {35await callback(writeFile, input_path, input);3637// spawn the python formatter38const util = options.util || "yapf";3940if (util !== "yapf") {41throw new Error(42"This project only supports 'yapf' for formatting Python"43);44}4546const py_formatter = yapf(input_path);4748py_formatter.on("error", (err) => {49// ATTN do not throw an error here, because this is triggered by the subprocess!50logger.debug(51`Formatting utility exited with error no ${(err as any).errno}`52);53});5455// stdout/err capture56let stdout: string = "";57let stderr: string = "";58// read data as it is produced.59py_formatter.stdout.on("data", (data) => (stdout += data.toString()));60py_formatter.stderr.on("data", (data) => (stderr += data.toString()));61// wait for subprocess to close.62const code = await callback(close, py_formatter);63// only last line64// stdout = last_line(stdout);65if (code) {66if (code === -2) {67// ENOENT68throw new Error(`Formatting utility "${util}" is not installed`);69}70const err_msg = `Python formatter "${util}" exited with code ${code}:${71stdout.trim() ? "\n" + stdout.trim() : ""72}\n${stderr.trim()}\n${addContext(input, stderr)}'`;73logger.debug(`format python error: ${err_msg}`);74throw new Error(err_msg);75}7677// all fine, we read from the temp file78const output: Buffer = await callback(readFile, input_path);79const s: string = output.toString("utf-8");80return s;81} finally {82unlink(input_path, () => {});83}84}8586// This is designed to look like the context output by prettier.87export function addContext(input: string, stderr: string): string {88// the format of an error is89// yapf: a.py:2:27: EOL while scanning string literal90// and there is ABSOLUTELY NO WAY to get yapf to provide any context91// around the error. So we add it right here.9293// Given that stderr looks like 'yapf: /tmp/tmp-35898eBshJwli6pIM.tmp:2:27: EOL while scanning string literal'94// figure out the line number (2 in this case), etc.9596const pattern = /:([\d]+):/;97const match = stderr.match(pattern);98if (match != null && match?.[1] != null) {99const lineNum = parseInt(match?.[1] ?? "0");100101// split input into lines so we can extract the relevant line102const lines = input.split("\n");103let n = Math.max(0, lineNum - 3);104const line = () => {105n += 1;106return n;107};108109const before = lines110.slice(Math.max(0, lineNum - 3), lineNum - 1)111.map((x) => ` ${line()} | ${x}`)112.join("\n");113const at = `> ${line()} | ${lines[lineNum - 1]}`;114const after = lines115.slice(lineNum, lineNum + 2)116.map((x) => ` ${line()} | ${x}`)117.join("\n");118119return `Error occurred at line ${lineNum}:120121${before}122${at}123${after}`;124}125return "";126}127128129