shelltastic 0.8.1__tar.gz → 0.9.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.8.1 → shelltastic-0.9.0}/PKG-INFO +1 -1
  2. shelltastic-0.9.0/pyproject.toml +59 -0
  3. shelltastic-0.8.1/pyproject.toml → shelltastic-0.9.0/pyproject.toml.orig +16 -12
  4. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/default/common.py +69 -52
  5. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/exception.py +4 -0
  6. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/result.py +21 -6
  7. {shelltastic-0.8.1 → shelltastic-0.9.0}/LICENSE +0 -0
  8. {shelltastic-0.8.1 → shelltastic-0.9.0}/README.md +0 -0
  9. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/__init__.py +0 -0
  10. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/__init__.py +0 -0
  11. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/base.py +0 -0
  12. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/default/__init__.py +0 -0
  13. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/default/local.py +0 -0
  14. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/default/remote.py +0 -0
  15. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/display.py +0 -0
  16. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/enum.py +0 -0
  17. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/__init__.py +0 -0
  18. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/common.py +0 -0
  19. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/git.py +0 -0
  20. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/scp.py +0 -0
  21. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/shell.py +0 -0
  22. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/host.py +0 -0
  23. {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: A fantastic shell command runner for python
5
5
  Author: Bearmine
6
6
  License-Expression: MPL-2.0
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "shelltastic"
3
+ version = "0.9.0"
4
+ description = "A fantastic shell command runner for python"
5
+ readme = "README.md"
6
+ license = "MPL-2.0"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ dependencies = []
10
+
11
+ [[project.authors]]
12
+ name = "Bearmine"
13
+
14
+ [project.urls]
15
+ Repository = "https://forge.bearmine.com/bearmine/shelltastic"
16
+
17
+ [dependency-groups]
18
+ lint = ["ruff"]
19
+ test = [
20
+ "pytest",
21
+ "pytest-cov>=7.1.0",
22
+ ]
23
+
24
+ [[dependency-groups.dev]]
25
+ include-group = "lint"
26
+
27
+ [[dependency-groups.dev]]
28
+ include-group = "test"
29
+
30
+ [build-system]
31
+ requires = ["uv_build>=0.11.18,<0.12.0"]
32
+ build-backend = "uv_build"
33
+
34
+ [tool.ruff.lint]
35
+ extend-select = [
36
+ "F",
37
+ "W",
38
+ "E",
39
+ "I",
40
+ "UP",
41
+ "C4",
42
+ "FA",
43
+ "ISC",
44
+ "ICN",
45
+ "RET",
46
+ "SIM",
47
+ "TID",
48
+ "TC",
49
+ "PTH",
50
+ "TD",
51
+ "NPY",
52
+ "FURB",
53
+ ]
54
+
55
+ [tool.ruff.lint.pycodestyle]
56
+ max-line-length = 100
57
+
58
+ [tool.pytest.ini_options]
59
+ addopts = "--cov=shelltastic --cov-report html"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shelltastic"
3
- version = "0.8.1"
3
+ version = "0.9.0"
4
4
  description = "A fantastic shell command runner for python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -13,6 +13,19 @@ license-files = [
13
13
  requires-python = ">=3.12"
14
14
  dependencies = []
15
15
 
16
+ [dependency-groups]
17
+ dev = [
18
+ {include-group = "lint"},
19
+ {include-group = "test"},
20
+ ]
21
+ lint = [
22
+ "ruff"
23
+ ]
24
+ test = [
25
+ "pytest",
26
+ "pytest-cov>=7.1.0"
27
+ ]
28
+
16
29
  [project.urls]
17
30
  Repository = "https://forge.bearmine.com/bearmine/shelltastic"
18
31
 
@@ -44,14 +57,5 @@ extend-select = [
44
57
  [tool.ruff.lint.pycodestyle]
45
58
  max-line-length = 100
46
59
 
47
- [dependency-groups]
48
- dev = [
49
- {include-group = "lint"},
50
- {include-group = "test"}
51
- ]
52
- lint = [
53
- "ruff"
54
- ]
55
- test = [
56
- "pytest"
57
- ]
60
+ [tool.pytest.ini_options]
61
+ addopts = "--cov=shelltastic --cov-report html"
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import logging
2
3
  import os
3
4
  import shlex
@@ -6,13 +7,14 @@ import time
6
7
  from io import RawIOBase
7
8
  from multiprocessing import Pipe
8
9
  from multiprocessing.connection import Connection
9
- from multiprocessing.pool import ThreadPool
10
10
  from pathlib import Path
11
+ from threading import Thread
11
12
  from typing import IO, Literal
12
13
 
13
14
  from shelltastic import display
14
15
  from shelltastic.backend.base import ShellBackend
15
16
  from shelltastic.enum import CaptureMode, DisplayMode
17
+ from shelltastic.exception import ShellClosedException
16
18
  from shelltastic.io import PipeReader
17
19
  from shelltastic.result import ShellResult, ShellResultAsync
18
20
 
@@ -90,24 +92,20 @@ class IOTimeoutStream:
90
92
  def _output_and_pipe(
91
93
  io: IO[bytes] | None, display: display.IODisplay | None, pipe: Connection | None
92
94
  ) -> 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)
95
+ if io is None:
96
+ return
104
97
 
98
+ if pipe:
105
99
  if display:
106
100
  for line in IOTimeoutStream(io):
101
+ pipe.send_bytes(line)
107
102
  display.printbytes(line)
108
- finally:
109
- if pipe:
110
- pipe.close()
103
+ for line in IOTimeoutStream(io):
104
+ pipe.send_bytes(line)
105
+
106
+ if display:
107
+ for line in IOTimeoutStream(io):
108
+ display.printbytes(line)
111
109
 
112
110
 
113
111
  def _determine_subprocess_output_mode(
@@ -168,7 +166,7 @@ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
168
166
  stderr_display: display.IODisplay | None,
169
167
  check: bool,
170
168
  ) -> None:
171
- _display_pool = None
169
+ super().__init__(cmd)
172
170
  _stdout_async_result = None
173
171
  _stderr_async_result = None
174
172
 
@@ -176,81 +174,100 @@ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
176
174
  # If stdout is PIPE, then setup reader
177
175
  if stdout_capture == CaptureMode.PIPE:
178
176
  stdout_rcv, stdout_snd = Pipe(False)
179
- stdout = PipeReader(stdout_rcv)
177
+ self._stdout = PipeReader(stdout_rcv)
180
178
  else:
181
179
  stdout_snd = None
182
- stdout = None
180
+ self._stdout = None
183
181
 
184
182
  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,
183
+ _stdout_async_result = Thread(
184
+ target=_output_and_pipe,
190
185
  args=[
191
186
  popen.stdout,
192
187
  stdout_display,
193
188
  stdout_snd,
194
189
  ],
195
190
  )
191
+ _stdout_async_result.start()
196
192
  else:
197
- stdout = None
193
+ self._stdout = None
198
194
 
199
195
  if popen.stderr:
200
196
  if stderr_capture == CaptureMode.PIPE:
201
197
  stderr_rcv, stderr_snd = Pipe(False)
202
- stderr = PipeReader(stderr_rcv)
198
+ self._stderr = PipeReader(stderr_rcv)
203
199
  else:
204
200
  stderr_snd = None
205
- stderr = None
201
+ self._stderr = None
206
202
 
207
203
  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,
204
+ _stderr_async_result = Thread(
205
+ target=_output_and_pipe,
213
206
  args=[
214
207
  popen.stderr,
215
208
  stderr_display,
216
209
  stderr_snd,
217
210
  ],
218
211
  )
212
+ _stderr_async_result.start()
219
213
  else:
220
- stderr = None
221
-
222
- super().__init__(cmd, stdout, stderr)
214
+ self._stderr = None
223
215
 
224
- self._display_pool = _display_pool
225
216
  self._stdout_async_result = _stdout_async_result
226
217
  self._stderr_async_result = _stderr_async_result
227
218
  self._popen = popen
228
219
  self._check = check
220
+ self._closed = False
229
221
 
230
- def __exit__(self, exc_type, exc, tb):
231
- # Wait for command to finish before closing everything
232
- self.wait()
222
+ @property
223
+ def stdout(self) -> RawIOBase | None:
224
+ if self._closed:
225
+ raise ShellClosedException(f"{self} is closed")
233
226
 
234
- # Close popen
235
- self._popen.__exit__(exc_type, exc, tb)
227
+ return self._stdout
236
228
 
237
- if self._display_pool:
238
- self._display_pool.__exit__(exc_type, exc, tb)
229
+ @property
230
+ def stderr(self) -> RawIOBase | None:
231
+ if self._closed:
232
+ raise ShellClosedException(f"{self} is closed")
239
233
 
240
- # Close stdout and stderr
241
- if self.stdout is not None:
242
- self.stdout.close()
243
- if self.stderr is not None:
244
- self.stderr.close()
234
+ return self._stderr
245
235
 
246
- def close(self) -> None:
247
- self.__exit__(None, None, None)
236
+ def __exit__(self, exc_type, exc, tb):
237
+ with contextlib.suppress(KeyboardInterrupt, subprocess.TimeoutExpired):
238
+ self.close()
239
+
240
+ def close(self, timeout: float | None = None) -> None:
241
+ if self._closed:
242
+ return
243
+
244
+ # Wait for command to finish before closing everything
245
+ try:
246
+ self._popen.wait(timeout)
247
+ except (KeyboardInterrupt, subprocess.TimeoutExpired):
248
+ # Kill the process and then reraise
249
+ self._popen.kill()
250
+ LOGGER.error("pid %s killed", self._popen.pid)
251
+ raise
252
+ finally:
253
+ # Close stdout and stderr
254
+ if self.stdout is not None:
255
+ self.stdout.close()
256
+ if self.stderr is not None:
257
+ self.stderr.close()
258
+
259
+ self._closed = True
248
260
 
249
261
  def terminate(self) -> None:
262
+ if self._closed:
263
+ raise ShellClosedException(f"{self} is closed")
264
+
250
265
  self._popen.terminate()
251
- self.close()
252
266
 
253
267
  def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
268
+ if self._closed:
269
+ raise ShellClosedException(f"{self} is closed")
270
+
254
271
  # Get result
255
272
  result = ShellResult(
256
273
  cmd=self.cmd,
@@ -261,10 +278,10 @@ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
261
278
 
262
279
  # Wait for our async readers to be complete
263
280
  if self._stdout_async_result:
264
- self._stdout_async_result.wait()
281
+ self._stdout_async_result.join()
265
282
 
266
283
  if self._stderr_async_result:
267
- self._stderr_async_result.wait()
284
+ self._stderr_async_result.join()
268
285
 
269
286
  # Check if set
270
287
  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