libPyshell 0.2.0__tar.gz → 0.3.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,11 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libPyshell
3
- Version: 0.2.0
3
+ Version: 0.3.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
7
7
  Author-email: stefan.wehr@gmail.com
8
- Requires-Python: >=3.6
8
+ Requires-Python: >=3.9
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
 
@@ -37,6 +37,10 @@ magicFiles = run(['grep', 'magic'] + files, captureStdout=splitLines, onError='i
37
37
 
38
38
  ## Changelog
39
39
 
40
+ * 0.3.0 (2024-02-01)
41
+ * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
42
+ and RunError which are slightly backwards incompatible.
43
+
40
44
  * 0.2.0 (2024-01-29)
41
45
  * Better static type information
42
46
 
@@ -26,6 +26,10 @@ magicFiles = run(['grep', 'magic'] + files, captureStdout=splitLines, onError='i
26
26
 
27
27
  ## Changelog
28
28
 
29
+ * 0.3.0 (2024-02-01)
30
+ * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
31
+ and RunError which are slightly backwards incompatible.
32
+
29
33
  * 0.2.0 (2024-01-29)
30
34
  * Better static type information
31
35
 
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libPyshell
3
- Version: 0.2.0
3
+ Version: 0.3.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
7
7
  Author-email: stefan.wehr@gmail.com
8
- Requires-Python: >=3.6
8
+ Requires-Python: >=3.9
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
 
@@ -37,6 +37,10 @@ magicFiles = run(['grep', 'magic'] + files, captureStdout=splitLines, onError='i
37
37
 
38
38
  ## Changelog
39
39
 
40
+ * 0.3.0 (2024-02-01)
41
+ * uniform treatment when capturing stdout and stderr. This lead to changes to RunResult
42
+ and RunError which are slightly backwards incompatible.
43
+
40
44
  * 0.2.0 (2024-01-29)
41
45
  * Better static type information
42
46
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from distutils.core import setup
4
4
 
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
 
7
7
  with open("README.md", "r", encoding="utf-8") as fh:
8
8
  long_description = fh.read()
@@ -17,5 +17,5 @@ setup(name='libPyshell',
17
17
  url='https://github.com/skogsbaer/libPyshell',
18
18
  package_dir={'shell': 'src'},
19
19
  packages=['shell'],
20
- python_requires='>=3.6'
20
+ python_requires='>=3.9'
21
21
  )
@@ -50,7 +50,7 @@ except:
50
50
 
51
51
  DEV_NULL = _devNull
52
52
 
53
- _FILE: TypeAlias = None | int | IO[Any]
53
+ _FILE = Union[int, IO[Any], None]
54
54
 
55
55
  atexit.register(lambda: DEV_NULL.close())
56
56
 
@@ -62,7 +62,7 @@ def fatal(s: str):
62
62
  """Display an error message to stderr."""
63
63
  sys.stderr.write('ERROR: ' + str(s) + '\n')
64
64
 
65
- def resolveProg(*l: str) -> str | None:
65
+ def resolveProg(*l: str) -> Optional[str]:
66
66
  """Return the first program in the list that exist and is runnable.
67
67
  >>> resolveProg()
68
68
  >>> resolveProg('foobarbaz', 'cat', 'grep')
@@ -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__
@@ -125,14 +126,21 @@ class RunError(ShellError):
125
126
  * `exitcode`
126
127
  * `stderr`: output on stderr (if `run` configured to capture this output)
127
128
  """
128
- def __init__(self, cmd: str | list[str], exitcode: int, stderr: str|bytes|None=None):
129
+ def __init__(self, cmd: Union[str, list[str]],
130
+ exitcode: int,
131
+ stdout: Union[str,bytes],
132
+ stderr: Union[str,bytes]):
129
133
  self.cmd = cmd
130
134
  self.exitcode = exitcode
131
135
  self.stderr = stderr
136
+ self.stdout = stdout
132
137
  msg = 'Command ' + repr(self.cmd) + " failed with exit code " + str(self.exitcode)
133
138
  if stderr:
134
139
  msg = msg + '\nstderr:\n' + str(stderr)
135
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)
136
144
 
137
145
  def splitOn(splitter: str) -> Callable[[str], list[str]]:
138
146
  """Return a function that splits a string on the given splitter string.
@@ -173,10 +181,10 @@ def splitLines(s: str) -> list[str]:
173
181
 
174
182
  def run(cmd: Union[list[str], str],
175
183
  onError: Literal['raise', 'die', 'ignore']='raise',
176
- input: str | bytes | None=None,
184
+ input: Union[str, bytes, None]=None,
177
185
  encoding: str='utf-8',
178
- captureStdout: bool|Callable[[str|bytes], Any]|_FILE=False,
179
- captureStderr: bool|Callable[[str|bytes], Any]|_FILE=False,
186
+ captureStdout: Union[bool,Callable[[str], Any],_FILE]=False,
187
+ captureStderr: Union[bool,Callable[[str], Any],_FILE]=False,
180
188
  stderrToStdout: bool=False,
181
189
  cwd: Optional[str]=None,
182
190
  env: Optional[Dict[str, str]]=None,
@@ -203,7 +211,8 @@ def run(cmd: Union[list[str], str],
203
211
  * False: stdout is not captured and goes to stdout of the parent process (the default)
204
212
  * True: stdout is captured and returned
205
213
  * A function: stdout is captured and the result of applying the function to the captured
206
- output is returned. Use splitLines as this function to split the output into lines
214
+ output is returned. In this case, encoding must not be `'raw'`.
215
+ Use splitLines as this function to split the output into lines
207
216
  * An existing file descriptor or a file object: stdout goes to the file descriptor or file
208
217
  * `stderrToStdout`: should stderr be sent to stdout?
209
218
  * `cwd`: working directory
@@ -221,25 +230,29 @@ def run(cmd: Union[list[str], str],
221
230
 
222
231
  Starting with Python 3.5, the `subprocess` module defines a similar function.
223
232
 
224
- >>> run('/bin/echo foo') == RunResult(exitcode=0, stdout='')
225
- True
226
- >>> run('/bin/echo -n foo', captureStdout=True) == RunResult(exitcode=0, stdout='foo')
227
- True
228
- >>> run('/bin/echo -n foo', captureStdout=lambda s: s + 'X') == \
229
- RunResult(exitcode=0, stdout='fooX')
230
- True
231
- >>> run('/bin/echo foo', captureStdout=False) == RunResult(exitcode=0, stdout='')
232
- True
233
- >>> run('cat', captureStdout=True, input='blub') == RunResult(exitcode=0, stdout='blub')
234
- True
233
+ >>> run('/bin/echo foo')
234
+ RunResult(exitcode=0, stdout='', stderr='')
235
+ >>> run('/bin/echo -n foo', captureStdout=True)
236
+ RunResult(exitcode=0, stdout='foo', stderr='')
237
+ >>> run('/bin/echo -n foo', captureStdout=lambda s: s + 'X')
238
+ RunResult(exitcode=0, stdout='fooX', stderr='')
239
+ >>> run('/bin/echo foo', captureStdout=False)
240
+ RunResult(exitcode=0, stdout='', stderr='')
241
+ >>> run('cat', captureStdout=True, input='blub')
242
+ RunResult(exitcode=0, stdout='blub', stderr='')
235
243
  >>> try:
236
- ... run('false')
244
+ ... run('/bin/echo -n foo 1>&2; /bin/echo -n bar; false', captureStdout=True, captureStderr=True)
237
245
  ... raise 'exception expected'
238
- ... except RunError:
239
- ... pass
246
+ ... except RunError as e:
247
+ ... print(repr(e))
240
248
  ...
241
- >>> run('false', onError='ignore') == RunResult(exitcode=1, stdout='')
242
- True
249
+ RunError(cmd='/bin/echo -n foo 1>&2; /bin/echo -n bar; false', exitcode=1, stdout='bar', stderr='foo')
250
+ >>> run('false', onError='ignore')
251
+ RunResult(exitcode=1, stdout='', stderr='')
252
+ >>> run('/bin/echo -n foo; /bin/echo -n bar 1>&2', captureStdout=True, captureStderr=True)
253
+ RunResult(exitcode=0, stdout='foo', stderr='bar')
254
+ >>> run('/bin/echo -n foo 1>&2; /bin/echo -n bar', captureStderr=lambda s: s + 'X')
255
+ RunResult(exitcode=0, stdout='', stderr='fooX')
243
256
  """
244
257
  if type(cmd) != str and type(cmd) != list:
245
258
  raise ShellError('cmd parameter must be a string or a list')
@@ -250,9 +263,7 @@ def run(cmd: Union[list[str], str],
250
263
  decodeErrorsStdout = decodeErrors
251
264
  if decodeErrorsStderr is None:
252
265
  decodeErrorsStderr = decodeErrors
253
- stdoutIsFileLike = isinstance(captureStdout, int) or isinstance(captureStdout, IO)
254
- stdoutIsProcFun = not stdoutIsFileLike and isinstance(captureStdout, Callable)
255
- shouldReturnStdout = (stdoutIsProcFun or
266
+ shouldReturnStdout = (isinstance(captureStdout, Callable) or
256
267
  (type(captureStdout) == bool and captureStdout))
257
268
  stdout: _FILE = None
258
269
  if shouldReturnStdout:
@@ -268,7 +279,7 @@ def run(cmd: Union[list[str], str],
268
279
  elif captureStderr:
269
280
  stderr = subprocess.PIPE
270
281
  input_str = 'None'
271
- inputBytes: bytes | None = None
282
+ inputBytes: Optional[bytes] = None
272
283
  if input and isinstance(input, str):
273
284
  input_str = '<' + str(len(input)) + ' characters>'
274
285
  if encoding != 'raw':
@@ -298,25 +309,25 @@ def run(cmd: Union[list[str], str],
298
309
  cwd=cwd, env=popenEnv
299
310
  )
300
311
  (stdoutData, stderrData) = pipe.communicate(input=inputBytes)
301
- if stdoutData and encoding != 'raw':
302
- stdoutData = stdoutData.decode(encoding, errors=decodeErrorsStdout)
303
- if stderrData and encoding != 'raw':
304
- stderrData = stderrData.decode(encoding, errors=decodeErrorsStderr)
312
+ stdoutData = massageOutput(stdoutData, encoding, decodeErrorsStdout, captureStdout)
313
+ stderrData = massageOutput(stderrData, encoding, decodeErrorsStderr, captureStderr)
305
314
  exitcode = pipe.returncode
306
315
  if onError == 'raise' and exitcode != 0:
307
- d = stderrData
308
- if stderrToStdout:
309
- d = stdoutData
310
- err = RunError(cmd, exitcode, d)
316
+ err = RunError(cmd, exitcode, stdoutData, stderrData)
311
317
  raise err
312
318
  if onError == 'die' and exitcode != 0:
313
319
  sys.exit(exitcode)
314
- stdoutRes = stdoutData
315
- if not stdoutRes:
316
- stdoutRes = ''
317
- if not stdoutIsFileLike and isinstance(captureStdout, Callable):
318
- stdoutRes = captureStdout(stdoutData)
319
- return RunResult(stdoutRes, exitcode)
320
+ return RunResult(stdoutData, stderrData, exitcode)
321
+
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
320
331
 
321
332
  # the quote function is stolen from https://hg.python.org/cpython/file/3.5/Lib/shlex.py
322
333
  _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
@@ -531,7 +542,7 @@ class _ExitHooks(object):
531
542
  sys.exit = self.exit
532
543
  sys.excepthook = self.exc_handler
533
544
 
534
- def exit(self, code: int|None=0):
545
+ def exit(self, code: Optional[int]=0):
535
546
  if code is None:
536
547
  myCode = 0
537
548
  elif type(code) != int:
@@ -569,7 +580,9 @@ def _registerAtExit(action: Any, mode: AtExitMode):
569
580
  _debug('Not running exit action')
570
581
  atexit.register(f)
571
582
 
572
- def mkTempFile(suffix: str='', prefix: str='', dir:str|None=None, deleteAtExit:AtExitMode=True):
583
+ def mkTempFile(suffix: str='', prefix: str='',
584
+ dir:Optional[str]=None,
585
+ deleteAtExit:AtExitMode=True):
573
586
  """Create a temporary file.
574
587
 
575
588
  `deleteAtExit` controls if and how the file is deleted once the shell sript terminates.
@@ -589,7 +602,9 @@ def mkTempFile(suffix: str='', prefix: str='', dir:str|None=None, deleteAtExit:A
589
602
  _registerAtExit(action, deleteAtExit)
590
603
  return f
591
604
 
592
- def mkTempDir(suffix: str='', prefix: str='tmp', dir: str|None=None, deleteAtExit: AtExitMode=True):
605
+ def mkTempDir(suffix: str='', prefix: str='tmp',
606
+ dir: Optional[str]=None,
607
+ deleteAtExit: AtExitMode=True):
593
608
  """Create a temporary directory. The `deleteAtExit` parameter
594
609
  has the same meaning as for `mkTempFile`.
595
610
  """
@@ -615,7 +630,7 @@ class tempDir:
615
630
  With `delete=False`, deletion is deactivated. With `onException=False`, deletion
616
631
  is only performed if the `with`-block finishes without an exception.
617
632
  """
618
- def __init__(self, suffix: str='', prefix: str='tmp', dir: str|None=None,
633
+ def __init__(self, suffix: str='', prefix: str='tmp', dir: Optional[str]=None,
619
634
  onException: bool=True, delete: bool=True):
620
635
  self.suffix = suffix
621
636
  self.prefix = prefix
@@ -644,7 +659,7 @@ def ls(d: str, *globs: str) -> list[str]:
644
659
 
645
660
  The pathnames in the result list contain the directory part `d`.
646
661
 
647
- >>> '../src/shell.py' in ls('../src/', '*.py', '*.txt')
662
+ >>> '../src/__init__.py' in ls('../src/', '*.py', '*.txt')
648
663
  True
649
664
  """
650
665
  res: list[str] = []
File without changes
File without changes
File without changes