From 7c3f6ad46591c70832b9af073f5422821485602d Mon Sep 17 00:00:00 2001 From: Florian Breit Date: Tue, 19 Nov 2024 21:09:03 +0000 Subject: [PATCH] Refined type annotations to reflect move to python>=3.7 (#683) Co-authored-by: Samuel Williams --- eel/__init__.py | 332 ++++++++++++++++++++----- eel/__main__.py | 1 + eel/browsers.py | 3 +- eel/chrome.py | 12 +- eel/edge.py | 1 + eel/electron.py | 17 +- eel/types.py | 54 ++-- examples/07 - CreateReactApp/README.md | 2 +- mypy.ini | 9 - requirements.txt | 2 +- setup.py | 2 +- 11 files changed, 330 insertions(+), 105 deletions(-) diff --git a/eel/__init__.py b/eel/__init__.py index 0dfa8864..e1ab255b 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -1,15 +1,10 @@ +from __future__ import annotations 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 +from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable +from typing_extensions import Literal +from eel.types import OptionsDictT, WebSocketT import gevent as gvt import json as jsn import bottle as btl @@ -46,28 +41,10 @@ # Can be overridden through `eel.init` with the kwarg `js_result_timeout` (default: 10000) _js_result_timeout: int = 10000 -# All start() options must provide a default value and explanation here -_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) - 'block': True, # Whether start() blocks calling thread - 'jinja_templates': None, # Folder for jinja2 templates - 'cmdline_args': ['--disable-http-cache'], # Extra cmdline flags to pass to browser start - 'size': None, # (width, height) of main window - 'position': None, # (left, top) of main window - 'geometry': {}, # Dictionary of size/position for all windows - 'close_callback': None, # Callback for when all windows have closed - 'app_mode': True, # (Chrome specific option) - 'all_interfaces': False, # Allow bottle server to listen for connections on all interfaces - '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 -} +# Attribute holding the start args from calls to eel.start() +_start_args: OptionsDictT = {} # == Temporary (suppressible) error message to inform users of breaking API change for v1.0.0 === -_start_args['suppress_error'] = False api_error_message: str = ''' ---------------------------------------------------------------------------------- 'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel @@ -77,9 +54,45 @@ ''' # =============================================================================================== + # Public functions + def expose(name_or_function: Optional[Callable[..., Any]] = None) -> Callable[..., Any]: + '''Decorator to expose Python callables via Eel's JavaScript API. + + When an exposed function is called, a callback function can be passed + immediately afterwards. This callback will be called asynchronously with + the return value (possibly `None`) when the Python function has finished + executing. + + Blocking calls to the exposed function from the JavaScript side are only + possible using the :code:`await` keyword inside an :code:`async function`. + These still have to make a call to the response, i.e. + :code:`await eel.py_random()();` inside an :code:`async function` will work, + but just :code:`await eel.py_random();` will not. + + :Example: + + In Python do: + + .. code-block:: python + + @expose + def say_hello_py(name: str = 'You') -> None: + print(f'{name} said hello from the JavaScript world!') + + In JavaScript do: + + .. code-block:: javascript + + eel.say_hello_py('Alice')(); + + Expected output on the Python console:: + + Alice said hello from the JavaScript world! + + ''' # Deal with '@eel.expose()' - treat as '@eel.expose' if name_or_function is None: return expose @@ -113,8 +126,28 @@ def decorator(function: Callable[..., Any]) -> Any: ) -def init(path: str, allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm', - '.xhtml', '.vue'], js_result_timeout: int = 10000) -> None: +def init( + path: str, + allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm', '.xhtml', '.vue'], + js_result_timeout: int = 10000) -> None: + '''Initialise Eel. + + This function should be called before :func:`start()` to initialise the + parameters for the web interface, such as the path to the files to be + served. + + :param path: Sets the path on the filesystem where files to be served to + the browser are located, e.g. :file:`web`. + :param allowed_extensions: A list of filename extensions which will be + parsed for exposed eel functions which should be callable from python. + Files with extensions not in *allowed_extensions* will still be served, + but any JavaScript functions, even if marked as exposed, will not be + accessible from python. + *Default:* :code:`['.js', '.html', '.txt', '.htm', '.xhtml', '.vue']`. + :param js_result_timeout: How long Eel should be waiting to register the + results from a call to Eel's JavaScript API before before timing out. + *Default:* :code:`10000` milliseconds. + ''' global root_path, _js_functions, _js_result_timeout root_path = _get_real_path(path) @@ -145,14 +178,117 @@ def init(path: str, allowed_extensions: List[str] = ['.js', '.html', '.txt', '.h _js_result_timeout = js_result_timeout -def start(*start_urls: str, **kwargs: Any) -> None: - _start_args.update(kwargs) - - if 'options' in kwargs: - if _start_args['suppress_error']: - _start_args.update(kwargs['options']) - else: - raise RuntimeError(api_error_message) +def start( + *start_urls: str, + mode: Optional[Union[str, Literal[False]]] = 'chrome', + host: str = 'localhost', + port: int = 8000, + block: bool = True, + jinja_templates: Optional[str] = None, + cmdline_args: List[str] = ['--disable-http-cache'], + size: Optional[Tuple[int, int]] = None, + position: Optional[Tuple[int, int]] = None, + geometry: Dict[str, Tuple[int, int]] = {}, + close_callback: Optional[Callable[..., Any]] = None, + app_mode: bool = True, + all_interfaces: bool = False, + disable_cache: bool = True, + default_path: str = 'index.html', + app: btl.Bottle = btl.default_app(), + shutdown_delay: float = 1.0, + suppress_error: bool = False) -> None: + '''Start the Eel app. + + Suppose you put all the frontend files in a directory called + :file:`web`, including your start page :file:`main.html`, then the app + is started like this: + + .. code-block:: python + + import eel + eel.init('web') + eel.start('main.html') + + This will start a webserver on the default settings + (http://localhost:8000) and open a browser to + http://localhost:8000/main.html. + + If Chrome or Chromium is installed then by default it will open 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). + + :param mode: What browser is used, e.g. :code:`'chrome'`, + :code:`'electron'`, :code:`'edge'`, :code:`'custom'`. Can also be + `None` or `False` to not open a window. *Default:* :code:`'chrome'`. + :param host: Hostname used for Bottle server. *Default:* + :code:`'localhost'`. + :param port: Port used for Bottle server. Use :code:`0` for port to be + picked automatically. *Default:* :code:`8000`. + :param block: Whether the call to :func:`start()` blocks the calling + thread. *Default:* `True`. + :param jinja_templates: Folder for :mod:`jinja2` templates, e.g. + :file:`my_templates`. *Default:* `None`. + :param cmdline_args: A list of strings to pass to the command starting the + browser. For example, we might add extra flags to Chrome with + :code:`eel.start('main.html', mode='chrome-app', port=8080, + cmdline_args=['--start-fullscreen', '--browser-startup-dialog'])`. + *Default:* :code:`[]`. + :param size: Tuple specifying the (width, height) of the main window in + pixels. *Default:* `None`. + :param position: Tuple specifying the (left, top) position of the main + window in pixels. *Default*: `None`. + :param geometry: A dictionary of specifying the size/position for all + windows. The keys should be the relative path of the page, and the + values should be a dictionary of the form + :code:`{'size': (200, 100), 'position': (300, 50)}`. *Default:* + :code:`{}`. + :param close_callback: A lambda or function that is called when a websocket + or 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 the other websockets that are still open. + *Default:* `None`. + :param app_mode: Whether to run Chrome/Edge in App Mode. You can also + specify *mode* as :code:`mode='chrome-app'` as a shorthand to start + Chrome in App Mode. + :param all_interfaces: Whether to allow the :mod:`bottle` server to listen + for connections on all interfaces. + :param disable_cache: Sets the no-store response header when serving + assets. + :param default_path: The default file to retrieve for the root URL. + :param app: An instance of :class:`bottle.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 *app* is not a :class:`bottle.Bottle` instance, + you will need to call :code:`eel.register_eel_routes(app)` on your + custom app instance. + :param 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. *Default:* :code:`1.0` seconds. + :param suppress_error: Temporary (suppressible) error message to inform + users of breaking API change for v1.0.0. Set to `True` to suppress + the error message. + ''' + _start_args.update({ + 'mode': mode, + 'host': host, + 'port': port, + 'block': block, + 'jinja_templates': jinja_templates, + 'cmdline_args': cmdline_args, + 'size': size, + 'position': position, + 'geometry': geometry, + 'close_callback': close_callback, + 'app_mode': app_mode, + 'all_interfaces': all_interfaces, + 'disable_cache': disable_cache, + 'default_path': default_path, + 'app': app, + 'shutdown_delay': shutdown_delay, + 'suppress_error': suppress_error, + }) if _start_args['port'] == 0: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -160,24 +296,28 @@ def start(*start_urls: str, **kwargs: Any) -> None: _start_args['port'] = sock.getsockname()[1] sock.close() - if _start_args['jinja_templates'] != None: + if _start_args['jinja_templates'] is not 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'") + 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'])) + _start_args['jinja_env'] = Environment( + loader=FileSystemLoader(templates_path), + autoescape=select_autoescape(['html', 'xml']) + ) # verify shutdown_delay is correct value if not isinstance(_start_args['shutdown_delay'], (int, float)): - raise ValueError("`shutdown_delay` must be a number, "\ - "got a {}".format(type(_start_args['shutdown_delay']))) + raise ValueError( + '`shutdown_delay` must be a number, ' + 'got a {}'.format(type(_start_args['shutdown_delay'])) + ) # Launch the browser to the starting URLs show(*start_urls) def run_lambda() -> None: - if _start_args['all_interfaces'] == True: + if _start_args['all_interfaces'] is True: HOST = '0.0.0.0' else: if not isinstance(_start_args['host'], str): @@ -196,7 +336,7 @@ def run_lambda() -> None: port=_start_args['port'], server=wbs.GeventWebSocketServer, quiet=True, - app=app) # Always returns None + app=app) # Always returns None # Start the webserver if _start_args['block']: @@ -206,18 +346,78 @@ def run_lambda() -> None: def show(*start_urls: str) -> None: + '''Show the specified URL(s) in the browser. + + Suppose you have two files in your :file:`web` folder. The file + :file:`hello.html` regularly includes :file:`eel.js` and provides + interactivity, and the file :file:`goodbye.html` does not include + :file:`eel.js` and simply provides plain HTML content not reliant on Eel. + + First, we defien a callback function to be called when the browser + window is closed: + + .. code-block:: python + + def last_calls(): + eel.show('goodbye.html') + + Now we initialise and start Eel, with a :code:`close_callback` to our + function: + + ..code-block:: python + + eel.init('web') + eel.start('hello.html', mode='chrome-app', close_callback=last_calls) + + When the websocket from :file:`hello.html` is closed (e.g. because the + user closed the browser window), Eel will wait *shutdown_delay* seconds + (by default 1 second), then call our :code:`last_calls()` function, which + opens another window with the :file:`goodbye.html` shown before our Eel app + terminates. + + :param start_urls: One or more URLs to be opened. + ''' brw.open(list(start_urls), _start_args) def sleep(seconds: Union[int, float]) -> None: + '''A non-blocking sleep call compatible with the Gevent event loop. + + .. note:: + While this function simply wraps :func:`gevent.sleep()`, it is better + to call :func:`eel.sleep()` in your eel app, as this will ensure future + compatibility in case the implementation of Eel should change in some + respect. + + :param seconds: The number of seconds to sleep. + ''' gvt.sleep(seconds) def spawn(function: Callable[..., Any], *args: Any, **kwargs: Any) -> gvt.Greenlet: + '''Spawn a new Greenlet. + + Calling this function will spawn a new :class:`gevent.Greenlet` running + *function* asynchronously. + + .. caution:: + If you spawn your own Greenlets to run in addition to those spawned by + Eel's internal core functionality, you will have to ensure that those + Greenlets will terminate as appropriate (either by returning or by + being killed via Gevent's kill mechanism), otherwise your app may not + terminate correctly when Eel itself terminates. + + :param function: The function to be called and run as the Greenlet. + :param *args: Any positional arguments that should be passed to *function*. + :param **kwargs: Any key-word arguments that should be passed to + *function*. + ''' return gvt.spawn(function, *args, **kwargs) + # Bottle Routes + def _eel() -> str: start_geometry = {'default': {'size': _start_args['size'], 'position': _start_args['position']}, @@ -231,12 +431,14 @@ def _eel() -> str: _set_response_headers(btl.response) return page -def _root() -> Optional[btl.Response]: + +def _root() -> 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.Response]: + +def _static(path: str) -> btl.Response: response = None if 'jinja_env' in _start_args and 'jinja_templates' in _start_args: if not isinstance(_start_args['jinja_templates'], str): @@ -244,7 +446,7 @@ def _static(path: str) -> Optional[btl.Response]: template_prefix = _start_args['jinja_templates'] + '/' if path.startswith(template_prefix): n = len(template_prefix) - template = _start_args['jinja_env'].get_template(path[n:]) # type: ignore # depends on conditional import in start() + template = _start_args['jinja_env'].get_template(path[n:]) response = btl.HTTPResponse(template.render()) if response is None: @@ -253,6 +455,7 @@ def _static(path: str) -> Optional[btl.Response]: _set_response_headers(response) return response + def _websocket(ws: WebSocketT) -> None: global _websockets @@ -286,21 +489,34 @@ def _websocket(ws: WebSocketT) -> None: "/eel": (_websocket, dict(apply=[wbs.websocket])) } + 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: - app = bottle.Bottle() - eel.register_eel_routes(app) - middleware = beaker.middleware.SessionMiddleware(app) - eel.start(app=middleware) + '''Register the required eel routes with `app`. + + .. note:: + + :func:`eel.register_eel_routes()` is normally invoked implicitly by + :func:`eel.start()` and does not need to be called explicitly in most + cases. Registering the eel routes explicitly is only needed if you are + passing something other than an instance of :class:`bottle.Bottle` to + :func:`eel.start()`. + + :Example: + + >>> app = bottle.Bottle() + >>> eel.register_eel_routes(app) + >>> middleware = beaker.middleware.SessionMiddleware(app) + >>> eel.start(app=middleware) + ''' for route_path, route_params in BOTTLE_ROUTES.items(): route_func, route_kwargs = route_params app.route(path=route_path, callback=route_func, **route_kwargs) + # Private functions + def _safe_json(obj: Any) -> str: return jsn.dumps(obj, default=lambda o: None) @@ -348,7 +564,7 @@ def _process_message(message: Dict[str, Any], ws: WebSocketT) -> None: def _get_real_path(path: str) -> str: if getattr(sys, 'frozen', False): - return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstaller + return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstaller else: return os.path.abspath(path) diff --git a/eel/__main__.py b/eel/__main__.py index b4027eb6..74910995 100644 --- a/eel/__main__.py +++ b/eel/__main__.py @@ -1,3 +1,4 @@ +from __future__ import annotations import pkg_resources as pkg import PyInstaller.__main__ as pyi import os diff --git a/eel/browsers.py b/eel/browsers.py index 89b040e9..183dd905 100644 --- a/eel/browsers.py +++ b/eel/browsers.py @@ -1,3 +1,4 @@ +from __future__ import annotations import subprocess as sps import webbrowser as wbr from typing import Union, List, Dict, Iterable, Optional @@ -53,7 +54,7 @@ def open(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDict start_urls = _build_urls(start_pages, options) mode = options.get('mode') - if not isinstance(mode, (str, bool, type(None))) or mode is True: + if not isinstance(mode, (str, type(None))) and mode is not False: 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 diff --git a/eel/chrome.py b/eel/chrome.py index c07356f0..a8112f5d 100644 --- a/eel/chrome.py +++ b/eel/chrome.py @@ -1,6 +1,9 @@ -import sys, subprocess as sps, os +from __future__ import annotations +import sys +import os +import subprocess as sps +from shutil import which from typing import List, Optional - from eel.types import OptionsDictT # Every browser specific module must define run(), find_path() and name like this @@ -57,16 +60,15 @@ def _find_chromium_mac() -> Optional[str]: def _find_chrome_linux() -> Optional[str]: - import whichcraft as wch chrome_names = ['chromium-browser', 'chromium', 'google-chrome', 'google-chrome-stable'] for name in chrome_names: - chrome = wch.which(name) + chrome = which(name) if chrome is not None: - return chrome # type: ignore # whichcraft doesn't currently have type hints + return chrome return None diff --git a/eel/edge.py b/eel/edge.py index cea91894..7d785233 100644 --- a/eel/edge.py +++ b/eel/edge.py @@ -1,3 +1,4 @@ +from __future__ import annotations import platform import subprocess as sps import sys diff --git a/eel/electron.py b/eel/electron.py index 14cbc802..4ff8fcc1 100644 --- a/eel/electron.py +++ b/eel/electron.py @@ -1,8 +1,8 @@ -#from __future__ import annotations +from __future__ import annotations import sys import os import subprocess as sps -import whichcraft as wch +from shutil import which from typing import List, Optional from eel.types import OptionsDictT @@ -20,11 +20,10 @@ def run(path: str, options: OptionsDictT, start_urls: List[str]) -> None: 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') + bat_path = which('electron') + if bat_path: + 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') # type: ignore # whichcraft doesn't currently have type hints - else: - return None - + # This should work fine... + return which('electron') + return None diff --git a/eel/types.py b/eel/types.py index 55475816..0cd2d6ee 100644 --- a/eel/types.py +++ b/eel/types.py @@ -1,28 +1,42 @@ +from __future__ import annotations from typing import Union, Dict, List, Tuple, Callable, Optional, Any, TYPE_CHECKING +from typing_extensions import Literal, TypedDict, TypeAlias +from bottle import Bottle # 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. +# because Python <3.10 doesn't support TypeAlias, jinja2 may not be available +# at runtime, and geventwebsocket.websocket doesn't have type annotations so +# that direct imports will raise an error. 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 + JinjaEnvironmentT: TypeAlias = Environment from geventwebsocket.websocket import WebSocket - WebSocketT = WebSocket + WebSocketT: TypeAlias = WebSocket else: - JinjaEnvironmentT = None - WebSocketT = Any + JinjaEnvironmentT: TypeAlias = Any + WebSocketT: TypeAlias = Any -OptionsDictT = Dict[ - str, - Optional[ - Union[ - str, bool, int, float, - List[str], Tuple[int, int], Dict[str, Tuple[int, int]], - Callable[..., Any], JinjaEnvironmentT - ] - ] - ] +OptionsDictT = TypedDict( + 'OptionsDictT', + { + 'mode': Optional[Union[str, Literal[False]]], + 'host': str, + 'port': int, + 'block': bool, + 'jinja_templates': Optional[str], + 'cmdline_args': List[str], + 'size': Optional[Tuple[int, int]], + 'position': Optional[Tuple[int, int]], + 'geometry': Dict[str, Tuple[int, int]], + 'close_callback': Optional[Callable[..., Any]], + 'app_mode': bool, + 'all_interfaces': bool, + 'disable_cache': bool, + 'default_path': str, + 'app': Bottle, + 'shutdown_delay': float, + 'suppress_error': bool, + 'jinja_env': JinjaEnvironmentT, + }, + total=False +) diff --git a/examples/07 - CreateReactApp/README.md b/examples/07 - CreateReactApp/README.md index f2b0f249..3f6a85b6 100644 --- a/examples/07 - CreateReactApp/README.md +++ b/examples/07 - CreateReactApp/README.md @@ -19,7 +19,7 @@ If you run into any issues with this example, open a [new issue](https://github. ## Quick Start -1. **Configure:** In the app's directory, run `npm install` and `pip install bottle bottle-websocket future whichcraft pyinstaller` +1. **Configure:** In the app's directory, run `npm install` and `pip install bottle bottle-websocket future pyinstaller` 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/` 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)) 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. diff --git a/mypy.ini b/mypy.ini index 6fa89652..4be68351 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,9 +5,6 @@ warn_unused_configs = True [mypy-bottle_websocket] ignore_missing_imports = True -[mypy-jinja2] -ignore_missing_imports = True - [mypy-gevent] ignore_missing_imports = True @@ -26,12 +23,6 @@ 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 diff --git a/requirements.txt b/requirements.txt index af6da07d..3150cc01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ gevent gevent-websocket<1.0.0 greenlet>=1.0.0,<2.0.0 pyparsing>=3.0.0,<4.0.0 -whichcraft~=0.4.1 +typing-extensions>=4.3.0 diff --git a/setup.py b/setup.py index 03dccdf0..3519c49b 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ package_data={ 'eel': ['eel.js', 'py.typed'], }, - install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing', 'whichcraft'], + install_requires=['bottle', 'bottle-websocket', 'future', 'pyparsing'], extras_require={ "jinja2": ['jinja2>=2.10'] },