mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
fix(python): preserve strings containing Infinity/NaN in result JSON (#9149)
* fix(python): preserve strings containing Infinity/NaN in result JSON * test(python): add sanity checks for Infinity/NaN in results * refactor(python): use string-aware regex callback for single-pass cleanup * refactor(python): compact regex + handle backslash-escape parity * perf(python): short-circuit cleanup when no Infinity/NaN/NUL in result
This commit is contained in:
@@ -760,6 +760,112 @@ def main():
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_python_result_preserves_infinity_in_string(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let content = r#"
|
||||
def main():
|
||||
return {
|
||||
"plain": "Infinity",
|
||||
"embedded": "value=-Infinity end",
|
||||
"nan_word": "this is NaN inside text",
|
||||
"nested": [{"k": "Infinity"}],
|
||||
}
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
lock: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
modules: None,
|
||||
tag: None,
|
||||
});
|
||||
|
||||
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!({
|
||||
"plain": "Infinity",
|
||||
"embedded": "value=-Infinity end",
|
||||
"nan_word": "this is NaN inside text",
|
||||
"nested": [{"k": "Infinity"}],
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_python_result_non_finite_floats_become_null(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let content = r#"
|
||||
def main():
|
||||
return {
|
||||
"inf": float("inf"),
|
||||
"neg_inf": float("-inf"),
|
||||
"nan": float("nan"),
|
||||
"finite": 1.5,
|
||||
"nested": [float("inf"), {"x": float("nan")}],
|
||||
}
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
lock: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
modules: None,
|
||||
tag: None,
|
||||
});
|
||||
|
||||
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!({
|
||||
"inf": null,
|
||||
"neg_inf": null,
|
||||
"nan": null,
|
||||
"finite": 1.5,
|
||||
"nested": [null, {"x": null}],
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_python_global_site_packages(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
|
||||
@@ -841,7 +841,11 @@ def to_b_64(v: bytes):
|
||||
b64 = base64.b64encode(v)
|
||||
return b64.decode('ascii')
|
||||
|
||||
replace_invalid_fields = re.compile(r'(?:\bNaN\b|\\*\\u0000|Infinity|\-Infinity)')
|
||||
_u=re.compile(r'\\\\|\\u0000')
|
||||
_us=lambda m:' null ' if m.group(0)[1]=='u' else m.group(0)
|
||||
_r=lambda m,s='':(_u.sub(_us,s) if '\\u0000' in s else s) if (s:=m.group(0))[0]=='"' else ' null '
|
||||
replace_invalid_fields=re.compile(r'"(?:\\.|[^"\\])*"|\bNaN\b|-?Infinity')
|
||||
_fix=lambda s:s if 'Infinity' not in s and 'NaN' not in s and '\\u0000' not in s else re.sub(replace_invalid_fields,_r,s)
|
||||
|
||||
result_json = os.path.join(os.path.abspath(os.path.dirname(__file__)), "result.json")
|
||||
|
||||
@@ -1367,7 +1371,11 @@ def to_b_64(v: bytes):
|
||||
b64 = base64.b64encode(v)
|
||||
return b64.decode('ascii')
|
||||
|
||||
replace_invalid_fields = re.compile(r'(?:\bNaN\b|\\u0000|Infinity|\-Infinity)')
|
||||
_u=re.compile(r'\\\\|\\u0000')
|
||||
_us=lambda m:' null ' if m.group(0)[1]=='u' else m.group(0)
|
||||
_r=lambda m,s='':(_u.sub(_us,s) if '\\u0000' in s else s) if (s:=m.group(0))[0]=='"' else ' null '
|
||||
replace_invalid_fields=re.compile(r'"(?:\\.|[^"\\])*"|\bNaN\b|-?Infinity')
|
||||
_fix=lambda s:s if 'Infinity' not in s and 'NaN' not in s and '\\u0000' not in s else re.sub(replace_invalid_fields,_r,s)
|
||||
|
||||
def res_to_json(res, typ):
|
||||
{res_to_json_body}
|
||||
@@ -2951,7 +2959,7 @@ fn get_result_postprocessor<'a>(skip: bool) -> &'a str {
|
||||
if skip {
|
||||
"unprocessed"
|
||||
} else {
|
||||
"re.sub(replace_invalid_fields, ' null ', unprocessed)"
|
||||
"_fix(unprocessed)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user