diff --git a/backend/windmill-common/src/workspace_dependencies.rs b/backend/windmill-common/src/workspace_dependencies.rs index cf38003057..051ed9db41 100644 --- a/backend/windmill-common/src/workspace_dependencies.rs +++ b/backend/windmill-common/src/workspace_dependencies.rs @@ -872,16 +872,29 @@ impl WorkspaceDependenciesAnnotatedRefs { validity_re_o: Option<&Regex>, runnable_path: &str, ) -> Option { - let (extra_deps, manual_deps) = (format!("extra_{keyword}:"), format!("{keyword}:")); + let extra_deps_underscore = format!("extra_{keyword}:"); + let extra_deps_hyphen = format!("extra-{keyword}:"); + let manual_deps = format!("{keyword}:"); - let Some((pos, mat)) = code.lines().find_position(|l| { - l.starts_with(&comment) && (l.contains(&extra_deps) || l.contains(&manual_deps)) - }) else { + let is_extra = |l: &str| { + l.strip_prefix(comment) + .map(str::trim_start) + .is_some_and(|s| { + s.starts_with(&extra_deps_underscore) || s.starts_with(&extra_deps_hyphen) + }) + }; + let is_manual = |l: &str| { + l.strip_prefix(comment) + .map(str::trim_start) + .is_some_and(|s| s.starts_with(&manual_deps)) + }; + + let Some((pos, mat)) = code.lines().find_position(|l| is_extra(l) || is_manual(l)) else { return None; }; let mut lines_it = code.lines().skip(pos); - let mode = if mat.contains(&extra_deps) { + let mode = if is_extra(mat) { Mode::extra } else { Mode::manual @@ -904,7 +917,9 @@ impl WorkspaceDependenciesAnnotatedRefs { .map(|s| { match mode { Mode::manual => s.replace(&manual_deps, ""), - Mode::extra => s.replace(&extra_deps, ""), + Mode::extra => s + .replace(&extra_deps_underscore, "") + .replace(&extra_deps_hyphen, ""), } .replace(comment, "") }) @@ -993,6 +1008,29 @@ def main(): # extra_requirements: utils #numpy>=1.24.0 +def main(): + pass +"#; + + let result = WorkspaceDependenciesAnnotatedRefs::::parse( + "#", + "requirements", + code, + None, + "", + ) + .unwrap(); + assert!(matches!(result.mode, Mode::extra)); + assert_eq!(result.external, vec!["utils".to_owned()]); + assert_eq!(result.inline.as_ref().unwrap(), "numpy>=1.24.0"); + } + + #[test] + fn test_parse_annotation_python_extra_requirements_hyphen() { + let code = r#" +# extra-requirements: utils +#numpy>=1.24.0 + def main(): pass "#; diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 0e10798725..590c3d7d8e 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -339,16 +339,29 @@ export function extractWorkspaceDepsAnnotation( if (!config) return null; const { comment, keyword, validityRe } = config; - const extraMarker = `extra_${keyword}:`; + const extraMarkerUnderscore = `extra_${keyword}:`; + const extraMarkerHyphen = `extra-${keyword}:`; const manualMarker = `${keyword}:`; + const stripComment = (l: string): string | null => { + if (!l.startsWith(comment)) return null; + return l.substring(comment.length).trimStart(); + }; + const isExtra = (l: string): boolean => { + const s = stripComment(l); + return s !== null && (s.startsWith(extraMarkerUnderscore) || s.startsWith(extraMarkerHyphen)); + }; + const isManual = (l: string): boolean => { + const s = stripComment(l); + return s !== null && s.startsWith(manualMarker); + }; + const lines = scriptContent.split("\n"); // Find first annotation line (mirrors Rust find_position) let pos = -1; for (let i = 0; i < lines.length; i++) { - const l = lines[i]; - if (l.startsWith(comment) && (l.includes(extraMarker) || l.includes(manualMarker))) { + if (isExtra(lines[i]) || isManual(lines[i])) { pos = i; break; } @@ -356,10 +369,12 @@ export function extractWorkspaceDepsAnnotation( if (pos === -1) return null; const annotationLine = lines[pos]; - const mode: AnnotationMode = annotationLine.includes(extraMarker) ? "extra" : "manual"; + const mode: AnnotationMode = isExtra(annotationLine) ? "extra" : "manual"; // Parse external references from the annotation line - const marker = mode === "extra" ? extraMarker : manualMarker; + const marker = mode === "extra" + ? (annotationLine.includes(extraMarkerUnderscore) ? extraMarkerUnderscore : extraMarkerHyphen) + : manualMarker; const unparsed = annotationLine.replaceAll(marker, "").replaceAll(comment, ""); const external = unparsed .split(",") diff --git a/cli/test/lock_cache.test.ts b/cli/test/lock_cache.test.ts index 9f30b67d90..6225087540 100644 --- a/cli/test/lock_cache.test.ts +++ b/cli/test/lock_cache.test.ts @@ -46,15 +46,28 @@ function extractWorkspaceDepsAnnotation( if (!config) return null; const { comment, keyword, validityRe } = config; - const extraMarker = `extra_${keyword}:`; + const extraMarkerUnderscore = `extra_${keyword}:`; + const extraMarkerHyphen = `extra-${keyword}:`; const manualMarker = `${keyword}:`; + const stripComment = (l: string): string | null => { + if (!l.startsWith(comment)) return null; + return l.substring(comment.length).trimStart(); + }; + const isExtra = (l: string): boolean => { + const s = stripComment(l); + return s !== null && (s.startsWith(extraMarkerUnderscore) || s.startsWith(extraMarkerHyphen)); + }; + const isManual = (l: string): boolean => { + const s = stripComment(l); + return s !== null && s.startsWith(manualMarker); + }; + const lines = scriptContent.split("\n"); let pos = -1; for (let i = 0; i < lines.length; i++) { - const l = lines[i]; - if (l.startsWith(comment) && (l.includes(extraMarker) || l.includes(manualMarker))) { + if (isExtra(lines[i]) || isManual(lines[i])) { pos = i; break; } @@ -62,9 +75,11 @@ function extractWorkspaceDepsAnnotation( if (pos === -1) return null; const annotationLine = lines[pos]; - const mode: AnnotationMode = annotationLine.includes(extraMarker) ? "extra" : "manual"; + const mode: AnnotationMode = isExtra(annotationLine) ? "extra" : "manual"; - const marker = mode === "extra" ? extraMarker : manualMarker; + const marker = mode === "extra" + ? (annotationLine.includes(extraMarkerUnderscore) ? extraMarkerUnderscore : extraMarkerHyphen) + : manualMarker; const unparsed = annotationLine.replaceAll(marker, "").replaceAll(comment, ""); const external = unparsed .split(",") @@ -185,6 +200,18 @@ def main(): expect(r.inline).toEqual("numpy>=1.24.0"); }); +test("python: extra-requirements (hyphen) mode", () => { + const code = `# extra-requirements: utils +#numpy>=1.24.0 + +def main(): + pass`; + const r = extractWorkspaceDepsAnnotation(code, "python3")!; + expect(r.mode).toEqual("extra"); + expect(r.external).toEqual(["utils"]); + expect(r.inline).toEqual("numpy>=1.24.0"); +}); + test("python: empty requirements (opt-out)", () => { const code = `# requirements: def main():