fix: python results with a KeyError-raising __getattr__ fail the job (#11517)

* fix: python results with a KeyError-raising __getattr__ no longer fail

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep the stream probe on the instance so enum results still serialize

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: probe python results through class dicts, never their __getattr__

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-05 08:20:52 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 289d3c3941
commit 3fff1bd3dc
3 changed files with 47 additions and 10 deletions
@@ -860,7 +860,7 @@ pub async fn handle_python_job(
if v == '<function call>':
del pre_args[k]
_pre_result = inner_script.preprocessor(**pre_args)
if hasattr(_pre_result, '__await__'):
if any('__await__' in vars(c) for c in type(_pre_result).__mro__):
import asyncio
_pre_result = asyncio.run(_pre_result)
kwargs = _pre_result if _pre_result is not None else {{}}
@@ -981,7 +981,7 @@ try:
raise ValueError("{main_override} function is missing")
res = _await(inner_script.{main_override}(**args))
typ = type(res)
if hasattr(res, '__iter__') and not isinstance(res, (str, dict, list, bytes, tuple, set, frozenset, range, memoryview, bytearray)) and typ.__name__ != 'DataFrame':
if _defines(res, '__iter__') and not isinstance(res, (str, dict, list, bytes, tuple, set, frozenset, range, memoryview, bytearray)) and typ.__name__ != 'DataFrame':
for chunk in res:
print("WM_STREAM: " + chunk.replace('\n', '\\n'))
res = None
@@ -3340,10 +3340,16 @@ This is not normal behavior, please make sure all workers have enough memory.\n
/// loop-per-call would break them from the second job on. A loop the script
/// set while importing (`asyncio.set_event_loop`) is reused. asyncio is
/// imported lazily so sync jobs don't pay its import time.
/// `_defines` reads the class dicts instead of calling `hasattr`: a result is
/// arbitrary user data, and `hasattr` runs its `__getattr__` (or its
/// metaclass's), which may raise something other than AttributeError or
/// answer every name.
const PY_AWAIT_HELPER: &str = r#"_loop = None
def _defines(r, name):
return any(name in vars(c) for c in type(r).__mro__)
def _await(r):
global _loop
if not hasattr(r, '__await__'):
if not _defines(r, '__await__'):
return r
import asyncio
if _loop is None or _loop.is_closed():