shelltastic 0.4.1__tar.gz → 0.4.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.4.1 → shelltastic-0.4.2}/PKG-INFO +1 -1
  2. shelltastic-0.4.2/pyproject.toml +45 -0
  3. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/backend/__init__.py +6 -8
  4. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/backend/base.py +8 -5
  5. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/backend/default/common.py +91 -86
  6. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/backend/default/remote.py +8 -4
  7. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/frontend/common.py +2 -4
  8. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/frontend/git.py +3 -5
  9. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/frontend/shell.py +8 -7
  10. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/host.py +1 -2
  11. shelltastic-0.4.1/pyproject.toml +0 -21
  12. {shelltastic-0.4.1 → shelltastic-0.4.2}/LICENSE +0 -0
  13. {shelltastic-0.4.1 → shelltastic-0.4.2}/README.md +0 -0
  14. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/__init__.py +0 -0
  15. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/backend/default/__init__.py +0 -0
  16. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/backend/default/local.py +0 -0
  17. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/display.py +0 -0
  18. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/enum.py +0 -0
  19. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/exception.py +0 -0
  20. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/frontend/__init__.py +0 -0
  21. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/frontend/scp.py +0 -0
  22. {shelltastic-0.4.1 → shelltastic-0.4.2}/src/shelltastic/result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: A fantastic shell command runner for python
5
5
  Author: Bearmine
6
6
  License-Expression: MPL-2.0
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "shelltastic"
3
+ version = "0.4.2"
4
+ description = "A fantastic shell command runner for python"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Bearmine" }
8
+ ]
9
+ license = "MPL-2.0"
10
+ license-files = [
11
+ "LICENSE"
12
+ ]
13
+ requires-python = ">=3.12"
14
+ dependencies = []
15
+
16
+ [project.urls]
17
+ Repository = "https://forge.bearmine.com/bearmine/shelltastic"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.11.18,<0.12.0"]
21
+ build-backend = "uv_build"
22
+
23
+ [tool.ruff.lint]
24
+ extend-select = [
25
+ "F", # Pyflakes rules
26
+ "W", # PyCodeStyle warnings
27
+ "E", # PyCodeStyle errors
28
+ "I", # Sort imports properly
29
+ "UP", # Warn if certain things can changed due to newer Python versions
30
+ "C4", # Catch incorrect use of comprehensions, dict, list, etc
31
+ "FA", # Enforce from __future__ import annotations
32
+ "ISC", # Good use of string concatenation
33
+ "ICN", # Use common import conventions
34
+ "RET", # Good return practices
35
+ "SIM", # Common simplification rules
36
+ "TID", # Some good import practices
37
+ "TC", # Enforce importing certain types in a TYPE_CHECKING block
38
+ "PTH", # Use pathlib instead of os.path
39
+ "TD", # Be diligent with TODO comments
40
+ "NPY", # Some numpy-specific things
41
+ "FURB", # Suggest more idiomatic Python patterns
42
+ ]
43
+
44
+ [tool.ruff.lint.pycodestyle]
45
+ max-line-length = 100
@@ -1,26 +1,24 @@
1
- from typing import Type
2
-
3
1
  from shelltastic.backend.base import LocalShellBackend, RemoteShellBackend
4
2
  from shelltastic.backend.default.local import DefaultLocalBackend
5
3
  from shelltastic.backend.default.remote import DefaultRemoteBackend
6
4
  from shelltastic.host import Host
7
5
 
8
- _default_local_backend: Type[LocalShellBackend] = DefaultLocalBackend
9
- _default_remote_backend: Type[RemoteShellBackend] = DefaultRemoteBackend
6
+ _default_local_backend: type[LocalShellBackend] = DefaultLocalBackend
7
+ _default_remote_backend: type[RemoteShellBackend] = DefaultRemoteBackend
10
8
 
11
9
 
12
- def register_default_local_backend(backend: Type[LocalShellBackend]):
10
+ def register_default_local_backend(backend: type[LocalShellBackend]):
13
11
  global _default_local_backend
14
12
  _default_local_backend = backend
15
13
 
16
14
 
17
- def register_default_remote_backend(backend: Type[RemoteShellBackend]):
15
+ def register_default_remote_backend(backend: type[RemoteShellBackend]):
18
16
  global _default_remote_backend
19
17
  _default_remote_backend = backend
20
18
 
21
19
 
22
20
  def get_local_backend(
23
- *, backend: Type[LocalShellBackend] | None = None
21
+ *, backend: type[LocalShellBackend] | None = None
24
22
  ) -> LocalShellBackend:
25
23
  if backend:
26
24
  return backend.local()
@@ -28,7 +26,7 @@ def get_local_backend(
28
26
 
29
27
 
30
28
  def get_remote_backend(
31
- host: Host, *, backend: Type[RemoteShellBackend] | None = None
29
+ host: Host, *, backend: type[RemoteShellBackend] | None = None
32
30
  ) -> RemoteShellBackend:
33
31
  if backend:
34
32
  return backend.for_host(host)
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
5
 
6
- from shelltastic.display import IODisplay
7
- from shelltastic.enum import CaptureMode, DisplayMode
8
- from shelltastic.host import Host
9
- from shelltastic.result import ShellResult
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
10
13
 
11
14
 
12
15
  class ShellBackend(ABC):
@@ -1,17 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import pathlib
5
4
  import shlex
6
5
  import subprocess
7
6
  from multiprocessing.pool import ThreadPool
8
- from typing import IO, Literal
7
+ from typing import IO, TYPE_CHECKING, Literal
9
8
 
10
9
  from shelltastic import display
11
10
  from shelltastic.backend.base import ShellBackend
12
11
  from shelltastic.enum import CaptureMode, DisplayMode
13
12
  from shelltastic.result import ShellResult
14
13
 
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
15
17
  LOGGER = logging.getLogger(__name__)
16
18
 
17
19
 
@@ -23,67 +25,62 @@ def _output_and_collect(
23
25
 
24
26
  if collect:
25
27
  if display:
26
- return_bytes = bytes()
28
+ return_bytes: bytes = b""
27
29
  for line in io:
28
30
  return_bytes += line
29
31
  display.printbytes(line)
30
32
  return return_bytes
31
- else:
32
- return io.read()
33
- else:
34
- if display:
35
- for line in io:
36
- display.printbytes(line)
37
- return None
33
+ return io.read()
34
+
35
+ if display:
36
+ for line in io:
37
+ display.printbytes(line)
38
+
39
+ return None
38
40
 
39
41
 
40
42
  def _determine_subprocess_output_mode(
41
43
  capture_mode: CaptureMode | None,
42
44
  display_mode: DisplayMode | display.IODisplay | None,
43
45
  output_name: Literal["stdout", "stderr"],
44
- ) -> int | None:
45
- # If DisplayMode is None, we act like subpress does
46
+ ) -> int:
47
+ # If DisplayMode is None, we act like subprocess does
46
48
  if display_mode is None:
47
49
  if capture_mode == CaptureMode.PIPE:
48
50
  return subprocess.PIPE
49
- elif capture_mode == CaptureMode.STDOUT:
51
+ if capture_mode == CaptureMode.STDOUT:
50
52
  return subprocess.STDOUT
51
- elif capture_mode == CaptureMode.DEVNULL:
53
+ if capture_mode == CaptureMode.DEVNULL:
52
54
  return subprocess.DEVNULL
53
- else:
54
- assert capture_mode is None
55
- return None
55
+ return subprocess.PIPE
56
56
 
57
57
  # Don't print
58
58
  if display_mode == DisplayMode.DEVNULL:
59
59
  if capture_mode == CaptureMode.PIPE:
60
60
  return subprocess.PIPE
61
- elif capture_mode == CaptureMode.STDOUT:
61
+ if capture_mode == CaptureMode.STDOUT:
62
62
  if output_name == "stdout":
63
63
  # stdout
64
64
  return subprocess.PIPE
65
- else:
66
- # stderr
67
- return subprocess.STDOUT
68
- elif capture_mode == CaptureMode.DEVNULL:
69
- return subprocess.DEVNULL
70
- else:
71
- assert capture_mode is None
65
+ # stderr
66
+ return subprocess.STDOUT
67
+ if capture_mode == CaptureMode.DEVNULL:
72
68
  return subprocess.DEVNULL
69
+ assert capture_mode is None
70
+ return subprocess.DEVNULL
73
71
 
74
72
  # Print Output
75
73
  # display_mode == STDOUT, STDERR, LOG, or is an IODisplay
76
74
  if capture_mode == CaptureMode.PIPE:
77
75
  return subprocess.PIPE
78
- elif capture_mode == CaptureMode.STDOUT:
76
+ if capture_mode == CaptureMode.STDOUT:
79
77
  return subprocess.STDOUT
80
- elif capture_mode == CaptureMode.DEVNULL:
81
- # We need to PIPE to be able to print the output
82
- return subprocess.PIPE
83
- else:
84
- assert capture_mode is None
78
+ if capture_mode == CaptureMode.DEVNULL:
85
79
  # We need to PIPE to be able to print the output
86
80
  return subprocess.PIPE
81
+ assert capture_mode is None
82
+ # We need to PIPE to be able to print the output
83
+ return subprocess.PIPE
87
84
 
88
85
 
89
86
  class CommonDefaultBackend(ShellBackend):
@@ -98,7 +95,7 @@ class CommonDefaultBackend(ShellBackend):
98
95
  stderr: CaptureMode | None = None,
99
96
  stdout_display: DisplayMode | display.IODisplay | None = None,
100
97
  stderr_display: DisplayMode | display.IODisplay | None = None,
101
- cwd: str | pathlib.Path | None = None,
98
+ cwd: str | Path | None = None,
102
99
  echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
103
100
  **kwargs,
104
101
  ) -> ShellResult:
@@ -106,19 +103,13 @@ class CommonDefaultBackend(ShellBackend):
106
103
  raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
107
104
 
108
105
  # If cmd is a str, run as shell
109
- if isinstance(cmd, str):
110
- shell = True
111
- else:
112
- shell = False
106
+ shell = isinstance(cmd, str)
113
107
 
114
108
  # Echo command based on setting
115
109
  # Defaults to DEBUG_LOG
116
110
  # False to disable
117
111
  if isinstance(echo_cmd, bool):
118
- if echo_cmd:
119
- echo_cmd = DisplayMode.STDOUT
120
- else:
121
- echo_cmd = DisplayMode.DEVNULL
112
+ echo_cmd = DisplayMode.STDOUT if echo_cmd else DisplayMode.DEVNULL
122
113
 
123
114
  if echo_cmd is None:
124
115
  echo_cmd = DisplayMode.DEBUG_LOG
@@ -129,9 +120,9 @@ class CommonDefaultBackend(ShellBackend):
129
120
  echo_display = display.from_mode(echo_cmd)
130
121
 
131
122
  if isinstance(cmd, str):
132
- echo_display.printline("%s> %s", cwd if cwd else "", cmd)
123
+ echo_display.printline("%s> %s", cwd or "", cmd)
133
124
  else:
134
- echo_display.printline("%s> %s", cwd if cwd else "", shlex.join(cmd))
125
+ echo_display.printline("%s> %s", cwd or "", shlex.join(cmd))
135
126
 
136
127
  # Determine PIPE mode for stdout
137
128
  stdout_sub_mode = _determine_subprocess_output_mode(
@@ -143,51 +134,65 @@ class CommonDefaultBackend(ShellBackend):
143
134
  stderr, stderr_display, "stderr"
144
135
  )
145
136
 
137
+ if stdout_display is None:
138
+ stdout_display = (
139
+ DisplayMode.STDOUT if stdout is None else DisplayMode.DEVNULL
140
+ )
141
+
142
+ if stderr_display is None:
143
+ stderr_display = (
144
+ DisplayMode.STDERR if stderr is None else DisplayMode.DEVNULL
145
+ )
146
+
146
147
  # Run subprocess command
147
- with subprocess.Popen(
148
- cmd, shell=shell, stdout=stdout_sub_mode, stderr=stderr_sub_mode, cwd=cwd
149
- ) as popen:
150
- with ThreadPool(2) as display_pool:
151
- # Print out and capture stdout
152
- stdout_result = display_pool.apply_async(
153
- _output_and_collect,
154
- args=[
155
- popen.stdout,
156
- (
157
- stdout_display
158
- if stdout_display is None
159
- or isinstance(stdout_display, display.IODisplay)
160
- else display.from_mode(stdout_display)
161
- ),
162
- False if stdout == CaptureMode.DEVNULL else True,
163
- ],
164
- )
165
-
166
- # Print out and capture stderr
167
- stderr_result = display_pool.apply_async(
168
- _output_and_collect,
169
- args=[
170
- popen.stderr,
171
- (
172
- stderr_display
173
- if stderr_display is None
174
- or isinstance(stderr_display, display.IODisplay)
175
- else display.from_mode(stderr_display)
176
- ),
177
- False if stderr == CaptureMode.DEVNULL else True,
178
- ],
179
- )
180
-
181
- # Wait on command completion
182
- returncode = popen.wait()
183
-
184
- # Build our result
185
- result = ShellResult(
186
- cmd=cmd,
187
- stdout=stdout_result.get(),
188
- stderr=stderr_result.get(),
189
- returncode=returncode,
190
- )
148
+ with (
149
+ subprocess.Popen(
150
+ cmd,
151
+ shell=shell,
152
+ stdout=stdout_sub_mode,
153
+ stderr=stderr_sub_mode,
154
+ cwd=cwd,
155
+ ) as popen,
156
+ ThreadPool(2) as display_pool,
157
+ ):
158
+ # Print out and capture stdout
159
+ stdout_result = display_pool.apply_async(
160
+ _output_and_collect,
161
+ args=[
162
+ popen.stdout,
163
+ (
164
+ stdout_display
165
+ if isinstance(stdout_display, display.IODisplay)
166
+ else display.from_mode(stdout_display)
167
+ ),
168
+ stdout is not None and stdout != CaptureMode.DEVNULL,
169
+ ],
170
+ )
171
+
172
+ # Print out and capture stderr
173
+ stderr_result = display_pool.apply_async(
174
+ _output_and_collect,
175
+ args=[
176
+ popen.stderr,
177
+ (
178
+ stderr_display
179
+ if isinstance(stderr_display, display.IODisplay)
180
+ else display.from_mode(stderr_display)
181
+ ),
182
+ stderr is not None and stderr != CaptureMode.DEVNULL,
183
+ ],
184
+ )
185
+
186
+ # Wait on command completion
187
+ returncode = popen.wait()
188
+
189
+ # Build our result
190
+ result = ShellResult(
191
+ cmd=cmd,
192
+ stdout=stdout_result.get(),
193
+ stderr=stderr_result.get(),
194
+ returncode=returncode,
195
+ )
191
196
 
192
197
  # If check is True, raise exception if returncode is not 0
193
198
  if check:
@@ -1,14 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import pathlib
5
4
  import shlex
5
+ 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
9
  from shelltastic.exception import ShellException, SSHConnectionError
10
- from shelltastic.host import Host
11
- from shelltastic.result import ShellResult
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+ from shelltastic.host import Host
15
+ from shelltastic.result import ShellResult
12
16
 
13
17
  LOGGER = logging.getLogger(__name__)
14
18
 
@@ -27,7 +31,7 @@ class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
27
31
  cmd: str | list[str],
28
32
  *,
29
33
  subshell: bool = True,
30
- cwd: str | pathlib.Path | None = None,
34
+ cwd: str | Path | None = None,
31
35
  **kwargs,
32
36
  ) -> ShellResult:
33
37
  """
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Generic, TypeVar, overload
2
+ from typing import overload
3
3
 
4
4
  from shelltastic.host import Host
5
5
 
@@ -12,10 +12,8 @@ class RemoteFrontend(ABC):
12
12
  return self._host
13
13
 
14
14
 
15
- T = TypeVar("T", bound=RemoteFrontend)
16
15
 
17
-
18
- class RemoteFrontendFactory(Generic[T], ABC):
16
+ class RemoteFrontendFactory[T: RemoteFrontend](ABC):
19
17
  __slots__ = ()
20
18
 
21
19
  @overload
@@ -58,8 +58,7 @@ class GitFrontend:
58
58
  stderr=CaptureMode.PIPE,
59
59
  )
60
60
  assert result.stdout is not None
61
- configs = [i.strip() for i in result.stdout.decode().split("\n") if i.strip()]
62
- return configs
61
+ return [i.strip() for i in result.stdout.decode().split("\n") if i.strip()]
63
62
 
64
63
  def set_global_config(self, key: str, value: str | list[str]):
65
64
  LOGGER.info(
@@ -89,10 +88,9 @@ class GitFrontend:
89
88
  )
90
89
  if response.returncode != 0:
91
90
  return []
92
- else:
93
- assert response.stdout is not None
94
- gitcredentials = response.stdout.decode()
95
91
 
92
+ assert response.stdout is not None
93
+ gitcredentials = response.stdout.decode()
96
94
  return gitcredentials.splitlines()
97
95
 
98
96
 
@@ -2,14 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import shutil
4
4
  from pathlib import Path
5
+ from typing import TYPE_CHECKING
5
6
 
6
7
  from shelltastic import backend
7
- from shelltastic.backend.base import ShellBackend
8
- from shelltastic.display import IODisplay
9
8
  from shelltastic.enum import CaptureMode, DisplayMode, SystemType
10
9
  from shelltastic.frontend.common import RemoteFrontend, RemoteFrontendFactory
11
- from shelltastic.host import Host
12
- from shelltastic.result import ShellResult
10
+
11
+ if TYPE_CHECKING:
12
+ from shelltastic.backend.base import ShellBackend
13
+ from shelltastic.display import IODisplay
14
+ from shelltastic.host import Host
15
+ from shelltastic.result import ShellResult
13
16
 
14
17
 
15
18
  class ShellFrontend:
@@ -35,9 +38,7 @@ class ShellFrontend:
35
38
  stdout=CaptureMode.DEVNULL,
36
39
  stderr=CaptureMode.DEVNULL,
37
40
  )
38
- if result.returncode == 0:
39
- return True
40
- return False
41
+ return result.returncode == 0
41
42
 
42
43
  def home_dir(self) -> Path:
43
44
  """Return the path to the current user's home directory"""
@@ -10,5 +10,4 @@ class Host:
10
10
  def host_specifier(self):
11
11
  if self.username:
12
12
  return f"{self.username}@{self.hostname}"
13
- else:
14
- return self.hostname
13
+ return self.hostname
@@ -1,21 +0,0 @@
1
- [project]
2
- name = "shelltastic"
3
- version = "0.4.1"
4
- description = "A fantastic shell command runner for python"
5
- readme = "README.md"
6
- authors = [
7
- { name = "Bearmine" }
8
- ]
9
- license = "MPL-2.0"
10
- license-files = [
11
- "LICENSE"
12
- ]
13
- requires-python = ">=3.12"
14
- dependencies = []
15
-
16
- [project.urls]
17
- Repository = "https://forge.bearmine.com/bearmine/shelltastic"
18
-
19
- [build-system]
20
- requires = ["uv_build>=0.11.18,<0.12.0"]
21
- build-backend = "uv_build"
File without changes
File without changes