pyallel 0.12.2__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.12.2
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
@@ -40,7 +40,7 @@ pip install pyallel
40
40
  Once installed, you can run `pyallel` to see usage information, like so:
41
41
 
42
42
  ```
43
- usage: pyallel [-h] [-t] [-n] [-s] [-V] [-v] [commands ...]
43
+ usage: pyallel [-h] [-t] [-n] [-V] [-v] [commands ...]
44
44
 
45
45
  Run and handle the output of multiple executables in pyallel (as in parallel)
46
46
 
@@ -56,7 +56,6 @@ options:
56
56
  -t, --no-timer don't time how long each command is taking
57
57
  -n, --non-interactive
58
58
  run in non-interactive mode
59
- -s, --no-stream don't stream output of each command
60
59
  -V, --verbose run in verbose mode
61
60
  -v, --version print version and exit
62
61
  ```
@@ -19,7 +19,7 @@ pip install pyallel
19
19
  Once installed, you can run `pyallel` to see usage information, like so:
20
20
 
21
21
  ```
22
- usage: pyallel [-h] [-t] [-n] [-s] [-V] [-v] [commands ...]
22
+ usage: pyallel [-h] [-t] [-n] [-V] [-v] [commands ...]
23
23
 
24
24
  Run and handle the output of multiple executables in pyallel (as in parallel)
25
25
 
@@ -35,7 +35,6 @@ options:
35
35
  -t, --no-timer don't time how long each command is taking
36
36
  -n, --non-interactive
37
37
  run in non-interactive mode
38
- -s, --no-stream don't stream output of each command
39
38
  -V, --verbose run in verbose mode
40
39
  -v, --version print version and exit
41
40
  ```
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyallel"
3
- version = "0.12.2"
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"
@@ -15,13 +15,10 @@ def main_loop(
15
15
  interactive: bool = False,
16
16
  timer: bool = False,
17
17
  verbose: bool = False,
18
- stream: bool = False,
19
18
  ) -> int:
20
19
  process_group = ProcessGroup.from_commands(
21
20
  *commands, interactive=interactive, timer=timer, verbose=verbose
22
21
  )
23
- if not stream:
24
- return process_group.run()
25
22
 
26
23
  return process_group.stream()
27
24
 
@@ -46,7 +43,6 @@ def run(*args: str) -> int:
46
43
  interactive=parsed_args.interactive,
47
44
  timer=parsed_args.timer,
48
45
  verbose=parsed_args.verbose,
49
- stream=parsed_args.stream,
50
46
  )
51
47
  except InvalidExecutableErrors as e:
52
48
  exit_code = 1
@@ -55,14 +51,12 @@ def run(*args: str) -> int:
55
51
  exit_code = 1
56
52
  message = traceback.format_exc()
57
53
 
58
- if exit_code == 2:
59
- print(f"{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}")
60
- elif exit_code == 1:
54
+ if exit_code == 1:
61
55
  if not message:
62
56
  print(f"{constants.RED_BOLD}A command failed!{constants.RESET_COLOUR}")
63
57
  else:
64
58
  print(f"{constants.RED_BOLD}Error: {message}{constants.RESET_COLOUR}")
65
- else:
59
+ elif exit_code == 0:
66
60
  print(f"{constants.GREEN_BOLD}Success!{constants.RESET_COLOUR}")
67
61
 
68
62
  return exit_code
@@ -9,7 +9,6 @@ class Arguments:
9
9
  timer: bool
10
10
  verbose: bool
11
11
  version: bool
12
- stream: bool
13
12
 
14
13
  def __repr__(self) -> str:
15
14
  msg = "Arguments:\n"
@@ -54,14 +53,6 @@ def create_parser() -> ArgumentParser:
54
53
  dest="interactive",
55
54
  default=True,
56
55
  )
57
- parser.add_argument(
58
- "-s",
59
- "--no-stream",
60
- help="don't stream output of each command",
61
- action="store_false",
62
- default=True,
63
- dest="stream",
64
- )
65
56
  parser.add_argument(
66
57
  "-V",
67
58
  "--verbose",
@@ -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
@@ -74,26 +74,6 @@ def get_command_status(
74
74
  return output
75
75
 
76
76
 
77
- def print_command_output(process: Process) -> None:
78
- output = process.read()
79
- if output:
80
- print(prefix(output.decode(), keepend=False))
81
- print()
82
-
83
-
84
- def run_process(process: Process, timer: bool = False, verbose: bool = False) -> bool:
85
- print(f"{constants.CLEAR_LINE}{constants.CARRIAGE_RETURN}", end="")
86
-
87
- if process.return_code() != 0:
88
- print(get_command_status(process, passed=False, verbose=verbose, timer=timer))
89
- print_command_output(process)
90
- return False
91
- else:
92
- print(get_command_status(process, passed=True, verbose=verbose, timer=timer))
93
- print_command_output(process)
94
- return True
95
-
96
-
97
77
  @dataclass
98
78
  class ProcessGroup:
99
79
  processes: list[Process]
@@ -103,43 +83,11 @@ class ProcessGroup:
103
83
  output: dict[UUID, list[str]] = field(default_factory=lambda: defaultdict(list))
104
84
  process_lines: list[int] = field(default_factory=list)
105
85
  completed_processes: set[UUID] = field(default_factory=set)
86
+ exit_code: int = 0
87
+ interrupt_count: int = 0
106
88
  passed: bool = True
107
89
  icon: int = 0
108
90
 
109
- def run(self) -> int:
110
- for process in self.processes:
111
- process.run()
112
-
113
- if not self.interactive or not constants.IN_TTY:
114
- print(
115
- f"{constants.WHITE_BOLD}Running commands...{constants.RESET_COLOUR}\n"
116
- )
117
-
118
- while True:
119
- if self.interactive and constants.IN_TTY:
120
- for icon in constants.ICONS:
121
- print(
122
- f"{constants.CLEAR_LINE}{constants.CARRIAGE_RETURN}{constants.WHITE_BOLD}Running commands{constants.RESET_COLOUR} {icon}",
123
- end="",
124
- )
125
- time.sleep(0.1)
126
-
127
- for process in self.processes:
128
- if process.id in self.completed_processes or process.poll() is None:
129
- continue
130
-
131
- self.completed_processes.add(process.id)
132
- process_passed = run_process(
133
- process, verbose=self.verbose, timer=self.timer
134
- )
135
- if not process_passed:
136
- self.passed = False
137
-
138
- if len(self.completed_processes) == len(self.processes):
139
- break
140
-
141
- return 1 if not self.passed else 0
142
-
143
91
  def stream(self) -> int:
144
92
  for process in self.processes:
145
93
  process.run()
@@ -147,36 +95,31 @@ class ProcessGroup:
147
95
  if not self.interactive:
148
96
  return self.stream_non_interactive()
149
97
 
150
- interrupted = False
151
-
152
98
  while True:
153
- try:
154
- output = self.complete_output()
155
- self.icon += 1
156
- if self.icon == len(constants.ICONS):
157
- self.icon = 0
99
+ output = self.complete_output()
100
+ self.icon += 1
101
+ if self.icon == len(constants.ICONS):
102
+ self.icon = 0
158
103
 
159
- # Clear the screen and print the output
160
- 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="")
161
106
 
162
- # Clear the screen again
163
- print("\033[H\033[0J", end="")
107
+ # Clear the screen again
108
+ print("\033[H\033[0J", end="")
164
109
 
165
- if len(self.completed_processes) == len(self.processes):
166
- break
110
+ if len(self.completed_processes) == len(self.processes):
111
+ break
167
112
 
168
- time.sleep(0.1)
169
- except KeyboardInterrupt:
170
- interrupted = True
171
- for process in self.processes:
172
- process.interrupt()
173
- process.wait()
113
+ time.sleep(0.1)
174
114
 
175
115
  output = self.complete_output(all=True)
176
116
  # Clear the screen one final time before printing the output
177
117
  print(f"\033[3J{output}")
178
118
 
179
- 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
180
123
 
181
124
  def stream_non_interactive(self) -> int:
182
125
  running_process = None
@@ -185,67 +128,88 @@ class ProcessGroup:
185
128
  print(f"{constants.WHITE_BOLD}Running commands...{constants.RESET_COLOUR}\n")
186
129
 
187
130
  while True:
188
- try:
189
- output = ""
190
- for process in self.processes:
191
- if (
192
- running_process is None
193
- and process.id not in self.completed_processes
194
- ):
195
- output += get_command_status(process, verbose=self.verbose)
196
- output += "\n"
197
- running_process = process
198
- elif running_process is not process:
199
- # Need to do this to properly keep track of how long all the other
200
- # commands are taking
201
- process.poll()
202
- continue
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
203
145
 
204
- process_output = process.readline().decode()
146
+ process_output = process.readline().decode()
205
147
 
206
- if not self.output[process.id] and process_output:
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:
207
156
  process_output = prefix(process_output)
208
157
  self.output[process.id].append(process_output)
209
- output += process_output
210
- elif process_output:
211
- if self.output[process.id][-1][-1] != "\n":
212
- self.output[process.id][-1] += process_output
213
- else:
214
- process_output = prefix(process_output)
215
- self.output[process.id].append(process_output)
216
- output += process_output
217
-
218
- if process.poll() is not None:
219
- if process.return_code() != 0:
220
- self.passed = False
221
- process_output = process.read().decode()
222
- if process_output:
223
- output += prefix(process_output)
224
-
225
- output += get_command_status(
226
- process,
227
- passed=process.return_code() == 0,
228
- verbose=self.verbose,
229
- timer=self.timer,
230
- )
231
- output += "\n\n"
232
- self.completed_processes.add(process.id)
233
- running_process = None
234
-
235
- if output:
236
- print(output, end="")
237
-
238
- if len(self.completed_processes) == len(self.processes):
239
- break
240
-
241
- time.sleep(0.01)
242
- except KeyboardInterrupt:
243
- interrupted = True
244
- for process in self.processes:
245
- process.interrupt()
246
- process.wait()
247
-
248
- return 2 if interrupted else (1 if not self.passed else 0)
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"
183
+ ):
184
+ output += "\n"
185
+ output += f"\n{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}\n\n"
186
+ interrupted = True
187
+
188
+ if output:
189
+ print(output, end="")
190
+
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
249
213
 
250
214
  @classmethod
251
215
  def from_commands(
@@ -267,13 +231,18 @@ class ProcessGroup:
267
231
  if errors:
268
232
  raise InvalidExecutableErrors(*errors)
269
233
 
270
- return cls(
234
+ process_group = cls(
271
235
  processes=processes,
272
236
  interactive=interactive,
273
237
  timer=timer,
274
238
  verbose=verbose,
275
239
  )
276
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
+
277
246
  def complete_output(self, tail: int = 20, all: bool = False) -> str:
278
247
  num_processes = len(self.processes)
279
248
  lines = constants.LINES() - (2 * num_processes)
@@ -330,6 +299,14 @@ class ProcessGroup:
330
299
  if i != num_processes:
331
300
  output += "\n"
332
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
+
333
310
  return output
334
311
 
335
312
 
@@ -395,6 +372,10 @@ class Process:
395
372
  if self._process:
396
373
  self._process.send_signal(signal.SIGINT)
397
374
 
375
+ def kill(self) -> None:
376
+ if self._process:
377
+ self._process.send_signal(signal.SIGKILL)
378
+
398
379
  def wait(self) -> int:
399
380
  if self._process:
400
381
  return self._process.wait()
File without changes
File without changes
File without changes