forked from cuinixam/python-app-dev
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsubprocess.py
More file actions
99 lines (88 loc) · 4.02 KB
/
subprocess.py
File metadata and controls
99 lines (88 loc) · 4.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import locale
import shutil
import subprocess # nosec
from pathlib import Path
from typing import Any
from .exceptions import UserNotificationException
from .logging import logger
def which(app_name: str) -> Path | None:
"""Return the path to the app if it is in the PATH, otherwise return None."""
app_path = shutil.which(app_name)
return Path(app_path) if app_path else None
class SubprocessExecutor:
"""
Execute a command in a subprocess.
Args:
----
capture_output: If True, the output of the command will be captured.
print_output: If True, the output of the command will be printed to the logger.
One can set this to false in order to get the output in the returned CompletedProcess object.
"""
def __init__(
self,
command: str | list[str | Path],
cwd: Path | None = None,
capture_output: bool = True,
env: dict[str, str] | None = None,
shell: bool = False,
print_output: bool = True,
):
self.logger = logger.bind()
self.command = command
self.current_working_directory = cwd
self.capture_output = capture_output
self.env = env
self.shell = shell
self.print_output = print_output
@property
def command_str(self) -> str:
if isinstance(self.command, str):
return self.command
return " ".join(str(arg) if not isinstance(arg, str) else arg for arg in self.command)
def execute(self, handle_errors: bool = True) -> subprocess.CompletedProcess[Any] | None:
"""Execute the command and return the CompletedProcess object if handle_errors is False."""
try:
completed_process = None
stdout = ""
stderr = ""
self.logger.info(f"Running command: {self.command_str}")
cwd_path = (self.current_working_directory or Path.cwd()).as_posix()
with subprocess.Popen(
args=self.command,
cwd=cwd_path,
# Combine both streams to stdout (when captured)
stdout=(subprocess.PIPE if self.capture_output else subprocess.DEVNULL),
stderr=(subprocess.STDOUT if self.capture_output else subprocess.DEVNULL),
# enables line buffering, line is flushed after each \n
bufsize=1,
text=True,
# every new line is a \n
universal_newlines=True,
# decode bytes to str using current locale/system encoding
encoding=locale.getpreferredencoding(False),
# replace unknown characters with �
errors="replace",
env=self.env,
shell=self.shell,
) as process: # nosec
if self.capture_output and process.stdout is not None:
if self.print_output:
for line in iter(process.stdout.readline, ""):
self.logger.info(line.strip())
stdout += line
process.wait()
else:
stdout, stderr = process.communicate()
if handle_errors:
# Check return code
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, self.command_str)
else:
completed_process = subprocess.CompletedProcess(process.args, process.returncode, stdout, stderr)
except subprocess.CalledProcessError as e:
raise UserNotificationException(f"Command '{self.command_str}' execution failed with return code {e.returncode}") from None
except FileNotFoundError as e:
raise UserNotificationException(f"Command '{self.command_str}' could not be executed. Failed with error {e}") from None
except KeyboardInterrupt:
raise UserNotificationException(f"Command '{self.command_str}' execution interrupted by user") from None
return completed_process