Skip to content

Commit

Permalink
Fix new expression syntax in Python
Browse files Browse the repository at this point in the history
  • Loading branch information
texodus committed Nov 17, 2023
1 parent 4b1f190 commit 6ae8282
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 111 deletions.
14 changes: 6 additions & 8 deletions packages/perspective/src/js/perspective.js
Original file line number Diff line number Diff line change
Expand Up @@ -2208,14 +2208,12 @@ export default function (Module) {
*/
async init(msg) {
let wasmBinary = msg.buffer;
try {
const mod = await WebAssembly.instantiate(wasmBinary);
const exports = mod.instance.exports;
const size = exports.size();
const offset = exports.offset();
const array = new Uint8Array(exports.memory.buffer);
wasmBinary = array.slice(offset, offset + size);
} catch {}
const mod = await WebAssembly.instantiate(wasmBinary);
const exports = mod.instance.exports;
const size = exports.size();
const offset = exports.offset();
const array = new Uint8Array(exports.memory.buffer);
wasmBinary = array.slice(offset, offset + size);
__MODULE__ = await __MODULE__({
wasmBinary,
wasmJSMethod: "native-wasm",
Expand Down
14 changes: 6 additions & 8 deletions packages/perspective/src/js/perspective.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,12 @@ const SYNC_SERVER = new (class extends Server {
init(msg) {
this._loaded_promise = (async () => {
let wasmBinary = await buffer;
try {
const mod = await WebAssembly.instantiate(wasmBinary);
const exports = mod.instance.exports;
const size = exports.size();
const offset = exports.offset();
const array = new Uint8Array(exports.memory.buffer);
wasmBinary = array.slice(offset, offset + size);
} catch {}
const mod = await WebAssembly.instantiate(wasmBinary);
const exports = mod.instance.exports;
const size = exports.size();
const offset = exports.offset();
const array = new Uint8Array(exports.memory.buffer);
wasmBinary = array.slice(offset, offset + size);
const core = await load_perspective({
wasmBinary,
wasmJSMethod: "native-wasm",
Expand Down
142 changes: 92 additions & 50 deletions python/perspective/perspective/table/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

from operator import itemgetter
import re
from datetime import date, datetime
from functools import partial
Expand Down Expand Up @@ -156,11 +155,54 @@ def _parse_expression_inputs(expressions):
validated_expressions = []
alias_map = {}

for expression in expressions:
if type(expression) is dict:
expr_raw, alias = itemgetter("expr", "name")(expression)
parsed = expr_raw
else:
if isinstance(expressions, dict):
for alias in expressions.keys():
expression = expressions[alias]

column_id_map = {}
column_name_map = {}

# we need to be able to modify the running_cidx inside of every call to
# replacer_fn - must pass by reference unfortunately
running_cidx = [0]

replacer_fn = partial(
_replace_expression_column_name,
column_name_map,
column_id_map,
running_cidx,
)

parsed = expression
parsed = re.sub(
BOOLEAN_LITERAL_REGEX,
lambda match: "True" if match.group(0) == "true" else ("False" if match.group(0) == "false" else match.group(0)),
parsed,
)

parsed = re.sub(EXPRESSION_COLUMN_NAME_REGEX, replacer_fn, parsed)
parsed = re.sub(
STRING_LITERAL_REGEX,
lambda match: "intern({0})".format(match.group(0)),
parsed,
)

# remove the `intern()` in bucket and regex functions that take
# string literal parameters. TODO this logic should be centralized
# in C++ instead of being duplicated.
parsed = re.sub(FUNCTION_LITERAL_REGEX, _replace_interned_param, parsed)
parsed = re.sub(REPLACE_FN_REGEX, _replace_interned_param, parsed)

validated = [alias, expression, parsed, column_id_map]

if alias_map.get(alias) is not None:
idx = alias_map[alias]
validated_expressions[idx] = validated
else:
validated_expressions.append(validated)
alias_map[alias] = len(validated_expressions) - 1
if isinstance(expressions, list):
for expression in expressions:
expr_raw = expression
parsed = expr_raw
alias_match = re.match(ALIAS_REGEX, expr_raw)
Expand All @@ -169,49 +211,49 @@ def _parse_expression_inputs(expressions):
else:
alias = expr_raw

if '""' in expr_raw:
raise ValueError("Cannot reference empty column in expression!")

column_id_map = {}
column_name_map = {}

# we need to be able to modify the running_cidx inside of every call to
# replacer_fn - must pass by reference unfortunately
running_cidx = [0]

replacer_fn = partial(
_replace_expression_column_name,
column_name_map,
column_id_map,
running_cidx,
)

parsed = re.sub(
BOOLEAN_LITERAL_REGEX,
lambda match: "True" if match.group(0) == "true" else ("False" if match.group(0) == "false" else match.group(0)),
parsed,
)

parsed = re.sub(EXPRESSION_COLUMN_NAME_REGEX, replacer_fn, parsed)
parsed = re.sub(
STRING_LITERAL_REGEX,
lambda match: "intern({0})".format(match.group(0)),
parsed,
)

# remove the `intern()` in bucket and regex functions that take
# string literal parameters. TODO this logic should be centralized
# in C++ instead of being duplicated.
parsed = re.sub(FUNCTION_LITERAL_REGEX, _replace_interned_param, parsed)
parsed = re.sub(REPLACE_FN_REGEX, _replace_interned_param, parsed)

validated = [alias, expr_raw, parsed, column_id_map]

if alias_map.get(alias) is not None:
idx = alias_map[alias]
validated_expressions[idx] = validated
else:
validated_expressions.append(validated)
alias_map[alias] = len(validated_expressions) - 1
if '""' in expr_raw:
raise ValueError("Cannot reference empty column in expression!")

column_id_map = {}
column_name_map = {}

# we need to be able to modify the running_cidx inside of every call to
# replacer_fn - must pass by reference unfortunately
running_cidx = [0]

replacer_fn = partial(
_replace_expression_column_name,
column_name_map,
column_id_map,
running_cidx,
)

parsed = re.sub(
BOOLEAN_LITERAL_REGEX,
lambda match: "True" if match.group(0) == "true" else ("False" if match.group(0) == "false" else match.group(0)),
parsed,
)

parsed = re.sub(EXPRESSION_COLUMN_NAME_REGEX, replacer_fn, parsed)
parsed = re.sub(
STRING_LITERAL_REGEX,
lambda match: "intern({0})".format(match.group(0)),
parsed,
)

# remove the `intern()` in bucket and regex functions that take
# string literal parameters. TODO this logic should be centralized
# in C++ instead of being duplicated.
parsed = re.sub(FUNCTION_LITERAL_REGEX, _replace_interned_param, parsed)
parsed = re.sub(REPLACE_FN_REGEX, _replace_interned_param, parsed)

validated = [alias, expr_raw, parsed, column_id_map]

if alias_map.get(alias) is not None:
idx = alias_map[alias]
validated_expressions[idx] = validated
else:
validated_expressions.append(validated)
alias_map[alias] = len(validated_expressions) - 1

return validated_expressions
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ def test_manager_view_expression_schema(self):
"table_name": "table1",
"view_name": "view1",
"cmd": "view",
"config": {"expressions": ['// abc \n "a" + "a"']},
"config": {"expressions": {"abc": '"a" + "a"'}},
}
message = {
"id": 2,
Expand Down
22 changes: 12 additions & 10 deletions python/perspective/perspective/tests/table/test_view_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,18 @@ def test_expression_conversion(self):
"// g\nnow()",
"// h\nlength(123)",
]
test_expressions_dict = [
{"name": "x", "expr": '"a"'},
{"name": "y", "expr": '"b" * 0.5'},
{"name": "c", "expr": "'abcdefg'"},
{"name": "d", "expr": "true and false"},
{"name": "e", "expr": 'float("a") > 2 ? null : 1'},
{"name": "f", "expr": "today()"},
{"name": "g", "expr": "now()"},
{"name": "h", "expr": "length(123)"},
]

test_expressions_dict = {
"x": '"a"',
"y": '"b" * 0.5',
"c": "'abcdefg'",
"d": "true and false",
"e": 'float("a") > 2 ? null : 1',
"f": "today()",
"g": "now()",
"h": "length(123)",
}

str_validated = table.validate_expressions(test_expressions_str)
dict_validated = table.validate_expressions(test_expressions_dict)
assert str_validated["errors"] == dict_validated["errors"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ def test_validate_expressions(self):

def test_validate_expressions_invalid(self):
with raises(PerspectiveError):
assert validate.validate_expressions({})
assert validate.validate_expressions([])
8 changes: 4 additions & 4 deletions python/perspective/perspective/tests/viewer/test_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def test_viewer_delete_without_table(self):

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

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

# reset configuration
viewer.reset()
assert viewer.plugin == "Datagrid"
assert viewer.filter == []
assert viewer.expressions == []
assert viewer.expressions == {}

# restore configuration
viewer.restore(**config)
assert viewer.filter == [["a", "==", 2]]
assert viewer.plugin == "X Bar"
assert viewer.expressions == ['"a" * 2']
assert viewer.expressions == {'"a" * 2': '"a" * 2'}

def test_save_restore_plugin_config(self):
viewer = PerspectiveViewer(plugin="Datagrid", plugin_config={"columns": {"a": {"fixed": 4}}})
Expand Down
5 changes: 5 additions & 0 deletions python/perspective/perspective/viewer/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ def validate_expressions(expressions):
if not isinstance(expr, str):
raise PerspectiveError("Cannot parse non-string expression: {}".format(str(type(expr))))
return expressions
elif isinstance(expressions, dict):
for expr in expressions.values():
if not isinstance(expr, str):
raise PerspectiveError("Cannot parse non-string expression: {}".format(str(type(expr))))
return expressions
else:
raise PerspectiveError("Cannot parse expressions of type: {}".format(str(type(expressions))))

Expand Down
4 changes: 2 additions & 2 deletions python/perspective/perspective/viewer/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def __init__(
self.aggregates = validate_aggregates(aggregates) or {}
self.sort = validate_sort(sort) or []
self.filter = validate_filter(filter) or []
self.expressions = validate_expressions(expressions) or []
self.expressions = validate_expressions(expressions) or {}
self.plugin_config = validate_plugin_config(plugin_config) or {}
self.settings = settings
self.theme = theme
Expand Down Expand Up @@ -269,7 +269,7 @@ def reset(self):
self.split_by = []
self.filter = []
self.sort = []
self.expressions = []
self.expressions = {}
self.aggregates = {}
self.columns = []
self.plugin = "Datagrid"
Expand Down
2 changes: 1 addition & 1 deletion python/perspective/perspective/viewer/viewer_traitlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class PerspectiveTraitlets(HasTraits):
aggregates = Dict(default_value={}).tag(sync=True)
sort = List(default_value=[]).tag(sync=True)
filter = List(default_value=[]).tag(sync=True)
expressions = List(default_value=[]).tag(sync=True)
expressions = Dict(default_value=[]).tag(sync=True)
plugin_config = Dict(default_value={}).tag(sync=True)
settings = Bool(True).tag(sync=True)
theme = Unicode("Pro Light", allow_none=True).tag(sync=True)
Expand Down
1 change: 1 addition & 0 deletions rust/perspective-viewer/src/less/column-selector.less
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
align-items: center;
margin-right: 7px;
cursor: pointer;
margin-bottom: 4px;

// Button icon
&:before {
Expand Down
25 changes: 6 additions & 19 deletions rust/perspective-viewer/src/ts/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,26 @@ type Module = {
memory: WebAssembly.Memory;
};

// Perform a silly dance to deal with the different ways webpack and esbuild
// load binary, as this may either be an `ArrayBuffer` or `URL` depening
// on whether `inline` option was specified to `perspective-esbuild-plugin`.
async function compile(buff_or_url) {
if (buff_or_url instanceof URL) {
if (buff_or_url instanceof URL || typeof buff_or_url == "string") {
return await WebAssembly.instantiateStreaming(fetch(buff_or_url));
} else {
return await WebAssembly.instantiate(buff_or_url);
}
}

async function release_build(buff_or_url) {
async function load_wasm() {
const buff_or_url = await wasm;
const mod = await compile(buff_or_url);
const exports = mod.instance.exports as Module;
const size = exports.size();
const offset = exports.offset();
const array = new Uint8Array(exports.memory.buffer);
const uncompressed_wasm = array.slice(offset, offset + size);
await wasm_module.default(uncompressed_wasm);
}

async function debug_build(buff_or_url) {
await wasm_module.default(buff_or_url);
}

async function load_wasm() {
// Perform a silly dance to deal with the different ways webpack and esbuild
// load binary, as this may either be an `ArrayBuffer` or `URL` depening
// on whether `inline` option was specified to `perspective-esbuild-plugin`.
const buff_or_url = await wasm;
try {
await release_build(buff_or_url);
} catch {
await debug_build(buff_or_url);
}

wasm_module.init();
return wasm_module;
}
Expand Down
14 changes: 7 additions & 7 deletions rust/perspective-viewer/tasks/bundle/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ fn opt(outpath: &Path) {
.one_caller_inline_max_size(15)
.run(outpath, outpath)
.unwrap();

Command::new("cargo")
.args(["run"])
.args(["-p", "perspective-bootstrap"])
.args(["--"])
.args(["dist/pkg/perspective_bg.wasm"])
.execute();
}

Command::new("cargo")
.args(["run"])
.args(["-p", "perspective-bootstrap"])
.args(["--"])
.args(["dist/pkg/perspective_bg.wasm"])
.execute();
}

fn main() {
Expand Down

0 comments on commit 6ae8282

Please sign in to comment.