pyallel 0.14.0__tar.gz → 0.15.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.14.0
3
+ Version: 0.15.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] [-V] [-v] [commands ...]
43
+ usage: pyallel [-h] [-t] [-n] [-V] [-v] [--colour {yes,no,auto}] [commands ...]
44
44
 
45
45
  Run and handle the output of multiple executables in pyallel (as in parallel)
46
46
 
@@ -58,6 +58,8 @@ options:
58
58
  run in non-interactive mode
59
59
  -V, --verbose run in verbose mode
60
60
  -v, --version print version and exit
61
+ --colour {yes,no,auto}
62
+ colour terminal output, defaults to "auto"
61
63
  ```
62
64
 
63
65
  Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
@@ -94,6 +96,7 @@ pyallel "MYPY_FORCE_COLOR=1 mypy ." \
94
96
  - [x] Maybe make tail mode followed by an optional dump of all the command output once it
95
97
  finishes the default behaviour?
96
98
  - [x] Add graceful Ctrl-C interrupt handling to streamed modes
99
+ - [x] Add a `--colour` flag to configure pyallel print colours
97
100
  - [ ] Setup build system to convert `pyallel` into a single file executable for ease of
98
101
  use and distribution
99
102
  - [ ] Add custom parsing of command output to support filtering for errors (like vim's
@@ -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] [-V] [-v] [commands ...]
22
+ usage: pyallel [-h] [-t] [-n] [-V] [-v] [--colour {yes,no,auto}] [commands ...]
23
23
 
24
24
  Run and handle the output of multiple executables in pyallel (as in parallel)
25
25
 
@@ -37,6 +37,8 @@ options:
37
37
  run in non-interactive mode
38
38
  -V, --verbose run in verbose mode
39
39
  -v, --version print version and exit
40
+ --colour {yes,no,auto}
41
+ colour terminal output, defaults to "auto"
40
42
  ```
41
43
 
42
44
  Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
@@ -73,6 +75,7 @@ pyallel "MYPY_FORCE_COLOR=1 mypy ." \
73
75
  - [x] Maybe make tail mode followed by an optional dump of all the command output once it
74
76
  finishes the default behaviour?
75
77
  - [x] Add graceful Ctrl-C interrupt handling to streamed modes
78
+ - [x] Add a `--colour` flag to configure pyallel print colours
76
79
  - [ ] Setup build system to convert `pyallel` into a single file executable for ease of
77
80
  use and distribution
78
81
  - [ ] Add custom parsing of command output to support filtering for errors (like vim's
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyallel"
3
- version = "0.14.0"
3
+ version = "0.15.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"
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, fields
4
+ from typing import Literal
5
+ from pyallel import constants
6
+
7
+
8
+ @dataclass
9
+ class Colours:
10
+ white_bold: str = "\033[1m"
11
+ green_bold: str = "\033[1;32m"
12
+ blue_bold: str = "\033[1;34m"
13
+ red_bold: str = "\033[1;31m"
14
+ yellow_bold: str = "\033[1;33m"
15
+ clear_line: str = "\033[2K"
16
+ clear_screen: str = "\033[2J"
17
+ save_cursor: str = "\033[s"
18
+ restore_cursor: str = "\033[u"
19
+ up_line: str = "\033[1F"
20
+ reset_colour: str = "\033[0m"
21
+ dim_on: str = "\033[2m"
22
+ dim_off: str = "\033[22m"
23
+
24
+ @classmethod
25
+ def from_colour(cls, colour: Literal["yes", "no", "auto"]) -> Colours:
26
+ colours = cls()
27
+
28
+ if colour == "no" or colour == "auto" and not constants.IN_TTY:
29
+ for field in fields(colours):
30
+ setattr(colours, field.name, "")
31
+
32
+ return colours
@@ -0,0 +1,21 @@
1
+ import os
2
+ import sys
3
+
4
+ IN_TTY = sys.stdout.isatty()
5
+
6
+
7
+ if IN_TTY:
8
+
9
+ def LINES() -> int:
10
+ return os.get_terminal_size().lines
11
+
12
+ else:
13
+
14
+ def LINES() -> int:
15
+ return sys.maxsize
16
+
17
+
18
+ ICONS = ("/", "-", "\\", "|")
19
+ # Unicode character bytes to render different symbols in the terminal
20
+ TICK = "\u2713"
21
+ X = "\u2717"
@@ -3,21 +3,26 @@ from __future__ import annotations
3
3
  import sys
4
4
  import traceback
5
5
  import importlib.metadata
6
+ from pyallel.colours import Colours
6
7
  from pyallel.errors import InvalidExecutableErrors
7
8
 
8
9
  from pyallel.parser import Arguments, create_parser
9
10
  from pyallel.process import ProcessGroup
10
- from pyallel import constants
11
11
 
12
12
 
13
13
  def main_loop(
14
14
  *commands: str,
15
+ colours: Colours,
15
16
  interactive: bool = False,
16
17
  timer: bool = False,
17
18
  verbose: bool = False,
18
19
  ) -> int:
19
20
  process_group = ProcessGroup.from_commands(
20
- *commands, interactive=interactive, timer=timer, verbose=verbose
21
+ *commands,
22
+ colours=colours,
23
+ interactive=interactive,
24
+ timer=timer,
25
+ verbose=verbose,
21
26
  )
22
27
 
23
28
  return process_group.stream()
@@ -36,10 +41,13 @@ def run(*args: str) -> int:
36
41
  parser.print_help()
37
42
  return 2
38
43
 
44
+ colours = Colours.from_colour(parsed_args.colour)
45
+
39
46
  message = None
40
47
  try:
41
48
  exit_code = main_loop(
42
49
  *parsed_args.commands,
50
+ colours=colours,
43
51
  interactive=parsed_args.interactive,
44
52
  timer=parsed_args.timer,
45
53
  verbose=parsed_args.verbose,
@@ -53,11 +61,11 @@ def run(*args: str) -> int:
53
61
 
54
62
  if exit_code == 1:
55
63
  if not message:
56
- print(f"{constants.RED_BOLD}A command failed!{constants.RESET_COLOUR}")
64
+ print(f"{colours.red_bold}A command failed!{colours.reset_colour}")
57
65
  else:
58
- print(f"{constants.RED_BOLD}Error: {message}{constants.RESET_COLOUR}")
66
+ print(f"{colours.red_bold}Error: {message}{colours.reset_colour}")
59
67
  elif exit_code == 0:
60
- print(f"{constants.GREEN_BOLD}Success!{constants.RESET_COLOUR}")
68
+ print(f"{colours.green_bold}Success!{colours.reset_colour}")
61
69
 
62
70
  return exit_code
63
71
 
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from argparse import ArgumentParser, RawTextHelpFormatter
4
+ from typing import Literal
4
5
 
5
6
 
6
7
  class Arguments:
8
+ colour: Literal["yes", "no", "auto"]
7
9
  commands: list[str]
8
10
  interactive: bool
9
11
  timer: bool
@@ -67,5 +69,11 @@ def create_parser() -> ArgumentParser:
67
69
  action="store_true",
68
70
  default=False,
69
71
  )
72
+ parser.add_argument(
73
+ "--colour",
74
+ help="colour terminal output, defaults to \"%(default)s\"",
75
+ choices=("yes", "no", "auto"),
76
+ default="auto",
77
+ )
70
78
 
71
79
  return parser
@@ -13,19 +13,10 @@ from typing import Any, BinaryIO
13
13
  from pyallel import constants
14
14
 
15
15
  from dataclasses import dataclass, field
16
+ from pyallel.colours import Colours
16
17
  from pyallel.errors import InvalidExecutableErrors, InvalidExecutableError
17
18
 
18
19
 
19
- def prefix(output: str, keepend: bool = True) -> str:
20
- prefixed_output = "\n".join(
21
- f"{constants.DIM_ON}=>{constants.DIM_OFF} {line}{constants.RESET_COLOUR}"
22
- for line in output.splitlines()
23
- )
24
- if keepend and output and output[-1] == "\n":
25
- prefixed_output += "\n"
26
- return prefixed_output
27
-
28
-
29
20
  def format_time_taken(time_taken: float) -> str:
30
21
  time_taken = round(time_taken, 1)
31
22
  seconds = time_taken % (24 * 3600)
@@ -33,47 +24,6 @@ def format_time_taken(time_taken: float) -> str:
33
24
  return f"{seconds}s"
34
25
 
35
26
 
36
- def get_command_status(
37
- process: Process,
38
- icon: str | None = None,
39
- passed: bool | None = None,
40
- verbose: bool = False,
41
- timer: bool = False,
42
- ) -> str:
43
- if passed is True:
44
- colour = constants.GREEN_BOLD
45
- msg = "done"
46
- icon = icon or constants.TICK
47
- elif passed is False:
48
- colour = constants.RED_BOLD
49
- msg = "failed"
50
- icon = icon or constants.X
51
- else:
52
- colour = constants.WHITE_BOLD
53
- msg = "running"
54
- icon = icon or ""
55
- if not icon:
56
- msg += "..."
57
-
58
- output = f"[{constants.BLUE_BOLD}{process.name}"
59
-
60
- if verbose:
61
- output += f" {' '.join(process.args)}"
62
-
63
- output += f"{constants.RESET_COLOUR}]{colour} {msg} {icon}{constants.RESET_COLOUR}"
64
-
65
- if timer:
66
- end = process.end
67
- if not process.end:
68
- end = time.perf_counter()
69
- elapsed = end - process.start
70
- output += (
71
- f" ({constants.DIM_ON}{format_time_taken(elapsed)}{constants.DIM_OFF})"
72
- )
73
-
74
- return output
75
-
76
-
77
27
  @dataclass
78
28
  class ProcessGroup:
79
29
  processes: list[Process]
@@ -87,6 +37,7 @@ class ProcessGroup:
87
37
  interrupt_count: int = 0
88
38
  passed: bool = True
89
39
  icon: int = 0
40
+ colours: Colours = field(default_factory=Colours)
90
41
 
91
42
  def stream(self) -> int:
92
43
  for process in self.processes:
@@ -125,7 +76,9 @@ class ProcessGroup:
125
76
  running_process = None
126
77
  interrupted = False
127
78
 
128
- print(f"{constants.WHITE_BOLD}Running commands...{constants.RESET_COLOUR}\n")
79
+ print(
80
+ f"{self.colours.white_bold}Running commands...{self.colours.reset_colour}\n"
81
+ )
129
82
 
130
83
  while True:
131
84
  output = ""
@@ -134,7 +87,7 @@ class ProcessGroup:
134
87
  running_process is None
135
88
  and process.id not in self.completed_processes
136
89
  ):
137
- output += get_command_status(process, verbose=self.verbose)
90
+ output += self._get_command_status(process, verbose=self.verbose)
138
91
  output += "\n"
139
92
  running_process = process
140
93
  elif running_process is not process:
@@ -146,14 +99,14 @@ class ProcessGroup:
146
99
  process_output = process.readline().decode()
147
100
 
148
101
  if not self.output[process.id] and process_output:
149
- process_output = prefix(process_output)
102
+ process_output = self._prefix(process_output)
150
103
  self.output[process.id].append(process_output)
151
104
  output += process_output
152
105
  elif process_output:
153
106
  if self.output[process.id][-1][-1] != "\n":
154
107
  self.output[process.id][-1] += process_output
155
108
  else:
156
- process_output = prefix(process_output)
109
+ process_output = self._prefix(process_output)
157
110
  self.output[process.id].append(process_output)
158
111
  output += process_output
159
112
 
@@ -162,9 +115,9 @@ class ProcessGroup:
162
115
  self.passed = False
163
116
  process_output = process.read().decode()
164
117
  if process_output:
165
- output += prefix(process_output)
118
+ output += self._prefix(process_output)
166
119
 
167
- output += get_command_status(
120
+ output += self._get_command_status(
168
121
  process,
169
122
  passed=process.return_code() == 0,
170
123
  verbose=self.verbose,
@@ -182,7 +135,7 @@ class ProcessGroup:
182
135
  and self.output[process.id][-1][-1] != "\n"
183
136
  ):
184
137
  output += "\n"
185
- output += f"\n{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}\n\n"
138
+ output += f"\n{self.colours.yellow_bold}Interrupt!{self.colours.reset_colour}\n\n"
186
139
  interrupted = True
187
140
 
188
141
  if output:
@@ -194,13 +147,61 @@ class ProcessGroup:
194
147
  time.sleep(0.01)
195
148
 
196
149
  if self.interrupt_count == 2:
197
- print(f"{constants.RED_BOLD}Abort!{constants.RESET_COLOUR}")
150
+ print(f"{self.colours.red_bold}Abort!{self.colours.reset_colour}")
198
151
 
199
152
  if not self.exit_code and not self.passed:
200
153
  self.exit_code = 1
201
154
 
202
155
  return self.exit_code
203
156
 
157
+ def _prefix(self, output: str, keepend: bool = True) -> str:
158
+ prefixed_output = "\n".join(
159
+ f"{self.colours.dim_on}=>{self.colours.dim_off} {line}{self.colours.reset_colour}"
160
+ for line in output.splitlines()
161
+ )
162
+ if keepend and output and output[-1] == "\n":
163
+ prefixed_output += "\n"
164
+ return prefixed_output
165
+
166
+ def _get_command_status(
167
+ self,
168
+ process: Process,
169
+ icon: str | None = None,
170
+ passed: bool | None = None,
171
+ verbose: bool = False,
172
+ timer: bool = False,
173
+ ) -> str:
174
+ if passed is True:
175
+ colour = self.colours.green_bold
176
+ msg = "done"
177
+ icon = icon or constants.TICK
178
+ elif passed is False:
179
+ colour = self.colours.red_bold
180
+ msg = "failed"
181
+ icon = icon or constants.X
182
+ else:
183
+ colour = self.colours.white_bold
184
+ msg = "running"
185
+ icon = icon or ""
186
+ if not icon:
187
+ msg += "..."
188
+
189
+ output = f"[{self.colours.blue_bold}{process.name}"
190
+
191
+ if verbose:
192
+ output += f" {' '.join(process.args)}"
193
+
194
+ output += f"{self.colours.reset_colour}]{colour} {msg} {icon}{self.colours.reset_colour}"
195
+
196
+ if timer:
197
+ end = process.end
198
+ if not process.end:
199
+ end = time.perf_counter()
200
+ elapsed = end - process.start
201
+ output += f" ({self.colours.dim_on}{format_time_taken(elapsed)}{self.colours.dim_off})"
202
+
203
+ return output
204
+
204
205
  def _handle_signal(self, signum: int, _frame: Any) -> None:
205
206
  for process in self.processes:
206
207
  if self.interrupt_count == 0:
@@ -215,6 +216,7 @@ class ProcessGroup:
215
216
  def from_commands(
216
217
  cls,
217
218
  *commands: str,
219
+ colours: Colours,
218
220
  interactive: bool = False,
219
221
  timer: bool = False,
220
222
  verbose: bool = False,
@@ -236,6 +238,7 @@ class ProcessGroup:
236
238
  interactive=interactive,
237
239
  timer=timer,
238
240
  verbose=verbose,
241
+ colours=colours,
239
242
  )
240
243
 
241
244
  signal.signal(signal.SIGINT, process_group._handle_signal)
@@ -266,7 +269,7 @@ class ProcessGroup:
266
269
  self.completed_processes.add(process.id)
267
270
  if process.return_code() != 0:
268
271
  self.passed = False
269
- output += get_command_status(
272
+ output += self._get_command_status(
270
273
  process,
271
274
  passed=process.return_code() == 0,
272
275
  verbose=self.verbose,
@@ -274,7 +277,7 @@ class ProcessGroup:
274
277
  )
275
278
  output += "\n"
276
279
  else:
277
- output += get_command_status(
280
+ output += self._get_command_status(
278
281
  process,
279
282
  icon=constants.ICONS[self.icon],
280
283
  verbose=self.verbose,
@@ -293,7 +296,7 @@ class ProcessGroup:
293
296
  process_output.splitlines()[-self.process_lines[i - 1] :]
294
297
  )
295
298
  process_output += "\n"
296
- output += prefix(process_output)
299
+ output += self._prefix(process_output)
297
300
  if output and output[-1] != "\n":
298
301
  output += "\n"
299
302
  if i != num_processes:
@@ -303,9 +306,11 @@ class ProcessGroup:
303
306
  return output
304
307
 
305
308
  if self.interrupt_count == 1:
306
- output += f"\n{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}"
309
+ output += (
310
+ f"\n{self.colours.yellow_bold}Interrupt!{self.colours.reset_colour}"
311
+ )
307
312
  elif self.interrupt_count == 2:
308
- output += f"\n{constants.RED_BOLD}Abort!{constants.RESET_COLOUR}"
313
+ output += f"\n{self.colours.red_bold}Abort!{self.colours.reset_colour}"
309
314
 
310
315
  return output
311
316
 
@@ -1,55 +0,0 @@
1
- import os
2
- import sys
3
-
4
- IN_TTY = sys.stdout.isatty()
5
-
6
-
7
- if IN_TTY:
8
- WHITE_BOLD = "\033[1m"
9
- GREEN_BOLD = "\033[1;32m"
10
- BLUE_BOLD = "\033[1;34m"
11
- RED_BOLD = "\033[1;31m"
12
- YELLOW_BOLD = "\033[1;33m"
13
- CLEAR_LINE = "\033[2K"
14
- CLEAR_SCREEN = "\033[2J"
15
- SAVE_CURSOR = "\033[s"
16
- RESTORE_CURSOR = "\033[u"
17
- UP_LINE = "\033[1F"
18
- RESET_COLOUR = "\033[0m"
19
- CARRIAGE_RETURN = "\r"
20
- DIM_ON = "\033[2m"
21
- DIM_OFF = "\033[22m"
22
-
23
- def COLUMNS() -> int:
24
- return os.get_terminal_size().columns
25
-
26
- def LINES() -> int:
27
- return os.get_terminal_size().lines
28
-
29
- else:
30
- WHITE_BOLD = ""
31
- GREEN_BOLD = ""
32
- BLUE_BOLD = ""
33
- RED_BOLD = ""
34
- YELLOW_BOLD = ""
35
- CLEAR_LINE = ""
36
- CLEAR_SCREEN = ""
37
- SAVE_CURSOR = ""
38
- RESTORE_CURSOR = ""
39
- UP_LINE = ""
40
- RESET_COLOUR = ""
41
- CARRIAGE_RETURN = ""
42
- DIM_ON = ""
43
- DIM_OFF = ""
44
-
45
- def COLUMNS() -> int:
46
- return sys.maxsize
47
-
48
- def LINES() -> int:
49
- return sys.maxsize
50
-
51
-
52
- ICONS = ("/", "-", "\\", "|")
53
- # Unicode character bytes to render different symbols in the terminal
54
- TICK = "\u2713"
55
- X = "\u2717"
File without changes
File without changes
File without changes