Skip to content

Commit 7b335bf

Browse files
committed
Merge branch 'master' into disable-cache
2 parents af8b681 + 106617a commit 7b335bf

26 files changed

Lines changed: 7929 additions & 8909 deletions

File tree

README.md

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Eel is designed to take the hassle out of writing short and simple GUI applicati
2525
- [Synchronous returns](#synchronous-returns)
2626
- [Asynchronous Python](#asynchronous-python)
2727
- [Building distributable binary with PyInstaller](#building-distributable-binary-with-pyinstaller)
28+
- [Microsoft Edge](#microsoft-edge)
2829

2930
<!-- /TOC -->
3031

@@ -81,34 +82,23 @@ If Chrome or Chromium is installed then by default it will open in that in App M
8182

8283
### App options
8384

84-
Additional options can be passed to `eel.start()` by passing it an `options={}` argument.
85+
Additional options can be passed to `eel.start()` as keyword arguments.
8586

86-
Some of the options include the mode the app is in ('chrome', 'chrome-app', None), the port the app runs on, the host name of the app, and adding additional Chrome/Chromium command line flags.
87+
Some of the options include the mode the app is in (e.g. 'chrome'), the port the app runs on, the host name of the app, and adding additional command line flags.
8788

88-
The defaults are set to:
89+
As of Eel 1.0.0, the following options are available to `start()`:
90+
- **mode**, a string specifying what browser to use (e.g. `'chrome'`, `'electron'`, `'edge'`, `'custom'`). Can also be `None` or `False` to not open a window. *Default: `'chrome'`*
91+
- **host**, a string specifying what hostname to use for the Bottle server. *Default: `'localhost'`)*
92+
- **port**, an int specifying what port to use for the Bottle server. Use `0` for port to be picked automatically. *Default: `8000`*.
93+
- **block**, a bool saying whether or not the call to `start()` should block the calling thread. *Default: `True`*
94+
- **jinja_templates**, a string specifying a folder to use for Jinja2 templates, e.g. `my_templates`. *Default: `None`*
95+
- **cmdline_args**, a list of strings to pass to the command to start the browser. For example, we might add extra flags for Chrome; ```eel.start('main.html', mode='chrome-app', port=8080, cmdline_args=['--start-fullscreen', '--browser-startup-dialog'])```. *Default: `[]`*
96+
- **size**, a tuple of ints specifying the (width, height) of the main window in pixels *Default: `None`*
97+
- **position**, a tuple of ints specifying the (left, top) of the main window in pixels *Default: `None`*
98+
- **geometry**, a dictionary specifying the size and position for all windows. The keys should be the relative path of the page, and the values should be a dictionary of the form `{'size': (200, 100), 'position': (300, 50)}`. *Default: {}*
99+
- **close_callback**, a lambda or function that is called when a websocket to a window closes (i.e. when the user closes the window). It should take two arguments; a string which is the relative path of the page that just closed, and a list of other websockets that are still open. *Default: `None`*
89100

90-
```
91-
_default_options = {
92-
'mode': 'chrome-app',
93-
'host': 'localhost',
94-
'port': 8000,
95-
'chromeFlags': ""
96-
}
97-
```
98-
99-
#### Chrome/Chromium flags
100-
101-
You can add additional Chrome/Chromium command line flags by passing a list to the `chromeFlags` attribute on the `options` dictionary and then passing this to `eel.start()`
102-
103-
```
104-
web_app_options = {
105-
'mode': "chrome-app", #or "chrome"
106-
'port': 8080,
107-
'chromeFlags': ["--start-fullscreen", "--browser-startup-dialog"]
108-
}
109101

110-
eel.start('main.html', options=web_app_options)
111-
```
112102

113103
### Exposing functions
114104

@@ -327,3 +317,10 @@ If you want to package your app into a program that can be run on a computer wit
327317
6. When happy that your app is working correctly, add `--onefile --noconsole` flags to build a single executable file
328318

329319
Consult the [documentation for PyInstaller](http://PyInstaller.readthedocs.io/en/stable/) for more options.
320+
321+
## Microsoft Edge
322+
323+
For Windows 10 users, Microsoft Edge (`eel.start(.., mode='edge')`) is installed by default and a useful fallback if a preferred browser is not installed. See the examples:
324+
325+
- A Hello World example using Microsoft Edge: [examples/01 - hello_world-Edge/](https://github.com/ChrisKnott/Eel/tree/master/examples/01%20-%20hello_world-Edge)
326+
- Example implementing browser-fallbacks: [examples/07 - CreateReactApp/eel_CRA.py](https://github.com/ChrisKnott/Eel/tree/master/examples/07%20-%20CreateReactApp/eel_CRA.py)

eel/__init__.py

Lines changed: 87 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from __future__ import print_function
2-
import gevent as gvt
1+
from __future__ import print_function # Python 2 compatibility stuff
32
from builtins import range
43
from io import open
4+
5+
import gevent as gvt
56
import json as jsn
67
import bottle as btl
78
import bottle.ext.websocket as wbs
@@ -16,23 +17,42 @@
1617
_eel_js_file = pkg.resource_filename('eel', 'eel.js')
1718
_eel_js = open(_eel_js_file, encoding='utf-8').read()
1819
_websockets = []
19-
_message_loop_queue = []
2020
_call_return_values = {}
2121
_call_return_callbacks = {}
2222
_call_number = 0
2323
_exposed_functions = {}
2424
_js_functions = []
25-
_start_geometry = {}
2625
_mock_queue = []
2726
_mock_queue_done = set()
28-
_on_close_callback = None
29-
_default_options = {
30-
'mode': 'chrome-app',
31-
'host': 'localhost',
32-
'port': 8000,
33-
'chromeFlags': []
27+
28+
# All start() options must provide a default value and explanation here
29+
_start_args = {
30+
'mode': 'chrome', # What browser is used
31+
'host': 'localhost', # Hostname use for Bottle server
32+
'port': 8000, # Port used for Bottle server (use 0 for auto)
33+
'block': True, # Whether start() blocks calling thread
34+
'jinja_templates': None, # Folder for jinja2 templates
35+
'cmdline_args': ['--disable-http-cache'], # Extra cmdline flags to pass to browser start
36+
'size': None, # (width, height) of main window
37+
'position': None, # (left, top) of main window
38+
'geometry': {}, # Dictionary of size/position for all windows
39+
'close_callback': None, # Callback for when all windows have closed
40+
'app_mode': True, # (Chrome specific option)
41+
'all_interfaces': False, # Allow bottle server to listen for connections on all interfaces
42+
'disable_cache': True, # Sets the no-store response header when serving assets
3443
}
3544

45+
# == Temporary (suppressable) error message to inform users of breaking API change for v1.0.0 ===
46+
_start_args['suppress_error'] = False
47+
api_error_message = '''
48+
----------------------------------------------------------------------------------
49+
'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel
50+
To suppress this error, add 'suppress_error=True' to start() call.
51+
This option will be removed in future versions
52+
----------------------------------------------------------------------------------
53+
'''
54+
# ===============================================================================================
55+
3656
# Public functions
3757

3858
def expose(name_or_function=None):
@@ -53,7 +73,8 @@ def decorator(function):
5373
return function
5474

5575

56-
def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm', '.xhtml']):
76+
def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm',
77+
'.xhtml', '.vue']):
5778
global root_path, _js_functions
5879
root_path = _get_real_path(path)
5980

@@ -87,51 +108,52 @@ def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm', '.xhtml']):
87108

88109

89110
def start(*start_urls, **kwargs):
90-
global _on_close_callback, _jinja_env, _jinja_templates, _disable_cache
91-
block = kwargs.pop('block', True)
92-
_jinja_templates = kwargs.pop('templates', None)
93-
options = kwargs.pop('options', {})
94-
size = kwargs.pop('size', None)
95-
position = kwargs.pop('position', None)
96-
geometry = kwargs.pop('geometry', {})
97-
_on_close_callback = kwargs.pop('callback', None)
98-
_disable_cache = kwargs.pop('disable_cache', True)
99-
100-
for k, v in list(_default_options.items()):
101-
if k not in options:
102-
options[k] = v
103-
104-
_start_geometry['default'] = {'size': size, 'position': position}
105-
_start_geometry['pages'] = geometry
106-
107-
if options['port'] == 0:
111+
_start_args.update(kwargs)
112+
113+
if 'options' in kwargs:
114+
if _start_args['suppress_error']:
115+
_start_args.update(kwargs['options'])
116+
else:
117+
raise RuntimeError(api_error_message)
118+
119+
if _start_args['port'] == 0:
108120
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
109121
sock.bind(('localhost', 0))
110-
options['port'] = sock.getsockname()[1]
122+
_start_args['port'] = sock.getsockname()[1]
111123
sock.close()
112124

113-
if _jinja_templates != None:
125+
if _start_args['jinja_templates'] != None:
114126
from jinja2 import Environment, FileSystemLoader, select_autoescape
115-
templates_path = os.path.join(root_path, _jinja_templates)
116-
_jinja_env = Environment(loader=FileSystemLoader(templates_path),
127+
templates_path = os.path.join(root_path, _start_args['jinja_templates'])
128+
_start_args['jinja_env'] = Environment(loader=FileSystemLoader(templates_path),
117129
autoescape=select_autoescape(['html', 'xml']))
118-
else:
119-
_jinja_env = None
120130

121-
brw.open(start_urls, options)
122-
131+
132+
# Launch the browser to the starting URLs
133+
show(*start_urls)
134+
123135
def run_lambda():
136+
if _start_args['all_interfaces'] == True:
137+
HOST = '0.0.0.0'
138+
else:
139+
HOST = _start_args['host']
124140
return btl.run(
125-
host=options['host'],
126-
port=options['port'],
141+
host=HOST,
142+
port=_start_args['port'],
127143
server=wbs.GeventWebSocketServer,
128144
quiet=True)
129-
if block:
145+
146+
# Start the webserver
147+
if _start_args['block']:
130148
run_lambda()
131149
else:
132150
spawn(run_lambda)
133151

134152

153+
def show(*start_urls):
154+
brw.open(start_urls, _start_args)
155+
156+
135157
def sleep(seconds):
136158
gvt.sleep(seconds)
137159

@@ -143,48 +165,48 @@ def spawn(function, *args, **kwargs):
143165

144166
@btl.route('/eel.js')
145167
def _eel():
146-
funcs = list(_exposed_functions.keys())
168+
start_geometry = {'default': {'size': _start_args['size'],
169+
'position': _start_args['position']},
170+
'pages': _start_args['geometry']}
171+
147172
page = _eel_js.replace('/** _py_functions **/',
148-
'_py_functions: %s,' % funcs)
173+
'_py_functions: %s,' % list(_exposed_functions.keys()))
149174
page = page.replace('/** _start_geometry **/',
150-
'_start_geometry: %s,' % jsn.dumps(_start_geometry))
175+
'_start_geometry: %s,' % _safe_json(start_geometry))
151176
btl.response.content_type = 'application/javascript'
152177
return page
153178

154179

155180
@btl.route('/<path:path>')
156181
def _static(path):
157182
response = None
158-
if _jinja_templates is not None:
159-
template_prefix = _jinja_templates + '/'
160-
161-
if _jinja_env is not None and path.startswith(template_prefix):
183+
if 'jinja_env' in _start_args and 'jinja_templates' in _start_args:
184+
template_prefix = _start_args['jinja_templates'] + '/'
185+
if path.startswith(template_prefix):
162186
n = len(template_prefix)
163-
template = _jinja_env.get_template(path[n:])
187+
template = _start_args['jinja_env'].get_template(path[n:])
164188
response = btl.HTTPResponse(template.render())
165189

166190
if response is None:
167191
response = btl.static_file(path, root=root_path)
168192

169193
# https://stackoverflow.com/a/24748094/280852
170-
global _disable_cache
171-
if _disable_cache:
194+
if _start_args['disable_cache']:
172195
response.set_header('Cache-Control', 'no-store')
173196
return response
174197

175198

176199
@btl.get('/eel', apply=[wbs.websocket])
177200
def _websocket(ws):
178201
global _websockets
179-
global _message_loop_queue # <- what is this for...?
180-
202+
181203
for js_function in _js_functions:
182204
_import_js_function(js_function)
183205

184206
page = btl.request.query.page
185207
if page not in _mock_queue_done:
186208
for call in _mock_queue:
187-
_repeated_send(ws, jsn.dumps(call))
209+
_repeated_send(ws, _safe_json(call))
188210
_mock_queue_done.add(page)
189211

190212
_websockets += [(page, ws)]
@@ -202,6 +224,10 @@ def _websocket(ws):
202224

203225
# Private functions
204226

227+
def _safe_json(obj):
228+
return jsn.dumps(obj, default=lambda o: None)
229+
230+
205231
def _repeated_send(ws, msg):
206232
for attempt in range(100):
207233
try:
@@ -214,8 +240,8 @@ def _repeated_send(ws, msg):
214240
def _process_message(message, ws):
215241
if 'call' in message:
216242
return_val = _exposed_functions[message['name']](*message['args'])
217-
_repeated_send(ws, jsn.dumps({ 'return': message['call'],
218-
'value': return_val }))
243+
_repeated_send(ws, _safe_json({ 'return': message['call'],
244+
'value': return_val }))
219245
elif 'return' in message:
220246
call_id = message['return']
221247
if call_id in _call_return_callbacks:
@@ -259,7 +285,7 @@ def _mock_call(name, args):
259285
def _js_call(name, args):
260286
call_object = _call_object(name, args)
261287
for _, ws in _websockets:
262-
_repeated_send(ws, jsn.dumps(call_object))
288+
_repeated_send(ws, _safe_json(call_object))
263289
return _call_return(call_object)
264290

265291

@@ -284,10 +310,13 @@ def _expose(name, function):
284310

285311

286312
def _websocket_close(page):
287-
if _on_close_callback is not None:
313+
close_callback = _start_args.get('close_callback')
314+
315+
if close_callback is not None:
288316
sockets = [p for _, p in _websockets]
289-
_on_close_callback(page, sockets)
317+
close_callback(page, sockets)
290318
else:
319+
# Default behaviour - wait 1s, then quit if all sockets are closed
291320
sleep(1.0)
292321
if len(_websockets) == 0:
293322
sys.exit()

eel/browsers.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
import subprocess as sps
12
import webbrowser as wbr
3+
24
import eel.chrome as chm
35
import eel.electron as ele
4-
import subprocess as sps
6+
import eel.edge as edge
7+
#import eel.firefox as ffx TODO
8+
#import eel.safari as saf TODO
9+
10+
_browser_paths = {}
11+
_browser_modules = {'chrome': chm,
12+
'electron': ele,
13+
'edge': edge}
14+
515

616
def _build_url_from_dict(page, options):
717
scheme = page.get('scheme', 'http')
@@ -29,18 +39,40 @@ def _build_urls(start_pages, options):
2939

3040

3141
def open(start_pages, options):
42+
# Build full URLs for starting pages (including host and port)
3243
start_urls = _build_urls(start_pages, options)
33-
34-
if options['mode'] in ['chrome', 'chrome-app']:
35-
chm.run(options, start_urls)
36-
elif options['mode'] in [None, False]:
37-
pass # Don't open a browser
38-
elif options['mode'] == 'electron':
39-
ele.run(options, start_urls)
40-
elif options['mode'] == 'custom':
41-
sps.Popen(options['args'],
44+
45+
mode = options.get('mode')
46+
if mode in [None, False]:
47+
# Don't open a browser
48+
pass
49+
elif mode == 'custom':
50+
# Just run whatever command the user provided
51+
sps.Popen(options['cmdline_args'],
4252
stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE)
53+
elif mode in _browser_modules:
54+
# Run with a specific browser
55+
browser_module = _browser_modules[mode]
56+
path = _browser_paths.get(mode)
57+
if path is None:
58+
# Don't know this browser's path, try and find it ourselves
59+
path = browser_module.find_path()
60+
_browser_paths[mode] = path
61+
62+
if path is not None:
63+
browser_module.run(path, options, start_urls)
64+
else:
65+
raise EnvironmentError("Can't find %s installation" % browser_module.name)
4366
else:
44-
# Use system default browser
67+
# Fall back to system default browser
4568
for url in start_urls:
4669
wbr.open(url)
70+
71+
72+
def set_path(browser_name, path):
73+
_browser_paths[browser_name] = path
74+
75+
76+
def get_path(browser_name):
77+
return _browser_paths.get(browser_name)
78+

0 commit comments

Comments
 (0)