libPyshell 0.2.1__tar.gz → 0.4.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libPyshell
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: Support for writing shell scripts in Python
5
5
  Home-page: https://github.com/skogsbaer/libPyshell
6
6
  Author: Stefan Wehr
@@ -37,6 +37,14 @@ magicFiles = run(['grep', 'magic'] + files, captureStdout=splitLines, onError='i
37
37
 
38
38
  ## Changelog
39
39
 
40
+ * 0.4.0 (2024-03-20)
41
+ * re-implement run in terms of subprocess.run. This fixes a bug that caused stdout to
42
+ disappear.
43
+
44
+ * 0.3.0 (2024-02-01)
45
+ * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
46
+ and RunError which are slightly backwards incompatible.
47
+
40
48
  * 0.2.0 (2024-01-29)
41
49
  * Better static type information
42
50
 
@@ -26,6 +26,14 @@ magicFiles = run(['grep', 'magic'] + files, captureStdout=splitLines, onError='i
26
26
 
27
27
  ## Changelog
28
28
 
29
+ * 0.4.0 (2024-03-20)
30
+ * re-implement run in terms of subprocess.run. This fixes a bug that caused stdout to
31
+ disappear.
32
+
33
+ * 0.3.0 (2024-02-01)
34
+ * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
35
+ and RunError which are slightly backwards incompatible.
36
+
29
37
  * 0.2.0 (2024-01-29)
30
38
  * Better static type information
31
39
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libPyshell
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: Support for writing shell scripts in Python
5
5
  Home-page: https://github.com/skogsbaer/libPyshell
6
6
  Author: Stefan Wehr
@@ -37,6 +37,14 @@ magicFiles = run(['grep', 'magic'] + files, captureStdout=splitLines, onError='i
37
37
 
38
38
  ## Changelog
39
39
 
40
+ * 0.4.0 (2024-03-20)
41
+ * re-implement run in terms of subprocess.run. This fixes a bug that caused stdout to
42
+ disappear.
43
+
44
+ * 0.3.0 (2024-02-01)
45
+ * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
46
+ and RunError which are slightly backwards incompatible.
47
+
40
48
  * 0.2.0 (2024-01-29)
41
49
  * Better static type information
42
50
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from distutils.core import setup
4
4
 
5
- VERSION = '0.2.1'
5
+ VERSION = '0.4.0'
6
6
 
7
7
  with open("README.md", "r", encoding="utf-8") as fh:
8
8
  long_description = fh.read()
@@ -50,7 +50,7 @@ except:
50
50
 
51
51
  DEV_NULL = _devNull
52
52
 
53
- _FILE = Union[int, IO[Any], None]
53
+ _FILE = Union[int, IO[Any]]
54
54
 
55
55
  atexit.register(lambda: DEV_NULL.close())
56
56
 
@@ -94,11 +94,12 @@ class RunResult:
94
94
  attribute `stdout` contains the output printed in stdout (only if `run`
95
95
  was invoked with `captureStdout=True`).
96
96
  """
97
- def __init__(self, stdout: Any, exitcode: int):
97
+ def __init__(self, stdout: Any, stderr: Any, exitcode: int):
98
98
  self.stdout = stdout
99
+ self.stderr = stderr
99
100
  self.exitcode = exitcode
100
101
  def __repr__(self):
101
- return 'RunResult(exitcode=%d, stdout=%r) '% (self.exitcode, self.stdout)
102
+ return 'RunResult(exitcode=%d, stdout=%r, stderr=%r)' % (self.exitcode, self.stdout, self.stderr)
102
103
  def __eq__(self, other: Any):
103
104
  if type(other) is type(self):
104
105
  return self.__dict__ == other.__dict__
@@ -127,14 +128,19 @@ class RunError(ShellError):
127
128
  """
128
129
  def __init__(self, cmd: Union[str, list[str]],
129
130
  exitcode: int,
130
- stderr: Union[str,bytes,None]=None):
131
+ stdout: Union[str,bytes],
132
+ stderr: Union[str,bytes]):
131
133
  self.cmd = cmd
132
134
  self.exitcode = exitcode
133
135
  self.stderr = stderr
136
+ self.stdout = stdout
134
137
  msg = 'Command ' + repr(self.cmd) + " failed with exit code " + str(self.exitcode)
135
138
  if stderr:
136
139
  msg = msg + '\nstderr:\n' + str(stderr)
137
140
  super(RunError, self).__init__(msg)
141
+ def __repr__(self):
142
+ return 'RunError(cmd=%r, exitcode=%d, stdout=%r, stderr=%r)' % \
143
+ (self.cmd, self.exitcode, self.stdout, self.stderr)
138
144
 
139
145
  def splitOn(splitter: str) -> Callable[[str], list[str]]:
140
146
  """Return a function that splits a string on the given splitter string.
@@ -173,19 +179,55 @@ def splitLines(s: str) -> list[str]:
173
179
  else:
174
180
  return s.split('\n')
175
181
 
182
+ def _decode(input: Union[str, bytes, None], encoding: str, errors: str) -> Optional[bytes]:
183
+ inputBytes: Optional[bytes] = None
184
+ if input and isinstance(input, str):
185
+ if encoding != 'raw':
186
+ inputBytes = input.encode(encoding, errors)
187
+ else:
188
+ raise ValueError('Given str object as input, but encoding is raw')
189
+ elif input:
190
+ inputBytes = input
191
+ return inputBytes
192
+
193
+ CaptureType = Union[bool, Callable[[str], Any], _FILE, None]
194
+
195
+ def _handleCapture(capture: CaptureType) -> Optional[_FILE]:
196
+ if capture == True:
197
+ return subprocess.PIPE
198
+ elif callable(capture):
199
+ return subprocess.PIPE
200
+ elif capture is None:
201
+ return None
202
+ else:
203
+ return capture
204
+
205
+ def _massageOutput(data: Any, encoding: str, decodeErrors: Optional[str],
206
+ capture: CaptureType):
207
+ if not decodeErrors:
208
+ decodeErrors = 'strict'
209
+ if data and encoding != 'raw':
210
+ data = data.decode(encoding, errors=decodeErrors)
211
+ if not data:
212
+ data = ''
213
+ if isinstance(capture, Callable) and isinstance(data, str):
214
+ data = capture(data)
215
+ return data
216
+
176
217
  def run(cmd: Union[list[str], str],
177
218
  onError: Literal['raise', 'die', 'ignore']='raise',
178
219
  input: Union[str, bytes, None]=None,
179
220
  encoding: str='utf-8',
180
- captureStdout: Union[bool,Callable[[str], Any],_FILE]=False,
181
- captureStderr: Union[bool,Callable[[str], Any],_FILE]=False,
221
+ captureStdout: Union[bool, Callable[[str], Any], _FILE, None]=False,
222
+ captureStderr: Union[bool, Callable[[str], Any], _FILE, None]=False,
182
223
  stderrToStdout: bool=False,
183
224
  cwd: Optional[str]=None,
184
225
  env: Optional[Dict[str, str]]=None,
185
226
  freshEnv: Optional[Dict[str, str]]=None,
186
227
  decodeErrors: str='replace',
187
228
  decodeErrorsStdout: Optional[str]=None,
188
- decodeErrorsStderr: Optional[str]=None
229
+ decodeErrorsStderr: Optional[str]=None,
230
+ encodeErrorsStdin: Optional[str]=None
189
231
  ) -> RunResult:
190
232
  """Runs the given command.
191
233
 
@@ -211,10 +253,11 @@ def run(cmd: Union[list[str], str],
211
253
  * `stderrToStdout`: should stderr be sent to stdout?
212
254
  * `cwd`: working directory
213
255
  * `env`: dictionary with additional environment variables.
214
- * `freshEnv`: dictionary with a completely fresh environment.
215
- * `decodeErrors`: how to handle decoding errors on stdout and stderr.
216
- * `decodeErrorsStdout` and `decodeErrorsStderr`: overwrite the value of decodeErrors for stdout
217
- or stderr
256
+ * `freshEnv`: dictionary with a completely fresh environment. If `env` is also given, then
257
+ `freshEnv` is ignored.
258
+ * `decodeErrors`: how to handle decoding/encoding errors on stdout and stderr and stdin.
259
+ * `decodeErrorsStdout` and `decodeErrorsStderr` and `encodeErrorsStdin`: overwrite the value
260
+ of decodeErrors for stdout or stderr or stdin
218
261
 
219
262
  Returns:
220
263
  a `RunResult` value, given access to the captured stdout of the child process (if it was
@@ -222,105 +265,67 @@ def run(cmd: Union[list[str], str],
222
265
 
223
266
  Raises: a `RunError` if `onError='raise'` and the command terminates with a non-zero exit code.
224
267
 
225
- Starting with Python 3.5, the `subprocess` module defines a similar function.
226
-
227
- >>> run('/bin/echo foo') == RunResult(exitcode=0, stdout='')
228
- True
229
- >>> run('/bin/echo -n foo', captureStdout=True) == RunResult(exitcode=0, stdout='foo')
230
- True
231
- >>> run('/bin/echo -n foo', captureStdout=lambda s: s + 'X') == \
232
- RunResult(exitcode=0, stdout='fooX')
233
- True
234
- >>> run('/bin/echo foo', captureStdout=False) == RunResult(exitcode=0, stdout='')
235
- True
236
- >>> run('cat', captureStdout=True, input='blub') == RunResult(exitcode=0, stdout='blub')
237
- True
268
+ Starting with Python 3.5, the `subprocess` module defines a similar function. This function
269
+ is just a wrapper for it.
270
+
271
+ >>> run('/bin/echo foo')
272
+ RunResult(exitcode=0, stdout='', stderr='')
273
+ >>> run('/bin/echo -n foo', captureStdout=True)
274
+ RunResult(exitcode=0, stdout='foo', stderr='')
275
+ >>> run('/bin/echo -n foo', captureStdout=lambda s: s + 'X')
276
+ RunResult(exitcode=0, stdout='fooX', stderr='')
277
+ >>> run('/bin/echo foo', captureStdout=False)
278
+ RunResult(exitcode=0, stdout='', stderr='')
279
+ >>> run('cat', captureStdout=True, input='blub')
280
+ RunResult(exitcode=0, stdout='blub', stderr='')
238
281
  >>> try:
239
- ... run('false')
282
+ ... run('/bin/echo -n foo 1>&2; /bin/echo -n bar; false', captureStdout=True, captureStderr=True)
240
283
  ... raise 'exception expected'
241
- ... except RunError:
242
- ... pass
284
+ ... except RunError as e:
285
+ ... print(repr(e))
243
286
  ...
244
- >>> run('false', onError='ignore') == RunResult(exitcode=1, stdout='')
245
- True
246
- """
247
- if type(cmd) != str and type(cmd) != list:
248
- raise ShellError('cmd parameter must be a string or a list')
249
- if type(cmd) == str:
250
- cmd = cmd.replace('\x00', ' ')
251
- cmd = cmd.replace('\n', ' ')
252
- if decodeErrorsStdout is None:
253
- decodeErrorsStdout = decodeErrors
254
- if decodeErrorsStderr is None:
255
- decodeErrorsStderr = decodeErrors
256
- stdoutIsFileLike = isinstance(captureStdout, int) or isinstance(captureStdout, IO)
257
- stdoutIsProcFun = not stdoutIsFileLike and isinstance(captureStdout, Callable)
258
- shouldReturnStdout = (stdoutIsProcFun or
259
- (type(captureStdout) == bool and captureStdout))
260
- stdout: _FILE = None
261
- if shouldReturnStdout:
262
- stdout = subprocess.PIPE
263
- elif isinstance(captureStdout, int) or isinstance(captureStdout, IO):
264
- stdout = captureStdout
265
- stdin = None
266
- if input:
267
- stdin = subprocess.PIPE
268
- stderr = None
287
+ RunError(cmd='/bin/echo -n foo 1>&2; /bin/echo -n bar; false', exitcode=1, stdout='bar', stderr='foo')
288
+ >>> run('false', onError='ignore')
289
+ RunResult(exitcode=1, stdout='', stderr='')
290
+ >>> run('/bin/echo -n foo; /bin/echo -n bar 1>&2', captureStdout=True, captureStderr=True)
291
+ RunResult(exitcode=0, stdout='foo', stderr='bar')
292
+ >>> run('/bin/echo -n foo 1>&2; /bin/echo -n bar', captureStderr=lambda s: s + 'X')
293
+ RunResult(exitcode=0, stdout='', stderr='fooX')
294
+ """
295
+ shell = isinstance(cmd, str)
296
+ input = _decode(input, encoding, encodeErrorsStdin or decodeErrors)
297
+ stdout = _handleCapture(captureStdout)
269
298
  if stderrToStdout:
270
299
  stderr = subprocess.STDOUT
271
- elif captureStderr:
272
- stderr = subprocess.PIPE
273
- input_str = 'None'
274
- inputBytes: Optional[bytes] = None
275
- if input and isinstance(input, str):
276
- input_str = '<' + str(len(input)) + ' characters>'
277
- if encoding != 'raw':
278
- inputBytes = input.encode(encoding)
279
- else:
280
- raise ValueError('Given str object as input, but encoding is raw')
281
- elif input:
282
- inputBytes = input
283
- _debug('Running command ' + repr(cmd) + ' with captureStdout=' + str(captureStdout) +
284
- ', onError=' + onError + ', input=' + input_str)
285
- popenEnv = None
300
+ else:
301
+ stderr = _handleCapture(captureStderr)
302
+ runEnv = None
286
303
  if env:
287
- popenEnv = os.environ.copy()
288
- popenEnv.update(env)
304
+ runEnv = os.environ.copy()
305
+ runEnv.update(env)
289
306
  elif freshEnv:
290
- popenEnv = freshEnv.copy()
307
+ runEnv = freshEnv.copy()
291
308
  if env:
292
- popenEnv.update(env)
309
+ runEnv.update(env)
293
310
  # Ensure correct ordering of outputs
294
311
  if stdout is None:
295
312
  sys.stdout.flush()
296
313
  if stderr is None:
297
314
  sys.stderr.flush()
298
- pipe = subprocess.Popen(
299
- cmd, shell=(type(cmd) == str),
300
- stdout=stdout, stdin=stdin, stderr=stderr,
301
- cwd=cwd, env=popenEnv
302
- )
303
- (stdoutData, stderrData) = pipe.communicate(input=inputBytes)
304
- if stdoutData and encoding != 'raw':
305
- stdoutData = stdoutData.decode(encoding, errors=decodeErrorsStdout)
306
- if stderrData and encoding != 'raw':
307
- stderrData = stderrData.decode(encoding, errors=decodeErrorsStderr)
308
- exitcode = pipe.returncode
315
+ if _PYSHELL_DEBUG:
316
+ _debug(f'subprocess.run({cmd}, shell={shell}, input={input}, stdout={stdout}, ' \
317
+ f'stderr={stderr}, cwd={cwd}, env={runEnv})')
318
+ res = subprocess.run(cmd, shell=shell, input=input, stdout=stdout, stderr=stderr, cwd=cwd,
319
+ env=runEnv)
320
+ stdoutData = _massageOutput(res.stdout, encoding, decodeErrorsStdout or decodeErrors, captureStdout)
321
+ stderrData = _massageOutput(res.stderr, encoding, decodeErrorsStderr or decodeErrors, captureStderr)
322
+ exitcode = res.returncode
309
323
  if onError == 'raise' and exitcode != 0:
310
- d = stderrData
311
- if stderrToStdout:
312
- d = stdoutData
313
- err = RunError(cmd, exitcode, d)
324
+ err = RunError(cmd, exitcode, stdoutData, stderrData)
314
325
  raise err
315
326
  if onError == 'die' and exitcode != 0:
316
327
  sys.exit(exitcode)
317
- stdoutRes = stdoutData
318
- if not stdoutRes:
319
- stdoutRes = ''
320
- if not stdoutIsFileLike and isinstance(captureStdout, Callable) and \
321
- isinstance(stdoutData, str):
322
- stdoutRes = captureStdout(stdoutData)
323
- return RunResult(stdoutRes, exitcode)
328
+ return RunResult(stdoutData, stderrData, exitcode)
324
329
 
325
330
  # the quote function is stolen from https://hg.python.org/cpython/file/3.5/Lib/shlex.py
326
331
  _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
File without changes
File without changes
File without changes