shelltastic 0.7.0__tar.gz → 0.8.1__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.7.0 → shelltastic-0.8.1}/PKG-INFO +1 -1
  2. {shelltastic-0.7.0 → shelltastic-0.8.1}/pyproject.toml +1 -1
  3. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/__init__.py +2 -0
  4. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/base.py +18 -2
  5. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/default/common.py +6 -10
  6. shelltastic-0.8.1/src/shelltastic/backend/default/remote.py +147 -0
  7. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/exception.py +3 -6
  8. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/result.py +11 -1
  9. shelltastic-0.7.0/src/shelltastic/backend/default/remote.py +0 -69
  10. {shelltastic-0.7.0 → shelltastic-0.8.1}/LICENSE +0 -0
  11. {shelltastic-0.7.0 → shelltastic-0.8.1}/README.md +0 -0
  12. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/__init__.py +0 -0
  13. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/default/__init__.py +0 -0
  14. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/default/local.py +0 -0
  15. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/display.py +0 -0
  16. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/enum.py +0 -0
  17. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/__init__.py +0 -0
  18. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/common.py +0 -0
  19. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/git.py +0 -0
  20. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/scp.py +0 -0
  21. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/shell.py +0 -0
  22. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/host.py +0 -0
  23. {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.7.0
3
+ Version: 0.8.1
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.7.0"
3
+ version = "0.8.1"
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
 
@@ -157,7 +157,7 @@ def _determine_subprocess_output_mode(
157
157
  return subprocess.PIPE
158
158
 
159
159
 
160
- class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None]):
160
+ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None]):
161
161
  def __init__(
162
162
  self,
163
163
  cmd: str | list[str],
@@ -185,7 +185,7 @@ class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
185
185
  if _display_pool is None:
186
186
  _display_pool = ThreadPool(2)
187
187
 
188
- _stdout_async_result=_display_pool.apply_async(
188
+ _stdout_async_result = _display_pool.apply_async(
189
189
  _output_and_pipe,
190
190
  args=[
191
191
  popen.stdout,
@@ -231,20 +231,16 @@ class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
231
231
  # Wait for command to finish before closing everything
232
232
  self.wait()
233
233
 
234
- print("closing popen")
235
234
  # Close popen
236
235
  self._popen.__exit__(exc_type, exc, tb)
237
236
 
238
237
  if self._display_pool:
239
- print("closing _display_pool")
240
238
  self._display_pool.__exit__(exc_type, exc, tb)
241
239
 
242
240
  # Close stdout and stderr
243
- if self.stdout:
244
- print("closing stdout")
241
+ if self.stdout is not None:
245
242
  self.stdout.close()
246
- if self.stderr:
247
- print("closing stderr")
243
+ if self.stderr is not None:
248
244
  self.stderr.close()
249
245
 
250
246
  def close(self) -> None:
@@ -263,7 +259,7 @@ class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
263
259
  stderr=self.stderr,
264
260
  )
265
261
 
266
- # Wait for our async reader to be complete
262
+ # Wait for our async readers to be complete
267
263
  if self._stdout_async_result:
268
264
  self._stdout_async_result.wait()
269
265
 
@@ -373,7 +369,7 @@ class CommonDefaultBackend(ShellBackend):
373
369
  cwd=cwd,
374
370
  )
375
371
 
376
- return CommonShellAcyncResult(
372
+ return CommonShellAsyncResult(
377
373
  cmd=cmd,
378
374
  popen=popen,
379
375
  stdout_capture=stdout,
@@ -0,0 +1,147 @@
1
+ import logging
2
+ import shlex
3
+ from io import RawIOBase
4
+ from pathlib import Path
5
+ from subprocess import Popen
6
+
7
+ from shelltastic.backend.base import RemoteShellBackend
8
+ from shelltastic.backend.default.common import (
9
+ CommonDefaultBackend,
10
+ CommonShellAsyncResult,
11
+ )
12
+ from shelltastic.display import IODisplay
13
+ from shelltastic.enum import CaptureMode
14
+ from shelltastic.exception import ShellException, SSHConnectionError
15
+ from shelltastic.host import Host
16
+ from shelltastic.result import ShellResult, ShellResultAsync
17
+
18
+ LOGGER = logging.getLogger(__name__)
19
+
20
+
21
+ def _monkey_patch_async_result(
22
+ async_result: ShellResultAsync[RawIOBase | None, RawIOBase | None], host: Host
23
+ ):
24
+ result_fn = async_result.result
25
+
26
+ def patched_result_fn():
27
+ # Extra check for SSH Errors
28
+ try:
29
+ result = result_fn()
30
+ if (
31
+ result.returncode == 255
32
+ ): # 255 is what ssh returns if it has an error with ssh itself
33
+ result = result.to_result_bytes()
34
+ raise SSHConnectionError(
35
+ host.hostname, result.returncode, result.stdout, result.stderr
36
+ )
37
+ return result
38
+ except ShellException as ex:
39
+ if ex.returncode == 255:
40
+ raise SSHConnectionError(
41
+ host.hostname, ex.returncode, ex.stdout, ex.stderr
42
+ ) from ex
43
+ raise
44
+
45
+ async_result.result = patched_result_fn
46
+
47
+ return async_result
48
+
49
+
50
+ class RemoteShellAcyncResult(CommonShellAsyncResult):
51
+ def __init__(
52
+ self,
53
+ cmd: str | list[str],
54
+ popen: Popen,
55
+ stdout_capture: CaptureMode | None,
56
+ stderr_capture: CaptureMode | None,
57
+ stdout_display: IODisplay | None,
58
+ stderr_display: IODisplay | None,
59
+ check: bool,
60
+ host: Host,
61
+ ) -> None:
62
+ super().__init__(
63
+ cmd,
64
+ popen,
65
+ stdout_capture,
66
+ stderr_capture,
67
+ stdout_display,
68
+ stderr_display,
69
+ check,
70
+ )
71
+ self.host = host
72
+
73
+ def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
74
+ # Extra check for SSH Errors
75
+ try:
76
+ result = super().result()
77
+ if (
78
+ result.returncode == 255
79
+ ): # 255 is what ssh returns if it has an error with ssh itself
80
+ result = result.to_result_bytes()
81
+ raise SSHConnectionError(
82
+ self.host.hostname, result.returncode, result.stdout, result.stderr
83
+ )
84
+ return result
85
+ except ShellException as ex:
86
+ if ex.returncode == 255:
87
+ raise SSHConnectionError(
88
+ self.host.hostname, ex.returncode, ex.stdout, ex.stderr
89
+ ) from ex
90
+ raise
91
+
92
+
93
+ class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
94
+ def __init__(self, host: Host) -> None:
95
+ super().__init__()
96
+ self.host: Host = host
97
+
98
+ @staticmethod
99
+ def for_host(host: Host) -> RemoteShellBackend:
100
+ return DefaultRemoteBackend(host)
101
+
102
+ def run(
103
+ self,
104
+ cmd: str | list[str],
105
+ *,
106
+ subshell: bool = True,
107
+ cwd: str | Path | None = None,
108
+ **kwargs,
109
+ ) -> ShellResult:
110
+ """
111
+ Execute a remote command using SSH.
112
+ """
113
+ with self.run_async(cmd, subshell=subshell, cwd=cwd, **kwargs) as running_cmd:
114
+ return running_cmd.result_bytes()
115
+
116
+ def run_async(
117
+ self,
118
+ cmd: str | list[str],
119
+ *,
120
+ subshell: bool = True,
121
+ cwd: str | Path | None = None,
122
+ **kwargs,
123
+ ) -> ShellResultAsync[RawIOBase | None, RawIOBase | None]:
124
+ if isinstance(cmd, list):
125
+ cmd = shlex.join(cmd)
126
+
127
+ # Wrap user command in sh -c call
128
+ if subshell:
129
+ cmd = shlex.join(["bash", "-c", cmd])
130
+
131
+ # If we're redirecting stderr to stdout,
132
+ # add the same redirect inside the ssh command.
133
+ if kwargs.get("stderr") == CaptureMode.STDOUT:
134
+ cmd = cmd + " 2>&1"
135
+
136
+ # cd into cwd first if cwd is set
137
+ if cwd:
138
+ cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
139
+
140
+ port_flag = ["-p", str(self.host.port)] if self.host.port else []
141
+
142
+ return _monkey_patch_async_result(
143
+ super().run_async(
144
+ ["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
145
+ ),
146
+ self.host,
147
+ )
@@ -1,6 +1,3 @@
1
- from io import RawIOBase
2
-
3
-
4
1
  class SSHConnectionError(Exception):
5
2
  def __init__(
6
3
  self, hostname: str, returncode: int, stdout: bytes | None, stderr: bytes | None
@@ -15,13 +12,13 @@ class SSHConnectionError(Exception):
15
12
  self.stderr = stderr
16
13
 
17
14
 
18
- class ShellException[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None](Exception):
15
+ class ShellException(Exception):
19
16
  def __init__(
20
17
  self,
21
18
  cmd: str | list[str],
22
19
  returncode: int,
23
- stdout: SO,
24
- stderr: SE,
20
+ stdout: bytes | None,
21
+ stderr: bytes | None,
25
22
  ):
26
23
  super().__init__(f"Shell execution failed with returncode {returncode}")
27
24
  self.cmd = cmd
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABC, abstractmethod
2
4
  from dataclasses import dataclass
3
5
  from io import RawIOBase
@@ -15,7 +17,15 @@ class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
15
17
  def check_returncode(self):
16
18
  """If returncode is not 0, raise ShellException"""
17
19
  if self.returncode != 0:
18
- raise ShellException(self.cmd, self.returncode, self.stdout, self.stderr)
20
+ result = self.to_result_bytes()
21
+ raise ShellException(self.cmd, self.returncode, result.stdout, result.stderr)
22
+
23
+ def to_result_bytes(self) -> ShellResult[bytes | None, bytes | None]:
24
+ stdout = self.stdout.readall() if isinstance(self.stdout, RawIOBase) else self.stdout
25
+
26
+ stderr = self.stderr.readall() if isinstance(self.stderr, RawIOBase) else self.stderr
27
+
28
+ return ShellResult(self.cmd, self.returncode, stdout, stderr)
19
29
 
20
30
 
21
31
  class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
@@ -1,69 +0,0 @@
1
- import logging
2
- import shlex
3
- from pathlib import Path
4
-
5
- from shelltastic.backend.base import RemoteShellBackend
6
- from shelltastic.backend.default.common import CommonDefaultBackend
7
- from shelltastic.enum import CaptureMode
8
- from shelltastic.exception import ShellException, SSHConnectionError
9
- from shelltastic.host import Host
10
- from shelltastic.result import ShellResult
11
-
12
- LOGGER = logging.getLogger(__name__)
13
-
14
-
15
- class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
16
- def __init__(self, host: Host) -> None:
17
- super().__init__()
18
- self.host: Host = host
19
-
20
- @staticmethod
21
- def for_host(host: Host) -> RemoteShellBackend:
22
- return DefaultRemoteBackend(host)
23
-
24
- def run(
25
- self,
26
- cmd: str | list[str],
27
- *,
28
- subshell: bool = True,
29
- cwd: str | Path | None = None,
30
- **kwargs,
31
- ) -> ShellResult:
32
- """
33
- Execute a remote command using SSH.
34
- """
35
- if isinstance(cmd, list):
36
- cmd = shlex.join(cmd)
37
-
38
- # Wrap user command in sh -c call
39
- if subshell:
40
- cmd = shlex.join(["bash", "-c", cmd])
41
-
42
- # If we're redirecting stderr to stdout,
43
- # add the same redirect inside the ssh command.
44
- if kwargs.get("stderr") == CaptureMode.STDOUT:
45
- cmd = cmd + " 2>&1"
46
-
47
- # cd into cwd first if cwd is set
48
- if cwd:
49
- cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
50
-
51
- port_flag = ["-p", str(self.host.port)] if self.host.port else []
52
-
53
- try:
54
- result = super().run(
55
- ["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
56
- )
57
- if (
58
- result.returncode == 255
59
- ): # 255 is what ssh returns if it has an error with ssh itself
60
- raise SSHConnectionError(
61
- self.host.hostname, result.returncode, result.stdout, result.stderr
62
- )
63
- return result
64
- except ShellException as ex:
65
- if ex.returncode == 255:
66
- raise SSHConnectionError(
67
- self.host.hostname, ex.returncode, ex.stdout, ex.stderr
68
- ) from ex
69
- raise
File without changes
File without changes