Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/functickformatter args #4659

Merged
merged 19 commits into from
Sep 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions bokeh/models/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,26 +225,41 @@ def from_py_func(cls, func):
pyscript = import_required('flexx.pyscript',
'To use Python functions for CustomJS, you need Flexx ' +
'("conda install -c bokeh flexx" or "pip install flexx")')
argspec = inspect.getargspec(func)

arg = inspect.getargspec(func)[0]
if len(arg) != 1:
raise ValueError("Function `func` can have only one argument, but %d were supplied." % len(arg))
default_names = argspec.args
default_values = argspec.defaults or []

# Set the transpiled functions as `formatter` so that we can call it
code = pyscript.py2js(func, 'formatter')
# We wrap the transpiled function into an anonymous function with a single
# arg that matches that of func.
wrapped_code = "function (%s) {%sreturn formatter(%s)};" % (arg[0], code, arg[0])
if len(default_names) - len(default_values) != 0:
raise ValueError("Function `func` may only contain keyword arguments.")

return cls(code=wrapped_code)
if default_values and not any([isinstance(value, Model) for value in default_values]):
raise ValueError("Default value must be a plot object.")

func_kwargs = dict(zip(default_names, default_values))

# Wrap the code attr in a function named `formatter` and call it
# with arguments that match the `args` attr
code = pyscript.py2js(func, 'formatter') + 'formatter(%s);\n' % ', '.join(default_names)

return cls(code=code, args=func_kwargs)

@classmethod
def from_coffeescript(cls, code, args={}):
compiled = nodejs_compile(code, lang="coffeescript", file="???")
wrapped_code = "formatter = () -> %s" % (code,)

compiled = nodejs_compile(wrapped_code, lang="coffeescript", file="???")
if "error" in compiled:
raise CompilationError(compiled.error)
else:
return cls(code=compiled.code) # TODO: args=args
wrapped_compiled_code = "%s\nreturn formatter()" % (compiled.code,)
return cls(code=wrapped_compiled_code, args=args)

args = Dict(String, Instance(Model), help="""
A mapping of names to Bokeh plot objects. These objects are made
available to the formatter code snippet as the values of named
parameters to the callback.
""")

code = String(default="", help="""
An anonymous JavaScript function expression to reformat a
Expand Down
77 changes: 64 additions & 13 deletions bokeh/models/tests/test_formatters.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,73 @@
from unittest import skipIf
import pytest

try:
from flexx import pyscript
is_flexx = True
except ImportError as e:
is_flexx = False
from bokeh.models import FuncTickFormatter, Slider

from bokeh.models import FuncTickFormatter
flexx = pytest.importorskip("flexx")

@skipIf(not is_flexx, "flexx not installed")
def test_functickformatter_from_py_func():
def test_functickformatter_from_py_func_no_args():

def convert_to_minutes(seconds):
return seconds * 60
def convert_to_minutes():
return tick * 60 # noqa

formatter = FuncTickFormatter.from_py_func(convert_to_minutes)
js_code = pyscript.py2js(convert_to_minutes, 'formatter')
js_code = flexx.pyscript.py2js(convert_to_minutes, 'formatter')

function_wrapper = formatter.code.replace(js_code, '')

assert function_wrapper == "function (seconds) {return formatter(seconds)};"
assert function_wrapper == "formatter();\n"

def test_functickformatter_from_py_func_with_args():

slider = Slider()

def convert_to_minutes(x=slider):
return tick * 60 # noqa

formatter = FuncTickFormatter.from_py_func(convert_to_minutes)
js_code = flexx.pyscript.py2js(convert_to_minutes, 'formatter')

function_wrapper = formatter.code.replace(js_code, '')

assert function_wrapper == "formatter(x);\n"
assert formatter.args['x'] is slider

def test_functickformatter_bad_pyfunc_formats():
def has_positional_arg(x):
return None
with pytest.raises(ValueError):
FuncTickFormatter.from_py_func(has_positional_arg)

def has_positional_arg_with_kwargs(y, x=5):
return None
with pytest.raises(ValueError):
FuncTickFormatter.from_py_func(has_positional_arg_with_kwargs)

def has_non_Model_keyword_argument(x=10):
return None
with pytest.raises(ValueError):
FuncTickFormatter.from_py_func(has_non_Model_keyword_argument)

def test_functickformatter_from_coffeescript_no_arg():
coffee_code = """
square = (x) -> x * x
return square(tick)
"""

js_code = "\n var square;\n square = function(x) {\n return x * x;\n };\n return square(tick);\n"

formatter = FuncTickFormatter.from_coffeescript(code=coffee_code)
function_wrapper = formatter.code.replace(js_code, "")

assert function_wrapper == "var formatter;\n\nformatter = function() {};\n\nreturn formatter()"
assert formatter.args == {}

def test_functickformatter_from_coffeescript_with_args():
coffee_code = "return slider.get('value') + tick"
js_code = "\n return slider.get('value') + tick;\n"

slider = Slider()
formatter = FuncTickFormatter.from_coffeescript(code=coffee_code, args={"slider": slider})

function_wrapper = formatter.code.replace(js_code, "")
assert function_wrapper == "var formatter;\n\nformatter = function() {};\n\nreturn formatter()"
assert formatter.args == {"slider": slider}
1 change: 1 addition & 0 deletions bokehjs/src/coffee/api/typings/models/formatters.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ declare namespace Bokeh {
export var FuncTickFormatter: { new(attributes?: IFuncTickFormatter, options?: ModelOpts): FuncTickFormatter };
export interface FuncTickFormatter extends TickFormatter, IFuncTickFormatter {}
export interface IFuncTickFormatter extends ITickFormatter {
args?: Map<Model>;
code?: string;
}

Expand Down
18 changes: 11 additions & 7 deletions bokehjs/src/coffee/models/formatters/func_tick_formatter.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ class FuncTickFormatter extends TickFormatter.Model
type: 'FuncTickFormatter'

@define {
code: [ p.String, '' ]
}
args: [ p.Any, {} ] # TODO (bev) better type
code: [ p.String, '' ]
}

doFormat: (ticks) ->
# wrap the `code` fxn inside a function and make it a callable
# func = new Function("tick", "var a = " + code + "return a(tick)")
func = new Function("tick", "var func = " + @code + "return func(tick)")
initialize: (attrs, options) ->
super(attrs, options)

_make_func: () ->
return new Function("tick", _.keys(@args)..., "require", @code)

return _.map(ticks, func)
doFormat: (ticks) ->
func = @_make_func()
return (func(tick, _.values(@args)..., require) for tick in ticks)

module.exports =
Model: FuncTickFormatter
51 changes: 40 additions & 11 deletions bokehjs/test/models/formatters/func_tick_formatter.coffee
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
{expect} = require "chai"
utils = require "../../utils"

formatter = utils.require "models/formatters/func_tick_formatter"
FuncTickFormatter = utils.require("models/formatters/func_tick_formatter").Model
Range1d = utils.require("models/ranges/range1d").Model

describe "func_tick_formatter module", ->

it "should format numerical ticks appropriately", ->
obj = new formatter.Model
code: "function (x) {return x*10};"
describe "FuncTickFormatter._make_func method", ->
formatter = new FuncTickFormatter({code: "return 10"})
it "should return a Function", ->
expect(formatter._make_func()).to.be.an.instanceof(Function)

labels = obj.doFormat([-10, -0.1, 0, 0.1, 10])
expect(labels).to.deep.equal([-100, -1.0, 0, 1, 100])
it "should have code property as function body", ->
func = new Function("tick", "require", "return 10")
expect(formatter._make_func().toString()).to.be.equal(func.toString())

it "should format categorical ticks appropriately", ->
obj = new formatter.Model
code: "function (y) {return y + '_lat'};"
it "should have values as function args", ->
rng = new Range1d()
formatter.args = {foo: rng.ref()}
func = new Function("tick", "foo", "require", "return 10")
expect(formatter._make_func().toString()).to.be.equal(func.toString())

labels = obj.doFormat(["a", "b", "c", "d", "e"])
expect(labels).to.deep.equal(["a_lat", "b_lat", "c_lat", "d_lat", "e_lat"])
describe "doFormat method", ->
it "should format numerical ticks appropriately", ->
formatter = new FuncTickFormatter({code: "return tick * 10"})
labels = formatter.doFormat([-10, -0.1, 0, 0.1, 10])
expect(labels).to.deep.equal([-100, -1.0, 0, 1, 100])

it "should format categorical ticks appropriately", ->
formatter = new FuncTickFormatter({code: "return tick + '_lat'"})
labels = formatter.doFormat(["a", "b", "c", "d", "e"])
expect(labels).to.deep.equal(["a_lat", "b_lat", "c_lat", "d_lat", "e_lat"])

it "should support imports using require", ->
formatter = new FuncTickFormatter({
code: "var _ = require('underscore'); return _.max([1,2,3])"
})
labels = formatter.doFormat([0, 0, 0])
expect(labels).to.be.deep.equal([3,3,3])

it "should handle args appropriately", ->
rng = new Range1d({start: 5, end: 10})
formatter = new FuncTickFormatter({
code: "return foo.start + foo.end + tick"
args: {foo: rng}
})
labels = formatter.doFormat([-10, -0.1, 0, 0.1, 10])
expect(labels).to.deep.equal([5, 14.9, 15, 15.1, 25])