pyallel 1.2.8__tar.gz → 1.3.1__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: 1.2.8
3
+ Version: 1.3.1
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
@@ -62,15 +62,28 @@ positional arguments:
62
62
  "echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
63
63
  "pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
64
64
 
65
+ PROCESS GROUPS
66
+ --------------
65
67
  commands can be grouped using the group separator symbol (:::)
66
68
 
67
- "echo boil kettle" "sleep 1" ::: "echo make coffee"
69
+ %(prog)s "echo boil kettle" "sleep 1" ::: "echo make coffee"
68
70
 
69
71
  the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
70
72
 
71
73
  command groups are ran in the sequence you provide them, and if a command group fails
72
74
  (if a command fails inside the command group) the rest of the command groups in the sequence are not run
73
75
 
76
+ COMMAND MODIFIERS
77
+ -----------------
78
+ modifiers can be set for commands to augment their behaviour using the command modifier symbol (::)
79
+
80
+ lines (only used in interactive mode):
81
+ the lines modifier allows you to specify how many lines the command output can take up on the screen
82
+
83
+ %(prog)s "lines=90 :: echo running long command..." "echo running other command..."
84
+
85
+ 90 is expressed as a percentage value, which must be between 1 and 100 inclusive
86
+
74
87
  options:
75
88
  -h, --help show this help message and exit
76
89
  -t, --no-timer don't time how long each command is taking
@@ -142,8 +155,8 @@ python -m venv .venv && \
142
155
 
143
156
  - [x] Add support to have commands depend on other commands (some commands must complete
144
157
  before a given command can start)
158
+ - [x] Add support to state how many lines a command can use for it's output in interactive mode
145
159
  - [ ] Add a debug mode that logs debug information to a log file
146
- - [ ] Add support to state how many lines a command can use for it's output in interactive mode
147
160
  - [ ] Maybe add support to allow the user to provide stdin for commands that request it
148
161
  (such as a REPL)
149
162
  - [ ] Add custom parsing of command output to support filtering for errors (like vim's
@@ -38,15 +38,28 @@ positional arguments:
38
38
  "echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
39
39
  "pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
40
40
 
41
+ PROCESS GROUPS
42
+ --------------
41
43
  commands can be grouped using the group separator symbol (:::)
42
44
 
43
- "echo boil kettle" "sleep 1" ::: "echo make coffee"
45
+ %(prog)s "echo boil kettle" "sleep 1" ::: "echo make coffee"
44
46
 
45
47
  the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
46
48
 
47
49
  command groups are ran in the sequence you provide them, and if a command group fails
48
50
  (if a command fails inside the command group) the rest of the command groups in the sequence are not run
49
51
 
52
+ COMMAND MODIFIERS
53
+ -----------------
54
+ modifiers can be set for commands to augment their behaviour using the command modifier symbol (::)
55
+
56
+ lines (only used in interactive mode):
57
+ the lines modifier allows you to specify how many lines the command output can take up on the screen
58
+
59
+ %(prog)s "lines=90 :: echo running long command..." "echo running other command..."
60
+
61
+ 90 is expressed as a percentage value, which must be between 1 and 100 inclusive
62
+
50
63
  options:
51
64
  -h, --help show this help message and exit
52
65
  -t, --no-timer don't time how long each command is taking
@@ -118,8 +131,8 @@ python -m venv .venv && \
118
131
 
119
132
  - [x] Add support to have commands depend on other commands (some commands must complete
120
133
  before a given command can start)
134
+ - [x] Add support to state how many lines a command can use for it's output in interactive mode
121
135
  - [ ] Add a debug mode that logs debug information to a log file
122
- - [ ] Add support to state how many lines a command can use for it's output in interactive mode
123
136
  - [ ] Maybe add support to allow the user to provide stdin for commands that request it
124
137
  (such as a REPL)
125
138
  - [ ] 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 = "1.2.8"
3
+ version = "1.3.1"
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,2 @@
1
+ class InvalidLinesModifierError(Exception):
2
+ """Raised when the lines modifier is invalid"""
@@ -7,7 +7,7 @@ import time
7
7
 
8
8
  from pyallel import constants
9
9
  from pyallel.colours import Colours
10
- from pyallel.errors import InvalidExecutableErrors
10
+ from pyallel.errors import InvalidLinesModifierError
11
11
  from pyallel.parser import Arguments, create_parser
12
12
  from pyallel.printer import Printer
13
13
  from pyallel.process_group_manager import ProcessGroupManager
@@ -35,7 +35,7 @@ def run_interactive(
35
35
  if poll > 0:
36
36
  return poll
37
37
 
38
- printer.clear()
38
+ printer.reset()
39
39
  process_group_manager.run()
40
40
  if not process_group_manager.next():
41
41
  return 0
@@ -111,7 +111,7 @@ def run(*args: str) -> int:
111
111
  exit_code = run_interactive(process_group_manager, printer)
112
112
  else:
113
113
  exit_code = run_non_interactive(process_group_manager, printer)
114
- except InvalidExecutableErrors as e:
114
+ except InvalidLinesModifierError as e:
115
115
  exit_code = 1
116
116
  message = str(e)
117
117
  except Exception:
@@ -31,14 +31,27 @@ if you were running the command directly in a shell, some examples are below
31
31
  "echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
32
32
  "pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
33
33
 
34
+ PROCESS GROUPS
35
+ --------------
34
36
  commands can be grouped using the group separator symbol (:::)
35
37
 
36
- "echo boil kettle" "sleep 1" ::: "echo make coffee"
38
+ %(prog)s "echo boil kettle" "sleep 1" ::: "echo make coffee"
37
39
 
38
40
  the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
39
41
 
40
42
  command groups are ran in the sequence you provide them, and if a command group fails
41
43
  (if a command fails inside the command group) the rest of the command groups in the sequence are not run
44
+
45
+ COMMAND MODIFIERS
46
+ -----------------
47
+ modifiers can be set for commands to augment their behaviour using the command modifier symbol (::)
48
+
49
+ lines (only used in interactive mode):
50
+ the lines modifier allows you to specify how many lines the command output can take up on the screen
51
+
52
+ %(prog)s "lines=90 :: echo running long command..." "echo running other command..."
53
+
54
+ 90 is expressed as a percentage value, which must be between 1 and 100 inclusive
42
55
  """
43
56
 
44
57
 
@@ -21,6 +21,7 @@ class Printer:
21
21
  line: str,
22
22
  include_prefix: bool = False,
23
23
  end: str = "\n",
24
+ flush: bool = False,
24
25
  truncate: bool = False,
25
26
  ) -> None:
26
27
  truncate_num = 0
@@ -31,7 +32,7 @@ class Printer:
31
32
  columns = constants.COLUMNS() - truncate_num
32
33
  if get_num_lines(line, columns) > 1:
33
34
  line = truncate_line(line, columns)
34
- print(f"{prefix}{line}", end=end, flush=True)
35
+ print(f"{prefix}{line}", end=end, flush=flush)
35
36
 
36
37
  def info(self, msg: str) -> None:
37
38
  self.write(
@@ -70,6 +71,9 @@ class Printer:
70
71
  out: list[tuple[bool, str, str]] = []
71
72
  line_parts: tuple[bool, str, str]
72
73
 
74
+ if tail_output and output.process.lines == 0:
75
+ return out
76
+
73
77
  if include_cmd:
74
78
  status = self.generate_process_output_status(
75
79
  output, include_progress, include_timer
@@ -82,8 +86,11 @@ class Printer:
82
86
  lines = output.data.splitlines(keepends=True)
83
87
 
84
88
  if tail_output:
85
- output_lines = output.lines - 1
86
- lines = lines[-output_lines:]
89
+ output_lines = output.process.lines - 1
90
+ if output_lines == 0:
91
+ lines = []
92
+ else:
93
+ lines = lines[-output_lines:]
87
94
 
88
95
  for line in lines:
89
96
  prefix = True
@@ -213,6 +220,9 @@ class Printer:
213
220
  ):
214
221
  self.write(line, include_prefix, end)
215
222
 
223
+ # Force a flush otherwise lines that don't end in a newline character will not get printed as they are read
224
+ self.write("", include_prefix=False, end="", flush=True)
225
+
216
226
  def print_progress_group_output(
217
227
  self,
218
228
  output: ProcessGroupOutput,
@@ -233,29 +243,62 @@ class Printer:
233
243
  end="",
234
244
  )
235
245
 
236
- self.clear()
246
+ self.reset()
237
247
 
238
- def clear(self) -> None:
248
+ def reset(self) -> None:
239
249
  self._printed.clear()
240
250
 
241
251
 
242
252
  def set_process_lines(
243
253
  output: ProcessGroupOutput,
244
254
  interrupt_count: int = 0,
245
- lines: int | None = None,
255
+ lines: int = 0,
246
256
  ) -> None:
247
257
  lines = lines or constants.LINES() - 1
248
258
  if interrupt_count:
249
259
  lines -= 2
250
260
 
251
- num_processes = len(output.processes)
252
- remainder = lines % num_processes
253
- tail = lines // num_processes
254
-
261
+ # Allocate lines to processes that have a fixed percentage of lines set
262
+ used_lines = 0
263
+ process_with_most_lines: ProcessOutput | None = None
264
+ other_processes: list[ProcessOutput] = []
255
265
  for out in output.processes:
256
- out.lines = tail
257
- if remainder:
258
- output.processes[-1].lines += remainder
266
+ if not out.process.percentage_lines:
267
+ other_processes.append(out)
268
+ continue
269
+
270
+ out.process.lines = int(lines * out.process.percentage_lines)
271
+ used_lines += out.process.lines
272
+
273
+ if (
274
+ process_with_most_lines is None
275
+ or process_with_most_lines.process.lines < out.process.lines
276
+ ):
277
+ process_with_most_lines = out
278
+
279
+ lines -= used_lines
280
+
281
+ # Allocate the rest of the available lines to the other processes that don't have fixed lines set
282
+ num_dynamic_processes = len(other_processes)
283
+ if num_dynamic_processes:
284
+ remainder = lines % num_dynamic_processes
285
+ tail = lines // num_dynamic_processes
286
+
287
+ for out in other_processes:
288
+ out.process.lines = tail
289
+
290
+ # If we have lines left to allocate, we give all of them to the process with the highest
291
+ # allocated lines or to the last process
292
+ if remainder:
293
+ if process_with_most_lines:
294
+ process_with_most_lines.process.lines += remainder
295
+ else:
296
+ other_processes[-1].process.lines += remainder
297
+ else:
298
+ # Otherwise allocate remaining lines to the process with the highest allocated lines
299
+ remainder = lines
300
+ if remainder and process_with_most_lines:
301
+ process_with_most_lines.process.lines += remainder
259
302
 
260
303
 
261
304
  def get_num_lines(line: str, columns: int | None = None) -> int:
@@ -6,24 +6,27 @@ import tempfile
6
6
  import time
7
7
  from typing import BinaryIO
8
8
 
9
+ from pyallel.errors import InvalidLinesModifierError
10
+
9
11
 
10
12
  class ProcessOutput:
11
13
  def __init__(self, id: int, process: Process, data: str = "") -> None:
12
14
  self.id = id
13
15
  self.data = data
14
16
  self.process = process
15
- self.lines = -1
16
17
 
17
18
  def merge(self, other: ProcessOutput) -> None:
18
19
  self.data += other.data
19
20
 
20
21
 
21
22
  class Process:
22
- def __init__(self, id: int, command: str) -> None:
23
+ def __init__(self, id: int, command: str, percentage_lines: float = 0.0) -> None:
23
24
  self.id = id
24
25
  self.command = command
25
26
  self.start = 0.0
26
27
  self.end = 0.0
28
+ self.lines = 0
29
+ self.percentage_lines = percentage_lines
27
30
  self._fd: BinaryIO
28
31
  self._process: subprocess.Popen[bytes]
29
32
 
@@ -71,3 +74,34 @@ class Process:
71
74
  def wait(self) -> int:
72
75
  return self._process.wait()
73
76
 
77
+ @classmethod
78
+ def from_command(cls, id: int, command: str) -> Process:
79
+ cmd = command.split(" :: ", maxsplit=1)
80
+ if len(cmd) == 1:
81
+ return cls(id, cmd[0])
82
+
83
+ args, *parts = cmd
84
+
85
+ percentage_lines = 0
86
+ for arg in args.split(" "):
87
+ try:
88
+ arg, value = args.split("=")
89
+ except ValueError:
90
+ continue
91
+
92
+ if arg == "lines":
93
+ try:
94
+ percentage_lines = int(value)
95
+ except ValueError:
96
+ raise InvalidLinesModifierError(
97
+ "lines modifier must be a number between 1 and 100"
98
+ )
99
+
100
+ if not 0 < percentage_lines <= 100:
101
+ raise InvalidLinesModifierError(
102
+ "lines modifier must be a number between 1 and 100"
103
+ )
104
+
105
+ break
106
+
107
+ return cls(id, " ".join(parts), round(percentage_lines / 100, 2))
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Sequence
4
4
 
5
- from pyallel.errors import InvalidExecutableError, InvalidExecutableErrors
5
+ from pyallel.errors import (
6
+ InvalidLinesModifierError,
7
+ )
6
8
  from pyallel.process import Process, ProcessOutput
7
9
 
8
10
 
@@ -63,16 +65,17 @@ class ProcessGroup:
63
65
  @classmethod
64
66
  def from_commands(cls, id: int, process_id: int, *commands: str) -> ProcessGroup:
65
67
  processes: list[Process] = []
66
- errors: list[InvalidExecutableError] = []
67
68
 
69
+ percentage_lines_sum = 0.0
68
70
  for i, command in enumerate(commands):
69
- try:
70
- processes.append(Process(i + process_id, command))
71
- except InvalidExecutableError as e:
72
- errors.append(e)
73
-
74
- if errors:
75
- raise InvalidExecutableErrors(*errors)
71
+ process = Process.from_command(i + process_id, command)
72
+ percentage_lines_sum += process.percentage_lines
73
+ processes.append(process)
74
+
75
+ if round(percentage_lines_sum, 2) > 1.0:
76
+ raise InvalidLinesModifierError(
77
+ "lines modifier must not exceed 100 across all processes within each process group"
78
+ )
76
79
 
77
80
  process_group = cls(id=id, processes=processes)
78
81
 
@@ -1,11 +0,0 @@
1
- class InvalidExecutableError(Exception):
2
- def __init__(self, exe: str) -> None:
3
- super().__init__(exe)
4
- self.exe = exe
5
-
6
-
7
- class InvalidExecutableErrors(Exception):
8
- def __init__(self, *errors: InvalidExecutableError) -> None:
9
- super().__init__(
10
- f"executables [{', '.join(error.exe for error in errors)}] were not found"
11
- )
File without changes
File without changes
File without changes
File without changes