Open MarcSkovMadsen opened 2 years ago
Often you don't want to output the pane chosen by pn.panel
import panel as pn
pn.extension(sizing_mode="stretch_width")
def model(value):
return f"https://audio.qurancdn.com/wbw/001_001_00{value}.mp3"
selection = pn.widgets.Select(value=1, options=[1,2,3,4])
imodel = pn.bind(model, value=selection)
output=pn.panel(imodel)
pn.Column(output).servable()
If I for some reason want to output to pn.pane.Str
I have to
import panel as pn
pn.extension(sizing_mode="stretch_width")
def model(value):
return f"https://audio.qurancdn.com/wbw/001_001_00{value}.mp3"
selection = pn.widgets.Select(value=1, options=[1,2,3,4])
output = pn.pane.Str()
def update(value):
output.object=model(value)
update(selection.value)
pn.bind(update, value=selection, watch=True)
pn.Column(output).servable()
As I see it, this is a convoluted way of thinking and writing code, that makes Panel slower and more difficult to use than it has to be.
import panel as pn
pn.extension(sizing_mode="stretch_width")
def model(value):
return f"https://audio.qurancdn.com/wbw/001_001_00{value}.mp3"
selection = pn.widgets.Select(value=1, options=[1,2,3,4])
imodel=pn.bind(model, value=selection, watch=True)
outputs=pn.outputs(imodel, outputs=[pn.pane.Str])
pn.Column(outputs).servable()
This is much simpler and much more in line with building a pipeline starting with inputs, applying the model and piping to outputs. Together with hvplot.interactive
this is easy to understand for your brain.
Same as for the Alternative Output scenario plus
.object
or value
parameter should be supported.Maybe the above solutions can be further simplified and made more powerful and general. Feel free to suggest.
If I for some reason want to output to pn.pane.Str I have to
Just one question to figure out if it's me or you that is missing something but what's wrong with:
def model(value):
return pn.pane.Str(f"https://audio.qurancdn.com/wbw/001_001_00{value}.mp3")
in this scenario?
Note that I would be very open to finding some API for a user to declare the types of a function like this because in addition to being helpful for a user it would allow optimizing the implementation significantly.
If I for some reason want to output to pn.pane.Str I have to
Just one question to figure out if it's me or you that is missing something but what's wrong with:
def model(value): return pn.pane.Str(f"https://audio.qurancdn.com/wbw/001_001_00{value}.mp3")
in this scenario?
I see the following issues
model
function is often a function that really does not have anything with the Panel universe to do. So we would have to at least definedef model_wrapper(value):
return pn.pane.Str(model(value))
and that is a bit convoluted
object
of a pane. At least that is the theory.The more I think about it, the more I think it makes sense to provide a pipe
:
model, inputs, outputs = pn.pipe(model, inputs=[selection], outputs=[pn.pane.JSON, pn.pane.Audio])
where the inputs and outputs can be either classes or instances. The model returned is that same as returned from pn.bind.
Or maybe you should call it .interactive
similarly to hvplot.interactive
.
So make you DataFrames interactive with hvplot.interactive
and your models interactive with panel.interactive
😄
model, inputs, outputs = pn.interactive(model, inputs=[selection], outputs=[pn.pane.JSON, pn.pane.Audio])
The below is a proof of concept implementation of pipe
and interactive
. I'm pretty happy about it 😄
$ pytest 'tests\shared\test_pipe.py'
================================================= test session starts =================================================
platform win32 -- Python 3.8.4, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: C:\repos\private\panel-ai, configfile: pytest.ini
plugins: anyio-3.3.4, cov-3.0.0, mock-3.6.1
collected 6 items
tests\shared\test_pipe.py ...... [100%]
================================================== 6 passed in 1.30s ==================================================
from typing import Tuple
import panel as pn
def _clean(output):
if callable(output):
return output()
return output
def pipe(function, *outputs):
if not hasattr(function, "__wrapped__") or not hasattr(function, "_dinfo"):
raise ValueError("Function has not been bound. Please apply panel.bind before using the pipe")
inputs = tuple(function._dinfo["kw"].values())
def _get_results() -> Tuple:
args = tuple(getattr(input.owner, input.name) for input in inputs)
kwargs = dict(zip(function._dinfo["kw"].keys(), args))
results = function(**kwargs)
if not isinstance(results, (list, tuple)):
results = (results,)
return tuple(results)
def _set_result(results: tuple, outputs: tuple):
for index, output in enumerate(outputs):
output.object = results[index]
results = _get_results()
if not outputs:
outputs = tuple(pn.panel(result) for result in results)
else:
outputs = tuple(_clean(output) for output in outputs)
_set_result(results, outputs)
def _handle_change(*events):
results = _get_results()
_set_result(results, outputs)
for input in inputs:
input.owner.param.watch(_handle_change, parameter_names=[input.name])
if len(outputs)==1:
return outputs[0]
return outputs
def interactive(function, inputs, outputs=None):
if isinstance(inputs, dict):
inputs = {key: _clean(input) for key, input in inputs.items()}
ifunction = pn.bind(function, **inputs)
elif isinstance(inputs, list):
inputs = [_clean(input) for input in inputs]
ifunction = pn.bind(function, *inputs)
else:
inputs = _clean(inputs)
ifunction = pn.bind(function, inputs)
if isinstance(outputs, list):
outputs = [_clean(output) for output in outputs]
elif outputs:
outputs = [_clean(outputs)]
if outputs:
outputs = pipe(ifunction, *outputs)
else:
outputs = pipe(ifunction)
return inputs, outputs
def test_pipe():
# Given
def model(value1, value2):
return value1, value2
input1=pn.widgets.TextInput()
input2=pn.widgets.TextInput()
imodel = pn.bind(model, value1=input1, value2=input2)
# When
output1, output2 = pipe(imodel, pn.pane.Str, pn.pane.Str)
# Then
assert isinstance(output1, pn.pane.Str)
assert isinstance(output2, pn.pane.Str)
# When
input1.value = "Hello"
assert output1.object == input1.value
input2.value = "World"
assert output2.object == input2.value
def test_pipe_can_replace_pn_panel():
# Given
def model(value1):
return value1
input1=pn.widgets.TextInput()
imodel = pn.bind(model, value1=input1)
# When
output1 = pipe(imodel)
# Then
assert isinstance(output1, pn.pane.Markdown)
# When
input1.value = "Hello"
assert output1.object == input1.value
def test_pipe_can_infer_outputs():
# Given
def model(value1, value2):
return value1, value2
input1=pn.widgets.TextInput()
input2=pn.widgets.TextInput()
imodel = pn.bind(model, value1=input1, value2=input2)
# When
output1, output2 = pipe(imodel)
# Then
assert isinstance(output1, pn.pane.Markdown)
assert isinstance(output2, pn.pane.Markdown)
# When
input1.value = "Hello"
assert output1.object == input1.value
input2.value = "World"
assert output2.object == input2.value
def test_interactive():
# Given
def model(value1, value2):
return value1, value2
input1_org=pn.widgets.TextInput
input2_org=pn.widgets.TextInput()
output1_org=pn.pane.Str
output2_org=pn.pane.Str(name="Output 2")
# When
inputs, outputs = interactive(model, inputs=[input1_org, input2_org], outputs=[output1_org, output2_org])
# Then
input1, input2 = inputs
assert isinstance(input1, pn.widgets.TextInput)
assert input2==input2_org
output1, output2 = outputs
assert isinstance(output1, pn.pane.Str)
assert output2==output2_org
input1.value="Hello"
output1.object==input1.value
input2.value="World"
output2.object==input1.value
def test_multi_output():
def model(value):
return {"data": [value]}, f"https://audio.qurancdn.com/wbw/001_001_00{value}.mp3"
inputs, outputs = interactive(
model,
inputs=[
pn.widgets.Select(value=1, options=[1,2,3,4])
],
)
assert outputs[0].object
assert outputs[1].object
return pn.Row(pn.Column(*inputs), pn.Column(*outputs))
def test_alternative_output():
def model(value):
return f"https://audio.qurancdn.com/wbw/001_001_00{value}.mp3"
select = pn.widgets.Select(value=1, options=[1,2,3,4])
inputs, outputs = interactive(
model,
inputs=select,
outputs=pn.pane.Str,
)
assert inputs == select
assert outputs[0].object
assert outputs[0].object
return pn.Row(pn.Column(inputs), pn.Column(outputs))
if __name__.startswith("bokeh"):
pn.extension(sizing_mode="stretch_width")
pn.Column(
"# Test Alternative Output",
test_alternative_output(),
"# Test Multi Output",
test_multi_output(),
).servable()
Context
pn.panel
is pretty amazing in guessing the right pane to wrap an object into.But I've started to realize that there are use cases it does not support and where users then need to write a lot more code to handle then really necessary.
In the end I would like the user to end up with an easy to understand pipeline of transformations and code.
Scenario: Multiple Outputs
Example
Here
pn.panel
cannot correctly split the output into two panes. But it can find a pane for each individual result.The use case here is really that the user has the
model
function.Current Solution
This is the minimum solution I've been able to implement if my requirement is that I only want to rerun the function once when the
selection
changes.Solution: Full Blown
here
.output
could also be called.panels
,.pipe
etc. It might also just be an extension of the existingpn.panel
?Maybe the
object
argument should not be there but the user should just provide*args
to the.output
function?Please note
loading_indicator
supported similarly topn.panel
as its quite powerful..outputs
without specifying anyoutputs
argument. Then it will just now that it should split the result intopn.panel
s if the result is iterable.pn.panel
, I.e. ifoutputs = [pn.pane.JSON, None]
.Solution: Simple
Just support an
outputs
Integer inpn.panel
.Additional Context
What gradio does is to combine the apis. So the user would just write
From that they would get a layout. With functionality to screenshot and flag their model. A built in REST API for their model. And interactive documentation for the REST API.