From 76ba80e5ae3abf15dcdcd555830c129207fcb0e5 Mon Sep 17 00:00:00 2001 From: Michael Currin <18750745+MichaelCurrin@users.noreply.github.com> Date: Fri, 3 Dec 2021 15:28:16 +0200 Subject: [PATCH 01/45] docs: add README.md for example 4 --- examples/04 - file_access/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/04 - file_access/README.md diff --git a/examples/04 - file_access/README.md b/examples/04 - file_access/README.md new file mode 100644 index 00000000..e24ed93e --- /dev/null +++ b/examples/04 - file_access/README.md @@ -0,0 +1,3 @@ +# Example 4 - file access + +![Screenshot](Screenshot.png) From e5246600ff139f72be89256721d6ac4b1ea14cbb Mon Sep 17 00:00:00 2001 From: doug-benn Date: Sun, 5 Feb 2023 10:55:10 +0000 Subject: [PATCH 02/45] Fixed Tests v2 (#651) Fix tests for Windows --- .gitignore | 1 + requirements-test.txt | 1 + tests/conftest.py | 14 ++++++++++-- tests/integration/test_examples.py | 2 ++ tests/utils.py | 35 +++++++++++++++++++++++------- tox.ini | 6 ++--- 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index e152e6ea..781d9ca4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__ dist build +Drivers Eel.egg-info .tmp .DS_Store diff --git a/requirements-test.txt b/requirements-test.txt index a773aeb6..0a9d9728 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,3 +4,4 @@ psutil==5.9.2 pytest==7.0.1 pytest-timeout==2.1.0 selenium==3.141.0 +webdriver_manager==3.7.1 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index b3c3a66f..92b16fab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ import os +import platform from unittest import mock import pytest from selenium import webdriver from selenium.webdriver import DesiredCapabilities +from webdriver_manager.chrome import ChromeDriverManager @pytest.fixture @@ -14,9 +16,17 @@ def driver(): options = webdriver.ChromeOptions() options.headless = True capabilities = DesiredCapabilities.CHROME - capabilities['goog:loggingPrefs'] = {"browser": "ALL"} + capabilities["goog:loggingPrefs"] = {"browser": "ALL"} - driver = webdriver.Chrome(options=options, desired_capabilities=capabilities, service_log_path=os.path.devnull) + if platform.system() == "Windows": + options.binary_location = "C:/Program Files/Google/Chrome/Application/chrome.exe" + + driver = webdriver.Chrome( + ChromeDriverManager().install(), + options=options, + desired_capabilities=capabilities, + service_log_path=os.path.devnull, + ) # Firefox doesn't currently supported pulling JavaScript console logs, which we currently scan to affirm that # JS/Python can communicate in some places. So for now, we can't really use firefox/geckodriver during testing. diff --git a/tests/integration/test_examples.py b/tests/integration/test_examples.py index 63119ef7..ca1556f8 100644 --- a/tests/integration/test_examples.py +++ b/tests/integration/test_examples.py @@ -1,4 +1,5 @@ import os +import time from tempfile import TemporaryDirectory, NamedTemporaryFile from selenium import webdriver @@ -47,6 +48,7 @@ def test_04_file_access(driver: webdriver.Remote): with TemporaryDirectory() as temp_dir, NamedTemporaryFile(dir=temp_dir) as temp_file: driver.find_element_by_id('input-box').clear() driver.find_element_by_id('input-box').send_keys(temp_dir) + time.sleep(0.5) driver.find_element_by_css_selector('button').click() assert driver.find_element_by_id('file-name').text == os.path.basename(temp_file.name) diff --git a/tests/utils.py b/tests/utils.py index 1fdd8f8a..ff14d473 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,7 @@ import contextlib import os +import sys +import platform import subprocess import tempfile import time @@ -8,15 +10,30 @@ import psutil # Path to the test data folder. -TEST_DATA_DIR = Path(__file__).parent / 'data' +TEST_DATA_DIR = Path(__file__).parent / "data" def get_process_listening_port(proc): - psutil_proc = psutil.Process(proc.pid) - while not any(conn.status == 'LISTEN' for conn in psutil_proc.connections()): - time.sleep(0.01) - - conn = next(filter(lambda conn: conn.status == 'LISTEN', psutil_proc.connections())) + conn = None + if platform.system() == "Windows": + current_process = psutil.Process(proc.pid) + children = [] + while children == []: + time.sleep(0.01) + children = current_process.children(recursive=True) + if (3, 6) <= sys.version_info < (3, 7): + children = [current_process] + for child in children: + while child.connections() == [] and not any(conn.status == "LISTEN" for conn in child.connections()): + time.sleep(0.01) + + conn = next(filter(lambda conn: conn.status == "LISTEN", child.connections())) + else: + psutil_proc = psutil.Process(proc.pid) + while not any(conn.status == "LISTEN" for conn in psutil_proc.connections()): + time.sleep(0.01) + + conn = next(filter(lambda conn: conn.status == "LISTEN", psutil_proc.connections())) return conn.laddr.port @@ -40,8 +57,10 @@ def get_eel_server(example_py, start_html): import {os.path.splitext(os.path.basename(example_py))[0]} """) - - proc = subprocess.Popen(['python', test.name], cwd=os.path.dirname(example_py)) + proc = subprocess.Popen( + [sys.executable, test.name], + cwd=os.path.dirname(example_py), + ) eel_port = get_process_listening_port(proc) yield f"http://localhost:{eel_port}/{start_html}" diff --git a/tox.ini b/tox.ini index fe783da7..fefe2470 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = py36,py37,py38,py39,py310 [pytest] -timeout = 5 +timeout = 30 [gh-actions] python = @@ -17,5 +17,5 @@ deps = -r requirements-test.txt commands = # this ugly hack is here because: # https://github.com/tox-dev/tox/issues/149 - pip install -q -r {toxinidir}/requirements-test.txt - {envpython} -m pytest {posargs} + pip install -q -r '{toxinidir}'/requirements-test.txt + '{envpython}' -m pytest {posargs} \ No newline at end of file From 91c11cc119fdba976963342016ce0177c83596a6 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 5 Feb 2023 11:04:49 +0000 Subject: [PATCH 03/45] Broaden requirements Eel should be usable with a fairly generous set of package versions so that it meets more of our users requirements without being overly strict. --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3ed6fe71..af6da07d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -bottle==0.12.20 -bottle-websocket==0.2.9 -gevent==1.3.6 -gevent-websocket==0.10.1 -greenlet==0.4.15 -pyparsing==2.4.7 -whichcraft==0.4.1 +bottle<1.0.0 +bottle-websocket<1.0.0 +gevent +gevent-websocket<1.0.0 +greenlet>=1.0.0,<2.0.0 +pyparsing>=3.0.0,<4.0.0 +whichcraft~=0.4.1 From 6003c3d5de666c218e6ba7438c960e43ba8fe232 Mon Sep 17 00:00:00 2001 From: Mohaned Abd Elmonsef <6644557+mo0haned@users.noreply.github.com> Date: Wed, 26 Aug 2020 20:16:45 +0200 Subject: [PATCH 04/45] Update hello.py "options" is not supported anymore so I changed it with mode = 'electron' --- examples/09 - Eelectron-quick-start/hello.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/examples/09 - Eelectron-quick-start/hello.py b/examples/09 - Eelectron-quick-start/hello.py index c10d9fb7..a59975e1 100644 --- a/examples/09 - Eelectron-quick-start/hello.py +++ b/examples/09 - Eelectron-quick-start/hello.py @@ -1,5 +1,4 @@ import eel - # Set web files folder eel.init('web') @@ -10,11 +9,5 @@ def say_hello_py(x): say_hello_py('Python World!') eel.say_hello_js('Python World!') # Call a Javascript function - -options = { - 'mode': 'custom', - 'args': ['node_modules/electron/dist/electron.exe', '.'] -} - -eel.start('hello.html', options=options) +eel.start('hello.html',mode='electron') #eel.start('hello.html', mode='custom', cmdline_args=['node_modules/electron/dist/electron.exe', '.']) From 474fe7e6fa96fec13752ade3deece8f9919db022 Mon Sep 17 00:00:00 2001 From: Sentinent Date: Fri, 11 Mar 2022 23:36:53 -0800 Subject: [PATCH 05/45] Fix default eel routes not being added to custom app instances --- eel/__init__.py | 21 ++++++++++++++++--- examples/10 - custom_app_routes/custom_app.py | 15 +++++++++++++ .../10 - custom_app_routes/web/index.html | 8 +++++++ tests/integration/test_examples.py | 14 +++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 examples/10 - custom_app_routes/custom_app.py create mode 100644 examples/10 - custom_app_routes/web/index.html diff --git a/eel/__init__.py b/eel/__init__.py index 1f9d8bf4..bd0e1b6d 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -169,9 +169,11 @@ def run_lambda(): HOST = _start_args['host'] app = _start_args['app'] # type: btl.Bottle - for route_path, route_params in BOTTLE_ROUTES.items(): - route_func, route_kwargs = route_params - btl.route(path=route_path, callback=route_func, **route_kwargs) + + if isinstance(app, btl.Bottle): + add_eel_routes(app) + else: + add_eel_routes(btl.default_app()) return btl.run( host=HOST, @@ -264,6 +266,19 @@ def _websocket(ws): "/eel": (_websocket, dict(apply=[wbs.websocket])) } +def add_eel_routes(app): + ''' + Adds eel routes to `app`. Only needed if you are passing something besides `bottle.Bottle` to `eel.start()`. + Ex: + app = bottle.Bottle() + eel.add_eel_routes(app) + middleware = beaker.middleware.SessionMiddleware(app) + eel.start(app=middleware) + ''' + for route_path, route_params in BOTTLE_ROUTES.items(): + route_func, route_kwargs = route_params + app.route(path=route_path, callback=route_func, **route_kwargs) + # Private functions def _safe_json(obj): diff --git a/examples/10 - custom_app_routes/custom_app.py b/examples/10 - custom_app_routes/custom_app.py new file mode 100644 index 00000000..aa62aa5f --- /dev/null +++ b/examples/10 - custom_app_routes/custom_app.py @@ -0,0 +1,15 @@ +import eel +import bottle +from beaker.middleware import SessionMiddleware + +app = bottle.Bottle() +@app.route('/custom') +def custom_route(): + return 'Hello, World!' + +# need to manually add eel routes if we are wrapping our Bottle instance with middleware +eel.add_eel_routes(app) +middleware = SessionMiddleware(app) + +eel.init('web') +eel.start('index.html', app=middleware) \ No newline at end of file diff --git a/examples/10 - custom_app_routes/web/index.html b/examples/10 - custom_app_routes/web/index.html new file mode 100644 index 00000000..ae9b3acb --- /dev/null +++ b/examples/10 - custom_app_routes/web/index.html @@ -0,0 +1,8 @@ + + + + + Hello, World! + + + \ No newline at end of file diff --git a/tests/integration/test_examples.py b/tests/integration/test_examples.py index ca1556f8..effb02fe 100644 --- a/tests/integration/test_examples.py +++ b/tests/integration/test_examples.py @@ -61,3 +61,17 @@ def test_06_jinja_templates(driver: webdriver.Remote): driver.find_element_by_css_selector('a').click() WebDriverWait(driver, 2.0).until(expected_conditions.presence_of_element_located((By.XPATH, '//h1[text()="This is page 2"]'))) + + +def test_10_custom_app(driver: webdriver.Remote): + # test default eel routes are working + with get_eel_server('examples/10 - custom_app_routes/custom_app.py', 'index.html') as eel_url: + driver.get(eel_url) + # we really need to test if the page 404s, but selenium has no support for status codes + # so we just test if we can get our page title + assert driver.title == 'Hello, World!' + + # test default eel routes are working + with get_eel_server('examples/10 - custom_app_routes/custom_app.py', 'custom') as eel_url: + driver.get(eel_url) + assert 'Hello, World!' in driver.page_source \ No newline at end of file From ff5a7efa91c20470d8bb0d5ac0a46c29d1db2a02 Mon Sep 17 00:00:00 2001 From: Sentinent Date: Fri, 11 Mar 2022 23:59:01 -0800 Subject: [PATCH 06/45] Update custom_app_routes example to not use beaker middleware, but include middleware usage in comments --- examples/10 - custom_app_routes/custom_app.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/10 - custom_app_routes/custom_app.py b/examples/10 - custom_app_routes/custom_app.py index aa62aa5f..aafece28 100644 --- a/examples/10 - custom_app_routes/custom_app.py +++ b/examples/10 - custom_app_routes/custom_app.py @@ -1,15 +1,17 @@ import eel import bottle -from beaker.middleware import SessionMiddleware +# from beaker.middleware import SessionMiddleware app = bottle.Bottle() @app.route('/custom') def custom_route(): return 'Hello, World!' +eel.init('web') + # need to manually add eel routes if we are wrapping our Bottle instance with middleware -eel.add_eel_routes(app) -middleware = SessionMiddleware(app) +# eel.add_eel_routes(app) +# middleware = SessionMiddleware(app) +# eel.start('index.html', app=middleware) -eel.init('web') -eel.start('index.html', app=middleware) \ No newline at end of file +eel.start('index.html', app=app) \ No newline at end of file From fa7b30cd1af49f2e78d857859d14d37594589c64 Mon Sep 17 00:00:00 2001 From: Sentinent Date: Sat, 12 Mar 2022 00:37:23 -0800 Subject: [PATCH 07/45] typo: "default eel routes" -> "custom routes" --- tests/integration/test_examples.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_examples.py b/tests/integration/test_examples.py index effb02fe..6e51868d 100644 --- a/tests/integration/test_examples.py +++ b/tests/integration/test_examples.py @@ -71,7 +71,7 @@ def test_10_custom_app(driver: webdriver.Remote): # so we just test if we can get our page title assert driver.title == 'Hello, World!' - # test default eel routes are working + # test custom routes are working with get_eel_server('examples/10 - custom_app_routes/custom_app.py', 'custom') as eel_url: driver.get(eel_url) - assert 'Hello, World!' in driver.page_source \ No newline at end of file + assert 'Hello, World!' in driver.page_source From cc7eea58ef966480feb754aa350c10c5efbb21a1 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 5 Feb 2023 16:42:44 +0000 Subject: [PATCH 08/45] Rename to `register_eel_routes` --- eel/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eel/__init__.py b/eel/__init__.py index bd0e1b6d..898f1da0 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -171,9 +171,9 @@ def run_lambda(): app = _start_args['app'] # type: btl.Bottle if isinstance(app, btl.Bottle): - add_eel_routes(app) + register_eel_routes(app) else: - add_eel_routes(btl.default_app()) + register_eel_routes(btl.default_app()) return btl.run( host=HOST, @@ -266,12 +266,12 @@ def _websocket(ws): "/eel": (_websocket, dict(apply=[wbs.websocket])) } -def add_eel_routes(app): +def register_eel_routes(app): ''' Adds eel routes to `app`. Only needed if you are passing something besides `bottle.Bottle` to `eel.start()`. Ex: app = bottle.Bottle() - eel.add_eel_routes(app) + eel.register_eel_routes(app) middleware = beaker.middleware.SessionMiddleware(app) eel.start(app=middleware) ''' From c83f4f87e2f8f6eec1358306407d41561d9ea788 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 5 Feb 2023 16:43:46 +0000 Subject: [PATCH 09/45] Bump to v0.15.2 To release `eel.register_eel_routes` --- CHANGELOG.md | 3 +++ README.md | 3 +-- examples/10 - custom_app_routes/custom_app.py | 2 +- examples/10 - custom_app_routes/web/index.html | 2 +- setup.py | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d973c4a2..8a3dd497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change log +### v0.15.2 +* Adds `register_eel_routes` to handle applying Eel routes to non-Bottle custom app instances. + ### v0.15.1 * Bump bottle dependency from 0.12.13 to 0.12.20 to address the critical CVE-2022-31799 and moderate CVE-2020-28473. diff --git a/README.md b/README.md index 9d6d1480..4ebc0a11 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,7 @@ As of Eel v0.12.0, the following options are available to `start()`: - **position**, a tuple of ints specifying the (left, top) of the main window in pixels *Default: `None`* - **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: {}* - **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`* - - **app**, an instance of Bottle which will be used rather than creating a fresh one. This can be used to install middleware on the - instance before starting eel, e.g. for session management, authentication, etc. + - **app**, an instance of Bottle which will be used rather than creating a fresh one. This can be used to install middleware on the instance before starting eel, e.g. for session management, authentication, etc. If your `app` is not a Bottle instance, you will need to call `eel.register_eel_routes(app)` on your custom app instance. - **shutdown_delay**, timer configurable for Eel's shutdown detection mechanism, whereby when any websocket closes, it waits `shutdown_delay` seconds, and then checks if there are now any websocket connections. If not, then Eel closes. In case the user has closed the browser and wants to exit the program. By default, the value of **shutdown_delay** is `1.0` second diff --git a/examples/10 - custom_app_routes/custom_app.py b/examples/10 - custom_app_routes/custom_app.py index aafece28..a95ab082 100644 --- a/examples/10 - custom_app_routes/custom_app.py +++ b/examples/10 - custom_app_routes/custom_app.py @@ -14,4 +14,4 @@ def custom_route(): # middleware = SessionMiddleware(app) # eel.start('index.html', app=middleware) -eel.start('index.html', app=app) \ No newline at end of file +eel.start('index.html', app=app) diff --git a/examples/10 - custom_app_routes/web/index.html b/examples/10 - custom_app_routes/web/index.html index ae9b3acb..e802b567 100644 --- a/examples/10 - custom_app_routes/web/index.html +++ b/examples/10 - custom_app_routes/web/index.html @@ -5,4 +5,4 @@ Hello, World! - \ No newline at end of file + diff --git a/setup.py b/setup.py index 4e2c0250..ec65c93d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='Eel', - version='0.15.1', + version='0.15.2', author='Python Eel Organisation', author_email='python-eel@protonmail.com', url='https://github.com/python-eel/Eel', From 6fbc91e2145235517e67cce2e0c76d0df1add7c3 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 5 Feb 2023 16:49:15 +0000 Subject: [PATCH 10/45] Don't cancel all jobs if one fails --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f6c44255..39f07856 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-20.04, windows-latest, macos-latest] python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] From 929c51baff7b1875c11d3f8e8524fdfbd81be8ed Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 12 Feb 2023 21:28:01 +0000 Subject: [PATCH 11/45] Add type hints to eel - Added type hints to cover external and internal APIs - Added py.typed to package_data (PEP 561) - Added mypy configuration - Added typecheck requirements to requirements-test.txt - Update tox and GitHub Actions to have typechecks in CI --- .github/workflows/test.yml | 15 +++++ eel/__init__.py | 128 +++++++++++++++++++++---------------- eel/__main__.py | 23 ++++--- eel/browsers.py | 46 ++++++++----- eel/chrome.py | 24 ++++--- eel/edge.py | 9 ++- eel/electron.py | 14 ++-- eel/py.typed | 0 eel/types.py | 28 ++++++++ mypy.ini | 36 +++++++++++ requirements-meta.txt | 2 +- requirements-test.txt | 5 +- setup.py | 2 +- tox.ini | 11 +++- 14 files changed, 241 insertions(+), 102 deletions(-) create mode 100644 eel/py.typed create mode 100644 eel/types.py create mode 100644 mypy.ini diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39f07856..552a0c67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,3 +26,18 @@ jobs: run: pip3 install -r requirements-meta.txt - name: Run tox tests run: tox -- --durations=0 --timeout=30 + + typecheck: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + - name: Setup test execution environment. + run: pip3 install -r requirements-meta.txt + - name: Run tox tests + run: tox -e typecheck diff --git a/eel/__init__.py b/eel/__init__.py index 898f1da0..fd09b45b 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -1,6 +1,13 @@ from builtins import range import traceback from io import open +from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable, TYPE_CHECKING + +if TYPE_CHECKING: + from eel.types import OptionsDictT, WebSocketT +else: + WebSocketT = Any + OptionsDictT = Any from gevent.threading import Timer import gevent as gvt @@ -17,25 +24,27 @@ import socket import mimetypes + mimetypes.add_type('application/javascript', '.js') -_eel_js_file = pkg.resource_filename('eel', 'eel.js') -_eel_js = open(_eel_js_file, encoding='utf-8').read() -_websockets = [] -_call_return_values = {} -_call_return_callbacks = {} -_call_number = 0 -_exposed_functions = {} -_js_functions = [] -_mock_queue = [] -_mock_queue_done = set() -_shutdown = None +_eel_js_file: str = pkg.resource_filename('eel', 'eel.js') +_eel_js: str = open(_eel_js_file, encoding='utf-8').read() +_websockets: List[Tuple[Any, WebSocketT]] = [] +_call_return_values: Dict[Any, Any] = {} +_call_return_callbacks: Dict[float, Tuple[Callable[..., Any], Optional[Callable[..., Any]]]] = {} +_call_number: int = 0 +_exposed_functions: Dict[Any, Any] = {} +_js_functions: List[Any] = [] +_mock_queue: List[Any] = [] +_mock_queue_done: Set[Any] = set() +_shutdown: Optional[gvt.Greenlet] = None # Later assigned as global by _websocket_close() +root_path: str # Later assigned as global by init() # The maximum time (in milliseconds) that Python will try to retrieve a return value for functions executing in JS # Can be overridden through `eel.init` with the kwarg `js_result_timeout` (default: 10000) -_js_result_timeout = 10000 +_js_result_timeout: int = 10000 # All start() options must provide a default value and explanation here -_start_args = { +_start_args: OptionsDictT = { 'mode': 'chrome', # What browser is used 'host': 'localhost', # Hostname use for Bottle server 'port': 8000, # Port used for Bottle server (use 0 for auto) @@ -51,12 +60,12 @@ 'disable_cache': True, # Sets the no-store response header when serving assets 'default_path': 'index.html', # The default file to retrieve for the root URL 'app': btl.default_app(), # Allows passing in a custom Bottle instance, e.g. with middleware - 'shutdown_delay': 1.0 # how long to wait after a websocket closes before detecting complete shutdown + 'shutdown_delay': 1.0 # how long to wait after a websocket closes before detecting complete shutdown } # == Temporary (suppressible) error message to inform users of breaking API change for v1.0.0 === _start_args['suppress_error'] = False -api_error_message = ''' +api_error_message: str = ''' ---------------------------------------------------------------------------------- 'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel To suppress this error, add 'suppress_error=True' to start() call. @@ -67,15 +76,15 @@ # Public functions -def expose(name_or_function=None): +def expose(name_or_function: Optional[Callable[..., Any]] = None) -> Callable[..., Any]: # Deal with '@eel.expose()' - treat as '@eel.expose' if name_or_function is None: return expose - if type(name_or_function) == str: # Called as '@eel.expose("my_name")' + if isinstance(name_or_function, str): # Called as '@eel.expose("my_name")' name = name_or_function - def decorator(function): + def decorator(function: Callable[..., Any]) -> Any: _expose(name, function) return function return decorator @@ -87,7 +96,7 @@ def decorator(function): # PyParsing grammar for parsing exposed functions in JavaScript code # Examples: `eel.expose(w, "func_name")`, `eel.expose(func_name)`, `eel.expose((function (e){}), "func_name")` -EXPOSED_JS_FUNCTIONS = pp.ZeroOrMore( +EXPOSED_JS_FUNCTIONS: pp.ZeroOrMore = pp.ZeroOrMore( pp.Suppress( pp.SkipTo(pp.Literal('eel.expose(')) + pp.Literal('eel.expose(') @@ -101,8 +110,8 @@ def decorator(function): ) -def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm', - '.xhtml', '.vue'], js_result_timeout=10000): +def init(path: str, allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm', + '.xhtml', '.vue'], js_result_timeout: int = 10000) -> None: global root_path, _js_functions, _js_result_timeout root_path = _get_real_path(path) @@ -133,7 +142,7 @@ def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm', _js_result_timeout = js_result_timeout -def start(*start_urls, **kwargs): +def start(*start_urls: str, **kwargs: Any) -> None: _start_args.update(kwargs) if 'options' in kwargs: @@ -150,6 +159,8 @@ def start(*start_urls, **kwargs): if _start_args['jinja_templates'] != None: from jinja2 import Environment, FileSystemLoader, select_autoescape + if not isinstance(_start_args['jinja_templates'], str): + raise TypeError("'jinja_templates start_arg/option must be of type str'") templates_path = os.path.join(root_path, _start_args['jinja_templates']) _start_args['jinja_env'] = Environment(loader=FileSystemLoader(templates_path), autoescape=select_autoescape(['html', 'xml'])) @@ -162,25 +173,27 @@ def start(*start_urls, **kwargs): # Launch the browser to the starting URLs show(*start_urls) - def run_lambda(): + def run_lambda() -> None: if _start_args['all_interfaces'] == True: HOST = '0.0.0.0' else: + if not isinstance(_start_args['host'], str): + raise TypeError("'host' start_arg/option must be of type str") HOST = _start_args['host'] - app = _start_args['app'] # type: btl.Bottle + app = _start_args['app'] if isinstance(app, btl.Bottle): register_eel_routes(app) else: register_eel_routes(btl.default_app()) - return btl.run( + btl.run( host=HOST, port=_start_args['port'], server=wbs.GeventWebSocketServer, quiet=True, - app=app) + app=app) # Always returns None # Start the webserver if _start_args['block']: @@ -189,20 +202,20 @@ def run_lambda(): spawn(run_lambda) -def show(*start_urls): - brw.open(start_urls, _start_args) +def show(*start_urls: str) -> None: + brw.open(list(start_urls), _start_args) -def sleep(seconds): +def sleep(seconds: Union[int, float]) -> None: gvt.sleep(seconds) -def spawn(function, *args, **kwargs): +def spawn(function: Callable[..., Any], *args: Any, **kwargs: Any) -> gvt.Greenlet: return gvt.spawn(function, *args, **kwargs) # Bottle Routes -def _eel(): +def _eel() -> str: start_geometry = {'default': {'size': _start_args['size'], 'position': _start_args['position']}, 'pages': _start_args['geometry']} @@ -215,16 +228,20 @@ def _eel(): _set_response_headers(btl.response) return page -def _root(): +def _root() -> Optional[btl.Response]: + if not isinstance(_start_args['default_path'], str): + raise TypeError("'default_path' start_arg/option must be of type str") return _static(_start_args['default_path']) -def _static(path): +def _static(path: str) -> Optional[btl.Response]: response = None if 'jinja_env' in _start_args and 'jinja_templates' in _start_args: + if not isinstance(_start_args['jinja_templates'], str): + raise TypeError("'jinja_templates' start_arg/option must be of type str") template_prefix = _start_args['jinja_templates'] + '/' if path.startswith(template_prefix): n = len(template_prefix) - template = _start_args['jinja_env'].get_template(path[n:]) + template = _start_args['jinja_env'].get_template(path[n:]) # type: ignore # depends on conditional import in start() response = btl.HTTPResponse(template.render()) if response is None: @@ -233,7 +250,7 @@ def _static(path): _set_response_headers(response) return response -def _websocket(ws): +def _websocket(ws: WebSocketT) -> None: global _websockets for js_function in _js_functions: @@ -259,14 +276,14 @@ def _websocket(ws): _websocket_close(page) -BOTTLE_ROUTES = { +BOTTLE_ROUTES: Dict[str, Tuple[Callable[..., Any], Dict[Any, Any]]] = { "/eel.js": (_eel, dict()), "/": (_root, dict()), "/": (_static, dict()), "/eel": (_websocket, dict(apply=[wbs.websocket])) } -def register_eel_routes(app): +def register_eel_routes(app: btl.Bottle) -> None: ''' Adds eel routes to `app`. Only needed if you are passing something besides `bottle.Bottle` to `eel.start()`. Ex: @@ -281,11 +298,11 @@ def register_eel_routes(app): # Private functions -def _safe_json(obj): +def _safe_json(obj: Any) -> str: return jsn.dumps(obj, default=lambda o: None) -def _repeated_send(ws, msg): +def _repeated_send(ws: WebSocketT, msg: str) -> None: for attempt in range(100): try: ws.send(msg) @@ -294,7 +311,7 @@ def _repeated_send(ws, msg): sleep(0.001) -def _process_message(message, ws): +def _process_message(message: Dict[str, Any], ws: WebSocketT) -> None: if 'call' in message: error_info = {} try: @@ -326,47 +343,48 @@ def _process_message(message, ws): print('Invalid message received: ', message) -def _get_real_path(path): +def _get_real_path(path: str) -> str: if getattr(sys, 'frozen', False): - return os.path.join(sys._MEIPASS, path) + return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstaller else: return os.path.abspath(path) -def _mock_js_function(f): +def _mock_js_function(f: str) -> None: exec('%s = lambda *args: _mock_call("%s", args)' % (f, f), globals()) -def _import_js_function(f): +def _import_js_function(f: str) -> None: exec('%s = lambda *args: _js_call("%s", args)' % (f, f), globals()) -def _call_object(name, args): +def _call_object(name: str, args: Any) -> Dict[str, Any]: global _call_number _call_number += 1 call_id = _call_number + rnd.random() return {'call': call_id, 'name': name, 'args': args} -def _mock_call(name, args): +def _mock_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: call_object = _call_object(name, args) global _mock_queue _mock_queue += [call_object] return _call_return(call_object) -def _js_call(name, args): +def _js_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: call_object = _call_object(name, args) for _, ws in _websockets: _repeated_send(ws, _safe_json(call_object)) return _call_return(call_object) -def _call_return(call): +def _call_return(call: Dict[str, Any]) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: global _js_result_timeout call_id = call['call'] - def return_func(callback=None, error_callback=None): + def return_func(callback: Optional[Callable[..., Any]] = None, + error_callback: Optional[Callable[..., Any]] = None) -> Any: if callback is not None: _call_return_callbacks[call_id] = (callback, error_callback) else: @@ -377,33 +395,35 @@ def return_func(callback=None, error_callback=None): return return_func -def _expose(name, function): +def _expose(name: str, function: Callable[..., Any]) -> None: msg = 'Already exposed function with name "%s"' % name assert name not in _exposed_functions, msg _exposed_functions[name] = function -def _detect_shutdown(): +def _detect_shutdown() -> None: if len(_websockets) == 0: sys.exit() -def _websocket_close(page): +def _websocket_close(page: str) -> None: global _shutdown close_callback = _start_args.get('close_callback') if close_callback is not None: + if not callable(close_callback): + raise TypeError("'close_callback' start_arg/option must be callable or None") sockets = [p for _, p in _websockets] close_callback(page, sockets) else: - if _shutdown: + if isinstance(_shutdown, gvt.Greenlet): _shutdown.kill() _shutdown = gvt.spawn_later(_start_args['shutdown_delay'], _detect_shutdown) -def _set_response_headers(response): +def _set_response_headers(response: btl.Response) -> None: if _start_args['disable_cache']: # https://stackoverflow.com/a/24748094/280852 response.set_header('Cache-Control', 'no-store') diff --git a/eel/__main__.py b/eel/__main__.py index f5a81601..b4027eb6 100644 --- a/eel/__main__.py +++ b/eel/__main__.py @@ -1,9 +1,10 @@ import pkg_resources as pkg import PyInstaller.__main__ as pyi import os -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace +from typing import List -parser = ArgumentParser(description=""" +parser: ArgumentParser = ArgumentParser(description=""" Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps, with full access to Python capabilities and libraries. """) @@ -17,20 +18,22 @@ type=str, help="Folder including all web files including file as html, css, ico, etc." ) +args: Namespace +unknown_args: List[str] args, unknown_args = parser.parse_known_args() -main_script = args.main_script -web_folder = args.web_folder +main_script: str = args.main_script +web_folder: str = args.web_folder print("Building executable with main script '%s' and web folder '%s'...\n" % (main_script, web_folder)) -eel_js_file = pkg.resource_filename('eel', 'eel.js') -js_file_arg = '%s%seel' % (eel_js_file, os.pathsep) -web_folder_arg = '%s%s%s' % (web_folder, os.pathsep, web_folder) +eel_js_file: str = pkg.resource_filename('eel', 'eel.js') +js_file_arg: str = '%s%seel' % (eel_js_file, os.pathsep) +web_folder_arg: str = '%s%s%s' % (web_folder, os.pathsep, web_folder) -needed_args = ['--hidden-import', 'bottle_websocket', - '--add-data', js_file_arg, '--add-data', web_folder_arg] -full_args = [main_script] + needed_args + unknown_args +needed_args: List[str] = ['--hidden-import', 'bottle_websocket', + '--add-data', js_file_arg, '--add-data', web_folder_arg] +full_args: List[str] = [main_script] + needed_args + unknown_args print('Running:\npyinstaller', ' '.join(full_args), '\n') pyi.run(full_args) diff --git a/eel/browsers.py b/eel/browsers.py index 79639141..d555b2d9 100644 --- a/eel/browsers.py +++ b/eel/browsers.py @@ -1,53 +1,65 @@ import subprocess as sps import webbrowser as wbr +from typing import Union, List, Dict, Iterable, Optional +from types import ModuleType +from eel.types import OptionsDictT import eel.chrome as chm import eel.electron as ele import eel.edge as edge #import eel.firefox as ffx TODO #import eel.safari as saf TODO -_browser_paths = {} -_browser_modules = {'chrome': chm, - 'electron': ele, - 'edge': edge} +_browser_paths: Dict[str, str] = {} +_browser_modules: Dict[str, ModuleType] = {'chrome': chm, + 'electron': ele, + 'edge': edge} -def _build_url_from_dict(page, options): +def _build_url_from_dict(page: Dict[str, str], options: OptionsDictT) -> str: scheme = page.get('scheme', 'http') host = page.get('host', 'localhost') port = page.get('port', options["port"]) path = page.get('path', '') - return '%s://%s:%d/%s' % (scheme, host, port, path) + if not isinstance(port, (int, str)): + raise TypeError("'port' option must be an integer") + return '%s://%s:%d/%s' % (scheme, host, int(port), path) -def _build_url_from_string(page, options): - base_url = 'http://%s:%d/' % (options['host'], options['port']) +def _build_url_from_string(page: str, options: OptionsDictT) -> str: + if not isinstance(options['port'], (int, str)): + raise TypeError("'port' option must be an integer") + base_url = 'http://%s:%d/' % (options['host'], int(options['port'])) return base_url + page -def _build_urls(start_pages, options): - urls = [] +def _build_urls(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDictT) -> List[str]: + urls: List[str] = [] for page in start_pages: - method = _build_url_from_dict if isinstance( - page, dict) else _build_url_from_string - url = method(page, options) + if isinstance(page, dict): + url = _build_url_from_dict(page, options) + else: + url = _build_url_from_string(page, options) urls.append(url) return urls -def open(start_pages, options): +def open(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDictT) -> None: # Build full URLs for starting pages (including host and port) start_urls = _build_urls(start_pages, options) mode = options.get('mode') - if mode in [None, False]: + if not isinstance(mode, (str, bool, type(None))) or mode is True: + raise TypeError("'mode' option must by either a string, False, or None") + if mode is None or mode is False: # Don't open a browser pass elif mode == 'custom': # Just run whatever command the user provided + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") sps.Popen(options['cmdline_args'], stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE) elif mode in _browser_modules: @@ -69,10 +81,10 @@ def open(start_pages, options): wbr.open(url) -def set_path(browser_name, path): +def set_path(browser_name: str, path: str) -> None: _browser_paths[browser_name] = path -def get_path(browser_name): +def get_path(browser_name: str) -> Optional[str]: return _browser_paths.get(browser_name) diff --git a/eel/chrome.py b/eel/chrome.py index f827b20b..c07356f0 100644 --- a/eel/chrome.py +++ b/eel/chrome.py @@ -1,22 +1,27 @@ import sys, subprocess as sps, os +from typing import List, Optional + +from eel.types import OptionsDictT # Every browser specific module must define run(), find_path() and name like this -name = 'Google Chrome/Chromium' +name: str = 'Google Chrome/Chromium' -def run(path, options, start_urls): +def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") if options['app_mode']: for url in start_urls: sps.Popen([path, '--app=%s' % url] + options['cmdline_args'], stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE) else: - args = options['cmdline_args'] + start_urls + args: List[str] = options['cmdline_args'] + start_urls sps.Popen([path, '--new-window'] + args, stdout=sps.PIPE, stderr=sys.stderr, stdin=sps.PIPE) -def find_path(): +def find_path() -> Optional[str]: if sys.platform in ['win32', 'win64']: return _find_chrome_win() elif sys.platform == 'darwin': @@ -27,7 +32,7 @@ def find_path(): return None -def _find_chrome_mac(): +def _find_chrome_mac() -> Optional[str]: default_dir = r'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' if os.path.exists(default_dir): return default_dir @@ -39,7 +44,7 @@ def _find_chrome_mac(): return None -def _find_chromium_mac(): +def _find_chromium_mac() -> Optional[str]: default_dir = r'/Applications/Chromium.app/Contents/MacOS/Chromium' if os.path.exists(default_dir): return default_dir @@ -51,7 +56,7 @@ def _find_chromium_mac(): return None -def _find_chrome_linux(): +def _find_chrome_linux() -> Optional[str]: import whichcraft as wch chrome_names = ['chromium-browser', 'chromium', @@ -61,13 +66,14 @@ def _find_chrome_linux(): for name in chrome_names: chrome = wch.which(name) if chrome is not None: - return chrome + return chrome # type: ignore # whichcraft doesn't currently have type hints return None -def _find_chrome_win(): +def _find_chrome_win() -> Optional[str]: import winreg as reg reg_path = r'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' + chrome_path: Optional[str] = None for install_type in reg.HKEY_CURRENT_USER, reg.HKEY_LOCAL_MACHINE: try: diff --git a/eel/edge.py b/eel/edge.py index cef818aa..7f2dab1e 100644 --- a/eel/edge.py +++ b/eel/edge.py @@ -1,16 +1,19 @@ import platform import subprocess as sps import sys +from typing import List -name = 'Edge' +from eel.types import OptionsDictT +name: str = 'Edge' -def run(_path, options, start_urls): + +def run(_path: str, options: OptionsDictT, start_urls: List[str]) -> None: cmd = 'start microsoft-edge:{}'.format(start_urls[0]) sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) -def find_path(): +def find_path() -> bool: if platform.system() == 'Windows': return True diff --git a/eel/electron.py b/eel/electron.py index 7a443025..14cbc802 100644 --- a/eel/electron.py +++ b/eel/electron.py @@ -1,24 +1,30 @@ +#from __future__ import annotations import sys import os import subprocess as sps import whichcraft as wch +from typing import List, Optional -name = 'Electron' +from eel.types import OptionsDictT -def run(path, options, start_urls): +name: str = 'Electron' + +def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") cmd = [path] + options['cmdline_args'] cmd += ['.', ';'.join(start_urls)] sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE) -def find_path(): +def find_path() -> Optional[str]: if sys.platform in ['win32', 'win64']: # It doesn't work well passing the .bat file to Popen, so we get the actual .exe bat_path = wch.which('electron') return os.path.join(bat_path, r'..\node_modules\electron\dist\electron.exe') elif sys.platform in ['darwin', 'linux']: # This should work find... - return wch.which('electron') + return wch.which('electron') # type: ignore # whichcraft doesn't currently have type hints else: return None diff --git a/eel/py.typed b/eel/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/eel/types.py b/eel/types.py new file mode 100644 index 00000000..55475816 --- /dev/null +++ b/eel/types.py @@ -0,0 +1,28 @@ +from typing import Union, Dict, List, Tuple, Callable, Optional, Any, TYPE_CHECKING + +# This business is slightly awkward, but needed for backward compatibility, +# because Python < 3.7 doesn't have __future__/annotations, and <3.10 doesn't +# support TypeAlias. +if TYPE_CHECKING: + from jinja2 import Environment + try: + from typing import TypeAlias # Introduced in Python 3.10 + JinjaEnvironmentT: TypeAlias = Environment + except ImportError: + JinjaEnvironmentT = Environment # type: ignore + from geventwebsocket.websocket import WebSocket + WebSocketT = WebSocket +else: + JinjaEnvironmentT = None + WebSocketT = Any + +OptionsDictT = Dict[ + str, + Optional[ + Union[ + str, bool, int, float, + List[str], Tuple[int, int], Dict[str, Tuple[int, int]], + Callable[..., Any], JinjaEnvironmentT + ] + ] + ] diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..ba16833d --- /dev/null +++ b/mypy.ini @@ -0,0 +1,36 @@ +[mypy] +python_version = 3.10 +warn_unused_configs = True + +[mypy-jinja2] +ignore_missing_imports = True + +[mypy-gevent] +ignore_missing_imports = True + +[mypy-gevent.threading] +ignore_missing_imports = True + +[mypy-geventwebsocket.websocket] +ignore_missing_imports = True + +[mypy-bottle] +ignore_missing_imports = True + +[mypy-bottle.ext] +ignore_missing_imports = True + +[mypy-bottle.ext.websocket] +ignore_missing_imports = True + +[mypy-whichcraft] +ignore_missing_imports = True + +[mypy-pyparsing] +ignore_missing_imports = True + +[mypy-PyInstaller] +ignore_missing_imports = True + +[mypy-PyInstaller.__main__] +ignore_missing_imports = True diff --git a/requirements-meta.txt b/requirements-meta.txt index 0ebe9256..f4d054a7 100644 --- a/requirements-meta.txt +++ b/requirements-meta.txt @@ -1,4 +1,4 @@ tox>=3.15.2,<4.0.0 tox-pyenv==1.1.0 -tox-gh-actions==1.3.0 +tox-gh-actions==2.0.0 virtualenv>=16.7.10 diff --git a/requirements-test.txt b/requirements-test.txt index 0a9d9728..9d145d8c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,4 +4,7 @@ psutil==5.9.2 pytest==7.0.1 pytest-timeout==2.1.0 selenium==3.141.0 -webdriver_manager==3.7.1 \ No newline at end of file +webdriver_manager==3.7.1 +mypy==0.971 +pyinstaller==4.10 +types-setuptools==67.2.0.1 diff --git a/setup.py b/setup.py index ec65c93d..7df8e22e 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ url='https://github.com/python-eel/Eel', packages=['eel'], package_data={ - 'eel': ['eel.js'], + 'eel': ['eel.js', 'py.typed'], }, install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'whichcraft'], extras_require={ diff --git a/tox.ini b/tox.ini index fefe2470..2d292bbf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,py39,py310 +envlist = typecheck,py{36,37,38,39,310} [pytest] timeout = 30 @@ -13,9 +13,16 @@ python = 3.10: py310 [testenv] +description = run py.test tests deps = -r requirements-test.txt commands = # this ugly hack is here because: # https://github.com/tox-dev/tox/issues/149 pip install -q -r '{toxinidir}'/requirements-test.txt - '{envpython}' -m pytest {posargs} \ No newline at end of file + '{envpython}' -m pytest {posargs} + +[testenv:typecheck] +description = run type checks +deps = -r requirements-test.txt +commands = + mypy --strict {posargs:eel} From 28d359e632c28e1725c4576edae512c772666011 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 13 Feb 2023 14:56:15 +0000 Subject: [PATCH 12/45] Bump version to 0.15.3 Prepare a release for v0.15.3 to allow users to benefit from the new type hints added by @thatfloflo --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3dd497..5f229aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change log +### v0.15.3 +* Comprehensive type hints implement by @thatfloflo in https://github.com/python-eel/Eel/pull/577. + ### v0.15.2 * Adds `register_eel_routes` to handle applying Eel routes to non-Bottle custom app instances. diff --git a/setup.py b/setup.py index 7df8e22e..c0653119 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='Eel', - version='0.15.2', + version='0.15.3', author='Python Eel Organisation', author_email='python-eel@protonmail.com', url='https://github.com/python-eel/Eel', From 498709fd8e658db0663b60105b8d8456e412d198 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 20 Feb 2023 20:07:52 +0000 Subject: [PATCH 13/45] Drop support for python 3.6, which is EOL --- .github/workflows/test.yml | 2 +- .python-version | 3 +-- CHANGELOG.md | 3 +++ README-developers.md | 4 ++-- setup.py | 5 ++--- tox.ini | 3 +-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 552a0c67..b72275a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-20.04, windows-latest, macos-latest] - python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10"] steps: - name: Checkout repository diff --git a/.python-version b/.python-version index 551bdc68..543423dd 100644 --- a/.python-version +++ b/.python-version @@ -1,5 +1,4 @@ -3.6.15 3.7.14 3.8.14 3.9.13 -3.10.8 \ No newline at end of file +3.10.8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f229aa0..60b15157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change log +### v0.16.0 +* Drop support for Python versions below 3.7 + ### v0.15.3 * Comprehensive type hints implement by @thatfloflo in https://github.com/python-eel/Eel/pull/577. diff --git a/README-developers.md b/README-developers.md index d628b753..a0d00d6c 100644 --- a/README-developers.md +++ b/README-developers.md @@ -29,7 +29,7 @@ pip3 install -r requirements-meta.txt # tox ``` ### (Recommended) Run Automated Tests -Tox is configured to run tests against each major version we support (3.6+). In order to run Tox as configured, you will need to install multiple versions of Python. See the pinned minor versions in `.python-version` for recommendations. +Tox is configured to run tests against each major version we support (3.7+). In order to run Tox as configured, you will need to install multiple versions of Python. See the pinned minor versions in `.python-version` for recommendations. #### Tox Setup Our Tox configuration requires [Chrome](https://www.google.com/chrome) and [ChromeDriver](https://chromedriver.chromium.org/home). See each of those respective project pages for more information on setting each up. @@ -38,7 +38,7 @@ Our Tox configuration requires [Chrome](https://www.google.com/chrome) and [Chro #### Running Tests -To test Eel against a specific version of Python you have installed, e.g. Python 3.6 in this case, run: +To test Eel against a specific version of Python you have installed, e.g. Python 3.7 in this case, run: ```bash tox -e py36 diff --git a/setup.py b/setup.py index c0653119..ca76d2d0 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='Eel', - version='0.15.3', + version='0.16.0', author='Python Eel Organisation', author_email='python-eel@protonmail.com', url='https://github.com/python-eel/Eel', @@ -18,7 +18,7 @@ extras_require={ "jinja2": ['jinja2>=2.10'] }, - python_requires='>=3.6', + python_requires='>=3.7', description='For little HTML GUI applications, with easy Python/JS interop', long_description=long_description, long_description_content_type='text/markdown', @@ -30,7 +30,6 @@ 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows :: Windows 10', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', diff --git a/tox.ini b/tox.ini index 2d292bbf..a8d977aa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,11 @@ [tox] -envlist = typecheck,py{36,37,38,39,310} +envlist = typecheck,py{37,38,39,310} [pytest] timeout = 30 [gh-actions] python = - 3.6: py36 3.7: py37 3.8: py38 3.9: py39 From 172d35eae74e314609fa5fc1e69e0d60e6a01493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lipt=C3=A1k?= Date: Sun, 26 Feb 2023 21:40:11 -0500 Subject: [PATCH 14/45] Delete LGTM badges from README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gábor Lipták --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 4ebc0a11..6b70d27d 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,6 @@ ![Python](https://img.shields.io/pypi/pyversions/Eel?style=for-the-badge) [![License](https://img.shields.io/pypi/l/Eel.svg?style=for-the-badge)](https://pypi.org/project/Eel/) - -[![Total alerts](https://img.shields.io/lgtm/alerts/g/samuelhwilliams/Eel.svg?logo=lgtm&style=for-the-badge)](https://lgtm.com/projects/g/samuelhwilliams/Eel/alerts/) -[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/samuelhwilliams/Eel.svg?logo=lgtm&style=for-the-badge)](https://lgtm.com/projects/g/samuelhwilliams/Eel/context:javascript) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/samuelhwilliams/Eel.svg?logo=lgtm&style=for-the-badge)](https://lgtm.com/projects/g/samuelhwilliams/Eel/context:python) - - Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps, with full access to Python capabilities and libraries. > **Eel hosts a local webserver, then lets you annotate functions in Python so that they can be called from Javascript, and vice versa.** From 6dc13275ac81be47b29ba98a3ceef67fca797700 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 21:57:41 +0100 Subject: [PATCH 15/45] Update test.yml workflow branch --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b72275a4..bafba400 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test Eel on: push: - branches: [ master ] + branches: [ main ] pull_request: jobs: From b4836d0f76f6635eba665f6b38328c803b221066 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 21:58:24 +0100 Subject: [PATCH 16/45] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a1d535a0..a1621116 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,10 +2,10 @@ name: "CodeQL" on: push: - branches: [master] + branches: [main] pull_request: # The branches below must be a subset of the branches above - branches: [master] + branches: [main] schedule: - cron: '0 11 * * 0' From 51ea94663e05a6a9e92250399e856e63663d51d7 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 22:18:54 +0100 Subject: [PATCH 17/45] Fix tests for new selenium+webdriver --- .python-version | 5 +---- requirements-test.txt | 4 ++-- tests/conftest.py | 11 ++++------- tests/integration/test_examples.py | 10 +++++----- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/.python-version b/.python-version index 543423dd..c8cfe395 100644 --- a/.python-version +++ b/.python-version @@ -1,4 +1 @@ -3.7.14 -3.8.14 -3.9.13 -3.10.8 +3.10 diff --git a/requirements-test.txt b/requirements-test.txt index 9d145d8c..84701082 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,8 +3,8 @@ psutil==5.9.2 pytest==7.0.1 pytest-timeout==2.1.0 -selenium==3.141.0 -webdriver_manager==3.7.1 +selenium>=4.0.0,<5.0.0 +webdriver_manager>=4.0.0,<5.0.0 mypy==0.971 pyinstaller==4.10 types-setuptools==67.2.0.1 diff --git a/tests/conftest.py b/tests/conftest.py index 92b16fab..e222ab58 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import pytest from selenium import webdriver -from selenium.webdriver import DesiredCapabilities +from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager @@ -14,18 +14,15 @@ def driver(): if TEST_BROWSER == "chrome": options = webdriver.ChromeOptions() - options.headless = True - capabilities = DesiredCapabilities.CHROME - capabilities["goog:loggingPrefs"] = {"browser": "ALL"} + options.add_argument('--headless=new') + options.set_capability("goog:loggingPrefs", {"browser": "ALL"}) if platform.system() == "Windows": options.binary_location = "C:/Program Files/Google/Chrome/Application/chrome.exe" driver = webdriver.Chrome( - ChromeDriverManager().install(), + service=ChromeService(ChromeDriverManager().install(), log_output=os.path.devnull), options=options, - desired_capabilities=capabilities, - service_log_path=os.path.devnull, ) # Firefox doesn't currently supported pulling JavaScript console logs, which we currently scan to affirm that diff --git a/tests/integration/test_examples.py b/tests/integration/test_examples.py index 6e51868d..53dff2a0 100644 --- a/tests/integration/test_examples.py +++ b/tests/integration/test_examples.py @@ -46,12 +46,12 @@ def test_04_file_access(driver: webdriver.Remote): assert driver.title == "Eel Demo" with TemporaryDirectory() as temp_dir, NamedTemporaryFile(dir=temp_dir) as temp_file: - driver.find_element_by_id('input-box').clear() - driver.find_element_by_id('input-box').send_keys(temp_dir) + driver.find_element(value='input-box').clear() + driver.find_element(value='input-box').send_keys(temp_dir) time.sleep(0.5) - driver.find_element_by_css_selector('button').click() + driver.find_element(By.CSS_SELECTOR, 'button').click() - assert driver.find_element_by_id('file-name').text == os.path.basename(temp_file.name) + assert driver.find_element(value='file-name').text == os.path.basename(temp_file.name) def test_06_jinja_templates(driver: webdriver.Remote): @@ -59,7 +59,7 @@ def test_06_jinja_templates(driver: webdriver.Remote): driver.get(eel_url) assert driver.title == "Hello, World!" - driver.find_element_by_css_selector('a').click() + driver.find_element(By.CSS_SELECTOR, 'a').click() WebDriverWait(driver, 2.0).until(expected_conditions.presence_of_element_located((By.XPATH, '//h1[text()="This is page 2"]'))) From 9d40c8f28d61fda3e92eeb0a6f8e158045cbdd61 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 22:25:24 +0100 Subject: [PATCH 18/45] Remove logging redirect --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e222ab58..30ca7198 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ def driver(): options.binary_location = "C:/Program Files/Google/Chrome/Application/chrome.exe" driver = webdriver.Chrome( - service=ChromeService(ChromeDriverManager().install(), log_output=os.path.devnull), + service=ChromeService(ChromeDriverManager().install()), options=options, ) From b01e6f9a55972365993296c67cbc2c0ff1e43aa0 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 22:36:22 +0100 Subject: [PATCH 19/45] Remove macos python 3.7 check - no longer available --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bafba400..104194b7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,9 @@ jobs: matrix: os: [ubuntu-20.04, windows-latest, macos-latest] python-version: [3.7, 3.8, 3.9, "3.10"] + exclude: + - os: macos-latest + python-version: 3.7 steps: - name: Checkout repository From f3c636ee8314c38e036c84e3e6e38aabc0c2faaf Mon Sep 17 00:00:00 2001 From: Philip du Plessis Date: Thu, 22 Feb 2024 22:47:02 +0200 Subject: [PATCH 20/45] Fix WebSocket import error for Python 3.12 compatibility --- eel/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eel/__init__.py b/eel/__init__.py index fd09b45b..0dfa8864 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -13,7 +13,10 @@ import gevent as gvt import json as jsn import bottle as btl -import bottle.ext.websocket as wbs +try: + import bottle_websocket as wbs +except ImportError: + import bottle.ext.websocket as wbs import re as rgx import os import eel.browsers as brw From 739f6a4063cd4bd2db20871225acd5a610849254 Mon Sep 17 00:00:00 2001 From: Philip du Plessis Date: Thu, 22 Feb 2024 22:51:59 +0200 Subject: [PATCH 21/45] Added tox versions for python 3.11 and 3.12 --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index a8d977aa..0e3c2fed 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,9 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + [testenv] description = run py.test tests From 4c29ca651f25e194fa0378617a28d896a10a0de3 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 22:49:36 +0100 Subject: [PATCH 22/45] Add python 3.11+3.12 to test matrix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 104194b7..b8451d7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-20.04, windows-latest, macos-latest] - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] exclude: - os: macos-latest python-version: 3.7 From 765321715bbe7b028eabdb58530d7b45e2b5b13e Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 22:52:05 +0100 Subject: [PATCH 23/45] Add py311 and py312 to tox envlist --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0e3c2fed..272bb77e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = typecheck,py{37,38,39,310} +envlist = typecheck,py{37,38,39,310,311,312} [pytest] timeout = 30 From e9c122c698db7a883578fc936a54df5b8932f3eb Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 22:56:10 +0100 Subject: [PATCH 24/45] Update test dependencies to suit all python versions --- requirements-test.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 84701082..44bfd622 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,10 +1,10 @@ .[jinja2] -psutil==5.9.2 -pytest==7.0.1 -pytest-timeout==2.1.0 +psutil>=5.0.0,<6.0.0 +pytest>=7.0.0,<8.0.0 +pytest-timeout>=2.0.0,<3.0.0 selenium>=4.0.0,<5.0.0 webdriver_manager>=4.0.0,<5.0.0 mypy==0.971 -pyinstaller==4.10 -types-setuptools==67.2.0.1 +pyinstaller +types-setuptools From 70adbf7efa9079532c4f250f03775b8966d6b95d Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 23:03:04 +0100 Subject: [PATCH 25/45] Add setuptools for python3.12 --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 272bb77e..2bda8001 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,9 @@ python = [testenv] description = run py.test tests -deps = -r requirements-test.txt +deps = + -r requirements-test.txt + setuptools commands = # this ugly hack is here because: # https://github.com/tox-dev/tox/issues/149 From 3926a09afbeaea3e14d25c7649ef6facfee07266 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 23:07:32 +0100 Subject: [PATCH 26/45] move setuptools to meta requirements --- requirements-meta.txt | 1 + tox.ini | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements-meta.txt b/requirements-meta.txt index f4d054a7..db1fb74b 100644 --- a/requirements-meta.txt +++ b/requirements-meta.txt @@ -2,3 +2,4 @@ tox>=3.15.2,<4.0.0 tox-pyenv==1.1.0 tox-gh-actions==2.0.0 virtualenv>=16.7.10 +setuptools diff --git a/tox.ini b/tox.ini index 2bda8001..272bb77e 100644 --- a/tox.ini +++ b/tox.ini @@ -16,9 +16,7 @@ python = [testenv] description = run py.test tests -deps = - -r requirements-test.txt - setuptools +deps = -r requirements-test.txt commands = # this ugly hack is here because: # https://github.com/tox-dev/tox/issues/149 From 682f504f361c03266d834dd63fb8a8edf11c1e15 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 23:19:47 +0100 Subject: [PATCH 27/45] Bump to version 0.17.0 --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b15157..32381939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change log +### 0.17.0 +* Adds support for Python 3.11 and Python 3.12 + ### v0.16.0 * Drop support for Python versions below 3.7 diff --git a/setup.py b/setup.py index ca76d2d0..03dccdf0 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='Eel', - version='0.16.0', + version='0.17.0', author='Python Eel Organisation', author_email='python-eel@protonmail.com', url='https://github.com/python-eel/Eel', From cef000992039f03df61e6f156a8ed65ed98957a5 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 23:36:39 +0100 Subject: [PATCH 28/45] Install setuptools for publish action --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 25a8949c..3559c5d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,8 @@ jobs: with: python-version: 3.x architecture: x64 + - name: Install setuptools + run: pip install setuptools - name: Build a source distribution run: python setup.py sdist - name: Publish to prod PyPI From e53765391f53fd6ebaa66df729612be267440776 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 23:45:11 +0100 Subject: [PATCH 29/45] Update testing workflow --- .github/workflows/test.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8451d7d..9bddaadf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,13 +2,14 @@ name: Test Eel on: push: - branches: [ main ] + branches: [main] pull_request: + # The branches below must be a subset of the branches above + branches: [main] + workflow_dispatch: jobs: test: - runs-on: ${{ matrix.os }} - strategy: fail-fast: false matrix: @@ -18,6 +19,8 @@ jobs: - os: macos-latest python-version: 3.7 + runs-on: ${{ matrix.os }} + steps: - name: Checkout repository uses: actions/checkout@v2 @@ -31,7 +34,11 @@ jobs: run: tox -- --durations=0 --timeout=30 typecheck: - runs-on: windows-latest + strategy: + matrix: + os: [windows-latest] + + runs-on: ${{ matrix.os }} steps: - name: Checkout repository @@ -39,7 +46,7 @@ jobs: - name: Setup python uses: actions/setup-python@v2 with: - python-version: "3.10" + python-version: "3.x" - name: Setup test execution environment. run: pip3 install -r requirements-meta.txt - name: Run tox tests From 734c60d85a1fbf25fd5dfb3678021feba01f5c3d Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 23:46:31 +0100 Subject: [PATCH 30/45] Ignore bottle_websocket for now --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index ba16833d..6fa89652 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,6 +2,9 @@ python_version = 3.10 warn_unused_configs = True +[mypy-bottle_websocket] +ignore_missing_imports = True + [mypy-jinja2] ignore_missing_imports = True From 26057dda4d437947cb6873e439684256b03ab9fa Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 11 Jul 2024 23:48:13 +0100 Subject: [PATCH 31/45] Update mypy version --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 44bfd622..4ee6b5dc 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,6 +5,6 @@ pytest>=7.0.0,<8.0.0 pytest-timeout>=2.0.0,<3.0.0 selenium>=4.0.0,<5.0.0 webdriver_manager>=4.0.0,<5.0.0 -mypy==0.971 +mypy>=1.0.0,<2.0.0 pyinstaller types-setuptools From 094d4463ff99dd737e50bc2dabdda603a81b8817 Mon Sep 17 00:00:00 2001 From: Pintu Meena Date: Tue, 19 Nov 2024 23:34:23 +0530 Subject: [PATCH 32/45] Added app mode support for micorsoft edge, IE mode for internet explorer and increase timeout for test run. (#744) * Added msie mode for internet explorer and fixed app mode for edge browser. Added MSIE mode to support device which does not have edge installed by default ( win <10). Added support in edge to support edge mode or new window mode if app mode is not requested. --- .github/workflows/test.yml | 2 +- README.md | 2 +- eel/browsers.py | 4 +++- eel/edge.py | 13 ++++++++++--- eel/msIE.py | 20 ++++++++++++++++++++ 5 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 eel/msIE.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9bddaadf..43a0e619 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: - name: Setup test execution environment. run: pip3 install -r requirements-meta.txt - name: Run tox tests - run: tox -- --durations=0 --timeout=30 + run: tox -- --durations=0 --timeout=240 typecheck: strategy: diff --git a/README.md b/README.md index 6b70d27d..b4e5e671 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Additional options can be passed to `eel.start()` as keyword arguments. 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. As of Eel v0.12.0, the following options are available to `start()`: - - **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'`* + - **mode**, a string specifying what browser to use (e.g. `'chrome'`, `'electron'`, `'edge'`,`'msie'`, `'custom'`). Can also be `None` or `False` to not open a window. *Default: `'chrome'`* - **host**, a string specifying what hostname to use for the Bottle server. *Default: `'localhost'`)* - **port**, an int specifying what port to use for the Bottle server. Use `0` for port to be picked automatically. *Default: `8000`*. - **block**, a bool saying whether or not the call to `start()` should block the calling thread. *Default: `True`* diff --git a/eel/browsers.py b/eel/browsers.py index d555b2d9..89b040e9 100644 --- a/eel/browsers.py +++ b/eel/browsers.py @@ -7,13 +7,15 @@ import eel.chrome as chm import eel.electron as ele import eel.edge as edge +import eel.msIE as ie #import eel.firefox as ffx TODO #import eel.safari as saf TODO _browser_paths: Dict[str, str] = {} _browser_modules: Dict[str, ModuleType] = {'chrome': chm, 'electron': ele, - 'edge': edge} + 'edge': edge, + 'msie':ie} def _build_url_from_dict(page: Dict[str, str], options: OptionsDictT) -> str: diff --git a/eel/edge.py b/eel/edge.py index 7f2dab1e..cea91894 100644 --- a/eel/edge.py +++ b/eel/edge.py @@ -9,9 +9,16 @@ def run(_path: str, options: OptionsDictT, start_urls: List[str]) -> None: - cmd = 'start microsoft-edge:{}'.format(start_urls[0]) - sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) - + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") + args: List[str] = options['cmdline_args'] + if options['app_mode']: + cmd = 'start msedge --app={} '.format(start_urls[0]) + cmd = cmd + (" ".join(args)) + sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) + else: + cmd = "start msedge --new-window "+(" ".join(args)) +" "+(start_urls[0]) + sps.Popen(cmd,stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) def find_path() -> bool: if platform.system() == 'Windows': diff --git a/eel/msIE.py b/eel/msIE.py new file mode 100644 index 00000000..7f7f2e26 --- /dev/null +++ b/eel/msIE.py @@ -0,0 +1,20 @@ +import platform +import subprocess as sps +import sys +from typing import List + +from eel.types import OptionsDictT + +name: str = 'MSIE' + + +def run(_path: str, options: OptionsDictT, start_urls: List[str]) -> None: + cmd = 'start microsoft-edge:{}'.format(start_urls[0]) + sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) + + +def find_path() -> bool: + if platform.system() == 'Windows': + return True + + return False From 7c3f6ad46591c70832b9af073f5422821485602d Mon Sep 17 00:00:00 2001 From: Florian Breit Date: Tue, 19 Nov 2024 21:09:03 +0000 Subject: [PATCH 33/45] Refined type annotations to reflect move to python>=3.7 (#683) Co-authored-by: Samuel Williams --- eel/__init__.py | 332 ++++++++++++++++++++----- eel/__main__.py | 1 + eel/browsers.py | 3 +- eel/chrome.py | 12 +- eel/edge.py | 1 + eel/electron.py | 17 +- eel/types.py | 54 ++-- examples/07 - CreateReactApp/README.md | 2 +- mypy.ini | 9 - requirements.txt | 2 +- setup.py | 2 +- 11 files changed, 330 insertions(+), 105 deletions(-) diff --git a/eel/__init__.py b/eel/__init__.py index 0dfa8864..e1ab255b 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -1,15 +1,10 @@ +from __future__ import annotations from builtins import range import traceback from io import open -from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable, TYPE_CHECKING - -if TYPE_CHECKING: - from eel.types import OptionsDictT, WebSocketT -else: - WebSocketT = Any - OptionsDictT = Any - -from gevent.threading import Timer +from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable +from typing_extensions import Literal +from eel.types import OptionsDictT, WebSocketT import gevent as gvt import json as jsn import bottle as btl @@ -46,28 +41,10 @@ # Can be overridden through `eel.init` with the kwarg `js_result_timeout` (default: 10000) _js_result_timeout: int = 10000 -# All start() options must provide a default value and explanation here -_start_args: OptionsDictT = { - 'mode': 'chrome', # What browser is used - 'host': 'localhost', # Hostname use for Bottle server - 'port': 8000, # Port used for Bottle server (use 0 for auto) - 'block': True, # Whether start() blocks calling thread - 'jinja_templates': None, # Folder for jinja2 templates - 'cmdline_args': ['--disable-http-cache'], # Extra cmdline flags to pass to browser start - 'size': None, # (width, height) of main window - 'position': None, # (left, top) of main window - 'geometry': {}, # Dictionary of size/position for all windows - 'close_callback': None, # Callback for when all windows have closed - 'app_mode': True, # (Chrome specific option) - 'all_interfaces': False, # Allow bottle server to listen for connections on all interfaces - 'disable_cache': True, # Sets the no-store response header when serving assets - 'default_path': 'index.html', # The default file to retrieve for the root URL - 'app': btl.default_app(), # Allows passing in a custom Bottle instance, e.g. with middleware - 'shutdown_delay': 1.0 # how long to wait after a websocket closes before detecting complete shutdown -} +# Attribute holding the start args from calls to eel.start() +_start_args: OptionsDictT = {} # == Temporary (suppressible) error message to inform users of breaking API change for v1.0.0 === -_start_args['suppress_error'] = False api_error_message: str = ''' ---------------------------------------------------------------------------------- 'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel @@ -77,9 +54,45 @@ ''' # =============================================================================================== + # Public functions + def expose(name_or_function: Optional[Callable[..., Any]] = None) -> Callable[..., Any]: + '''Decorator to expose Python callables via Eel's JavaScript API. + + When an exposed function is called, a callback function can be passed + immediately afterwards. This callback will be called asynchronously with + the return value (possibly `None`) when the Python function has finished + executing. + + Blocking calls to the exposed function from the JavaScript side are only + possible using the :code:`await` keyword inside an :code:`async function`. + These still have to make a call to the response, i.e. + :code:`await eel.py_random()();` inside an :code:`async function` will work, + but just :code:`await eel.py_random();` will not. + + :Example: + + In Python do: + + .. code-block:: python + + @expose + def say_hello_py(name: str = 'You') -> None: + print(f'{name} said hello from the JavaScript world!') + + In JavaScript do: + + .. code-block:: javascript + + eel.say_hello_py('Alice')(); + + Expected output on the Python console:: + + Alice said hello from the JavaScript world! + + ''' # Deal with '@eel.expose()' - treat as '@eel.expose' if name_or_function is None: return expose @@ -113,8 +126,28 @@ def decorator(function: Callable[..., Any]) -> Any: ) -def init(path: str, allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm', - '.xhtml', '.vue'], js_result_timeout: int = 10000) -> None: +def init( + path: str, + allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm', '.xhtml', '.vue'], + js_result_timeout: int = 10000) -> None: + '''Initialise Eel. + + This function should be called before :func:`start()` to initialise the + parameters for the web interface, such as the path to the files to be + served. + + :param path: Sets the path on the filesystem where files to be served to + the browser are located, e.g. :file:`web`. + :param allowed_extensions: A list of filename extensions which will be + parsed for exposed eel functions which should be callable from python. + Files with extensions not in *allowed_extensions* will still be served, + but any JavaScript functions, even if marked as exposed, will not be + accessible from python. + *Default:* :code:`['.js', '.html', '.txt', '.htm', '.xhtml', '.vue']`. + :param js_result_timeout: How long Eel should be waiting to register the + results from a call to Eel's JavaScript API before before timing out. + *Default:* :code:`10000` milliseconds. + ''' global root_path, _js_functions, _js_result_timeout root_path = _get_real_path(path) @@ -145,14 +178,117 @@ def init(path: str, allowed_extensions: List[str] = ['.js', '.html', '.txt', '.h _js_result_timeout = js_result_timeout -def start(*start_urls: str, **kwargs: Any) -> None: - _start_args.update(kwargs) - - if 'options' in kwargs: - if _start_args['suppress_error']: - _start_args.update(kwargs['options']) - else: - raise RuntimeError(api_error_message) +def start( + *start_urls: str, + mode: Optional[Union[str, Literal[False]]] = 'chrome', + host: str = 'localhost', + port: int = 8000, + block: bool = True, + jinja_templates: Optional[str] = None, + cmdline_args: List[str] = ['--disable-http-cache'], + size: Optional[Tuple[int, int]] = None, + position: Optional[Tuple[int, int]] = None, + geometry: Dict[str, Tuple[int, int]] = {}, + close_callback: Optional[Callable[..., Any]] = None, + app_mode: bool = True, + all_interfaces: bool = False, + disable_cache: bool = True, + default_path: str = 'index.html', + app: btl.Bottle = btl.default_app(), + shutdown_delay: float = 1.0, + suppress_error: bool = False) -> None: + '''Start the Eel app. + + Suppose you put all the frontend files in a directory called + :file:`web`, including your start page :file:`main.html`, then the app + is started like this: + + .. code-block:: python + + import eel + eel.init('web') + eel.start('main.html') + + This will start a webserver on the default settings + (http://localhost:8000) and open a browser to + http://localhost:8000/main.html. + + If Chrome or Chromium is installed then by default it will open that in + *App Mode* (with the `--app` cmdline flag), regardless of what the OS's + default browser is set to (it is possible to override this behaviour). + + :param mode: What browser is used, e.g. :code:`'chrome'`, + :code:`'electron'`, :code:`'edge'`, :code:`'custom'`. Can also be + `None` or `False` to not open a window. *Default:* :code:`'chrome'`. + :param host: Hostname used for Bottle server. *Default:* + :code:`'localhost'`. + :param port: Port used for Bottle server. Use :code:`0` for port to be + picked automatically. *Default:* :code:`8000`. + :param block: Whether the call to :func:`start()` blocks the calling + thread. *Default:* `True`. + :param jinja_templates: Folder for :mod:`jinja2` templates, e.g. + :file:`my_templates`. *Default:* `None`. + :param cmdline_args: A list of strings to pass to the command starting the + browser. For example, we might add extra flags to Chrome with + :code:`eel.start('main.html', mode='chrome-app', port=8080, + cmdline_args=['--start-fullscreen', '--browser-startup-dialog'])`. + *Default:* :code:`[]`. + :param size: Tuple specifying the (width, height) of the main window in + pixels. *Default:* `None`. + :param position: Tuple specifying the (left, top) position of the main + window in pixels. *Default*: `None`. + :param geometry: A dictionary of specifying the size/position for all + windows. The keys should be the relative path of the page, and the + values should be a dictionary of the form + :code:`{'size': (200, 100), 'position': (300, 50)}`. *Default:* + :code:`{}`. + :param close_callback: A lambda or function that is called when a websocket + or 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 the other websockets that are still open. + *Default:* `None`. + :param app_mode: Whether to run Chrome/Edge in App Mode. You can also + specify *mode* as :code:`mode='chrome-app'` as a shorthand to start + Chrome in App Mode. + :param all_interfaces: Whether to allow the :mod:`bottle` server to listen + for connections on all interfaces. + :param disable_cache: Sets the no-store response header when serving + assets. + :param default_path: The default file to retrieve for the root URL. + :param app: An instance of :class:`bottle.Bottle` which will be used rather + than creating a fresh one. This can be used to install middleware on + the instance before starting Eel, e.g. for session management, + authentication, etc. If *app* is not a :class:`bottle.Bottle` instance, + you will need to call :code:`eel.register_eel_routes(app)` on your + custom app instance. + :param shutdown_delay: Timer configurable for Eel's shutdown detection + mechanism, whereby when any websocket closes, it waits *shutdown_delay* + seconds, and then checks if there are now any websocket connections. + If not, then Eel closes. In case the user has closed the browser and + wants to exit the program. *Default:* :code:`1.0` seconds. + :param suppress_error: Temporary (suppressible) error message to inform + users of breaking API change for v1.0.0. Set to `True` to suppress + the error message. + ''' + _start_args.update({ + 'mode': mode, + 'host': host, + 'port': port, + 'block': block, + 'jinja_templates': jinja_templates, + 'cmdline_args': cmdline_args, + 'size': size, + 'position': position, + 'geometry': geometry, + 'close_callback': close_callback, + 'app_mode': app_mode, + 'all_interfaces': all_interfaces, + 'disable_cache': disable_cache, + 'default_path': default_path, + 'app': app, + 'shutdown_delay': shutdown_delay, + 'suppress_error': suppress_error, + }) if _start_args['port'] == 0: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -160,24 +296,28 @@ def start(*start_urls: str, **kwargs: Any) -> None: _start_args['port'] = sock.getsockname()[1] sock.close() - if _start_args['jinja_templates'] != None: + if _start_args['jinja_templates'] is not None: from jinja2 import Environment, FileSystemLoader, select_autoescape if not isinstance(_start_args['jinja_templates'], str): - raise TypeError("'jinja_templates start_arg/option must be of type str'") + raise TypeError("'jinja_templates' start_arg/option must be of type str") templates_path = os.path.join(root_path, _start_args['jinja_templates']) - _start_args['jinja_env'] = Environment(loader=FileSystemLoader(templates_path), - autoescape=select_autoescape(['html', 'xml'])) + _start_args['jinja_env'] = Environment( + loader=FileSystemLoader(templates_path), + autoescape=select_autoescape(['html', 'xml']) + ) # verify shutdown_delay is correct value if not isinstance(_start_args['shutdown_delay'], (int, float)): - raise ValueError("`shutdown_delay` must be a number, "\ - "got a {}".format(type(_start_args['shutdown_delay']))) + raise ValueError( + '`shutdown_delay` must be a number, ' + 'got a {}'.format(type(_start_args['shutdown_delay'])) + ) # Launch the browser to the starting URLs show(*start_urls) def run_lambda() -> None: - if _start_args['all_interfaces'] == True: + if _start_args['all_interfaces'] is True: HOST = '0.0.0.0' else: if not isinstance(_start_args['host'], str): @@ -196,7 +336,7 @@ def run_lambda() -> None: port=_start_args['port'], server=wbs.GeventWebSocketServer, quiet=True, - app=app) # Always returns None + app=app) # Always returns None # Start the webserver if _start_args['block']: @@ -206,18 +346,78 @@ def run_lambda() -> None: def show(*start_urls: str) -> None: + '''Show the specified URL(s) in the browser. + + Suppose you have two files in your :file:`web` folder. The file + :file:`hello.html` regularly includes :file:`eel.js` and provides + interactivity, and the file :file:`goodbye.html` does not include + :file:`eel.js` and simply provides plain HTML content not reliant on Eel. + + First, we defien a callback function to be called when the browser + window is closed: + + .. code-block:: python + + def last_calls(): + eel.show('goodbye.html') + + Now we initialise and start Eel, with a :code:`close_callback` to our + function: + + ..code-block:: python + + eel.init('web') + eel.start('hello.html', mode='chrome-app', close_callback=last_calls) + + When the websocket from :file:`hello.html` is closed (e.g. because the + user closed the browser window), Eel will wait *shutdown_delay* seconds + (by default 1 second), then call our :code:`last_calls()` function, which + opens another window with the :file:`goodbye.html` shown before our Eel app + terminates. + + :param start_urls: One or more URLs to be opened. + ''' brw.open(list(start_urls), _start_args) def sleep(seconds: Union[int, float]) -> None: + '''A non-blocking sleep call compatible with the Gevent event loop. + + .. note:: + While this function simply wraps :func:`gevent.sleep()`, it is better + to call :func:`eel.sleep()` in your eel app, as this will ensure future + compatibility in case the implementation of Eel should change in some + respect. + + :param seconds: The number of seconds to sleep. + ''' gvt.sleep(seconds) def spawn(function: Callable[..., Any], *args: Any, **kwargs: Any) -> gvt.Greenlet: + '''Spawn a new Greenlet. + + Calling this function will spawn a new :class:`gevent.Greenlet` running + *function* asynchronously. + + .. caution:: + If you spawn your own Greenlets to run in addition to those spawned by + Eel's internal core functionality, you will have to ensure that those + Greenlets will terminate as appropriate (either by returning or by + being killed via Gevent's kill mechanism), otherwise your app may not + terminate correctly when Eel itself terminates. + + :param function: The function to be called and run as the Greenlet. + :param *args: Any positional arguments that should be passed to *function*. + :param **kwargs: Any key-word arguments that should be passed to + *function*. + ''' return gvt.spawn(function, *args, **kwargs) + # Bottle Routes + def _eel() -> str: start_geometry = {'default': {'size': _start_args['size'], 'position': _start_args['position']}, @@ -231,12 +431,14 @@ def _eel() -> str: _set_response_headers(btl.response) return page -def _root() -> Optional[btl.Response]: + +def _root() -> btl.Response: if not isinstance(_start_args['default_path'], str): raise TypeError("'default_path' start_arg/option must be of type str") return _static(_start_args['default_path']) -def _static(path: str) -> Optional[btl.Response]: + +def _static(path: str) -> btl.Response: response = None if 'jinja_env' in _start_args and 'jinja_templates' in _start_args: if not isinstance(_start_args['jinja_templates'], str): @@ -244,7 +446,7 @@ def _static(path: str) -> Optional[btl.Response]: template_prefix = _start_args['jinja_templates'] + '/' if path.startswith(template_prefix): n = len(template_prefix) - template = _start_args['jinja_env'].get_template(path[n:]) # type: ignore # depends on conditional import in start() + template = _start_args['jinja_env'].get_template(path[n:]) response = btl.HTTPResponse(template.render()) if response is None: @@ -253,6 +455,7 @@ def _static(path: str) -> Optional[btl.Response]: _set_response_headers(response) return response + def _websocket(ws: WebSocketT) -> None: global _websockets @@ -286,21 +489,34 @@ def _websocket(ws: WebSocketT) -> None: "/eel": (_websocket, dict(apply=[wbs.websocket])) } + def register_eel_routes(app: btl.Bottle) -> None: - ''' - Adds eel routes to `app`. Only needed if you are passing something besides `bottle.Bottle` to `eel.start()`. - Ex: - app = bottle.Bottle() - eel.register_eel_routes(app) - middleware = beaker.middleware.SessionMiddleware(app) - eel.start(app=middleware) + '''Register the required eel routes with `app`. + + .. note:: + + :func:`eel.register_eel_routes()` is normally invoked implicitly by + :func:`eel.start()` and does not need to be called explicitly in most + cases. Registering the eel routes explicitly is only needed if you are + passing something other than an instance of :class:`bottle.Bottle` to + :func:`eel.start()`. + + :Example: + + >>> app = bottle.Bottle() + >>> eel.register_eel_routes(app) + >>> middleware = beaker.middleware.SessionMiddleware(app) + >>> eel.start(app=middleware) + ''' for route_path, route_params in BOTTLE_ROUTES.items(): route_func, route_kwargs = route_params app.route(path=route_path, callback=route_func, **route_kwargs) + # Private functions + def _safe_json(obj: Any) -> str: return jsn.dumps(obj, default=lambda o: None) @@ -348,7 +564,7 @@ def _process_message(message: Dict[str, Any], ws: WebSocketT) -> None: def _get_real_path(path: str) -> str: if getattr(sys, 'frozen', False): - return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstaller + return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstaller else: return os.path.abspath(path) diff --git a/eel/__main__.py b/eel/__main__.py index b4027eb6..74910995 100644 --- a/eel/__main__.py +++ b/eel/__main__.py @@ -1,3 +1,4 @@ +from __future__ import annotations import pkg_resources as pkg import PyInstaller.__main__ as pyi import os diff --git a/eel/browsers.py b/eel/browsers.py index 89b040e9..183dd905 100644 --- a/eel/browsers.py +++ b/eel/browsers.py @@ -1,3 +1,4 @@ +from __future__ import annotations import subprocess as sps import webbrowser as wbr from typing import Union, List, Dict, Iterable, Optional @@ -53,7 +54,7 @@ def open(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDict start_urls = _build_urls(start_pages, options) mode = options.get('mode') - if not isinstance(mode, (str, bool, type(None))) or mode is True: + if not isinstance(mode, (str, type(None))) and mode is not False: raise TypeError("'mode' option must by either a string, False, or None") if mode is None or mode is False: # Don't open a browser diff --git a/eel/chrome.py b/eel/chrome.py index c07356f0..a8112f5d 100644 --- a/eel/chrome.py +++ b/eel/chrome.py @@ -1,6 +1,9 @@ -import sys, subprocess as sps, os +from __future__ import annotations +import sys +import os +import subprocess as sps +from shutil import which from typing import List, Optional - from eel.types import OptionsDictT # Every browser specific module must define run(), find_path() and name like this @@ -57,16 +60,15 @@ def _find_chromium_mac() -> Optional[str]: def _find_chrome_linux() -> Optional[str]: - import whichcraft as wch chrome_names = ['chromium-browser', 'chromium', 'google-chrome', 'google-chrome-stable'] for name in chrome_names: - chrome = wch.which(name) + chrome = which(name) if chrome is not None: - return chrome # type: ignore # whichcraft doesn't currently have type hints + return chrome return None diff --git a/eel/edge.py b/eel/edge.py index cea91894..7d785233 100644 --- a/eel/edge.py +++ b/eel/edge.py @@ -1,3 +1,4 @@ +from __future__ import annotations import platform import subprocess as sps import sys diff --git a/eel/electron.py b/eel/electron.py index 14cbc802..4ff8fcc1 100644 --- a/eel/electron.py +++ b/eel/electron.py @@ -1,8 +1,8 @@ -#from __future__ import annotations +from __future__ import annotations import sys import os import subprocess as sps -import whichcraft as wch +from shutil import which from typing import List, Optional from eel.types import OptionsDictT @@ -20,11 +20,10 @@ def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: def find_path() -> Optional[str]: if sys.platform in ['win32', 'win64']: # It doesn't work well passing the .bat file to Popen, so we get the actual .exe - bat_path = wch.which('electron') - return os.path.join(bat_path, r'..\node_modules\electron\dist\electron.exe') + bat_path = which('electron') + if bat_path: + return os.path.join(bat_path, r'..\node_modules\electron\dist\electron.exe') elif sys.platform in ['darwin', 'linux']: - # This should work find... - return wch.which('electron') # type: ignore # whichcraft doesn't currently have type hints - else: - return None - + # This should work fine... + return which('electron') + return None diff --git a/eel/types.py b/eel/types.py index 55475816..0cd2d6ee 100644 --- a/eel/types.py +++ b/eel/types.py @@ -1,28 +1,42 @@ +from __future__ import annotations from typing import Union, Dict, List, Tuple, Callable, Optional, Any, TYPE_CHECKING +from typing_extensions import Literal, TypedDict, TypeAlias +from bottle import Bottle # This business is slightly awkward, but needed for backward compatibility, -# because Python < 3.7 doesn't have __future__/annotations, and <3.10 doesn't -# support TypeAlias. +# because Python <3.10 doesn't support TypeAlias, jinja2 may not be available +# at runtime, and geventwebsocket.websocket doesn't have type annotations so +# that direct imports will raise an error. if TYPE_CHECKING: from jinja2 import Environment - try: - from typing import TypeAlias # Introduced in Python 3.10 - JinjaEnvironmentT: TypeAlias = Environment - except ImportError: - JinjaEnvironmentT = Environment # type: ignore + JinjaEnvironmentT: TypeAlias = Environment from geventwebsocket.websocket import WebSocket - WebSocketT = WebSocket + WebSocketT: TypeAlias = WebSocket else: - JinjaEnvironmentT = None - WebSocketT = Any + JinjaEnvironmentT: TypeAlias = Any + WebSocketT: TypeAlias = Any -OptionsDictT = Dict[ - str, - Optional[ - Union[ - str, bool, int, float, - List[str], Tuple[int, int], Dict[str, Tuple[int, int]], - Callable[..., Any], JinjaEnvironmentT - ] - ] - ] +OptionsDictT = TypedDict( + 'OptionsDictT', + { + 'mode': Optional[Union[str, Literal[False]]], + 'host': str, + 'port': int, + 'block': bool, + 'jinja_templates': Optional[str], + 'cmdline_args': List[str], + 'size': Optional[Tuple[int, int]], + 'position': Optional[Tuple[int, int]], + 'geometry': Dict[str, Tuple[int, int]], + 'close_callback': Optional[Callable[..., Any]], + 'app_mode': bool, + 'all_interfaces': bool, + 'disable_cache': bool, + 'default_path': str, + 'app': Bottle, + 'shutdown_delay': float, + 'suppress_error': bool, + 'jinja_env': JinjaEnvironmentT, + }, + total=False +) diff --git a/examples/07 - CreateReactApp/README.md b/examples/07 - CreateReactApp/README.md index f2b0f249..3f6a85b6 100644 --- a/examples/07 - CreateReactApp/README.md +++ b/examples/07 - CreateReactApp/README.md @@ -19,7 +19,7 @@ If you run into any issues with this example, open a [new issue](https://github. ## Quick Start -1. **Configure:** In the app's directory, run `npm install` and `pip install bottle bottle-websocket future whichcraft pyinstaller` +1. **Configure:** In the app's directory, run `npm install` and `pip install bottle bottle-websocket future pyinstaller` 2. **Demo:** Build static files with `npm run build` then run the application with `python eel_CRA.py`. A Chrome-app window should open running the built code from `build/` 3. **Distribute:** (Run `npm run build` first) Build a binary distribution with PyInstaller using `python -m eel eel_CRA.py build --onefile` (See more detailed PyInstaller instructions at bottom of [the main README](https://github.com/ChrisKnott/Eel)) 4. **Develop:** Open two prompts. In one, run `python eel_CRA.py true` and the other, `npm start`. A browser window should open in your default web browser at: [http://localhost:3000/](http://localhost:3000/). As you make changes to the JavaScript in `src/` the browser will reload. Any changes to `eel_CRA.py` will require a restart to take effect. You may need to refresh the browser window if it gets out of sync with eel. diff --git a/mypy.ini b/mypy.ini index 6fa89652..4be68351 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,9 +5,6 @@ warn_unused_configs = True [mypy-bottle_websocket] ignore_missing_imports = True -[mypy-jinja2] -ignore_missing_imports = True - [mypy-gevent] ignore_missing_imports = True @@ -26,12 +23,6 @@ ignore_missing_imports = True [mypy-bottle.ext.websocket] ignore_missing_imports = True -[mypy-whichcraft] -ignore_missing_imports = True - -[mypy-pyparsing] -ignore_missing_imports = True - [mypy-PyInstaller] ignore_missing_imports = True diff --git a/requirements.txt b/requirements.txt index af6da07d..3150cc01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ gevent gevent-websocket<1.0.0 greenlet>=1.0.0,<2.0.0 pyparsing>=3.0.0,<4.0.0 -whichcraft~=0.4.1 +typing-extensions>=4.3.0 diff --git a/setup.py b/setup.py index 03dccdf0..3519c49b 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ package_data={ 'eel': ['eel.js', 'py.typed'], }, - install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'whichcraft'], + install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing'], extras_require={ "jinja2": ['jinja2>=2.10'] }, From f53b1eef7d3d4540b0419de31d59c5b9ef4a8732 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 20 Nov 2024 10:29:42 +0000 Subject: [PATCH 34/45] Release version 0.18.0 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32381939..e7097ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change log +### 0.18.0 +* Added support for MS Internet Explorer in #744. +* Added supported for app_mode in the Edge browser in #744. +* Improved type annotations in #683. + ### 0.17.0 * Adds support for Python 3.11 and Python 3.12 diff --git a/setup.py b/setup.py index 3519c49b..250b25b1 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='Eel', - version='0.17.0', + version='0.18.0', author='Python Eel Organisation', author_email='python-eel@protonmail.com', url='https://github.com/python-eel/Eel', From b96ae07e31f81224283264379518d5bfe7a59911 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 22 Nov 2024 13:03:51 +0000 Subject: [PATCH 35/45] Add typing_extensions to package requirements --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 250b25b1..79c7e506 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ package_data={ 'eel': ['eel.js', 'py.typed'], }, - install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing'], + install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'typing_extensions'], extras_require={ "jinja2": ['jinja2>=2.10'] }, From 0ede901a5d09133f80d49a4f109c164e40cbc6b7 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 22 Nov 2024 16:50:28 +0000 Subject: [PATCH 36/45] Fix broken v0.18.0 missing typing_extensions --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7097ab3..f04dcd1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change log +### 0.18.1 + +* Fix: Include `typing_extensions` in install requirements. + ### 0.18.0 * Added support for MS Internet Explorer in #744. * Added supported for app_mode in the Edge browser in #744. diff --git a/setup.py b/setup.py index 79c7e506..e25f54dd 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='Eel', - version='0.18.0', + version='0.18.1', author='Python Eel Organisation', author_email='python-eel@protonmail.com', url='https://github.com/python-eel/Eel', From b553095f0d2a22bcb228a59cfb4627047a3b02bc Mon Sep 17 00:00:00 2001 From: v01d Date: Mon, 16 Jun 2025 01:48:49 +0400 Subject: [PATCH 37/45] Init --- eel/__init__.py | 12 +++++++++--- requirements.txt | 1 + tox.ini | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/eel/__init__.py b/eel/__init__.py index e1ab255b..34fc7ce1 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -18,14 +18,20 @@ import pyparsing as pp import random as rnd import sys -import pkg_resources as pkg +import importlib_resources import socket import mimetypes mimetypes.add_type('application/javascript', '.js') -_eel_js_file: str = pkg.resource_filename('eel', 'eel.js') -_eel_js: str = open(_eel_js_file, encoding='utf-8').read() + +# https://setuptools.pypa.io/en/latest/pkg_resources.html +# Use of pkg_resources is deprecated in favor of importlib.resources +# Migration guide: https://importlib-resources.readthedocs.io/en/latest/migration.html +_eel_js_reference = importlib_resources.files('eel') / 'eel.js' +with importlib_resources.as_file(_eel_js_reference) as _eel_js_path: + _eel_js: str = _eel_js_path.read_text(encoding='utf-8') + _websockets: List[Tuple[Any, WebSocketT]] = [] _call_return_values: Dict[Any, Any] = {} _call_return_callbacks: Dict[float, Tuple[Callable[..., Any], Optional[Callable[..., Any]]]] = {} diff --git a/requirements.txt b/requirements.txt index 3150cc01..77d8646e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ gevent-websocket<1.0.0 greenlet>=1.0.0,<2.0.0 pyparsing>=3.0.0,<4.0.0 typing-extensions>=4.3.0 +importlib_resources>=1.3 diff --git a/tox.ini b/tox.ini index 272bb77e..0d3838e4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = typecheck,py{37,38,39,310,311,312} +envlist = typecheck,py{37,38,39,310,311,312,313} [pytest] timeout = 30 @@ -12,6 +12,7 @@ python = 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 [testenv] From 1fac1a869401fb9b7926a1e44ce2dde8b04771b6 Mon Sep 17 00:00:00 2001 From: v01d Date: Wed, 18 Jun 2025 01:46:32 +0400 Subject: [PATCH 38/45] Update requirements-test.txt --- requirements-test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-test.txt b/requirements-test.txt index 4ee6b5dc..ec2da232 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,3 +8,4 @@ webdriver_manager>=4.0.0,<5.0.0 mypy>=1.0.0,<2.0.0 pyinstaller types-setuptools +importlib_resources>=1.3 From de7bed868413595f7376fdb7c3ade31707b81dcc Mon Sep 17 00:00:00 2001 From: v01d Date: Thu, 19 Jun 2025 14:47:32 +0400 Subject: [PATCH 39/45] Upd --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43a0e619..1c302518 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, windows-latest, macos-latest] + os: [ubuntu-24.04, windows-latest, macos-latest] python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] exclude: - os: macos-latest From 4921b0ea332451edcb571901ce3103e60adcdf7c Mon Sep 17 00:00:00 2001 From: v01d Date: Thu, 19 Jun 2025 19:42:26 +0400 Subject: [PATCH 40/45] No Python37 for Ubuntu-24.04 --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c302518..3ae373a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,8 @@ jobs: exclude: - os: macos-latest python-version: 3.7 + - os: ubuntu-24.04 + python-version: 3.7 runs-on: ${{ matrix.os }} From a5df09ffbbd1653c7a6f8e8a4264279cb5da397a Mon Sep 17 00:00:00 2001 From: v01d Date: Sun, 22 Jun 2025 15:09:21 +0400 Subject: [PATCH 41/45] Upd --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e25f54dd..4b5f3e5e 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ package_data={ 'eel': ['eel.js', 'py.typed'], }, - install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'typing_extensions'], + install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'typing_extensions', 'importlib_resources'], extras_require={ "jinja2": ['jinja2>=2.10'] }, From a06e678a6921c1b650accaa02f457c285e525432 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 22 Jun 2025 16:09:52 +0100 Subject: [PATCH 42/45] Switch from pkg_resources to importlib.resources --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f04dcd1f..b91f88d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change log +### 0.18.2 + +* Switch from using `pkg_resources` to `importlib.resources`: https://github.com/python-eel/Eel/pull/766 + ### 0.18.1 * Fix: Include `typing_extensions` in install requirements. diff --git a/setup.py b/setup.py index 4b5f3e5e..000dfbeb 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='Eel', - version='0.18.1', + version='0.18.2', author='Python Eel Organisation', author_email='python-eel@protonmail.com', url='https://github.com/python-eel/Eel', From 07c0ec7cab33727db20924b5be52837b60ce8397 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 22 Jun 2025 19:18:31 +0100 Subject: [PATCH 43/45] Pin setuptools for publishing https://github.com/pypi/warehouse/issues/15611 This isn't an ideal fix - ideally we'd use latest things as sensible, but given archiving Eel soon I don't consider it worth fixing publishing 'properly'. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3559c5d0..8f67eff8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: python-version: 3.x architecture: x64 - name: Install setuptools - run: pip install setuptools + run: pip install setuptools==76.1.0 - name: Build a source distribution run: python setup.py sdist - name: Publish to prod PyPI From b161f13caaae1e6449033d12feaca73cf9e3dca2 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 16 Jun 2025 18:16:18 +0100 Subject: [PATCH 44/45] Add unmaintained warning to readme. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b4e5e671..ec337414 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Eel +> [!IMPORTANT] +> This project is effectively unmaintained. It has not received regular update in a number of years, and there are no plans by active maintainers for this to change. Please treat this project in that light and use it with careful consideration towards how you will secure and maintain anything you build using it. + [![PyPI version](https://img.shields.io/pypi/v/Eel?style=for-the-badge)](https://pypi.org/project/Eel/) [![PyPi Downloads](https://img.shields.io/pypi/dm/Eel?style=for-the-badge)](https://pypistats.org/packages/eel) ![Python](https://img.shields.io/pypi/pyversions/Eel?style=for-the-badge) From f8b22efedfd5ad7516039dd80bab8f237d56fb78 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 16 Jun 2025 18:17:26 +0100 Subject: [PATCH 45/45] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec337414..a2dfc4dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Eel -> [!IMPORTANT] +> [!CAUTION] > This project is effectively unmaintained. It has not received regular update in a number of years, and there are no plans by active maintainers for this to change. Please treat this project in that light and use it with careful consideration towards how you will secure and maintain anything you build using it. [![PyPI version](https://img.shields.io/pypi/v/Eel?style=for-the-badge)](https://pypi.org/project/Eel/)