Skip to content

Commit 78e27b9

Browse files
authored
Better handling for unpickle failure (#160)
When we pickle the object, also store its repr. If we fail to unpickle, include the repr in the error message. Also, add the info to the error using `add_node`. This requires Python 3.11 so I bumped the requires_python. This requires Pytest 8 for the test to work, so I needed to merge #161 first.
1 parent 6a6a4f6 commit 78e27b9

File tree

4 files changed

+50
-19
lines changed

4 files changed

+50
-19
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ classifiers = [
1515
"Framework :: Pytest"
1616
]
1717
dynamic = ["version"]
18-
requires-python = ">=3.10"
18+
requires-python = ">=3.11"
1919
dependencies = [
2020
"pexpect",
2121
"pytest",
@@ -49,7 +49,7 @@ Changelog = "https://github.com/pyodide/pytest-pyodide/blob/main/CHANGELOG.md"
4949
source = "vcs"
5050

5151
[tool.mypy]
52-
python_version = "3.10"
52+
python_version = "3.11"
5353
show_error_codes = true
5454
warn_unreachable = true
5555
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]

pytest_pyodide/_decorator_in_pyodide.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def decode(x: str) -> Any:
9797

9898
async def run_in_pyodide_main(
9999
mod64: str, args64: str, module_filename: str, func_name: str, async_func: bool
100-
) -> tuple[int, str]:
100+
) -> tuple[int, str, str]:
101101
"""
102102
This actually runs the code for run_in_pyodide.
103103
"""
@@ -119,7 +119,7 @@ async def run_in_pyodide_main(
119119
result = d[func_name](None, *args)
120120
if async_func:
121121
result = await result
122-
return (0, encode(result))
122+
return (0, encode(result), repr(result))
123123
except BaseException as e:
124124
try:
125125
# If tblib is present, we can show much better tracebacks.
@@ -141,7 +141,7 @@ def get_locals(frame):
141141

142142
except ImportError:
143143
pass
144-
return (1, encode(e))
144+
return (1, encode(e), repr(e))
145145

146146

147147
__all__ = ["PyodideHandle", "encode"]

pytest_pyodide/decorator.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,19 @@ def _encode(obj: Any) -> str:
100100
return b64encode(b.getvalue()).decode()
101101

102102

103-
def _decode(result: str, selenium: SeleniumType) -> Any:
103+
def _decode(selenium: SeleniumType, result, status, repr) -> Any:
104+
if status:
105+
thing = "exception raised"
106+
else:
107+
thing = "value returned"
104108
try:
105109
return Unpickler(BytesIO(b64decode(result)), selenium).load()
106-
except ModuleNotFoundError as exc:
107-
raise ModuleNotFoundError(
108-
f"There was a problem with unpickling the return value/exception from your pyodide environment. "
109-
f"This usually means the type of the return value/exception does not exist in your host environment. "
110-
f"The original message is: {exc}."
111-
) from None
110+
except Exception as e:
111+
e.add_note(
112+
f"The error occurred while unpickling the {thing} from pyodide.\n"
113+
f"The repr of the object we failed to unpickle was '{repr}'"
114+
)
115+
raise
112116

113117

114118
def all_args(funcdef: MaybeAsyncFuncDef) -> list[ast.arg]:
@@ -450,9 +454,9 @@ def _run(self, selenium: SeleniumType, args: tuple[Any, ...]):
450454
selenium.load_package(self._pkgs)
451455

452456
r = selenium.run_async(code)
453-
[status, result] = r
457+
[status, result, repr] = r
454458

455-
result = _decode(result, selenium)
459+
result = _decode(selenium, result, status, repr)
456460
if status:
457461
raise result
458462
else:

tests/test_decorator.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def inner_function(selenium):
8686
inner_function(selenium)
8787

8888

89-
def test_not_unpickable_return_value(selenium):
89+
def test_unpickleable_return_value(selenium):
9090
@run_in_pyodide
9191
async def inner_function(selenium):
9292
with open("some_module.py", "w") as fp:
@@ -96,10 +96,37 @@ async def inner_function(selenium):
9696

9797
return Test()
9898

99-
with pytest.raises(
100-
ModuleNotFoundError,
101-
match="There was a problem with unpickling the return.*",
102-
):
99+
regex = "\n".join(
100+
[
101+
r"No module named 'some_module'",
102+
r"The error occurred while unpickling the value returned from pyodide.",
103+
r"The repr of the object we failed to unpickle was '<some_module.Test object at 0x[0-9a-f]*>'",
104+
]
105+
)
106+
107+
with pytest.raises(ModuleNotFoundError, match=regex):
108+
inner_function(selenium)
109+
110+
111+
def test_unpickleable_exception(selenium):
112+
@run_in_pyodide
113+
async def inner_function(selenium):
114+
with open("some_module2.py", "w") as fp:
115+
fp.write("class TestException(Exception): pass\n")
116+
117+
from some_module2 import TestException
118+
119+
raise TestException
120+
121+
regex = "\n".join(
122+
[
123+
r"No module named 'some_module2'",
124+
r"The error occurred while unpickling the exception raised from pyodide.",
125+
r"The repr of the object we failed to unpickle was 'TestException\(\)'",
126+
]
127+
)
128+
129+
with pytest.raises(ModuleNotFoundError, match=regex):
103130
inner_function(selenium)
104131

105132

0 commit comments

Comments
 (0)