Skip to content

Commit ef3a44a

Browse files
committed
Add support for launching modules with pydev
1 parent 8632338 commit ef3a44a

File tree

2 files changed

+136
-6
lines changed

2 files changed

+136
-6
lines changed

python/helper-image/launcher/launcher.go

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import (
5252
"flag"
5353
"fmt"
5454
"io"
55+
"io/ioutil"
5556
"os"
5657
"os/exec"
5758
"path/filepath"
@@ -312,15 +313,19 @@ func (pc *pythonContext) updateCommandLine(ctx context.Context) error {
312313
if pc.env["WRAPPER_VERBOSE"] != "" {
313314
cmdline = append(cmdline, "--DEBUG")
314315
}
315-
if pc.debugMode == ModePydevdPycharm {
316-
// From the pydevd source, PyCharm wants multiproc
317-
cmdline = append(cmdline, "--multiproc")
318-
}
319316
if !pc.wait {
320317
cmdline = append(cmdline, "--continue")
321318
}
322-
cmdline = append(cmdline, "--file") // --file is expected as last argument
323-
cmdline = append(cmdline, pc.args[1:]...)
319+
320+
// --file is expected as last pydev argument, but it must be a file, and so launching with
321+
// a module requires some special handling.
322+
cmdline = append(cmdline, "--file")
323+
file, args, err := handlePydevModule(pc.args[1:])
324+
if err != nil {
325+
return err
326+
}
327+
cmdline = append(cmdline, file)
328+
cmdline = append(cmdline, args...)
324329
if pc.wait {
325330
logrus.Warn("pydevd does not support wait-for-client")
326331
}
@@ -356,6 +361,47 @@ func determinePythonMajorMinor(ctx context.Context, launcherBin string, env env)
356361
return
357362
}
358363

364+
// handlePydevModule applies special pydevd handling for a python module. When a module is
365+
// found, we write out a python script that uses runpy to invoke the module.
366+
func handlePydevModule(args []string) (string, []string, error) {
367+
switch {
368+
case len(args) == 0:
369+
return "", nil, fmt.Errorf("no python command-line specified") // shouldn't happen
370+
case !strings.HasPrefix(args[0], "-"):
371+
// this is a file
372+
return args[0], args[1:], nil
373+
case !strings.HasPrefix(args[0], "-m"):
374+
// this is some other command-line flag
375+
return "", nil, fmt.Errorf("expected python module: %q", args)
376+
}
377+
module := args[0][2:]
378+
remaining := args[1:]
379+
if module == "" {
380+
if len(args) == 1 {
381+
return "", nil, fmt.Errorf("missing python module: %q", args)
382+
}
383+
module = args[1]
384+
remaining = args[2:]
385+
}
386+
387+
snippet := strings.ReplaceAll(`import sys
388+
import runpy
389+
runpy.run_module('{module}', run_name="__main__",alter_sys=True)
390+
`, `{module}`, module)
391+
392+
// write out the temp location as other locations may not be writable
393+
d, err := ioutil.TempDir("", "pydevd*")
394+
if err != nil {
395+
return "", nil, err
396+
}
397+
// use a skaffold-specific file name to ensure no possibility of it matching a user import
398+
f := filepath.Join(d, "skaffold_pydevd_launch.py")
399+
if err := ioutil.WriteFile(f, []byte(snippet), 0755); err != nil {
400+
return "", nil, err
401+
}
402+
return f, remaining, nil
403+
}
404+
359405
func isEnabled(env env) bool {
360406
v, found := env["WRAPPER_ENABLED"]
361407
return !found || (v != "0" && v != "false" && v != "no")

python/helper-image/launcher/launcher_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import (
2020
"context"
2121
"fmt"
2222
"io/ioutil"
23+
"os"
2324
"path/filepath"
2425
"testing"
2526

2627
"github.com/google/go-cmp/cmp"
28+
"github.com/google/go-cmp/cmp/cmpopts"
2729
)
2830

2931
func TestValidateDebugMode(t *testing.T) {
@@ -312,3 +314,85 @@ func TestPathExists(t *testing.T) {
312314
t.Error("pathExists failed on real path")
313315
}
314316
}
317+
318+
func TestHandlePydevModule(t *testing.T) {
319+
tmp := os.TempDir()
320+
321+
tests := []struct {
322+
description string
323+
args []string
324+
shouldErr bool
325+
module string
326+
file string
327+
remaining []string
328+
}{
329+
{
330+
description: "plain file",
331+
args: []string{"app.py"},
332+
file: "app.py",
333+
},
334+
{
335+
description: "-mmodule",
336+
args: []string{"-mmodule"},
337+
file: filepath.Join(tmp, "*", "skaffold_pydevd_launch.py"),
338+
},
339+
{
340+
description: "-m module",
341+
args: []string{"-m", "module"},
342+
file: filepath.Join(tmp, "*", "skaffold_pydevd_launch.py"),
343+
},
344+
{
345+
description: "- should error",
346+
args: []string{"-", "module"},
347+
shouldErr: true,
348+
},
349+
{
350+
description: "-x should error",
351+
args: []string{"-x", "module"},
352+
shouldErr: true,
353+
},
354+
{
355+
description: "lone -m should error",
356+
args: []string{"-m"},
357+
shouldErr: true,
358+
},
359+
{
360+
description: "no args should error",
361+
shouldErr: true,
362+
},
363+
}
364+
for _, test := range tests {
365+
t.Run(test.description, func(t *testing.T) {
366+
file, args, err := handlePydevModule(test.args)
367+
if test.shouldErr {
368+
if err == nil {
369+
t.Error("Expected an error")
370+
}
371+
} else {
372+
if !fileMatch(t, test.file, file) {
373+
t.Errorf("Wanted %q but got %q", test.file, file)
374+
}
375+
if diff := cmp.Diff(args, test.remaining, cmpopts.EquateEmpty()); diff != "" {
376+
t.Errorf("remaining args %T differ (-got, +want): %s", test.remaining, diff)
377+
}
378+
}
379+
})
380+
}
381+
}
382+
383+
func fileMatch(t *testing.T, glob, file string) bool {
384+
if file == glob {
385+
return true
386+
}
387+
matches, err := filepath.Glob(glob)
388+
if err != nil {
389+
t.Errorf("Failed to expand globe %q: %v", glob, err)
390+
return false
391+
}
392+
for _, m := range matches {
393+
if file == m {
394+
return true
395+
}
396+
}
397+
return false
398+
}

0 commit comments

Comments
 (0)