shelltastic 0.8.0__tar.gz → 0.8.2__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 (22) hide show
  1. {shelltastic-0.8.0 → shelltastic-0.8.2}/PKG-INFO +1 -1
  2. {shelltastic-0.8.0 → shelltastic-0.8.2}/pyproject.toml +1 -1
  3. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/backend/__init__.py +2 -0
  4. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/backend/base.py +18 -2
  5. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/backend/default/common.py +58 -45
  6. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/exception.py +4 -0
  7. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/result.py +21 -6
  8. {shelltastic-0.8.0 → shelltastic-0.8.2}/LICENSE +0 -0
  9. {shelltastic-0.8.0 → shelltastic-0.8.2}/README.md +0 -0
  10. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/__init__.py +0 -0
  11. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/backend/default/__init__.py +0 -0
  12. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/backend/default/local.py +0 -0
  13. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/backend/default/remote.py +0 -0
  14. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/display.py +0 -0
  15. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/enum.py +0 -0
  16. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/frontend/__init__.py +0 -0
  17. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/frontend/common.py +0 -0
  18. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/frontend/git.py +0 -0
  19. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/frontend/scp.py +0 -0
  20. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/frontend/shell.py +0 -0
  21. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/host.py +0 -0
  22. {shelltastic-0.8.0 → shelltastic-0.8.2}/src/shelltastic/io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.8.0
3
+ Version: 0.8.2
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.8.0"
3
+ version = "0.8.2"
4
4
  description = "A fantastic shell command runner for python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -4,6 +4,7 @@ from shelltastic.backend.base import (
4
4
  RemoteShellBackend,
5
5
  ShellBackend,
6
6
  add_shim,
7
+ clear_shims,
7
8
  )
8
9
  from shelltastic.backend.default.local import DefaultLocalBackend
9
10
  from shelltastic.backend.default.remote import DefaultRemoteBackend
@@ -17,6 +18,7 @@ __all__ = [
17
18
  "register_default_local_backend",
18
19
  "register_default_remote_backend",
19
20
  "add_shim",
21
+ "clear_shims",
20
22
  "get_local_backend",
21
23
  "get_remote_backend",
22
24
  ]
@@ -90,6 +90,10 @@ class BackendShim(ABC):
90
90
 
91
91
  _backend_shims: list[BackendShim] = []
92
92
 
93
+ def clear_shims():
94
+ """Clear all shims"""
95
+ global _backend_shims
96
+ _backend_shims.clear()
93
97
 
94
98
  def add_shim(shim: BackendShim):
95
99
  """Add a shim to manipulate `ShellBackend` params before any `ShellBackend` runs"""
@@ -103,7 +107,19 @@ def _shim_run(f, backend_type: type):
103
107
  result: BackendShimResult = BackendShimResult(*args, **kwargs)
104
108
  for shim in _backend_shims:
105
109
  result = shim.run_shim(
106
- backend_type, f.__name__, *result.args, **result.kwargs
110
+ backend_type, *result.args, **result.kwargs
111
+ )
112
+ return f(*result.args, **result.kwargs)
113
+
114
+ return wrapper
115
+
116
+ def _shim_run_async(f, backend_type: type):
117
+ @wraps(f)
118
+ def wrapper(*args, **kwargs):
119
+ result: BackendShimResult = BackendShimResult(*args, **kwargs)
120
+ for shim in _backend_shims:
121
+ result = shim.run_async_shim(
122
+ backend_type, *result.args, **result.kwargs
107
123
  )
108
124
  return f(*result.args, **result.kwargs)
109
125
 
@@ -114,7 +130,7 @@ class _InstallShims(ABCMeta):
114
130
  def __call__(self, *args: Any, **kwds: Any) -> Any:
115
131
  instance = super().__call__(*args, **kwds)
116
132
  instance.run = _shim_run(instance.run, instance.__class__)
117
- instance.run_async = _shim_run(instance.run_async, instance.__class__)
133
+ instance.run_async = _shim_run_async(instance.run_async, instance.__class__)
118
134
  return instance
119
135
 
120
136
 
@@ -6,13 +6,14 @@ import time
6
6
  from io import RawIOBase
7
7
  from multiprocessing import Pipe
8
8
  from multiprocessing.connection import Connection
9
- from multiprocessing.pool import ThreadPool
10
9
  from pathlib import Path
10
+ from threading import Thread
11
11
  from typing import IO, Literal
12
12
 
13
13
  from shelltastic import display
14
14
  from shelltastic.backend.base import ShellBackend
15
15
  from shelltastic.enum import CaptureMode, DisplayMode
16
+ from shelltastic.exception import ShellClosedException
16
17
  from shelltastic.io import PipeReader
17
18
  from shelltastic.result import ShellResult, ShellResultAsync
18
19
 
@@ -90,24 +91,20 @@ class IOTimeoutStream:
90
91
  def _output_and_pipe(
91
92
  io: IO[bytes] | None, display: display.IODisplay | None, pipe: Connection | None
92
93
  ) -> None:
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)
94
+ if io is None:
95
+ return
104
96
 
97
+ if pipe:
105
98
  if display:
106
99
  for line in IOTimeoutStream(io):
100
+ pipe.send_bytes(line)
107
101
  display.printbytes(line)
108
- finally:
109
- if pipe:
110
- pipe.close()
102
+ for line in IOTimeoutStream(io):
103
+ pipe.send_bytes(line)
104
+
105
+ if display:
106
+ for line in IOTimeoutStream(io):
107
+ display.printbytes(line)
111
108
 
112
109
 
113
110
  def _determine_subprocess_output_mode(
@@ -168,7 +165,7 @@ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
168
165
  stderr_display: display.IODisplay | None,
169
166
  check: bool,
170
167
  ) -> None:
171
- _display_pool = None
168
+ super().__init__(cmd)
172
169
  _stdout_async_result = None
173
170
  _stderr_async_result = None
174
171
 
@@ -176,66 +173,77 @@ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
176
173
  # If stdout is PIPE, then setup reader
177
174
  if stdout_capture == CaptureMode.PIPE:
178
175
  stdout_rcv, stdout_snd = Pipe(False)
179
- stdout = PipeReader(stdout_rcv)
176
+ self._stdout = PipeReader(stdout_rcv)
180
177
  else:
181
178
  stdout_snd = None
182
- stdout = None
179
+ self._stdout = None
183
180
 
184
181
  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,
182
+ _stdout_async_result = Thread(
183
+ target=_output_and_pipe,
190
184
  args=[
191
185
  popen.stdout,
192
186
  stdout_display,
193
187
  stdout_snd,
194
188
  ],
195
189
  )
190
+ _stdout_async_result.start()
196
191
  else:
197
- stdout = None
192
+ self._stdout = None
198
193
 
199
194
  if popen.stderr:
200
195
  if stderr_capture == CaptureMode.PIPE:
201
196
  stderr_rcv, stderr_snd = Pipe(False)
202
- stderr = PipeReader(stderr_rcv)
197
+ self._stderr = PipeReader(stderr_rcv)
203
198
  else:
204
199
  stderr_snd = None
205
- stderr = None
200
+ self._stderr = None
206
201
 
207
202
  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,
203
+ _stderr_async_result = Thread(
204
+ target=_output_and_pipe,
213
205
  args=[
214
206
  popen.stderr,
215
207
  stderr_display,
216
208
  stderr_snd,
217
209
  ],
218
210
  )
211
+ _stderr_async_result.start()
219
212
  else:
220
- stderr = None
221
-
222
- super().__init__(cmd, stdout, stderr)
213
+ self._stderr = None
223
214
 
224
- self._display_pool = _display_pool
225
215
  self._stdout_async_result = _stdout_async_result
226
216
  self._stderr_async_result = _stderr_async_result
227
217
  self._popen = popen
228
218
  self._check = check
219
+ self._closed = False
220
+
221
+ @property
222
+ def stdout(self) -> RawIOBase | None:
223
+ if self._closed:
224
+ raise ShellClosedException(f"{self} is closed")
225
+
226
+ return self._stdout
227
+
228
+ @property
229
+ def stderr(self) -> RawIOBase | None:
230
+ if self._closed:
231
+ raise ShellClosedException(f"{self} is closed")
232
+
233
+ return self._stderr
229
234
 
230
235
  def __exit__(self, exc_type, exc, tb):
231
- # Wait for command to finish before closing everything
232
- self.wait()
236
+ self.close()
237
+
238
+ def close(self) -> None:
239
+ if self._closed:
240
+ return
233
241
 
234
- # Close popen
235
- self._popen.__exit__(exc_type, exc, tb)
242
+ # Terminate popen
243
+ self._popen.terminate()
236
244
 
237
- if self._display_pool:
238
- self._display_pool.__exit__(exc_type, exc, tb)
245
+ # Wait for command to finish before closing everything
246
+ self.wait()
239
247
 
240
248
  # Close stdout and stderr
241
249
  if self.stdout is not None:
@@ -243,14 +251,19 @@ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
243
251
  if self.stderr is not None:
244
252
  self.stderr.close()
245
253
 
246
- def close(self) -> None:
247
- self.__exit__(None, None, None)
254
+ self._closed = True
248
255
 
249
256
  def terminate(self) -> None:
257
+ if self._closed:
258
+ raise ShellClosedException(f"{self} is closed")
259
+
250
260
  self._popen.terminate()
251
261
  self.close()
252
262
 
253
263
  def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
264
+ if self._closed:
265
+ raise ShellClosedException(f"{self} is closed")
266
+
254
267
  # Get result
255
268
  result = ShellResult(
256
269
  cmd=self.cmd,
@@ -261,10 +274,10 @@ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
261
274
 
262
275
  # Wait for our async readers to be complete
263
276
  if self._stdout_async_result:
264
- self._stdout_async_result.wait()
277
+ self._stdout_async_result.join()
265
278
 
266
279
  if self._stderr_async_result:
267
- self._stderr_async_result.wait()
280
+ self._stderr_async_result.join()
268
281
 
269
282
  # Check if set
270
283
  if self._check:
@@ -25,3 +25,7 @@ class ShellException(Exception):
25
25
  self.returncode = returncode
26
26
  self.stdout = stdout
27
27
  self.stderr = stderr
28
+
29
+
30
+ class ShellClosedException(Exception):
31
+ pass
@@ -18,12 +18,18 @@ class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
18
18
  """If returncode is not 0, raise ShellException"""
19
19
  if self.returncode != 0:
20
20
  result = self.to_result_bytes()
21
- raise ShellException(self.cmd, self.returncode, result.stdout, result.stderr)
21
+ raise ShellException(
22
+ self.cmd, self.returncode, result.stdout, result.stderr
23
+ )
22
24
 
23
25
  def to_result_bytes(self) -> ShellResult[bytes | None, bytes | None]:
24
- stdout = self.stdout.readall() if isinstance(self.stdout, RawIOBase) else self.stdout
26
+ stdout = (
27
+ self.stdout.readall() if isinstance(self.stdout, RawIOBase) else self.stdout
28
+ )
25
29
 
26
- stderr = self.stderr.readall() if isinstance(self.stderr, RawIOBase) else self.stderr
30
+ stderr = (
31
+ self.stderr.readall() if isinstance(self.stderr, RawIOBase) else self.stderr
32
+ )
27
33
 
28
34
  return ShellResult(self.cmd, self.returncode, stdout, stderr)
29
35
 
@@ -39,15 +45,24 @@ class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
39
45
  stderr: Bytes stream for stderr
40
46
  """
41
47
 
42
- def __init__(self, cmd: str | list[str], stdout: SO, stderr: SE) -> None:
48
+ def __init__(self, cmd: str | list[str]) -> None:
43
49
  self.cmd = cmd
44
50
  """The running command"""
45
51
 
46
- self.stdout = stdout
52
+ def __str__(self) -> str:
53
+ return f"{self.__class__.__name__}({self.cmd})"
54
+
55
+ @property
56
+ @abstractmethod
57
+ def stdout(self) -> SO:
47
58
  """Byte based IO stream for stdout"""
59
+ raise NotImplementedError()
48
60
 
49
- self.stderr = stderr
61
+ @property
62
+ @abstractmethod
63
+ def stderr(self) -> SE:
50
64
  """Byte based IO stream for stderr"""
65
+ raise NotImplementedError()
51
66
 
52
67
  def __enter__(self):
53
68
  return self
File without changes
File without changes