diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39f07856..552a0c67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,3 +26,18 @@ jobs: run: pip3 install -r requirements-meta.txt - name: Run tox tests run: tox -- --durations=0 --timeout=30 + + typecheck: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + - name: Setup test execution environment. + run: pip3 install -r requirements-meta.txt + - name: Run tox tests + run: tox -e typecheck diff --git a/eel/__init__.py b/eel/__init__.py index 898f1da0..fd09b45b 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -1,6 +1,13 @@ from builtins import range import traceback from io import open +from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable, TYPE_CHECKING + +if TYPE_CHECKING: + from eel.types import OptionsDictT, WebSocketT +else: + WebSocketT = Any + OptionsDictT = Any from gevent.threading import Timer import gevent as gvt @@ -17,25 +24,27 @@ import socket import mimetypes + mimetypes.add_type('application/javascript', '.js') -_eel_js_file = pkg.resource_filename('eel', 'eel.js') -_eel_js = open(_eel_js_file, encoding='utf-8').read() -_websockets = [] -_call_return_values = {} -_call_return_callbacks = {} -_call_number = 0 -_exposed_functions = {} -_js_functions = [] -_mock_queue = [] -_mock_queue_done = set() -_shutdown = None +_eel_js_file: str = pkg.resource_filename('eel', 'eel.js') +_eel_js: str = open(_eel_js_file, encoding='utf-8').read() +_websockets: List[Tuple[Any, WebSocketT]] = [] +_call_return_values: Dict[Any, Any] = {} +_call_return_callbacks: Dict[float, Tuple[Callable[..., Any], Optional[Callable[..., Any]]]] = {} +_call_number: int = 0 +_exposed_functions: Dict[Any, Any] = {} +_js_functions: List[Any] = [] +_mock_queue: List[Any] = [] +_mock_queue_done: Set[Any] = set() +_shutdown: Optional[gvt.Greenlet] = None # Later assigned as global by _websocket_close() +root_path: str # Later assigned as global by init() # The maximum time (in milliseconds) that Python will try to retrieve a return value for functions executing in JS # Can be overridden through `eel.init` with the kwarg `js_result_timeout` (default: 10000) -_js_result_timeout = 10000 +_js_result_timeout: int = 10000 # All start() options must provide a default value and explanation here -_start_args = { +_start_args: OptionsDictT = { 'mode': 'chrome', # What browser is used 'host': 'localhost', # Hostname use for Bottle server 'port': 8000, # Port used for Bottle server (use 0 for auto) @@ -51,12 +60,12 @@ 'disable_cache': True, # Sets the no-store response header when serving assets 'default_path': 'index.html', # The default file to retrieve for the root URL 'app': btl.default_app(), # Allows passing in a custom Bottle instance, e.g. with middleware - 'shutdown_delay': 1.0 # how long to wait after a websocket closes before detecting complete shutdown + 'shutdown_delay': 1.0 # how long to wait after a websocket closes before detecting complete shutdown } # == Temporary (suppressible) error message to inform users of breaking API change for v1.0.0 === _start_args['suppress_error'] = False -api_error_message = ''' +api_error_message: str = ''' ---------------------------------------------------------------------------------- 'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel To suppress this error, add 'suppress_error=True' to start() call. @@ -67,15 +76,15 @@ # Public functions -def expose(name_or_function=None): +def expose(name_or_function: Optional[Callable[..., Any]] = None) -> Callable[..., Any]: # Deal with '@eel.expose()' - treat as '@eel.expose' if name_or_function is None: return expose - if type(name_or_function) == str: # Called as '@eel.expose("my_name")' + if isinstance(name_or_function, str): # Called as '@eel.expose("my_name")' name = name_or_function - def decorator(function): + def decorator(function: Callable[..., Any]) -> Any: _expose(name, function) return function return decorator @@ -87,7 +96,7 @@ def decorator(function): # PyParsing grammar for parsing exposed functions in JavaScript code # Examples: `eel.expose(w, "func_name")`, `eel.expose(func_name)`, `eel.expose((function (e){}), "func_name")` -EXPOSED_JS_FUNCTIONS = pp.ZeroOrMore( +EXPOSED_JS_FUNCTIONS: pp.ZeroOrMore = pp.ZeroOrMore( pp.Suppress( pp.SkipTo(pp.Literal('eel.expose(')) + pp.Literal('eel.expose(') @@ -101,8 +110,8 @@ def decorator(function): ) -def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm', - '.xhtml', '.vue'], js_result_timeout=10000): +def init(path: str, allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm', + '.xhtml', '.vue'], js_result_timeout: int = 10000) -> None: global root_path, _js_functions, _js_result_timeout root_path = _get_real_path(path) @@ -133,7 +142,7 @@ def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm', _js_result_timeout = js_result_timeout -def start(*start_urls, **kwargs): +def start(*start_urls: str, **kwargs: Any) -> None: _start_args.update(kwargs) if 'options' in kwargs: @@ -150,6 +159,8 @@ def start(*start_urls, **kwargs): if _start_args['jinja_templates'] != None: from jinja2 import Environment, FileSystemLoader, select_autoescape + if not isinstance(_start_args['jinja_templates'], str): + raise TypeError("'jinja_templates start_arg/option must be of type str'") templates_path = os.path.join(root_path, _start_args['jinja_templates']) _start_args['jinja_env'] = Environment(loader=FileSystemLoader(templates_path), autoescape=select_autoescape(['html', 'xml'])) @@ -162,25 +173,27 @@ def start(*start_urls, **kwargs): # Launch the browser to the starting URLs show(*start_urls) - def run_lambda(): + def run_lambda() -> None: if _start_args['all_interfaces'] == True: HOST = '0.0.0.0' else: + if not isinstance(_start_args['host'], str): + raise TypeError("'host' start_arg/option must be of type str") HOST = _start_args['host'] - app = _start_args['app'] # type: btl.Bottle + app = _start_args['app'] if isinstance(app, btl.Bottle): register_eel_routes(app) else: register_eel_routes(btl.default_app()) - return btl.run( + btl.run( host=HOST, port=_start_args['port'], server=wbs.GeventWebSocketServer, quiet=True, - app=app) + app=app) # Always returns None # Start the webserver if _start_args['block']: @@ -189,20 +202,20 @@ def run_lambda(): spawn(run_lambda) -def show(*start_urls): - brw.open(start_urls, _start_args) +def show(*start_urls: str) -> None: + brw.open(list(start_urls), _start_args) -def sleep(seconds): +def sleep(seconds: Union[int, float]) -> None: gvt.sleep(seconds) -def spawn(function, *args, **kwargs): +def spawn(function: Callable[..., Any], *args: Any, **kwargs: Any) -> gvt.Greenlet: return gvt.spawn(function, *args, **kwargs) # Bottle Routes -def _eel(): +def _eel() -> str: start_geometry = {'default': {'size': _start_args['size'], 'position': _start_args['position']}, 'pages': _start_args['geometry']} @@ -215,16 +228,20 @@ def _eel(): _set_response_headers(btl.response) return page -def _root(): +def _root() -> Optional[btl.Response]: + if not isinstance(_start_args['default_path'], str): + raise TypeError("'default_path' start_arg/option must be of type str") return _static(_start_args['default_path']) -def _static(path): +def _static(path: str) -> Optional[btl.Response]: response = None if 'jinja_env' in _start_args and 'jinja_templates' in _start_args: + if not isinstance(_start_args['jinja_templates'], str): + raise TypeError("'jinja_templates' start_arg/option must be of type str") template_prefix = _start_args['jinja_templates'] + '/' if path.startswith(template_prefix): n = len(template_prefix) - template = _start_args['jinja_env'].get_template(path[n:]) + template = _start_args['jinja_env'].get_template(path[n:]) # type: ignore # depends on conditional import in start() response = btl.HTTPResponse(template.render()) if response is None: @@ -233,7 +250,7 @@ def _static(path): _set_response_headers(response) return response -def _websocket(ws): +def _websocket(ws: WebSocketT) -> None: global _websockets for js_function in _js_functions: @@ -259,14 +276,14 @@ def _websocket(ws): _websocket_close(page) -BOTTLE_ROUTES = { +BOTTLE_ROUTES: Dict[str, Tuple[Callable[..., Any], Dict[Any, Any]]] = { "/eel.js": (_eel, dict()), "/": (_root, dict()), "/": (_static, dict()), "/eel": (_websocket, dict(apply=[wbs.websocket])) } -def register_eel_routes(app): +def register_eel_routes(app: btl.Bottle) -> None: ''' Adds eel routes to `app`. Only needed if you are passing something besides `bottle.Bottle` to `eel.start()`. Ex: @@ -281,11 +298,11 @@ def register_eel_routes(app): # Private functions -def _safe_json(obj): +def _safe_json(obj: Any) -> str: return jsn.dumps(obj, default=lambda o: None) -def _repeated_send(ws, msg): +def _repeated_send(ws: WebSocketT, msg: str) -> None: for attempt in range(100): try: ws.send(msg) @@ -294,7 +311,7 @@ def _repeated_send(ws, msg): sleep(0.001) -def _process_message(message, ws): +def _process_message(message: Dict[str, Any], ws: WebSocketT) -> None: if 'call' in message: error_info = {} try: @@ -326,47 +343,48 @@ def _process_message(message, ws): print('Invalid message received: ', message) -def _get_real_path(path): +def _get_real_path(path: str) -> str: if getattr(sys, 'frozen', False): - return os.path.join(sys._MEIPASS, path) + return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstaller else: return os.path.abspath(path) -def _mock_js_function(f): +def _mock_js_function(f: str) -> None: exec('%s = lambda *args: _mock_call("%s", args)' % (f, f), globals()) -def _import_js_function(f): +def _import_js_function(f: str) -> None: exec('%s = lambda *args: _js_call("%s", args)' % (f, f), globals()) -def _call_object(name, args): +def _call_object(name: str, args: Any) -> Dict[str, Any]: global _call_number _call_number += 1 call_id = _call_number + rnd.random() return {'call': call_id, 'name': name, 'args': args} -def _mock_call(name, args): +def _mock_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: call_object = _call_object(name, args) global _mock_queue _mock_queue += [call_object] return _call_return(call_object) -def _js_call(name, args): +def _js_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: call_object = _call_object(name, args) for _, ws in _websockets: _repeated_send(ws, _safe_json(call_object)) return _call_return(call_object) -def _call_return(call): +def _call_return(call: Dict[str, Any]) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: global _js_result_timeout call_id = call['call'] - def return_func(callback=None, error_callback=None): + def return_func(callback: Optional[Callable[..., Any]] = None, + error_callback: Optional[Callable[..., Any]] = None) -> Any: if callback is not None: _call_return_callbacks[call_id] = (callback, error_callback) else: @@ -377,33 +395,35 @@ def return_func(callback=None, error_callback=None): return return_func -def _expose(name, function): +def _expose(name: str, function: Callable[..., Any]) -> None: msg = 'Already exposed function with name "%s"' % name assert name not in _exposed_functions, msg _exposed_functions[name] = function -def _detect_shutdown(): +def _detect_shutdown() -> None: if len(_websockets) == 0: sys.exit() -def _websocket_close(page): +def _websocket_close(page: str) -> None: global _shutdown close_callback = _start_args.get('close_callback') if close_callback is not None: + if not callable(close_callback): + raise TypeError("'close_callback' start_arg/option must be callable or None") sockets = [p for _, p in _websockets] close_callback(page, sockets) else: - if _shutdown: + if isinstance(_shutdown, gvt.Greenlet): _shutdown.kill() _shutdown = gvt.spawn_later(_start_args['shutdown_delay'], _detect_shutdown) -def _set_response_headers(response): +def _set_response_headers(response: btl.Response) -> None: if _start_args['disable_cache']: # https://stackoverflow.com/a/24748094/280852 response.set_header('Cache-Control', 'no-store') diff --git a/eel/__main__.py b/eel/__main__.py index f5a81601..b4027eb6 100644 --- a/eel/__main__.py +++ b/eel/__main__.py @@ -1,9 +1,10 @@ import pkg_resources as pkg import PyInstaller.__main__ as pyi import os -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace +from typing import List -parser = ArgumentParser(description=""" +parser: ArgumentParser = ArgumentParser(description=""" Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps, with full access to Python capabilities and libraries. """) @@ -17,20 +18,22 @@ type=str, help="Folder including all web files including file as html, css, ico, etc." ) +args: Namespace +unknown_args: List[str] args, unknown_args = parser.parse_known_args() -main_script = args.main_script -web_folder = args.web_folder +main_script: str = args.main_script +web_folder: str = args.web_folder print("Building executable with main script '%s' and web folder '%s'...\n" % (main_script, web_folder)) -eel_js_file = pkg.resource_filename('eel', 'eel.js') -js_file_arg = '%s%seel' % (eel_js_file, os.pathsep) -web_folder_arg = '%s%s%s' % (web_folder, os.pathsep, web_folder) +eel_js_file: str = pkg.resource_filename('eel', 'eel.js') +js_file_arg: str = '%s%seel' % (eel_js_file, os.pathsep) +web_folder_arg: str = '%s%s%s' % (web_folder, os.pathsep, web_folder) -needed_args = ['--hidden-import', 'bottle_websocket', - '--add-data', js_file_arg, '--add-data', web_folder_arg] -full_args = [main_script] + needed_args + unknown_args +needed_args: List[str] = ['--hidden-import', 'bottle_websocket', + '--add-data', js_file_arg, '--add-data', web_folder_arg] +full_args: List[str] = [main_script] + needed_args + unknown_args print('Running:\npyinstaller', ' '.join(full_args), '\n') pyi.run(full_args) diff --git a/eel/browsers.py b/eel/browsers.py index 79639141..d555b2d9 100644 --- a/eel/browsers.py +++ b/eel/browsers.py @@ -1,53 +1,65 @@ import subprocess as sps import webbrowser as wbr +from typing import Union, List, Dict, Iterable, Optional +from types import ModuleType +from eel.types import OptionsDictT import eel.chrome as chm import eel.electron as ele import eel.edge as edge #import eel.firefox as ffx TODO #import eel.safari as saf TODO -_browser_paths = {} -_browser_modules = {'chrome': chm, - 'electron': ele, - 'edge': edge} +_browser_paths: Dict[str, str] = {} +_browser_modules: Dict[str, ModuleType] = {'chrome': chm, + 'electron': ele, + 'edge': edge} -def _build_url_from_dict(page, options): +def _build_url_from_dict(page: Dict[str, str], options: OptionsDictT) -> str: scheme = page.get('scheme', 'http') host = page.get('host', 'localhost') port = page.get('port', options["port"]) path = page.get('path', '') - return '%s://%s:%d/%s' % (scheme, host, port, path) + if not isinstance(port, (int, str)): + raise TypeError("'port' option must be an integer") + return '%s://%s:%d/%s' % (scheme, host, int(port), path) -def _build_url_from_string(page, options): - base_url = 'http://%s:%d/' % (options['host'], options['port']) +def _build_url_from_string(page: str, options: OptionsDictT) -> str: + if not isinstance(options['port'], (int, str)): + raise TypeError("'port' option must be an integer") + base_url = 'http://%s:%d/' % (options['host'], int(options['port'])) return base_url + page -def _build_urls(start_pages, options): - urls = [] +def _build_urls(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDictT) -> List[str]: + urls: List[str] = [] for page in start_pages: - method = _build_url_from_dict if isinstance( - page, dict) else _build_url_from_string - url = method(page, options) + if isinstance(page, dict): + url = _build_url_from_dict(page, options) + else: + url = _build_url_from_string(page, options) urls.append(url) return urls -def open(start_pages, options): +def open(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDictT) -> None: # Build full URLs for starting pages (including host and port) start_urls = _build_urls(start_pages, options) mode = options.get('mode') - if mode in [None, False]: + if not isinstance(mode, (str, bool, type(None))) or mode is True: + raise TypeError("'mode' option must by either a string, False, or None") + if mode is None or mode is False: # Don't open a browser pass elif mode == 'custom': # Just run whatever command the user provided + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") sps.Popen(options['cmdline_args'], stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE) elif mode in _browser_modules: @@ -69,10 +81,10 @@ def open(start_pages, options): wbr.open(url) -def set_path(browser_name, path): +def set_path(browser_name: str, path: str) -> None: _browser_paths[browser_name] = path -def get_path(browser_name): +def get_path(browser_name: str) -> Optional[str]: return _browser_paths.get(browser_name) diff --git a/eel/chrome.py b/eel/chrome.py index f827b20b..c07356f0 100644 --- a/eel/chrome.py +++ b/eel/chrome.py @@ -1,22 +1,27 @@ import sys, subprocess as sps, os +from typing import List, Optional + +from eel.types import OptionsDictT # Every browser specific module must define run(), find_path() and name like this -name = 'Google Chrome/Chromium' +name: str = 'Google Chrome/Chromium' -def run(path, options, start_urls): +def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") if options['app_mode']: for url in start_urls: sps.Popen([path, '--app=%s' % url] + options['cmdline_args'], stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE) else: - args = options['cmdline_args'] + start_urls + args: List[str] = options['cmdline_args'] + start_urls sps.Popen([path, '--new-window'] + args, stdout=sps.PIPE, stderr=sys.stderr, stdin=sps.PIPE) -def find_path(): +def find_path() -> Optional[str]: if sys.platform in ['win32', 'win64']: return _find_chrome_win() elif sys.platform == 'darwin': @@ -27,7 +32,7 @@ def find_path(): return None -def _find_chrome_mac(): +def _find_chrome_mac() -> Optional[str]: default_dir = r'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' if os.path.exists(default_dir): return default_dir @@ -39,7 +44,7 @@ def _find_chrome_mac(): return None -def _find_chromium_mac(): +def _find_chromium_mac() -> Optional[str]: default_dir = r'/Applications/Chromium.app/Contents/MacOS/Chromium' if os.path.exists(default_dir): return default_dir @@ -51,7 +56,7 @@ def _find_chromium_mac(): return None -def _find_chrome_linux(): +def _find_chrome_linux() -> Optional[str]: import whichcraft as wch chrome_names = ['chromium-browser', 'chromium', @@ -61,13 +66,14 @@ def _find_chrome_linux(): for name in chrome_names: chrome = wch.which(name) if chrome is not None: - return chrome + return chrome # type: ignore # whichcraft doesn't currently have type hints return None -def _find_chrome_win(): +def _find_chrome_win() -> Optional[str]: import winreg as reg reg_path = r'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' + chrome_path: Optional[str] = None for install_type in reg.HKEY_CURRENT_USER, reg.HKEY_LOCAL_MACHINE: try: diff --git a/eel/edge.py b/eel/edge.py index cef818aa..7f2dab1e 100644 --- a/eel/edge.py +++ b/eel/edge.py @@ -1,16 +1,19 @@ import platform import subprocess as sps import sys +from typing import List -name = 'Edge' +from eel.types import OptionsDictT +name: str = 'Edge' -def run(_path, options, start_urls): + +def run(_path: str, options: OptionsDictT, start_urls: List[str]) -> None: cmd = 'start microsoft-edge:{}'.format(start_urls[0]) sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True) -def find_path(): +def find_path() -> bool: if platform.system() == 'Windows': return True diff --git a/eel/electron.py b/eel/electron.py index 7a443025..14cbc802 100644 --- a/eel/electron.py +++ b/eel/electron.py @@ -1,24 +1,30 @@ +#from __future__ import annotations import sys import os import subprocess as sps import whichcraft as wch +from typing import List, Optional -name = 'Electron' +from eel.types import OptionsDictT -def run(path, options, start_urls): +name: str = 'Electron' + +def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: + if not isinstance(options['cmdline_args'], list): + raise TypeError("'cmdline_args' option must be of type List[str]") cmd = [path] + options['cmdline_args'] cmd += ['.', ';'.join(start_urls)] sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE) -def find_path(): +def find_path() -> Optional[str]: if sys.platform in ['win32', 'win64']: # It doesn't work well passing the .bat file to Popen, so we get the actual .exe bat_path = wch.which('electron') return os.path.join(bat_path, r'..\node_modules\electron\dist\electron.exe') elif sys.platform in ['darwin', 'linux']: # This should work find... - return wch.which('electron') + return wch.which('electron') # type: ignore # whichcraft doesn't currently have type hints else: return None diff --git a/eel/py.typed b/eel/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/eel/types.py b/eel/types.py new file mode 100644 index 00000000..55475816 --- /dev/null +++ b/eel/types.py @@ -0,0 +1,28 @@ +from typing import Union, Dict, List, Tuple, Callable, Optional, Any, TYPE_CHECKING + +# This business is slightly awkward, but needed for backward compatibility, +# because Python < 3.7 doesn't have __future__/annotations, and <3.10 doesn't +# support TypeAlias. +if TYPE_CHECKING: + from jinja2 import Environment + try: + from typing import TypeAlias # Introduced in Python 3.10 + JinjaEnvironmentT: TypeAlias = Environment + except ImportError: + JinjaEnvironmentT = Environment # type: ignore + from geventwebsocket.websocket import WebSocket + WebSocketT = WebSocket +else: + JinjaEnvironmentT = None + WebSocketT = Any + +OptionsDictT = Dict[ + str, + Optional[ + Union[ + str, bool, int, float, + List[str], Tuple[int, int], Dict[str, Tuple[int, int]], + Callable[..., Any], JinjaEnvironmentT + ] + ] + ] diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..ba16833d --- /dev/null +++ b/mypy.ini @@ -0,0 +1,36 @@ +[mypy] +python_version = 3.10 +warn_unused_configs = True + +[mypy-jinja2] +ignore_missing_imports = True + +[mypy-gevent] +ignore_missing_imports = True + +[mypy-gevent.threading] +ignore_missing_imports = True + +[mypy-geventwebsocket.websocket] +ignore_missing_imports = True + +[mypy-bottle] +ignore_missing_imports = True + +[mypy-bottle.ext] +ignore_missing_imports = True + +[mypy-bottle.ext.websocket] +ignore_missing_imports = True + +[mypy-whichcraft] +ignore_missing_imports = True + +[mypy-pyparsing] +ignore_missing_imports = True + +[mypy-PyInstaller] +ignore_missing_imports = True + +[mypy-PyInstaller.__main__] +ignore_missing_imports = True diff --git a/requirements-meta.txt b/requirements-meta.txt index 0ebe9256..f4d054a7 100644 --- a/requirements-meta.txt +++ b/requirements-meta.txt @@ -1,4 +1,4 @@ tox>=3.15.2,<4.0.0 tox-pyenv==1.1.0 -tox-gh-actions==1.3.0 +tox-gh-actions==2.0.0 virtualenv>=16.7.10 diff --git a/requirements-test.txt b/requirements-test.txt index 0a9d9728..9d145d8c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,4 +4,7 @@ psutil==5.9.2 pytest==7.0.1 pytest-timeout==2.1.0 selenium==3.141.0 -webdriver_manager==3.7.1 \ No newline at end of file +webdriver_manager==3.7.1 +mypy==0.971 +pyinstaller==4.10 +types-setuptools==67.2.0.1 diff --git a/setup.py b/setup.py index ec65c93d..7df8e22e 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ url='https://github.com/python-eel/Eel', packages=['eel'], package_data={ - 'eel': ['eel.js'], + 'eel': ['eel.js', 'py.typed'], }, install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'whichcraft'], extras_require={ diff --git a/tox.ini b/tox.ini index fefe2470..2d292bbf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,py39,py310 +envlist = typecheck,py{36,37,38,39,310} [pytest] timeout = 30 @@ -13,9 +13,16 @@ python = 3.10: py310 [testenv] +description = run py.test tests deps = -r requirements-test.txt commands = # this ugly hack is here because: # https://github.com/tox-dev/tox/issues/149 pip install -q -r '{toxinidir}'/requirements-test.txt - '{envpython}' -m pytest {posargs} \ No newline at end of file + '{envpython}' -m pytest {posargs} + +[testenv:typecheck] +description = run type checks +deps = -r requirements-test.txt +commands = + mypy --strict {posargs:eel}