pyallel 1.3.4__tar.gz → 1.3.6__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.
- {pyallel-1.3.4 → pyallel-1.3.6}/PKG-INFO +1 -1
- {pyallel-1.3.4 → pyallel-1.3.6}/pyproject.toml +1 -1
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/colours.py +0 -5
- pyallel-1.3.6/src/pyallel/main.py +87 -0
- pyallel-1.3.6/src/pyallel/printer.py +437 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/process_group_manager.py +14 -4
- pyallel-1.3.4/src/pyallel/main.py +0 -137
- pyallel-1.3.4/src/pyallel/printer.py +0 -392
- {pyallel-1.3.4 → pyallel-1.3.6}/LICENSE +0 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/README.md +0 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/__init__.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/constants.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/errors.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/parser.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/process.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/process_group.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.6}/src/pyallel/py.typed +0 -0
|
@@ -13,11 +13,6 @@ class Colours:
|
|
|
13
13
|
blue_bold: str = "\033[1;34m"
|
|
14
14
|
red_bold: str = "\033[1;31m"
|
|
15
15
|
yellow_bold: str = "\033[1;33m"
|
|
16
|
-
clear_line: str = "\033[2K"
|
|
17
|
-
clear_screen: str = "\033[2J"
|
|
18
|
-
save_cursor: str = "\033[s"
|
|
19
|
-
restore_cursor: str = "\033[u"
|
|
20
|
-
up_line: str = "\033[1F"
|
|
21
16
|
reset_colour: str = "\033[0m"
|
|
22
17
|
dim_on: str = "\033[2m"
|
|
23
18
|
dim_off: str = "\033[22m"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
|
|
8
|
+
from pyallel import constants
|
|
9
|
+
from pyallel.colours import Colours
|
|
10
|
+
from pyallel.errors import InvalidLinesModifierError
|
|
11
|
+
from pyallel.parser import Arguments, create_parser
|
|
12
|
+
from pyallel.printer import (
|
|
13
|
+
InteractiveConsolePrinter,
|
|
14
|
+
NonInteractiveConsolePrinter,
|
|
15
|
+
Printer,
|
|
16
|
+
)
|
|
17
|
+
from pyallel.process_group_manager import ProcessGroupManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def entry_point(*args: str) -> int:
|
|
21
|
+
args = args or tuple(sys.argv[1:])
|
|
22
|
+
parser = create_parser()
|
|
23
|
+
parsed_args = parser.parse_args(args=args, namespace=Arguments())
|
|
24
|
+
|
|
25
|
+
if parsed_args.version:
|
|
26
|
+
my_version = importlib.metadata.version("pyallel")
|
|
27
|
+
print(my_version)
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
if not parsed_args.commands:
|
|
31
|
+
parser.print_help()
|
|
32
|
+
return 2
|
|
33
|
+
|
|
34
|
+
colours = Colours.from_colour(parsed_args.colour)
|
|
35
|
+
printer: Printer
|
|
36
|
+
if not parsed_args.interactive or not constants.IN_TTY:
|
|
37
|
+
printer = NonInteractiveConsolePrinter(colours, timer=parsed_args.timer)
|
|
38
|
+
else:
|
|
39
|
+
printer = InteractiveConsolePrinter(colours, timer=parsed_args.timer)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
process_group_manager = ProcessGroupManager.from_args(*parsed_args.commands)
|
|
43
|
+
except InvalidLinesModifierError as e:
|
|
44
|
+
print(f"{colours.red_bold}Error: {str(e)}{colours.reset_colour}")
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
exit_code = run(process_group_manager, printer)
|
|
49
|
+
except Exception:
|
|
50
|
+
print(
|
|
51
|
+
f"{colours.red_bold}Error: {traceback.format_exc()}{colours.reset_colour}"
|
|
52
|
+
)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
if exit_code == 1:
|
|
56
|
+
print(f"{colours.red_bold}\nFailed!{colours.reset_colour}")
|
|
57
|
+
elif exit_code == 0:
|
|
58
|
+
print(f"{colours.green_bold}\nDone!{colours.reset_colour}")
|
|
59
|
+
|
|
60
|
+
return exit_code
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run(process_group_manager: ProcessGroupManager, printer: Printer) -> int:
|
|
64
|
+
process_group_manager.run()
|
|
65
|
+
while True:
|
|
66
|
+
process_group_manager.stream()
|
|
67
|
+
printer.print(process_group_manager)
|
|
68
|
+
|
|
69
|
+
poll = process_group_manager.poll()
|
|
70
|
+
if poll is not None:
|
|
71
|
+
# If we still have new output to print after the process group manager has completed,
|
|
72
|
+
# make sure to print it here before continuing
|
|
73
|
+
if process_group_manager.stream().has_output():
|
|
74
|
+
printer.print(process_group_manager)
|
|
75
|
+
|
|
76
|
+
if poll > 0:
|
|
77
|
+
return poll
|
|
78
|
+
|
|
79
|
+
process_group_manager.run()
|
|
80
|
+
if not process_group_manager.next():
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
time.sleep(0.1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
sys.exit(entry_point())
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from pyallel import constants
|
|
7
|
+
from pyallel.colours import Colours
|
|
8
|
+
from pyallel.process import Process, ProcessOutput
|
|
9
|
+
from pyallel.process_group import ProcessGroupOutput
|
|
10
|
+
from pyallel.process_group_manager import ProcessGroupManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Printer(Protocol):
|
|
14
|
+
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
15
|
+
"""Print output obtained from the provided process group manager.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
process_group_manager: manager to obtain output from
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConsolePrinter:
|
|
23
|
+
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
24
|
+
self._colours = colours or Colours()
|
|
25
|
+
self._timer = timer
|
|
26
|
+
self._prefix = f"{self._colours.dim_on}=>{self._colours.dim_off} "
|
|
27
|
+
self._icon = 0
|
|
28
|
+
self._to_print: list[tuple[bool, str, str]] = []
|
|
29
|
+
|
|
30
|
+
def write(
|
|
31
|
+
self,
|
|
32
|
+
line: str,
|
|
33
|
+
include_prefix: bool = False,
|
|
34
|
+
end: str = "\n",
|
|
35
|
+
flush: bool = False,
|
|
36
|
+
truncate: bool = False,
|
|
37
|
+
columns: int | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
truncate_num = 0
|
|
40
|
+
prefix = self._prefix if include_prefix else ""
|
|
41
|
+
columns = columns or constants.COLUMNS()
|
|
42
|
+
if prefix:
|
|
43
|
+
truncate_num = 6
|
|
44
|
+
if truncate:
|
|
45
|
+
columns = columns - truncate_num
|
|
46
|
+
if self.get_num_lines(line, columns) > 1:
|
|
47
|
+
line = self.truncate_line(line, columns)
|
|
48
|
+
print(f"{prefix}{line}", end=end, flush=flush)
|
|
49
|
+
|
|
50
|
+
def generate_process_output(
|
|
51
|
+
self,
|
|
52
|
+
output: ProcessOutput,
|
|
53
|
+
tail_output: bool = False,
|
|
54
|
+
include_cmd: bool = True,
|
|
55
|
+
include_output: bool = True,
|
|
56
|
+
include_progress: bool = True,
|
|
57
|
+
include_timer: bool | None = None,
|
|
58
|
+
append_newlines: bool = False,
|
|
59
|
+
) -> list[tuple[bool, str, str]]:
|
|
60
|
+
out: list[tuple[bool, str, str]] = []
|
|
61
|
+
line_parts: tuple[bool, str, str]
|
|
62
|
+
|
|
63
|
+
if tail_output and output.process.lines == 0:
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
if include_cmd:
|
|
67
|
+
status = self.generate_process_output_status(
|
|
68
|
+
output, include_progress, include_timer
|
|
69
|
+
)
|
|
70
|
+
line_parts = (False, status, "\n")
|
|
71
|
+
out.append(line_parts)
|
|
72
|
+
self._to_print.append(line_parts)
|
|
73
|
+
|
|
74
|
+
if include_output:
|
|
75
|
+
lines = output.data.splitlines(keepends=True)
|
|
76
|
+
|
|
77
|
+
if tail_output:
|
|
78
|
+
output_lines = output.process.lines - 1
|
|
79
|
+
if output_lines == 0:
|
|
80
|
+
lines = []
|
|
81
|
+
else:
|
|
82
|
+
lines = lines[-output_lines:]
|
|
83
|
+
|
|
84
|
+
for line in lines:
|
|
85
|
+
prefix = True
|
|
86
|
+
end = line[-1]
|
|
87
|
+
if append_newlines and end != "\n":
|
|
88
|
+
end = "\n"
|
|
89
|
+
else:
|
|
90
|
+
line = line[:-1]
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
prev_line = self._to_print[-1]
|
|
94
|
+
except IndexError:
|
|
95
|
+
pass
|
|
96
|
+
else:
|
|
97
|
+
if prev_line[2] != "\n":
|
|
98
|
+
prefix = False
|
|
99
|
+
|
|
100
|
+
line_parts = (prefix, line, end)
|
|
101
|
+
out.append(line_parts)
|
|
102
|
+
self._to_print.append(line_parts)
|
|
103
|
+
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
def generate_process_output_status(
|
|
107
|
+
self,
|
|
108
|
+
output: ProcessOutput,
|
|
109
|
+
include_progress: bool = True,
|
|
110
|
+
include_timer: bool | None = None,
|
|
111
|
+
) -> str:
|
|
112
|
+
include_timer = include_timer if include_timer is not None else self._timer
|
|
113
|
+
|
|
114
|
+
passed = None
|
|
115
|
+
icon = ""
|
|
116
|
+
poll = output.process.poll()
|
|
117
|
+
if include_progress:
|
|
118
|
+
icon = constants.ICONS[self._icon]
|
|
119
|
+
if poll is not None:
|
|
120
|
+
passed = poll == 0
|
|
121
|
+
|
|
122
|
+
if passed is True:
|
|
123
|
+
colour = self._colours.green_bold
|
|
124
|
+
msg = "done"
|
|
125
|
+
icon = constants.TICK
|
|
126
|
+
elif passed is False:
|
|
127
|
+
colour = self._colours.red_bold
|
|
128
|
+
msg = "failed"
|
|
129
|
+
icon = constants.X
|
|
130
|
+
else:
|
|
131
|
+
colour = self._colours.white_bold
|
|
132
|
+
msg = "running"
|
|
133
|
+
|
|
134
|
+
if not icon:
|
|
135
|
+
msg += "..."
|
|
136
|
+
|
|
137
|
+
timer = ""
|
|
138
|
+
if include_timer:
|
|
139
|
+
end = output.process.end
|
|
140
|
+
if not output.process.end:
|
|
141
|
+
end = time.perf_counter()
|
|
142
|
+
elapsed = end - output.process.start
|
|
143
|
+
timer = f"({self.format_time_taken(elapsed)})"
|
|
144
|
+
|
|
145
|
+
command = output.process.command
|
|
146
|
+
if self.get_num_lines(output.process.command) > 1:
|
|
147
|
+
columns = constants.COLUMNS() - (len(msg) + len(timer) + 9)
|
|
148
|
+
command = self.truncate_line(command, columns)
|
|
149
|
+
|
|
150
|
+
out = f"{self._colours.white_bold}[{self._colours.reset_colour}{self._colours.blue_bold}{command}{self._colours.reset_colour}{self._colours.white_bold}]{self._colours.reset_colour}{colour} {msg} {icon}{self._colours.reset_colour}"
|
|
151
|
+
|
|
152
|
+
if timer:
|
|
153
|
+
out += f" {self._colours.dim_on}{timer}{self._colours.dim_off}"
|
|
154
|
+
|
|
155
|
+
return out
|
|
156
|
+
|
|
157
|
+
def generate_process_group_output(
|
|
158
|
+
self,
|
|
159
|
+
output: ProcessGroupOutput,
|
|
160
|
+
interrupt_count: int = 0,
|
|
161
|
+
tail_output: bool = True,
|
|
162
|
+
) -> list[tuple[bool, str, str]]:
|
|
163
|
+
self.set_process_lines(output, interrupt_count)
|
|
164
|
+
|
|
165
|
+
for out in output.processes:
|
|
166
|
+
self.generate_process_output(out, tail_output, append_newlines=True)
|
|
167
|
+
|
|
168
|
+
if interrupt_count == 1:
|
|
169
|
+
self._to_print.append((False, "", "\n"))
|
|
170
|
+
self._to_print.append(
|
|
171
|
+
(
|
|
172
|
+
False,
|
|
173
|
+
f"{self._colours.yellow_bold}Interrupt!{self._colours.reset_colour}",
|
|
174
|
+
"\n",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
elif interrupt_count == 2:
|
|
178
|
+
self._to_print.append((False, "", "\n"))
|
|
179
|
+
self._to_print.append(
|
|
180
|
+
(
|
|
181
|
+
False,
|
|
182
|
+
f"{self._colours.red_bold}Abort!{self._colours.reset_colour}",
|
|
183
|
+
"\n",
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
self._icon += 1
|
|
188
|
+
if self._icon == len(constants.ICONS):
|
|
189
|
+
self._icon = 0
|
|
190
|
+
|
|
191
|
+
return self._to_print
|
|
192
|
+
|
|
193
|
+
def set_process_lines(
|
|
194
|
+
self,
|
|
195
|
+
output: ProcessGroupOutput,
|
|
196
|
+
interrupt_count: int = 0,
|
|
197
|
+
lines: int = 0,
|
|
198
|
+
) -> None:
|
|
199
|
+
lines = lines or constants.LINES() - 1
|
|
200
|
+
if interrupt_count:
|
|
201
|
+
lines -= 2
|
|
202
|
+
|
|
203
|
+
# Allocate lines to processes that have a fixed percentage of lines set
|
|
204
|
+
allocated_process_lines = lines // len(output.processes)
|
|
205
|
+
processes_with_dynamic_lines: list[ProcessOutput] = []
|
|
206
|
+
used_lines = 0
|
|
207
|
+
for process_output in output.processes:
|
|
208
|
+
# This process output doesn't have percentage_lines set, so skip it
|
|
209
|
+
if not process_output.process.percentage_lines:
|
|
210
|
+
processes_with_dynamic_lines.append(process_output)
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
process_output.process.lines = int(
|
|
214
|
+
lines * process_output.process.percentage_lines
|
|
215
|
+
)
|
|
216
|
+
used_lines += process_output.process.lines
|
|
217
|
+
|
|
218
|
+
# Remove the used lines from the total available lines
|
|
219
|
+
lines -= used_lines
|
|
220
|
+
|
|
221
|
+
while lines:
|
|
222
|
+
# Calculate how many lines each process should have based on how many processes and lines are left
|
|
223
|
+
num_processes = len(processes_with_dynamic_lines) or 1
|
|
224
|
+
allocated_process_lines = lines // num_processes
|
|
225
|
+
processes_with_excess_output: list[ProcessOutput] = []
|
|
226
|
+
recalculate_lines = False
|
|
227
|
+
for process_output in processes_with_dynamic_lines:
|
|
228
|
+
# If the number of lines in this process output is less than how many terminal lines we would allocate it,
|
|
229
|
+
# Set it's allocated terminal lines to the exact number of lines in its output and remove this number from
|
|
230
|
+
# the total available terminal lines
|
|
231
|
+
if process_output.lines < allocated_process_lines:
|
|
232
|
+
process_output.process.lines = process_output.lines
|
|
233
|
+
lines -= process_output.process.lines
|
|
234
|
+
recalculate_lines = True
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
processes_with_excess_output.append(process_output)
|
|
238
|
+
|
|
239
|
+
# We need to re-calcuate how many terminal lines we can allocate to each process if the output of at least one process
|
|
240
|
+
# contains less lines than what we would normally allocate it. This is done so we can allocate these extra lines to the
|
|
241
|
+
# other processes that contain more lines of output.
|
|
242
|
+
if recalculate_lines:
|
|
243
|
+
processes_with_dynamic_lines = processes_with_excess_output
|
|
244
|
+
else:
|
|
245
|
+
# All remaining processes exceed the number of terminal lines we will allocate them, so allocate them
|
|
246
|
+
# their terminal lines as normal and break out of the while loop
|
|
247
|
+
for process_output in processes_with_excess_output:
|
|
248
|
+
process_output.process.lines = allocated_process_lines
|
|
249
|
+
lines -= allocated_process_lines
|
|
250
|
+
|
|
251
|
+
# If there is any lines left, allocate them to the process that currently contains the most lines in its output, or
|
|
252
|
+
# allocate them to the first process if no process contains enough lines
|
|
253
|
+
if lines:
|
|
254
|
+
process_with_most_lines: ProcessOutput | None = None
|
|
255
|
+
most_lines = 0
|
|
256
|
+
for process_output in output.processes:
|
|
257
|
+
if process_output.process.lines > most_lines:
|
|
258
|
+
process_with_most_lines = process_output
|
|
259
|
+
most_lines = process_output.process.lines
|
|
260
|
+
|
|
261
|
+
if not process_with_most_lines:
|
|
262
|
+
output.processes[0].process.lines += lines
|
|
263
|
+
else:
|
|
264
|
+
process_with_most_lines.process.lines += lines
|
|
265
|
+
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
def get_num_lines(self, line: str, columns: int | None = None) -> int:
|
|
269
|
+
lines = 0
|
|
270
|
+
columns = columns or constants.COLUMNS()
|
|
271
|
+
line = constants.ANSI_ESCAPE.sub("", line)
|
|
272
|
+
length = len(line)
|
|
273
|
+
line_lines = 1
|
|
274
|
+
if length > columns:
|
|
275
|
+
line_lines = length // columns
|
|
276
|
+
remainder = length % columns
|
|
277
|
+
if remainder:
|
|
278
|
+
line_lines += 1
|
|
279
|
+
lines += 1 * line_lines
|
|
280
|
+
return lines
|
|
281
|
+
|
|
282
|
+
def truncate_line(self, line: str, columns: int | None = None) -> str:
|
|
283
|
+
columns = columns or constants.COLUMNS()
|
|
284
|
+
escaped_line = constants.ANSI_ESCAPE.sub("", line)
|
|
285
|
+
return "".join(escaped_line[:columns]) + "..."
|
|
286
|
+
|
|
287
|
+
def format_time_taken(self, time_taken: float) -> str:
|
|
288
|
+
time_taken = round(time_taken, 1)
|
|
289
|
+
seconds = time_taken % (24 * 3600)
|
|
290
|
+
|
|
291
|
+
return f"{seconds}s"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class InteractiveConsolePrinter(ConsolePrinter):
|
|
295
|
+
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
296
|
+
super().__init__(colours, timer)
|
|
297
|
+
self._last_printed: list[tuple[bool, str, str]] = []
|
|
298
|
+
|
|
299
|
+
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
300
|
+
output = process_group_manager.get_cur_process_group_output()
|
|
301
|
+
self.print_process_group_output(output, process_group_manager._interrupt_count)
|
|
302
|
+
|
|
303
|
+
poll = process_group_manager.poll()
|
|
304
|
+
if poll is not None:
|
|
305
|
+
self.clear_last_printed_lines()
|
|
306
|
+
self.reset()
|
|
307
|
+
self.print_process_group_output(
|
|
308
|
+
output, process_group_manager._interrupt_count, tail_output=False
|
|
309
|
+
)
|
|
310
|
+
self.reset()
|
|
311
|
+
|
|
312
|
+
def print_process_group_output(
|
|
313
|
+
self,
|
|
314
|
+
output: ProcessGroupOutput,
|
|
315
|
+
interrupt_count: int = 0,
|
|
316
|
+
tail_output: bool = True,
|
|
317
|
+
) -> None:
|
|
318
|
+
columns = constants.COLUMNS()
|
|
319
|
+
self.generate_process_group_output(output, interrupt_count, tail_output)
|
|
320
|
+
|
|
321
|
+
num_lines_to_print = len(self._to_print)
|
|
322
|
+
num_last_printed_lines = len(self._last_printed)
|
|
323
|
+
|
|
324
|
+
# If we don't have any last printed lines or we don't want to tail the output,
|
|
325
|
+
# we just print all the new lines
|
|
326
|
+
if not num_last_printed_lines or not tail_output:
|
|
327
|
+
for include_prefix, line, end in self._to_print:
|
|
328
|
+
self.write(
|
|
329
|
+
line, include_prefix, end, truncate=tail_output, columns=columns
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
# Compare the number of last lines and new lines and only update what has changed.
|
|
333
|
+
#
|
|
334
|
+
# Move the cursor up the amount the lines that were last printed so we can start
|
|
335
|
+
# comparing the last printed lines with the new lines that were generated
|
|
336
|
+
print(f"\033[{num_last_printed_lines}A", end="")
|
|
337
|
+
cursor_line = 0
|
|
338
|
+
for cur_line, line_parts in enumerate(self._last_printed):
|
|
339
|
+
# If the current line is not the same as it's newly generated version, we update the line
|
|
340
|
+
if line_parts[1] != self._to_print[cur_line][1]:
|
|
341
|
+
include_prefix, line, end = self._to_print[cur_line]
|
|
342
|
+
# Jump to the line that needs to be changed
|
|
343
|
+
lines_to_jump = cur_line - cursor_line
|
|
344
|
+
if lines_to_jump:
|
|
345
|
+
print(f"\033[{lines_to_jump}B\r", end="")
|
|
346
|
+
# Clear the current line
|
|
347
|
+
print(f"{constants.CLEAR_LINE}\r", end="")
|
|
348
|
+
# Write the new line, this will move the cursor to the next line automatically
|
|
349
|
+
self.write(
|
|
350
|
+
line, include_prefix, end, truncate=tail_output, columns=columns
|
|
351
|
+
)
|
|
352
|
+
# Need to set the cursor_line to be the current line + 1 as the above write
|
|
353
|
+
# will move the cursor to the next line
|
|
354
|
+
cursor_line = cur_line + 1
|
|
355
|
+
|
|
356
|
+
if num_lines_to_print > num_last_printed_lines:
|
|
357
|
+
# Jump to the start of the new lines that needs to be printed
|
|
358
|
+
lines_to_jump = num_last_printed_lines - cursor_line
|
|
359
|
+
if lines_to_jump:
|
|
360
|
+
print(f"\033[{lines_to_jump}B\r", end="")
|
|
361
|
+
|
|
362
|
+
# Just print the new lines as normal
|
|
363
|
+
for line_parts in self._to_print[num_last_printed_lines:]:
|
|
364
|
+
include_prefix, line, end = line_parts
|
|
365
|
+
self.write(
|
|
366
|
+
line, include_prefix, end, truncate=tail_output, columns=columns
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
# Jump to the end of the output
|
|
370
|
+
lines_to_jump = num_lines_to_print - cursor_line
|
|
371
|
+
if lines_to_jump:
|
|
372
|
+
print(f"\033[{lines_to_jump}B\r", end="")
|
|
373
|
+
|
|
374
|
+
# Force a flush to return the cursor to the bottom immediately
|
|
375
|
+
print("", end="", flush=True)
|
|
376
|
+
|
|
377
|
+
self._last_printed = self._to_print.copy()
|
|
378
|
+
self._to_print.clear()
|
|
379
|
+
|
|
380
|
+
def clear_last_printed_lines(self) -> None:
|
|
381
|
+
# Clear all the lines that were just printed
|
|
382
|
+
print(
|
|
383
|
+
f"{constants.CLEAR_LINE}{constants.UP_LINE}{constants.CLEAR_LINE}"
|
|
384
|
+
* len(self._last_printed),
|
|
385
|
+
end="",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def reset(self) -> None:
|
|
389
|
+
self._last_printed.clear()
|
|
390
|
+
self._to_print.clear()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class NonInteractiveConsolePrinter(ConsolePrinter):
|
|
394
|
+
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
395
|
+
super().__init__(colours, timer)
|
|
396
|
+
self._current_process: Process | None = None
|
|
397
|
+
|
|
398
|
+
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
399
|
+
outputs = process_group_manager.cur_output
|
|
400
|
+
for pg in outputs.process_group_outputs.values():
|
|
401
|
+
for output in pg.processes:
|
|
402
|
+
if self._current_process is None:
|
|
403
|
+
self._current_process = output.process
|
|
404
|
+
output = process_group_manager.get_process(output.id)
|
|
405
|
+
self.print_process_output(
|
|
406
|
+
output, include_progress=False, include_timer=False
|
|
407
|
+
)
|
|
408
|
+
elif self._current_process is not output.process:
|
|
409
|
+
continue
|
|
410
|
+
else:
|
|
411
|
+
self.print_process_output(output, include_cmd=False)
|
|
412
|
+
|
|
413
|
+
if output.process.poll() is not None:
|
|
414
|
+
self.print_process_output(output, include_output=False)
|
|
415
|
+
self._current_process = None
|
|
416
|
+
|
|
417
|
+
def print_process_output(
|
|
418
|
+
self,
|
|
419
|
+
output: ProcessOutput,
|
|
420
|
+
tail_output: bool = False,
|
|
421
|
+
include_cmd: bool = True,
|
|
422
|
+
include_output: bool = True,
|
|
423
|
+
include_progress: bool = True,
|
|
424
|
+
include_timer: bool | None = None,
|
|
425
|
+
) -> None:
|
|
426
|
+
for include_prefix, line, end in self.generate_process_output(
|
|
427
|
+
output,
|
|
428
|
+
tail_output,
|
|
429
|
+
include_cmd,
|
|
430
|
+
include_output,
|
|
431
|
+
include_progress,
|
|
432
|
+
include_timer,
|
|
433
|
+
):
|
|
434
|
+
self.write(line, include_prefix, end)
|
|
435
|
+
|
|
436
|
+
# Force a flush otherwise lines that don't end in a newline character will not get printed as they are read
|
|
437
|
+
print("", end="", flush=True)
|
|
@@ -24,6 +24,14 @@ class ProcessGroupManagerOutput:
|
|
|
24
24
|
else:
|
|
25
25
|
self.process_group_outputs[key] = value
|
|
26
26
|
|
|
27
|
+
def has_output(self) -> bool:
|
|
28
|
+
for pg in self.process_group_outputs.values():
|
|
29
|
+
for process in pg.processes:
|
|
30
|
+
if process.data:
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
return False
|
|
34
|
+
|
|
27
35
|
|
|
28
36
|
class ProcessGroupManager:
|
|
29
37
|
def __init__(self, process_groups: list[ProcessGroup]) -> None:
|
|
@@ -31,7 +39,7 @@ class ProcessGroupManager:
|
|
|
31
39
|
self._interrupt_count = 0
|
|
32
40
|
self._cur_process_group: ProcessGroup | None = None
|
|
33
41
|
self._process_groups = process_groups
|
|
34
|
-
self.
|
|
42
|
+
self._all_output = ProcessGroupManagerOutput(
|
|
35
43
|
process_group_outputs={
|
|
36
44
|
pg.id: ProcessGroupOutput(
|
|
37
45
|
id=pg.id,
|
|
@@ -40,6 +48,7 @@ class ProcessGroupManager:
|
|
|
40
48
|
for pg in self._process_groups
|
|
41
49
|
}
|
|
42
50
|
)
|
|
51
|
+
self.cur_output = ProcessGroupManagerOutput()
|
|
43
52
|
|
|
44
53
|
def run(self) -> None:
|
|
45
54
|
if self._process_groups:
|
|
@@ -62,18 +71,19 @@ class ProcessGroupManager:
|
|
|
62
71
|
},
|
|
63
72
|
)
|
|
64
73
|
|
|
65
|
-
self.
|
|
74
|
+
self._all_output.merge(output)
|
|
75
|
+
self.cur_output = output
|
|
66
76
|
|
|
67
77
|
return output
|
|
68
78
|
|
|
69
79
|
def get_cur_process_group_output(self) -> ProcessGroupOutput:
|
|
70
80
|
if self._cur_process_group:
|
|
71
|
-
return self.
|
|
81
|
+
return self._all_output.process_group_outputs[self._cur_process_group.id]
|
|
72
82
|
|
|
73
83
|
raise KeyError("no current process group output")
|
|
74
84
|
|
|
75
85
|
def get_process(self, id: int) -> ProcessOutput:
|
|
76
|
-
for pg in self.
|
|
86
|
+
for pg in self._all_output.process_group_outputs.values():
|
|
77
87
|
for process in pg.processes:
|
|
78
88
|
if process.id == id:
|
|
79
89
|
return process
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib.metadata
|
|
4
|
-
import sys
|
|
5
|
-
import traceback
|
|
6
|
-
import time
|
|
7
|
-
|
|
8
|
-
from pyallel import constants
|
|
9
|
-
from pyallel.colours import Colours
|
|
10
|
-
from pyallel.errors import InvalidLinesModifierError
|
|
11
|
-
from pyallel.parser import Arguments, create_parser
|
|
12
|
-
from pyallel.printer import Printer
|
|
13
|
-
from pyallel.process_group_manager import ProcessGroupManager
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def run_interactive(
|
|
17
|
-
process_group_manager: ProcessGroupManager, printer: Printer
|
|
18
|
-
) -> int:
|
|
19
|
-
while True:
|
|
20
|
-
process_group_manager.stream()
|
|
21
|
-
|
|
22
|
-
output = process_group_manager.get_cur_process_group_output()
|
|
23
|
-
printer.print_progress_group_output(
|
|
24
|
-
output, process_group_manager._interrupt_count
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
poll = process_group_manager.poll()
|
|
28
|
-
if poll is not None:
|
|
29
|
-
printer.clear_last_printed_lines()
|
|
30
|
-
printer.reset()
|
|
31
|
-
printer.print_progress_group_output(
|
|
32
|
-
output, process_group_manager._interrupt_count, tail_output=False
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
if poll > 0:
|
|
36
|
-
return poll
|
|
37
|
-
|
|
38
|
-
printer.reset()
|
|
39
|
-
process_group_manager.run()
|
|
40
|
-
if not process_group_manager.next():
|
|
41
|
-
return 0
|
|
42
|
-
|
|
43
|
-
time.sleep(0.1)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def run_non_interactive(
|
|
47
|
-
process_group_manager: ProcessGroupManager, printer: Printer
|
|
48
|
-
) -> int:
|
|
49
|
-
current_process = None
|
|
50
|
-
|
|
51
|
-
while True:
|
|
52
|
-
outputs = process_group_manager.stream()
|
|
53
|
-
|
|
54
|
-
for pg in outputs.process_group_outputs.values():
|
|
55
|
-
for output in pg.processes:
|
|
56
|
-
if current_process is None:
|
|
57
|
-
current_process = output.process
|
|
58
|
-
output = process_group_manager.get_process(output.id)
|
|
59
|
-
printer.print_process_output(
|
|
60
|
-
output, include_progress=False, include_timer=False
|
|
61
|
-
)
|
|
62
|
-
elif current_process is not output.process:
|
|
63
|
-
continue
|
|
64
|
-
else:
|
|
65
|
-
printer.print_process_output(output, include_cmd=False)
|
|
66
|
-
|
|
67
|
-
if output.process.poll() is not None:
|
|
68
|
-
printer.print_process_output(output, include_output=False)
|
|
69
|
-
current_process = None
|
|
70
|
-
|
|
71
|
-
poll = process_group_manager.poll()
|
|
72
|
-
if poll is not None:
|
|
73
|
-
if poll > 0:
|
|
74
|
-
return poll
|
|
75
|
-
|
|
76
|
-
process_group_manager.run()
|
|
77
|
-
if not process_group_manager.next():
|
|
78
|
-
return 0
|
|
79
|
-
|
|
80
|
-
time.sleep(0.1)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def run(*args: str) -> int:
|
|
84
|
-
parser = create_parser()
|
|
85
|
-
parsed_args = parser.parse_args(args=args, namespace=Arguments())
|
|
86
|
-
|
|
87
|
-
if parsed_args.version:
|
|
88
|
-
my_version = importlib.metadata.version("pyallel")
|
|
89
|
-
print(my_version)
|
|
90
|
-
return 0
|
|
91
|
-
|
|
92
|
-
if not parsed_args.commands:
|
|
93
|
-
parser.print_help()
|
|
94
|
-
return 2
|
|
95
|
-
|
|
96
|
-
colours = Colours.from_colour(parsed_args.colour)
|
|
97
|
-
printer = Printer(colours, timer=parsed_args.timer)
|
|
98
|
-
|
|
99
|
-
interactive = True
|
|
100
|
-
if not parsed_args.interactive:
|
|
101
|
-
interactive = False
|
|
102
|
-
elif not constants.IN_TTY:
|
|
103
|
-
interactive = False
|
|
104
|
-
|
|
105
|
-
message = None
|
|
106
|
-
try:
|
|
107
|
-
process_group_manager = ProcessGroupManager.from_args(*parsed_args.commands)
|
|
108
|
-
process_group_manager.run()
|
|
109
|
-
|
|
110
|
-
if interactive:
|
|
111
|
-
exit_code = run_interactive(process_group_manager, printer)
|
|
112
|
-
else:
|
|
113
|
-
exit_code = run_non_interactive(process_group_manager, printer)
|
|
114
|
-
except InvalidLinesModifierError as e:
|
|
115
|
-
exit_code = 1
|
|
116
|
-
message = str(e)
|
|
117
|
-
except Exception:
|
|
118
|
-
exit_code = 1
|
|
119
|
-
message = traceback.format_exc()
|
|
120
|
-
|
|
121
|
-
if exit_code == 1:
|
|
122
|
-
if not message:
|
|
123
|
-
printer.error("\nFailed!")
|
|
124
|
-
else:
|
|
125
|
-
printer.error(f"Error: {message}")
|
|
126
|
-
elif exit_code == 0:
|
|
127
|
-
printer.ok("\nDone!")
|
|
128
|
-
|
|
129
|
-
return exit_code
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def entry_point() -> None:
|
|
133
|
-
sys.exit(run(*sys.argv[1:]))
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if __name__ == "__main__":
|
|
137
|
-
entry_point()
|
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
from pyallel import constants
|
|
6
|
-
from pyallel.colours import Colours
|
|
7
|
-
from pyallel.process import ProcessOutput
|
|
8
|
-
from pyallel.process_group import ProcessGroupOutput
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Printer:
|
|
12
|
-
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
13
|
-
self._colours = colours or Colours()
|
|
14
|
-
self._timer = timer
|
|
15
|
-
self._prefix = f"{self._colours.dim_on}=>{self._colours.dim_off} "
|
|
16
|
-
self._icon = 0
|
|
17
|
-
self._last_printed: list[tuple[bool, str, str]] = []
|
|
18
|
-
self._to_print: list[tuple[bool, str, str]] = []
|
|
19
|
-
|
|
20
|
-
def write(
|
|
21
|
-
self,
|
|
22
|
-
line: str,
|
|
23
|
-
include_prefix: bool = False,
|
|
24
|
-
end: str = "\n",
|
|
25
|
-
flush: bool = False,
|
|
26
|
-
truncate: bool = False,
|
|
27
|
-
columns: int | None = None,
|
|
28
|
-
) -> None:
|
|
29
|
-
truncate_num = 0
|
|
30
|
-
prefix = self._prefix if include_prefix else ""
|
|
31
|
-
columns = columns or constants.COLUMNS()
|
|
32
|
-
if prefix:
|
|
33
|
-
truncate_num = 6
|
|
34
|
-
if truncate:
|
|
35
|
-
columns = columns - truncate_num
|
|
36
|
-
if get_num_lines(line, columns) > 1:
|
|
37
|
-
line = truncate_line(line, columns)
|
|
38
|
-
print(f"{prefix}{line}", end=end, flush=flush)
|
|
39
|
-
|
|
40
|
-
def info(self, msg: str) -> None:
|
|
41
|
-
self.write(
|
|
42
|
-
f"{self._colours.white_bold}{msg}{self._colours.reset_colour}",
|
|
43
|
-
include_prefix=False,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
def ok(self, msg: str) -> None:
|
|
47
|
-
self.write(
|
|
48
|
-
f"{self._colours.green_bold}{msg}{self._colours.reset_colour}",
|
|
49
|
-
include_prefix=False,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
def warn(self, msg: str) -> None:
|
|
53
|
-
self.write(
|
|
54
|
-
f"{self._colours.yellow_bold}{msg}{self._colours.reset_colour}",
|
|
55
|
-
include_prefix=False,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
def error(self, msg: str) -> None:
|
|
59
|
-
self.write(
|
|
60
|
-
f"{self._colours.red_bold}{msg}{self._colours.reset_colour}",
|
|
61
|
-
include_prefix=False,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
def generate_process_output(
|
|
65
|
-
self,
|
|
66
|
-
output: ProcessOutput,
|
|
67
|
-
tail_output: bool = False,
|
|
68
|
-
include_cmd: bool = True,
|
|
69
|
-
include_output: bool = True,
|
|
70
|
-
include_progress: bool = True,
|
|
71
|
-
include_timer: bool | None = None,
|
|
72
|
-
append_newlines: bool = False,
|
|
73
|
-
) -> list[tuple[bool, str, str]]:
|
|
74
|
-
out: list[tuple[bool, str, str]] = []
|
|
75
|
-
line_parts: tuple[bool, str, str]
|
|
76
|
-
|
|
77
|
-
if tail_output and output.process.lines == 0:
|
|
78
|
-
return out
|
|
79
|
-
|
|
80
|
-
if include_cmd:
|
|
81
|
-
status = self.generate_process_output_status(
|
|
82
|
-
output, include_progress, include_timer
|
|
83
|
-
)
|
|
84
|
-
line_parts = (False, status, "\n")
|
|
85
|
-
out.append(line_parts)
|
|
86
|
-
self._to_print.append(line_parts)
|
|
87
|
-
|
|
88
|
-
if include_output:
|
|
89
|
-
lines = output.data.splitlines(keepends=True)
|
|
90
|
-
|
|
91
|
-
if tail_output:
|
|
92
|
-
output_lines = output.process.lines - 1
|
|
93
|
-
if output_lines == 0:
|
|
94
|
-
lines = []
|
|
95
|
-
else:
|
|
96
|
-
lines = lines[-output_lines:]
|
|
97
|
-
|
|
98
|
-
for line in lines:
|
|
99
|
-
prefix = True
|
|
100
|
-
end = line[-1]
|
|
101
|
-
if append_newlines and end != "\n":
|
|
102
|
-
end = "\n"
|
|
103
|
-
else:
|
|
104
|
-
line = line[:-1]
|
|
105
|
-
|
|
106
|
-
try:
|
|
107
|
-
prev_line = self._to_print[-1]
|
|
108
|
-
except IndexError:
|
|
109
|
-
pass
|
|
110
|
-
else:
|
|
111
|
-
if prev_line[2] != "\n":
|
|
112
|
-
prefix = False
|
|
113
|
-
|
|
114
|
-
line_parts = (prefix, line, end)
|
|
115
|
-
out.append(line_parts)
|
|
116
|
-
self._to_print.append(line_parts)
|
|
117
|
-
|
|
118
|
-
return out
|
|
119
|
-
|
|
120
|
-
def generate_process_output_status(
|
|
121
|
-
self,
|
|
122
|
-
output: ProcessOutput,
|
|
123
|
-
include_progress: bool = True,
|
|
124
|
-
include_timer: bool | None = None,
|
|
125
|
-
) -> str:
|
|
126
|
-
include_timer = include_timer if include_timer is not None else self._timer
|
|
127
|
-
|
|
128
|
-
passed = None
|
|
129
|
-
icon = ""
|
|
130
|
-
poll = output.process.poll()
|
|
131
|
-
if include_progress:
|
|
132
|
-
icon = constants.ICONS[self._icon]
|
|
133
|
-
if poll is not None:
|
|
134
|
-
passed = poll == 0
|
|
135
|
-
|
|
136
|
-
if passed is True:
|
|
137
|
-
colour = self._colours.green_bold
|
|
138
|
-
msg = "done"
|
|
139
|
-
icon = constants.TICK
|
|
140
|
-
elif passed is False:
|
|
141
|
-
colour = self._colours.red_bold
|
|
142
|
-
msg = "failed"
|
|
143
|
-
icon = constants.X
|
|
144
|
-
else:
|
|
145
|
-
colour = self._colours.white_bold
|
|
146
|
-
msg = "running"
|
|
147
|
-
|
|
148
|
-
if not icon:
|
|
149
|
-
msg += "..."
|
|
150
|
-
|
|
151
|
-
timer = ""
|
|
152
|
-
if include_timer:
|
|
153
|
-
end = output.process.end
|
|
154
|
-
if not output.process.end:
|
|
155
|
-
end = time.perf_counter()
|
|
156
|
-
elapsed = end - output.process.start
|
|
157
|
-
timer = f"({format_time_taken(elapsed)})"
|
|
158
|
-
|
|
159
|
-
command = output.process.command
|
|
160
|
-
if get_num_lines(output.process.command) > 1:
|
|
161
|
-
columns = constants.COLUMNS() - (len(msg) + len(timer) + 9)
|
|
162
|
-
command = truncate_line(command, columns)
|
|
163
|
-
|
|
164
|
-
out = f"{self._colours.white_bold}[{self._colours.reset_colour}{self._colours.blue_bold}{command}{self._colours.reset_colour}{self._colours.white_bold}]{self._colours.reset_colour}{colour} {msg} {icon}{self._colours.reset_colour}"
|
|
165
|
-
|
|
166
|
-
if timer:
|
|
167
|
-
out += f" {self._colours.dim_on}{timer}{self._colours.dim_off}"
|
|
168
|
-
|
|
169
|
-
return out
|
|
170
|
-
|
|
171
|
-
def generate_process_group_output(
|
|
172
|
-
self,
|
|
173
|
-
output: ProcessGroupOutput,
|
|
174
|
-
interrupt_count: int = 0,
|
|
175
|
-
tail_output: bool = True,
|
|
176
|
-
) -> list[tuple[bool, str, str]]:
|
|
177
|
-
set_process_lines(output, interrupt_count)
|
|
178
|
-
|
|
179
|
-
for out in output.processes:
|
|
180
|
-
self.generate_process_output(out, tail_output, append_newlines=True)
|
|
181
|
-
|
|
182
|
-
if interrupt_count == 1:
|
|
183
|
-
self._to_print.append((False, "", "\n"))
|
|
184
|
-
self._to_print.append(
|
|
185
|
-
(
|
|
186
|
-
False,
|
|
187
|
-
f"{self._colours.yellow_bold}Interrupt!{self._colours.reset_colour}",
|
|
188
|
-
"\n",
|
|
189
|
-
)
|
|
190
|
-
)
|
|
191
|
-
elif interrupt_count == 2:
|
|
192
|
-
self._to_print.append((False, "", "\n"))
|
|
193
|
-
self._to_print.append(
|
|
194
|
-
(
|
|
195
|
-
False,
|
|
196
|
-
f"{self._colours.red_bold}Abort!{self._colours.reset_colour}",
|
|
197
|
-
"\n",
|
|
198
|
-
)
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
self._icon += 1
|
|
202
|
-
if self._icon == len(constants.ICONS):
|
|
203
|
-
self._icon = 0
|
|
204
|
-
|
|
205
|
-
return self._to_print
|
|
206
|
-
|
|
207
|
-
def print_process_output(
|
|
208
|
-
self,
|
|
209
|
-
output: ProcessOutput,
|
|
210
|
-
tail_output: bool = False,
|
|
211
|
-
include_cmd: bool = True,
|
|
212
|
-
include_output: bool = True,
|
|
213
|
-
include_progress: bool = True,
|
|
214
|
-
include_timer: bool | None = None,
|
|
215
|
-
) -> None:
|
|
216
|
-
for include_prefix, line, end in self.generate_process_output(
|
|
217
|
-
output,
|
|
218
|
-
tail_output,
|
|
219
|
-
include_cmd,
|
|
220
|
-
include_output,
|
|
221
|
-
include_progress,
|
|
222
|
-
include_timer,
|
|
223
|
-
):
|
|
224
|
-
self.write(line, include_prefix, end)
|
|
225
|
-
|
|
226
|
-
# Force a flush otherwise lines that don't end in a newline character will not get printed as they are read
|
|
227
|
-
print("", end="", flush=True)
|
|
228
|
-
|
|
229
|
-
def print_progress_group_output(
|
|
230
|
-
self,
|
|
231
|
-
output: ProcessGroupOutput,
|
|
232
|
-
interrupt_count: int = 0,
|
|
233
|
-
tail_output: bool = True,
|
|
234
|
-
) -> None:
|
|
235
|
-
columns = constants.COLUMNS()
|
|
236
|
-
self.generate_process_group_output(output, interrupt_count, tail_output)
|
|
237
|
-
|
|
238
|
-
num_last_printed_lines = len(self._last_printed)
|
|
239
|
-
|
|
240
|
-
# If we don't have any last printed lines or we don't want to tail the output,
|
|
241
|
-
# we just print all the new lines
|
|
242
|
-
if not num_last_printed_lines or not tail_output:
|
|
243
|
-
for include_prefix, line, end in self._to_print:
|
|
244
|
-
self.write(
|
|
245
|
-
line, include_prefix, end, truncate=tail_output, columns=columns
|
|
246
|
-
)
|
|
247
|
-
else:
|
|
248
|
-
# Compare the number of last lines and new lines and only update what has changed.
|
|
249
|
-
#
|
|
250
|
-
# Move the cursor up the amount the lines that were last printed so we can start
|
|
251
|
-
# comparing the last printed lines with the new lines that were generated
|
|
252
|
-
print(f"\033[{num_last_printed_lines}A", end="")
|
|
253
|
-
for i, line_parts in enumerate(self._to_print):
|
|
254
|
-
# If this is a completely new line, just print it
|
|
255
|
-
if i >= num_last_printed_lines:
|
|
256
|
-
include_prefix, line, end = line_parts
|
|
257
|
-
self.write(
|
|
258
|
-
line, include_prefix, end, truncate=tail_output, columns=columns
|
|
259
|
-
)
|
|
260
|
-
# If the current line is not the same as it's newly generated version, we update the line
|
|
261
|
-
elif line_parts[1] != self._last_printed[i][1]:
|
|
262
|
-
include_prefix, line, end = line_parts
|
|
263
|
-
# Clear the current line
|
|
264
|
-
print(f"{constants.CLEAR_LINE}\r", end="")
|
|
265
|
-
# Write the new line, this will move the cursor to the next line automatically
|
|
266
|
-
self.write(
|
|
267
|
-
line, include_prefix, end, truncate=tail_output, columns=columns
|
|
268
|
-
)
|
|
269
|
-
else:
|
|
270
|
-
# Move on to the next line as this one doesn't need to be updated
|
|
271
|
-
print(constants.DOWN_LINE, end="")
|
|
272
|
-
|
|
273
|
-
# Force a flush to return the cursor to the bottom immediately
|
|
274
|
-
print("", end="", flush=True)
|
|
275
|
-
|
|
276
|
-
self._last_printed = self._to_print.copy()
|
|
277
|
-
self._to_print.clear()
|
|
278
|
-
|
|
279
|
-
def clear_last_printed_lines(self) -> None:
|
|
280
|
-
# Clear all the lines that were just printed
|
|
281
|
-
print(
|
|
282
|
-
f"{constants.CLEAR_LINE}{constants.UP_LINE}{constants.CLEAR_LINE}"
|
|
283
|
-
* len(self._last_printed),
|
|
284
|
-
end="",
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
def reset(self) -> None:
|
|
288
|
-
self._last_printed.clear()
|
|
289
|
-
self._to_print.clear()
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def set_process_lines(
|
|
293
|
-
output: ProcessGroupOutput,
|
|
294
|
-
interrupt_count: int = 0,
|
|
295
|
-
lines: int = 0,
|
|
296
|
-
) -> None:
|
|
297
|
-
lines = lines or constants.LINES() - 1
|
|
298
|
-
if interrupt_count:
|
|
299
|
-
lines -= 2
|
|
300
|
-
|
|
301
|
-
# Allocate lines to processes that have a fixed percentage of lines set
|
|
302
|
-
allocated_process_lines = lines // len(output.processes)
|
|
303
|
-
processes_with_dynamic_lines: list[ProcessOutput] = []
|
|
304
|
-
used_lines = 0
|
|
305
|
-
for process_output in output.processes:
|
|
306
|
-
# This process output doesn't have percentage_lines set, so skip it
|
|
307
|
-
if not process_output.process.percentage_lines:
|
|
308
|
-
processes_with_dynamic_lines.append(process_output)
|
|
309
|
-
continue
|
|
310
|
-
|
|
311
|
-
process_output.process.lines = int(
|
|
312
|
-
lines * process_output.process.percentage_lines
|
|
313
|
-
)
|
|
314
|
-
used_lines += process_output.process.lines
|
|
315
|
-
|
|
316
|
-
# Remove the used lines from the total available lines
|
|
317
|
-
lines -= used_lines
|
|
318
|
-
|
|
319
|
-
while lines:
|
|
320
|
-
# Calculate how many lines each process should have based on how many processes and lines are left
|
|
321
|
-
num_processes = len(processes_with_dynamic_lines) or 1
|
|
322
|
-
allocated_process_lines = lines // num_processes
|
|
323
|
-
processes_with_excess_output: list[ProcessOutput] = []
|
|
324
|
-
recalculate_lines = False
|
|
325
|
-
for process_output in processes_with_dynamic_lines:
|
|
326
|
-
# If the number of lines in this process output is less than how many terminal lines we would allocate it,
|
|
327
|
-
# Set it's allocated terminal lines to the exact number of lines in its output and remove this number from
|
|
328
|
-
# the total available terminal lines
|
|
329
|
-
if process_output.lines < allocated_process_lines:
|
|
330
|
-
process_output.process.lines = process_output.lines
|
|
331
|
-
lines -= process_output.process.lines
|
|
332
|
-
recalculate_lines = True
|
|
333
|
-
continue
|
|
334
|
-
|
|
335
|
-
processes_with_excess_output.append(process_output)
|
|
336
|
-
|
|
337
|
-
# We need to re-calcuate how many terminal lines we can allocate to each process if the output of at least one process
|
|
338
|
-
# contains less lines than what we would normally allocate it. This is done so we can allocate these extra lines to the
|
|
339
|
-
# other processes that contain more lines of output.
|
|
340
|
-
if recalculate_lines:
|
|
341
|
-
processes_with_dynamic_lines = processes_with_excess_output
|
|
342
|
-
else:
|
|
343
|
-
# All remaining processes exceed the number of terminal lines we will allocate them, so allocate them
|
|
344
|
-
# their terminal lines as normal and break out of the while loop
|
|
345
|
-
for process_output in processes_with_excess_output:
|
|
346
|
-
process_output.process.lines = allocated_process_lines
|
|
347
|
-
lines -= allocated_process_lines
|
|
348
|
-
|
|
349
|
-
# If there is any lines left, allocate them to the process that currently contains the most lines in its output, or
|
|
350
|
-
# allocate them to the first process if no process contains enough lines
|
|
351
|
-
if lines:
|
|
352
|
-
process_with_most_lines: ProcessOutput | None = None
|
|
353
|
-
most_lines = 0
|
|
354
|
-
for process_output in output.processes:
|
|
355
|
-
if process_output.process.lines > most_lines:
|
|
356
|
-
process_with_most_lines = process_output
|
|
357
|
-
most_lines = process_output.process.lines
|
|
358
|
-
|
|
359
|
-
if not process_with_most_lines:
|
|
360
|
-
output.processes[0].process.lines += lines
|
|
361
|
-
else:
|
|
362
|
-
process_with_most_lines.process.lines += lines
|
|
363
|
-
|
|
364
|
-
break
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def get_num_lines(line: str, columns: int | None = None) -> int:
|
|
368
|
-
lines = 0
|
|
369
|
-
columns = columns or constants.COLUMNS()
|
|
370
|
-
line = constants.ANSI_ESCAPE.sub("", line)
|
|
371
|
-
length = len(line)
|
|
372
|
-
line_lines = 1
|
|
373
|
-
if length > columns:
|
|
374
|
-
line_lines = length // columns
|
|
375
|
-
remainder = length % columns
|
|
376
|
-
if remainder:
|
|
377
|
-
line_lines += 1
|
|
378
|
-
lines += 1 * line_lines
|
|
379
|
-
return lines
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def truncate_line(line: str, columns: int | None = None) -> str:
|
|
383
|
-
columns = columns or constants.COLUMNS()
|
|
384
|
-
escaped_line = constants.ANSI_ESCAPE.sub("", line)
|
|
385
|
-
return "".join(escaped_line[:columns]) + "..."
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def format_time_taken(time_taken: float) -> str:
|
|
389
|
-
time_taken = round(time_taken, 1)
|
|
390
|
-
seconds = time_taken % (24 * 3600)
|
|
391
|
-
|
|
392
|
-
return f"{seconds}s"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|