skilleter-modules 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,306 @@
1
+ #! /usr/bin/env python3
2
+
3
+ ################################################################################
4
+ """ Code for running a subprocess, optionally capturing stderr and/or stdout and
5
+ optionally echoing either or both to the console in realtime and storing.
6
+
7
+ Uses threads to capture and output stderr and stdout since this seems to be
8
+ the only way to do it (Popen does not have the ability to output the process
9
+ stdout output to the stdout output).
10
+
11
+ Intended for more versatile replacement for the thingy process.run() function
12
+ which can handle all combinations of foreground/background console/return
13
+ stderr/stdout/both options. """
14
+
15
+ # TODO: This does not run on Python versions <3.5 (so Ubuntu 14.04 is a problem!)
16
+ ################################################################################
17
+
18
+ ################################################################################
19
+ # Imports
20
+
21
+ import sys
22
+ import subprocess
23
+ import threading
24
+ import shlex
25
+
26
+ import tidy
27
+
28
+ ################################################################################
29
+
30
+ class RunError(Exception):
31
+ """ Run exception """
32
+
33
+ def __init__(self, msg, status=1):
34
+ super().__init__(msg)
35
+ self.msg = msg
36
+ self.status = status
37
+
38
+ ################################################################################
39
+
40
+ # TODO: This is the _process() and run() replacement once additional parameters have been implemented
41
+ # as those functions are probably over-specified, so need to work out what functionality is ACTUALLY being used!
42
+
43
+ def command(cmd, show_stdout=False, show_stderr=False):
44
+ """
45
+ Run an external command and optionally stream its stdout/stderr to the console
46
+ while capturing them.
47
+
48
+ Args:
49
+ cmd: Command to run (string or argv list). If a string, it will be split with shlex.split().
50
+ show_stdout: If True, echo stdout lines to the console as they arrive.
51
+ show_stderr: If True, echo stderr lines to the console as they arrive.
52
+
53
+ Returns:
54
+ (returncode, stdout_lines, stderr_lines)
55
+ """
56
+
57
+ def _pump(stream, sink, echo):
58
+ """Thread to capture and optionally echo output from subprocess.Popen"""
59
+
60
+ # Read line-by-line until EOF
61
+
62
+ for line in iter(stream.readline, ""):
63
+ # Strip trailing newline when storing; keep original when echoing
64
+ sink.append(line.rstrip("\n"))
65
+
66
+ if echo:
67
+ print(tidy.convert_ansi(line), end="", flush=True)
68
+
69
+ stream.close()
70
+
71
+ # Normalize command to be a string
72
+
73
+ if isinstance(cmd, str):
74
+ cmd = shlex.split(cmd)
75
+
76
+ # Storage for stdout/stderr
77
+
78
+ stdout_lines = []
79
+ stderr_lines = []
80
+
81
+ # Start process with separate pipes; line-buffering for timely reads
82
+
83
+ proc = subprocess.Popen(
84
+ cmd,
85
+ stdout=subprocess.PIPE,
86
+ stderr=subprocess.PIPE,
87
+ text=True, # decode to str
88
+ bufsize=1, # line-buffered
89
+ universal_newlines=True, # compatibility alias
90
+ errors="replace" # avoid crashes on decoding issues
91
+ )
92
+
93
+ # Threads to read both streams concurrently (prevents deadlocks)
94
+
95
+ t_out = threading.Thread(target=_pump, args=(proc.stdout, stdout_lines, show_stdout))
96
+ t_err = threading.Thread(target=_pump, args=(proc.stderr, stderr_lines, show_stderr))
97
+
98
+ t_out.start()
99
+ t_err.start()
100
+
101
+ # Wait for process to complete and threads to drain
102
+
103
+ returncode = proc.wait()
104
+ t_out.join()
105
+ t_err.join()
106
+
107
+ # Return the status, stdout and stderr
108
+
109
+ return returncode, stdout_lines, stderr_lines
110
+
111
+ ################################################################################
112
+
113
+ def capture_output(cmd, input_stream, output_streams):
114
+ """ Capture data from a stream (input_stream), optionally
115
+ outputting it (if output_streams is not None and optionally
116
+ saving it into a variable (data, if not None), terminating
117
+ when the specified command (cmd, which is presumed to be the process
118
+ outputting to the input stream) exits.
119
+ TODO: Use of convert_ansi should be controlled via a parameter (off/light/dark)
120
+ TODO: Another issue is that readline() only returns at EOF or EOL, so if you get a prompt "Continue?" with no newline you do not see it until after you respond to it.
121
+ """
122
+
123
+ while True:
124
+ output = input_stream.readline()
125
+
126
+ if output:
127
+ if output_streams:
128
+ for stream in output_streams:
129
+ if isinstance(stream, list):
130
+ stream.append(output.rstrip())
131
+ else:
132
+ if stream in (sys.stdout, sys.stderr):
133
+ stream.write(tidy.convert_ansi(output))
134
+ else:
135
+ stream.write(output)
136
+
137
+ elif cmd.poll() is not None:
138
+ return
139
+
140
+ ################################################################################
141
+
142
+ def _process(command,
143
+ stdout=None, stderr=None,
144
+ output=None):
145
+ """ Run an external command.
146
+
147
+ stdout and stderr indicate whether stdout/err are output and/or sent to a file and/or stored in a variable.
148
+ They can be boolean (True: output to sys.stdout/err, False: Do nothing), a file handle or a variable, or an
149
+ array of any number of these (except booleans).
150
+
151
+ If output is True then stdout and stderr are both output as if stdout=True and stderr=True (in addition to
152
+ any other values passed in those parameters)
153
+
154
+ The return value is a tuple consisting of the status code, captured stdout (if any) and captured
155
+ stderr (if any).
156
+
157
+ Will raise OSError if the command could not be run and RunError if the command returned a non-zero status code.
158
+ """
159
+
160
+ # If stdout/stderr are booleans then output to stdout/stderr if True, else discard output
161
+
162
+ if isinstance(stdout, bool):
163
+ stdout = sys.stdout if stdout else None
164
+
165
+ if isinstance(stderr, bool):
166
+ stderr = sys.stderr if stderr else None
167
+
168
+ # If stdout/stderr are not arrays then make them so
169
+
170
+ if not isinstance(stdout, list):
171
+ stdout = [stdout] if stdout else []
172
+
173
+ if not isinstance(stderr, list):
174
+ stderr = [stderr] if stderr else []
175
+
176
+ # If output is True then add stderr/out to the list of outputs
177
+
178
+ if output:
179
+ if sys.stdout not in stdout:
180
+ stdout.append(sys.stdout)
181
+
182
+ if sys.stderr not in stderr:
183
+ stderr.append(sys.stderr)
184
+
185
+ # Capture stdout/stderr to arrays
186
+
187
+ stdout_data = []
188
+ stderr_data = []
189
+
190
+ stdout.append(stdout_data)
191
+ stderr.append(stderr_data)
192
+
193
+ if isinstance(command, str):
194
+ command = shlex.split(command, comments=True)
195
+
196
+ # Use a pipe for stdout/stderr if are are capturing it
197
+ # and send it to /dev/null if we don't care about it at all.
198
+
199
+ if stdout == [sys.stdout] and not stderr:
200
+ stdout_stream = subprocess.STDOUT
201
+ stderr_stream = subprocess.DEVNULL
202
+ else:
203
+ stdout_stream = subprocess.PIPE if stdout else subprocess.DEVNULL
204
+ stderr_stream = subprocess.PIPE if stderr else subprocess.DEVNULL
205
+
206
+ # Run the command with no buffering and capturing output if we
207
+ # want it - this will raise OSError if there was a problem running
208
+ # the command.
209
+
210
+ cmd = subprocess.Popen(command,
211
+ bufsize=0,
212
+ stdout=stdout_stream,
213
+ stderr=stderr_stream,
214
+ text=True,
215
+ errors='ignore',
216
+ encoding='ascii')
217
+
218
+ # Create threads to capture stderr and/or stdout if necessary
219
+
220
+ if stdout_stream == subprocess.PIPE:
221
+ stdout_thread = threading.Thread(target=capture_output, args=(cmd, cmd.stdout, stdout), daemon=True)
222
+ stdout_thread.start()
223
+ else:
224
+ stdout_thread = None
225
+
226
+ if stderr_stream == subprocess.PIPE:
227
+ stderr_thread = threading.Thread(target=capture_output, args=(cmd, cmd.stderr, stderr), daemon=True)
228
+ stderr_thread.start()
229
+ else:
230
+ stderr_thread = None
231
+
232
+ # Wait until the command terminates (and set the returncode)
233
+
234
+ if stdout_thread:
235
+ stdout_thread.join()
236
+
237
+ if stderr_thread:
238
+ stderr_thread.join()
239
+
240
+ cmd.wait()
241
+
242
+ # If the command failed, raise an exception
243
+
244
+ if cmd.returncode:
245
+ raise RunError('\n'.join(stderr_data) if stderr_data else 'Error %d running "%s"' % (cmd.returncode, ' '.join(command)))
246
+
247
+ # Return status, stdout, stderr (the latter 2 may be empty if we did not capture data).
248
+
249
+ return {'status': cmd.returncode, 'stdout': stdout_data, 'stderr': stderr_data}
250
+
251
+ ################################################################################
252
+
253
+ def run(command,
254
+ stdout=None,
255
+ stderr=None,
256
+ output=None):
257
+ """ Simple interface to the _process() function
258
+ Has the same parameters, with the same defaults.
259
+ The return value is either the data output to stdout, if any
260
+ or the data output to stderr otherwise.
261
+ The status code is not returned, but the function will raise an exception
262
+ if it is non-zero """
263
+
264
+ result = _process(command=command,
265
+ stdout=stdout,
266
+ stderr=stderr,
267
+ output=output)
268
+
269
+ return result['stdout'] if result['stdout'] else result['stderr']
270
+
271
+ ################################################################################
272
+
273
+ if __name__ == '__main__':
274
+ def test_run(cmd,
275
+ stdout=None, stderr=None):
276
+ """ Test wrapper for the process() function. """
277
+
278
+ print('-' * 80)
279
+ print('Running: %s' % (cmd if isinstance(cmd, str) else ' '.join(cmd)))
280
+
281
+ result = _process(cmd, stdout=stdout, stderr=stderr)
282
+
283
+ print('Status: %d' % result['status'])
284
+
285
+ def test():
286
+ """ Test code """
287
+
288
+ test_run('echo nothing')
289
+
290
+ test_run(['ls', '-l', 'run_jed'])
291
+ test_run(['ls -l run_*'], stdout=True)
292
+ test_run('false')
293
+ test_run('true', stdout=sys.stdout)
294
+ test_run(['git', 'status'], stdout=sys.stdout, stderr=sys.stderr)
295
+
296
+ test_run(['make'], stderr=sys.stderr)
297
+ test_run(['make'], stdout=sys.stdout, stderr=[sys.stderr])
298
+ test_run(['make'], stdout=True)
299
+ test_run(['make'], stdout=sys.stdout)
300
+ test_run(['make'])
301
+
302
+ output = []
303
+ test_run('ls -l x*; sleep 1; echo "Bye!"', stderr=[sys.stderr, output], stdout=sys.stdout)
304
+ print('Output=%s' % output)
305
+
306
+ test()