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

Side by Side 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2017 The LUCI Authors. All rights reserved.
2 // Use of this source code is governed under the Apache License, Version 2.0
3 // that can be found in the LICENSE file.
4
5 package python
6
7 import (
8 "os"
9 "os/exec"
10 "strings"
11 "sync"
12
13 "github.com/luci/luci-go/common/errors"
14 "github.com/luci/luci-go/common/logging"
15 "github.com/luci/luci-go/common/system/exitcode"
16
17 "golang.org/x/net/context"
18 )
19
20 type runnerFunc func(cmd *exec.Cmd, capture bool) (string, error)
21
22 // 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.
23 type Interpreter struct {
24 // Python is the path to the Python interpreter.
iannucci 2017/02/21 10:08:16 absolute?
dnj 2017/02/21 23:21:35 Done.
25 Python string
26
27 // 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.
28 WorkDir string
29
30 // 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.
31 ConnectSTDIN bool
32
33 // Isolated means that the Python invocation should include flags to iso late
34 // 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.
35 Isolated bool
36
37 // Env, if not nil, is the environment to supply.
38 Env []string
39
40 // runner is the runner function to use to run an exec.Cmd. This can be
41 // swapped out for testing.
42 runner runnerFunc
iannucci 2017/02/21 10:08:16 lets prefix all testing members with 'testing'
dnj 2017/02/21 23:21:35 Done.
43
44 // cachedVersion is the cached Version for this interpreter. It is popul ated
45 // on the first GetVersion call.
46 cachedVersion *Version
47 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
48 }
49
50 func (i *Interpreter) getRunnerFunc() runnerFunc {
51 if i.runner != nil {
52 return i.runner
53 }
54
55 // Return default runner (actually run the process).
56 return func(cmd *exec.Cmd, capture bool) (string, error) {
57 // If we're capturing output, combine STDOUT and STDERR (see Get Version
58 // for details).
59 if capture {
60 out, err := cmd.CombinedOutput()
61 if err != nil {
62 return "", errors.Annotate(err).Err()
63 }
64 return string(out), nil
65 }
66
67 // Non-capturing run.
68 if err := cmd.Run(); err != nil {
69 return "", errors.Annotate(err).Err()
70 }
71 return "", nil
72 }
73 }
74
75 // 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
76 //
77 // Run returns wrapped errors. Use errors.Unwrap to get the main cause, if
78 // needed. If an error occurs during setup or invocation, it will be returned
79 // directly. If the interpreter runs and returns zero, nil will be returned. If
80 // the interpreter runs and returns non-zero, an Error instance will be returned
81 // containing that return code.
82 func (i *Interpreter) Run(c context.Context, args ...string) error {
83 if i.Python == "" {
84 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.
85 }
86
87 if i.Isolated {
88 args = append([]string{
89 "-B", // Don't compile "pyo" binaries.
90 "-E", // Don't use PYTHON* enviornment variables.
91 "-s", // Don't use user 'site.py'.
92 }, args...)
93 }
94
95 cmd := exec.CommandContext(c, i.Python, args...)
96 cmd.Stdout = os.Stdout
97 cmd.Stderr = os.Stderr
98 if i.ConnectSTDIN {
99 cmd.Stdin = os.Stdin
100 }
101 cmd.Dir = i.WorkDir
102 cmd.Env = i.Env
103
104 if logging.IsLogging(c, logging.Debug) {
105 logging.Debugf(c, "Running Python command (cwd=%s): %s",
106 cmd.Dir, strings.Join(cmd.Args, " "))
107 }
108
109 // Allow testing to supply an alternative runner function.
110 rf := i.getRunnerFunc()
111 if _, err := rf(cmd, false); err != nil {
112 // If the process failed because of a non-zero return value, ret urn that
113 // as our error.
114 if rc, has := exitcode.Get(err); has {
115 return errors.Annotate(Error(rc)).Reason("Python bootstr ap returned non-zero").Err()
116 }
117
118 return errors.Annotate(err).Reason("failed to run Python command ").
119 D("python", i.Python).
120 D("args", args).
121 Err()
122 }
123 return nil
124 }
125
126 // GetVersion runs the specified Python interpreter with the "--version"
127 // flag and maps it to a known specification verison.
128 func (i *Interpreter) GetVersion(c context.Context) (v Version, err error) {
129 if i.Python == "" {
130 err = errors.New("a Python interpreter must be supplied")
131 return
132 }
133
134 i.cachedVersionMu.Lock()
135 defer i.cachedVersionMu.Unlock()
136
137 // Check again, under write-lock.
138 if i.cachedVersion != nil {
139 v = *i.cachedVersion
140 return
141 }
142
143 // We use CombinedOutput here becuase Python2 writes the version to STDE RR,
144 // while Python3+ writes it to STDOUT.
145 cmd := exec.CommandContext(c, i.Python, "--version")
146
147 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.
148 out, err := rf(cmd, true)
149 if err != nil {
150 err = errors.Annotate(err).Reason("failed to get Python version" ).Err()
151 return
152 }
153 if v, err = parseVersionOutput(strings.TrimSpace(out)); err != nil {
154 err = errors.Annotate(err).Err()
155 return
156 }
157
158 i.cachedVersion = &v
159 return
160 }
161
162 func parseVersionOutput(output string) (Version, error) {
163 // Expected output:
164 // Python X.Y.Z
165 parts := strings.SplitN(output, " ", 2)
166 if len(parts) != 2 || parts[0] != "Python" {
167 return Version{}, errors.Reason("unknown version output").
168 D("output", output).
169 Err()
170 }
171
172 v, err := ParseVersion(parts[1])
173 if err != nil {
174 err = errors.Annotate(err).Reason("failed to parse version from: %(value)q").
175 D("value", parts[1]).
176 Err()
177 return v, err
178 }
179 return v, nil
180 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698