diff --git a/backend/tests/python_jobs.rs b/backend/tests/python_jobs.rs index 3f6d682c5a..3a1550d395 100644 --- a/backend/tests/python_jobs.rs +++ b/backend/tests/python_jobs.rs @@ -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) -> 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, +) -> 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) -> anyhow::Result<()> { diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 4db7b20fd5..1f20148841 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -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)" } }