Skip to content

Commit 6ae8282

Browse files
committed
Fix new expression syntax in Python
1 parent 4b1f190 commit 6ae8282

File tree

13 files changed

+144
-111
lines changed

13 files changed

+144
-111
lines changed

packages/perspective/src/js/perspective.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2208,14 +2208,12 @@ export default function (Module) {
22082208
*/
22092209
async init(msg) {
22102210
let wasmBinary = msg.buffer;
2211-
try {
2212-
const mod = await WebAssembly.instantiate(wasmBinary);
2213-
const exports = mod.instance.exports;
2214-
const size = exports.size();
2215-
const offset = exports.offset();
2216-
const array = new Uint8Array(exports.memory.buffer);
2217-
wasmBinary = array.slice(offset, offset + size);
2218-
} catch {}
2211+
const mod = await WebAssembly.instantiate(wasmBinary);
2212+
const exports = mod.instance.exports;
2213+
const size = exports.size();
2214+
const offset = exports.offset();
2215+
const array = new Uint8Array(exports.memory.buffer);
2216+
wasmBinary = array.slice(offset, offset + size);
22192217
__MODULE__ = await __MODULE__({
22202218
wasmBinary,
22212219
wasmJSMethod: "native-wasm",

packages/perspective/src/js/perspective.node.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,12 @@ const SYNC_SERVER = new (class extends Server {
3434
init(msg) {
3535
this._loaded_promise = (async () => {
3636
let wasmBinary = await buffer;
37-
try {
38-
const mod = await WebAssembly.instantiate(wasmBinary);
39-
const exports = mod.instance.exports;
40-
const size = exports.size();
41-
const offset = exports.offset();
42-
const array = new Uint8Array(exports.memory.buffer);
43-
wasmBinary = array.slice(offset, offset + size);
44-
} catch {}
37+
const mod = await WebAssembly.instantiate(wasmBinary);
38+
const exports = mod.instance.exports;
39+
const size = exports.size();
40+
const offset = exports.offset();
41+
const array = new Uint8Array(exports.memory.buffer);
42+
wasmBinary = array.slice(offset, offset + size);
4543
const core = await load_perspective({
4644
wasmBinary,
4745
wasmJSMethod: "native-wasm",

python/perspective/perspective/table/_utils.py

Lines changed: 92 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13-
from operator import itemgetter
1413
import re
1514
from datetime import date, datetime
1615
from functools import partial
@@ -156,11 +155,54 @@ def _parse_expression_inputs(expressions):
156155
validated_expressions = []
157156
alias_map = {}
158157

159-
for expression in expressions:
160-
if type(expression) is dict:
161-
expr_raw, alias = itemgetter("expr", "name")(expression)
162-
parsed = expr_raw
163-
else:
158+
if isinstance(expressions, dict):
159+
for alias in expressions.keys():
160+
expression = expressions[alias]
161+
162+
column_id_map = {}
163+
column_name_map = {}
164+
165+
# we need to be able to modify the running_cidx inside of every call to
166+
# replacer_fn - must pass by reference unfortunately
167+
running_cidx = [0]
168+
169+
replacer_fn = partial(
170+
_replace_expression_column_name,
171+
column_name_map,
172+
column_id_map,
173+
running_cidx,
174+
)
175+
176+
parsed = expression
177+
parsed = re.sub(
178+
BOOLEAN_LITERAL_REGEX,
179+
lambda match: "True" if match.group(0) == "true" else ("False" if match.group(0) == "false" else match.group(0)),
180+
parsed,
181+
)
182+
183+
parsed = re.sub(EXPRESSION_COLUMN_NAME_REGEX, replacer_fn, parsed)
184+
parsed = re.sub(
185+
STRING_LITERAL_REGEX,
186+
lambda match: "intern({0})".format(match.group(0)),
187+
parsed,
188+
)
189+
190+
# remove the `intern()` in bucket and regex functions that take
191+
# string literal parameters. TODO this logic should be centralized
192+
# in C++ instead of being duplicated.
193+
parsed = re.sub(FUNCTION_LITERAL_REGEX, _replace_interned_param, parsed)
194+
parsed = re.sub(REPLACE_FN_REGEX, _replace_interned_param, parsed)
195+
196+
validated = [alias, expression, parsed, column_id_map]
197+
198+
if alias_map.get(alias) is not None:
199+
idx = alias_map[alias]
200+
validated_expressions[idx] = validated
201+
else:
202+
validated_expressions.append(validated)
203+
alias_map[alias] = len(validated_expressions) - 1
204+
if isinstance(expressions, list):
205+
for expression in expressions:
164206
expr_raw = expression
165207
parsed = expr_raw
166208
alias_match = re.match(ALIAS_REGEX, expr_raw)
@@ -169,49 +211,49 @@ def _parse_expression_inputs(expressions):
169211
else:
170212
alias = expr_raw
171213

172-
if '""' in expr_raw:
173-
raise ValueError("Cannot reference empty column in expression!")
174-
175-
column_id_map = {}
176-
column_name_map = {}
177-
178-
# we need to be able to modify the running_cidx inside of every call to
179-
# replacer_fn - must pass by reference unfortunately
180-
running_cidx = [0]
181-
182-
replacer_fn = partial(
183-
_replace_expression_column_name,
184-
column_name_map,
185-
column_id_map,
186-
running_cidx,
187-
)
188-
189-
parsed = re.sub(
190-
BOOLEAN_LITERAL_REGEX,
191-
lambda match: "True" if match.group(0) == "true" else ("False" if match.group(0) == "false" else match.group(0)),
192-
parsed,
193-
)
194-
195-
parsed = re.sub(EXPRESSION_COLUMN_NAME_REGEX, replacer_fn, parsed)
196-
parsed = re.sub(
197-
STRING_LITERAL_REGEX,
198-
lambda match: "intern({0})".format(match.group(0)),
199-
parsed,
200-
)
201-
202-
# remove the `intern()` in bucket and regex functions that take
203-
# string literal parameters. TODO this logic should be centralized
204-
# in C++ instead of being duplicated.
205-
parsed = re.sub(FUNCTION_LITERAL_REGEX, _replace_interned_param, parsed)
206-
parsed = re.sub(REPLACE_FN_REGEX, _replace_interned_param, parsed)
207-
208-
validated = [alias, expr_raw, parsed, column_id_map]
209-
210-
if alias_map.get(alias) is not None:
211-
idx = alias_map[alias]
212-
validated_expressions[idx] = validated
213-
else:
214-
validated_expressions.append(validated)
215-
alias_map[alias] = len(validated_expressions) - 1
214+
if '""' in expr_raw:
215+
raise ValueError("Cannot reference empty column in expression!")
216+
217+
column_id_map = {}
218+
column_name_map = {}
219+
220+
# we need to be able to modify the running_cidx inside of every call to
221+
# replacer_fn - must pass by reference unfortunately
222+
running_cidx = [0]
223+
224+
replacer_fn = partial(
225+
_replace_expression_column_name,
226+
column_name_map,
227+
column_id_map,
228+
running_cidx,
229+
)
230+
231+
parsed = re.sub(
232+
BOOLEAN_LITERAL_REGEX,
233+
lambda match: "True" if match.group(0) == "true" else ("False" if match.group(0) == "false" else match.group(0)),
234+
parsed,
235+
)
236+
237+
parsed = re.sub(EXPRESSION_COLUMN_NAME_REGEX, replacer_fn, parsed)
238+
parsed = re.sub(
239+
STRING_LITERAL_REGEX,
240+
lambda match: "intern({0})".format(match.group(0)),
241+
parsed,
242+
)
243+
244+
# remove the `intern()` in bucket and regex functions that take
245+
# string literal parameters. TODO this logic should be centralized
246+
# in C++ instead of being duplicated.
247+
parsed = re.sub(FUNCTION_LITERAL_REGEX, _replace_interned_param, parsed)
248+
parsed = re.sub(REPLACE_FN_REGEX, _replace_interned_param, parsed)
249+
250+
validated = [alias, expr_raw, parsed, column_id_map]
251+
252+
if alias_map.get(alias) is not None:
253+
idx = alias_map[alias]
254+
validated_expressions[idx] = validated
255+
else:
256+
validated_expressions.append(validated)
257+
alias_map[alias] = len(validated_expressions) - 1
216258

217259
return validated_expressions

python/perspective/perspective/tests/manager/test_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ def test_manager_view_expression_schema(self):
471471
"table_name": "table1",
472472
"view_name": "view1",
473473
"cmd": "view",
474-
"config": {"expressions": ['// abc \n "a" + "a"']},
474+
"config": {"expressions": {"abc": '"a" + "a"'}},
475475
}
476476
message = {
477477
"id": 2,

python/perspective/perspective/tests/table/test_view_expression.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,18 @@ def test_expression_conversion(self):
3737
"// g\nnow()",
3838
"// h\nlength(123)",
3939
]
40-
test_expressions_dict = [
41-
{"name": "x", "expr": '"a"'},
42-
{"name": "y", "expr": '"b" * 0.5'},
43-
{"name": "c", "expr": "'abcdefg'"},
44-
{"name": "d", "expr": "true and false"},
45-
{"name": "e", "expr": 'float("a") > 2 ? null : 1'},
46-
{"name": "f", "expr": "today()"},
47-
{"name": "g", "expr": "now()"},
48-
{"name": "h", "expr": "length(123)"},
49-
]
40+
41+
test_expressions_dict = {
42+
"x": '"a"',
43+
"y": '"b" * 0.5',
44+
"c": "'abcdefg'",
45+
"d": "true and false",
46+
"e": 'float("a") > 2 ? null : 1',
47+
"f": "today()",
48+
"g": "now()",
49+
"h": "length(123)",
50+
}
51+
5052
str_validated = table.validate_expressions(test_expressions_str)
5153
dict_validated = table.validate_expressions(test_expressions_dict)
5254
assert str_validated["errors"] == dict_validated["errors"]

python/perspective/perspective/tests/viewer/test_validate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ def test_validate_expressions(self):
5757

5858
def test_validate_expressions_invalid(self):
5959
with raises(PerspectiveError):
60-
assert validate.validate_expressions({})
60+
assert validate.validate_expressions([])

python/perspective/perspective/tests/viewer/test_viewer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def test_viewer_delete_without_table(self):
198198

199199
def test_save_restore(self):
200200
table = Table({"a": [1, 2, 3]})
201-
viewer = PerspectiveViewer(plugin="X Bar", filter=[["a", "==", 2]], expressions=['"a" * 2'])
201+
viewer = PerspectiveViewer(plugin="X Bar", filter=[["a", "==", 2]], expressions={'"a" * 2': '"a" * 2'})
202202
viewer.load(table)
203203

204204
# Save config
@@ -207,19 +207,19 @@ def test_save_restore(self):
207207
assert config["filter"] == [["a", "==", 2]]
208208
assert viewer.plugin == "X Bar"
209209
assert config["plugin"] == "X Bar"
210-
assert config["expressions"] == ['"a" * 2']
210+
assert config["expressions"] == {'"a" * 2': '"a" * 2'}
211211

212212
# reset configuration
213213
viewer.reset()
214214
assert viewer.plugin == "Datagrid"
215215
assert viewer.filter == []
216-
assert viewer.expressions == []
216+
assert viewer.expressions == {}
217217

218218
# restore configuration
219219
viewer.restore(**config)
220220
assert viewer.filter == [["a", "==", 2]]
221221
assert viewer.plugin == "X Bar"
222-
assert viewer.expressions == ['"a" * 2']
222+
assert viewer.expressions == {'"a" * 2': '"a" * 2'}
223223

224224
def test_save_restore_plugin_config(self):
225225
viewer = PerspectiveViewer(plugin="Datagrid", plugin_config={"columns": {"a": {"fixed": 4}}})

python/perspective/perspective/viewer/validate.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ def validate_expressions(expressions):
148148
if not isinstance(expr, str):
149149
raise PerspectiveError("Cannot parse non-string expression: {}".format(str(type(expr))))
150150
return expressions
151+
elif isinstance(expressions, dict):
152+
for expr in expressions.values():
153+
if not isinstance(expr, str):
154+
raise PerspectiveError("Cannot parse non-string expression: {}".format(str(type(expr))))
155+
return expressions
151156
else:
152157
raise PerspectiveError("Cannot parse expressions of type: {}".format(str(type(expressions))))
153158

python/perspective/perspective/viewer/viewer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def __init__(
134134
self.aggregates = validate_aggregates(aggregates) or {}
135135
self.sort = validate_sort(sort) or []
136136
self.filter = validate_filter(filter) or []
137-
self.expressions = validate_expressions(expressions) or []
137+
self.expressions = validate_expressions(expressions) or {}
138138
self.plugin_config = validate_plugin_config(plugin_config) or {}
139139
self.settings = settings
140140
self.theme = theme
@@ -269,7 +269,7 @@ def reset(self):
269269
self.split_by = []
270270
self.filter = []
271271
self.sort = []
272-
self.expressions = []
272+
self.expressions = {}
273273
self.aggregates = {}
274274
self.columns = []
275275
self.plugin = "Datagrid"

python/perspective/perspective/viewer/viewer_traitlets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class PerspectiveTraitlets(HasTraits):
4747
aggregates = Dict(default_value={}).tag(sync=True)
4848
sort = List(default_value=[]).tag(sync=True)
4949
filter = List(default_value=[]).tag(sync=True)
50-
expressions = List(default_value=[]).tag(sync=True)
50+
expressions = Dict(default_value=[]).tag(sync=True)
5151
plugin_config = Dict(default_value={}).tag(sync=True)
5252
settings = Bool(True).tag(sync=True)
5353
theme = Unicode("Pro Light", allow_none=True).tag(sync=True)

0 commit comments

Comments
 (0)