gristlabs_grist-core/app/server/lib/SafePythonComponent.ts
Alex Hall 42afb17e36 (core) Run and test imports only in Python 3, upgrade openpyxl, fix weird date handling
Summary:
Python 2 only needs to be supported for the sake of old documents and formulas. This doesn't apply to the separate sandboxes that parse files for imports. Using Python 3 only allows using newer libraries and library versions. In particular, the latest version of openpyxl doesn't support Python 2. This will also make it easier to make other similar changes in the future, such as replacing messytables with a modern library. See https://grist.slack.com/archives/C0234CPPXPA/p1661261829343999?thread_ts=1661260442.837959&cid=C0234CPPXPA

The latest openpyxl is better at handling a particular edge case with broken dates in Excel, but still doesn't quite do what we want, so we monkeypatch it. Discussion: https://grist.slack.com/archives/C02EGJ1FUCV/p1661440851911869?thread_ts=1661154219.515549&cid=C02EGJ1FUCV

Setting `preferredPythonVersion` to '3' in SafePythonComponent ensures that JS always creates import sandboxes that use Python 3. Within Python, a module used by all imports will raise an error in Python 2. Python unit tests of imports are now only run in Python 3, using the `load_tests` protocol of `unittest`.

Test Plan: Mostly existing tests. Added another strange date to the Excel fixture.

Reviewers: dsagal

Reviewed By: dsagal

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3606
2022-09-02 16:27:34 +02:00

72 lines
2.5 KiB
TypeScript

import {LocalPlugin} from 'app/common/plugin';
import {BaseComponent, createRpcLogger} from 'app/common/PluginInstance';
import {GristServer} from 'app/server/lib/GristServer';
import {ISandbox} from 'app/server/lib/ISandbox';
import log from 'app/server/lib/log';
import {IMsgCustom, IMsgRpcCall} from 'grain-rpc';
// TODO safePython component should be able to call other components function
// TODO calling a function on safePython component with a name that was not register chould fail
// gracefully.
/**
* The safePython component used by a PluginInstance.
*
* It uses `NSandbox` implementation of rpc for calling methods within the sandbox.
*/
export class SafePythonComponent extends BaseComponent {
private _sandbox?: ISandbox;
private _logMeta: log.ILogMeta;
// safe python component does not need pluginInstance.rpc because it is not possible to forward
// calls to other component from within python
constructor(_localPlugin: LocalPlugin,
private _tmpDir: string,
docName: string, private _server: GristServer,
rpcLogger = createRpcLogger(log, `PLUGIN ${_localPlugin.id} SafePython:`)) {
super(_localPlugin.manifest, rpcLogger);
this._logMeta = {plugin: _localPlugin.id, docId: docName};
}
/**
* `SafePythonComponent` activation creates the Sandbox. Throws if the plugin has no `safePyton`
* components.
*/
protected async activateImplementation(): Promise<void> {
if (!this._tmpDir) {
throw new Error("Sanbox should have a tmpDir");
}
this._sandbox = this._server.create.NSandbox({
importMount: this._tmpDir,
logTimes: true,
logMeta: this._logMeta,
preferredPythonVersion: '3',
});
}
protected async deactivateImplementation(): Promise<void> {
log.info('SafePython deactivating ...');
if (!this._sandbox) {
log.info(' sandbox is undefined');
}
if (this._sandbox) {
await this._sandbox.shutdown();
log.info('SafePython done deactivating the sandbox');
delete this._sandbox;
}
}
protected doForwardCall(c: IMsgRpcCall): Promise<any> {
if (!this._sandbox) { throw new Error("Component should have be activated"); }
const {meth, iface, args} = c;
const funcName = meth === "invoke" ? iface : iface + "." + meth;
return this._sandbox.pyCall(funcName, ...args);
}
protected doForwardMessage(c: IMsgCustom): Promise<any> {
throw new Error("Forwarding messages to python sandbox is not supported");
}
}