Created
April 23, 2020 17:47
-
-
Save maximlt/c660b159fa0a268c4134662debd7ad60 to your computer and use it in GitHub Desktop.
Bundling a panel app as an exe with PyInstaller
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from PyInstaller.utils.hooks import collect_data_files | |
datas = collect_data_files('panel', include_py_files=True) + \ | |
collect_data_files('pyviz_comms', include_py_files=True) + \ | |
collect_data_files('bokeh') # See https://github.com/pyinstaller/pyinstaller/pull/4746 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Pane class which render various markup languages including HTML, | |
Markdown, and also regular strings. | |
""" | |
from __future__ import absolute_import, division, unicode_literals | |
import json | |
import textwrap | |
from six import string_types | |
import param | |
from ..models import HTML as _BkHTML, JSON as _BkJSON | |
from ..util import escape | |
from ..viewable import Layoutable | |
from .base import PaneBase | |
class DivPaneBase(PaneBase): | |
""" | |
Baseclass for Panes which render HTML inside a Bokeh Div. | |
See the documentation for Bokeh Div for more detail about | |
the supported options like style and sizing_mode. | |
""" | |
style = param.Dict(default=None, doc=""" | |
Dictionary of CSS property:value pairs to apply to this Div.""") | |
_bokeh_model = _BkHTML | |
_rename = {'object': 'text'} | |
_updates = True | |
__abstract = True | |
def _get_properties(self): | |
return {p : getattr(self, p) for p in list(Layoutable.param) + ['style'] | |
if getattr(self, p) is not None} | |
def _get_model(self, doc, root=None, parent=None, comm=None): | |
model = self._bokeh_model(**self._get_properties()) | |
if root is None: | |
root = model | |
self._models[root.ref['id']] = (model, parent) | |
return model | |
def _update(self, model): | |
model.update(**self._get_properties()) | |
class HTML(DivPaneBase): | |
""" | |
HTML panes wrap HTML text in a Panel HTML model. The | |
provided object can either be a text string, or an object that | |
has a `_repr_html_` method that can be called to get the HTML | |
text string. The height and width can optionally be specified, to | |
allow room for whatever is being wrapped. | |
""" | |
# Priority is dependent on the data type | |
priority = None | |
@classmethod | |
def applies(cls, obj): | |
module, name = getattr(obj, '__module__', ''), type(obj).__name__ | |
if ((any(m in module for m in ('pandas', 'dask')) and | |
name in ('DataFrame', 'Series')) or hasattr(obj, '_repr_html_')): | |
return 0.2 | |
elif isinstance(obj, string_types): | |
return None | |
else: | |
return False | |
def _get_properties(self): | |
properties = super(HTML, self)._get_properties() | |
text = '' if self.object is None else self.object | |
if hasattr(text, '_repr_html_'): | |
text = text._repr_html_() | |
return dict(properties, text=escape(text)) | |
class DataFrame(HTML): | |
""" | |
DataFrame renders pandas, dask and streamz DataFrame types using | |
their custom HTML repr. In the case of a streamz DataFrame the | |
rendered data will update periodically. | |
""" | |
bold_rows = param.Boolean(default=True, doc=""" | |
Make the row labels bold in the output.""") | |
border = param.Integer(default=0, doc=""" | |
A ``border=border`` attribute is included in the opening | |
`<table>` tag.""") | |
classes = param.List(default=['panel-df'], doc=""" | |
CSS class(es) to apply to the resulting html table.""") | |
col_space = param.ClassSelector(default=None, class_=(str, int), doc=""" | |
The minimum width of each column in CSS length units. An int | |
is assumed to be px units.""") | |
decimal = param.String(default='.', doc=""" | |
Character recognized as decimal separator, e.g. ',' in Europe.""") | |
float_format = param.Callable(default=None, doc=""" | |
Formatter function to apply to columns' elements if they are | |
floats. The result of this function must be a unicode string.""") | |
formatters = param.ClassSelector(default=None, class_=(dict, list), doc=""" | |
Formatter functions to apply to columns' elements by position | |
or name. The result of each function must be a unicode string.""") | |
header = param.Boolean(default=True, doc=""" | |
Whether to print column labels.""") | |
index = param.Boolean(default=True, doc=""" | |
Whether to print index (row) labels.""") | |
index_names = param.Boolean(default=True, doc=""" | |
Prints the names of the indexes.""") | |
justify = param.ObjectSelector(default=None, allow_None=True, objects=[ | |
'left', 'right', 'center', 'justify', 'justify-all', 'start', | |
'end', 'inherit', 'match-parent', 'initial', 'unset'], doc=""" | |
How to justify the column labels.""") | |
max_rows = param.Integer(default=None, doc=""" | |
Maximum number of rows to display.""") | |
max_cols = param.Integer(default=None, doc=""" | |
Maximum number of columns to display.""") | |
na_rep = param.String(default='NaN', doc=""" | |
String representation of NAN to use.""") | |
render_links = param.Boolean(default=False, doc=""" | |
Convert URLs to HTML links.""") | |
show_dimensions = param.Boolean(default=False, doc=""" | |
Display DataFrame dimensions (number of rows by number of | |
columns).""") | |
sparsify = param.Boolean(default=True, doc=""" | |
Set to False for a DataFrame with a hierarchical index to | |
print every multi-index key at each row.""") | |
_object = param.Parameter(default=None, doc="""Hidden parameter.""") | |
_dask_params = ['max_rows'] | |
_rerender_params = [ | |
'object', '_object', 'bold_rows', 'border', 'classes', | |
'col_space', 'decimal', 'float_format', 'formatters', | |
'header', 'index', 'index_names', 'justify', 'max_rows', | |
'max_cols', 'na_rep', 'render_links', 'show_dimensions', | |
'sparsify', 'sizing_mode' | |
] | |
def __init__(self, object=None, **params): | |
super(DataFrame, self).__init__(object, **params) | |
self._stream = None | |
self._setup_stream() | |
@classmethod | |
def applies(cls, obj): | |
module = getattr(obj, '__module__', '') | |
name = type(obj).__name__ | |
if (any(m in module for m in ('pandas', 'dask', 'streamz')) and | |
name in ('DataFrame', 'Series', 'Random', 'DataFrames', 'Seriess')): | |
return 0.3 | |
else: | |
return False | |
def _set_object(self, object): | |
self._object = object | |
@param.depends('object', watch=True) | |
def _setup_stream(self): | |
if not self._models or not hasattr(self.object, 'stream'): | |
return | |
elif self._stream: | |
self._stream.destroy() | |
self._stream = None | |
self._stream = self.object.stream.latest().rate_limit(0.5).gather() | |
self._stream.sink(self._set_object) | |
def _get_model(self, doc, root=None, parent=None, comm=None): | |
model = super(DataFrame, self)._get_model(doc, root, parent, comm) | |
self._setup_stream() | |
return model | |
def _cleanup(self, model): | |
super(DataFrame, self)._cleanup(model) | |
if not self._models and self._stream: | |
self._stream.destroy() | |
self._stream = None | |
def _get_properties(self): | |
properties = DivPaneBase._get_properties(self) | |
if self._stream: | |
df = self._object | |
else: | |
df = self.object | |
if hasattr(df, 'to_frame'): | |
df = df.to_frame() | |
module = getattr(df, '__module__', '') | |
if hasattr(df, 'to_html'): | |
if 'dask' in module: | |
html = df.to_html(max_rows=self.max_rows).replace('border="1"', '') | |
else: | |
kwargs = {p: getattr(self, p) for p in self._rerender_params | |
if p not in DivPaneBase.param and p != '_object'} | |
html = df.to_html(**kwargs) | |
else: | |
html = '' | |
return dict(properties, text=escape(html)) | |
class Str(DivPaneBase): | |
""" | |
A Str pane renders any object for which `str()` can be called, | |
escaping any HTML markup and then wrapping the resulting string in | |
a bokeh Div model. Set to a low priority because generally one | |
will want a better representation, but allows arbitrary objects to | |
be used as a Pane (numbers, arrays, objects, etc.). | |
""" | |
priority = 0 | |
_target_transforms = {'object': """JSON.stringify(value).replace(/,/g, ", ").replace(/:/g, ": ")"""} | |
_bokeh_model = _BkHTML | |
@classmethod | |
def applies(cls, obj): | |
return True | |
def _get_properties(self): | |
properties = super(Str, self)._get_properties() | |
if self.object is None: | |
text = '' | |
else: | |
text = '<pre>'+str(self.object)+'</pre>' | |
return dict(properties, text=escape(text)) | |
class Markdown(DivPaneBase): | |
""" | |
A Markdown pane renders the markdown markup language to HTML and | |
displays it inside a bokeh Div model. It has no explicit | |
priority since it cannot be easily be distinguished from a | |
standard string, therefore it has to be invoked explicitly. | |
""" | |
dedent = param.Boolean(default=True, doc=""" | |
Whether to dedent common whitespace across all lines.""") | |
extensions = param.List(default=[ | |
"markdown.extensions.abbr", | |
"markdown.extensions.attr_list", | |
"markdown.extensions.def_list", | |
"markdown.extensions.fenced_code", | |
"markdown.extensions.footnotes", | |
"markdown.extensions.tables", | |
"markdown.extensions.smarty", | |
"markdown.extensions.codehilite"], doc=""" | |
Markdown extension to apply when transforming markup.""") | |
# Priority depends on the data type | |
priority = None | |
_target_transforms = {'object': None} | |
_rerender_params = ['object', 'dedent', 'extensions'] | |
@classmethod | |
def applies(cls, obj): | |
if hasattr(obj, '_repr_markdown_'): | |
return 0.3 | |
elif isinstance(obj, string_types): | |
return 0.1 | |
else: | |
return False | |
def _get_properties(self): | |
import markdown | |
data = self.object | |
if data is None: | |
data = '' | |
elif not isinstance(data, string_types): | |
data = data._repr_markdown_() | |
if self.dedent: | |
data = textwrap.dedent(data) | |
properties = super(Markdown, self)._get_properties() | |
properties['style'] = properties.get('style', {}) | |
css_classes = properties.pop('css_classes', []) + ['markdown'] | |
html = markdown.markdown(data, extensions=self.extensions, | |
output_format='html5') | |
return dict(properties, text=escape(html), css_classes=css_classes) | |
class JSON(DivPaneBase): | |
depth = param.Integer(default=1, bounds=(-1, None), doc=""" | |
Depth to which the JSON tree will be expanded on initialization.""") | |
encoder = param.ClassSelector(class_=json.JSONEncoder, is_instance=False, doc=""" | |
Custom JSONEncoder class used to serialize objects to JSON string.""") | |
hover_preview = param.Boolean(default=False, doc=""" | |
Whether to display a hover preview for collapsed nodes.""") | |
margin = param.Parameter(default=(5, 20, 5, 5), doc=""" | |
Allows to create additional space around the component. May | |
be specified as a two-tuple of the form (vertical, horizontal) | |
or a four-tuple (top, right, bottom, left).""") | |
theme = param.ObjectSelector(default="dark", objects=["light", "dark"], doc=""" | |
Whether the JSON tree view is expanded by default.""") | |
priority = None | |
_applies_kw = True | |
_bokeh_model = _BkJSON | |
_rename = {"name": None, "object": "text", "encoder": None} | |
@classmethod | |
def applies(cls, obj, **params): | |
if isinstance(obj, (list, dict)): | |
try: | |
json.dumps(obj, cls=params.get('encoder', cls.encoder)) | |
except Exception: | |
return False | |
else: | |
return 0.1 | |
elif isinstance(obj, string_types): | |
return 0 | |
else: | |
return None | |
def _get_properties(self): | |
properties = super(JSON, self)._get_properties() | |
if isinstance(self.object, string_types): | |
text = self.object | |
else: | |
text = json.dumps(self.object or {}, cls=self.encoder) | |
depth = float('inf') if self.depth < 0 else self.depth | |
return dict(text=text, theme=self.theme, depth=depth, | |
hover_preview=self.hover_preview, **properties) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
The goal is to bundle this little panel app with PyInstaller as an .exe file. | |
Things to note: | |
- I use pn.serve(app) to launch the app directly as I assume PyInstaller will run something like ``python panel_pyinstaller.py`` | |
- With a PyInstaller hook, I included everything from ``panel`` and ``pyviz_comms`` to run the app, | |
it's likely there is only a subset of those directories required to run the app. | |
- I had to add ``bokeh`` too as a hook, because the current hook provided by PyInstaller isn't complete | |
after the last 2.0.0 release of Bokeh introduced a new file. | |
- I had to patch a ``markup.py``, a panel module, to make PyInstaller happy. Potential PR for ``panel`` here. | |
- I run the command: | |
pyinstaller --clean --noconfirm --additional-hooks-dir=. .\panel_pyinstaller.py | |
with both this file and ``hook-panel.py`` in the current directory. | |
Note: It does not create a onefile distribution but a directory distribution, it's a little faster this way | |
so it's easier to iterate and check wheter the build app works. | |
Notes: | |
- The hook file could be improved! | |
- It's a really simple app, more complex apps will (for sure) need more work. | |
- Built from a conda env the exe is huge (> 200 Mo) because of that annoying MKL package... | |
""" | |
import panel as pn | |
app = pn.Column("# Panel bundled as an executable with PyInstaller", pn.widgets.Spinner()) | |
pn.serve(app, title="Panel with PyInstaller", show=True) |
Oh nice! I worked on that a long long time ago and think that I actually never used that. So @ovidner did you manage to bundle a panel app with pyinstaller? If so it'd be awesome if you could share some of that on the Holoviz discourse :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for this! It helped me a lot with getting started.
You can get away from patching
markup.py
by including the package metadata formarkdown
in the PyInstaller build. I've made a PR to the PyInstaller hooks to do this: pyinstaller/pyinstaller-hooks-contrib#336