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

Side by Side Diff: vpython/python/interpreter.go

Issue 2701073002: vpython: Add Python interpreter handling package. (Closed)
Patch Set: rebase 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
« no previous file with comments | « vpython/python/find.go ('k') | vpython/python/python.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 represents a system Python interpreter. It exposes the ability
23 // to use common functionality of that interpreter.
24 type Interpreter struct {
25 // Python is the path to the system Python interpreter.
26 Python string
27
28 // cachedVersion is the cached Version for this interpreter. It is popul ated
29 // on the first GetVersion call.
30 cachedVersion *Version
31 cachedVersionMu sync.Mutex
32
33 // testRunner is the runner function to use to run an exec.Cmd. This can be
34 // swapped out for testing.
35 testRunner runnerFunc
36 }
37
38 // Command returns a configurable command structure bound to this Interpreter.
39 func (i *Interpreter) Command() *Command {
40 return &Command{
41 Python: i.Python,
42 testRunner: i.testRunner,
43 }
44 }
45
46 // GetVersion runs the specified Python interpreter with the "--version"
47 // flag and maps it to a known specification verison.
48 func (i *Interpreter) GetVersion(c context.Context) (v Version, err error) {
49 if i.Python == "" {
50 err = errors.New("missing Python interpreter")
51 return
52 }
53
54 i.cachedVersionMu.Lock()
55 defer i.cachedVersionMu.Unlock()
56
57 // Check again, under write-lock.
58 if i.cachedVersion != nil {
59 v = *i.cachedVersion
60 return
61 }
62
63 // We use CombinedOutput here becuase Python2 writes the version to STDE RR,
64 // while Python3+ writes it to STDOUT.
65 cmd := exec.CommandContext(c, i.Python, "--version")
66
67 rf := i.testRunner
68 if rf == nil {
69 rf = defaultRunnerFunc
70 }
71
72 out, err := rf(cmd, true)
73 if err != nil {
74 err = errors.Annotate(err).Reason("failed to get Python version" ).Err()
75 return
76 }
77 if v, err = parseVersionOutput(strings.TrimSpace(out)); err != nil {
78 err = errors.Annotate(err).Err()
79 return
80 }
81
82 i.cachedVersion = &v
83 return
84 }
85
86 func parseVersionOutput(output string) (Version, error) {
87 // Expected output:
88 // Python X.Y.Z
89 parts := strings.SplitN(output, " ", 2)
90 if len(parts) != 2 || parts[0] != "Python" {
91 return Version{}, errors.Reason("unknown version output").
92 D("output", output).
93 Err()
94 }
95
96 v, err := ParseVersion(parts[1])
97 if err != nil {
98 err = errors.Annotate(err).Reason("failed to parse version from: %(value)q").
99 D("value", parts[1]).
100 Err()
101 return v, err
102 }
103 return v, nil
104 }
105
106 // Command can run Python commands using an Interpreter.
107 //
108 // It is created using an Interpreter's Command method.
109 type Command struct {
110 // Python is the path to the Python interpreter to use. It is automatica lly
111 // populated from an Interpreter when created through an Interpreter's C ommand
112 // method.
113 Python string
114
115 // WorkDir is the working directory to use when running the interpreter. If
116 // empty, the current working directory will be used.
117 WorkDir string
118
119 // ConnectSTDIN will cause this process' STDIN to be passed through to t he
120 // Python subprocess. Otherwise, the Python subprocess will receive a cl osed
121 // STDIN.
122 ConnectSTDIN bool
123
124 // Isolated means that the Python invocation should include flags to iso late
125 // it from local system modification.
126 //
127 // This removes environmental factors such as:
128 // - The user's "site.py".
129 // - The current PYTHONPATH environment variable.
130 // - Compiled ".pyc/.pyo" files.
131 Isolated bool
132
133 // Env, if not nil, is the environment to supply.
134 Env []string
135
136 // testRunner is the runner function to use to run an exec.Cmd. This can be
137 // swapped out for testing.
138 testRunner runnerFunc
139 }
140
141 // Run runs the configured Command with the supplied arguments.
142 //
143 // Run returns wrapped errors. Use errors.Unwrap to get the main cause, if
144 // needed. If an error occurs during setup or invocation, it will be returned
145 // directly. If the interpreter runs and returns zero, nil will be returned. If
146 // the interpreter runs and returns non-zero, an Error instance will be returned
147 // containing that return code.
148 func (ic *Command) Run(c context.Context, args ...string) error {
149 if ic.Python == "" {
150 return errors.New("a Python interpreter must be supplied")
151 }
152
153 if ic.Isolated {
154 args = append([]string{
155 "-B", // Don't compile "pyo" binaries.
156 "-E", // Don't use PYTHON* enviornment variables.
157 "-s", // Don't use user 'site.py'.
158 }, args...)
159 }
160
161 cmd := exec.CommandContext(c, ic.Python, args...)
162 cmd.Stdout = os.Stdout
163 cmd.Stderr = os.Stderr
164 if ic.ConnectSTDIN {
165 cmd.Stdin = os.Stdin
166 }
167 cmd.Dir = ic.WorkDir
168 cmd.Env = ic.Env
169
170 if logging.IsLogging(c, logging.Debug) {
171 logging.Debugf(c, "Running Python command (cwd=%s): %s",
172 cmd.Dir, strings.Join(cmd.Args, " "))
173 }
174
175 // Allow testing to supply an alternative runner function.
176 rf := ic.testRunner
177 if rf == nil {
178 rf = defaultRunnerFunc
179 }
180
181 if _, err := rf(cmd, false); err != nil {
182 // If the process failed because of a non-zero return value, ret urn that
183 // as our error.
184 if rc, has := exitcode.Get(err); has {
185 return errors.Annotate(Error(rc)).Reason("Python bootstr ap returned non-zero").Err()
186 }
187
188 return errors.Annotate(err).Reason("failed to run Python command ").
189 D("python", ic.Python).
190 D("args", args).
191 Err()
192 }
193 return nil
194 }
195
196 func defaultRunnerFunc(cmd *exec.Cmd, capture bool) (string, error) {
197 // If we're capturing output, combine STDOUT and STDERR (see GetVersion
198 // for details).
199 if capture {
200 out, err := cmd.CombinedOutput()
201 if err != nil {
202 return "", errors.Annotate(err).Err()
203 }
204 return string(out), nil
205 }
206
207 // Non-capturing run.
208 if err := cmd.Run(); err != nil {
209 return "", errors.Annotate(err).Err()
210 }
211 return "", nil
212 }
OLDNEW
« no previous file with comments | « vpython/python/find.go ('k') | vpython/python/python.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698