Skip to content

Commit

Permalink
Fix parsing minified code with PEG (#363) (#365)
Browse files Browse the repository at this point in the history
Update JS function name parsing

Use a custom PyParsing grammar to extract function names
from JavaScript names rather than a basic and somewhat
flawed regex pattern.
  • Loading branch information
KyleKing authored Aug 17, 2020
1 parent 2e671b6 commit 24cee9a
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 9 deletions.
2 changes: 1 addition & 1 deletion README-developers.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Create a dev virtual environment. Your process for doing this may vary, but migh

```bash
python3 -m venv venv
./venv/bin/activate
source venv/bin/activate
```

We support Python 3.6+ so developers should ideally run their tests against the latest minor version of each major version we support from there. Tox is configured to run tests against each major version we support. In order to run tox fully, you will need to install multiple versions of Python. See the pinned minor versions in `.python-version`.
Expand Down
25 changes: 19 additions & 6 deletions eel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import re as rgx
import os
import eel.browsers as brw
import pyparsing as pp
import random as rnd
import sys
import pkg_resources as pkg
Expand Down Expand Up @@ -83,6 +84,22 @@ def decorator(function):
return function


# PyParsing grammar for parsing exposed functions in JavaScript code
# Examples: `eel.expose(w, "func_name")`, `eel.expose(func_name)`, `eel.expose((function (e){}), "func_name")`
EXPOSED_JS_FUNCTIONS = pp.ZeroOrMore(
pp.Suppress(
pp.SkipTo(pp.Literal('eel.expose('))
+ pp.Literal('eel.expose(')
+ pp.Optional(
pp.Or([pp.nestedExpr(), pp.Word(pp.printables, excludeChars=',')]) + pp.Literal(',')
)
)
+ pp.Suppress(pp.Regex(r'["\']?'))
+ pp.Word(pp.printables, excludeChars='"\')')
+ pp.Suppress(pp.Regex(r'["\']?\s*\)')),
)


def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm',
'.xhtml', '.vue'], js_result_timeout=10000):
global root_path, _js_functions, _js_result_timeout
Expand All @@ -98,12 +115,8 @@ def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm',
with open(os.path.join(root, name), encoding='utf-8') as file:
contents = file.read()
expose_calls = set()
finder = rgx.findall(r'eel\.expose\(([^\)]+)\)', contents)
for expose_call in finder:
# If name specified in 2nd argument, strip quotes and store as function name
if ',' in expose_call:
expose_call = rgx.sub(r'["\']', '', expose_call.split(',')[1])
expose_call = expose_call.strip()
matches = EXPOSED_JS_FUNCTIONS.parseString(contents).asList()
for expose_call in matches:
# Verify that function name is valid
msg = "eel.expose() call contains '(' or '='"
assert rgx.findall(r'[\(=]', expose_call) == [], msg
Expand Down
2 changes: 2 additions & 0 deletions examples/07 - CreateReactApp/eel_CRA.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def start_eel(develop):
say_hello_py('Python World!')
eel.say_hello_js('Python World!') # Call a JavaScript function (must be after `eel.init()`)

eel.show_log('https://github.com/samuelhwilliams/Eel/issues/363 (show_log)')

eel_kwargs = dict(
host='localhost',
port=8080,
Expand Down
6 changes: 6 additions & 0 deletions examples/07 - CreateReactApp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ function sayHelloJS( x: any ) {
// WARN: must use window.eel to keep parse-able eel.expose{...}
window.eel.expose( sayHelloJS, 'say_hello_js' )

// Test anonymous function when minimized. See https://github.com/samuelhwilliams/Eel/issues/363
function show_log(msg:string) {
console.log(msg)
}
window.eel.expose(show_log, 'show_log')

// Test calling sayHelloJS, then call the corresponding Python function
sayHelloJS( 'Javascript World!' )
eel.say_hello_py( 'Javascript World!' )
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ bottle-websocket==0.2.9
gevent==1.3.6
gevent-websocket==0.10.1
greenlet==0.4.15
pyparsing==2.4.7
whichcraft==0.4.1
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
package_data={
'eel': ['eel.js'],
},
install_requires=['bottle', 'bottle-websocket', 'future', 'whichcraft'],
install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'whichcraft'],
extras_require={
"jinja2": ['jinja2>=2.10']
},
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def driver():
# Firefox doesn't currently supported pulling JavaScript console logs, which we currently scan to affirm that
# JS/Python can communicate in some places. So for now, we can't really use firefox/geckodriver during testing.
# This may be added in the future: https://github.com/mozilla/geckodriver/issues/284

# elif TEST_BROWSER == "firefox":
# options = webdriver.FirefoxOptions()
# options.headless = True
Expand Down
58 changes: 58 additions & 0 deletions tests/data/init_test/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

// Point Eel web socket to the instance
export const eel = window.eel
eel.set_host( 'ws://localhost:8080' )

// Expose the `sayHelloJS` function to Python as `say_hello_js`
function sayHelloJS( x: any ) {
console.log( 'Hello from ' + x )
}
// WARN: must use window.eel to keep parse-able eel.expose{...}
window.eel.expose( sayHelloJS, 'say_hello_js' )

// Test anonymous function when minimized. See https://github.com/samuelhwilliams/Eel/issues/363
function show_log(msg:string) {
console.log(msg)
}
window.eel.expose(show_log, 'show_log')

// Test calling sayHelloJS, then call the corresponding Python function
sayHelloJS( 'Javascript World!' )
eel.say_hello_py( 'Javascript World!' )

// Set the default path. Would be a text input, but this is a basic example after all
const defPath = '~'

interface IAppState {
message: string
path: string
}

export class App extends Component<{}, {}> {
public state: IAppState = {
message: `Click button to choose a random file from the user's system`,
path: defPath,
}

public pickFile = () => {
eel.pick_file(defPath)(( message: string ) => this.setState( { message } ) )
}

public render() {
eel.expand_user(defPath)(( path: string ) => this.setState( { path } ) )
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>{this.state.message}</p>
<button className='App-button' onClick={this.pickFile}>Pick Random File From `{this.state.path}`</button>
</header>
</div>
);
}
}

export default App;
27 changes: 27 additions & 0 deletions tests/data/init_test/hello.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block title %}Hello, World!{% endblock %}
{% block head_scripts %}
eel.expose(say_hello_js); // Expose this function to Python
function say_hello_js(x) {
console.log("Hello from " + x);
}

eel.expose(js_random);
function js_random() {
return Math.random();
}

function print_num(n) {
console.log('Got this from Python: ' + n);
}

eel.py_random()(print_num);

say_hello_js("Javascript World!");
eel.say_hello_py("Javascript World!"); // Call a Python function
{% endblock %}
{% block content %}
Hello, World!
<br />
<a href="page2.html">Page 2</a>
{% endblock %}
3 changes: 3 additions & 0 deletions tests/data/init_test/minified.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions tests/data/init_test/sample.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!-- Below is from: https://github.com/samuelhwilliams/Eel/blob/master/examples/05%20-%20input/web/main.html -->

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<!-- Include eel.js - note this file doesn't exist in the 'web' directory -->
<script type="text/javascript" src="/eel.js"></script>
<script type="text/javascript">
$(function(){

eel.expose(say_hello_js); // Expose this function to Python
function say_hello_js(x) {
console.log("Hello from " + x);
}
say_hello_js("Javascript World!");
eel.handleinput("connected!"); // Call a Python function

$("#btn").click(function(){
eel.handleinput($("#inp").val());
$('#inp').val('');
});
});
</script>
</head>

<body>
<h1>Input Example: Enter a value and check python console</h1>
<div>
<input type='text' id='inp' placeholder='Write anything'>
<input type='button' id='btn' class='btn btn-primary' value='Submit'>
</div>
</body>
</html>
29 changes: 29 additions & 0 deletions tests/unit/test_eel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import eel
import pytest
from tests.utils import TEST_DATA_DIR

# Directory for testing eel.__init__
INIT_DIR = TEST_DATA_DIR / 'init_test'


@pytest.mark.parametrize('js_code, expected_matches', [
('eel.expose(w,"say_hello_js")', ['say_hello_js']),
('eel.expose(function(e){console.log(e)},"show_log_alt")', ['show_log_alt']),
(' \t\nwindow.eel.expose((function show_log(e) {console.log(e)}), "show_log")\n', ['show_log']),
((INIT_DIR / 'minified.js').read_text(), ['say_hello_js', 'show_log_alt', 'show_log']),
((INIT_DIR / 'sample.html').read_text(), ['say_hello_js']),
((INIT_DIR / 'App.tsx').read_text(), ['say_hello_js', 'show_log']),
((INIT_DIR / 'hello.html').read_text(), ['say_hello_js', 'js_random']),
])
def test_exposed_js_functions(js_code, expected_matches):
"""Test the PyParsing PEG against several specific test cases."""
matches = eel.EXPOSED_JS_FUNCTIONS.parseString(js_code).asList()
assert matches == expected_matches, f'Expected {expected_matches} (found: {matches}) in: {js_code}'


def test_init():
"""Test eel.init() against a test directory and ensure that all JS functions are in the global _js_functions."""
eel.init(path=INIT_DIR)
result = eel._js_functions.sort()
functions = ['show_log', 'js_random', 'show_log_alt', 'say_hello_js'].sort()
assert result == functions, f'Expected {functions} (found: {result}) in {INIT_DIR}'
4 changes: 4 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import subprocess
import tempfile
import time
from pathlib import Path

import psutil

# Path to the test data folder.
TEST_DATA_DIR = Path(__file__).parent / 'data'


def get_process_listening_port(proc):
psutil_proc = psutil.Process(proc.pid)
Expand Down

0 comments on commit 24cee9a

Please sign in to comment.