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

Add ability to define download callback #1146

Merged
merged 2 commits into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
74 changes: 74 additions & 0 deletions examples/gallery/simple/save_filtered_df.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"from bokeh.sampledata.autompg import autompg\n",
"from io import StringIO\n",
"\n",
"pn.extension()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"years = pn.widgets.MultiChoice(\n",
" name='Years', options=list(autompg.yr.unique()), margin=(0, 20, 0, 0)\n",
")\n",
"mpg = pn.widgets.RangeSlider(\n",
" name='Mile per Gallon', start=autompg.mpg.min(), end=autompg.mpg.max()\n",
")\n",
"\n",
"@pn.depends(years, mpg)\n",
"def filtered_mpg(yrs, mpg):\n",
" df = autompg\n",
" if years.value:\n",
" df = autompg[autompg.yr.isin(yrs)]\n",
" return df[(df.mpg >= mpg[0]) & (df.mpg <= mpg[1])]\n",
"\n",
"@pn.depends(years, mpg)\n",
"def filtered_file(yr, mpg):\n",
" df = filtered_mpg(yr, mpg)\n",
" sio = StringIO()\n",
" df.to_csv(sio)\n",
" sio.seek(0)\n",
" return sio\n",
"\n",
"fd = pn.widgets.FileDownload(\n",
" callback=filtered_file, filename='filtered_autompg.csv'\n",
")\n",
"\n",
"pn.Column(pn.Row(years, mpg), fd, pn.panel(filtered_mpg, width=600), width=600)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.5"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
63 changes: 60 additions & 3 deletions examples/reference/widgets/FileDownload.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
"##### Core\n",
"\n",
"* **``auto``** (boolean): Whether to download the file the initial click (if `True`) or when clicking a second time (or via the right-click Save file menu).\n",
"* **``callback``** (callable): A callable that returns a file or file-like object (takes precedence over `file` if set). \n",
"* **``embed``** (boolean): Whether to embed the data on initialization.\n",
"* **``file``** (str): A path to a filename.\n",
"* **``file``** (str or file-like object): A path to a file or a file-like object.\n",
"* **``filename``** (str): The filename to save the file as.\n",
"\n",
"##### Display\n",
Expand Down Expand Up @@ -87,7 +88,7 @@
"source": [
"pn.widgets.FileDownload(\n",
" file='FileDownload.ipynb', button_type='success', auto=False,\n",
" embed=True, name=\"Right-click to download using 'Save as' dialog\"\n",
" embed=False, name=\"Right-click to download using 'Save as' dialog\"\n",
")"
]
},
Expand All @@ -105,19 +106,75 @@
"outputs": [],
"source": [
"from bokeh.sampledata.autompg import autompg\n",
"\n",
"from io import StringIO\n",
"sio = StringIO()\n",
"autompg.to_csv(sio)\n",
"sio.seek(0)\n",
"\n",
"pn.widgets.FileDownload(sio, embed=True, filename='autompg.csv')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you want to generate the file dynamically, e.g. because it depends on the parameters of some widget you can also supply a callback (which may be decorated with the widgets and/or parameters it depends on):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"years = pn.widgets.MultiChoice(\n",
" name='Years', options=list(autompg.yr.unique()), margin=(0, 20, 0, 0)\n",
")\n",
"mpg = pn.widgets.RangeSlider(\n",
" name='Mile per Gallon', start=autompg.mpg.min(), end=autompg.mpg.max()\n",
")\n",
"\n",
"@pn.depends(years, mpg)\n",
"def filtered_mpg(yrs, mpg):\n",
" df = autompg\n",
" if years.value:\n",
" df = autompg[autompg.yr.isin(yrs)]\n",
" return df[(df.mpg >= mpg[0]) & (df.mpg <= mpg[1])]\n",
"\n",
"@pn.depends(years, mpg)\n",
"def filtered_file(yr, mpg):\n",
" df = filtered_mpg(yr, mpg)\n",
" sio = StringIO()\n",
" df.to_csv(sio)\n",
" sio.seek(0)\n",
" return sio\n",
"\n",
"fd = pn.widgets.FileDownload(\n",
" callback=filtered_file, filename='filtered_autompg.csv'\n",
")\n",
"\n",
"pn.Column(pn.Row(years, mpg), fd, pn.panel(filtered_mpg, width=600), width=600)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"pygments_lexer": "ipython3"
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.5"
}
},
"nbformat": 4,
Expand Down
10 changes: 5 additions & 5 deletions panel/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,14 +559,14 @@ def __init__(self, object=None, **params):
super(ParamMethod, self).__init__(object, **params)
self._link_object_params()
if object is not None:
self._update_inner(self._eval_function(object))
self._update_inner(self.eval(object))

#----------------------------------------------------------------
# Callback API
#----------------------------------------------------------------

@classmethod
def _eval_function(self, function):
def eval(self, function):
args, kwargs = (), {}
if hasattr(function, '_dinfo'):
arg_deps = function._dinfo['dependencies']
Expand All @@ -587,7 +587,7 @@ def _update_pane(self, *events):
self._callbacks = callbacks
self._link_object_params()
if object is not None:
self._update_inner(self._eval_function(self.object))
self._update_inner(self.eval(self.object))

def _link_object_params(self):
parameterized = get_method_owner(self.object)
Expand Down Expand Up @@ -618,7 +618,7 @@ def update_pane(*events):
self._callbacks.append(watcher)
for p in params:
deps.append(p)
new_object = self._eval_function(self.object)
new_object = self.eval(self.object)
self._update_inner(new_object)

for _, params in full_groupby(params, lambda x: (x.inst or x.cls, x.what)):
Expand Down Expand Up @@ -650,7 +650,7 @@ class ParamFunction(ParamMethod):
priority = 0.6

def _replace_pane(self, *args):
new_object = self._eval_function(self.object)
new_object = self.eval(self.object)
self._update_inner(new_object)

def _link_object_params(self):
Expand Down
33 changes: 22 additions & 11 deletions panel/widgets/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ class FileDownload(Widget):
button_type = param.ObjectSelector(default='default', objects=[
'default', 'primary', 'success', 'warning', 'danger'])

callback = param.Callable(default=None, doc="""
A callable that return the file path or file-like object.""")

data = param.String(default=None, doc="""
The data being transferred.""")

Expand Down Expand Up @@ -209,7 +212,10 @@ class FileDownload(Widget):

_widget_type = _BkFileDownload

_rename = {'embed': None, 'file': None, '_clicks': 'clicks', 'name': 'title'}
_rename = {
'callback': None, 'embed': None, 'file': None,
'_clicks': 'clicks', 'name': 'title'
}

def __init__(self, file=None, **params):
self._default_label = 'label' not in params
Expand All @@ -227,7 +233,7 @@ def _update_default(self):
def _update_label(self):
label = 'Download' if self._synced or self.auto else 'Transfer'
if self._default_label:
if self.file is None:
if self.file is None and self.callback is None:
label = 'No file set'
else:
try:
Expand All @@ -239,33 +245,38 @@ def _update_label(self):
self.set_param(label=label)
self._default_label = True

@param.depends('embed', 'file', watch=True)
@param.depends('embed', 'file', 'callback', watch=True)
def _update_embed(self):
if self.embed:
self._transfer()

@param.depends('_clicks', watch=True)
def _transfer(self):
if self.file is None:
if self.file is None and self.callback is None:
return

from ..param import ParamFunction
filename = self.filename
if isinstance(self.file, str) and os.path.isfile(self.file):
with open(self.file, 'rb') as f:
if self.callback is None:
fileobj = self.file
else:
fileobj = ParamFunction.eval(self.callback)
if isinstance(fileobj, str) and os.path.isfile(fileobj):
with open(fileobj, 'rb') as f:
b64 = b64encode(f.read()).decode("utf-8")
if filename is None:
filename = os.path.basename(self.file)
elif hasattr(self.file, 'read'):
bdata = self.file.read()
filename = os.path.basename(fileobj)
elif hasattr(fileobj, 'read'):
bdata = fileobj.read()
if not isinstance(bdata, bytes):
bdata = bdata.encode("utf-8")
b64 = b64encode(bdata).decode("utf-8")
if self.filename is None:
if filename is None:
raise ValueError('Must provide filename if file-like '
'object is provided.')
else:
raise ValueError('Cannot transfer unknown file type %s' %
type(self.file).__name__)
type(fileobj).__name__)

ext = filename.split('.')[-1]
for mtype, subtypes in self._mime_types.items():
Expand Down