From 60486cf11899895534f3d909f335f27c1a2ce9e8 Mon Sep 17 00:00:00 2001 From: Koen Vossen Date: Tue, 11 Oct 2022 21:26:17 +0200 Subject: [PATCH 1/7] Fix JsException passing from pyodide environment to host --- pytest_pyodide/decorator.py | 23 ++++++++++++++++++++++- pytest_pyodide/pyodide.py | 15 +++++++++++++++ tests/test_decorator.py | 16 ++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 pytest_pyodide/pyodide.py diff --git a/pytest_pyodide/decorator.py b/pytest_pyodide/decorator.py index 952f7c5d..d56e97f9 100644 --- a/pytest_pyodide/decorator.py +++ b/pytest_pyodide/decorator.py @@ -4,12 +4,14 @@ from base64 import b64decode, b64encode from collections.abc import Callable, Collection from copy import deepcopy +from io import BytesIO from typing import Any import pytest from .hook import ORIGINAL_MODULE_ASTS, REWRITTEN_MODULE_ASTS from .utils import package_is_built as _package_is_built +from .pyodide import JsException def package_is_built(package_name: str): @@ -35,6 +37,25 @@ def _encode(obj: Any) -> str: return b64encode(pickle.dumps(obj)).decode() +class Unpickler(pickle.Unpickler): + def find_class(self, module, name): + """ + Catch exceptions that only exist in the pyodide environment and + convert them to exception in the host. + """ + if module == "pyodide" and name == "JsException": + return JsException + else: + return super().find_class(module, name) + + +def _decode(result: str) -> Any: + buffer = BytesIO() + buffer.write(b64decode(result)) + buffer.seek(0) + return Unpickler(buffer).load() + + def _create_outer_test_function( run_test: Callable, node: Any, @@ -229,7 +250,7 @@ def _run_test(self, selenium: SeleniumType, args: tuple): r = selenium.run_async(code) [status, result] = r - result = pickle.loads(b64decode(result)) + result = _decode(result) if status: raise result else: diff --git a/pytest_pyodide/pyodide.py b/pytest_pyodide/pyodide.py new file mode 100644 index 00000000..701b8bf7 --- /dev/null +++ b/pytest_pyodide/pyodide.py @@ -0,0 +1,15 @@ +class JsException(Exception): + """ + The python code of the test can call javascript functions using + ``` + from js import XMLHttpRequest + + xhr = XMLHttpRequest.new() + xhr.responseType = 'arraybuffer'; + xhr.open('url', None, False) # this will fail in main thread + ``` + + The code fails and raises a `JsException` in the pyodide environment. When the exception + is sent back to host, the host tries to unpickle the exception. The unpickle will fail + because "pyodide.JsException" only exists in the pyodide environment. + """ \ No newline at end of file diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 54e260ca..6a368b60 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -3,6 +3,7 @@ from pytest_pyodide.decorator import run_in_pyodide from pytest_pyodide.hypothesis import any_strategy, std_hypothesis_settings +from pytest_pyodide.pyodide import JsException from pytest_pyodide.utils import parse_driver_timeout @@ -69,6 +70,21 @@ def inner_function(selenium, x): assert inner_function(selenium, 6) == 7 +def test_inner_function_js_exception(selenium): + @run_in_pyodide + def inner_function(selenium): + # Try to do a sync request with non-standard responseType. + # This is not allowed in the main thread and will raise a JsException + from js import XMLHttpRequest + xhr = XMLHttpRequest.new() + xhr.responseType = "arraybuffer" + xhr.open("GET", "http://non-existing-url/", False) + + with pytest.raises(JsException, match="InvalidAccessError: Failed to execute 'open' on 'XMLHttpRequest': " + "Synchronous requests from a document must not set a response type."): + inner_function(selenium) + + def complicated_decorator(attr_name: str): def inner_func(value): def dec(func): From 1f68ca9686d4a68d1c83655e6426d931c3fc8467 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 19:30:06 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pytest_pyodide/decorator.py | 2 +- pytest_pyodide/pyodide.py | 2 +- tests/test_decorator.py | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pytest_pyodide/decorator.py b/pytest_pyodide/decorator.py index d56e97f9..8117bf0e 100644 --- a/pytest_pyodide/decorator.py +++ b/pytest_pyodide/decorator.py @@ -10,8 +10,8 @@ import pytest from .hook import ORIGINAL_MODULE_ASTS, REWRITTEN_MODULE_ASTS -from .utils import package_is_built as _package_is_built from .pyodide import JsException +from .utils import package_is_built as _package_is_built def package_is_built(package_name: str): diff --git a/pytest_pyodide/pyodide.py b/pytest_pyodide/pyodide.py index 701b8bf7..56306cc6 100644 --- a/pytest_pyodide/pyodide.py +++ b/pytest_pyodide/pyodide.py @@ -12,4 +12,4 @@ class JsException(Exception): The code fails and raises a `JsException` in the pyodide environment. When the exception is sent back to host, the host tries to unpickle the exception. The unpickle will fail because "pyodide.JsException" only exists in the pyodide environment. - """ \ No newline at end of file + """ diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 6a368b60..68a312fc 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -76,12 +76,16 @@ def inner_function(selenium): # Try to do a sync request with non-standard responseType. # This is not allowed in the main thread and will raise a JsException from js import XMLHttpRequest + xhr = XMLHttpRequest.new() xhr.responseType = "arraybuffer" xhr.open("GET", "http://non-existing-url/", False) - with pytest.raises(JsException, match="InvalidAccessError: Failed to execute 'open' on 'XMLHttpRequest': " - "Synchronous requests from a document must not set a response type."): + with pytest.raises( + JsException, + match="InvalidAccessError: Failed to execute 'open' on 'XMLHttpRequest': " + "Synchronous requests from a document must not set a response type.", + ): inner_function(selenium) From 7f680c0e8daebdccc7e7f3231d8f2cded450f265 Mon Sep 17 00:00:00 2001 From: Koen Vossen Date: Tue, 11 Oct 2022 21:35:16 +0200 Subject: [PATCH 3/7] Make match pattern more generic --- tests/test_decorator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 68a312fc..47ad13d5 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -83,8 +83,7 @@ def inner_function(selenium): with pytest.raises( JsException, - match="InvalidAccessError: Failed to execute 'open' on 'XMLHttpRequest': " - "Synchronous requests from a document must not set a response type.", + match="InvalidAccessError.*XMLHttpRequest.*", ): inner_function(selenium) From 9849728f12b78d04383e1c60de946ed5b11dc887 Mon Sep 17 00:00:00 2001 From: Koen Vossen Date: Tue, 11 Oct 2022 21:38:28 +0200 Subject: [PATCH 4/7] Simplify exception so it runs on all runners --- tests/test_decorator.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 47ad13d5..85163c08 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -73,17 +73,13 @@ def inner_function(selenium, x): def test_inner_function_js_exception(selenium): @run_in_pyodide def inner_function(selenium): - # Try to do a sync request with non-standard responseType. - # This is not allowed in the main thread and will raise a JsException - from js import XMLHttpRequest + from js import eval as js_eval - xhr = XMLHttpRequest.new() - xhr.responseType = "arraybuffer" - xhr.open("GET", "http://non-existing-url/", False) + js_eval("throw 'some error'") with pytest.raises( JsException, - match="InvalidAccessError.*XMLHttpRequest.*", + match="Error: some error", ): inner_function(selenium) From fd345d7dd57c2970b57b40f20383ed3810d10814 Mon Sep 17 00:00:00 2001 From: koenvo Date: Tue, 11 Oct 2022 21:44:02 +0200 Subject: [PATCH 5/7] Update tests/test_decorator.py Co-authored-by: Hood Chatham --- tests/test_decorator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 85163c08..a1a8f800 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -73,9 +73,9 @@ def inner_function(selenium, x): def test_inner_function_js_exception(selenium): @run_in_pyodide def inner_function(selenium): - from js import eval as js_eval + from pyodide.code import run_js - js_eval("throw 'some error'") + run_js("throw 'some error'") with pytest.raises( JsException, From d7dcdeb4cc28456a3cc7d58e2a99c84c4637902c Mon Sep 17 00:00:00 2001 From: Koen Vossen Date: Wed, 12 Oct 2022 15:56:27 +0200 Subject: [PATCH 6/7] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9198e428..b84caa28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.22.3] - 2022.10.12 + +- `JsException` raise from within pyodide is now unpickled correctly in the host. ([#45](https://github.com/pyodide/pytest-pyodide/issues/45)) + ## [0.22.2] - 2022.09.08 - Host tests will now run by default. If you want to disable running host tests, add `-no-host` suffix in the `--runtime` option. ([#33](https://github.com/pyodide/pytest-pyodide/pull/33)) From 539d866c07cc2d88548619a93e04fc986a7b49f6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 12 Oct 2022 13:58:32 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b84caa28..fa5fddb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [0.22.3] - 2022.10.12 -- `JsException` raise from within pyodide is now unpickled correctly in the host. ([#45](https://github.com/pyodide/pytest-pyodide/issues/45)) +- `JsException` raise from within pyodide is now unpickled correctly in the host. ([#45](https://github.com/pyodide/pytest-pyodide/issues/45)) ## [0.22.2] - 2022.09.08