From 7290806232ef22ee0760dd87c2c9dd822e9b75b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 10 Jun 2026 15:25:47 +0000 Subject: [PATCH] wheel: strip extras from console-script entry points; lock: strip inline comments from first '#' - console-script targets like 'pkg.mod:main [extra]' no longer leak the '[extra]' suffix into the generated launcher; module-less entry points are skipped instead of producing invalid Python - stripComment now strips from the first '#' (not only ' #') and trims trailing whitespace, matching its docstring - adds tests demonstrating both behaviors Co-authored-by: Jason Hall --- pymage/internal/lock/lock.go | 14 ++++----- pymage/internal/lock/lock_test.go | 20 +++++++++++++ pymage/internal/wheel/wheel.go | 14 ++++++++- pymage/internal/wheel/wheel_test.go | 45 +++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/pymage/internal/lock/lock.go b/pymage/internal/lock/lock.go index 66abe02..1b02fff 100644 --- a/pymage/internal/lock/lock.go +++ b/pymage/internal/lock/lock.go @@ -119,15 +119,13 @@ func Parse(r io.Reader) ([]Requirement, error) { } func stripComment(s string) string { - // A '#' starts a comment unless escaped; requirements files don't escape so - // we treat the first unquoted '#' as a comment start. - if i := strings.Index(s, " #"); i >= 0 { - return s[:i] + // A '#' starts a comment. Requirements files don't support escaping '#', + // and no valid pinned/hashed line contains one, so we strip from the first + // '#' and drop any trailing whitespace it leaves behind. + if i := strings.IndexByte(s, '#'); i >= 0 { + s = s[:i] } - if strings.HasPrefix(strings.TrimSpace(s), "#") { - return "" - } - return s + return strings.TrimRight(s, " \t") } func isOption(s string) bool { diff --git a/pymage/internal/lock/lock_test.go b/pymage/internal/lock/lock_test.go index c3005a8..77eeac5 100644 --- a/pymage/internal/lock/lock_test.go +++ b/pymage/internal/lock/lock_test.go @@ -92,6 +92,26 @@ requests[socks,security]==2.31.0 --hash=sha256:` + strings.Repeat("a", 64) + ` } } +func TestStripsInlineComments(t *testing.T) { + h := strings.Repeat("a", 64) + for _, in := range []string{ + "flask==2.3.3 --hash=sha256:" + h + " # via app", // space before # + "flask==2.3.3 --hash=sha256:" + h + "#no-space", // no space before # + "flask==2.3.3 --hash=sha256:" + h + "\t# tabbed", // tab before # + } { + reqs, err := Parse(strings.NewReader(in)) + if err != nil { + t.Fatalf("Parse(%q): %v", in, err) + } + if len(reqs) != 1 || reqs[0].Name != "flask" || reqs[0].Version != "2.3.3" { + t.Fatalf("Parse(%q) = %+v", in, reqs) + } + if len(reqs[0].Hashes) != 1 { + t.Errorf("Parse(%q): expected hash to survive comment stripping, got %v", in, reqs[0].Hashes) + } + } +} + func TestNormalizeName(t *testing.T) { for _, tc := range []struct{ in, want string }{ {"Flask", "flask"}, diff --git a/pymage/internal/wheel/wheel.go b/pymage/internal/wheel/wheel.go index 9a96040..db80ddb 100644 --- a/pymage/internal/wheel/wheel.go +++ b/pymage/internal/wheel/wheel.go @@ -379,11 +379,23 @@ func parseConsoleScripts(s string) map[string]entryPoint { } name = strings.TrimSpace(name) target = strings.TrimSpace(target) + // Entry-point targets may carry an optional "[extras]" suffix + // (e.g. "pkg.mod:main [cli]"); it is not part of the import path and + // must be stripped or the generated launcher is invalid Python. + if i := strings.IndexByte(target, '['); i >= 0 { + target = strings.TrimSpace(target[:i]) + } module, attr, _ := strings.Cut(target, ":") + module = strings.TrimSpace(module) + attr = strings.TrimSpace(attr) + if module == "" { + // Without a module we cannot generate a valid launcher. + continue + } if attr == "" { attr = "main" } - out[name] = entryPoint{module: strings.TrimSpace(module), attr: strings.TrimSpace(attr)} + out[name] = entryPoint{module: module, attr: attr} } return out } diff --git a/pymage/internal/wheel/wheel_test.go b/pymage/internal/wheel/wheel_test.go index d61d13b..4516aed 100644 --- a/pymage/internal/wheel/wheel_test.go +++ b/pymage/internal/wheel/wheel_test.go @@ -151,6 +151,51 @@ func TestFilesLayout(t *testing.T) { } } +func TestConsoleScriptExtrasStripped(t *testing.T) { + dir := t.TempDir() + path, _ := testwheel.Write(t, dir, testwheel.Spec{ + Name: "tool", + Version: "1.0", + Modules: map[string]string{"tool/cli.py": "def main():\n pass\n"}, + ConsoleScripts: map[string]string{ + "tool": "tool.cli:main [fast]", // extras must be stripped + "broken": ":main", // no module: must be skipped + "colored": "tool.cli:main[color]", // extras with no space + }, + }) + w, err := Open(path) + if err != nil { + t.Fatal(err) + } + defer func() { _ = w.Close() }() + + files, err := w.Files(layout) + if err != nil { + t.Fatal(err) + } + byPath := map[string]string{} + for _, f := range files { + byPath[f.Path] = string(f.Data) + } + + bin := "app/.venv/bin/" + for _, name := range []string{"tool", "colored"} { + body, ok := byPath[bin+name] + if !ok { + t.Fatalf("missing console script %q", name) + } + if strings.Contains(body, "[") { + t.Errorf("script %q still contains extras:\n%s", name, body) + } + if !strings.Contains(body, "from tool.cli import main") || !strings.Contains(body, "sys.exit(main())") { + t.Errorf("script %q has unexpected body:\n%s", name, body) + } + } + if _, ok := byPath[bin+"broken"]; ok { + t.Error("module-less entry point should not produce a launcher") + } +} + func TestFilesDeterministic(t *testing.T) { path := writeFixture(t) digest := func() string {