fix: use exact matching for python requirements directive parsing (#8199)

* fix: use exact matching for python requirements directive parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: apply same exact matching fix to CLI parser

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-03-02 21:21:28 +00:00
committed by GitHub
parent 50defdded1
commit 2b2be38f12
3 changed files with 96 additions and 16 deletions
@@ -872,16 +872,29 @@ impl WorkspaceDependenciesAnnotatedRefs<String> {
validity_re_o: Option<&Regex>,
runnable_path: &str,
) -> Option<Self> {
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<String> {
.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::<String>::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
"#;
+20 -5
View File
@@ -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(",")
+32 -5
View File
@@ -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():