libPyshell 0.3.0__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.3.0
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,10 @@ 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
+
40
44
  * 0.3.0 (2024-02-01)
41
45
  * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
42
46
  and RunError which are slightly backwards incompatible.
@@ -26,6 +26,10 @@ 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
+
29
33
  * 0.3.0 (2024-02-01)
30
34
  * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
31
35
  and RunError which are slightly backwards incompatible.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libPyshell
3
- Version: 0.3.0
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,10 @@ 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
+
40
44
  * 0.3.0 (2024-02-01)
41
45
  * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
42
46
  and RunError which are slightly backwards incompatible.
@@ -2,7 +2,7 @@
2
2
 
3
3
  from distutils.core import setup
4
4
 
5
- VERSION = '0.3.0'
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
 
@@ -179,19 +179,55 @@ def splitLines(s: str) -> list[str]:
179
179
  else:
180
180
  return s.split('\n')
181
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
+
182
217
  def run(cmd: Union[list[str], str],
183
218
  onError: Literal['raise', 'die', 'ignore']='raise',
184
219
  input: Union[str, bytes, None]=None,
185
220
  encoding: str='utf-8',
186
- captureStdout: Union[bool,Callable[[str], Any],_FILE]=False,
187
- 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,
188
223
  stderrToStdout: bool=False,
189
224
  cwd: Optional[str]=None,
190
225
  env: Optional[Dict[str, str]]=None,
191
226
  freshEnv: Optional[Dict[str, str]]=None,
192
227
  decodeErrors: str='replace',
193
228
  decodeErrorsStdout: Optional[str]=None,
194
- decodeErrorsStderr: Optional[str]=None
229
+ decodeErrorsStderr: Optional[str]=None,
230
+ encodeErrorsStdin: Optional[str]=None
195
231
  ) -> RunResult:
196
232
  """Runs the given command.
197
233
 
@@ -217,10 +253,11 @@ def run(cmd: Union[list[str], str],
217
253
  * `stderrToStdout`: should stderr be sent to stdout?
218
254
  * `cwd`: working directory
219
255
  * `env`: dictionary with additional environment variables.
220
- * `freshEnv`: dictionary with a completely fresh environment.
221
- * `decodeErrors`: how to handle decoding errors on stdout and stderr.
222
- * `decodeErrorsStdout` and `decodeErrorsStderr`: overwrite the value of decodeErrors for stdout
223
- 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
224
261
 
225
262
  Returns:
226
263
  a `RunResult` value, given access to the captured stdout of the child process (if it was
@@ -228,7 +265,8 @@ def run(cmd: Union[list[str], str],
228
265
 
229
266
  Raises: a `RunError` if `onError='raise'` and the command terminates with a non-zero exit code.
230
267
 
231
- Starting with Python 3.5, the `subprocess` module defines a similar function.
268
+ Starting with Python 3.5, the `subprocess` module defines a similar function. This function
269
+ is just a wrapper for it.
232
270
 
233
271
  >>> run('/bin/echo foo')
234
272
  RunResult(exitcode=0, stdout='', stderr='')
@@ -254,64 +292,34 @@ def run(cmd: Union[list[str], str],
254
292
  >>> run('/bin/echo -n foo 1>&2; /bin/echo -n bar', captureStderr=lambda s: s + 'X')
255
293
  RunResult(exitcode=0, stdout='', stderr='fooX')
256
294
  """
257
- if type(cmd) != str and type(cmd) != list:
258
- raise ShellError('cmd parameter must be a string or a list')
259
- if type(cmd) == str:
260
- cmd = cmd.replace('\x00', ' ')
261
- cmd = cmd.replace('\n', ' ')
262
- if decodeErrorsStdout is None:
263
- decodeErrorsStdout = decodeErrors
264
- if decodeErrorsStderr is None:
265
- decodeErrorsStderr = decodeErrors
266
- shouldReturnStdout = (isinstance(captureStdout, Callable) or
267
- (type(captureStdout) == bool and captureStdout))
268
- stdout: _FILE = None
269
- if shouldReturnStdout:
270
- stdout = subprocess.PIPE
271
- elif isinstance(captureStdout, int) or isinstance(captureStdout, IO):
272
- stdout = captureStdout
273
- stdin = None
274
- if input:
275
- stdin = subprocess.PIPE
276
- stderr = None
295
+ shell = isinstance(cmd, str)
296
+ input = _decode(input, encoding, encodeErrorsStdin or decodeErrors)
297
+ stdout = _handleCapture(captureStdout)
277
298
  if stderrToStdout:
278
299
  stderr = subprocess.STDOUT
279
- elif captureStderr:
280
- stderr = subprocess.PIPE
281
- input_str = 'None'
282
- inputBytes: Optional[bytes] = None
283
- if input and isinstance(input, str):
284
- input_str = '<' + str(len(input)) + ' characters>'
285
- if encoding != 'raw':
286
- inputBytes = input.encode(encoding)
287
- else:
288
- raise ValueError('Given str object as input, but encoding is raw')
289
- elif input:
290
- inputBytes = input
291
- _debug('Running command ' + repr(cmd) + ' with captureStdout=' + str(captureStdout) +
292
- ', onError=' + onError + ', input=' + input_str)
293
- popenEnv = None
300
+ else:
301
+ stderr = _handleCapture(captureStderr)
302
+ runEnv = None
294
303
  if env:
295
- popenEnv = os.environ.copy()
296
- popenEnv.update(env)
304
+ runEnv = os.environ.copy()
305
+ runEnv.update(env)
297
306
  elif freshEnv:
298
- popenEnv = freshEnv.copy()
307
+ runEnv = freshEnv.copy()
299
308
  if env:
300
- popenEnv.update(env)
309
+ runEnv.update(env)
301
310
  # Ensure correct ordering of outputs
302
311
  if stdout is None:
303
312
  sys.stdout.flush()
304
313
  if stderr is None:
305
314
  sys.stderr.flush()
306
- pipe = subprocess.Popen(
307
- cmd, shell=(type(cmd) == str),
308
- stdout=stdout, stdin=stdin, stderr=stderr,
309
- cwd=cwd, env=popenEnv
310
- )
311
- (stdoutData, stderrData) = pipe.communicate(input=inputBytes)
312
- stdoutData = massageOutput(stdoutData, encoding, decodeErrorsStdout, captureStdout)
313
- stderrData = massageOutput(stderrData, encoding, decodeErrorsStderr, captureStderr)
314
- 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
315
323
  if onError == 'raise' and exitcode != 0:
316
324
  err = RunError(cmd, exitcode, stdoutData, stderrData)
317
325
  raise err
@@ -319,16 +327,6 @@ def run(cmd: Union[list[str], str],
319
327
  sys.exit(exitcode)
320
328
  return RunResult(stdoutData, stderrData, exitcode)
321
329
 
322
- def massageOutput(data: Any, encoding: str, decodeErrors: str,
323
- capture: Union[bool,Callable[[str], Any],_FILE]):
324
- if data and encoding != 'raw':
325
- data = data.decode(encoding, errors=decodeErrors)
326
- if not data:
327
- data = ''
328
- if isinstance(capture, Callable) and isinstance(data, str):
329
- data = capture(data)
330
- return data
331
-
332
330
  # the quote function is stolen from https://hg.python.org/cpython/file/3.5/Lib/shlex.py
333
331
  _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
334
332
  def quote(s: str) -> str:
File without changes
File without changes
File without changes