diff --git a/.circleci/config.yml b/.circleci/config.yml index 4cfcd3d29..35d6ab89c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2 defaults: &defaults working_directory: ~/repo docker: - - image: iodide/pyodide-env:11 + - image: iodide/pyodide-env:12 environment: - EMSDK_NUM_CORES: 4 EMCC_CORES: 4 diff --git a/.gitignore b/.gitignore index e9a21a02f..1e1984101 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ firefox/ .vscode .idea .mypy_cache/ +.hypothesis node_modules/ build diff --git a/Dockerfile b/Dockerfile index 084dec2bf..30f81428c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,21 @@ RUN apt-get update \ libgconf-2-4 chromium \ && rm -rf /var/lib/apt/lists/* -RUN pip3 --no-cache-dir install pytest pytest-xdist pytest-instafail pytest-rerunfailures \ - pytest-httpserver pytest-cov selenium PyYAML flake8 black distlib mypy "Cython<3.0" +RUN pip3 --no-cache-dir install \ + black \ + "cython<3.0" \ + distlib \ + flake8 \ + hypothesis \ + mypy \ + pytest \ + pytest-cov \ + pytest-httpserver \ + pytest-instafail \ + pytest-rerunfailures \ + pytest-xdist \ + pyyaml \ + selenium # Get firefox 70.0.1 and geckodriver RUN wget -qO- https://ftp.mozilla.org/pub/firefox/releases/70.0.1/linux-x86_64/en-US/firefox-70.0.1.tar.bz2 | tar jx \ diff --git a/conftest.py b/conftest.py index eb15501ee..db4fde0f2 100644 --- a/conftest.py +++ b/conftest.py @@ -227,53 +227,52 @@ class ChromeWrapper(SeleniumWrapper): if pytest is not None: - @pytest.fixture(params=["firefox", "chrome"]) + @contextlib.contextmanager + def selenium_common(request, web_server_main): + server_hostname, server_port, server_log = web_server_main + if request.param == "firefox": + cls = FirefoxWrapper + elif request.param == "chrome": + cls = ChromeWrapper + selenium = cls( + build_dir=request.config.option.build_dir, + server_port=server_port, + server_hostname=server_hostname, + server_log=server_log, + ) + try: + yield selenium + finally: + selenium.driver.quit() + + @pytest.fixture(params=["firefox", "chrome"], scope="function") def selenium_standalone(request, web_server_main): - server_hostname, server_port, server_log = web_server_main - if request.param == "firefox": - cls = FirefoxWrapper - elif request.param == "chrome": - cls = ChromeWrapper - selenium = cls( - build_dir=request.config.option.build_dir, - server_port=server_port, - server_hostname=server_hostname, - server_log=server_log, - ) - try: - yield selenium - finally: - print(selenium.logs) - selenium.driver.quit() + with selenium_common(request, web_server_main) as selenium: + try: + yield selenium + finally: + print(selenium.logs) + # selenium instance cached at the module level @pytest.fixture(params=["firefox", "chrome"], scope="module") - def _selenium_cached(request, web_server_main): - # Cached selenium instance. This is a copy-paste of - # selenium_standalone to avoid fixture scope issues - server_hostname, server_port, server_log = web_server_main - if request.param == "firefox": - cls = FirefoxWrapper - elif request.param == "chrome": - cls = ChromeWrapper - selenium = cls( - build_dir=request.config.option.build_dir, - server_port=server_port, - server_hostname=server_hostname, - server_log=server_log, - ) - try: + def selenium_module_scope(request, web_server_main): + with selenium_common(request, web_server_main) as selenium: yield selenium - finally: - selenium.driver.quit() - @pytest.fixture - def selenium(_selenium_cached): - # selenium instance cached at the module level + # We want one version of this decorated as a function-scope fixture and one + # version decorated as a context manager. + def selenium_per_function(selenium_module_scope): try: - _selenium_cached.clean_logs() - yield _selenium_cached + selenium_module_scope.clean_logs() + yield selenium_module_scope finally: - print(_selenium_cached.logs) + print(selenium_module_scope.logs) + + selenium = pytest.fixture(selenium_per_function) + # Hypothesis is unhappy with function scope fixtures. Instead, use the + # module scope fixture `selenium_module_scope` and use: + # `with selenium_context_manager(selenium_module_scope) as selenium` + selenium_context_manager = contextlib.contextmanager(selenium_per_function) @pytest.fixture(scope="session") diff --git a/run_docker b/run_docker index 31ebd1cea..33c5d881f 100755 --- a/run_docker +++ b/run_docker @@ -26,7 +26,7 @@ function error() { } -PYODIDE_IMAGE_TAG="11" +PYODIDE_IMAGE_TAG="12" PYODIDE_PREBUILT_IMAGE_TAG="0.16.1" DEFAULT_PYODIDE_DOCKER_IMAGE="iodide/pyodide-env:${PYODIDE_IMAGE_TAG}" DEFAULT_PYODIDE_SYSTEM_PORT="8000" diff --git a/src/pyodide-py/pyodide/_core.py b/src/pyodide-py/pyodide/_core.py index 595a0f11a..13ba6f4af 100644 --- a/src/pyodide-py/pyodide/_core.py +++ b/src/pyodide-py/pyodide/_core.py @@ -1,26 +1,20 @@ -# type: ignore import platform if platform.system() == "Emscripten": - from _pyodide_core import JsProxy, JsException, JsBuffer + from _pyodide_core import JsProxy, JsException else: # Can add shims here if we are so inclined. - class JsException(Exception): + class JsException(Exception): # type: ignore """ A wrapper around a Javascript Error to allow the Error to be thrown in Python. """ # Defined in jsproxy.c - class JsProxy: + class JsProxy: # type: ignore """A proxy to make a Javascript object behave like a Python object""" # Defined in jsproxy.c - class JsBuffer: - """A proxy to make it possible to call Javascript typed arrays from Python.""" - # Defined in jsproxy.c - - -__all__ = [JsProxy, JsException] +__all__ = ["JsProxy", "JsException"] diff --git a/src/pyodide-py/pyodide/webloop.py b/src/pyodide-py/pyodide/webloop.py index b427107b7..8788c60a6 100644 --- a/src/pyodide-py/pyodide/webloop.py +++ b/src/pyodide-py/pyodide/webloop.py @@ -4,7 +4,7 @@ import time import contextvars -from typing import Awaitable, Callable +from typing import Callable class WebLoop(asyncio.AbstractEventLoop): @@ -65,7 +65,7 @@ class WebLoop(asyncio.AbstractEventLoop): """ pass - def run_until_complete(self, future: Awaitable): + def run_until_complete(self, future): """Run until future is done. If the argument is a coroutine, it is wrapped in a Task. @@ -99,7 +99,7 @@ class WebLoop(asyncio.AbstractEventLoop): return self.call_later(delay, callback, *args, context=context) def call_soon_threadsafe( - callback: Callable, *args, context: contextvars.Context = None + self, callback: Callable, *args, context: contextvars.Context = None ): """Like ``call_soon()``, but thread-safe. @@ -223,7 +223,7 @@ class WebLoop(asyncio.AbstractEventLoop): return self._task_factory -class WebLoopPolicy(asyncio.DefaultEventLoopPolicy): +class WebLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore """ A simple event loop policy for managing WebLoop based event loops. """ diff --git a/src/tests/test_typeconversions.py b/src/tests/test_typeconversions.py index 88cee46e1..03c3c5ecc 100644 --- a/src/tests/test_typeconversions.py +++ b/src/tests/test_typeconversions.py @@ -1,5 +1,28 @@ # See also test_pyproxy, test_jsproxy, and test_python. import pytest +from hypothesis import given +from hypothesis.strategies import text +from conftest import selenium_context_manager + + +@given(s=text()) +def test_string_conversion(selenium_module_scope, s): + with selenium_context_manager(selenium_module_scope) as selenium: + # careful string escaping here -- hypothesis will fuzz it. + sbytes = list(s.encode()) + selenium.run_js( + f""" + window.sjs = (new TextDecoder("utf8")).decode(new Uint8Array({sbytes})); + pyodide.runPython('spy = bytes({sbytes}).decode()'); + """ + ) + assert selenium.run_js(f"""return pyodide.runPython('spy') === sjs;""") + assert selenium.run( + """ + from js import sjs + sjs == spy + """ + ) def test_python2js(selenium):