shelltastic 0.6.0__tar.gz → 0.7.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.
Files changed (23) hide show
  1. {shelltastic-0.6.0 → shelltastic-0.7.0}/PKG-INFO +1 -1
  2. {shelltastic-0.6.0 → shelltastic-0.7.0}/pyproject.toml +1 -1
  3. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/backend/default/common.py +132 -169
  4. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/backend/default/local.py +0 -2
  5. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/backend/default/remote.py +3 -9
  6. shelltastic-0.7.0/src/shelltastic/result.py +101 -0
  7. shelltastic-0.6.0/src/shelltastic/result.py +0 -47
  8. {shelltastic-0.6.0 → shelltastic-0.7.0}/LICENSE +0 -0
  9. {shelltastic-0.6.0 → shelltastic-0.7.0}/README.md +0 -0
  10. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/__init__.py +0 -0
  11. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/backend/__init__.py +0 -0
  12. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/backend/base.py +0 -0
  13. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/backend/default/__init__.py +0 -0
  14. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/display.py +0 -0
  15. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/enum.py +0 -0
  16. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/exception.py +0 -0
  17. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/frontend/__init__.py +0 -0
  18. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/frontend/common.py +0 -0
  19. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/frontend/git.py +0 -0
  20. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/frontend/scp.py +0 -0
  21. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/frontend/shell.py +0 -0
  22. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/host.py +0 -0
  23. {shelltastic-0.6.0 → shelltastic-0.7.0}/src/shelltastic/io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: A fantastic shell command runner for python
5
5
  Author: Bearmine
6
6
  License-Expression: MPL-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shelltastic"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "A fantastic shell command runner for python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import logging
4
2
  import os
5
3
  import shlex
@@ -7,8 +5,10 @@ import subprocess
7
5
  import time
8
6
  from io import RawIOBase
9
7
  from multiprocessing import Pipe
8
+ from multiprocessing.connection import Connection
10
9
  from multiprocessing.pool import ThreadPool
11
- from typing import IO, TYPE_CHECKING, Literal
10
+ from pathlib import Path
11
+ from typing import IO, Literal
12
12
 
13
13
  from shelltastic import display
14
14
  from shelltastic.backend.base import ShellBackend
@@ -16,10 +16,6 @@ from shelltastic.enum import CaptureMode, DisplayMode
16
16
  from shelltastic.io import PipeReader
17
17
  from shelltastic.result import ShellResult, ShellResultAsync
18
18
 
19
- if TYPE_CHECKING:
20
- from multiprocessing.connection import Connection
21
- from pathlib import Path
22
-
23
19
  LOGGER = logging.getLogger(__name__)
24
20
 
25
21
 
@@ -91,45 +87,27 @@ class IOTimeoutStream:
91
87
  return return_val
92
88
 
93
89
 
94
- def _output_and_collect(
95
- io: IO[bytes] | None, display: display.IODisplay | None, collect: bool = True
96
- ) -> bytes | None:
97
- if io is None:
98
- return None
99
-
100
- if collect:
101
- if display:
102
- return_bytes: bytes = b""
103
- for line in IOTimeoutStream(io):
104
- return_bytes += line
105
- display.printbytes(line)
106
- return return_bytes
107
- return io.read()
108
-
109
- if display:
110
- for line in IOTimeoutStream(io):
111
- display.printbytes(line)
112
-
113
- return None
114
-
115
-
116
90
  def _output_and_pipe(
117
91
  io: IO[bytes] | None, display: display.IODisplay | None, pipe: Connection | None
118
92
  ) -> None:
119
- if io is None:
120
- return
93
+ try:
94
+ if io is None:
95
+ return
96
+
97
+ if pipe:
98
+ if display:
99
+ for line in IOTimeoutStream(io):
100
+ pipe.send_bytes(line)
101
+ display.printbytes(line)
102
+ for line in IOTimeoutStream(io):
103
+ pipe.send_bytes(line)
121
104
 
122
- if pipe:
123
105
  if display:
124
106
  for line in IOTimeoutStream(io):
125
- pipe.send_bytes(line)
126
107
  display.printbytes(line)
127
- for line in IOTimeoutStream(io):
128
- pipe.send_bytes(line)
129
-
130
- if display:
131
- for line in IOTimeoutStream(io):
132
- display.printbytes(line)
108
+ finally:
109
+ if pipe:
110
+ pipe.close()
133
111
 
134
112
 
135
113
  def _determine_subprocess_output_mode(
@@ -184,50 +162,120 @@ class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
184
162
  self,
185
163
  cmd: str | list[str],
186
164
  popen: subprocess.Popen,
165
+ stdout_capture: CaptureMode | None,
166
+ stderr_capture: CaptureMode | None,
187
167
  stdout_display: display.IODisplay | None,
188
168
  stderr_display: display.IODisplay | None,
169
+ check: bool,
189
170
  ) -> None:
190
- stdout_rcv, stdout_snd = Pipe(False)
191
- stderr_rcv, stderr_snd = Pipe(False)
192
- super().__init__(cmd, PipeReader(stdout_rcv), PipeReader(stderr_rcv))
193
- self._popen = popen
194
- self._display_pool = ThreadPool(2)
195
-
196
- # Print out and capture stdout
197
- self._stdout_result = self._display_pool.apply_async(
198
- _output_and_pipe,
199
- args=[
200
- popen.stdout,
201
- stdout_display,
202
- stdout_snd,
203
- ],
204
- )
171
+ _display_pool = None
172
+ _stdout_async_result = None
173
+ _stderr_async_result = None
174
+
175
+ if popen.stdout:
176
+ # If stdout is PIPE, then setup reader
177
+ if stdout_capture == CaptureMode.PIPE:
178
+ stdout_rcv, stdout_snd = Pipe(False)
179
+ stdout = PipeReader(stdout_rcv)
180
+ else:
181
+ stdout_snd = None
182
+ stdout = None
183
+
184
+ if stdout_snd or stdout_display:
185
+ if _display_pool is None:
186
+ _display_pool = ThreadPool(2)
187
+
188
+ _stdout_async_result=_display_pool.apply_async(
189
+ _output_and_pipe,
190
+ args=[
191
+ popen.stdout,
192
+ stdout_display,
193
+ stdout_snd,
194
+ ],
195
+ )
196
+ else:
197
+ stdout = None
205
198
 
206
- # Print out and capture stderr
207
- self._stderr_result = self._display_pool.apply_async(
208
- _output_and_pipe,
209
- args=[
210
- popen.stderr,
211
- stderr_display,
212
- stderr_snd,
213
- ],
214
- )
199
+ if popen.stderr:
200
+ if stderr_capture == CaptureMode.PIPE:
201
+ stderr_rcv, stderr_snd = Pipe(False)
202
+ stderr = PipeReader(stderr_rcv)
203
+ else:
204
+ stderr_snd = None
205
+ stderr = None
206
+
207
+ if stderr_snd or stderr_display:
208
+ if _display_pool is None:
209
+ _display_pool = ThreadPool(2)
210
+
211
+ _stderr_async_result = _display_pool.apply_async(
212
+ _output_and_pipe,
213
+ args=[
214
+ popen.stderr,
215
+ stderr_display,
216
+ stderr_snd,
217
+ ],
218
+ )
219
+ else:
220
+ stderr = None
221
+
222
+ super().__init__(cmd, stdout, stderr)
223
+
224
+ self._display_pool = _display_pool
225
+ self._stdout_async_result = _stdout_async_result
226
+ self._stderr_async_result = _stderr_async_result
227
+ self._popen = popen
228
+ self._check = check
215
229
 
216
230
  def __exit__(self, exc_type, exc, tb):
231
+ # Wait for command to finish before closing everything
232
+ self.wait()
233
+
234
+ print("closing popen")
235
+ # Close popen
217
236
  self._popen.__exit__(exc_type, exc, tb)
218
- self._display_pool.__exit__(exc_type, exc, tb)
237
+
238
+ if self._display_pool:
239
+ print("closing _display_pool")
240
+ self._display_pool.__exit__(exc_type, exc, tb)
241
+
242
+ # Close stdout and stderr
243
+ if self.stdout:
244
+ print("closing stdout")
245
+ self.stdout.close()
246
+ if self.stderr:
247
+ print("closing stderr")
248
+ self.stderr.close()
219
249
 
220
250
  def close(self) -> None:
221
251
  self.__exit__(None, None, None)
222
252
 
253
+ def terminate(self) -> None:
254
+ self._popen.terminate()
255
+ self.close()
256
+
223
257
  def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
224
- return ShellResult(
258
+ # Get result
259
+ result = ShellResult(
225
260
  cmd=self.cmd,
226
261
  returncode=self._popen.wait(),
227
262
  stdout=self.stdout,
228
263
  stderr=self.stderr,
229
264
  )
230
265
 
266
+ # Wait for our async reader to be complete
267
+ if self._stdout_async_result:
268
+ self._stdout_async_result.wait()
269
+
270
+ if self._stderr_async_result:
271
+ self._stderr_async_result.wait()
272
+
273
+ # Check if set
274
+ if self._check:
275
+ result.check_returncode()
276
+
277
+ return result
278
+
231
279
 
232
280
  class CommonDefaultBackend(ShellBackend):
233
281
  __slots__ = ()
@@ -245,106 +293,18 @@ class CommonDefaultBackend(ShellBackend):
245
293
  echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
246
294
  **kwargs,
247
295
  ) -> ShellResult[bytes | None, bytes | None]:
248
- if kwargs:
249
- raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
250
-
251
- # If cmd is a str, run as shell
252
- shell = isinstance(cmd, str)
253
-
254
- # Echo command based on setting
255
- # Defaults to DEBUG_LOG
256
- # False to disable
257
- if isinstance(echo_cmd, bool):
258
- echo_cmd = DisplayMode.STDOUT if echo_cmd else DisplayMode.DEVNULL
259
-
260
- if echo_cmd is None:
261
- echo_cmd = DisplayMode.DEBUG_LOG
262
-
263
- if isinstance(echo_cmd, display.IODisplay):
264
- echo_display = echo_cmd
265
- else:
266
- echo_display = display.from_mode(echo_cmd)
267
-
268
- if isinstance(cmd, str):
269
- echo_display.printline("%s> %s", cwd or "", cmd)
270
- else:
271
- echo_display.printline("%s> %s", cwd or "", shlex.join(cmd))
272
-
273
- # Determine PIPE mode for stdout
274
- stdout_sub_mode = _determine_subprocess_output_mode(
275
- stdout, stdout_display, "stdout"
276
- )
277
-
278
- # Determine PIPE mode for stderr
279
- stderr_sub_mode = _determine_subprocess_output_mode(
280
- stderr, stderr_display, "stderr"
281
- )
282
-
283
- if stdout_display is None:
284
- stdout_display = (
285
- DisplayMode.STDOUT if stdout is None else DisplayMode.DEVNULL
286
- )
287
-
288
- if stderr_display is None:
289
- stderr_display = (
290
- DisplayMode.STDERR if stderr is None else DisplayMode.DEVNULL
291
- )
292
-
293
- # Run subprocess command
294
- with (
295
- subprocess.Popen(
296
- cmd,
297
- shell=shell,
298
- stdout=stdout_sub_mode,
299
- stderr=stderr_sub_mode,
300
- cwd=cwd,
301
- ) as popen,
302
- ThreadPool(2) as display_pool,
303
- ):
304
- # Print out and capture stdout
305
- stdout_result = display_pool.apply_async(
306
- _output_and_collect,
307
- args=[
308
- popen.stdout,
309
- (
310
- stdout_display
311
- if isinstance(stdout_display, display.IODisplay)
312
- else display.from_mode(stdout_display)
313
- ),
314
- stdout is not None and stdout != CaptureMode.DEVNULL,
315
- ],
316
- )
317
-
318
- # Print out and capture stderr
319
- stderr_result = display_pool.apply_async(
320
- _output_and_collect,
321
- args=[
322
- popen.stderr,
323
- (
324
- stderr_display
325
- if isinstance(stderr_display, display.IODisplay)
326
- else display.from_mode(stderr_display)
327
- ),
328
- stderr is not None and stderr != CaptureMode.DEVNULL,
329
- ],
330
- )
331
-
332
- # Wait on command completion
333
- returncode = popen.wait()
334
-
335
- # Build our result
336
- result = ShellResult(
337
- cmd=cmd,
338
- stdout=stdout_result.get(),
339
- stderr=stderr_result.get(),
340
- returncode=returncode,
341
- )
342
-
343
- # If check is True, raise exception if returncode is not 0
344
- if check:
345
- result.check_returncode()
346
-
347
- return result
296
+ with self.run_async(
297
+ cmd,
298
+ check=check,
299
+ stdout=stdout,
300
+ stderr=stderr,
301
+ stdout_display=stdout_display,
302
+ stderr_display=stderr_display,
303
+ cwd=cwd,
304
+ echo_cmd=echo_cmd,
305
+ **kwargs,
306
+ ) as running_cmd:
307
+ return running_cmd.result_bytes()
348
308
 
349
309
  def run_async(
350
310
  self,
@@ -414,16 +374,19 @@ class CommonDefaultBackend(ShellBackend):
414
374
  )
415
375
 
416
376
  return CommonShellAcyncResult(
417
- cmd,
418
- popen,
419
- (
377
+ cmd=cmd,
378
+ popen=popen,
379
+ stdout_capture=stdout,
380
+ stderr_capture=stderr,
381
+ stdout_display=(
420
382
  stdout_display
421
383
  if isinstance(stdout_display, display.IODisplay)
422
384
  else display.from_mode(stdout_display)
423
385
  ),
424
- (
386
+ stderr_display=(
425
387
  stderr_display
426
388
  if isinstance(stderr_display, display.IODisplay)
427
389
  else display.from_mode(stderr_display)
428
390
  ),
391
+ check=check,
429
392
  )
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import logging
4
2
 
5
3
  from shelltastic.backend.base import LocalShellBackend
@@ -1,19 +1,13 @@
1
- from __future__ import annotations
2
-
3
1
  import logging
4
2
  import shlex
5
- from typing import TYPE_CHECKING
3
+ from pathlib import Path
6
4
 
7
5
  from shelltastic.backend.base import RemoteShellBackend
8
6
  from shelltastic.backend.default.common import CommonDefaultBackend
9
7
  from shelltastic.enum import CaptureMode
10
8
  from shelltastic.exception import ShellException, SSHConnectionError
11
-
12
- if TYPE_CHECKING:
13
- from pathlib import Path
14
-
15
- from shelltastic.host import Host
16
- from shelltastic.result import ShellResult
9
+ from shelltastic.host import Host
10
+ from shelltastic.result import ShellResult
17
11
 
18
12
  LOGGER = logging.getLogger(__name__)
19
13
 
@@ -0,0 +1,101 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from io import RawIOBase
4
+
5
+ from shelltastic.exception import ShellException
6
+
7
+
8
+ @dataclass
9
+ class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
10
+ cmd: str | list[str]
11
+ returncode: int
12
+ stdout: SO
13
+ stderr: SE
14
+
15
+ def check_returncode(self):
16
+ """If returncode is not 0, raise ShellException"""
17
+ if self.returncode != 0:
18
+ raise ShellException(self.cmd, self.returncode, self.stdout, self.stderr)
19
+
20
+
21
+ class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
22
+ """
23
+ An asynchronsly running shell command.
24
+
25
+ cmd: String for the running shell command.
26
+
27
+ stdout: Bytes stream for stdout
28
+
29
+ stderr: Bytes stream for stderr
30
+ """
31
+
32
+ def __init__(self, cmd: str | list[str], stdout: SO, stderr: SE) -> None:
33
+ self.cmd = cmd
34
+ """The running command"""
35
+
36
+ self.stdout = stdout
37
+ """Byte based IO stream for stdout"""
38
+
39
+ self.stderr = stderr
40
+ """Byte based IO stream for stderr"""
41
+
42
+ def __enter__(self):
43
+ return self
44
+
45
+ def __exit__(self, exc_type, exc, tb):
46
+ self.close()
47
+
48
+ def close(self) -> None:
49
+ """Close the running command."""
50
+ return
51
+
52
+ def terminate(self) -> None:
53
+ """Terminate the running command"""
54
+ raise NotImplementedError()
55
+
56
+ @abstractmethod
57
+ def result(self) -> ShellResult[SO, SE]:
58
+ """
59
+ Blocking
60
+
61
+ Wait for the command to finish and return a ShellResult with the outcome.
62
+ """
63
+ raise NotImplementedError()
64
+
65
+ def wait(self) -> None:
66
+ """
67
+ Blocking
68
+
69
+ Wait for the command to finish.
70
+ """
71
+ self.result()
72
+
73
+ def returncode(self) -> int:
74
+ """
75
+ Blocking
76
+
77
+ Get the returncode of the command.
78
+ """
79
+ return self.result().returncode
80
+
81
+ def check_returncode(self):
82
+ """
83
+ Blocking
84
+
85
+ Raise a ShellException if the returncode is not zero.
86
+ """
87
+ self.result().check_returncode()
88
+
89
+ def result_bytes(self) -> ShellResult[bytes | None, bytes | None]:
90
+ result = self.result()
91
+ stdout = result.stdout
92
+ stdout_bytes = stdout.readall() if stdout is not None else None
93
+ stderr = result.stderr
94
+ stderr_bytes = stderr.readall() if stderr is not None else None
95
+
96
+ return ShellResult(
97
+ cmd=result.cmd,
98
+ returncode=result.returncode,
99
+ stdout=stdout_bytes,
100
+ stderr=stderr_bytes,
101
+ )
@@ -1,47 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from dataclasses import dataclass
3
- from io import RawIOBase
4
-
5
- from shelltastic.exception import ShellException
6
-
7
-
8
- @dataclass
9
- class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
10
- cmd: str | list[str]
11
- returncode: int
12
- stdout: SO
13
- stderr: SE
14
-
15
- def check_returncode(self):
16
- """If returncode is not 0, raise ShellException"""
17
- if self.returncode != 0:
18
- raise ShellException(self.cmd, self.returncode, self.stdout, self.stderr)
19
-
20
-
21
- class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
22
- def __init__(self, cmd: str | list[str], stdout: SO, stderr: SE) -> None:
23
- self.cmd = cmd
24
- self.stdout = stdout
25
- self.stderr = stderr
26
-
27
- def __enter__(self):
28
- return self
29
-
30
- def __exit__(self, exc_type, exc, tb):
31
- self.close()
32
-
33
- def close(self) -> None:
34
- return
35
-
36
- @abstractmethod
37
- def result(self) -> ShellResult[SO, SE]:
38
- raise NotImplementedError()
39
-
40
- def wait(self) -> None:
41
- self.result()
42
-
43
- def returncode(self) -> int:
44
- return self.result().returncode
45
-
46
- def check_returncode(self):
47
- self.result().check_returncode()
File without changes
File without changes