Compare commits

...

3 Commits

Author SHA1 Message Date
Pyra
42eba98ea9 Merge branch 'main' into py-typechecked-decorator 2026-03-28 17:57:03 +01:00
Pyra
f6863bd1ba Merge branch 'main' into py-typechecked-decorator 2026-03-28 14:07:51 +01:00
pyranota
acfae3552c fix: support @typechecked decorator in Python relative imports
WindmillFinder's ModuleSpec lacked origin, so __file__ was never set on
loaded modules. inspect.getfile() then raised "is a built-in module",
breaking typeguard's @typechecked and anything else that introspects
module source. Use spec_from_file_location() which sets origin correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 14:52:37 +01:00
3 changed files with 63 additions and 2 deletions

View File

@@ -0,0 +1,20 @@
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
import inspect
import sys
def greet(name: str) -> str:
# Verify that __file__ is set on this module (same check typeguard does)
mod = sys.modules[__name__]
source_file = inspect.getfile(mod)
return f"Hello, {name}! from {source_file}"
def main():
return greet("World")
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/typechecked_helper', 12349, 'python3', '');

View File

@@ -923,3 +923,43 @@ async def main(item: str, qty: int, email: str):
.await;
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "typechecked_python"))]
async fn test_typechecked_decorator_python(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let content = r#"
from f.system.typechecked_helper import greet
def main():
return greet("World")
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: Some("f/system/test_typechecked".to_string()),
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,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
.await
.json_result()
.unwrap();
let result_str = result.as_str().unwrap();
assert!(result_str.starts_with("Hello, World! from "), "unexpected result: {result_str}");
Ok(())
}

View File

@@ -2,6 +2,7 @@ import sys
import os
from importlib.abc import MetaPathFinder, Loader
from importlib.machinery import ModuleSpec, SourceFileLoader
from importlib.util import spec_from_file_location
import time
class WindmillLoader(Loader):
@@ -33,7 +34,7 @@ class WindmillFinder(MetaPathFinder):
fullpath = folder + "/" + splitted[-1] + ".py"
if os.path.exists(fullpath):
return ModuleSpec(name, SourceFileLoader(name, fullpath))
return spec_from_file_location(name, fullpath)
import urllib.parse
@@ -62,7 +63,7 @@ class WindmillFinder(MetaPathFinder):
return ModuleSpec(name, WindmillLoader(name))
with open(fullpath, "w+") as f:
f.write(r)
return ModuleSpec(name, SourceFileLoader(name, fullpath))
return spec_from_file_location(name, fullpath)
except urllib.error.HTTPError as e:
duration = time.time() - req_start
if e.code != 404: