shelltastic 0.4.3__tar.gz → 0.4.5__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.4.3 → shelltastic-0.4.5}/PKG-INFO +1 -1
  2. {shelltastic-0.4.3 → shelltastic-0.4.5}/pyproject.toml +1 -1
  3. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/backend/__init__.py +19 -1
  4. shelltastic-0.4.5/src/shelltastic/backend/base.py +131 -0
  5. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/backend/default/remote.py +6 -0
  6. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/display.py +14 -19
  7. shelltastic-0.4.3/src/shelltastic/backend/base.py +0 -50
  8. {shelltastic-0.4.3 → shelltastic-0.4.5}/LICENSE +0 -0
  9. {shelltastic-0.4.3 → shelltastic-0.4.5}/README.md +0 -0
  10. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/__init__.py +0 -0
  11. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/backend/default/__init__.py +0 -0
  12. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/backend/default/common.py +0 -0
  13. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/backend/default/local.py +0 -0
  14. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/enum.py +0 -0
  15. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/exception.py +0 -0
  16. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/frontend/__init__.py +0 -0
  17. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/frontend/common.py +0 -0
  18. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/frontend/git.py +0 -0
  19. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/frontend/scp.py +0 -0
  20. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/frontend/shell.py +0 -0
  21. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/host.py +0 -0
  22. {shelltastic-0.4.3 → shelltastic-0.4.5}/src/shelltastic/result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.4.3
3
+ Version: 0.4.5
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.4.3"
3
+ version = "0.4.5"
4
4
  description = "A fantastic shell command runner for python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,8 +1,26 @@
1
- from shelltastic.backend.base import LocalShellBackend, RemoteShellBackend
1
+ from shelltastic.backend.base import (
2
+ BackendShim,
3
+ LocalShellBackend,
4
+ RemoteShellBackend,
5
+ ShellBackend,
6
+ add_shim,
7
+ )
2
8
  from shelltastic.backend.default.local import DefaultLocalBackend
3
9
  from shelltastic.backend.default.remote import DefaultRemoteBackend
4
10
  from shelltastic.host import Host
5
11
 
12
+ __all__ = [
13
+ "LocalShellBackend",
14
+ "RemoteShellBackend",
15
+ "BackendShim",
16
+ "ShellBackend",
17
+ "register_default_local_backend",
18
+ "register_default_remote_backend",
19
+ "add_shim",
20
+ "get_local_backend",
21
+ "get_remote_backend",
22
+ ]
23
+
6
24
  _default_local_backend: type[LocalShellBackend] = DefaultLocalBackend
7
25
  _default_remote_backend: type[RemoteShellBackend] = DefaultRemoteBackend
8
26
 
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, ABCMeta, abstractmethod
4
+ from functools import wraps
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from pathlib import Path
9
+
10
+ from shelltastic.display import IODisplay
11
+ from shelltastic.enum import CaptureMode, DisplayMode
12
+ from shelltastic.host import Host
13
+ from shelltastic.result import ShellResult
14
+
15
+
16
+ class BackendShimResult:
17
+ def __init__(self, *args, **kwargs) -> None:
18
+ self.args = args
19
+ self.kwargs = kwargs
20
+
21
+
22
+ class BackendShim(ABC):
23
+ @abstractmethod
24
+ def run_shim(
25
+ self,
26
+ backend_type: type,
27
+ /,
28
+ *args,
29
+ **kwargs,
30
+ ) -> BackendShimResult:
31
+ """
32
+ Shim for run() method of `ShellBackend`
33
+
34
+ Use `self.run(...)` to create and return a `BackendShimResult`
35
+
36
+ ```python
37
+ return self.run(*args, **kwargs)
38
+ ```
39
+ """
40
+ raise NotImplementedError()
41
+
42
+ def run(
43
+ self,
44
+ cmd: str | list[str],
45
+ *,
46
+ check: bool = True,
47
+ stdout: CaptureMode | None = None,
48
+ stderr: CaptureMode | None = None,
49
+ stdout_display: DisplayMode | IODisplay | None = None,
50
+ stderr_display: DisplayMode | IODisplay | None = None,
51
+ cwd: str | Path | None = None,
52
+ echo_cmd: DisplayMode | IODisplay | bool | None = None,
53
+ **kwargs,
54
+ ) -> BackendShimResult:
55
+ """A helper function to make a BackendShimResult"""
56
+ return BackendShimResult(
57
+ cmd,
58
+ check=check,
59
+ stdout=stdout,
60
+ stderr=stderr,
61
+ stdout_display=stdout_display,
62
+ stderr_display=stderr_display,
63
+ cwd=cwd,
64
+ echo_cmd=echo_cmd,
65
+ **kwargs,
66
+ )
67
+
68
+
69
+ _backend_shims: list[BackendShim] = []
70
+
71
+
72
+ def add_shim(shim: BackendShim):
73
+ """Add a shim to manipulate `ShellBackend` params before any `ShellBackend` runs"""
74
+ global _backend_shims
75
+ _backend_shims.append(shim)
76
+
77
+
78
+ def _shim_run(f, backend_type: type):
79
+ @wraps(f)
80
+ def wrapper(*args, **kwargs):
81
+ result: BackendShimResult = BackendShimResult(*args, **kwargs)
82
+ for shim in _backend_shims:
83
+ result = shim.run_shim(backend_type, *result.args, **result.kwargs)
84
+ return f(*result.args, **result.kwargs)
85
+
86
+ return wrapper
87
+
88
+
89
+ class _InstallShims(ABCMeta):
90
+ def __call__(self, *args: Any, **kwds: Any) -> Any:
91
+ instance = super().__call__(*args, **kwds)
92
+ instance.run = _shim_run(instance.run, instance.__class__)
93
+ return instance
94
+
95
+
96
+ class ShellBackend(metaclass=_InstallShims):
97
+ __slots__ = ()
98
+
99
+ @abstractmethod
100
+ def run(
101
+ self,
102
+ cmd: str | list[str],
103
+ *,
104
+ check: bool = True,
105
+ stdout: CaptureMode | None = None,
106
+ stderr: CaptureMode | None = None,
107
+ stdout_display: DisplayMode | IODisplay | None = None,
108
+ stderr_display: DisplayMode | IODisplay | None = None,
109
+ cwd: str | Path | None = None,
110
+ echo_cmd: DisplayMode | IODisplay | bool | None = None,
111
+ **kwargs,
112
+ ) -> ShellResult:
113
+ raise NotImplementedError()
114
+
115
+
116
+ class LocalShellBackend(ShellBackend):
117
+ __slots__ = ()
118
+
119
+ @staticmethod
120
+ @abstractmethod
121
+ def local() -> LocalShellBackend:
122
+ raise NotImplementedError()
123
+
124
+
125
+ class RemoteShellBackend(ShellBackend):
126
+ __slots__ = ()
127
+
128
+ @staticmethod
129
+ @abstractmethod
130
+ def for_host(host: Host) -> RemoteShellBackend:
131
+ raise NotImplementedError()
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from shelltastic.backend.base import RemoteShellBackend
8
8
  from shelltastic.backend.default.common import CommonDefaultBackend
9
+ from shelltastic.enum import CaptureMode
9
10
  from shelltastic.exception import ShellException, SSHConnectionError
10
11
 
11
12
  if TYPE_CHECKING:
@@ -44,6 +45,11 @@ class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
44
45
  if subshell:
45
46
  cmd = shlex.join(["bash", "-c", cmd])
46
47
 
48
+ # If we're redirecting stderr to stdout,
49
+ # add the same redirect inside the ssh command.
50
+ if kwargs.get("stderr") == CaptureMode.STDOUT:
51
+ cmd = cmd + " 2>&1"
52
+
47
53
  # cd into cwd first if cwd is set
48
54
  if cwd:
49
55
  cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
@@ -1,43 +1,41 @@
1
1
  import logging
2
2
  import sys
3
+ import threading
3
4
  from abc import ABC, abstractmethod
4
5
 
5
6
  from shelltastic.enum import DisplayMode
6
7
 
7
8
  LOGGER = logging.getLogger(__name__)
8
9
 
10
+ _io_display_lock = threading.RLock()
11
+
9
12
 
10
13
  class IODisplay(ABC):
11
- @abstractmethod
12
14
  def printbytes(self, line: bytes):
13
- raise NotImplementedError
15
+ with _io_display_lock:
16
+ self._emit_line(line.decode().rstrip())
14
17
 
15
- @abstractmethod
16
18
  def printline(self, msg: object, *args: object):
19
+ with _io_display_lock:
20
+ self._emit_line(msg, *args)
21
+
22
+ @abstractmethod
23
+ def _emit_line(self, msg: object, *args: object):
17
24
  raise NotImplementedError
18
25
 
19
26
 
20
27
  class DevNullDisplay(IODisplay):
21
- def printbytes(self, line: bytes):
22
- pass
23
-
24
- def printline(self, msg, *args):
28
+ def _emit_line(self, msg: object, *args: object):
25
29
  pass
26
30
 
27
31
 
28
32
  class StdoutDisplay(IODisplay):
29
- def printbytes(self, line: bytes):
30
- print(line.decode(), end="", flush=True)
31
-
32
- def printline(self, msg, *args):
33
+ def _emit_line(self, msg: object, *args: object):
33
34
  print(str(msg) % args, flush=True)
34
35
 
35
36
 
36
37
  class StderrDisplay(IODisplay):
37
- def printbytes(self, line: bytes):
38
- print(line.decode(), end="", file=sys.stderr, flush=True)
39
-
40
- def printline(self, msg, *args):
38
+ def _emit_line(self, msg: object, *args: object):
41
39
  print(str(msg) % args, file=sys.stderr, flush=True)
42
40
 
43
41
 
@@ -46,10 +44,7 @@ class LogDisplay(IODisplay):
46
44
  super().__init__()
47
45
  self.log_level = log_level
48
46
 
49
- def printbytes(self, line: bytes):
50
- LOGGER.log(self.log_level, line.decode().rstrip(), stacklevel=2)
51
-
52
- def printline(self, msg: object, *args: object):
47
+ def _emit_line(self, msg: object, *args: object):
53
48
  LOGGER.log(self.log_level, msg, *args, stacklevel=2)
54
49
 
55
50
 
@@ -1,50 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- from pathlib import Path
8
-
9
- from shelltastic.display import IODisplay
10
- from shelltastic.enum import CaptureMode, DisplayMode
11
- from shelltastic.host import Host
12
- from shelltastic.result import ShellResult
13
-
14
-
15
- class ShellBackend(ABC):
16
- __slots__ = ()
17
-
18
- @abstractmethod
19
- def run(
20
- self,
21
- cmd: str | list[str],
22
- *,
23
- check: bool = True,
24
- stdout: CaptureMode | None = None,
25
- stderr: CaptureMode | None = None,
26
- stdout_display: DisplayMode | IODisplay | None = None,
27
- stderr_display: DisplayMode | IODisplay | None = None,
28
- cwd: str | Path | None = None,
29
- echo_cmd: DisplayMode | IODisplay | bool | None = None,
30
- **kwargs,
31
- ) -> ShellResult:
32
- raise NotImplementedError()
33
-
34
-
35
- class LocalShellBackend(ShellBackend):
36
- __slots__ = ()
37
-
38
- @staticmethod
39
- @abstractmethod
40
- def local() -> LocalShellBackend:
41
- raise NotImplementedError()
42
-
43
-
44
- class RemoteShellBackend(ShellBackend):
45
- __slots__ = ()
46
-
47
- @staticmethod
48
- @abstractmethod
49
- def for_host(host: Host) -> RemoteShellBackend:
50
- raise NotImplementedError()
File without changes
File without changes