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.
- {shelltastic-0.8.1 → shelltastic-0.9.0}/PKG-INFO +1 -1
- shelltastic-0.9.0/pyproject.toml +59 -0
- shelltastic-0.8.1/pyproject.toml → shelltastic-0.9.0/pyproject.toml.orig +16 -12
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/default/common.py +69 -52
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/exception.py +4 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/result.py +21 -6
- {shelltastic-0.8.1 → shelltastic-0.9.0}/LICENSE +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/README.md +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/__init__.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/__init__.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/base.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/default/__init__.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/default/local.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/backend/default/remote.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/display.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/enum.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/__init__.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/common.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/git.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/scp.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/frontend/shell.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/host.py +0 -0
- {shelltastic-0.8.1 → shelltastic-0.9.0}/src/shelltastic/io.py +0 -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.
|
|
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
|
-
[
|
|
48
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
+
self._stdout = PipeReader(stdout_rcv)
|
|
180
178
|
else:
|
|
181
179
|
stdout_snd = None
|
|
182
|
-
|
|
180
|
+
self._stdout = None
|
|
183
181
|
|
|
184
182
|
if stdout_snd or stdout_display:
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
+
self._stderr = PipeReader(stderr_rcv)
|
|
203
199
|
else:
|
|
204
200
|
stderr_snd = None
|
|
205
|
-
|
|
201
|
+
self._stderr = None
|
|
206
202
|
|
|
207
203
|
if stderr_snd or stderr_display:
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
self.
|
|
222
|
+
@property
|
|
223
|
+
def stdout(self) -> RawIOBase | None:
|
|
224
|
+
if self._closed:
|
|
225
|
+
raise ShellClosedException(f"{self} is closed")
|
|
233
226
|
|
|
234
|
-
|
|
235
|
-
self._popen.__exit__(exc_type, exc, tb)
|
|
227
|
+
return self._stdout
|
|
236
228
|
|
|
237
|
-
|
|
238
|
-
|
|
229
|
+
@property
|
|
230
|
+
def stderr(self) -> RawIOBase | None:
|
|
231
|
+
if self._closed:
|
|
232
|
+
raise ShellClosedException(f"{self} is closed")
|
|
239
233
|
|
|
240
|
-
|
|
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
|
|
247
|
-
|
|
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.
|
|
281
|
+
self._stdout_async_result.join()
|
|
265
282
|
|
|
266
283
|
if self._stderr_async_result:
|
|
267
|
-
self._stderr_async_result.
|
|
284
|
+
self._stderr_async_result.join()
|
|
268
285
|
|
|
269
286
|
# Check if set
|
|
270
287
|
if self._check:
|
|
@@ -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(
|
|
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 =
|
|
26
|
+
stdout = (
|
|
27
|
+
self.stdout.readall() if isinstance(self.stdout, RawIOBase) else self.stdout
|
|
28
|
+
)
|
|
25
29
|
|
|
26
|
-
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]
|
|
48
|
+
def __init__(self, cmd: str | list[str]) -> None:
|
|
43
49
|
self.cmd = cmd
|
|
44
50
|
"""The running command"""
|
|
45
51
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|