Skip to content

Commit e6bc4b5

Browse files
authored
Merge pull request r0x0r#843 from r0x0r/evaluate_js_async
r0x0r#801 Evaluate js async
2 parents 75a6b24 + fdb4a88 commit e6bc4b5

File tree

9 files changed

+95
-34
lines changed

9 files changed

+95
-34
lines changed

docs/guide/api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,10 @@ Destroy the window.
171171
## evaluate_js
172172

173173
``` python
174-
window.evaluate_js(script)
174+
window.evaluate_js(script, callback=None)
175175
```
176176

177-
Execute Javascript code. The last evaluated expression is returned. Javascript types are converted to Python types, eg. JS objects to dicts, arrays to lists, undefined to None. Note that due implementation limitations the string 'null' will be evaluated to None.
177+
Execute Javascript code. The last evaluated expression is returned. Promises are resolved and callback function is called with the result. Javascript types are converted to Python types, eg. JS objects to dicts, arrays to lists, undefined to None. Note that due implementation limitations the string 'null' will be evaluated to None.
178178
You must escape \n and \r among other escape sequences if they present in Javascript code. Otherwise they get parsed by Python. r'strings' is a recommended way to load Javascript. For GTK WebKit2 versions older than 2.22, there is a limit of about ~900 characters for a value returned by `evaluate_js`.
179179

180180
## get_current_url

examples/evaluate_js_async.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import webview
2+
3+
"""
4+
This example demonstrates evaluating async JavaScript
5+
"""
6+
7+
def callback(result):
8+
print(result)
9+
10+
def evaluate_js_async(window):
11+
window.evaluate_js(
12+
"""
13+
new Promise((resolve, reject) => {
14+
setTimeout(() => {
15+
resolve('Whaddup!');
16+
}, 300);
17+
});
18+
""", callback)
19+
20+
21+
if __name__ == '__main__':
22+
window = webview.create_window('Run custom JavaScript', html='<html><body></body></html>')
23+
webview.start(evaluate_js_async, window, debug=True)

webview/js/api.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,13 @@
6969
}, 100)
7070
},
7171
72-
_returnValues: {}
72+
_returnValues: {},
73+
_asyncCallback: function(result, id) {
74+
window.pywebview._bridge.call('asyncCallback', result, id)
75+
},
76+
_isPromise: function (obj) {
77+
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
78+
}
7379
}
7480
window.pywebview._createApi(%s);
7581

webview/platforms/cef.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -117,26 +117,17 @@ def resize(self, width, height):
117117
0x0002 | 0x0004 | 0x0010)
118118
self.browser.NotifyMoveOrResizeStarted()
119119

120-
def evaluate_js(self, code):
120+
def evaluate_js(self, code, unique_id):
121121
self.loaded.wait()
122-
eval_script = """
123-
try {{
124-
window.external.return_result({0}, '{1}');
125-
}} catch(e) {{
126-
console.error(e.stack);
127-
window.external.return_result(null, '{1}');
128-
}}
129-
"""
130122

131-
id_ = uuid1().hex[:8]
132-
self.eval_events[id_] = Event()
133-
self.browser.ExecuteJavascript(eval_script.format(code, id_))
134-
self.eval_events[id_].wait() # result is obtained via JSBridge.return_result
123+
self.eval_events[unique_id] = Event()
124+
result = self.browser.ExecuteJavascript(code)
125+
self.eval_events[unique_id].wait() # result is obtained via JSBridge.return_result
135126

136-
result = copy(self.js_bridge.results[id_])
127+
result = copy(self.js_bridge.results[unique_id])
137128

138-
del self.eval_events[id_]
139-
del self.js_bridge.results[id_]
129+
del self.eval_events[unique_id]
130+
del self.js_bridge.results[unique_id]
140131

141132
return result
142133

@@ -191,7 +182,7 @@ def wrapper(*args, **kwargs):
191182
uid = args[-1]
192183

193184
if uid not in instances:
194-
logger.debug('CEF window with uid {0} does not exist'.format(uid))
185+
logger.error('CEF window with uid {0} does not exist'.format(uid))
195186
return
196187

197188
return func(*args, **kwargs)
@@ -278,9 +269,9 @@ def load_url(url, uid):
278269

279270

280271
@_cef_call
281-
def evaluate_js(code, uid):
272+
def evaluate_js(code, result, uid):
282273
instance = instances[uid]
283-
return instance.evaluate_js(code)
274+
return instance.evaluate_js(code, result)
284275

285276

286277
@_cef_call

webview/platforms/cocoa.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ def eval():
557557
self.webkit.evaluateJavaScript_completionHandler_(script, handler)
558558

559559
def handler(result, error):
560-
JSResult.result = None if result is None or result == 'null' else json.loads(result)
560+
JSResult.result = None if result is None else json.loads(result)
561561
JSResult.result_semaphore.release()
562562

563563
class JSResult:

webview/platforms/edgechromium.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ def load_url(self, url):
117117

118118
def on_script_notify(self, _, args):
119119
try:
120-
func_name, func_param, value_id = json.loads(args.get_WebMessageAsJson())
121-
120+
return_value = args.get_WebMessageAsJson()
121+
func_name, func_param, value_id = json.loads(return_value)
122122
if func_name == 'alert':
123123
WinForms.MessageBox.Show(func_param)
124124
elif func_name == 'console':

webview/platforms/winforms.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def __init__(self, window):
206206

207207
if window.frameless:
208208
self.frameless = window.frameless
209-
self.FormBorderStyle = 0
209+
self.FormBorderStyle = getattr(WinForms.FormBorderStyle, 'None')
210210
if is_cef:
211211
CEF.create_browser(window, self.Handle.ToInt32(), BrowserView.alert)
212212
elif is_chromium:
@@ -339,7 +339,7 @@ def _toggle():
339339
self.old_state = self.WindowState
340340
self.old_style = self.FormBorderStyle
341341
self.old_location = self.Location
342-
self.FormBorderStyle = 0 # FormBorderStyle.None
342+
self.FormBorderStyle = getattr(WinForms.FormBorderStyle, 'None')
343343
self.Bounds = WinForms.Screen.PrimaryScreen.Bounds
344344
self.WindowState = WinForms.FormWindowState.Maximized
345345
self.is_fullscreen = True
@@ -406,7 +406,7 @@ def _restore():
406406

407407
@staticmethod
408408
def alert(message):
409-
WinForms.MessageBox.Show(message)
409+
WinForms.MessageBox.Show(str(message))
410410

411411

412412
def _set_ie_mode():
@@ -664,9 +664,9 @@ def _close():
664664
window.browser.js_result_semaphore.release()
665665

666666

667-
def evaluate_js(script, uid):
667+
def evaluate_js(script, uid, result_id=None):
668668
if is_cef:
669-
return CEF.evaluate_js(script, uid)
669+
return CEF.evaluate_js(script, result_id, uid)
670670
else:
671671
return BrowserView.instances[uid].evaluate_js(script)
672672

webview/util.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,17 @@ def _call():
153153
window.move(*param)
154154
return
155155

156+
if func_name == 'asyncCallback':
157+
value = json.loads(param) if param is not None else None
158+
159+
if callable(window._callbacks[value_id]):
160+
window._callbacks[value_id](value)
161+
else:
162+
logger.error('Async function executed and callback is not callable. Returned value {0}'.format(value))
163+
164+
del window._callbacks[value_id]
165+
return
166+
156167
func = window._functions.get(func_name) or getattr(window._js_api, func_name, None)
157168

158169
if func is not None:

webview/window.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
from enum import Flag, auto
55
from functools import wraps
6+
from uuid import uuid1
67

78
from webview.event import Event
89
from webview.localization import original_localization
@@ -24,7 +25,7 @@ def wrapper(*args, **kwargs):
2425
event = args[0].loaded if event_type == 'loaded' else args[0].shown
2526

2627
try:
27-
if not event.wait(15):
28+
if not event.wait(20):
2829
raise WebViewException('Main window failed to start')
2930

3031
if args[0].gui is None:
@@ -81,6 +82,7 @@ def __init__(self, uid, title, url, html, width, height, x, y, resizable, fullsc
8182

8283
self._js_api = js_api
8384
self._functions = {}
85+
self._callbacks = {}
8486

8587
self.closed = Event()
8688
self.closing = Event(True)
@@ -293,14 +295,42 @@ def move(self, x, y):
293295
self.gui.move(x, y, self.uid)
294296

295297
@_loaded_call
296-
def evaluate_js(self, script):
298+
def evaluate_js(self, script, callback=None):
297299
"""
298300
Evaluate given JavaScript code and return the result
299301
:param script: The JavaScript code to be evaluated
300302
:return: Return value of the evaluated code
303+
:callback: Optional callback function that will be called for resolved promises
301304
"""
302-
escaped_script = 'JSON.stringify(eval("{0}"))'.format(escape_string(script))
303-
return self.gui.evaluate_js(escaped_script, self.uid)
305+
unique_id = uuid1().hex
306+
self._callbacks[unique_id] = callback
307+
308+
if self.gui.renderer == 'cef':
309+
sync_eval = 'window.external.return_result(JSON.stringify(value), "{0}");'.format(unique_id,)
310+
else:
311+
sync_eval = 'JSON.stringify(value);'
312+
313+
314+
if callback:
315+
escaped_script = """
316+
var value = eval("{0}");
317+
if (pywebview._isPromise(value)) {{
318+
value.then(function evaluate_async(result) {{
319+
pywebview._asyncCallback(JSON.stringify(result), "{1}")
320+
}});
321+
true;
322+
}} else {{ {2} }}
323+
""".format(escape_string(script), unique_id, sync_eval)
324+
else:
325+
escaped_script = """
326+
var value = eval("{0}");
327+
{1};
328+
""".format(escape_string(script), sync_eval)
329+
330+
if self.gui.renderer == 'cef':
331+
return self.gui.evaluate_js(escaped_script, self.uid, unique_id)
332+
else:
333+
return self.gui.evaluate_js(escaped_script, self.uid)
304334

305335
@_shown_call
306336
def create_file_dialog(self, dialog_type=10, directory='', allow_multiple=False, save_filename='', file_types=()):

0 commit comments

Comments
 (0)