diff --git a/internal/cmd/evaluator.go b/internal/cmd/evaluator.go index b936e723..31cf906d 100644 --- a/internal/cmd/evaluator.go +++ b/internal/cmd/evaluator.go @@ -284,6 +284,7 @@ func newEvaluatorUploadCmd() *cobra.Command { } re := regexp.MustCompile(`\bdef\s+` + regexp.QuoteMeta(funcName) + `\s*\(`) sourceStr = re.ReplaceAllString(sourceStr, "def "+evalFuncName+"(") + sourceStr = stripPythonReturnAnnotation(sourceStr, evalFuncName) case "javascript": sourceStr = extractJSFunction(string(source), funcName) if sourceStr == "" { @@ -438,6 +439,21 @@ func extractPythonFunction(source string, funcName string) string { return strings.Join(lines[startIdx:endIdx], "\n") + "\n" } +// stripPythonReturnAnnotation removes a return-type annotation from the def line of an +// already-extracted, already-renamed function (e.g. `def perform_eval(run) -> dict | None:` +// becomes `def perform_eval(run):`). +// +// The evaluator service's function finder does not handle PEP 604 union return annotations +// (e.g. `-> dict | None`): uploading such a function fails with "Code evaluator does not +// contain function with name perform_eval" even though the function is present. A plain +// annotation (`-> dict`) or no annotation works. The annotation is irrelevant to execution, +// so we drop it. Only single-line def signatures are handled (the common case); a multi-line +// signature is left untouched. +func stripPythonReturnAnnotation(source, funcName string) string { + re := regexp.MustCompile(`(?m)^(def ` + regexp.QuoteMeta(funcName) + `\(.*\))\s*->.*?:(\s*(?:#.*)?)$`) + return re.ReplaceAllString(source, "$1:$2") +} + // detectLanguage returns the API language value and the canonical eval function // name based on the file extension. func detectLanguage(filename string) (language, evalFuncName string) { diff --git a/internal/cmd/evaluator_test.go b/internal/cmd/evaluator_test.go index 49368212..19a47732 100644 --- a/internal/cmd/evaluator_test.go +++ b/internal/cmd/evaluator_test.go @@ -117,6 +117,49 @@ func TestExtractPythonFunction_EmptySource(t *testing.T) { } } +// ---------- stripPythonReturnAnnotation ---------- + +func TestStripPythonReturnAnnotation_Union(t *testing.T) { + // PEP 604 union return annotation: the evaluator service's function finder + // can't handle it, so it must be stripped. + source := "def perform_eval(run) -> dict | None:\n return {\"score\": 1}\n" + result := stripPythonReturnAnnotation(source, "perform_eval") + if !contains(result, "def perform_eval(run):") { + t.Errorf("union return annotation should be stripped, got:\n%s", result) + } + if contains(result, "->") { + t.Error("result should not contain a return annotation") + } + if !contains(result, `return {"score": 1}`) { + t.Error("body should be preserved") + } +} + +func TestStripPythonReturnAnnotation_Plain(t *testing.T) { + source := "def perform_eval(run) -> dict:\n return {\"score\": 1}\n" + result := stripPythonReturnAnnotation(source, "perform_eval") + if !contains(result, "def perform_eval(run):") { + t.Errorf("plain return annotation should be stripped, got:\n%s", result) + } +} + +func TestStripPythonReturnAnnotation_WithParamsAndComment(t *testing.T) { + // params plus a trailing comment after the colon + source := "def perform_eval(run, example) -> dict | None: # noqa\n return {}\n" + result := stripPythonReturnAnnotation(source, "perform_eval") + if !contains(result, "def perform_eval(run, example):") { + t.Errorf("should strip annotation and keep params, got:\n%s", result) + } +} + +func TestStripPythonReturnAnnotation_NoAnnotationUnchanged(t *testing.T) { + source := "def perform_eval(run):\n return {\"score\": 1}\n" + result := stripPythonReturnAnnotation(source, "perform_eval") + if result != source { + t.Errorf("source without an annotation should be unchanged, got:\n%s", result) + } +} + // ---------- extractJSFunction ---------- func TestExtractJSFunction_FunctionDeclaration(t *testing.T) {