Skip to content
This repository was archived by the owner on Jun 22, 2025. It is now read-only.

Commit 929c51b

Browse files
committed
Add type hints to eel
- Added type hints to cover external and internal APIs - Added py.typed to package_data (PEP 561) - Added mypy configuration - Added typecheck requirements to requirements-test.txt - Update tox and GitHub Actions to have typechecks in CI
1 parent 5051761 commit 929c51b

File tree

14 files changed

+241
-102
lines changed

14 files changed

+241
-102
lines changed

.github/workflows/test.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,18 @@ jobs:
2626
run: pip3 install -r requirements-meta.txt
2727
- name: Run tox tests
2828
run: tox -- --durations=0 --timeout=30
29+
30+
typecheck:
31+
runs-on: windows-latest
32+
33+
steps:
34+
- name: Checkout repository
35+
uses: actions/checkout@v2
36+
- name: Setup python
37+
uses: actions/setup-python@v2
38+
with:
39+
python-version: "3.10"
40+
- name: Setup test execution environment.
41+
run: pip3 install -r requirements-meta.txt
42+
- name: Run tox tests
43+
run: tox -e typecheck

eel/__init__.py

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
from builtins import range
22
import traceback
33
from io import open
4+
from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable, TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from eel.types import OptionsDictT, WebSocketT
8+
else:
9+
WebSocketT = Any
10+
OptionsDictT = Any
411

512
from gevent.threading import Timer
613
import gevent as gvt
@@ -17,25 +24,27 @@
1724
import socket
1825
import mimetypes
1926

27+
2028
mimetypes.add_type('application/javascript', '.js')
21-
_eel_js_file = pkg.resource_filename('eel', 'eel.js')
22-
_eel_js = open(_eel_js_file, encoding='utf-8').read()
23-
_websockets = []
24-
_call_return_values = {}
25-
_call_return_callbacks = {}
26-
_call_number = 0
27-
_exposed_functions = {}
28-
_js_functions = []
29-
_mock_queue = []
30-
_mock_queue_done = set()
31-
_shutdown = None
29+
_eel_js_file: str = pkg.resource_filename('eel', 'eel.js')
30+
_eel_js: str = open(_eel_js_file, encoding='utf-8').read()
31+
_websockets: List[Tuple[Any, WebSocketT]] = []
32+
_call_return_values: Dict[Any, Any] = {}
33+
_call_return_callbacks: Dict[float, Tuple[Callable[..., Any], Optional[Callable[..., Any]]]] = {}
34+
_call_number: int = 0
35+
_exposed_functions: Dict[Any, Any] = {}
36+
_js_functions: List[Any] = []
37+
_mock_queue: List[Any] = []
38+
_mock_queue_done: Set[Any] = set()
39+
_shutdown: Optional[gvt.Greenlet] = None # Later assigned as global by _websocket_close()
40+
root_path: str # Later assigned as global by init()
3241

3342
# The maximum time (in milliseconds) that Python will try to retrieve a return value for functions executing in JS
3443
# Can be overridden through `eel.init` with the kwarg `js_result_timeout` (default: 10000)
35-
_js_result_timeout = 10000
44+
_js_result_timeout: int = 10000
3645

3746
# All start() options must provide a default value and explanation here
38-
_start_args = {
47+
_start_args: OptionsDictT = {
3948
'mode': 'chrome', # What browser is used
4049
'host': 'localhost', # Hostname use for Bottle server
4150
'port': 8000, # Port used for Bottle server (use 0 for auto)
@@ -51,12 +60,12 @@
5160
'disable_cache': True, # Sets the no-store response header when serving assets
5261
'default_path': 'index.html', # The default file to retrieve for the root URL
5362
'app': btl.default_app(), # Allows passing in a custom Bottle instance, e.g. with middleware
54-
'shutdown_delay': 1.0 # how long to wait after a websocket closes before detecting complete shutdown
63+
'shutdown_delay': 1.0 # how long to wait after a websocket closes before detecting complete shutdown
5564
}
5665

5766
# == Temporary (suppressible) error message to inform users of breaking API change for v1.0.0 ===
5867
_start_args['suppress_error'] = False
59-
api_error_message = '''
68+
api_error_message: str = '''
6069
----------------------------------------------------------------------------------
6170
'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel
6271
To suppress this error, add 'suppress_error=True' to start() call.
@@ -67,15 +76,15 @@
6776

6877
# Public functions
6978

70-
def expose(name_or_function=None):
79+
def expose(name_or_function: Optional[Callable[..., Any]] = None) -> Callable[..., Any]:
7180
# Deal with '@eel.expose()' - treat as '@eel.expose'
7281
if name_or_function is None:
7382
return expose
7483

75-
if type(name_or_function) == str: # Called as '@eel.expose("my_name")'
84+
if isinstance(name_or_function, str): # Called as '@eel.expose("my_name")'
7685
name = name_or_function
7786

78-
def decorator(function):
87+
def decorator(function: Callable[..., Any]) -> Any:
7988
_expose(name, function)
8089
return function
8190
return decorator
@@ -87,7 +96,7 @@ def decorator(function):
8796

8897
# PyParsing grammar for parsing exposed functions in JavaScript code
8998
# Examples: `eel.expose(w, "func_name")`, `eel.expose(func_name)`, `eel.expose((function (e){}), "func_name")`
90-
EXPOSED_JS_FUNCTIONS = pp.ZeroOrMore(
99+
EXPOSED_JS_FUNCTIONS: pp.ZeroOrMore = pp.ZeroOrMore(
91100
pp.Suppress(
92101
pp.SkipTo(pp.Literal('eel.expose('))
93102
+ pp.Literal('eel.expose(')
@@ -101,8 +110,8 @@ def decorator(function):
101110
)
102111

103112

104-
def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm',
105-
'.xhtml', '.vue'], js_result_timeout=10000):
113+
def init(path: str, allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm',
114+
'.xhtml', '.vue'], js_result_timeout: int = 10000) -> None:
106115
global root_path, _js_functions, _js_result_timeout
107116
root_path = _get_real_path(path)
108117

@@ -133,7 +142,7 @@ def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm',
133142
_js_result_timeout = js_result_timeout
134143

135144

136-
def start(*start_urls, **kwargs):
145+
def start(*start_urls: str, **kwargs: Any) -> None:
137146
_start_args.update(kwargs)
138147

139148
if 'options' in kwargs:
@@ -150,6 +159,8 @@ def start(*start_urls, **kwargs):
150159

151160
if _start_args['jinja_templates'] != None:
152161
from jinja2 import Environment, FileSystemLoader, select_autoescape
162+
if not isinstance(_start_args['jinja_templates'], str):
163+
raise TypeError("'jinja_templates start_arg/option must be of type str'")
153164
templates_path = os.path.join(root_path, _start_args['jinja_templates'])
154165
_start_args['jinja_env'] = Environment(loader=FileSystemLoader(templates_path),
155166
autoescape=select_autoescape(['html', 'xml']))
@@ -162,25 +173,27 @@ def start(*start_urls, **kwargs):
162173
# Launch the browser to the starting URLs
163174
show(*start_urls)
164175

165-
def run_lambda():
176+
def run_lambda() -> None:
166177
if _start_args['all_interfaces'] == True:
167178
HOST = '0.0.0.0'
168179
else:
180+
if not isinstance(_start_args['host'], str):
181+
raise TypeError("'host' start_arg/option must be of type str")
169182
HOST = _start_args['host']
170183

171-
app = _start_args['app'] # type: btl.Bottle
184+
app = _start_args['app']
172185

173186
if isinstance(app, btl.Bottle):
174187
register_eel_routes(app)
175188
else:
176189
register_eel_routes(btl.default_app())
177190

178-
return btl.run(
191+
btl.run(
179192
host=HOST,
180193
port=_start_args['port'],
181194
server=wbs.GeventWebSocketServer,
182195
quiet=True,
183-
app=app)
196+
app=app) # Always returns None
184197

185198
# Start the webserver
186199
if _start_args['block']:
@@ -189,20 +202,20 @@ def run_lambda():
189202
spawn(run_lambda)
190203

191204

192-
def show(*start_urls):
193-
brw.open(start_urls, _start_args)
205+
def show(*start_urls: str) -> None:
206+
brw.open(list(start_urls), _start_args)
194207

195208

196-
def sleep(seconds):
209+
def sleep(seconds: Union[int, float]) -> None:
197210
gvt.sleep(seconds)
198211

199212

200-
def spawn(function, *args, **kwargs):
213+
def spawn(function: Callable[..., Any], *args: Any, **kwargs: Any) -> gvt.Greenlet:
201214
return gvt.spawn(function, *args, **kwargs)
202215

203216
# Bottle Routes
204217

205-
def _eel():
218+
def _eel() -> str:
206219
start_geometry = {'default': {'size': _start_args['size'],
207220
'position': _start_args['position']},
208221
'pages': _start_args['geometry']}
@@ -215,16 +228,20 @@ def _eel():
215228
_set_response_headers(btl.response)
216229
return page
217230

218-
def _root():
231+
def _root() -> Optional[btl.Response]:
232+
if not isinstance(_start_args['default_path'], str):
233+
raise TypeError("'default_path' start_arg/option must be of type str")
219234
return _static(_start_args['default_path'])
220235

221-
def _static(path):
236+
def _static(path: str) -> Optional[btl.Response]:
222237
response = None
223238
if 'jinja_env' in _start_args and 'jinja_templates' in _start_args:
239+
if not isinstance(_start_args['jinja_templates'], str):
240+
raise TypeError("'jinja_templates' start_arg/option must be of type str")
224241
template_prefix = _start_args['jinja_templates'] + '/'
225242
if path.startswith(template_prefix):
226243
n = len(template_prefix)
227-
template = _start_args['jinja_env'].get_template(path[n:])
244+
template = _start_args['jinja_env'].get_template(path[n:]) # type: ignore # depends on conditional import in start()
228245
response = btl.HTTPResponse(template.render())
229246

230247
if response is None:
@@ -233,7 +250,7 @@ def _static(path):
233250
_set_response_headers(response)
234251
return response
235252

236-
def _websocket(ws):
253+
def _websocket(ws: WebSocketT) -> None:
237254
global _websockets
238255

239256
for js_function in _js_functions:
@@ -259,14 +276,14 @@ def _websocket(ws):
259276
_websocket_close(page)
260277

261278

262-
BOTTLE_ROUTES = {
279+
BOTTLE_ROUTES: Dict[str, Tuple[Callable[..., Any], Dict[Any, Any]]] = {
263280
"/eel.js": (_eel, dict()),
264281
"/": (_root, dict()),
265282
"/<path:path>": (_static, dict()),
266283
"/eel": (_websocket, dict(apply=[wbs.websocket]))
267284
}
268285

269-
def register_eel_routes(app):
286+
def register_eel_routes(app: btl.Bottle) -> None:
270287
'''
271288
Adds eel routes to `app`. Only needed if you are passing something besides `bottle.Bottle` to `eel.start()`.
272289
Ex:
@@ -281,11 +298,11 @@ def register_eel_routes(app):
281298

282299
# Private functions
283300

284-
def _safe_json(obj):
301+
def _safe_json(obj: Any) -> str:
285302
return jsn.dumps(obj, default=lambda o: None)
286303

287304

288-
def _repeated_send(ws, msg):
305+
def _repeated_send(ws: WebSocketT, msg: str) -> None:
289306
for attempt in range(100):
290307
try:
291308
ws.send(msg)
@@ -294,7 +311,7 @@ def _repeated_send(ws, msg):
294311
sleep(0.001)
295312

296313

297-
def _process_message(message, ws):
314+
def _process_message(message: Dict[str, Any], ws: WebSocketT) -> None:
298315
if 'call' in message:
299316
error_info = {}
300317
try:
@@ -326,47 +343,48 @@ def _process_message(message, ws):
326343
print('Invalid message received: ', message)
327344

328345

329-
def _get_real_path(path):
346+
def _get_real_path(path: str) -> str:
330347
if getattr(sys, 'frozen', False):
331-
return os.path.join(sys._MEIPASS, path)
348+
return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstaller
332349
else:
333350
return os.path.abspath(path)
334351

335352

336-
def _mock_js_function(f):
353+
def _mock_js_function(f: str) -> None:
337354
exec('%s = lambda *args: _mock_call("%s", args)' % (f, f), globals())
338355

339356

340-
def _import_js_function(f):
357+
def _import_js_function(f: str) -> None:
341358
exec('%s = lambda *args: _js_call("%s", args)' % (f, f), globals())
342359

343360

344-
def _call_object(name, args):
361+
def _call_object(name: str, args: Any) -> Dict[str, Any]:
345362
global _call_number
346363
_call_number += 1
347364
call_id = _call_number + rnd.random()
348365
return {'call': call_id, 'name': name, 'args': args}
349366

350367

351-
def _mock_call(name, args):
368+
def _mock_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]:
352369
call_object = _call_object(name, args)
353370
global _mock_queue
354371
_mock_queue += [call_object]
355372
return _call_return(call_object)
356373

357374

358-
def _js_call(name, args):
375+
def _js_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]:
359376
call_object = _call_object(name, args)
360377
for _, ws in _websockets:
361378
_repeated_send(ws, _safe_json(call_object))
362379
return _call_return(call_object)
363380

364381

365-
def _call_return(call):
382+
def _call_return(call: Dict[str, Any]) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]:
366383
global _js_result_timeout
367384
call_id = call['call']
368385

369-
def return_func(callback=None, error_callback=None):
386+
def return_func(callback: Optional[Callable[..., Any]] = None,
387+
error_callback: Optional[Callable[..., Any]] = None) -> Any:
370388
if callback is not None:
371389
_call_return_callbacks[call_id] = (callback, error_callback)
372390
else:
@@ -377,33 +395,35 @@ def return_func(callback=None, error_callback=None):
377395
return return_func
378396

379397

380-
def _expose(name, function):
398+
def _expose(name: str, function: Callable[..., Any]) -> None:
381399
msg = 'Already exposed function with name "%s"' % name
382400
assert name not in _exposed_functions, msg
383401
_exposed_functions[name] = function
384402

385403

386-
def _detect_shutdown():
404+
def _detect_shutdown() -> None:
387405
if len(_websockets) == 0:
388406
sys.exit()
389407

390408

391-
def _websocket_close(page):
409+
def _websocket_close(page: str) -> None:
392410
global _shutdown
393411

394412
close_callback = _start_args.get('close_callback')
395413

396414
if close_callback is not None:
415+
if not callable(close_callback):
416+
raise TypeError("'close_callback' start_arg/option must be callable or None")
397417
sockets = [p for _, p in _websockets]
398418
close_callback(page, sockets)
399419
else:
400-
if _shutdown:
420+
if isinstance(_shutdown, gvt.Greenlet):
401421
_shutdown.kill()
402422

403423
_shutdown = gvt.spawn_later(_start_args['shutdown_delay'], _detect_shutdown)
404424

405425

406-
def _set_response_headers(response):
426+
def _set_response_headers(response: btl.Response) -> None:
407427
if _start_args['disable_cache']:
408428
# https://stackoverflow.com/a/24748094/280852
409429
response.set_header('Cache-Control', 'no-store')

0 commit comments

Comments
 (0)