pyallel 0.13.0__tar.gz → 0.14.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyallel
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Run and handle the output of multiple executables in pyallel (as in parallel)
5
5
  Home-page: https://github.com/Danthewaann/pyallel
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyallel"
3
- version = "0.13.0"
3
+ version = "0.14.0"
4
4
  description = "Run and handle the output of multiple executables in pyallel (as in parallel)"
5
5
  authors = ["Daniel Black <danielcrblack@gmail.com>"]
6
6
  license = "MIT"
@@ -51,14 +51,12 @@ def run(*args: str) -> int:
51
51
  exit_code = 1
52
52
  message = traceback.format_exc()
53
53
 
54
- if exit_code == 2:
55
- print(f"{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}")
56
- elif exit_code == 1:
54
+ if exit_code == 1:
57
55
  if not message:
58
56
  print(f"{constants.RED_BOLD}A command failed!{constants.RESET_COLOUR}")
59
57
  else:
60
58
  print(f"{constants.RED_BOLD}Error: {message}{constants.RESET_COLOUR}")
61
- else:
59
+ elif exit_code == 0:
62
60
  print(f"{constants.GREEN_BOLD}Success!{constants.RESET_COLOUR}")
63
61
 
64
62
  return exit_code
@@ -9,7 +9,7 @@ import shlex
9
9
  import shutil
10
10
  import os
11
11
  from uuid import UUID, uuid4
12
- from typing import BinaryIO
12
+ from typing import Any, BinaryIO
13
13
  from pyallel import constants
14
14
 
15
15
  from dataclasses import dataclass, field
@@ -83,6 +83,8 @@ class ProcessGroup:
83
83
  output: dict[UUID, list[str]] = field(default_factory=lambda: defaultdict(list))
84
84
  process_lines: list[int] = field(default_factory=list)
85
85
  completed_processes: set[UUID] = field(default_factory=set)
86
+ exit_code: int = 0
87
+ interrupt_count: int = 0
86
88
  passed: bool = True
87
89
  icon: int = 0
88
90
 
@@ -93,36 +95,31 @@ class ProcessGroup:
93
95
  if not self.interactive:
94
96
  return self.stream_non_interactive()
95
97
 
96
- interrupted = False
97
-
98
98
  while True:
99
- try:
100
- output = self.complete_output()
101
- self.icon += 1
102
- if self.icon == len(constants.ICONS):
103
- self.icon = 0
99
+ output = self.complete_output()
100
+ self.icon += 1
101
+ if self.icon == len(constants.ICONS):
102
+ self.icon = 0
104
103
 
105
- # Clear the screen and print the output
106
- print(f"\033[H\033[0J{output}", end="")
104
+ # Clear the screen and print the output
105
+ print(f"\033[H\033[0J{output}", end="")
107
106
 
108
- # Clear the screen again
109
- print("\033[H\033[0J", end="")
107
+ # Clear the screen again
108
+ print("\033[H\033[0J", end="")
110
109
 
111
- if len(self.completed_processes) == len(self.processes):
112
- break
110
+ if len(self.completed_processes) == len(self.processes):
111
+ break
113
112
 
114
- time.sleep(0.1)
115
- except KeyboardInterrupt:
116
- interrupted = True
117
- for process in self.processes:
118
- process.interrupt()
119
- process.wait()
113
+ time.sleep(0.1)
120
114
 
121
115
  output = self.complete_output(all=True)
122
116
  # Clear the screen one final time before printing the output
123
117
  print(f"\033[3J{output}")
124
118
 
125
- return 2 if interrupted else (1 if not self.passed else 0)
119
+ if not self.exit_code and not self.passed:
120
+ self.exit_code = 1
121
+
122
+ return self.exit_code
126
123
 
127
124
  def stream_non_interactive(self) -> int:
128
125
  running_process = None
@@ -131,67 +128,88 @@ class ProcessGroup:
131
128
  print(f"{constants.WHITE_BOLD}Running commands...{constants.RESET_COLOUR}\n")
132
129
 
133
130
  while True:
134
- try:
135
- output = ""
136
- for process in self.processes:
137
- if (
138
- running_process is None
139
- and process.id not in self.completed_processes
131
+ output = ""
132
+ for process in self.processes:
133
+ if (
134
+ running_process is None
135
+ and process.id not in self.completed_processes
136
+ ):
137
+ output += get_command_status(process, verbose=self.verbose)
138
+ output += "\n"
139
+ running_process = process
140
+ elif running_process is not process:
141
+ # Need to do this to properly keep track of how long all the other
142
+ # commands are taking
143
+ process.poll()
144
+ continue
145
+
146
+ process_output = process.readline().decode()
147
+
148
+ if not self.output[process.id] and process_output:
149
+ process_output = prefix(process_output)
150
+ self.output[process.id].append(process_output)
151
+ output += process_output
152
+ elif process_output:
153
+ if self.output[process.id][-1][-1] != "\n":
154
+ self.output[process.id][-1] += process_output
155
+ else:
156
+ process_output = prefix(process_output)
157
+ self.output[process.id].append(process_output)
158
+ output += process_output
159
+
160
+ if process.poll() is not None:
161
+ if process.return_code() != 0:
162
+ self.passed = False
163
+ process_output = process.read().decode()
164
+ if process_output:
165
+ output += prefix(process_output)
166
+
167
+ output += get_command_status(
168
+ process,
169
+ passed=process.return_code() == 0,
170
+ verbose=self.verbose,
171
+ timer=self.timer,
172
+ )
173
+ output += "\n\n"
174
+ self.completed_processes.add(process.id)
175
+ running_process = None
176
+
177
+ if self.interrupt_count == 0:
178
+ pass
179
+ elif not interrupted and self.interrupt_count == 1:
180
+ if (output and output[-1] != "\n") or (
181
+ self.output[process.id]
182
+ and self.output[process.id][-1][-1] != "\n"
140
183
  ):
141
- output += get_command_status(process, verbose=self.verbose)
142
184
  output += "\n"
143
- running_process = process
144
- elif running_process is not process:
145
- # Need to do this to properly keep track of how long all the other
146
- # commands are taking
147
- process.poll()
148
- continue
185
+ output += f"\n{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}\n\n"
186
+ interrupted = True
149
187
 
150
- process_output = process.readline().decode()
188
+ if output:
189
+ print(output, end="")
151
190
 
152
- if not self.output[process.id] and process_output:
153
- process_output = prefix(process_output)
154
- self.output[process.id].append(process_output)
155
- output += process_output
156
- elif process_output:
157
- if self.output[process.id][-1][-1] != "\n":
158
- self.output[process.id][-1] += process_output
159
- else:
160
- process_output = prefix(process_output)
161
- self.output[process.id].append(process_output)
162
- output += process_output
163
-
164
- if process.poll() is not None:
165
- if process.return_code() != 0:
166
- self.passed = False
167
- process_output = process.read().decode()
168
- if process_output:
169
- output += prefix(process_output)
170
-
171
- output += get_command_status(
172
- process,
173
- passed=process.return_code() == 0,
174
- verbose=self.verbose,
175
- timer=self.timer,
176
- )
177
- output += "\n\n"
178
- self.completed_processes.add(process.id)
179
- running_process = None
180
-
181
- if output:
182
- print(output, end="")
183
-
184
- if len(self.completed_processes) == len(self.processes):
185
- break
186
-
187
- time.sleep(0.01)
188
- except KeyboardInterrupt:
189
- interrupted = True
190
- for process in self.processes:
191
- process.interrupt()
192
- process.wait()
193
-
194
- return 2 if interrupted else (1 if not self.passed else 0)
191
+ if len(self.completed_processes) == len(self.processes):
192
+ break
193
+
194
+ time.sleep(0.01)
195
+
196
+ if self.interrupt_count == 2:
197
+ print(f"{constants.RED_BOLD}Abort!{constants.RESET_COLOUR}")
198
+
199
+ if not self.exit_code and not self.passed:
200
+ self.exit_code = 1
201
+
202
+ return self.exit_code
203
+
204
+ def _handle_signal(self, signum: int, _frame: Any) -> None:
205
+ for process in self.processes:
206
+ if self.interrupt_count == 0:
207
+ process.interrupt()
208
+ else:
209
+ process.kill()
210
+
211
+ self.exit_code = 128 + signum
212
+ self.interrupt_count += 1
195
213
 
196
214
  @classmethod
197
215
  def from_commands(
@@ -213,13 +231,18 @@ class ProcessGroup:
213
231
  if errors:
214
232
  raise InvalidExecutableErrors(*errors)
215
233
 
216
- return cls(
234
+ process_group = cls(
217
235
  processes=processes,
218
236
  interactive=interactive,
219
237
  timer=timer,
220
238
  verbose=verbose,
221
239
  )
222
240
 
241
+ signal.signal(signal.SIGINT, process_group._handle_signal)
242
+ signal.signal(signal.SIGTERM, process_group._handle_signal)
243
+
244
+ return process_group
245
+
223
246
  def complete_output(self, tail: int = 20, all: bool = False) -> str:
224
247
  num_processes = len(self.processes)
225
248
  lines = constants.LINES() - (2 * num_processes)
@@ -276,6 +299,14 @@ class ProcessGroup:
276
299
  if i != num_processes:
277
300
  output += "\n"
278
301
 
302
+ if self.interrupt_count == 0:
303
+ return output
304
+
305
+ if self.interrupt_count == 1:
306
+ output += f"\n{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}"
307
+ elif self.interrupt_count == 2:
308
+ output += f"\n{constants.RED_BOLD}Abort!{constants.RESET_COLOUR}"
309
+
279
310
  return output
280
311
 
281
312
 
@@ -341,6 +372,10 @@ class Process:
341
372
  if self._process:
342
373
  self._process.send_signal(signal.SIGINT)
343
374
 
375
+ def kill(self) -> None:
376
+ if self._process:
377
+ self._process.send_signal(signal.SIGKILL)
378
+
344
379
  def wait(self) -> int:
345
380
  if self._process:
346
381
  return self._process.wait()
File without changes
File without changes
File without changes
File without changes
File without changes