(core) Remove REPL code

Summary: Remove repl.py, REPLTab.js, some wiring code, CSS, and a test in testscript.json.

Test Plan: NA

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2923
This commit is contained in:
Alex Hall 2021-07-20 14:52:21 +02:00
parent 67aca9ccf6
commit 1f6e693b6e
12 changed files with 1 additions and 757 deletions

View File

@ -12,7 +12,6 @@ import {DocComm, DocUserAction} from 'app/client/components/DocComm';
import * as DocConfigTab from 'app/client/components/DocConfigTab'; import * as DocConfigTab from 'app/client/components/DocConfigTab';
import * as GridView from 'app/client/components/GridView'; import * as GridView from 'app/client/components/GridView';
import {Importer} from 'app/client/components/Importer'; import {Importer} from 'app/client/components/Importer';
import * as REPLTab from 'app/client/components/REPLTab';
import {ActionGroupWithCursorPos, UndoStack} from 'app/client/components/UndoStack'; import {ActionGroupWithCursorPos, UndoStack} from 'app/client/components/UndoStack';
import {ViewLayout} from 'app/client/components/ViewLayout'; import {ViewLayout} from 'app/client/components/ViewLayout';
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals'; import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
@ -80,7 +79,7 @@ export interface TabOptions {
category?: any; category?: any;
} }
const RightPanelTool = StringUnion("none", "docHistory", "validations", "repl"); const RightPanelTool = StringUnion("none", "docHistory", "validations");
export interface IExtraTool { export interface IExtraTool {
icon: IconName; icon: IconName;
@ -248,13 +247,6 @@ export class GristDoc extends DisposableWithEvents {
this.autoDispose(DocConfigTab.create({gristDoc: this})); this.autoDispose(DocConfigTab.create({gristDoc: this}));
const replTab = this.autoDispose(REPLTab.create(this));
this.autoDispose(this.addOptionsTab(
'REPL', dom('span.glyphicon.glyphicon-console'),
replTab.buildConfigDomObj(),
{ hideSearchContent: true }
));
this.rightPanelTool = Computed.create(this, (use) => this._getToolContent(use(this._rightPanelTool))); this.rightPanelTool = Computed.create(this, (use) => this._getToolContent(use(this._rightPanelTool)));
this.comparison = options.comparison || null; this.comparison = options.comparison || null;
@ -738,10 +730,6 @@ export class GristDoc extends DisposableWithEvents {
const content = this._rightPanelTabs.get("Validate Data"); const content = this._rightPanelTabs.get("Validate Data");
return content ? {icon: 'Validation', label: 'Validation Rules', content} : null; return content ? {icon: 'Validation', label: 'Validation Rules', content} : null;
} }
case 'repl': {
const content = this._rightPanelTabs.get("REPL");
return content ? {icon: 'Repl', label: 'REPL', content} : null;
}
case 'none': case 'none':
default: { default: {
return null; return null;

View File

@ -1,106 +0,0 @@
.repl-container {
padding: 0 10px;
}
.repl-text {
cursor: pointer;
display: inline-block;
font-family: monospace;
white-space: pre;
tab-size: 4;
-moz-tab-size: 4;
-o-tab-size: 4;
word-wrap: break-word;
}
.repl-field {
display: inline-block;
}
.repl-error {
color: #D00;
}
.repl-text_line:hover {
background-color: #E5E5E5;
}
.re-eval_line_button {
cursor: pointer;
float: right;
text-align: center;
width: 15px;
color: #808080;
}
.erase_line_button {
cursor: pointer;
float: right;
text-align: center;
width: 15px;
color: #808080;
}
.re-eval_line_button:hover {
color: #000000;
}
.erase_line_button:hover {
color: #000000;
}
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pointer_group {
cursor: pointer;
display: inline-block;
width: 25px;
}
.pointer {
font-family: monospace;
font-weight: bold;
}
.repl-newline {
font-family: monospace;
font-weight: bold;
padding: 2px 10px 15px 10px;
}
.repl-cursor_editor {
display: inline;
}
.repl-text_editor {
position: absolute;
padding: 2px 0 0 0;
min-width: 10px;
min-height: 100%;
width: 100%;
height: 100%;
border: none;
resize: none;
box-shadow: none;
outline: none;
background: transparent;
z-index: 10;
overflow: hidden;
}
.repl-content_measure {
position: absolute;
left: 0;
top: 0;
padding-top: 2px;
padding-right: 1em;
border: none;
visibility: hidden;
overflow: visible;
}
.formula-text {
font-family: monospace;
tab-size: 4;
-moz-tab-size: 4;
-o-tab-size: 4;
}

View File

@ -1,318 +0,0 @@
/**
* A Tab that contains a REPL.
* The REPL allows the user to write snippets of python code and see the results of evaluating
* them. In particular, the REPL has access to the usercode module, so they can see the results
* of quick operations on their data.
* The REPL supports evaluation of code, removal of lines from history, and re-computation
* and editing of older lines.
*/
var kd = require('../lib/koDom');
var ko = require('knockout');
var dom = require('../lib/dom');
var Base = require('./Base');
var commands = require('./commands');
var NEW_LINE = -1;
/**
* Hard tab used instead of soft tabs, as soft-tabs would require a lot of additional
* editor logic (partial-width tabs, backspacing a tab, ...) for
* which we may want to eventually use a 3rd-party library for in addition to syntax highlighting, etc
*/
var INDENT_STR = "\t";
function REPLTab(gristDoc) {
Base.call(this, gristDoc);
this.replHist = gristDoc.docModel.replHist.createAllRowsModel("id");
this.docData = gristDoc.docData;
this.editingIndex = ko.observable(null);
this.histIndex = ko.observable(this.replHist.peekLength);
this.editorActive = ko.observable(false);
this.numLines = ko.observable(0);
this.row = null;
this._contentSizer = ko.observable('');
this._originalValue = '';
this._textInput = null;
this.commandGroup = this.autoDispose(commands.createGroup(
REPLTab.replCommands, this, this.editorActive));
}
Base.setBaseFor(REPLTab);
/**
* Editor commands for the cellEditor in the REPL Tab
* TODO: Using the command group, distinguish between "on enter" saves and "on blur" saves
* So that we can give up focus on blur
*/
REPLTab.replCommands = {
// TODO: GridView commands are activated more recently after startup.
fieldEditSave: function() {
if (!this._textInput || !this.editorActive() ||
!this._textInput.value.trim() && this.editingIndex() === NEW_LINE) { return; }
// TODO: Scroll pane does not automatically scroll down on save.
var self = this;
this.save()
.then(function(success) {
if (success) {
self.editingIndex(NEW_LINE);
self.clear();
// Refresh the history index.
self.histIndex(self.replHist.peekLength);
} else {
self.write("\n");
// Since focus is staying in the current input, increment lines.
self.numLines(self.numLines.peek()+1);
}
});
},
fieldEditCancel: function() {
this.clear();
this.editingIndex(NEW_LINE);
},
nextField: function() {
// In this case, 'nextField' (Tab) inserts a tab.
this.write(INDENT_STR);
},
historyPrevious: function() {
// Fills the editor with the code previously entered.
if (this.editingIndex() === NEW_LINE) { this.writePrev(); }
},
historyNext: function() {
// Fills the editor with the code entered after the current code.
if (this.editingIndex() === NEW_LINE) { this.writeNext(); }
}
};
/**
* Sends the entered code as an EvalCode Useraction.
* @param {Function} callback - Is called with a single argument 'success' indicating
* whether the save was successful.
*/
REPLTab.prototype.save = function(callback) {
if (!this._textInput.value.trim()) {
// If its text is cleared, remove history item.
var currentEditIndex = this.editingIndex();
this.histIndex(this.replHist.peekLength - 1);
this.editorActive(false);
return this.docData.sendAction(["RemoveRecord", "_grist_REPL_Hist", currentEditIndex]);
}
else {
// If something is entered, save value.
var rowId = this.row ? this.row.id() : null;
return this.docData.sendAction(["EvalCode", this._textInput.value, rowId]);
}
};
// Builds object with REPLTab dom builder and settings for the sidepane.
REPLTab.prototype.buildConfigDomObj = function() {
return [{
'buildDom': this.buildDom.bind(this),
'keywords': ['repl', 'console', 'python', 'code', 'terminal']
}];
};
REPLTab.prototype.buildDom = function() {
var self = this;
return dom('div',
kd.foreach(this.replHist, function(replLine) {
return dom('div.repl-container',
dom('div.repl-text_line',
kd.scope(function() { return self.editingIndex() === replLine.id(); },
function(isEditing) {
if (isEditing) {
return dom('div.field.repl-field',
kd.scope(self.numLines, function(numLines) {
return self.buildPointerGroup(numLines);
}),
self.attachEditorDom(replLine));
} else {
var numLines = replLine.code().trim().split('\n').length;
return dom('div.repl-field',
dom.on('click', function() {
// TODO: Flickering occurs on click for multiline code segments.
self.editingIndex(replLine.id());
self.focus();
}),
self.buildPointerGroup(numLines),
dom('div.repl-text',
kd.text(replLine.code)
)
);
}
}
),
dom('div.erase_line_button.unselectable', dom.on('click', function() {
self.histIndex(self.replHist.peekLength - 1);
return self.docData.sendAction(
["RemoveRecord", "_grist_REPL_Hist", replLine.id()]
);
}), '\u2A09'),
dom('div.re-eval_line_button.unselectable', dom.on('click', function() {
return self.docData.sendAction(
["EvalCode", replLine.code(), replLine.id()]
);
}), '\u27f3') // 'refresh' symbol
),
kd.maybe(replLine.outputText, function() {
return dom('div.repl-text.repl-output', kd.text(replLine.outputText));
}),
kd.maybe(replLine.errorText, function() {
return dom('div.repl-text.repl-error', kd.text(replLine.errorText));
})
);
}),
// Special bottom editor which sends actions to add new records to the REPL hist.
dom('div.repl-newline',
dom.on('click', function() {
self.editingIndex(NEW_LINE);
self.focus();
}),
dom('div.field.repl-field',
kd.scope(self.numLines, function(numLines) {
return self.buildPointerGroup(self.editingIndex() === NEW_LINE ? numLines : 1);
}),
kd.maybe(ko.pureComputed(function() { return self.editingIndex() === NEW_LINE; }),
function() { return self.attachEditorDom(null); }
)
)
)
);
};
/**
* Builds the set of pointers to the left of the code
* @param {String} code - The code for which the pointer group is to be built.
*/
REPLTab.prototype.buildPointerGroup = function(numLines) {
var pointers = [];
for (var i = 0; i < numLines; i++) {
pointers.push(dom('div.pointer', i ? '...' : '>>>'));
}
return dom('div.pointer_group.unselectable', pointers);
};
REPLTab.prototype.buildEditorDom = function() {
var self = this;
return dom('div.repl-cursor_editor',
dom('div.repl-content_measure.formula-text', kd.text(this._contentSizer)),
function() {
self._textInput = dom('textarea.repl-text_editor.formula-text',
kd.value(self.row ? self.row.code() : ""),
dom.on('focus', function() {
self.numLines(this.value.split('\n').length);
}),
dom.on('blur', function() {
if (!this._textInput || !this.editorActive()) { return; }
self.save()
.then(function(success) {
if (success) {
// If editing a new line, clear it to start fresh.
if (self.editingIndex() === NEW_LINE) { self.clear(); }
// Refresh the history index.
self.histIndex(self.replHist.peekLength);
} else {
self.write("\n");
}
self.editorActive(false);
});
}),
//Resizes the textbox whenever user writes in it.
dom.on('input', function() {
self.numLines(this.value.split('\n').length);
self.resizeElem();
}),
dom.defer(function(elem) {
self.resizeElem();
elem.focus();
// Set the cursor at the end.
var elemLen = elem.value.length;
elem.selectionStart = elemLen;
elem.selectionEnd = elemLen;
}),
dom.on('mouseup mousedown click', function(event) { event.stopPropagation(); }),
self.commandGroup.attach()
);
return self._textInput;
}
);
};
/**
* This function measures a hidden div with the same value as the textarea being edited and then resizes the textarea to match.
*/
REPLTab.prototype.resizeElem = function() {
// \u200B is a zero-width space; it is used so the textbox will expand vertically
// on newlines, but it does not add any width the string
this._contentSizer(this._textInput.value + '\u200B');
var rect = this._textInput.parentNode.childNodes[0].getBoundingClientRect();
//Allows form to expand passed its container div.
this._textInput.style.width = Math.ceil(rect.width) + 'px';
this._textInput.style.height = Math.ceil(rect.height) + 'px';
};
/**
* Appends text to the contents being edited
*/
REPLTab.prototype.write = function(text) {
this._textInput.value += text;
this.resizeElem();
};
/**
* Clears both the current text and any memory of text in the currently edited cell.
*/
REPLTab.prototype.clear = function() {
this._textInput.value = "";
this._orignalValue = "";
this.numLines(1);
this.resizeElem();
};
/**
* Restores focus to the most recent input.
*/
REPLTab.prototype.focus = function() {
if (this._textInput) {
this._textInput.focus();
this.editorActive(true);
}
};
/**
* Writes the code entered before the current code to the input.
*/
REPLTab.prototype.writePrev = function() {
this.histIndex(Math.max(this.histIndex.peek() - 1, 0));
this.clear();
if (this.replHist.at(this.histIndex.peek())) {
this.write(this.replHist.at(this.histIndex.peek()).code());
}
};
/**
* Writes the code entered after the current code to the input.
*/
REPLTab.prototype.writeNext = function() {
this.histIndex(Math.min(this.histIndex() + 1, this.replHist.peekLength));
this.clear();
if (this.histIndex.peek() < this.replHist.peekLength) {
this.write(this.replHist.at(this.histIndex.peek()).code());
}
};
/**
* This function is called in the DOM element where an editor is desired.
* It attaches to as a child of that element with that elements value as default or whatever is set as an override value.
*/
REPLTab.prototype.attachEditorDom = function(row) {
var self = this;
self.row = row;
self._originalValue = self.row ? self.row.code() : "";
return self.buildEditorDom();
};
module.exports = REPLTab;

View File

@ -6,7 +6,6 @@ declare module "app/client/components/DocConfigTab";
declare module "app/client/components/GridView"; declare module "app/client/components/GridView";
declare module "app/client/components/Layout"; declare module "app/client/components/Layout";
declare module "app/client/components/LayoutEditor"; declare module "app/client/components/LayoutEditor";
declare module "app/client/components/REPLTab";
declare module "app/client/components/commandList"; declare module "app/client/components/commandList";
declare module "app/client/lib/Mousetrap"; declare module "app/client/lib/Mousetrap";
declare module "app/client/lib/browserGlobals"; declare module "app/client/lib/browserGlobals";

View File

@ -28,7 +28,6 @@ import {ACLRuleRec, createACLRuleRec} from 'app/client/models/entities/ACLRuleRe
import {ColumnRec, createColumnRec} from 'app/client/models/entities/ColumnRec'; import {ColumnRec, createColumnRec} from 'app/client/models/entities/ColumnRec';
import {createDocInfoRec, DocInfoRec} from 'app/client/models/entities/DocInfoRec'; import {createDocInfoRec, DocInfoRec} from 'app/client/models/entities/DocInfoRec';
import {createPageRec, PageRec} from 'app/client/models/entities/PageRec'; import {createPageRec, PageRec} from 'app/client/models/entities/PageRec';
import {createREPLRec, REPLRec} from 'app/client/models/entities/REPLRec';
import {createTabBarRec, TabBarRec} from 'app/client/models/entities/TabBarRec'; import {createTabBarRec, TabBarRec} from 'app/client/models/entities/TabBarRec';
import {createTableRec, TableRec} from 'app/client/models/entities/TableRec'; import {createTableRec, TableRec} from 'app/client/models/entities/TableRec';
import {createTableViewRec, TableViewRec} from 'app/client/models/entities/TableViewRec'; import {createTableViewRec, TableViewRec} from 'app/client/models/entities/TableViewRec';
@ -42,7 +41,6 @@ import {createViewSectionRec, ViewSectionRec} from 'app/client/models/entities/V
export {ColumnRec} from 'app/client/models/entities/ColumnRec'; export {ColumnRec} from 'app/client/models/entities/ColumnRec';
export {DocInfoRec} from 'app/client/models/entities/DocInfoRec'; export {DocInfoRec} from 'app/client/models/entities/DocInfoRec';
export {PageRec} from 'app/client/models/entities/PageRec'; export {PageRec} from 'app/client/models/entities/PageRec';
export {REPLRec} from 'app/client/models/entities/REPLRec';
export {TabBarRec} from 'app/client/models/entities/TabBarRec'; export {TabBarRec} from 'app/client/models/entities/TabBarRec';
export {TableRec} from 'app/client/models/entities/TableRec'; export {TableRec} from 'app/client/models/entities/TableRec';
export {TableViewRec} from 'app/client/models/entities/TableViewRec'; export {TableViewRec} from 'app/client/models/entities/TableViewRec';
@ -110,7 +108,6 @@ export class DocModel {
public tableViews: MTM<TableViewRec> = this._metaTableModel("_grist_TableViews", createTableViewRec); public tableViews: MTM<TableViewRec> = this._metaTableModel("_grist_TableViews", createTableViewRec);
public tabBar: MTM<TabBarRec> = this._metaTableModel("_grist_TabBar", createTabBarRec); public tabBar: MTM<TabBarRec> = this._metaTableModel("_grist_TabBar", createTabBarRec);
public validations: MTM<ValidationRec> = this._metaTableModel("_grist_Validations", createValidationRec); public validations: MTM<ValidationRec> = this._metaTableModel("_grist_Validations", createValidationRec);
public replHist: MTM<REPLRec> = this._metaTableModel("_grist_REPL_Hist", createREPLRec);
public pages: MTM<PageRec> = this._metaTableModel("_grist_Pages", createPageRec); public pages: MTM<PageRec> = this._metaTableModel("_grist_Pages", createPageRec);
public rules: MTM<ACLRuleRec> = this._metaTableModel("_grist_ACLRules", createACLRuleRec); public rules: MTM<ACLRuleRec> = this._metaTableModel("_grist_ACLRules", createACLRuleRec);

View File

@ -1,8 +0,0 @@
import {DocModel, IRowModel} from 'app/client/models/DocModel';
// Record of input code and output text and error info for REPL.
export type REPLRec = IRowModel<"_grist_REPL_Hist">
export function createREPLRec(this: REPLRec, docModel: DocModel): void {
// no extra fields
}

View File

@ -54,12 +54,6 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
cssPageLink(cssPageIcon('Validation'), cssLinkText('Validate Data'), testId('validate'), cssPageLink(cssPageIcon('Validation'), cssLinkText('Validate Data'), testId('validate'),
dom.on('click', () => gristDoc.showTool('validations')))) dom.on('click', () => gristDoc.showTool('validations'))))
), ),
// TODO: polish repl and add it back.
dom.maybe((use) => use(gristDoc.app.features).replTool, () =>
cssPageEntry(
cssPageLink(cssPageIcon('Repl'), cssLinkText('REPL'), testId('repl'),
dom.on('click', () => gristDoc.showTool('repl'))))
),
cssPageEntry( cssPageEntry(
cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'code'), cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'code'),
cssPageLink(cssPageIcon('Code'), cssPageLink(cssPageIcon('Code'),

View File

@ -25,6 +25,5 @@ export interface ISupportedFeatures {
// Plugin views, REPL, and Validations all need work, but are exposed here to allow existing // Plugin views, REPL, and Validations all need work, but are exposed here to allow existing
// tests to continue running. These only affect client-side code. // tests to continue running. These only affect client-side code.
customViewPlugin?: boolean; customViewPlugin?: boolean;
replTool?: boolean;
validationsTool?: boolean; validationsTool?: boolean;
} }

View File

@ -36,7 +36,6 @@ import table as table_module
from user import User # pylint:disable=wrong-import-order from user import User # pylint:disable=wrong-import-order
import useractions import useractions
import column import column
import repl
import urllib_patch # noqa imported for side effect # pylint:disable=unused-import import urllib_patch # noqa imported for side effect # pylint:disable=unused-import
log = logger.Logger(__name__, logger.INFO) log = logger.Logger(__name__, logger.INFO)
@ -232,9 +231,6 @@ class Engine(object):
# A flag for when a useraction causes a schema change, to verify consistency afterwards. # A flag for when a useraction causes a schema change, to verify consistency afterwards.
self._schema_updated = False self._schema_updated = False
# Locals dict for recently executed code in the REPL
self._repl = repl.REPLInterpreter()
# Stores an exception representing the first unevaluated cell met while recomputing the # Stores an exception representing the first unevaluated cell met while recomputing the
# current cell. # current cell.
self._cell_required_error = None self._cell_required_error = None
@ -976,11 +972,6 @@ class Engine(object):
for (col_obj, values) in cols} for (col_obj, values) in cols}
) )
def eval_user_code(self, src):
ret = self._repl.runsource(src)
self.gencode.usercode.__dict__.update(self._repl.locals)
return ret
def invalidate_records(self, table_id, row_ids=depend.ALL_ROWS, col_ids=None, def invalidate_records(self, table_id, row_ids=depend.ALL_ROWS, col_ids=None,
data_cols_to_recompute=frozenset()): data_cols_to_recompute=frozenset()):
""" """
@ -1017,8 +1008,6 @@ class Engine(object):
def rebuild_usercode(self): def rebuild_usercode(self):
""" """
Compiles the usercode from the schema, and updates all tables and columns to match. Compiles the usercode from the schema, and updates all tables and columns to match.
Also, keeps the locals in the repl in sync with the user code, so that the repl has access to
usercode and vice-versa.
""" """
self.gencode.make_module(self.schema) self.gencode.make_module(self.schema)
@ -1040,7 +1029,6 @@ class Engine(object):
for table_id, table in six.iteritems(old_tables): for table_id, table in six.iteritems(old_tables):
if table_id not in self.tables: if table_id not in self.tables:
self._update_table_model(table, None) self._update_table_model(table, None)
self._repl.locals.pop(table_id, None)
# Update docmodel with references to the updated metadata tables. # Update docmodel with references to the updated metadata tables.
self.docmodel.update_tables() self.docmodel.update_tables()
@ -1048,11 +1036,6 @@ class Engine(object):
# Set flag to rebuild dependencies of trigger columns after any potential renames, etc. # Set flag to rebuild dependencies of trigger columns after any potential renames, etc.
self.trigger_columns_changed() self.trigger_columns_changed()
# The order here is important to make sure that when we update the usercode,
# we don't overwrite with outdated usercode entries
self._repl.locals.update(self.gencode.usercode.__dict__)
self.gencode.usercode.__dict__.update(self._repl.locals)
# Update the context used for autocompletions. # Update the context used for autocompletions.
self._autocomplete_context = AutocompleteContext(self.gencode.usercode.__dict__) self._autocomplete_context = AutocompleteContext(self.gencode.usercode.__dict__)

View File

@ -1,91 +0,0 @@
"""
This module implements an interpreter for a REPL. It subclasses Python's
code.InteractiveInterpreter class, implementing most of its methods, but with
slight changes in order to be convenient for Grist's purposes
"""
import code
import sys
from collections import namedtuple
import six
SUCCESS = 0
INCOMPLETE = 1
ERROR = 2
EvalTuple = namedtuple("EvalTuple", ("output", "error", "status"))
#pylint: disable=exec-used, bare-except
class REPLInterpreter(code.InteractiveInterpreter):
def __init__(self):
code.InteractiveInterpreter.__init__(self)
self.error_text = ""
def runsource(self, source, filename="<input>", symbol="single"):
"""
Compiles and executes source. Returns an EvalTuple with a status
INCOMPLETE if the code is incomplete,
ERROR if it encountered a compilation or run-time error,
SUCCESS otherwise.
an output, which gives all of the output of the user's program
(with stderr piped to stdout, essentially, though mock-file objects are used)
an error, which reports a syntax error at compilation or a runtime error with a
Traceback.
"""
old_stdout = sys.stdout
old_stderr = sys.stderr
user_output = six.StringIO()
self.error_text = ""
try:
code = self.compile(source, filename, symbol)
except (OverflowError, SyntaxError, ValueError):
self.showsyntaxerror(filename)
status = ERROR
else:
status = INCOMPLETE if code is None else SUCCESS
if status == SUCCESS:
try:
# We use temproray variables to access stdio/stdout
# to make sure the client can't do funky things
# like get/set attr and have that hurt us
sys.stdout = user_output
sys.stderr = user_output
exec(code, self.locals)
except:
# bare except to catch absolutely all things the user can throw
self.showtraceback()
status = ERROR
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr
program_output = user_output.getvalue()
try:
user_output.close()
except:
pass
return EvalTuple(program_output, self.error_text, status)
def write(self, txt):
"""
Used by showsyntaxerror and showtraceback
"""
self.error_text += txt
def runcode(self, code):
"""
This would normally do the part of runsource after compiling the code, but doesn't quite
make sense as its own function for our purposes because it couldn't support an INCOMPLETE
return value, etc. We explicitly hide it here to make sure the base class's version isn't
called by accident.
"""
raise NotImplementedError("REPLInterpreter.runcode not implemented, use runsource instead")

View File

@ -2709,167 +2709,6 @@
}] }]
] ]
}, { }, {
//------------------------------------------------------------------------
"TEST_CASE" : "eval_code",
//------------------------------------------------------------------------
"BODY": [
["LOAD_SAMPLE", "basic"],
["APPLY", {
"USER_ACTIONS": [
// Basic tests
["EvalCode", "print('cats')", null],
["EvalCode", "2 * 3 - 1 // 7 + 4", null],
// Exception
["EvalCode", "raise Exception('everything broke')", null],
// Incomplete structure
["EvalCode", "for x in range(1, 100):", null],
// Re-evaluation
["EvalCode", "print('cats')", 1],
["EvalCode", "print('dogs')", 1],
// Function definition
["EvalCode", "def foo(x):\n\treturn x * 10\n", null],
["EvalCode", "foo(10)", null]
],
"ACTIONS" : {
"stored" : [
["AddRecord", "_grist_REPL_Hist", 1,
{"code": "print('cats')", "errorText": "", "outputText": "cats\n"}],
["AddRecord", "_grist_REPL_Hist", 2,
{"code": "2 * 3 - 1 // 7 + 4", "errorText": "", "outputText": "10\n"}],
["AddRecord", "_grist_REPL_Hist", 3,
{"code": "raise Exception('everything broke')", "errorText": "Traceback (most recent call last):\n File \"<input>\", line 1, in <module>\nException: everything broke\n", "outputText": ""}],
["UpdateRecord", "_grist_REPL_Hist", 1,
{"code": "print('cats')", "errorText": "", "outputText": "cats\n"}],
["UpdateRecord", "_grist_REPL_Hist", 1,
{"code": "print('dogs')", "errorText": "", "outputText": "dogs\n"}],
["AddRecord", "_grist_REPL_Hist", 4,
{"code": "def foo(x):\n\treturn x * 10\n", "errorText": "", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 5,
{"code": "foo(10)", "errorText": "", "outputText": "100\n"}]
],
"direct": [true, true, true, true, true, true, true],
"undo" : [
["RemoveRecord", "_grist_REPL_Hist", 1],
["RemoveRecord", "_grist_REPL_Hist", 2],
["RemoveRecord", "_grist_REPL_Hist", 3],
["UpdateRecord", "_grist_REPL_Hist", 1,
{"code": "print('cats')", "errorText": "", "outputText": "cats\n"}],
["UpdateRecord", "_grist_REPL_Hist", 1,
{"code": "print('cats')", "errorText": "", "outputText": "cats\n"}],
["RemoveRecord", "_grist_REPL_Hist", 4],
["RemoveRecord", "_grist_REPL_Hist", 5]
],
"retValue" : [true,true,true,false,true,true,true,true]
}
}],
["APPLY", {
"USER_ACTIONS": [
// Access to usercode before and after re-generation
["EvalCode", "list(map(str, Students.all.firstName))", null],
["UpdateRecord", "Students", 1, {"firstName": "2e6"}],
["ModifyColumn", "Students", "firstName", { "type" : "Numeric" }],
["EvalCode", "list(Students.all.firstName)", 6],
// Make sure that other tables are still usable as well after a schema change.
["EvalCode", "len(Schools.all)", 6]
],
"ACTIONS": {
"stored": [
["AddRecord", "_grist_REPL_Hist", 6,
{"code": "list(map(str, Students.all.firstName))", "errorText": "", "outputText": "['Barack', 'George W', 'Bill', 'George H', 'Ronald', 'Jimmy', 'Gerald']\n"}],
["UpdateRecord", "Students", 1, {"firstName": "2e6"}],
["ModifyColumn", "Students", "firstName", {"type": "Numeric"}],
["UpdateRecord", "Students", 1, {"firstName": 2e6}],
["UpdateRecord", "_grist_Tables_column", 1, {"type": "Numeric"}],
["UpdateRecord", "_grist_REPL_Hist", 6, {"code": "list(Students.all.firstName)",
"errorText": "",
"outputText": "[2000000.0, AltText('George W'), AltText('Bill'), AltText('George H'), AltText('Ronald'), AltText('Jimmy'), AltText('Gerald')]\n"}],
["UpdateRecord", "_grist_REPL_Hist", 6,
{"code": "len(Schools.all)", "errorText": "", "outputText": "6\n"}],
["BulkUpdateRecord", "Students", [1, 2, 3, 4, 5, 6, 8], {
"fullName": [["E", "TypeError"], ["E","TypeError"], ["E","TypeError"],
["E","TypeError"], ["E","TypeError"], ["E","TypeError"], ["E","TypeError"]]
}],
["BulkUpdateRecord", "Students", [1, 2, 3, 4, 5, 6, 8], {
"fullNameLen": [["E","TypeError"], ["E","TypeError"], ["E","TypeError"],
["E","TypeError"], ["E","TypeError"], ["E","TypeError"], ["E","TypeError"]]
}]
],
"direct": [true, true, true, false, true, true, true, false, false],
"undo": [
["RemoveRecord", "_grist_REPL_Hist", 6],
["UpdateRecord", "Students", 1, {"firstName": "Barack"}],
["UpdateRecord", "Students", 1, {"firstName": "2e6"}],
["ModifyColumn", "Students", "firstName", {"type": "Text"}],
["UpdateRecord", "_grist_Tables_column", 1, {"type": "Text"}],
["UpdateRecord", "_grist_REPL_Hist", 6, {"code": "list(map(str, Students.all.firstName))",
"errorText": "", "outputText": "['Barack', 'George W', 'Bill', 'George H', 'Ronald', 'Jimmy', 'Gerald']\n"}],
["UpdateRecord", "_grist_REPL_Hist", 6, {"code": "list(Students.all.firstName)",
"errorText": "",
"outputText": "[2000000.0, AltText('George W'), AltText('Bill'), AltText('George H'), AltText('Ronald'), AltText('Jimmy'), AltText('Gerald')]\n"}],
["BulkUpdateRecord", "Students", [1, 2, 3, 4, 5, 6, 8],
{"fullName": ["Barack Obama", "George W Bush", "Bill Clinton", "George H Bush", "Ronald Reagan", "Jimmy Carter", "Gerald Ford"]}],
["BulkUpdateRecord", "Students", [1, 2, 3, 4, 5, 6, 8],
{"fullNameLen": [12, 13, 12, 13, 13, 12, 11]}]
],
"retValue": [true,null,null,true,true]
}
}],
["APPLY", {
"USER_ACTIONS": [
// Syntax Error
["EvalCode", "not correct c", null],
// Other continuations
["EvalCode", "map(filter, ", null],
["EvalCode", "[1,2,3,", null],
// sys.exit
["EvalCode", "import sys", null],
["EvalCode", "sys.exit(0)", null],
// User reassignment of sys.stdout/sys.stderr
["EvalCode", "sys.stdout = None", null],
["EvalCode", "setattr(sys.stdout, 'getvalue', lambda : 2)", null],
["EvalCode", "def foo():\n global stdout\n exec('stdout = 2')\n", null],
["EvalCode", "setattr(sys.stderr, 'close', foo)", null]
],
"ACTIONS": {
"stored": [
["AddRecord", "_grist_REPL_Hist", 7,
{"code": "not correct c", "errorText": " File \"<input>\", line 1\n not correct c\n ^\nSyntaxError: invalid syntax\n", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 8,
{"code": "import sys", "errorText": "", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 9,
{"code": "sys.exit(0)", "errorText": "Traceback (most recent call last):\n File \"<input>\", line 1, in <module>\nSystemExit: 0\n", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 10,
{"code": "sys.stdout = None", "errorText": "", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 11,
{"code": "setattr(sys.stdout, 'getvalue', lambda : 2)", "errorText": "", "outputText": 2}],
["AddRecord", "_grist_REPL_Hist", 12,
{"code": "def foo():\n global stdout\n exec('stdout = 2')\n", "errorText": "", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 13,
{"code": "setattr(sys.stderr, 'close', foo)", "errorText": "", "outputText": ""}]
],
"direct": [true, true, true, true, true, true, true],
"undo": [
["RemoveRecord", "_grist_REPL_Hist", 7],
["RemoveRecord", "_grist_REPL_Hist", 8],
["RemoveRecord", "_grist_REPL_Hist", 9],
["RemoveRecord", "_grist_REPL_Hist", 10],
["RemoveRecord", "_grist_REPL_Hist", 11],
["RemoveRecord", "_grist_REPL_Hist", 12],
["RemoveRecord", "_grist_REPL_Hist", 13]
],
"retValue" : [true,false,false,true,true,true,true,true,true ]
}
}]
]
}, {
//------------------------------------------------------------------------ //------------------------------------------------------------------------
"TEST_CASE" : "reserved_keywords", "TEST_CASE" : "reserved_keywords",
//------------------------------------------------------------------------ //------------------------------------------------------------------------

View File

@ -17,7 +17,6 @@ import schema
from schema import RecalcWhen from schema import RecalcWhen
import summary import summary
import import_actions import import_actions
import repl
import textbuilder import textbuilder
import usertypes import usertypes
import treeview import treeview
@ -239,37 +238,6 @@ class UserActions(object):
""" """
pass pass
#--------------------------------------
# User Actions on usercode
#--------------------------------------
@useraction
def EvalCode(self, code, row_id):
"""
Evaluates code in the REPL.
a return value of false indicates that the user's code was incomplete
(and in this case, we do not create any docactions)
otherwise, we either add a new record to the REPL_hist (row_id=null) or Update an existing
record (row_id=num) with the results of evaluating the code.
"""
evaluation = self._engine.eval_user_code(code)
if evaluation.status == repl.INCOMPLETE:
return False
hist = self._engine.tables["_grist_REPL_Hist"]
record = { "code" : code, "outputText" : evaluation.output, "errorText" : evaluation.error }
if row_id is None:
# This is a new evaluation, append it to the REPL history
action = actions.AddRecord(hist.table_id, hist.next_row_id(), record)
else:
# This is a re-evaluation, update the old retValue
action = actions.UpdateRecord(hist.table_id, row_id, record)
self._do_doc_action(action)
return True
#---------------------------------------- #----------------------------------------
# User actions on records. # User actions on records.
#---------------------------------------- #----------------------------------------