├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── help-me.md └── workflows │ ├── codeql-analysis.yml │ ├── main.yml │ └── test.yml ├── .gitignore ├── .python-version ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README-developers.md ├── README.md ├── eel ├── __init__.py ├── __main__.py ├── browsers.py ├── chrome.py ├── edge.py ├── eel.js ├── electron.py ├── msIE.py ├── py.typed └── types.py ├── examples ├── 01 - hello_world-Edge │ └── hello.py ├── 01 - hello_world │ ├── hello.py │ └── web │ │ ├── favicon.ico │ │ └── hello.html ├── 02 - callbacks │ ├── callbacks.py │ └── web │ │ ├── callbacks.html │ │ └── favicon.ico ├── 03 - sync_callbacks │ ├── sync_callbacks.py │ └── web │ │ ├── favicon.ico │ │ └── sync_callbacks.html ├── 04 - file_access │ ├── README.md │ ├── Screenshot.png │ ├── file_access.py │ └── web │ │ ├── favicon.ico │ │ └── file_access.html ├── 05 - input │ ├── script.py │ └── web │ │ ├── favicon.ico │ │ └── main.html ├── 06 - jinja_templates │ ├── hello.py │ └── web │ │ ├── favicon.ico │ │ └── templates │ │ ├── base.html │ │ ├── hello.html │ │ └── page2.html ├── 07 - CreateReactApp │ ├── .gitignore │ ├── Demo.png │ ├── README.md │ ├── eel_CRA.py │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ └── serviceWorker.ts │ └── tsconfig.json ├── 08 - disable_cache │ ├── disable_cache.py │ └── web │ │ ├── disable_cache.html │ │ ├── dont_cache_me.js │ │ └── favicon.ico ├── 09 - Eelectron-quick-start │ ├── .gitignore │ ├── hello.py │ ├── main.js │ ├── package-lock.json │ ├── package.json │ └── web │ │ ├── favicon.ico │ │ └── hello.html └── 10 - custom_app_routes │ ├── custom_app.py │ └── web │ └── index.html ├── mypy.ini ├── requirements-meta.txt ├── requirements-test.txt ├── requirements.txt ├── setup.py ├── tests ├── conftest.py ├── data │ └── init_test │ │ ├── App.tsx │ │ ├── hello.html │ │ ├── minified.js │ │ └── sample.html ├── integration │ └── test_examples.py ├── unit │ └── test_eel.py └── utils.py └── tox.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: samuelhwilliams 2 | patreon: # Replace with a single Patreon username 3 | open_collective: # Replace with a single Open Collective username 4 | ko_fi: # Replace with a single Ko-fi username 5 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 6 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 7 | liberapay: # Replace with a single Liberapay username 8 | issuehunt: # Replace with a single IssueHunt username 9 | otechie: # Replace with a single Otechie username 10 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Eel version** 11 | Please state the version of Eel you're using. 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **System Information** 27 | - OS: [e.g. Windows 10 x64, Linux Ubuntu, macOS 12] 28 | - Browser: [e.g. Chrome 108.0.5359.99 (Official Build) (64-bit), Safari 16, Firefox 107.0.1] 29 | - Python Distribution: [e.g. Python.org 3.9, Anaconda3 2021.11 3.9, ActivePython 3.9] 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help-me.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help me 3 | about: Get help with Eel 4 | title: '' 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the problem** 11 | A clear and concise description of what you're trying to accomplish, and where you're having difficulty. 12 | 13 | **Code snippet(s)** 14 | Here is some code that can be easily used to reproduce the problem or understand what I need help with. 15 | 16 | - [ ] I know that if I don't provide sample code that allows someone to quickly step into my shoes, I may not get the help I want or my issue may be closed. 17 | 18 | ```python 19 | import eel 20 | 21 | ... 22 | ``` 23 | 24 | ```html 25 | 26 | ... 27 | 28 | ``` 29 | 30 | **Desktop (please complete the following information):** 31 | - OS: [e.g. iOS] 32 | - Browser [e.g. chrome, safari] 33 | - Version [e.g. 22] 34 | 35 | **Smartphone (please complete the following information):** 36 | - Device: [e.g. iPhone6] 37 | - OS: [e.g. iOS8.1] 38 | - Browser [e.g. stock browser, safari] 39 | - Version [e.g. 22] 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '0 11 * * 0' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | with: 35 | languages: javascript, python 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v1 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@master 15 | - name: Setup Python 16 | uses: actions/setup-python@master 17 | with: 18 | python-version: 3.x 19 | architecture: x64 20 | - name: Install setuptools 21 | run: pip install setuptools 22 | - name: Build a source distribution 23 | run: python setup.py sdist 24 | - name: Publish to prod PyPI 25 | uses: pypa/gh-action-pypi-publish@4f4304928fc886cd021893f6defb1bd53d0a1e5a 26 | with: 27 | user: __token__ 28 | password: ${{ secrets.pypi_token }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Eel 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-20.04, windows-latest, macos-latest] 17 | python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] 18 | exclude: 19 | - os: macos-latest 20 | python-version: 3.7 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v2 27 | - name: Setup python 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | - name: Setup test execution environment. 32 | run: pip3 install -r requirements-meta.txt 33 | - name: Run tox tests 34 | run: tox -- --durations=0 --timeout=240 35 | 36 | typecheck: 37 | strategy: 38 | matrix: 39 | os: [windows-latest] 40 | 41 | runs-on: ${{ matrix.os }} 42 | 43 | steps: 44 | - name: Checkout repository 45 | uses: actions/checkout@v2 46 | - name: Setup python 47 | uses: actions/setup-python@v2 48 | with: 49 | python-version: "3.x" 50 | - name: Setup test execution environment. 51 | run: pip3 install -r requirements-meta.txt 52 | - name: Run tox tests 53 | run: tox -e typecheck 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | dist 3 | build 4 | Drivers 5 | Eel.egg-info 6 | .tmp 7 | .DS_Store 8 | *.pyc 9 | *.swp 10 | venv/ 11 | .tox 12 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | python: 4 | - 2.7 5 | - 3.6 6 | matrix: 7 | allow_failures: 8 | - python: 2.7 9 | install: 10 | #- pip install -r requirements.txt 11 | - pip install flake8 # pytest # add another testing frameworks later 12 | before_script: 13 | # stop the build if there are Python syntax errors or undefined names 14 | - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics 15 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 16 | - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 17 | script: 18 | - true # pytest --capture=sys # add other tests here 19 | notifications: 20 | on_success: change 21 | on_failure: change # `always` will be the setting once code changes slow down 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ### 0.18.1 4 | 5 | * Fix: Include `typing_extensions` in install requirements. 6 | 7 | ### 0.18.0 8 | * Added support for MS Internet Explorer in #744. 9 | * Added supported for app_mode in the Edge browser in #744. 10 | * Improved type annotations in #683. 11 | 12 | ### 0.17.0 13 | * Adds support for Python 3.11 and Python 3.12 14 | 15 | ### v0.16.0 16 | * Drop support for Python versions below 3.7 17 | 18 | ### v0.15.3 19 | * Comprehensive type hints implement by @thatfloflo in https://github.com/python-eel/Eel/pull/577. 20 | 21 | ### v0.15.2 22 | * Adds `register_eel_routes` to handle applying Eel routes to non-Bottle custom app instances. 23 | 24 | ### v0.15.1 25 | * Bump bottle dependency from 0.12.13 to 0.12.20 to address the critical CVE-2022-31799 and moderate CVE-2020-28473. 26 | 27 | ### v0.15.0 28 | * Add `shutdown_delay` as a `start()` function parameter ([#529](https://github.com/python-eel/Eel/pull/529)) 29 | 30 | ### v0.14.0 31 | * Change JS function name parsing to use PyParsing rather than regex, courtesy @KyleKing. 32 | 33 | ### v0.13.2 34 | * Add `default_path` start arg to define a default file to retrieve when hitting the root URL. 35 | 36 | ### v0.13.1 37 | * Shut down the Eel server less aggressively when websockets get closed (#337) 38 | 39 | ## v0.13.0 40 | * Drop support for Python versions below 3.6 41 | * Add `jinja2` as an extra for pip installation, e.g. `pip install eel[jinja2]`. 42 | * Bump dependencies in examples to dismiss github security notices. We probably want to set up a policy to ignore example dependencies as they shouldn't be considered a source of vulnerabilities. 43 | * Disable edge on non-Windows platforms until we implement proper support. 44 | 45 | ### v0.12.4 46 | * Return greenlet task from `spawn()` ([#300](https://github.com/samuelhwilliams/Eel/pull/300)) 47 | * Set JS mimetype to reduce errors on Windows platform ([#289](https://github.com/samuelhwilliams/Eel/pull/289)) 48 | 49 | ### v0.12.3 50 | * Search for Chromium on macOS. 51 | 52 | ### v0.12.2 53 | * Fix a bug that prevents using middleware via a custom Bottle. 54 | 55 | ### v0.12.1 56 | * Check that Chrome path is a file that exists on Windows before blindly returning it. 57 | 58 | ## v0.12.0 59 | * Allow users to override the amount of time Python will wait for Javascript functions running via Eel to run before bailing and returning None. 60 | 61 | ### v0.11.1 62 | * Fix the implementation of #203, allowing users to pass their own bottle instances into Eel. 63 | 64 | ## v0.11.0 65 | * Added support for `app` parameter to `eel.start`, which will override the bottle app instance used to run eel. This 66 | allows developers to apply any middleware they wish to before handing over to eel. 67 | * Disable page caching by default via new `disable_cache` parameter to `eel.start`. 68 | * Add support for listening on all network interfaces via new `all_interfaces` parameter to `eel.start`. 69 | * Support for Microsoft Edge 70 | 71 | ### v0.10.4 72 | * Fix PyPi project description. 73 | 74 | ### v0.10.3 75 | * Fix a bug that prevented using Eel without Jinja templating. 76 | 77 | ### v0.10.2 78 | * Only render templates from within the declared jinja template directory. 79 | 80 | ### v0.10.1 81 | * Avoid name collisions when using Electron, so jQuery etc work normally 82 | 83 | ## v0.10.0 84 | * Corrective version bump after new feature included in 0.9.13 85 | * Fix a bug with example 06 for Jinja templating; the `templates` kwarg to `eel.start` takes a filepath, not a bool. 86 | 87 | ### v0.9.13 88 | * Add support for Jinja templating. 89 | 90 | ### Earlier 91 | * No changelog notes for earlier versions. 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chris Knott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /README-developers.md: -------------------------------------------------------------------------------- 1 | # Eel Developers 2 | 3 | ## Setting up your environment 4 | 5 | In order to start developing with Eel you'll need to checkout the code, set up a development and testing environment, and check that everything is in order. 6 | 7 | ### Clone the repository 8 | ```bash 9 | git clone git@github.com:python-eel/Eel.git 10 | ``` 11 | 12 | ### (Recommended) Create a virtual environment 13 | It's recommended that you use virtual environments for this project. Your process for setting up a virutal environment will vary depending on OS and tool of choice, but might look something like this: 14 | 15 | ```bash 16 | python3 -m venv venv 17 | source venv/bin/activate 18 | ``` 19 | 20 | **Note**: `venv` is listed in the `.gitignore` file so it's the recommended virtual environment name 21 | 22 | 23 | ### Install project requirements 24 | 25 | ```bash 26 | pip3 install -r requirements.txt # eel's 'prod' requirements 27 | pip3 install -r requirements-test.txt # pytest and selenium 28 | pip3 install -r requirements-meta.txt # tox 29 | ``` 30 | 31 | ### (Recommended) Run Automated Tests 32 | 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. 33 | 34 | #### Tox Setup 35 | 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. 36 | 37 | **Note**: Pay attention to the version of Chrome that is installed on your OS because you need to select the compatible ChromeDriver version. 38 | 39 | #### Running Tests 40 | 41 | To test Eel against a specific version of Python you have installed, e.g. Python 3.7 in this case, run: 42 | 43 | ```bash 44 | tox -e py36 45 | ``` 46 | 47 | To test Eel against all supported versions, run the following: 48 | 49 | ```bash 50 | tox 51 | ``` 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eel 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/Eel?style=for-the-badge)](https://pypi.org/project/Eel/) 4 | [![PyPi Downloads](https://img.shields.io/pypi/dm/Eel?style=for-the-badge)](https://pypistats.org/packages/eel) 5 | ![Python](https://img.shields.io/pypi/pyversions/Eel?style=for-the-badge) 6 | [![License](https://img.shields.io/pypi/l/Eel.svg?style=for-the-badge)](https://pypi.org/project/Eel/) 7 | 8 | Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps, with full access to Python capabilities and libraries. 9 | 10 | > **Eel hosts a local webserver, then lets you annotate functions in Python so that they can be called from Javascript, and vice versa.** 11 | 12 | Eel is designed to take the hassle out of writing short and simple GUI applications. If you are familiar with Python and web development, probably just jump to [this example](https://github.com/ChrisKnott/Eel/tree/master/examples/04%20-%20file_access) which picks random file names out of the given folder (something that is impossible from a browser). 13 | 14 |

15 | 16 | 17 | 18 | - [Eel](#eel) 19 | - [Intro](#intro) 20 | - [Install](#install) 21 | - [Usage](#usage) 22 | - [Directory Structure](#directory-structure) 23 | - [Starting the app](#starting-the-app) 24 | - [App options](#app-options) 25 | - [Chrome/Chromium flags](#chromechromium-flags) 26 | - [Exposing functions](#exposing-functions) 27 | - [Eello, World!](#eello-world) 28 | - [Return values](#return-values) 29 | - [Callbacks](#callbacks) 30 | - [Synchronous returns](#synchronous-returns) 31 | - [Asynchronous Python](#asynchronous-python) 32 | - [Building distributable binary with PyInstaller](#building-distributable-binary-with-pyinstaller) 33 | - [Microsoft Edge](#microsoft-edge) 34 | 35 | 36 | 37 | ## Intro 38 | 39 | There are several options for making GUI apps in Python, but if you want to use HTML/JS (in order to use jQueryUI or Bootstrap, for example) then you generally have to write a lot of boilerplate code to communicate from the Client (Javascript) side to the Server (Python) side. 40 | 41 | The closest Python equivalent to Electron (to my knowledge) is [cefpython](https://github.com/cztomczak/cefpython). It is a bit heavy weight for what I wanted. 42 | 43 | Eel is not as fully-fledged as Electron or cefpython - it is probably not suitable for making full blown applications like Atom - but it is very suitable for making the GUI equivalent of little utility scripts that you use internally in your team. 44 | 45 | For some reason many of the best-in-class number crunching and maths libraries are in Python (Tensorflow, Numpy, Scipy etc) but many of the best visualization libraries are in Javascript (D3, THREE.js etc). Hopefully Eel makes it easy to combine these into simple utility apps for assisting your development. 46 | 47 | Join Eel's users and maintainers on [Discord](https://discord.com/invite/3nqXPFX), if you like. 48 | 49 | ## Install 50 | 51 | Install from pypi with `pip`: 52 | 53 | ```shell 54 | pip install eel 55 | ``` 56 | 57 | To include support for HTML templating, currently using [Jinja2](https://pypi.org/project/Jinja2/#description): 58 | 59 | ```shell 60 | pip install eel[jinja2] 61 | ``` 62 | 63 | ## Usage 64 | 65 | ### Directory Structure 66 | 67 | An Eel application will be split into a frontend consisting of various web-technology files (.html, .js, .css) and a backend consisting of various Python scripts. 68 | 69 | All the frontend files should be put in a single directory (they can be further divided into folders inside this if necessary). 70 | 71 | ``` 72 | my_python_script.py <-- Python scripts 73 | other_python_module.py 74 | static_web_folder/ <-- Web folder 75 | main_page.html 76 | css/ 77 | style.css 78 | img/ 79 | logo.png 80 | ``` 81 | 82 | ### Starting the app 83 | 84 | Suppose you put all the frontend files in a directory called `web`, including your start page `main.html`, then the app is started like this; 85 | 86 | ```python 87 | import eel 88 | eel.init('web') 89 | eel.start('main.html') 90 | ``` 91 | 92 | This will start a webserver on the default settings (http://localhost:8000) and open a browser to http://localhost:8000/main.html. 93 | 94 | If Chrome or Chromium is installed then by default it will open in 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). 95 | 96 | ### App options 97 | 98 | Additional options can be passed to `eel.start()` as keyword arguments. 99 | 100 | 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. 101 | 102 | As of Eel v0.12.0, the following options are available to `start()`: 103 | - **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'`* 104 | - **host**, a string specifying what hostname to use for the Bottle server. *Default: `'localhost'`)* 105 | - **port**, an int specifying what port to use for the Bottle server. Use `0` for port to be picked automatically. *Default: `8000`*. 106 | - **block**, a bool saying whether or not the call to `start()` should block the calling thread. *Default: `True`* 107 | - **jinja_templates**, a string specifying a folder to use for Jinja2 templates, e.g. `my_templates`. *Default: `None`* 108 | - **cmdline_args**, a list of strings to pass to the command to start the browser. For example, we might add extra flags for Chrome; ```eel.start('main.html', mode='chrome-app', port=8080, cmdline_args=['--start-fullscreen', '--browser-startup-dialog'])```. *Default: `[]`* 109 | - **size**, a tuple of ints specifying the (width, height) of the main window in pixels *Default: `None`* 110 | - **position**, a tuple of ints specifying the (left, top) of the main window in pixels *Default: `None`* 111 | - **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: {}* 112 | - **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`* 113 | - **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. 114 | - **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 115 | 116 | 117 | 118 | ### Exposing functions 119 | 120 | In addition to the files in the frontend folder, a Javascript library will be served at `/eel.js`. You should include this in any pages: 121 | 122 | ```html 123 | 124 | ``` 125 | 126 | Including this library creates an `eel` object which can be used to communicate with the Python side. 127 | 128 | Any functions in the Python code which are decorated with `@eel.expose` like this... 129 | 130 | ```python 131 | @eel.expose 132 | def my_python_function(a, b): 133 | print(a, b, a + b) 134 | ``` 135 | 136 | ...will appear as methods on the `eel` object on the Javascript side, like this... 137 | 138 | ```javascript 139 | console.log("Calling Python..."); 140 | eel.my_python_function(1, 2); // This calls the Python function that was decorated 141 | ``` 142 | 143 | Similarly, any Javascript functions which are exposed like this... 144 | 145 | ```javascript 146 | eel.expose(my_javascript_function); 147 | function my_javascript_function(a, b, c, d) { 148 | if (a < b) { 149 | console.log(c * d); 150 | } 151 | } 152 | ``` 153 | 154 | can be called from the Python side like this... 155 | 156 | ```python 157 | print('Calling Javascript...') 158 | eel.my_javascript_function(1, 2, 3, 4) # This calls the Javascript function 159 | ``` 160 | 161 | The exposed name can also be overridden by passing in a second argument. If your app minifies JavaScript during builds, this may be necessary to ensure that functions can be resolved on the Python side: 162 | 163 | ```javascript 164 | eel.expose(someFunction, "my_javascript_function"); 165 | ``` 166 | 167 | When passing complex objects as arguments, bear in mind that internally they are converted to JSON and sent down a websocket (a process that potentially loses information). 168 | 169 | ### Eello, World! 170 | 171 | > See full example in: [examples/01 - hello_world](https://github.com/ChrisKnott/Eel/tree/master/examples/01%20-%20hello_world) 172 | 173 | Putting this together into a **Hello, World!** example, we have a short HTML page, `web/hello.html`: 174 | 175 | ```html 176 | 177 | 178 | 179 | Hello, World! 180 | 181 | 182 | 183 | 192 | 193 | 194 | 195 | Hello, World! 196 | 197 | 198 | ``` 199 | 200 | and a short Python script `hello.py`: 201 | 202 | ```python 203 | import eel 204 | 205 | # Set web files folder and optionally specify which file types to check for eel.expose() 206 | # *Default allowed_extensions are: ['.js', '.html', '.txt', '.htm', '.xhtml'] 207 | eel.init('web', allowed_extensions=['.js', '.html']) 208 | 209 | @eel.expose # Expose this function to Javascript 210 | def say_hello_py(x): 211 | print('Hello from %s' % x) 212 | 213 | say_hello_py('Python World!') 214 | eel.say_hello_js('Python World!') # Call a Javascript function 215 | 216 | eel.start('hello.html') # Start (this blocks and enters loop) 217 | ``` 218 | 219 | If we run the Python script (`python hello.py`), then a browser window will open displaying `hello.html`, and we will see... 220 | 221 | ``` 222 | Hello from Python World! 223 | Hello from Javascript World! 224 | ``` 225 | 226 | ...in the terminal, and... 227 | 228 | ``` 229 | Hello from Javascript World! 230 | Hello from Python World! 231 | ``` 232 | 233 | ...in the browser console (press F12 to open). 234 | 235 | You will notice that in the Python code, the Javascript function is called before the browser window is even started - any early calls like this are queued up and then sent once the websocket has been established. 236 | 237 | ### Return values 238 | 239 | While we want to think of our code as comprising a single application, the Python interpreter and the browser window run in separate processes. This can make communicating back and forth between them a bit of a mess, especially if we always had to explicitly _send_ values from one side to the other. 240 | 241 | Eel supports two ways of retrieving _return values_ from the other side of the app, which helps keep the code concise. 242 | 243 | To prevent hanging forever on the Python side, a timeout has been put in place for trying to retrieve values from 244 | the JavaScript side, which defaults to 10000 milliseconds (10 seconds). This can be changed with the `_js_result_timeout` parameter to `eel.init`. There is no corresponding timeout on the JavaScript side. 245 | 246 | #### Callbacks 247 | 248 | When you call an exposed function, you can immediately pass a callback function afterwards. This callback will automatically be called asynchronously with the return value when the function has finished executing on the other side. 249 | 250 | For example, if we have the following function defined and exposed in Javascript: 251 | 252 | ```javascript 253 | eel.expose(js_random); 254 | function js_random() { 255 | return Math.random(); 256 | } 257 | ``` 258 | 259 | Then in Python we can retrieve random values from the Javascript side like so: 260 | 261 | ```python 262 | def print_num(n): 263 | print('Got this from Javascript:', n) 264 | 265 | # Call Javascript function, and pass explicit callback function 266 | eel.js_random()(print_num) 267 | 268 | # Do the same with an inline lambda as callback 269 | eel.js_random()(lambda n: print('Got this from Javascript:', n)) 270 | ``` 271 | 272 | (It works exactly the same the other way around). 273 | 274 | #### Synchronous returns 275 | 276 | In most situations, the calls to the other side are to quickly retrieve some piece of data, such as the state of a widget or contents of an input field. In these cases it is more convenient to just synchronously wait a few milliseconds then continue with your code, rather than breaking the whole thing up into callbacks. 277 | 278 | To synchronously retrieve the return value, simply pass nothing to the second set of brackets. So in Python we would write: 279 | 280 | ```python 281 | n = eel.js_random()() # This immediately returns the value 282 | print('Got this from Javascript:', n) 283 | ``` 284 | 285 | You can only perform synchronous returns after the browser window has started (after calling `eel.start()`), otherwise obviously the call will hang. 286 | 287 | In Javascript, the language doesn't allow us to block while we wait for a callback, except by using `await` from inside an `async` function. So the equivalent code from the Javascript side would be: 288 | 289 | ```javascript 290 | async function run() { 291 | // Inside a function marked 'async' we can use the 'await' keyword. 292 | 293 | let n = await eel.py_random()(); // Must prefix call with 'await', otherwise it's the same syntax 294 | console.log("Got this from Python: " + n); 295 | } 296 | 297 | run(); 298 | ``` 299 | 300 | ## Asynchronous Python 301 | 302 | Eel is built on Bottle and Gevent, which provide an asynchronous event loop similar to Javascript. A lot of Python's standard library implicitly assumes there is a single execution thread - to deal with this, Gevent can "[monkey patch](https://en.wikipedia.org/wiki/Monkey_patch)" many of the standard modules such as `time`. ~~This monkey patching is done automatically when you call `import eel`~~. If you need monkey patching you should `import gevent.monkey` and call `gevent.monkey.patch_all()` _before_ you `import eel`. Monkey patching can interfere with things like debuggers so should be avoided unless necessary. 303 | 304 | For most cases you should be fine by avoiding using `time.sleep()` and instead using the versions provided by `gevent`. For convenience, the two most commonly needed gevent methods, `sleep()` and `spawn()` are provided directly from Eel (to save importing `time` and/or `gevent` as well). 305 | 306 | In this example... 307 | 308 | ```python 309 | import eel 310 | eel.init('web') 311 | 312 | def my_other_thread(): 313 | while True: 314 | print("I'm a thread") 315 | eel.sleep(1.0) # Use eel.sleep(), not time.sleep() 316 | 317 | eel.spawn(my_other_thread) 318 | 319 | eel.start('main.html', block=False) # Don't block on this call 320 | 321 | while True: 322 | print("I'm a main loop") 323 | eel.sleep(1.0) # Use eel.sleep(), not time.sleep() 324 | ``` 325 | 326 | ...we would then have three "threads" (greenlets) running; 327 | 328 | 1. Eel's internal thread for serving the web folder 329 | 2. The `my_other_thread` method, repeatedly printing **"I'm a thread"** 330 | 3. The main Python thread, which would be stuck in the final `while` loop, repeatedly printing **"I'm a main loop"** 331 | 332 | ## Building distributable binary with PyInstaller 333 | 334 | If you want to package your app into a program that can be run on a computer without a Python interpreter installed, you should use **PyInstaller**. 335 | 336 | 1. Configure a virtualenv with desired Python version and minimum necessary Python packages 337 | 2. Install PyInstaller `pip install PyInstaller` 338 | 3. In your app's folder, run `python -m eel [your_main_script] [your_web_folder]` (for example, you might run `python -m eel hello.py web`) 339 | 4. This will create a new folder `dist/` 340 | 5. Valid PyInstaller flags can be passed through, such as excluding modules with the flag: `--exclude module_name`. For example, you might run `python -m eel file_access.py web --exclude win32com --exclude numpy --exclude cryptography` 341 | 6. When happy that your app is working correctly, add `--onefile --noconsole` flags to build a single executable file 342 | 343 | Consult the [documentation for PyInstaller](http://PyInstaller.readthedocs.io/en/stable/) for more options. 344 | 345 | ## Microsoft Edge 346 | 347 | For Windows 10 users, Microsoft Edge (`eel.start(.., mode='edge')`) is installed by default and a useful fallback if a preferred browser is not installed. See the examples: 348 | 349 | - A Hello World example using Microsoft Edge: [examples/01 - hello_world-Edge/](https://github.com/ChrisKnott/Eel/tree/master/examples/01%20-%20hello_world-Edge) 350 | - Example implementing browser-fallbacks: [examples/07 - CreateReactApp/eel_CRA.py](https://github.com/ChrisKnott/Eel/tree/master/examples/07%20-%20CreateReactApp/eel_CRA.py) 351 | -------------------------------------------------------------------------------- /eel/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from builtins import range 3 | import traceback 4 | from io import open 5 | from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable 6 | from typing_extensions import Literal 7 | from eel.types import OptionsDictT, WebSocketT 8 | import gevent as gvt 9 | import json as jsn 10 | import bottle as btl 11 | try: 12 | import bottle_websocket as wbs 13 | except ImportError: 14 | import bottle.ext.websocket as wbs 15 | import re as rgx 16 | import os 17 | import eel.browsers as brw 18 | import pyparsing as pp 19 | import random as rnd 20 | import sys 21 | import pkg_resources as pkg 22 | import socket 23 | import mimetypes 24 | 25 | 26 | mimetypes.add_type('application/javascript', '.js') 27 | _eel_js_file: str = pkg.resource_filename('eel', 'eel.js') 28 | _eel_js: str = open(_eel_js_file, encoding='utf-8').read() 29 | _websockets: List[Tuple[Any, WebSocketT]] = [] 30 | _call_return_values: Dict[Any, Any] = {} 31 | _call_return_callbacks: Dict[float, Tuple[Callable[..., Any], Optional[Callable[..., Any]]]] = {} 32 | _call_number: int = 0 33 | _exposed_functions: Dict[Any, Any] = {} 34 | _js_functions: List[Any] = [] 35 | _mock_queue: List[Any] = [] 36 | _mock_queue_done: Set[Any] = set() 37 | _shutdown: Optional[gvt.Greenlet] = None # Later assigned as global by _websocket_close() 38 | root_path: str # Later assigned as global by init() 39 | 40 | # The maximum time (in milliseconds) that Python will try to retrieve a return value for functions executing in JS 41 | # Can be overridden through `eel.init` with the kwarg `js_result_timeout` (default: 10000) 42 | _js_result_timeout: int = 10000 43 | 44 | # Attribute holding the start args from calls to eel.start() 45 | _start_args: OptionsDictT = {} 46 | 47 | # == Temporary (suppressible) error message to inform users of breaking API change for v1.0.0 === 48 | api_error_message: str = ''' 49 | ---------------------------------------------------------------------------------- 50 | 'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel 51 | To suppress this error, add 'suppress_error=True' to start() call. 52 | This option will be removed in future versions 53 | ---------------------------------------------------------------------------------- 54 | ''' 55 | # =============================================================================================== 56 | 57 | 58 | # Public functions 59 | 60 | 61 | def expose(name_or_function: Optional[Callable[..., Any]] = None) -> Callable[..., Any]: 62 | '''Decorator to expose Python callables via Eel's JavaScript API. 63 | 64 | When an exposed function is called, a callback function can be passed 65 | immediately afterwards. This callback will be called asynchronously with 66 | the return value (possibly `None`) when the Python function has finished 67 | executing. 68 | 69 | Blocking calls to the exposed function from the JavaScript side are only 70 | possible using the :code:`await` keyword inside an :code:`async function`. 71 | These still have to make a call to the response, i.e. 72 | :code:`await eel.py_random()();` inside an :code:`async function` will work, 73 | but just :code:`await eel.py_random();` will not. 74 | 75 | :Example: 76 | 77 | In Python do: 78 | 79 | .. code-block:: python 80 | 81 | @expose 82 | def say_hello_py(name: str = 'You') -> None: 83 | print(f'{name} said hello from the JavaScript world!') 84 | 85 | In JavaScript do: 86 | 87 | .. code-block:: javascript 88 | 89 | eel.say_hello_py('Alice')(); 90 | 91 | Expected output on the Python console:: 92 | 93 | Alice said hello from the JavaScript world! 94 | 95 | ''' 96 | # Deal with '@eel.expose()' - treat as '@eel.expose' 97 | if name_or_function is None: 98 | return expose 99 | 100 | if isinstance(name_or_function, str): # Called as '@eel.expose("my_name")' 101 | name = name_or_function 102 | 103 | def decorator(function: Callable[..., Any]) -> Any: 104 | _expose(name, function) 105 | return function 106 | return decorator 107 | else: 108 | function = name_or_function 109 | _expose(function.__name__, function) 110 | return function 111 | 112 | 113 | # PyParsing grammar for parsing exposed functions in JavaScript code 114 | # Examples: `eel.expose(w, "func_name")`, `eel.expose(func_name)`, `eel.expose((function (e){}), "func_name")` 115 | EXPOSED_JS_FUNCTIONS: pp.ZeroOrMore = pp.ZeroOrMore( 116 | pp.Suppress( 117 | pp.SkipTo(pp.Literal('eel.expose(')) 118 | + pp.Literal('eel.expose(') 119 | + pp.Optional( 120 | pp.Or([pp.nestedExpr(), pp.Word(pp.printables, excludeChars=',')]) + pp.Literal(',') 121 | ) 122 | ) 123 | + pp.Suppress(pp.Regex(r'["\']?')) 124 | + pp.Word(pp.printables, excludeChars='"\')') 125 | + pp.Suppress(pp.Regex(r'["\']?\s*\)')), 126 | ) 127 | 128 | 129 | def init( 130 | path: str, 131 | allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm', '.xhtml', '.vue'], 132 | js_result_timeout: int = 10000) -> None: 133 | '''Initialise Eel. 134 | 135 | This function should be called before :func:`start()` to initialise the 136 | parameters for the web interface, such as the path to the files to be 137 | served. 138 | 139 | :param path: Sets the path on the filesystem where files to be served to 140 | the browser are located, e.g. :file:`web`. 141 | :param allowed_extensions: A list of filename extensions which will be 142 | parsed for exposed eel functions which should be callable from python. 143 | Files with extensions not in *allowed_extensions* will still be served, 144 | but any JavaScript functions, even if marked as exposed, will not be 145 | accessible from python. 146 | *Default:* :code:`['.js', '.html', '.txt', '.htm', '.xhtml', '.vue']`. 147 | :param js_result_timeout: How long Eel should be waiting to register the 148 | results from a call to Eel's JavaScript API before before timing out. 149 | *Default:* :code:`10000` milliseconds. 150 | ''' 151 | global root_path, _js_functions, _js_result_timeout 152 | root_path = _get_real_path(path) 153 | 154 | js_functions = set() 155 | for root, _, files in os.walk(root_path): 156 | for name in files: 157 | if not any(name.endswith(ext) for ext in allowed_extensions): 158 | continue 159 | 160 | try: 161 | with open(os.path.join(root, name), encoding='utf-8') as file: 162 | contents = file.read() 163 | expose_calls = set() 164 | matches = EXPOSED_JS_FUNCTIONS.parseString(contents).asList() 165 | for expose_call in matches: 166 | # Verify that function name is valid 167 | msg = "eel.expose() call contains '(' or '='" 168 | assert rgx.findall(r'[\(=]', expose_call) == [], msg 169 | expose_calls.add(expose_call) 170 | js_functions.update(expose_calls) 171 | except UnicodeDecodeError: 172 | pass # Malformed file probably 173 | 174 | _js_functions = list(js_functions) 175 | for js_function in _js_functions: 176 | _mock_js_function(js_function) 177 | 178 | _js_result_timeout = js_result_timeout 179 | 180 | 181 | def start( 182 | *start_urls: str, 183 | mode: Optional[Union[str, Literal[False]]] = 'chrome', 184 | host: str = 'localhost', 185 | port: int = 8000, 186 | block: bool = True, 187 | jinja_templates: Optional[str] = None, 188 | cmdline_args: List[str] = ['--disable-http-cache'], 189 | size: Optional[Tuple[int, int]] = None, 190 | position: Optional[Tuple[int, int]] = None, 191 | geometry: Dict[str, Tuple[int, int]] = {}, 192 | close_callback: Optional[Callable[..., Any]] = None, 193 | app_mode: bool = True, 194 | all_interfaces: bool = False, 195 | disable_cache: bool = True, 196 | default_path: str = 'index.html', 197 | app: btl.Bottle = btl.default_app(), 198 | shutdown_delay: float = 1.0, 199 | suppress_error: bool = False) -> None: 200 | '''Start the Eel app. 201 | 202 | Suppose you put all the frontend files in a directory called 203 | :file:`web`, including your start page :file:`main.html`, then the app 204 | is started like this: 205 | 206 | .. code-block:: python 207 | 208 | import eel 209 | eel.init('web') 210 | eel.start('main.html') 211 | 212 | This will start a webserver on the default settings 213 | (http://localhost:8000) and open a browser to 214 | http://localhost:8000/main.html. 215 | 216 | If Chrome or Chromium is installed then by default it will open that in 217 | *App Mode* (with the `--app` cmdline flag), regardless of what the OS's 218 | default browser is set to (it is possible to override this behaviour). 219 | 220 | :param mode: What browser is used, e.g. :code:`'chrome'`, 221 | :code:`'electron'`, :code:`'edge'`, :code:`'custom'`. Can also be 222 | `None` or `False` to not open a window. *Default:* :code:`'chrome'`. 223 | :param host: Hostname used for Bottle server. *Default:* 224 | :code:`'localhost'`. 225 | :param port: Port used for Bottle server. Use :code:`0` for port to be 226 | picked automatically. *Default:* :code:`8000`. 227 | :param block: Whether the call to :func:`start()` blocks the calling 228 | thread. *Default:* `True`. 229 | :param jinja_templates: Folder for :mod:`jinja2` templates, e.g. 230 | :file:`my_templates`. *Default:* `None`. 231 | :param cmdline_args: A list of strings to pass to the command starting the 232 | browser. For example, we might add extra flags to Chrome with 233 | :code:`eel.start('main.html', mode='chrome-app', port=8080, 234 | cmdline_args=['--start-fullscreen', '--browser-startup-dialog'])`. 235 | *Default:* :code:`[]`. 236 | :param size: Tuple specifying the (width, height) of the main window in 237 | pixels. *Default:* `None`. 238 | :param position: Tuple specifying the (left, top) position of the main 239 | window in pixels. *Default*: `None`. 240 | :param geometry: A dictionary of specifying the size/position for all 241 | windows. The keys should be the relative path of the page, and the 242 | values should be a dictionary of the form 243 | :code:`{'size': (200, 100), 'position': (300, 50)}`. *Default:* 244 | :code:`{}`. 245 | :param close_callback: A lambda or function that is called when a websocket 246 | or window closes (i.e. when the user closes the window). It should take 247 | two arguments: a string which is the relative path of the page that 248 | just closed, and a list of the other websockets that are still open. 249 | *Default:* `None`. 250 | :param app_mode: Whether to run Chrome/Edge in App Mode. You can also 251 | specify *mode* as :code:`mode='chrome-app'` as a shorthand to start 252 | Chrome in App Mode. 253 | :param all_interfaces: Whether to allow the :mod:`bottle` server to listen 254 | for connections on all interfaces. 255 | :param disable_cache: Sets the no-store response header when serving 256 | assets. 257 | :param default_path: The default file to retrieve for the root URL. 258 | :param app: An instance of :class:`bottle.Bottle` which will be used rather 259 | than creating a fresh one. This can be used to install middleware on 260 | the instance before starting Eel, e.g. for session management, 261 | authentication, etc. If *app* is not a :class:`bottle.Bottle` instance, 262 | you will need to call :code:`eel.register_eel_routes(app)` on your 263 | custom app instance. 264 | :param shutdown_delay: Timer configurable for Eel's shutdown detection 265 | mechanism, whereby when any websocket closes, it waits *shutdown_delay* 266 | seconds, and then checks if there are now any websocket connections. 267 | If not, then Eel closes. In case the user has closed the browser and 268 | wants to exit the program. *Default:* :code:`1.0` seconds. 269 | :param suppress_error: Temporary (suppressible) error message to inform 270 | users of breaking API change for v1.0.0. Set to `True` to suppress 271 | the error message. 272 | ''' 273 | _start_args.update({ 274 | 'mode': mode, 275 | 'host': host, 276 | 'port': port, 277 | 'block': block, 278 | 'jinja_templates': jinja_templates, 279 | 'cmdline_args': cmdline_args, 280 | 'size': size, 281 | 'position': position, 282 | 'geometry': geometry, 283 | 'close_callback': close_callback, 284 | 'app_mode': app_mode, 285 | 'all_interfaces': all_interfaces, 286 | 'disable_cache': disable_cache, 287 | 'default_path': default_path, 288 | 'app': app, 289 | 'shutdown_delay': shutdown_delay, 290 | 'suppress_error': suppress_error, 291 | }) 292 | 293 | if _start_args['port'] == 0: 294 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 295 | sock.bind(('localhost', 0)) 296 | _start_args['port'] = sock.getsockname()[1] 297 | sock.close() 298 | 299 | if _start_args['jinja_templates'] is not None: 300 | from jinja2 import Environment, FileSystemLoader, select_autoescape 301 | if not isinstance(_start_args['jinja_templates'], str): 302 | raise TypeError("'jinja_templates' start_arg/option must be of type str") 303 | templates_path = os.path.join(root_path, _start_args['jinja_templates']) 304 | _start_args['jinja_env'] = Environment( 305 | loader=FileSystemLoader(templates_path), 306 | autoescape=select_autoescape(['html', 'xml']) 307 | ) 308 | 309 | # verify shutdown_delay is correct value 310 | if not isinstance(_start_args['shutdown_delay'], (int, float)): 311 | raise ValueError( 312 | '`shutdown_delay` must be a number, ' 313 | 'got a {}'.format(type(_start_args['shutdown_delay'])) 314 | ) 315 | 316 | # Launch the browser to the starting URLs 317 | show(*start_urls) 318 | 319 | def run_lambda() -> None: 320 | if _start_args['all_interfaces'] is True: 321 | HOST = '0.0.0.0' 322 | else: 323 | if not isinstance(_start_args['host'], str): 324 | raise TypeError("'host' start_arg/option must be of type str") 325 | HOST = _start_args['host'] 326 | 327 | app = _start_args['app'] 328 | 329 | if isinstance(app, btl.Bottle): 330 | register_eel_routes(app) 331 | else: 332 | register_eel_routes(btl.default_app()) 333 | 334 | btl.run( 335 | host=HOST, 336 | port=_start_args['port'], 337 | server=wbs.GeventWebSocketServer, 338 | quiet=True, 339 | app=app) # Always returns None 340 | 341 | # Start the webserver 342 | if _start_args['block']: 343 | run_lambda() 344 | else: 345 | spawn(run_lambda) 346 | 347 | 348 | def show(*start_urls: str) -> None: 349 | '''Show the specified URL(s) in the browser. 350 | 351 | Suppose you have two files in your :file:`web` folder. The file 352 | :file:`hello.html` regularly includes :file:`eel.js` and provides 353 | interactivity, and the file :file:`goodbye.html` does not include 354 | :file:`eel.js` and simply provides plain HTML content not reliant on Eel. 355 | 356 | First, we defien a callback function to be called when the browser 357 | window is closed: 358 | 359 | .. code-block:: python 360 | 361 | def last_calls(): 362 | eel.show('goodbye.html') 363 | 364 | Now we initialise and start Eel, with a :code:`close_callback` to our 365 | function: 366 | 367 | ..code-block:: python 368 | 369 | eel.init('web') 370 | eel.start('hello.html', mode='chrome-app', close_callback=last_calls) 371 | 372 | When the websocket from :file:`hello.html` is closed (e.g. because the 373 | user closed the browser window), Eel will wait *shutdown_delay* seconds 374 | (by default 1 second), then call our :code:`last_calls()` function, which 375 | opens another window with the :file:`goodbye.html` shown before our Eel app 376 | terminates. 377 | 378 | :param start_urls: One or more URLs to be opened. 379 | ''' 380 | brw.open(list(start_urls), _start_args) 381 | 382 | 383 | def sleep(seconds: Union[int, float]) -> None: 384 | '''A non-blocking sleep call compatible with the Gevent event loop. 385 | 386 | .. note:: 387 | While this function simply wraps :func:`gevent.sleep()`, it is better 388 | to call :func:`eel.sleep()` in your eel app, as this will ensure future 389 | compatibility in case the implementation of Eel should change in some 390 | respect. 391 | 392 | :param seconds: The number of seconds to sleep. 393 | ''' 394 | gvt.sleep(seconds) 395 | 396 | 397 | def spawn(function: Callable[..., Any], *args: Any, **kwargs: Any) -> gvt.Greenlet: 398 | '''Spawn a new Greenlet. 399 | 400 | Calling this function will spawn a new :class:`gevent.Greenlet` running 401 | *function* asynchronously. 402 | 403 | .. caution:: 404 | If you spawn your own Greenlets to run in addition to those spawned by 405 | Eel's internal core functionality, you will have to ensure that those 406 | Greenlets will terminate as appropriate (either by returning or by 407 | being killed via Gevent's kill mechanism), otherwise your app may not 408 | terminate correctly when Eel itself terminates. 409 | 410 | :param function: The function to be called and run as the Greenlet. 411 | :param *args: Any positional arguments that should be passed to *function*. 412 | :param **kwargs: Any key-word arguments that should be passed to 413 | *function*. 414 | ''' 415 | return gvt.spawn(function, *args, **kwargs) 416 | 417 | 418 | # Bottle Routes 419 | 420 | 421 | def _eel() -> str: 422 | start_geometry = {'default': {'size': _start_args['size'], 423 | 'position': _start_args['position']}, 424 | 'pages': _start_args['geometry']} 425 | 426 | page = _eel_js.replace('/** _py_functions **/', 427 | '_py_functions: %s,' % list(_exposed_functions.keys())) 428 | page = page.replace('/** _start_geometry **/', 429 | '_start_geometry: %s,' % _safe_json(start_geometry)) 430 | btl.response.content_type = 'application/javascript' 431 | _set_response_headers(btl.response) 432 | return page 433 | 434 | 435 | def _root() -> btl.Response: 436 | if not isinstance(_start_args['default_path'], str): 437 | raise TypeError("'default_path' start_arg/option must be of type str") 438 | return _static(_start_args['default_path']) 439 | 440 | 441 | def _static(path: str) -> btl.Response: 442 | response = None 443 | if 'jinja_env' in _start_args and 'jinja_templates' in _start_args: 444 | if not isinstance(_start_args['jinja_templates'], str): 445 | raise TypeError("'jinja_templates' start_arg/option must be of type str") 446 | template_prefix = _start_args['jinja_templates'] + '/' 447 | if path.startswith(template_prefix): 448 | n = len(template_prefix) 449 | template = _start_args['jinja_env'].get_template(path[n:]) 450 | response = btl.HTTPResponse(template.render()) 451 | 452 | if response is None: 453 | response = btl.static_file(path, root=root_path) 454 | 455 | _set_response_headers(response) 456 | return response 457 | 458 | 459 | def _websocket(ws: WebSocketT) -> None: 460 | global _websockets 461 | 462 | for js_function in _js_functions: 463 | _import_js_function(js_function) 464 | 465 | page = btl.request.query.page 466 | if page not in _mock_queue_done: 467 | for call in _mock_queue: 468 | _repeated_send(ws, _safe_json(call)) 469 | _mock_queue_done.add(page) 470 | 471 | _websockets += [(page, ws)] 472 | 473 | while True: 474 | msg = ws.receive() 475 | if msg is not None: 476 | message = jsn.loads(msg) 477 | spawn(_process_message, message, ws) 478 | else: 479 | _websockets.remove((page, ws)) 480 | break 481 | 482 | _websocket_close(page) 483 | 484 | 485 | BOTTLE_ROUTES: Dict[str, Tuple[Callable[..., Any], Dict[Any, Any]]] = { 486 | "/eel.js": (_eel, dict()), 487 | "/": (_root, dict()), 488 | "/": (_static, dict()), 489 | "/eel": (_websocket, dict(apply=[wbs.websocket])) 490 | } 491 | 492 | 493 | def register_eel_routes(app: btl.Bottle) -> None: 494 | '''Register the required eel routes with `app`. 495 | 496 | .. note:: 497 | 498 | :func:`eel.register_eel_routes()` is normally invoked implicitly by 499 | :func:`eel.start()` and does not need to be called explicitly in most 500 | cases. Registering the eel routes explicitly is only needed if you are 501 | passing something other than an instance of :class:`bottle.Bottle` to 502 | :func:`eel.start()`. 503 | 504 | :Example: 505 | 506 | >>> app = bottle.Bottle() 507 | >>> eel.register_eel_routes(app) 508 | >>> middleware = beaker.middleware.SessionMiddleware(app) 509 | >>> eel.start(app=middleware) 510 | 511 | ''' 512 | for route_path, route_params in BOTTLE_ROUTES.items(): 513 | route_func, route_kwargs = route_params 514 | app.route(path=route_path, callback=route_func, **route_kwargs) 515 | 516 | 517 | # Private functions 518 | 519 | 520 | def _safe_json(obj: Any) -> str: 521 | return jsn.dumps(obj, default=lambda o: None) 522 | 523 | 524 | def _repeated_send(ws: WebSocketT, msg: str) -> None: 525 | for attempt in range(100): 526 | try: 527 | ws.send(msg) 528 | break 529 | except Exception: 530 | sleep(0.001) 531 | 532 | 533 | def _process_message(message: Dict[str, Any], ws: WebSocketT) -> None: 534 | if 'call' in message: 535 | error_info = {} 536 | try: 537 | return_val = _exposed_functions[message['name']](*message['args']) 538 | status = 'ok' 539 | except Exception as e: 540 | err_traceback = traceback.format_exc() 541 | traceback.print_exc() 542 | return_val = None 543 | status = 'error' 544 | error_info['errorText'] = repr(e) 545 | error_info['errorTraceback'] = err_traceback 546 | _repeated_send(ws, _safe_json({ 'return': message['call'], 547 | 'status': status, 548 | 'value': return_val, 549 | 'error': error_info,})) 550 | elif 'return' in message: 551 | call_id = message['return'] 552 | if call_id in _call_return_callbacks: 553 | callback, error_callback = _call_return_callbacks.pop(call_id) 554 | if message['status'] == 'ok': 555 | callback(message['value']) 556 | elif message['status'] == 'error' and error_callback is not None: 557 | error_callback(message['error'], message['stack']) 558 | else: 559 | _call_return_values[call_id] = message['value'] 560 | 561 | else: 562 | print('Invalid message received: ', message) 563 | 564 | 565 | def _get_real_path(path: str) -> str: 566 | if getattr(sys, 'frozen', False): 567 | return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstaller 568 | else: 569 | return os.path.abspath(path) 570 | 571 | 572 | def _mock_js_function(f: str) -> None: 573 | exec('%s = lambda *args: _mock_call("%s", args)' % (f, f), globals()) 574 | 575 | 576 | def _import_js_function(f: str) -> None: 577 | exec('%s = lambda *args: _js_call("%s", args)' % (f, f), globals()) 578 | 579 | 580 | def _call_object(name: str, args: Any) -> Dict[str, Any]: 581 | global _call_number 582 | _call_number += 1 583 | call_id = _call_number + rnd.random() 584 | return {'call': call_id, 'name': name, 'args': args} 585 | 586 | 587 | def _mock_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: 588 | call_object = _call_object(name, args) 589 | global _mock_queue 590 | _mock_queue += [call_object] 591 | return _call_return(call_object) 592 | 593 | 594 | def _js_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: 595 | call_object = _call_object(name, args) 596 | for _, ws in _websockets: 597 | _repeated_send(ws, _safe_json(call_object)) 598 | return _call_return(call_object) 599 | 600 | 601 | def _call_return(call: Dict[str, Any]) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: 602 | global _js_result_timeout 603 | call_id = call['call'] 604 | 605 | def return_func(callback: Optional[Callable[..., Any]] = None, 606 | error_callback: Optional[Callable[..., Any]] = None) -> Any: 607 | if callback is not None: 608 | _call_return_callbacks[call_id] = (callback, error_callback) 609 | else: 610 | for w in range(_js_result_timeout): 611 | if call_id in _call_return_values: 612 | return _call_return_values.pop(call_id) 613 | sleep(0.001) 614 | return return_func 615 | 616 | 617 | def _expose(name: str, function: Callable[..., Any]) -> None: 618 | msg = 'Already exposed function with name "%s"' % name 619 | assert name not in _exposed_functions, msg 620 | _exposed_functions[name] = function 621 | 622 | 623 | def _detect_shutdown() -> None: 624 | if len(_websockets) == 0: 625 | sys.exit() 626 | 627 | 628 | def _websocket_close(page: str) -> None: 629 | global _shutdown 630 | 631 | close_callback = _start_args.get('close_callback') 632 | 633 | if close_callback is not None: 634 | if not callable(close_callback): 635 | raise TypeError("'close_callback' start_arg/option must be callable or None") 636 | sockets = [p for _, p in _websockets] 637 | close_callback(page, sockets) 638 | else: 639 | if isinstance(_shutdown, gvt.Greenlet): 640 | _shutdown.kill() 641 | 642 | _shutdown = gvt.spawn_later(_start_args['shutdown_delay'], _detect_shutdown) 643 | 644 | 645 | def _set_response_headers(response: btl.Response) -> None: 646 | if _start_args['disable_cache']: 647 | # https://stackoverflow.com/a/24748094/280852 648 | response.set_header('Cache-Control', 'no-store') 649 | -------------------------------------------------------------------------------- /eel/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import pkg_resources as pkg 3 | import PyInstaller.__main__ as pyi 4 | import os 5 | from argparse import ArgumentParser, Namespace 6 | from typing import List 7 | 8 | parser: ArgumentParser = ArgumentParser(description=""" 9 | Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps, 10 | with full access to Python capabilities and libraries. 11 | """) 12 | parser.add_argument( 13 | "main_script", 14 | type=str, 15 | help="Main python file to run app from" 16 | ) 17 | parser.add_argument( 18 | "web_folder", 19 | type=str, 20 | help="Folder including all web files including file as html, css, ico, etc." 21 | ) 22 | args: Namespace 23 | unknown_args: List[str] 24 | args, unknown_args = parser.parse_known_args() 25 | main_script: str = args.main_script 26 | web_folder: str = args.web_folder 27 | 28 | print("Building executable with main script '%s' and web folder '%s'...\n" % 29 | (main_script, web_folder)) 30 | 31 | eel_js_file: str = pkg.resource_filename('eel', 'eel.js') 32 | js_file_arg: str = '%s%seel' % (eel_js_file, os.pathsep) 33 | web_folder_arg: str = '%s%s%s' % (web_folder, os.pathsep, web_folder) 34 | 35 | needed_args: List[str] = ['--hidden-import', 'bottle_websocket', 36 | '--add-data', js_file_arg, '--add-data', web_folder_arg] 37 | full_args: List[str] = [main_script] + needed_args + unknown_args 38 | print('Running:\npyinstaller', ' '.join(full_args), '\n') 39 | 40 | pyi.run(full_args) 41 | -------------------------------------------------------------------------------- /eel/browsers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import subprocess as sps 3 | import webbrowser as wbr 4 | from typing import Union, List, Dict, Iterable, Optional 5 | from types import ModuleType 6 | 7 | from eel.types import OptionsDictT 8 | import eel.chrome as chm 9 | import eel.electron as ele 10 | import eel.edge as edge 11 | import eel.msIE as ie 12 | #import eel.firefox as ffx TODO 13 | #import eel.safari as saf TODO 14 | 15 | _browser_paths: Dict[str, str] = {} 16 | _browser_modules: Dict[str, ModuleType] = {'chrome': chm, 17 | 'electron': ele, 18 | 'edge': edge, 19 | 'msie':ie} 20 | 21 | 22 | def _build_url_from_dict(page: Dict[str, str], options: OptionsDictT) -> str: 23 | scheme = page.get('scheme', 'http') 24 | host = page.get('host', 'localhost') 25 | port = page.get('port', options["port"]) 26 | path = page.get('path', '') 27 | if not isinstance(port, (int, str)): 28 | raise TypeError("'port' option must be an integer") 29 | return '%s://%s:%d/%s' % (scheme, host, int(port), path) 30 | 31 | 32 | def _build_url_from_string(page: str, options: OptionsDictT) -> str: 33 | if not isinstance(options['port'], (int, str)): 34 | raise TypeError("'port' option must be an integer") 35 | base_url = 'http://%s:%d/' % (options['host'], int(options['port'])) 36 | return base_url + page 37 | 38 | 39 | def _build_urls(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDictT) -> List[str]: 40 | urls: List[str] = [] 41 | 42 | for page in start_pages: 43 | if isinstance(page, dict): 44 | url = _build_url_from_dict(page, options) 45 | else: 46 | url = _build_url_from_string(page, options) 47 | urls.append(url) 48 | 49 | return urls 50 | 51 | 52 | def open(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDictT) -> None: 53 | # Build full URLs for starting pages (including host and port) 54 | start_urls = _build_urls(start_pages, options) 55 | 56 | mode = options.get('mode') 57 | if not isinstance(mode, (str, type(None))) and mode is not False: 58 | raise TypeError("'mode' option must by either a string, False, or None") 59 | if mode is None or mode is False: 60 | # Don't open a browser 61 | pass 62 | elif mode == 'custom': 63 | # Just run whatever command the user provided 64 | if not isinstance(options['cmdline_args'], list): 65 | raise TypeError("'cmdline_args' option must be of type List[str]") 66 | sps.Popen(options['cmdline_args'], 67 | stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE) 68 | elif mode in _browser_modules: 69 | # Run with a specific browser 70 | browser_module = _browser_modules[mode] 71 | path = _browser_paths.get(mode) 72 | if path is None: 73 | # Don't know this browser's path, try and find it ourselves 74 | path = browser_module.find_path() 75 | _browser_paths[mode] = path 76 | 77 | if path is not None: 78 | browser_module.run(path, options, start_urls) 79 | else: 80 | raise EnvironmentError("Can't find %s installation" % browser_module.name) 81 | else: 82 | # Fall back to system default browser 83 | for url in start_urls: 84 | wbr.open(url) 85 | 86 | 87 | def set_path(browser_name: str, path: str) -> None: 88 | _browser_paths[browser_name] = path 89 | 90 | 91 | def get_path(browser_name: str) -> Optional[str]: 92 | return _browser_paths.get(browser_name) 93 | 94 | -------------------------------------------------------------------------------- /eel/chrome.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import sys 3 | import os 4 | import subprocess as sps 5 | from shutil import which 6 | from typing import List, Optional 7 | from eel.types import OptionsDictT 8 | 9 | # Every browser specific module must define run(), find_path() and name like this 10 | 11 | name: str = 'Google Chrome/Chromium' 12 | 13 | def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: 14 | if not isinstance(options['cmdline_args'], list): 15 | raise TypeError("'cmdline_args' option must be of type List[str]") 16 | if options['app_mode']: 17 | for url in start_urls: 18 | sps.Popen([path, '--app=%s' % url] + 19 | options['cmdline_args'], 20 | stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE) 21 | else: 22 | args: List[str] = options['cmdline_args'] + start_urls 23 | sps.Popen([path, '--new-window'] + args, 24 | stdout=sps.PIPE, stderr=sys.stderr, stdin=sps.PIPE) 25 | 26 | 27 | def find_path() -> Optional[str]: 28 | if sys.platform in ['win32', 'win64']: 29 | return _find_chrome_win() 30 | elif sys.platform == 'darwin': 31 | return _find_chrome_mac() or _find_chromium_mac() 32 | elif sys.platform.startswith('linux'): 33 | return _find_chrome_linux() 34 | else: 35 | return None 36 | 37 | 38 | def _find_chrome_mac() -> Optional[str]: 39 | default_dir = r'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' 40 | if os.path.exists(default_dir): 41 | return default_dir 42 | # use mdfind ci to locate Chrome in alternate locations and return the first one 43 | name = 'Google Chrome.app' 44 | alternate_dirs = [x for x in sps.check_output(["mdfind", name]).decode().split('\n') if x.endswith(name)] 45 | if len(alternate_dirs): 46 | return alternate_dirs[0] + '/Contents/MacOS/Google Chrome' 47 | return None 48 | 49 | 50 | def _find_chromium_mac() -> Optional[str]: 51 | default_dir = r'/Applications/Chromium.app/Contents/MacOS/Chromium' 52 | if os.path.exists(default_dir): 53 | return default_dir 54 | # use mdfind ci to locate Chromium in alternate locations and return the first one 55 | name = 'Chromium.app' 56 | alternate_dirs = [x for x in sps.check_output(["mdfind", name]).decode().split('\n') if x.endswith(name)] 57 | if len(alternate_dirs): 58 | return alternate_dirs[0] + '/Contents/MacOS/Chromium' 59 | return None 60 | 61 | 62 | def _find_chrome_linux() -> Optional[str]: 63 | chrome_names = ['chromium-browser', 64 | 'chromium', 65 | 'google-chrome', 66 | 'google-chrome-stable'] 67 | 68 | for name in chrome_names: 69 | chrome = which(name) 70 | if chrome is not None: 71 | return chrome 72 | return None 73 | 74 | 75 | def _find_chrome_win() -> Optional[str]: 76 | import winreg as reg 77 | reg_path = r'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' 78 | chrome_path: Optional[str] = None 79 | 80 | for install_type in reg.HKEY_CURRENT_USER, reg.HKEY_LOCAL_MACHINE: 81 | try: 82 | reg_key = reg.OpenKey(install_type, reg_path, 0, reg.KEY_READ) 83 | chrome_path = reg.QueryValue(reg_key, None) 84 | reg_key.Close() 85 | if not os.path.isfile(chrome_path): 86 | continue 87 | except WindowsError: 88 | chrome_path = None 89 | else: 90 | break 91 | 92 | return chrome_path 93 | -------------------------------------------------------------------------------- /eel/edge.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import platform 3 | import subprocess as sps 4 | import sys 5 | from typing import List 6 | 7 | from eel.types import OptionsDictT 8 | 9 | name: str = 'Edge' 10 | 11 | 12 | def run(_path: str, options: OptionsDictT, start_urls: List[str]) -> None: 13 | if not isinstance(options['cmdline_args'], list): 14 | raise TypeError("'cmdline_args' option must be of type List[str]") 15 | args: List[str] = options['cmdline_args'] 16 | if options['app_mode']: 17 | cmd = 'start msedge --app={} '.format(start_urls[0]) 18 | cmd = cmd + (" ".join(args)) 19 | sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) 20 | else: 21 | cmd = "start msedge --new-window "+(" ".join(args)) +" "+(start_urls[0]) 22 | sps.Popen(cmd,stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) 23 | 24 | def find_path() -> bool: 25 | if platform.system() == 'Windows': 26 | return True 27 | 28 | return False 29 | -------------------------------------------------------------------------------- /eel/eel.js: -------------------------------------------------------------------------------- 1 | eel = { 2 | _host: window.location.origin, 3 | 4 | set_host: function (hostname) { 5 | eel._host = hostname 6 | }, 7 | 8 | expose: function(f, name) { 9 | if(name === undefined){ 10 | name = f.toString(); 11 | let i = 'function '.length, j = name.indexOf('('); 12 | name = name.substring(i, j).trim(); 13 | } 14 | 15 | eel._exposed_functions[name] = f; 16 | }, 17 | 18 | guid: function() { 19 | return eel._guid; 20 | }, 21 | 22 | // These get dynamically added by library when file is served 23 | /** _py_functions **/ 24 | /** _start_geometry **/ 25 | 26 | _guid: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => 27 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 28 | ), 29 | 30 | _exposed_functions: {}, 31 | 32 | _mock_queue: [], 33 | 34 | _mock_py_functions: function() { 35 | for(let i = 0; i < eel._py_functions.length; i++) { 36 | let name = eel._py_functions[i]; 37 | eel[name] = function() { 38 | let call_object = eel._call_object(name, arguments); 39 | eel._mock_queue.push(call_object); 40 | return eel._call_return(call_object); 41 | } 42 | } 43 | }, 44 | 45 | _import_py_function: function(name) { 46 | let func_name = name; 47 | eel[name] = function() { 48 | let call_object = eel._call_object(func_name, arguments); 49 | eel._websocket.send(eel._toJSON(call_object)); 50 | return eel._call_return(call_object); 51 | } 52 | }, 53 | 54 | _call_number: 0, 55 | 56 | _call_return_callbacks: {}, 57 | 58 | _call_object: function(name, args) { 59 | let arg_array = []; 60 | for(let i = 0; i < args.length; i++){ 61 | arg_array.push(args[i]); 62 | } 63 | 64 | let call_id = (eel._call_number += 1) + Math.random(); 65 | return {'call': call_id, 'name': name, 'args': arg_array}; 66 | }, 67 | 68 | _sleep: function(ms) { 69 | return new Promise(resolve => setTimeout(resolve, ms)); 70 | }, 71 | 72 | _toJSON: function(obj) { 73 | return JSON.stringify(obj, (k, v) => v === undefined ? null : v); 74 | }, 75 | 76 | _call_return: function(call) { 77 | return function(callback = null) { 78 | if(callback != null) { 79 | eel._call_return_callbacks[call.call] = {resolve: callback}; 80 | } else { 81 | return new Promise(function(resolve, reject) { 82 | eel._call_return_callbacks[call.call] = {resolve: resolve, reject: reject}; 83 | }); 84 | } 85 | } 86 | }, 87 | 88 | _position_window: function(page) { 89 | let size = eel._start_geometry['default'].size; 90 | let position = eel._start_geometry['default'].position; 91 | 92 | if(page in eel._start_geometry.pages) { 93 | size = eel._start_geometry.pages[page].size; 94 | position = eel._start_geometry.pages[page].position; 95 | } 96 | 97 | if(size != null){ 98 | window.resizeTo(size[0], size[1]); 99 | } 100 | 101 | if(position != null){ 102 | window.moveTo(position[0], position[1]); 103 | } 104 | }, 105 | 106 | _init: function() { 107 | eel._mock_py_functions(); 108 | 109 | document.addEventListener("DOMContentLoaded", function(event) { 110 | let page = window.location.pathname.substring(1); 111 | eel._position_window(page); 112 | 113 | let websocket_addr = (eel._host + '/eel').replace('http', 'ws'); 114 | websocket_addr += ('?page=' + page); 115 | eel._websocket = new WebSocket(websocket_addr); 116 | 117 | eel._websocket.onopen = function() { 118 | for(let i = 0; i < eel._py_functions.length; i++){ 119 | let py_function = eel._py_functions[i]; 120 | eel._import_py_function(py_function); 121 | } 122 | 123 | while(eel._mock_queue.length > 0) { 124 | let call = eel._mock_queue.shift(); 125 | eel._websocket.send(eel._toJSON(call)); 126 | } 127 | }; 128 | 129 | eel._websocket.onmessage = function (e) { 130 | let message = JSON.parse(e.data); 131 | if(message.hasOwnProperty('call') ) { 132 | // Python making a function call into us 133 | if(message.name in eel._exposed_functions) { 134 | try { 135 | let return_val = eel._exposed_functions[message.name](...message.args); 136 | eel._websocket.send(eel._toJSON({'return': message.call, 'status':'ok', 'value': return_val})); 137 | } catch(err) { 138 | debugger 139 | eel._websocket.send(eel._toJSON( 140 | {'return': message.call, 141 | 'status':'error', 142 | 'error': err.message, 143 | 'stack': err.stack})); 144 | } 145 | } 146 | } else if(message.hasOwnProperty('return')) { 147 | // Python returning a value to us 148 | if(message['return'] in eel._call_return_callbacks) { 149 | if(message['status']==='ok'){ 150 | eel._call_return_callbacks[message['return']].resolve(message.value); 151 | } 152 | else if(message['status']==='error' && eel._call_return_callbacks[message['return']].reject) { 153 | eel._call_return_callbacks[message['return']].reject(message['error']); 154 | } 155 | } 156 | } else { 157 | throw 'Invalid message ' + message; 158 | } 159 | 160 | }; 161 | }); 162 | } 163 | }; 164 | 165 | eel._init(); 166 | 167 | if(typeof require !== 'undefined'){ 168 | // Avoid name collisions when using Electron, so jQuery etc work normally 169 | window.nodeRequire = require; 170 | delete window.require; 171 | delete window.exports; 172 | delete window.module; 173 | } 174 | -------------------------------------------------------------------------------- /eel/electron.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import sys 3 | import os 4 | import subprocess as sps 5 | from shutil import which 6 | from typing import List, Optional 7 | 8 | from eel.types import OptionsDictT 9 | 10 | name: str = 'Electron' 11 | 12 | def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: 13 | if not isinstance(options['cmdline_args'], list): 14 | raise TypeError("'cmdline_args' option must be of type List[str]") 15 | cmd = [path] + options['cmdline_args'] 16 | cmd += ['.', ';'.join(start_urls)] 17 | sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE) 18 | 19 | 20 | def find_path() -> Optional[str]: 21 | if sys.platform in ['win32', 'win64']: 22 | # It doesn't work well passing the .bat file to Popen, so we get the actual .exe 23 | bat_path = which('electron') 24 | if bat_path: 25 | return os.path.join(bat_path, r'..\node_modules\electron\dist\electron.exe') 26 | elif sys.platform in ['darwin', 'linux']: 27 | # This should work fine... 28 | return which('electron') 29 | return None 30 | -------------------------------------------------------------------------------- /eel/msIE.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import subprocess as sps 3 | import sys 4 | from typing import List 5 | 6 | from eel.types import OptionsDictT 7 | 8 | name: str = 'MSIE' 9 | 10 | 11 | def run(_path: str, options: OptionsDictT, start_urls: List[str]) -> None: 12 | cmd = 'start microsoft-edge:{}'.format(start_urls[0]) 13 | sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) 14 | 15 | 16 | def find_path() -> bool: 17 | if platform.system() == 'Windows': 18 | return True 19 | 20 | return False 21 | -------------------------------------------------------------------------------- /eel/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/eel/py.typed -------------------------------------------------------------------------------- /eel/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Union, Dict, List, Tuple, Callable, Optional, Any, TYPE_CHECKING 3 | from typing_extensions import Literal, TypedDict, TypeAlias 4 | from bottle import Bottle 5 | 6 | # This business is slightly awkward, but needed for backward compatibility, 7 | # because Python <3.10 doesn't support TypeAlias, jinja2 may not be available 8 | # at runtime, and geventwebsocket.websocket doesn't have type annotations so 9 | # that direct imports will raise an error. 10 | if TYPE_CHECKING: 11 | from jinja2 import Environment 12 | JinjaEnvironmentT: TypeAlias = Environment 13 | from geventwebsocket.websocket import WebSocket 14 | WebSocketT: TypeAlias = WebSocket 15 | else: 16 | JinjaEnvironmentT: TypeAlias = Any 17 | WebSocketT: TypeAlias = Any 18 | 19 | OptionsDictT = TypedDict( 20 | 'OptionsDictT', 21 | { 22 | 'mode': Optional[Union[str, Literal[False]]], 23 | 'host': str, 24 | 'port': int, 25 | 'block': bool, 26 | 'jinja_templates': Optional[str], 27 | 'cmdline_args': List[str], 28 | 'size': Optional[Tuple[int, int]], 29 | 'position': Optional[Tuple[int, int]], 30 | 'geometry': Dict[str, Tuple[int, int]], 31 | 'close_callback': Optional[Callable[..., Any]], 32 | 'app_mode': bool, 33 | 'all_interfaces': bool, 34 | 'disable_cache': bool, 35 | 'default_path': str, 36 | 'app': Bottle, 37 | 'shutdown_delay': float, 38 | 'suppress_error': bool, 39 | 'jinja_env': JinjaEnvironmentT, 40 | }, 41 | total=False 42 | ) 43 | -------------------------------------------------------------------------------- /examples/01 - hello_world-Edge/hello.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import sys 4 | 5 | # Use latest version of Eel from parent directory 6 | sys.path.insert(1, '../../') 7 | import eel 8 | 9 | # Use the same static files as the original Example 10 | os.chdir(os.path.join('..', '01 - hello_world')) 11 | 12 | # Set web files folder and optionally specify which file types to check for eel.expose() 13 | eel.init('web', allowed_extensions=['.js', '.html']) 14 | 15 | 16 | @eel.expose # Expose this function to Javascript 17 | def say_hello_py(x): 18 | print('Hello from %s' % x) 19 | 20 | 21 | say_hello_py('Python World!') 22 | eel.say_hello_js('Python World!') # Call a Javascript function 23 | 24 | # Launch example in Microsoft Edge only on Windows 10 and above 25 | if sys.platform in ['win32', 'win64'] and int(platform.release()) >= 10: 26 | eel.start('hello.html', mode='edge') 27 | else: 28 | raise EnvironmentError('Error: System is not Windows 10 or above') 29 | 30 | # # Launching Edge can also be gracefully handled as a fall back 31 | # try: 32 | # eel.start('hello.html', mode='chrome-app', size=(300, 200)) 33 | # except EnvironmentError: 34 | # # If Chrome isn't found, fallback to Microsoft Edge on Win10 or greater 35 | # if sys.platform in ['win32', 'win64'] and int(platform.release()) >= 10: 36 | # eel.start('hello.html', mode='edge') 37 | # else: 38 | # raise 39 | -------------------------------------------------------------------------------- /examples/01 - hello_world/hello.py: -------------------------------------------------------------------------------- 1 | import eel 2 | 3 | # Set web files folder 4 | eel.init('web') 5 | 6 | @eel.expose # Expose this function to Javascript 7 | def say_hello_py(x): 8 | print('Hello from %s' % x) 9 | 10 | say_hello_py('Python World!') 11 | eel.say_hello_js('Python World!') # Call a Javascript function 12 | 13 | eel.start('hello.html', size=(300, 200)) # Start 14 | -------------------------------------------------------------------------------- /examples/01 - hello_world/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/01 - hello_world/web/favicon.ico -------------------------------------------------------------------------------- /examples/01 - hello_world/web/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello, World! 5 | 6 | 7 | 8 | 19 | 20 | 21 | 22 | Hello, World! 23 | 24 | -------------------------------------------------------------------------------- /examples/02 - callbacks/callbacks.py: -------------------------------------------------------------------------------- 1 | import eel 2 | import random 3 | 4 | eel.init('web') 5 | 6 | @eel.expose 7 | def py_random(): 8 | return random.random() 9 | 10 | @eel.expose 11 | def py_exception(error): 12 | if error: 13 | raise ValueError("Test") 14 | else: 15 | return "No Error" 16 | 17 | def print_num(n): 18 | print('Got this from Javascript:', n) 19 | 20 | 21 | def print_num_failed(error, stack): 22 | print("This is an example of what javascript errors would look like:") 23 | print("\tError: ", error) 24 | print("\tStack: ", stack) 25 | 26 | # Call Javascript function, and pass explicit callback function 27 | eel.js_random()(print_num) 28 | 29 | # Do the same with an inline callback 30 | eel.js_random()(lambda n: print('Got this from Javascript:', n)) 31 | 32 | # Show error handling 33 | eel.js_with_error()(print_num, print_num_failed) 34 | 35 | 36 | eel.start('callbacks.html', size=(400, 300)) 37 | 38 | -------------------------------------------------------------------------------- /examples/02 - callbacks/web/callbacks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Callbacks Demo 5 | 6 | 7 | 49 | 50 | 51 | 52 | Callbacks demo 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/02 - callbacks/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/02 - callbacks/web/favicon.ico -------------------------------------------------------------------------------- /examples/03 - sync_callbacks/sync_callbacks.py: -------------------------------------------------------------------------------- 1 | import eel, random 2 | 3 | eel.init('web') 4 | 5 | @eel.expose 6 | def py_random(): 7 | return random.random() 8 | 9 | eel.start('sync_callbacks.html', block=False, size=(400, 300)) 10 | 11 | # Synchronous calls must happen after start() is called 12 | 13 | # Get result returned synchronously by 14 | # passing nothing in second brackets 15 | # v 16 | n = eel.js_random()() 17 | print('Got this from Javascript:', n) 18 | 19 | while True: 20 | eel.sleep(1.0) 21 | -------------------------------------------------------------------------------- /examples/03 - sync_callbacks/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/03 - sync_callbacks/web/favicon.ico -------------------------------------------------------------------------------- /examples/03 - sync_callbacks/web/sync_callbacks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Synchronous callbacks 5 | 6 | 7 | 27 | 28 | 29 | 30 | Synchronous callbacks 31 | 32 | -------------------------------------------------------------------------------- /examples/04 - file_access/README.md: -------------------------------------------------------------------------------- 1 | # Example 4 - file access 2 | 3 | ![Screenshot](Screenshot.png) 4 | -------------------------------------------------------------------------------- /examples/04 - file_access/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/04 - file_access/Screenshot.png -------------------------------------------------------------------------------- /examples/04 - file_access/file_access.py: -------------------------------------------------------------------------------- 1 | import eel, os, random 2 | 3 | eel.init('web') 4 | 5 | @eel.expose 6 | def pick_file(folder): 7 | if os.path.isdir(folder): 8 | return random.choice(os.listdir(folder)) 9 | else: 10 | return 'Not valid folder' 11 | 12 | eel.start('file_access.html', size=(320, 120)) 13 | -------------------------------------------------------------------------------- /examples/04 - file_access/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/04 - file_access/web/favicon.ico -------------------------------------------------------------------------------- /examples/04 - file_access/web/file_access.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Eel Demo 5 | 6 | 18 | 19 | 20 | 21 |
22 | 23 | 24 |
25 |
---
26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/05 - input/script.py: -------------------------------------------------------------------------------- 1 | import eel 2 | 3 | eel.init('web') # Give folder containing web files 4 | 5 | @eel.expose # Expose this function to Javascript 6 | def handleinput(x): 7 | print('%s' % x) 8 | 9 | eel.say_hello_js('connected!') # Call a Javascript function 10 | 11 | eel.start('main.html', size=(500, 200)) # Start 12 | -------------------------------------------------------------------------------- /examples/05 - input/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/05 - input/web/favicon.ico -------------------------------------------------------------------------------- /examples/05 - input/web/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 24 | 25 | 26 | 27 |

Input Example: Enter a value and check python console

28 |
29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/06 - jinja_templates/hello.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import eel 4 | 5 | eel.init('web') # Give folder containing web files 6 | 7 | @eel.expose 8 | def py_random(): 9 | return random.random() 10 | 11 | @eel.expose # Expose this function to Javascript 12 | def say_hello_py(x): 13 | print('Hello from %s' % x) 14 | 15 | say_hello_py('Python World!') 16 | eel.say_hello_js('Python World!') # Call a Javascript function 17 | 18 | eel.start('templates/hello.html', size=(300, 200), jinja_templates='templates') # Start 19 | -------------------------------------------------------------------------------- /examples/06 - jinja_templates/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/06 - jinja_templates/web/favicon.ico -------------------------------------------------------------------------------- /examples/06 - jinja_templates/web/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock %} 5 | 6 | 7 | 10 | 11 | 12 | {% block content %}{% endblock %} 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/06 - jinja_templates/web/templates/hello.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Hello, World!{% endblock %} 3 | {% block head_scripts %} 4 | eel.expose(say_hello_js); // Expose this function to Python 5 | function say_hello_js(x) { 6 | console.log("Hello from " + x); 7 | } 8 | 9 | eel.expose(js_random); 10 | function js_random() { 11 | return Math.random(); 12 | } 13 | 14 | function print_num(n) { 15 | console.log('Got this from Python: ' + n); 16 | } 17 | 18 | eel.py_random()(print_num); 19 | 20 | say_hello_js("Javascript World!"); 21 | eel.say_hello_py("Javascript World!"); // Call a Python function 22 | {% endblock %} 23 | {% block content %} 24 | Hello, World! 25 |
26 | Page 2 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /examples/06 - jinja_templates/web/templates/page2.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Hello, World!{% endblock %} 3 | {% block content %} 4 |

This is page 2

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | *.spec 26 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/Demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/07 - CreateReactApp/Demo.png -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/README.md: -------------------------------------------------------------------------------- 1 | > "Eello World example": Create-React-App (CRA) and Eel 2 | 3 | **Table of Contents** 4 | 5 | 6 | 7 | - [07 - CreateReactApp Documentation](#07---createreactapp-documentation) 8 | - [Quick Start](#quick-start) 9 | - [About](#about) 10 | - [Main Files](#main-files) 11 | 12 | 13 | 14 | # 07 - CreateReactApp Documentation 15 | 16 | Eello World example Create-React-App (CRA) with Eel. This particular project was bootstrapped with `npx create-react-app 07_CreateReactApp --typescript` (Typescript enabled), but the below modifications can be implemented in any CRA configuration or CRA version. 17 | 18 | If you run into any issues with this example, open a [new issue](https://github.com/ChrisKnott/Eel/issues/new) and tag @KyleKing 19 | 20 | ## Quick Start 21 | 22 | 1. **Configure:** In the app's directory, run `npm install` and `pip install bottle bottle-websocket future pyinstaller` 23 | 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/` 24 | 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)) 25 | 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. 26 | 27 | ![Demo.png](Demo.png) 28 | 29 | ## About 30 | 31 | > Use `window.eel.expose(func, 'func')` to circumvent `npm run build` code mangling 32 | 33 | `npm run build` will rename variables and functions to minimize file size renaming `eel.expose(funcName)` to something like `D.expose(J)`. The renaming breaks Eel's static JS-code analyzer, which uses a regular expression to look for `eel.expose(*)`. To fix this issue, in your JS code, convert all `eel.expose(funcName)` to `window.eel(funcName, 'funcName')`. This workaround guarantees that 'funcName' will be available to call from Python. 34 | 35 | ## Main Files 36 | 37 | Critical files for this demo 38 | 39 | - `src/App.tsx`: Modified to demonstrate exposing a function from JavaScript and how to use callbacks from Python to update React GUI 40 | - `eel_CRA.py`: Basic `eel` file 41 | - If run without arguments, the `eel` script will load `index.html` from the build/ directory (which is ideal for building with PyInstaller/distribution) 42 | - If any 2nd argument (i.e. `true`) is provided, the app enables a "development" mode and attempts to connect to the React server on port 3000 43 | - `public/index.html`: Added location of `eel.js` file based on options set in eel_CRA.py 44 | 45 | ```html 46 | 47 | 48 | ``` 49 | 50 | - `src/react-app-env.d.ts`: This file declares window.eel as a valid type for tslint. Note: capitalization of `window` 51 | - `src/App.css`: Added some basic button styling 52 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/eel_CRA.py: -------------------------------------------------------------------------------- 1 | """Main Python application file for the EEL-CRA demo.""" 2 | 3 | import os 4 | import platform 5 | import random 6 | import sys 7 | 8 | import eel 9 | 10 | # Use latest version of Eel from parent directory 11 | sys.path.insert(1, '../../') 12 | 13 | 14 | @eel.expose # Expose function to JavaScript 15 | def say_hello_py(x): 16 | """Print message from JavaScript on app initialization, then call a JS function.""" 17 | print('Hello from %s' % x) # noqa T001 18 | eel.say_hello_js('Python {from within say_hello_py()}!') 19 | 20 | 21 | @eel.expose 22 | def expand_user(folder): 23 | """Return the full path to display in the UI.""" 24 | return '{}/*'.format(os.path.expanduser(folder)) 25 | 26 | 27 | @eel.expose 28 | def pick_file(folder): 29 | """Return a random file from the specified folder.""" 30 | folder = os.path.expanduser(folder) 31 | if os.path.isdir(folder): 32 | listFiles = [_f for _f in os.listdir(folder) if not os.path.isdir(os.path.join(folder, _f))] 33 | if len(listFiles) == 0: 34 | return 'No Files found in {}'.format(folder) 35 | return random.choice(listFiles) 36 | else: 37 | return '{} is not a valid folder'.format(folder) 38 | 39 | 40 | def start_eel(develop): 41 | """Start Eel with either production or development configuration.""" 42 | 43 | if develop: 44 | directory = 'src' 45 | app = None 46 | page = {'port': 3000} 47 | else: 48 | directory = 'build' 49 | app = 'chrome-app' 50 | page = 'index.html' 51 | 52 | eel.init(directory, ['.tsx', '.ts', '.jsx', '.js', '.html']) 53 | 54 | # These will be queued until the first connection is made, but won't be repeated on a page reload 55 | say_hello_py('Python World!') 56 | eel.say_hello_js('Python World!') # Call a JavaScript function (must be after `eel.init()`) 57 | 58 | eel.show_log('https://github.com/samuelhwilliams/Eel/issues/363 (show_log)') 59 | 60 | eel_kwargs = dict( 61 | host='localhost', 62 | port=8080, 63 | size=(1280, 800), 64 | ) 65 | try: 66 | eel.start(page, mode=app, **eel_kwargs) 67 | except EnvironmentError: 68 | # If Chrome isn't found, fallback to Microsoft Edge on Win10 or greater 69 | if sys.platform in ['win32', 'win64'] and int(platform.release()) >= 10: 70 | eel.start(page, mode='edge', **eel_kwargs) 71 | else: 72 | raise 73 | 74 | 75 | if __name__ == '__main__': 76 | import sys 77 | 78 | # Pass any second argument to enable debugging 79 | start_eel(develop=len(sys.argv) == 2) 80 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "07___create-react-app", 3 | "version": "0.1.1", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "24.0.14", 7 | "@types/node": "12.0.8", 8 | "@types/react": "16.8.20", 9 | "@types/react-dom": "16.8.4", 10 | "react": "^16.8.6", 11 | "react-dom": "^16.8.6", 12 | "react-scripts": "^3.4.1", 13 | "typescript": "3.4.5" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/07 - CreateReactApp/public/favicon.ico -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-button { 22 | background-color: #61dafb; 23 | border-radius: 2vmin; 24 | color: #282c34; 25 | font-size: calc(10px + 2vmin); 26 | padding: 2vmin; 27 | } 28 | 29 | 30 | .App-button:hover { 31 | background-color: #7ce3ff; 32 | cursor: pointer; 33 | } 34 | 35 | @keyframes App-logo-spin { 36 | from { 37 | transform: rotate(0deg); 38 | } 39 | to { 40 | transform: rotate(360deg); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | // Point Eel web socket to the instance 6 | export const eel = window.eel 7 | eel.set_host( 'ws://localhost:8080' ) 8 | 9 | // Expose the `sayHelloJS` function to Python as `say_hello_js` 10 | function sayHelloJS( x: any ) { 11 | console.log( 'Hello from ' + x ) 12 | } 13 | // WARN: must use window.eel to keep parse-able eel.expose{...} 14 | window.eel.expose( sayHelloJS, 'say_hello_js' ) 15 | 16 | // Test anonymous function when minimized. See https://github.com/samuelhwilliams/Eel/issues/363 17 | function show_log(msg:string) { 18 | console.log(msg) 19 | } 20 | window.eel.expose(show_log, 'show_log') 21 | 22 | // Test calling sayHelloJS, then call the corresponding Python function 23 | sayHelloJS( 'Javascript World!' ) 24 | eel.say_hello_py( 'Javascript World!' ) 25 | 26 | // Set the default path. Would be a text input, but this is a basic example after all 27 | const defPath = '~' 28 | 29 | interface IAppState { 30 | message: string 31 | path: string 32 | } 33 | 34 | export class App extends Component<{}, {}> { 35 | public state: IAppState = { 36 | message: `Click button to choose a random file from the user's system`, 37 | path: defPath, 38 | } 39 | 40 | public pickFile = () => { 41 | eel.pick_file(defPath)(( message: string ) => this.setState( { message } ) ) 42 | } 43 | 44 | public render() { 45 | eel.expand_user(defPath)(( path: string ) => this.setState( { path } ) ) 46 | return ( 47 |
48 |
49 | logo 50 |

{this.state.message}

51 | 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | export default App; 59 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface Window { 4 | eel: any; 5 | } 6 | 7 | declare var window: Window; 8 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /examples/07 - CreateReactApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/08 - disable_cache/disable_cache.py: -------------------------------------------------------------------------------- 1 | import eel 2 | 3 | # Set web files folder and optionally specify which file types to check for eel.expose() 4 | eel.init('web') 5 | 6 | # disable_cache now defaults to True so this isn't strictly necessary. Set it to False to enable caching. 7 | eel.start('disable_cache.html', size=(300, 200), disable_cache=True) # Start 8 | -------------------------------------------------------------------------------- /examples/08 - disable_cache/web/disable_cache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello, World! 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Cache Proof! 13 | 14 | -------------------------------------------------------------------------------- /examples/08 - disable_cache/web/dont_cache_me.js: -------------------------------------------------------------------------------- 1 | console.log("Check the network activity to see that this file isn't getting cached."); 2 | -------------------------------------------------------------------------------- /examples/08 - disable_cache/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/08 - disable_cache/web/favicon.ico -------------------------------------------------------------------------------- /examples/09 - Eelectron-quick-start/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/09 - Eelectron-quick-start/hello.py: -------------------------------------------------------------------------------- 1 | import eel 2 | # Set web files folder 3 | eel.init('web') 4 | 5 | @eel.expose # Expose this function to Javascript 6 | def say_hello_py(x): 7 | print('Hello from %s' % x) 8 | 9 | say_hello_py('Python World!') 10 | eel.say_hello_js('Python World!') # Call a Javascript function 11 | 12 | eel.start('hello.html',mode='electron') 13 | #eel.start('hello.html', mode='custom', cmdline_args=['node_modules/electron/dist/electron.exe', '.']) 14 | -------------------------------------------------------------------------------- /examples/09 - Eelectron-quick-start/main.js: -------------------------------------------------------------------------------- 1 | // Modules to control application life and create native browser window 2 | const {app, BrowserWindow} = require('electron') 3 | 4 | // Keep a global reference of the window object, if you don't, the window will 5 | // be closed automatically when the JavaScript object is garbage collected. 6 | let mainWindow 7 | 8 | function createWindow () { 9 | // Create the browser window. 10 | mainWindow = new BrowserWindow({ 11 | width: 800, 12 | height: 600, 13 | webPreferences: { 14 | nodeIntegration: true 15 | } 16 | }) 17 | 18 | // and load the index.html of the app. 19 | mainWindow.loadURL('http://localhost:8000/hello.html'); 20 | 21 | // Open the DevTools. 22 | // mainWindow.webContents.openDevTools() 23 | 24 | // Emitted when the window is closed. 25 | mainWindow.on('closed', function () { 26 | // Dereference the window object, usually you would store windows 27 | // in an array if your app supports multi windows, this is the time 28 | // when you should delete the corresponding element. 29 | mainWindow = null 30 | }) 31 | } 32 | 33 | // This method will be called when Electron has finished 34 | // initialization and is ready to create browser windows. 35 | // Some APIs can only be used after this event occurs. 36 | app.on('ready', createWindow) 37 | 38 | // Quit when all windows are closed. 39 | app.on('window-all-closed', function () { 40 | // On macOS it is common for applications and their menu bar 41 | // to stay active until the user quits explicitly with Cmd + Q 42 | if (process.platform !== 'darwin') app.quit() 43 | }) 44 | 45 | app.on('activate', function () { 46 | // On macOS it's common to re-create a window in the app when the 47 | // dock icon is clicked and there are no other windows open. 48 | if (mainWindow === null) createWindow() 49 | }) 50 | 51 | // In this file you can include the rest of your app's specific main process 52 | // code. You can also put them in separate files and require them here. 53 | -------------------------------------------------------------------------------- /examples/09 - Eelectron-quick-start/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Eelectron-quick-start", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@electron/get": { 8 | "version": "1.12.2", 9 | "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.12.2.tgz", 10 | "integrity": "sha512-vAuHUbfvBQpYTJ5wB7uVIDq5c/Ry0fiTBMs7lnEYAo/qXXppIVcWdfBr57u6eRnKdVso7KSiH6p/LbQAG6Izrg==", 11 | "dev": true, 12 | "requires": { 13 | "debug": "^4.1.1", 14 | "env-paths": "^2.2.0", 15 | "fs-extra": "^8.1.0", 16 | "global-agent": "^2.0.2", 17 | "global-tunnel-ng": "^2.7.1", 18 | "got": "^9.6.0", 19 | "progress": "^2.0.3", 20 | "sanitize-filename": "^1.6.2", 21 | "sumchecker": "^3.0.1" 22 | } 23 | }, 24 | "@sindresorhus/is": { 25 | "version": "0.14.0", 26 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", 27 | "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", 28 | "dev": true 29 | }, 30 | "@szmarczak/http-timer": { 31 | "version": "1.1.2", 32 | "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", 33 | "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", 34 | "dev": true, 35 | "requires": { 36 | "defer-to-connect": "^1.0.1" 37 | } 38 | }, 39 | "@types/node": { 40 | "version": "12.12.48", 41 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.48.tgz", 42 | "integrity": "sha512-m3Nmo/YaDUfYzdCQlxjF5pIy7TNyDTAJhIa//xtHcF0dlgYIBKULKnmloCPtByDxtZXrWV8Pge1AKT6/lRvVWg==", 43 | "dev": true 44 | }, 45 | "boolean": { 46 | "version": "3.0.1", 47 | "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz", 48 | "integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==", 49 | "dev": true, 50 | "optional": true 51 | }, 52 | "buffer-crc32": { 53 | "version": "0.2.13", 54 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 55 | "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", 56 | "dev": true 57 | }, 58 | "buffer-from": { 59 | "version": "1.1.1", 60 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 61 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 62 | "dev": true 63 | }, 64 | "cacheable-request": { 65 | "version": "6.1.0", 66 | "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", 67 | "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", 68 | "dev": true, 69 | "requires": { 70 | "clone-response": "^1.0.2", 71 | "get-stream": "^5.1.0", 72 | "http-cache-semantics": "^4.0.0", 73 | "keyv": "^3.0.0", 74 | "lowercase-keys": "^2.0.0", 75 | "normalize-url": "^4.1.0", 76 | "responselike": "^1.0.2" 77 | }, 78 | "dependencies": { 79 | "get-stream": { 80 | "version": "5.1.0", 81 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", 82 | "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", 83 | "dev": true, 84 | "requires": { 85 | "pump": "^3.0.0" 86 | } 87 | }, 88 | "lowercase-keys": { 89 | "version": "2.0.0", 90 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", 91 | "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", 92 | "dev": true 93 | } 94 | } 95 | }, 96 | "clone-response": { 97 | "version": "1.0.2", 98 | "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", 99 | "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", 100 | "dev": true, 101 | "requires": { 102 | "mimic-response": "^1.0.0" 103 | } 104 | }, 105 | "concat-stream": { 106 | "version": "1.6.2", 107 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 108 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 109 | "dev": true, 110 | "requires": { 111 | "buffer-from": "^1.0.0", 112 | "inherits": "^2.0.3", 113 | "readable-stream": "^2.2.2", 114 | "typedarray": "^0.0.6" 115 | } 116 | }, 117 | "config-chain": { 118 | "version": "1.1.12", 119 | "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", 120 | "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", 121 | "dev": true, 122 | "optional": true, 123 | "requires": { 124 | "ini": "^1.3.4", 125 | "proto-list": "~1.2.1" 126 | } 127 | }, 128 | "core-js": { 129 | "version": "3.6.5", 130 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", 131 | "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", 132 | "dev": true, 133 | "optional": true 134 | }, 135 | "core-util-is": { 136 | "version": "1.0.2", 137 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 138 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 139 | "dev": true 140 | }, 141 | "debug": { 142 | "version": "4.1.1", 143 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 144 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 145 | "dev": true, 146 | "requires": { 147 | "ms": "^2.1.1" 148 | } 149 | }, 150 | "decompress-response": { 151 | "version": "3.3.0", 152 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", 153 | "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", 154 | "dev": true, 155 | "requires": { 156 | "mimic-response": "^1.0.0" 157 | } 158 | }, 159 | "defer-to-connect": { 160 | "version": "1.1.3", 161 | "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", 162 | "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", 163 | "dev": true 164 | }, 165 | "define-properties": { 166 | "version": "1.1.3", 167 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 168 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 169 | "dev": true, 170 | "optional": true, 171 | "requires": { 172 | "object-keys": "^1.0.12" 173 | } 174 | }, 175 | "detect-node": { 176 | "version": "2.0.4", 177 | "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", 178 | "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", 179 | "dev": true, 180 | "optional": true 181 | }, 182 | "duplexer3": { 183 | "version": "0.1.4", 184 | "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", 185 | "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", 186 | "dev": true 187 | }, 188 | "electron": { 189 | "version": "7.2.4", 190 | "resolved": "https://registry.npmjs.org/electron/-/electron-7.2.4.tgz", 191 | "integrity": "sha512-Z+R692uTzXgP8AHrabE+kkrMlQJ6pnAYoINenwj9QSqaD2YbO8IuXU9DMCcUY0+VpA91ee09wFZJNUKYPMnCKg==", 192 | "dev": true, 193 | "requires": { 194 | "@electron/get": "^1.0.1", 195 | "@types/node": "^12.0.12", 196 | "extract-zip": "^1.0.3" 197 | } 198 | }, 199 | "encodeurl": { 200 | "version": "1.0.2", 201 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 202 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", 203 | "dev": true, 204 | "optional": true 205 | }, 206 | "end-of-stream": { 207 | "version": "1.4.4", 208 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 209 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 210 | "dev": true, 211 | "requires": { 212 | "once": "^1.4.0" 213 | } 214 | }, 215 | "env-paths": { 216 | "version": "2.2.0", 217 | "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", 218 | "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", 219 | "dev": true 220 | }, 221 | "es6-error": { 222 | "version": "4.1.1", 223 | "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", 224 | "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", 225 | "dev": true, 226 | "optional": true 227 | }, 228 | "escape-string-regexp": { 229 | "version": "4.0.0", 230 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 231 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 232 | "dev": true, 233 | "optional": true 234 | }, 235 | "extract-zip": { 236 | "version": "1.7.0", 237 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", 238 | "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", 239 | "dev": true, 240 | "requires": { 241 | "concat-stream": "^1.6.2", 242 | "debug": "^2.6.9", 243 | "mkdirp": "^0.5.4", 244 | "yauzl": "^2.10.0" 245 | }, 246 | "dependencies": { 247 | "debug": { 248 | "version": "2.6.9", 249 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 250 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 251 | "dev": true, 252 | "requires": { 253 | "ms": "2.0.0" 254 | } 255 | }, 256 | "ms": { 257 | "version": "2.0.0", 258 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 259 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 260 | "dev": true 261 | } 262 | } 263 | }, 264 | "fd-slicer": { 265 | "version": "1.1.0", 266 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 267 | "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", 268 | "dev": true, 269 | "requires": { 270 | "pend": "~1.2.0" 271 | } 272 | }, 273 | "fs-extra": { 274 | "version": "8.1.0", 275 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", 276 | "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", 277 | "dev": true, 278 | "requires": { 279 | "graceful-fs": "^4.2.0", 280 | "jsonfile": "^4.0.0", 281 | "universalify": "^0.1.0" 282 | } 283 | }, 284 | "get-stream": { 285 | "version": "4.1.0", 286 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", 287 | "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", 288 | "dev": true, 289 | "requires": { 290 | "pump": "^3.0.0" 291 | } 292 | }, 293 | "global-agent": { 294 | "version": "2.1.12", 295 | "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.12.tgz", 296 | "integrity": "sha512-caAljRMS/qcDo69X9BfkgrihGUgGx44Fb4QQToNQjsiWh+YlQ66uqYVAdA8Olqit+5Ng0nkz09je3ZzANMZcjg==", 297 | "dev": true, 298 | "optional": true, 299 | "requires": { 300 | "boolean": "^3.0.1", 301 | "core-js": "^3.6.5", 302 | "es6-error": "^4.1.1", 303 | "matcher": "^3.0.0", 304 | "roarr": "^2.15.3", 305 | "semver": "^7.3.2", 306 | "serialize-error": "^7.0.1" 307 | } 308 | }, 309 | "global-tunnel-ng": { 310 | "version": "2.7.1", 311 | "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", 312 | "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", 313 | "dev": true, 314 | "optional": true, 315 | "requires": { 316 | "encodeurl": "^1.0.2", 317 | "lodash": "^4.17.10", 318 | "npm-conf": "^1.1.3", 319 | "tunnel": "^0.0.6" 320 | } 321 | }, 322 | "globalthis": { 323 | "version": "1.0.1", 324 | "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.1.tgz", 325 | "integrity": "sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==", 326 | "dev": true, 327 | "optional": true, 328 | "requires": { 329 | "define-properties": "^1.1.3" 330 | } 331 | }, 332 | "got": { 333 | "version": "9.6.0", 334 | "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", 335 | "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", 336 | "dev": true, 337 | "requires": { 338 | "@sindresorhus/is": "^0.14.0", 339 | "@szmarczak/http-timer": "^1.1.2", 340 | "cacheable-request": "^6.0.0", 341 | "decompress-response": "^3.3.0", 342 | "duplexer3": "^0.1.4", 343 | "get-stream": "^4.1.0", 344 | "lowercase-keys": "^1.0.1", 345 | "mimic-response": "^1.0.1", 346 | "p-cancelable": "^1.0.0", 347 | "to-readable-stream": "^1.0.0", 348 | "url-parse-lax": "^3.0.0" 349 | } 350 | }, 351 | "graceful-fs": { 352 | "version": "4.2.4", 353 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", 354 | "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", 355 | "dev": true 356 | }, 357 | "http-cache-semantics": { 358 | "version": "4.1.0", 359 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", 360 | "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", 361 | "dev": true 362 | }, 363 | "inherits": { 364 | "version": "2.0.4", 365 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 366 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 367 | "dev": true 368 | }, 369 | "ini": { 370 | "version": "1.3.5", 371 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 372 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", 373 | "dev": true, 374 | "optional": true 375 | }, 376 | "isarray": { 377 | "version": "1.0.0", 378 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 379 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 380 | "dev": true 381 | }, 382 | "json-buffer": { 383 | "version": "3.0.0", 384 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", 385 | "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", 386 | "dev": true 387 | }, 388 | "json-stringify-safe": { 389 | "version": "5.0.1", 390 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 391 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 392 | "dev": true, 393 | "optional": true 394 | }, 395 | "jsonfile": { 396 | "version": "4.0.0", 397 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 398 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 399 | "dev": true, 400 | "requires": { 401 | "graceful-fs": "^4.1.6" 402 | } 403 | }, 404 | "keyv": { 405 | "version": "3.1.0", 406 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", 407 | "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", 408 | "dev": true, 409 | "requires": { 410 | "json-buffer": "3.0.0" 411 | } 412 | }, 413 | "lodash": { 414 | "version": "4.17.19", 415 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", 416 | "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", 417 | "dev": true, 418 | "optional": true 419 | }, 420 | "lowercase-keys": { 421 | "version": "1.0.1", 422 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", 423 | "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", 424 | "dev": true 425 | }, 426 | "matcher": { 427 | "version": "3.0.0", 428 | "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", 429 | "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", 430 | "dev": true, 431 | "optional": true, 432 | "requires": { 433 | "escape-string-regexp": "^4.0.0" 434 | } 435 | }, 436 | "mimic-response": { 437 | "version": "1.0.1", 438 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", 439 | "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", 440 | "dev": true 441 | }, 442 | "minimist": { 443 | "version": "1.2.5", 444 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 445 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 446 | "dev": true 447 | }, 448 | "mkdirp": { 449 | "version": "0.5.5", 450 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 451 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 452 | "dev": true, 453 | "requires": { 454 | "minimist": "^1.2.5" 455 | } 456 | }, 457 | "ms": { 458 | "version": "2.1.2", 459 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 460 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 461 | "dev": true 462 | }, 463 | "normalize-url": { 464 | "version": "4.5.0", 465 | "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", 466 | "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", 467 | "dev": true 468 | }, 469 | "npm-conf": { 470 | "version": "1.1.3", 471 | "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", 472 | "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", 473 | "dev": true, 474 | "optional": true, 475 | "requires": { 476 | "config-chain": "^1.1.11", 477 | "pify": "^3.0.0" 478 | } 479 | }, 480 | "object-keys": { 481 | "version": "1.1.1", 482 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 483 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 484 | "dev": true, 485 | "optional": true 486 | }, 487 | "once": { 488 | "version": "1.4.0", 489 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 490 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 491 | "dev": true, 492 | "requires": { 493 | "wrappy": "1" 494 | } 495 | }, 496 | "p-cancelable": { 497 | "version": "1.1.0", 498 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", 499 | "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", 500 | "dev": true 501 | }, 502 | "pend": { 503 | "version": "1.2.0", 504 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 505 | "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", 506 | "dev": true 507 | }, 508 | "pify": { 509 | "version": "3.0.0", 510 | "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", 511 | "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", 512 | "dev": true, 513 | "optional": true 514 | }, 515 | "prepend-http": { 516 | "version": "2.0.0", 517 | "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", 518 | "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", 519 | "dev": true 520 | }, 521 | "process-nextick-args": { 522 | "version": "2.0.1", 523 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 524 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 525 | "dev": true 526 | }, 527 | "progress": { 528 | "version": "2.0.3", 529 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 530 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 531 | "dev": true 532 | }, 533 | "proto-list": { 534 | "version": "1.2.4", 535 | "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", 536 | "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", 537 | "dev": true, 538 | "optional": true 539 | }, 540 | "pump": { 541 | "version": "3.0.0", 542 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 543 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 544 | "dev": true, 545 | "requires": { 546 | "end-of-stream": "^1.1.0", 547 | "once": "^1.3.1" 548 | } 549 | }, 550 | "readable-stream": { 551 | "version": "2.3.7", 552 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 553 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 554 | "dev": true, 555 | "requires": { 556 | "core-util-is": "~1.0.0", 557 | "inherits": "~2.0.3", 558 | "isarray": "~1.0.0", 559 | "process-nextick-args": "~2.0.0", 560 | "safe-buffer": "~5.1.1", 561 | "string_decoder": "~1.1.1", 562 | "util-deprecate": "~1.0.1" 563 | } 564 | }, 565 | "responselike": { 566 | "version": "1.0.2", 567 | "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", 568 | "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", 569 | "dev": true, 570 | "requires": { 571 | "lowercase-keys": "^1.0.0" 572 | } 573 | }, 574 | "roarr": { 575 | "version": "2.15.3", 576 | "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.3.tgz", 577 | "integrity": "sha512-AEjYvmAhlyxOeB9OqPUzQCo3kuAkNfuDk/HqWbZdFsqDFpapkTjiw+p4svNEoRLvuqNTxqfL+s+gtD4eDgZ+CA==", 578 | "dev": true, 579 | "optional": true, 580 | "requires": { 581 | "boolean": "^3.0.0", 582 | "detect-node": "^2.0.4", 583 | "globalthis": "^1.0.1", 584 | "json-stringify-safe": "^5.0.1", 585 | "semver-compare": "^1.0.0", 586 | "sprintf-js": "^1.1.2" 587 | } 588 | }, 589 | "safe-buffer": { 590 | "version": "5.1.2", 591 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 592 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 593 | "dev": true 594 | }, 595 | "sanitize-filename": { 596 | "version": "1.6.3", 597 | "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", 598 | "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", 599 | "dev": true, 600 | "requires": { 601 | "truncate-utf8-bytes": "^1.0.0" 602 | } 603 | }, 604 | "semver": { 605 | "version": "7.3.2", 606 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", 607 | "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", 608 | "dev": true, 609 | "optional": true 610 | }, 611 | "semver-compare": { 612 | "version": "1.0.0", 613 | "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", 614 | "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", 615 | "dev": true, 616 | "optional": true 617 | }, 618 | "serialize-error": { 619 | "version": "7.0.1", 620 | "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", 621 | "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", 622 | "dev": true, 623 | "optional": true, 624 | "requires": { 625 | "type-fest": "^0.13.1" 626 | } 627 | }, 628 | "sprintf-js": { 629 | "version": "1.1.2", 630 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", 631 | "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", 632 | "dev": true, 633 | "optional": true 634 | }, 635 | "string_decoder": { 636 | "version": "1.1.1", 637 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 638 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 639 | "dev": true, 640 | "requires": { 641 | "safe-buffer": "~5.1.0" 642 | } 643 | }, 644 | "sumchecker": { 645 | "version": "3.0.1", 646 | "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", 647 | "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", 648 | "dev": true, 649 | "requires": { 650 | "debug": "^4.1.0" 651 | } 652 | }, 653 | "to-readable-stream": { 654 | "version": "1.0.0", 655 | "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", 656 | "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", 657 | "dev": true 658 | }, 659 | "truncate-utf8-bytes": { 660 | "version": "1.0.2", 661 | "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", 662 | "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", 663 | "dev": true, 664 | "requires": { 665 | "utf8-byte-length": "^1.0.1" 666 | } 667 | }, 668 | "tunnel": { 669 | "version": "0.0.6", 670 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 671 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", 672 | "dev": true, 673 | "optional": true 674 | }, 675 | "type-fest": { 676 | "version": "0.13.1", 677 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", 678 | "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", 679 | "dev": true, 680 | "optional": true 681 | }, 682 | "typedarray": { 683 | "version": "0.0.6", 684 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 685 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 686 | "dev": true 687 | }, 688 | "universalify": { 689 | "version": "0.1.2", 690 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 691 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", 692 | "dev": true 693 | }, 694 | "url-parse-lax": { 695 | "version": "3.0.0", 696 | "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", 697 | "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", 698 | "dev": true, 699 | "requires": { 700 | "prepend-http": "^2.0.0" 701 | } 702 | }, 703 | "utf8-byte-length": { 704 | "version": "1.0.4", 705 | "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", 706 | "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=", 707 | "dev": true 708 | }, 709 | "util-deprecate": { 710 | "version": "1.0.2", 711 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 712 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 713 | "dev": true 714 | }, 715 | "wrappy": { 716 | "version": "1.0.2", 717 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 718 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 719 | "dev": true 720 | }, 721 | "yauzl": { 722 | "version": "2.10.0", 723 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 724 | "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", 725 | "dev": true, 726 | "requires": { 727 | "buffer-crc32": "~0.2.3", 728 | "fd-slicer": "~1.1.0" 729 | } 730 | } 731 | } 732 | } 733 | -------------------------------------------------------------------------------- /examples/09 - Eelectron-quick-start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Eelectron-quick-start", 3 | "version": "1.0.0", 4 | "description": "A minimal Eelectron application", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron ." 8 | }, 9 | "repository": "https://github.com/electron/electron-quick-start", 10 | "keywords": [ 11 | "Electron", 12 | "quick", 13 | "start", 14 | "tutorial", 15 | "demo" 16 | ], 17 | "author": "GitHub", 18 | "license": "CC0-1.0", 19 | "devDependencies": { 20 | "electron": "^7.2.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/09 - Eelectron-quick-start/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-eel/Eel/41e2d8a9ac1696def4b4dd2c440fa85538a6d6df/examples/09 - Eelectron-quick-start/web/favicon.ico -------------------------------------------------------------------------------- /examples/09 - Eelectron-quick-start/web/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello, World! 5 | 6 | 7 | 8 | 19 | 20 | 21 | 22 | Hello, World! 23 | 24 | -------------------------------------------------------------------------------- /examples/10 - custom_app_routes/custom_app.py: -------------------------------------------------------------------------------- 1 | import eel 2 | import bottle 3 | # from beaker.middleware import SessionMiddleware 4 | 5 | app = bottle.Bottle() 6 | @app.route('/custom') 7 | def custom_route(): 8 | return 'Hello, World!' 9 | 10 | eel.init('web') 11 | 12 | # need to manually add eel routes if we are wrapping our Bottle instance with middleware 13 | # eel.add_eel_routes(app) 14 | # middleware = SessionMiddleware(app) 15 | # eel.start('index.html', app=middleware) 16 | 17 | eel.start('index.html', app=app) 18 | -------------------------------------------------------------------------------- /examples/10 - custom_app_routes/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello, World! 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.10 3 | warn_unused_configs = True 4 | 5 | [mypy-bottle_websocket] 6 | ignore_missing_imports = True 7 | 8 | [mypy-gevent] 9 | ignore_missing_imports = True 10 | 11 | [mypy-gevent.threading] 12 | ignore_missing_imports = True 13 | 14 | [mypy-geventwebsocket.websocket] 15 | ignore_missing_imports = True 16 | 17 | [mypy-bottle] 18 | ignore_missing_imports = True 19 | 20 | [mypy-bottle.ext] 21 | ignore_missing_imports = True 22 | 23 | [mypy-bottle.ext.websocket] 24 | ignore_missing_imports = True 25 | 26 | [mypy-PyInstaller] 27 | ignore_missing_imports = True 28 | 29 | [mypy-PyInstaller.__main__] 30 | ignore_missing_imports = True 31 | -------------------------------------------------------------------------------- /requirements-meta.txt: -------------------------------------------------------------------------------- 1 | tox>=3.15.2,<4.0.0 2 | tox-pyenv==1.1.0 3 | tox-gh-actions==2.0.0 4 | virtualenv>=16.7.10 5 | setuptools 6 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | .[jinja2] 2 | 3 | psutil>=5.0.0,<6.0.0 4 | pytest>=7.0.0,<8.0.0 5 | pytest-timeout>=2.0.0,<3.0.0 6 | selenium>=4.0.0,<5.0.0 7 | webdriver_manager>=4.0.0,<5.0.0 8 | mypy>=1.0.0,<2.0.0 9 | pyinstaller 10 | types-setuptools 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bottle<1.0.0 2 | bottle-websocket<1.0.0 3 | gevent 4 | gevent-websocket<1.0.0 5 | greenlet>=1.0.0,<2.0.0 6 | pyparsing>=3.0.0,<4.0.0 7 | typing-extensions>=4.3.0 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from io import open 2 | from setuptools import setup 3 | 4 | with open('README.md') as read_me: 5 | long_description = read_me.read() 6 | 7 | setup( 8 | name='Eel', 9 | version='0.18.1', 10 | author='Python Eel Organisation', 11 | author_email='python-eel@protonmail.com', 12 | url='https://github.com/python-eel/Eel', 13 | packages=['eel'], 14 | package_data={ 15 | 'eel': ['eel.js', 'py.typed'], 16 | }, 17 | install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'typing_extensions'], 18 | extras_require={ 19 | "jinja2": ['jinja2>=2.10'] 20 | }, 21 | python_requires='>=3.7', 22 | description='For little HTML GUI applications, with easy Python/JS interop', 23 | long_description=long_description, 24 | long_description_content_type='text/markdown', 25 | keywords=['gui', 'html', 'javascript', 'electron'], 26 | classifiers=[ 27 | 'Development Status :: 3 - Alpha', 28 | 'Natural Language :: English', 29 | 'Operating System :: MacOS', 30 | 'Operating System :: POSIX', 31 | 'Operating System :: Microsoft :: Windows :: Windows 10', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | 'Programming Language :: Python :: Implementation :: CPython', 36 | 'License :: OSI Approved :: MIT License', 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | from unittest import mock 4 | 5 | import pytest 6 | from selenium import webdriver 7 | from selenium.webdriver.chrome.service import Service as ChromeService 8 | from webdriver_manager.chrome import ChromeDriverManager 9 | 10 | 11 | @pytest.fixture 12 | def driver(): 13 | TEST_BROWSER = os.environ.get("TEST_BROWSER", "chrome").lower() 14 | 15 | if TEST_BROWSER == "chrome": 16 | options = webdriver.ChromeOptions() 17 | options.add_argument('--headless=new') 18 | options.set_capability("goog:loggingPrefs", {"browser": "ALL"}) 19 | 20 | if platform.system() == "Windows": 21 | options.binary_location = "C:/Program Files/Google/Chrome/Application/chrome.exe" 22 | 23 | driver = webdriver.Chrome( 24 | service=ChromeService(ChromeDriverManager().install()), 25 | options=options, 26 | ) 27 | 28 | # Firefox doesn't currently supported pulling JavaScript console logs, which we currently scan to affirm that 29 | # JS/Python can communicate in some places. So for now, we can't really use firefox/geckodriver during testing. 30 | # This may be added in the future: https://github.com/mozilla/geckodriver/issues/284 31 | 32 | # elif TEST_BROWSER == "firefox": 33 | # options = webdriver.FirefoxOptions() 34 | # options.headless = True 35 | # capabilities = DesiredCapabilities.FIREFOX 36 | # capabilities['loggingPrefs'] = {"browser": "ALL"} 37 | # 38 | # driver = webdriver.Firefox(options=options, capabilities=capabilities, service_log_path=os.path.devnull) 39 | 40 | else: 41 | raise ValueError(f"Unsupported browser for testing: {TEST_BROWSER}") 42 | 43 | with mock.patch("eel.browsers.open"): 44 | yield driver 45 | -------------------------------------------------------------------------------- /tests/data/init_test/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | // Point Eel web socket to the instance 6 | export const eel = window.eel 7 | eel.set_host( 'ws://localhost:8080' ) 8 | 9 | // Expose the `sayHelloJS` function to Python as `say_hello_js` 10 | function sayHelloJS( x: any ) { 11 | console.log( 'Hello from ' + x ) 12 | } 13 | // WARN: must use window.eel to keep parse-able eel.expose{...} 14 | window.eel.expose( sayHelloJS, 'say_hello_js' ) 15 | 16 | // Test anonymous function when minimized. See https://github.com/samuelhwilliams/Eel/issues/363 17 | function show_log(msg:string) { 18 | console.log(msg) 19 | } 20 | window.eel.expose(show_log, 'show_log') 21 | 22 | // Test calling sayHelloJS, then call the corresponding Python function 23 | sayHelloJS( 'Javascript World!' ) 24 | eel.say_hello_py( 'Javascript World!' ) 25 | 26 | // Set the default path. Would be a text input, but this is a basic example after all 27 | const defPath = '~' 28 | 29 | interface IAppState { 30 | message: string 31 | path: string 32 | } 33 | 34 | export class App extends Component<{}, {}> { 35 | public state: IAppState = { 36 | message: `Click button to choose a random file from the user's system`, 37 | path: defPath, 38 | } 39 | 40 | public pickFile = () => { 41 | eel.pick_file(defPath)(( message: string ) => this.setState( { message } ) ) 42 | } 43 | 44 | public render() { 45 | eel.expand_user(defPath)(( path: string ) => this.setState( { path } ) ) 46 | return ( 47 |
48 |
49 | logo 50 |

{this.state.message}

51 | 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | export default App; 59 | -------------------------------------------------------------------------------- /tests/data/init_test/hello.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Hello, World!{% endblock %} 3 | {% block head_scripts %} 4 | eel.expose(say_hello_js); // Expose this function to Python 5 | function say_hello_js(x) { 6 | console.log("Hello from " + x); 7 | } 8 | 9 | eel.expose(js_random); 10 | function js_random() { 11 | return Math.random(); 12 | } 13 | 14 | function print_num(n) { 15 | console.log('Got this from Python: ' + n); 16 | } 17 | 18 | eel.py_random()(print_num); 19 | 20 | say_hello_js("Javascript World!"); 21 | eel.say_hello_py("Javascript World!"); // Call a Python function 22 | {% endblock %} 23 | {% block content %} 24 | Hello, World! 25 |
26 | Page 2 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /tests/data/init_test/minified.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{15:function(e,t,o){},16:function(e,t,o){},17:function(e,t,o){"use strict";o.r(t);var n=o(0),a=o.n(n),s=o(2),c=o.n(s),r=(o(15),o(3)),i=o(4),l=o(7),p=o(5),u=o(8),m=o(6),h=o.n(m),d=(o(16),window.eel);function w(e){console.log("Hello from "+e)}d.set_host("ws://localhost:8081"),window.eel.expose(w,"say_hello_js"),window.eel.expose(function(e){console.log(e)},"show_log_alt"),window.eel.expose((function show_log(e) {console.log(e)}), "show_log"),w("Javascript World!"),d.say_hello_py("Javascript World!");var f="~",g=function(e){function t(){var e,o;Object(r.a)(this,t);for(var n=arguments.length,a=new Array(n),s=0;s 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 26 | 27 | 28 | 29 |

Input Example: Enter a value and check python console

30 |
31 | 32 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/integration/test_examples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from tempfile import TemporaryDirectory, NamedTemporaryFile 4 | 5 | from selenium import webdriver 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.support import expected_conditions 8 | from selenium.webdriver.support.wait import WebDriverWait 9 | 10 | from tests.utils import get_eel_server, get_console_logs 11 | 12 | 13 | def test_01_hello_world(driver): 14 | with get_eel_server('examples/01 - hello_world/hello.py', 'hello.html') as eel_url: 15 | driver.get(eel_url) 16 | assert driver.title == "Hello, World!" 17 | 18 | console_logs = get_console_logs(driver, minimum_logs=2) 19 | assert "Hello from Javascript World!" in console_logs[0]['message'] 20 | assert "Hello from Python World!" in console_logs[1]['message'] 21 | 22 | 23 | def test_02_callbacks(driver): 24 | with get_eel_server('examples/02 - callbacks/callbacks.py', 'callbacks.html') as eel_url: 25 | driver.get(eel_url) 26 | assert driver.title == "Callbacks Demo" 27 | 28 | console_logs = get_console_logs(driver, minimum_logs=1) 29 | assert "Got this from Python:" in console_logs[0]['message'] 30 | assert "callbacks.html" in console_logs[0]['message'] 31 | 32 | 33 | def test_03_callbacks(driver): 34 | with get_eel_server('examples/03 - sync_callbacks/sync_callbacks.py', 'sync_callbacks.html') as eel_url: 35 | driver.get(eel_url) 36 | assert driver.title == "Synchronous callbacks" 37 | 38 | console_logs = get_console_logs(driver, minimum_logs=1) 39 | assert "Got this from Python:" in console_logs[0]['message'] 40 | assert "callbacks.html" in console_logs[0]['message'] 41 | 42 | 43 | def test_04_file_access(driver: webdriver.Remote): 44 | with get_eel_server('examples/04 - file_access/file_access.py', 'file_access.html') as eel_url: 45 | driver.get(eel_url) 46 | assert driver.title == "Eel Demo" 47 | 48 | with TemporaryDirectory() as temp_dir, NamedTemporaryFile(dir=temp_dir) as temp_file: 49 | driver.find_element(value='input-box').clear() 50 | driver.find_element(value='input-box').send_keys(temp_dir) 51 | time.sleep(0.5) 52 | driver.find_element(By.CSS_SELECTOR, 'button').click() 53 | 54 | assert driver.find_element(value='file-name').text == os.path.basename(temp_file.name) 55 | 56 | 57 | def test_06_jinja_templates(driver: webdriver.Remote): 58 | with get_eel_server('examples/06 - jinja_templates/hello.py', 'templates/hello.html') as eel_url: 59 | driver.get(eel_url) 60 | assert driver.title == "Hello, World!" 61 | 62 | driver.find_element(By.CSS_SELECTOR, 'a').click() 63 | WebDriverWait(driver, 2.0).until(expected_conditions.presence_of_element_located((By.XPATH, '//h1[text()="This is page 2"]'))) 64 | 65 | 66 | def test_10_custom_app(driver: webdriver.Remote): 67 | # test default eel routes are working 68 | with get_eel_server('examples/10 - custom_app_routes/custom_app.py', 'index.html') as eel_url: 69 | driver.get(eel_url) 70 | # we really need to test if the page 404s, but selenium has no support for status codes 71 | # so we just test if we can get our page title 72 | assert driver.title == 'Hello, World!' 73 | 74 | # test custom routes are working 75 | with get_eel_server('examples/10 - custom_app_routes/custom_app.py', 'custom') as eel_url: 76 | driver.get(eel_url) 77 | assert 'Hello, World!' in driver.page_source 78 | -------------------------------------------------------------------------------- /tests/unit/test_eel.py: -------------------------------------------------------------------------------- 1 | import eel 2 | import pytest 3 | from tests.utils import TEST_DATA_DIR 4 | 5 | # Directory for testing eel.__init__ 6 | INIT_DIR = TEST_DATA_DIR / 'init_test' 7 | 8 | 9 | @pytest.mark.parametrize('js_code, expected_matches', [ 10 | ('eel.expose(w,"say_hello_js")', ['say_hello_js']), 11 | ('eel.expose(function(e){console.log(e)},"show_log_alt")', ['show_log_alt']), 12 | (' \t\nwindow.eel.expose((function show_log(e) {console.log(e)}), "show_log")\n', ['show_log']), 13 | ((INIT_DIR / 'minified.js').read_text(), ['say_hello_js', 'show_log_alt', 'show_log']), 14 | ((INIT_DIR / 'sample.html').read_text(), ['say_hello_js']), 15 | ((INIT_DIR / 'App.tsx').read_text(), ['say_hello_js', 'show_log']), 16 | ((INIT_DIR / 'hello.html').read_text(), ['say_hello_js', 'js_random']), 17 | ]) 18 | def test_exposed_js_functions(js_code, expected_matches): 19 | """Test the PyParsing PEG against several specific test cases.""" 20 | matches = eel.EXPOSED_JS_FUNCTIONS.parseString(js_code).asList() 21 | assert matches == expected_matches, f'Expected {expected_matches} (found: {matches}) in: {js_code}' 22 | 23 | 24 | def test_init(): 25 | """Test eel.init() against a test directory and ensure that all JS functions are in the global _js_functions.""" 26 | eel.init(path=INIT_DIR) 27 | result = eel._js_functions.sort() 28 | functions = ['show_log', 'js_random', 'show_log_alt', 'say_hello_js'].sort() 29 | assert result == functions, f'Expected {functions} (found: {result}) in {INIT_DIR}' 30 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import sys 4 | import platform 5 | import subprocess 6 | import tempfile 7 | import time 8 | from pathlib import Path 9 | 10 | import psutil 11 | 12 | # Path to the test data folder. 13 | TEST_DATA_DIR = Path(__file__).parent / "data" 14 | 15 | 16 | def get_process_listening_port(proc): 17 | conn = None 18 | if platform.system() == "Windows": 19 | current_process = psutil.Process(proc.pid) 20 | children = [] 21 | while children == []: 22 | time.sleep(0.01) 23 | children = current_process.children(recursive=True) 24 | if (3, 6) <= sys.version_info < (3, 7): 25 | children = [current_process] 26 | for child in children: 27 | while child.connections() == [] and not any(conn.status == "LISTEN" for conn in child.connections()): 28 | time.sleep(0.01) 29 | 30 | conn = next(filter(lambda conn: conn.status == "LISTEN", child.connections())) 31 | else: 32 | psutil_proc = psutil.Process(proc.pid) 33 | while not any(conn.status == "LISTEN" for conn in psutil_proc.connections()): 34 | time.sleep(0.01) 35 | 36 | conn = next(filter(lambda conn: conn.status == "LISTEN", psutil_proc.connections())) 37 | return conn.laddr.port 38 | 39 | 40 | @contextlib.contextmanager 41 | def get_eel_server(example_py, start_html): 42 | """Run an Eel example with the mode/port overridden so that no browser is launched and a random port is assigned""" 43 | test = None 44 | 45 | try: 46 | with tempfile.NamedTemporaryFile(mode='w', dir=os.path.dirname(example_py), delete=False) as test: 47 | # We want to run the examples unmodified to keep the test as realistic as possible, but all of the examples 48 | # want to launch browsers, which won't be supported in CI. The below script will configure eel to open on 49 | # a random port and not open a browser, before importing the Python example file - which will then 50 | # do the rest of the set up and start the eel server. This is definitely hacky, and means we can't 51 | # test mode/port settings for examples ... but this is OK for now. 52 | test.write(f""" 53 | import eel 54 | 55 | eel._start_args['mode'] = None 56 | eel._start_args['port'] = 0 57 | 58 | import {os.path.splitext(os.path.basename(example_py))[0]} 59 | """) 60 | proc = subprocess.Popen( 61 | [sys.executable, test.name], 62 | cwd=os.path.dirname(example_py), 63 | ) 64 | eel_port = get_process_listening_port(proc) 65 | 66 | yield f"http://localhost:{eel_port}/{start_html}" 67 | 68 | proc.terminate() 69 | 70 | finally: 71 | if test: 72 | try: 73 | os.unlink(test.name) 74 | except FileNotFoundError: 75 | pass 76 | 77 | 78 | def get_console_logs(driver, minimum_logs=0): 79 | console_logs = driver.get_log('browser') 80 | 81 | while len(console_logs) < minimum_logs: 82 | console_logs += driver.get_log('browser') 83 | time.sleep(0.1) 84 | 85 | return console_logs 86 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = typecheck,py{37,38,39,310,311,312} 3 | 4 | [pytest] 5 | timeout = 30 6 | 7 | [gh-actions] 8 | python = 9 | 3.7: py37 10 | 3.8: py38 11 | 3.9: py39 12 | 3.10: py310 13 | 3.11: py311 14 | 3.12: py312 15 | 16 | 17 | [testenv] 18 | description = run py.test tests 19 | deps = -r requirements-test.txt 20 | commands = 21 | # this ugly hack is here because: 22 | # https://github.com/tox-dev/tox/issues/149 23 | pip install -q -r '{toxinidir}'/requirements-test.txt 24 | '{envpython}' -m pytest {posargs} 25 | 26 | [testenv:typecheck] 27 | description = run type checks 28 | deps = -r requirements-test.txt 29 | commands = 30 | mypy --strict {posargs:eel} 31 | --------------------------------------------------------------------------------