diff --git a/eel/__init__.py b/eel/__init__.py index 9138b1ca..c5350974 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -6,6 +6,7 @@ if TYPE_CHECKING: from geventwebsocket.websocket import WebSocket + from eel.types import OptionsDictT from gevent.threading import Timer import gevent as gvt @@ -41,7 +42,7 @@ _js_result_timeout: int = 10000 # All start() options must provide a default value and explanation here -_start_args: dict[str, Any] = { +_start_args: OptionsDictT = { 'mode': 'chrome', # What browser is used 'host': 'localhost', # Hostname use for Bottle server 'port': 8000, # Port used for Bottle server (use 0 for auto) @@ -156,6 +157,8 @@ def start(*start_urls: str, **kwargs: Any) -> None: if _start_args['jinja_templates'] != None: from jinja2 import Environment, FileSystemLoader, select_autoescape + if not isinstance(_start_args['jinja_templates'], str): + raise TypeError("'jinja_templates start_arg/option must be of type str'") templates_path = os.path.join(root_path, _start_args['jinja_templates']) _start_args['jinja_env'] = Environment(loader=FileSystemLoader(templates_path), autoescape=select_autoescape(['html', 'xml'])) @@ -172,6 +175,8 @@ def run_lambda() -> None: if _start_args['all_interfaces'] == True: HOST = '0.0.0.0' else: + if not isinstance(_start_args['host'], str): + raise TypeError("'host' start_arg/option must be of type str") HOST = _start_args['host'] app = _start_args['app'] @@ -200,7 +205,7 @@ def show(*start_urls: str) -> None: def sleep(seconds: int | float) -> None: - gvt.sleep(seconds) # type: ignore # gevent docs specify int | float, available stubs are wrong + gvt.sleep(seconds) def spawn(function: Callable[..., Any], *args: Any, **kwargs: Any) -> gvt.Greenlet: @@ -222,15 +227,19 @@ def _eel() -> str: return page def _root() -> Optional[btl.Response]: + if not isinstance(_start_args['default_path'], str): + raise TypeError("'default_path' start_arg/option must be of type str") return _static(_start_args['default_path']) def _static(path: str) -> Optional[btl.HTTPResponse | btl.HTTPError]: response = None if 'jinja_env' in _start_args and 'jinja_templates' in _start_args: + if not isinstance(_start_args['jinja_templates'], str): + raise TypeError("'jinja_templates' start_arg/option must be of type str") template_prefix = _start_args['jinja_templates'] + '/' if path.startswith(template_prefix): n = len(template_prefix) - template = _start_args['jinja_env'].get_template(path[n:]) + template = _start_args['jinja_env'].get_template(path[n:]) # type: ignore # depends on conditional import in start() response = btl.HTTPResponse(template.render()) if response is None: @@ -291,7 +300,7 @@ def _safe_json(obj: Any) -> str: return jsn.dumps(obj, default=lambda o: None) -def _repeated_send(ws: WebSocket, msg: str): +def _repeated_send(ws: WebSocket, msg: str) -> None: for attempt in range(100): try: ws.send(msg) @@ -401,16 +410,18 @@ def _websocket_close(page: str) -> None: close_callback = _start_args.get('close_callback') if close_callback is not None: + if not callable(close_callback): + raise TypeError("'close_callback' start_arg/option must be callable or None") sockets = [p for _, p in _websockets] close_callback(page, sockets) else: - if _shutdown: + if isinstance(_shutdown, gvt.Greenlet): _shutdown.kill() _shutdown = gvt.spawn_later(_start_args['shutdown_delay'], _detect_shutdown) -def _set_response_headers(response: btl.Response): +def _set_response_headers(response: btl.Response) -> None: if _start_args['disable_cache']: # https://stackoverflow.com/a/24748094/280852 response.set_header('Cache-Control', 'no-store') diff --git a/eel/browsers.py b/eel/browsers.py index 4b3ead68..e54fd495 100644 --- a/eel/browsers.py +++ b/eel/browsers.py @@ -4,6 +4,7 @@ from typing import List, Dict, Iterable, Optional from types import ModuleType +from eel.types import OptionsDictT import eel.chrome as chm import eel.electron as ele import eel.edge as edge @@ -16,20 +17,24 @@ 'edge': edge} -def _build_url_from_dict(page: Dict[str, str], options: Dict[str, str]) -> str: +def _build_url_from_dict(page: Dict[str, str], options: OptionsDictT) -> str: scheme = page.get('scheme', 'http') host = page.get('host', 'localhost') port = page.get('port', options["port"]) path = page.get('path', '') + if not isinstance(port, (int, str)): + raise TypeError("'port' option must be an integer") return '%s://%s:%d/%s' % (scheme, host, int(port), path) -def _build_url_from_string(page: str, options: Dict[str, str]) -> str: +def _build_url_from_string(page: str, options: OptionsDictT) -> str: + if not isinstance(options['port'], (int, str)): + raise TypeError("'port' option must be an integer") base_url = 'http://%s:%d/' % (options['host'], int(options['port'])) return base_url + page -def _build_urls(start_pages: Iterable[str | Dict[str, str]], options: Dict[str, str]) -> List[str]: +def _build_urls(start_pages: Iterable[str | Dict[str, str]], options: OptionsDictT) -> List[str]: urls: List[str] = [] for page in start_pages: @@ -42,16 +47,20 @@ def _build_urls(start_pages: Iterable[str | Dict[str, str]], options: Dict[str, return urls -def open(start_pages: Iterable[str | Dict[str, str]], options: Dict[str, str]) -> None: +def open(start_pages: Iterable[str | Dict[str, str]], options: OptionsDictT) -> None: # Build full URLs for starting pages (including host and port) start_urls = _build_urls(start_pages, options) mode = options.get('mode') - if mode in [None, False]: + if not isinstance(mode, (str, bool, type(None))) or mode is True: + raise TypeError("'mode' option must by either a string, False, or None") + if mode is None or mode is False: # Don't open a browser pass elif mode == 'custom': # Just run whatever command the user provided + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") sps.Popen(options['cmdline_args'], stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE) elif mode in _browser_modules: diff --git a/eel/chrome.py b/eel/chrome.py index f3c233d2..9e212491 100644 --- a/eel/chrome.py +++ b/eel/chrome.py @@ -1,12 +1,16 @@ from __future__ import annotations import sys, subprocess as sps, os -from typing import Dict, List, Any, Optional +from typing import List, Optional + +from eel.types import OptionsDictT # Every browser specific module must define run(), find_path() and name like this name: str = 'Google Chrome/Chromium' -def run(path: str, options: Dict[str, Any], start_urls: List[str]) -> None: +def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") if options['app_mode']: for url in start_urls: sps.Popen([path, '--app=%s' % url] + @@ -63,7 +67,7 @@ def _find_chrome_linux() -> Optional[str]: for name in chrome_names: chrome = wch.which(name) if chrome is not None: - return chrome + return chrome # type: ignore # whichcraft doesn't currently have type hints return None diff --git a/eel/edge.py b/eel/edge.py index 9fcd9a50..631ecec1 100644 --- a/eel/edge.py +++ b/eel/edge.py @@ -2,12 +2,14 @@ import platform import subprocess as sps import sys -from typing import List, Dict, Any +from typing import List + +from eel.types import OptionsDictT name: str = 'Edge' -def run(_path: str, options: Dict[str, Any], start_urls: List[str]) -> None: +def run(_path: str, options: OptionsDictT, start_urls: List[str]) -> None: cmd = 'start microsoft-edge:{}'.format(start_urls[0]) sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) diff --git a/eel/electron.py b/eel/electron.py index f5897535..974b2dd9 100644 --- a/eel/electron.py +++ b/eel/electron.py @@ -3,11 +3,15 @@ import os import subprocess as sps import whichcraft as wch -from typing import List, Dict, Any, Optional +from typing import List, Optional + +from eel.types import OptionsDictT name: str = 'Electron' -def run(path: str, options: Dict[str, Any], start_urls: List[str]): +def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") cmd = [path] + options['cmdline_args'] cmd += ['.', ';'.join(start_urls)] sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE) @@ -20,7 +24,7 @@ def find_path() -> Optional[str]: return os.path.join(bat_path, r'..\node_modules\electron\dist\electron.exe') elif sys.platform in ['darwin', 'linux']: # This should work find... - return wch.which('electron') + return wch.which('electron') # type: ignore # whichcraft doesn't currently have type hints else: return None diff --git a/eel/py.typed b/eel/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/eel/types.py b/eel/types.py new file mode 100644 index 00000000..01fd0170 --- /dev/null +++ b/eel/types.py @@ -0,0 +1,15 @@ +from __future__ import annotations +from typing import Dict, List, Tuple, Callable, Optional, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from jinja2 import Environment + Jinja2Environment = Environment +else: + Jinja2Environment = None + +OptionsDictT = Dict[ + str, + Optional[ + str | bool | int | float | List[str] | Tuple[int, int] | Dict[str, Tuple[int, int]] | Callable[..., Any] | Jinja2Environment + ] + ] \ No newline at end of file