Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(433)

Unified Diff: vpython/python/interpreter.go

Issue 2701073002: vpython: Add Python interpreter handling package. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: vpython/python/interpreter.go
diff --git a/vpython/python/interpreter.go b/vpython/python/interpreter.go
new file mode 100644
index 0000000000000000000000000000000000000000..30c8e3968d8998b10c4174c49d70faef40d8ef3e
--- /dev/null
+++ b/vpython/python/interpreter.go
@@ -0,0 +1,180 @@
+// Copyright 2017 The LUCI Authors. All rights reserved.
+// Use of this source code is governed under the Apache License, Version 2.0
+// that can be found in the LICENSE file.
+
+package python
+
+import (
+ "os"
+ "os/exec"
+ "strings"
+ "sync"
+
+ "github.com/luci/luci-go/common/errors"
+ "github.com/luci/luci-go/common/logging"
+ "github.com/luci/luci-go/common/system/exitcode"
+
+ "golang.org/x/net/context"
+)
+
+type runnerFunc func(cmd *exec.Cmd, capture bool) (string, error)
+
+// Interpreter is a configured Python interpreter.
iannucci 2017/02/21 10:08:16 maybe Interpreter is a runnable Python interprete
dnj 2017/02/21 23:21:35 Done.
+type Interpreter struct {
+ // Python is the path to the Python interpreter.
iannucci 2017/02/21 10:08:16 absolute?
dnj 2017/02/21 23:21:35 Done.
+ Python string
+
+ // WorkDir is the working directory to use.
iannucci 2017/02/21 10:08:16 use for what?
dnj 2017/02/21 23:21:35 Done.
+ WorkDir string
+
+ // ConnectSTDIN, if true, says that STDIN should also be connected.
iannucci 2017/02/21 10:08:16 ConnectSTDIN will cause stdin to be passed through
dnj 2017/02/21 23:21:35 Done.
+ ConnectSTDIN bool
+
+ // Isolated means that the Python invocation should include flags to isolate
+ // it from local system modification.
iannucci 2017/02/21 10:08:16 Like what? "Local system modification"? From the c
dnj 2017/02/21 23:21:35 Done.
+ Isolated bool
+
+ // Env, if not nil, is the environment to supply.
+ Env []string
+
+ // runner is the runner function to use to run an exec.Cmd. This can be
+ // swapped out for testing.
+ runner runnerFunc
iannucci 2017/02/21 10:08:16 lets prefix all testing members with 'testing'
dnj 2017/02/21 23:21:35 Done.
+
+ // cachedVersion is the cached Version for this interpreter. It is populated
+ // on the first GetVersion call.
+ cachedVersion *Version
+ cachedVersionMu sync.Mutex
iannucci 2017/02/21 10:08:16 sync.Once?
dnj 2017/02/21 23:21:35 Because there can be an error return code, I would
+}
+
+func (i *Interpreter) getRunnerFunc() runnerFunc {
+ if i.runner != nil {
+ return i.runner
+ }
+
+ // Return default runner (actually run the process).
+ return func(cmd *exec.Cmd, capture bool) (string, error) {
+ // If we're capturing output, combine STDOUT and STDERR (see GetVersion
+ // for details).
+ if capture {
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", errors.Annotate(err).Err()
+ }
+ return string(out), nil
+ }
+
+ // Non-capturing run.
+ if err := cmd.Run(); err != nil {
+ return "", errors.Annotate(err).Err()
+ }
+ return "", nil
+ }
+}
+
+// Run runs the configured Interpreter with the supplied arguments.
iannucci 2017/02/21 10:08:16 Run executes the python interpreter with the suppl
dnj 2017/02/21 23:21:35 I split this off into "Command()" (returns a bound
+//
+// Run returns wrapped errors. Use errors.Unwrap to get the main cause, if
+// needed. If an error occurs during setup or invocation, it will be returned
+// directly. If the interpreter runs and returns zero, nil will be returned. If
+// the interpreter runs and returns non-zero, an Error instance will be returned
+// containing that return code.
+func (i *Interpreter) Run(c context.Context, args ...string) error {
+ if i.Python == "" {
+ return errors.New("a Python interpreter must be supplied")
iannucci 2017/02/21 10:08:16 please avoid passive voice everywhere
dnj 2017/02/21 23:21:35 Done.
+ }
+
+ if i.Isolated {
+ args = append([]string{
+ "-B", // Don't compile "pyo" binaries.
+ "-E", // Don't use PYTHON* enviornment variables.
+ "-s", // Don't use user 'site.py'.
+ }, args...)
+ }
+
+ cmd := exec.CommandContext(c, i.Python, args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if i.ConnectSTDIN {
+ cmd.Stdin = os.Stdin
+ }
+ cmd.Dir = i.WorkDir
+ cmd.Env = i.Env
+
+ if logging.IsLogging(c, logging.Debug) {
+ logging.Debugf(c, "Running Python command (cwd=%s): %s",
+ cmd.Dir, strings.Join(cmd.Args, " "))
+ }
+
+ // Allow testing to supply an alternative runner function.
+ rf := i.getRunnerFunc()
+ if _, err := rf(cmd, false); err != nil {
+ // If the process failed because of a non-zero return value, return that
+ // as our error.
+ if rc, has := exitcode.Get(err); has {
+ return errors.Annotate(Error(rc)).Reason("Python bootstrap returned non-zero").Err()
+ }
+
+ return errors.Annotate(err).Reason("failed to run Python command").
+ D("python", i.Python).
+ D("args", args).
+ Err()
+ }
+ return nil
+}
+
+// GetVersion runs the specified Python interpreter with the "--version"
+// flag and maps it to a known specification verison.
+func (i *Interpreter) GetVersion(c context.Context) (v Version, err error) {
+ if i.Python == "" {
+ err = errors.New("a Python interpreter must be supplied")
+ return
+ }
+
+ i.cachedVersionMu.Lock()
+ defer i.cachedVersionMu.Unlock()
+
+ // Check again, under write-lock.
+ if i.cachedVersion != nil {
+ v = *i.cachedVersion
+ return
+ }
+
+ // We use CombinedOutput here becuase Python2 writes the version to STDERR,
+ // while Python3+ writes it to STDOUT.
+ cmd := exec.CommandContext(c, i.Python, "--version")
+
+ rf := i.getRunnerFunc()
iannucci 2017/02/21 10:08:16 why not `i.run(cmd, capture)`? I think that might
dnj 2017/02/21 23:21:35 Changed to a Command struct.
+ out, err := rf(cmd, true)
+ if err != nil {
+ err = errors.Annotate(err).Reason("failed to get Python version").Err()
+ return
+ }
+ if v, err = parseVersionOutput(strings.TrimSpace(out)); err != nil {
+ err = errors.Annotate(err).Err()
+ return
+ }
+
+ i.cachedVersion = &v
+ return
+}
+
+func parseVersionOutput(output string) (Version, error) {
+ // Expected output:
+ // Python X.Y.Z
+ parts := strings.SplitN(output, " ", 2)
+ if len(parts) != 2 || parts[0] != "Python" {
+ return Version{}, errors.Reason("unknown version output").
+ D("output", output).
+ Err()
+ }
+
+ v, err := ParseVersion(parts[1])
+ if err != nil {
+ err = errors.Annotate(err).Reason("failed to parse version from: %(value)q").
+ D("value", parts[1]).
+ Err()
+ return v, err
+ }
+ return v, nil
+}

Powered by Google App Engine
This is Rietveld 408576698