libPyshell 0.3.0__tar.gz → 0.4.1__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.1
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.1 (2024-09-12)
41
+ * fix capture handling
42
+ * add failOnError option to rm commands
43
+
44
+ * 0.4.0 (2024-03-20)
45
+ * re-implement run in terms of subprocess.run. This fixes a bug that caused stdout to
46
+ disappear.
47
+
40
48
  * 0.3.0 (2024-02-01)
41
49
  * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
42
50
  and RunError which are slightly backwards incompatible.
@@ -26,6 +26,14 @@ magicFiles = run(['grep', 'magic'] + files, captureStdout=splitLines, onError='i
26
26
 
27
27
  ## Changelog
28
28
 
29
+ * 0.4.1 (2024-09-12)
30
+ * fix capture handling
31
+ * add failOnError option to rm commands
32
+
33
+ * 0.4.0 (2024-03-20)
34
+ * re-implement run in terms of subprocess.run. This fixes a bug that caused stdout to
35
+ disappear.
36
+
29
37
  * 0.3.0 (2024-02-01)
30
38
  * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
31
39
  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.1
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.1 (2024-09-12)
41
+ * fix capture handling
42
+ * add failOnError option to rm commands
43
+
44
+ * 0.4.0 (2024-03-20)
45
+ * re-implement run in terms of subprocess.run. This fixes a bug that caused stdout to
46
+ disappear.
47
+
40
48
  * 0.3.0 (2024-02-01)
41
49
  * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
42
50
  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.1'
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 == False:
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:
@@ -432,13 +430,15 @@ def removeFile(path: str):
432
430
 
433
431
  def cp(src: str, target: str):
434
432
  """
435
- Copy `src` to `target`.
433
+ Copy `src` to `target`. Behaves like the cp shell command.
436
434
 
437
435
  * If `src` is a file and `target` is a file: overwrites `target`.
438
436
  * If `src` is a file and `target` is a dirname: places the copy in directory `target`,
439
437
  with the basename of `src.
440
- * If `src` is a directory then `target` must also be a directory: copies
441
- the `src` directory (*not* its content) to `target`.
438
+ * If `src` is a directory and `target` exists, then `target` must also be a directory:
439
+ copies the `src` directory (*not* its content) to `target`.
440
+ * If `src` is a directory and `target` does not exist: copies the `src` directory
441
+ and names the copy `target`.
442
442
  """
443
443
  if isFile(src):
444
444
  if isDir(target):
@@ -513,22 +513,34 @@ class workingDir:
513
513
  cd(self.old_dir)
514
514
  return False # reraise expection
515
515
 
516
- def rm(path: str, force: bool=False):
516
+ def rm(path: str, force: bool=False, failOnError: bool=True):
517
517
  """
518
518
  Remove the file at `path`.
519
519
  """
520
520
  if force and not exists(path):
521
521
  return
522
- os.remove(path)
522
+ try:
523
+ os.remove(path)
524
+ except Exception as e:
525
+ if failOnError:
526
+ raise
527
+ else:
528
+ sys.stderr.write(str(e) + '\n')
523
529
 
524
- def rmdir(d: str, recursive: bool=False):
530
+ def rmdir(d: str, recursive: bool=False, failOnError: bool=True):
525
531
  """
526
532
  Remove directory `d`. Set `recursive=True` if the directory is not empty.
527
533
  """
528
- if recursive:
529
- shutil.rmtree(d)
530
- else:
531
- os.rmdir(d)
534
+ try:
535
+ if recursive:
536
+ shutil.rmtree(d)
537
+ else:
538
+ os.rmdir(d)
539
+ except Exception as e:
540
+ if failOnError:
541
+ raise
542
+ else:
543
+ sys.stderr.write(str(e) + '\n')
532
544
 
533
545
  # See https://stackoverflow.com/questions/9741351/how-to-find-exit-code-or-reason-when-atexit-callback-is-called-in-python
534
546
  class _ExitHooks(object):
@@ -583,7 +595,7 @@ def _registerAtExit(action: Any, mode: AtExitMode):
583
595
  def mkTempFile(suffix: str='', prefix: str='',
584
596
  dir:Optional[str]=None,
585
597
  deleteAtExit:AtExitMode=True):
586
- """Create a temporary file.
598
+ """Create a temporary file name.
587
599
 
588
600
  `deleteAtExit` controls if and how the file is deleted once the shell sript terminates.
589
601
  It has one of the following values.
@@ -598,7 +610,7 @@ def mkTempFile(suffix: str='', prefix: str='',
598
610
  if deleteAtExit:
599
611
  def action():
600
612
  if isFile(f):
601
- rm(f)
613
+ rm(f, failOnError=False)
602
614
  _registerAtExit(action, deleteAtExit)
603
615
  return f
604
616
 
@@ -612,7 +624,7 @@ def mkTempDir(suffix: str='', prefix: str='tmp',
612
624
  if deleteAtExit:
613
625
  def action():
614
626
  if isDir(d):
615
- rmdir(d, True)
627
+ rmdir(d, recursive=True, failOnError=False)
616
628
  _registerAtExit(action, deleteAtExit)
617
629
  return d
618
630
 
@@ -648,7 +660,7 @@ class tempDir:
648
660
  return False # reraise
649
661
  if self.delete:
650
662
  if isDir(self.dir_to_delete):
651
- rmdir(self.dir_to_delete, recursive=True)
663
+ rmdir(self.dir_to_delete, recursive=True, failOnError=False)
652
664
  return False # reraise expection
653
665
 
654
666
  def ls(d: str, *globs: str) -> list[str]:
File without changes
File without changes
File without changes