r0x0r / pywebview

Build GUI for your Python program with JavaScript, HTML, and CSS
https://pywebview.flowrl.com
BSD 3-Clause "New" or "Revised" License
4.68k stars 548 forks source link

Running async calls on evaluate_js or add api called eval_async_js. #801

Closed ZhenFTW closed 2 years ago

ZhenFTW commented 2 years ago

Specification

pywebview version: 3.7 operating system: Window web renderer: edgechromium, edgehtml, cef,

Description

Is there a way to run async calls and js promises on evaluate js? fetch and axios returns none or empty {}. I know that I can just use python request and connect it with js api, but running async js and promises (not only about fetch and axios) in scrape is important in my case. It seems evaluate js can only run synchronous calls. Do it need to communicate to js api to do this like using timeout? Hope you add another api just like on selenium (execute_async_script) that can run promises async api. It would help scraping alot and give more option on evaluating js on windows.

r0x0r commented 2 years ago

I haven't thought about this, but this is a valid concern. I suppose something like evaluate_js_async can be implemented using js api. Thoughts on architecture and API are appreciated.

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

CiberNin commented 2 years ago

Figured I would comment here to let it be known that I to also am in possession of a great desire to have a feature of similar functionality to that which has been implicated therein.

r0x0r commented 2 years ago

I have drafted an implementation in the evaluate_js_async branch. See examples\evaluate_js_async.py for usage details. Tested on macOS, but not elsewhere.

CiberNin commented 2 years ago

I cant find any file named evaluate_js_async.py And looking at that branch, it does not seem like you have made any recent commits to the examples folder, only the webview folder.

r0x0r commented 2 years ago

My bad. The async example is pushed now.

CiberNin commented 2 years ago

It's very educational looking through your implementation. What about Asyncio? It's nice to be able to wait for the JS to return something, but it's less useful if we can only wait for one thing at a time. Maybe window.evaluate_js_async that returns an awaitable. And similarly the ability to expose python coroutines.

r0x0r commented 2 years ago

My async python knowledge is limited, so suggestions are welcomed.

CiberNin commented 2 years ago

Well this is my first real project using it, but I think that before you call a python API function you need to use the inspect library to check if the function is a coroutine, and run it with the await keyword if it is. or maybe via asyncio.run_coroutine_threadsafe(coro_func(), loop) if you want to allow the user to pass their own event loop. loop.call_soon_threadsafe(callback, *args, context=None) is also probably important. Here is kind of the simplest API I can think of that I would expect to work. class Api: def __init__(self): self.i = 0 async def IOBoundFunction(self): self.i += 1 await asyncio.sleep(random.randint(1, 30)) return self.i So I could hammer the button that calls "IOBoundFunction" over an over really fast and none of the separate calls would block each other. Also when I call pywebview.api.IOBoundFunction() I would like a way to cancel the coroutine.

CiberNin commented 2 years ago

So I looked into it over the weekend and I think the simplest way to add what I'm looking for is to add an event_loop optional parameter to the window object & three lines to the _call function inside js_bridge_call

def js_bridge_call(window, func_name, param, value_id): async def _call(): try: result = func(*func_params.values()) if inspect.iscoroutinefunction(result): future = asyncio.run_coroutine_threadsafe(result, window.event_loop) result = future.result() result = json.dumps(result).replace('\', '\\').replace('\'', '\\'') code = 'window.pywebview._returnValues["{0}"]["{1}"] = {{value: \'{2}\'}}'.format(func_name, value_id, result)

Plus error checking to yell if we don't actually have an event loop. And it would also be nice to have a way to call future.cancel() from the javascript, but I'm not sure how that would work on the JS end since JS promises can't be canceled.

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

r0x0r commented 2 years ago

@CiberNin This is quite a dramatic change. While I am open to adding async support, it requires throughout planning, on how to integrate it into pywebview properly. One challenge is pywebview threading and how it would interact with asyncio.

Anyhow I have created a PR for my implementation. Comments are welcomed https://github.com/r0x0r/pywebview/pull/843

CiberNin commented 2 years ago

If you use run coroutine thread safe then it should just run the passed async function on the same thread as the passed event loop. So if you allow the user to pass an event loop when constructing a window, it gives them controll over the context in which the async function is called. Future.result will block the calling thread until the async function is complete. Unless Im missing something I think it drops in pretty cleanly.

r0x0r commented 2 years ago

I released async evaluate_js in 3.6

asyncio support will be considered for future releases. Somebody can maybe post an example that would illustrate benefits of asyncio in pywebview.

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] commented 2 years ago

The message to post on the issue when closing it. If none provided, will not comment when closing an issue.

Ratgor commented 9 months ago

Somebody can maybe post an example that would illustrate benefits of asyncio in pywebview.

May be it's not an example, but a use case: multiple small services written as async coroutines to be run in one physical core / thread / process. such as web server, db, task manager, reverse proxy, etc. with web GIU. In this architecture having pywebview UI as a main entrypoint is not cool, and attaching async coros pool as a single server to it not ok.