pyallel 1.1.0__tar.gz → 1.2.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: 1.1.0
3
+ Version: 1.2.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
@@ -48,10 +48,10 @@ usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
48
48
  Run and handle the output of multiple executables in pyallel (as in parallel)
49
49
 
50
50
  positional arguments:
51
- commands list of quoted commands to run e.g "mypy ." "black ."
51
+ commands list of quoted commands to run in parallel e.g "mypy ." "black ."
52
52
 
53
53
  each command is executed inside a shell, so shell syntax is supported as
54
- if you were running the command directly in a shell, some examples are below:
54
+ if you were running the command directly in a shell, some examples are below
55
55
 
56
56
  "MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
57
57
  "mypy | tee -a mypy.log" <- use pipes to redirect output
@@ -60,6 +60,14 @@ positional arguments:
60
60
  "echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
61
61
  "pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
62
62
 
63
+ commands can be grouped using the group separator symbol (:::)
64
+
65
+ "echo boil kettle" "sleep 1" ::: "echo make coffee"
66
+
67
+ the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
68
+
69
+ command groups are ran in the sequence you provide them, and if a command group fails
70
+ (if a command fails inside the command group) the rest of the command groups in the sequence are not run
63
71
 
64
72
  options:
65
73
  -h, --help show this help message and exit
@@ -110,6 +118,9 @@ python -m venv .venv && source .venv/bin/activate && pip install . -r requiremen
110
118
 
111
119
  ## TODOs
112
120
 
121
+ - [x] Add support to have commands depend on other commands (some commands must complete
122
+ before a given command can start)
123
+ - [ ] Add support to state how many lines a command can use for it's output in interactive mode
113
124
  - [ ] Maybe add support to allow the user to provide stdin for commands that request it
114
125
  (such as a REPL)
115
126
  - [ ] Add custom parsing of command output to support filtering for errors (like vim's
@@ -26,10 +26,10 @@ usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
26
26
  Run and handle the output of multiple executables in pyallel (as in parallel)
27
27
 
28
28
  positional arguments:
29
- commands list of quoted commands to run e.g "mypy ." "black ."
29
+ commands list of quoted commands to run in parallel e.g "mypy ." "black ."
30
30
 
31
31
  each command is executed inside a shell, so shell syntax is supported as
32
- if you were running the command directly in a shell, some examples are below:
32
+ if you were running the command directly in a shell, some examples are below
33
33
 
34
34
  "MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
35
35
  "mypy | tee -a mypy.log" <- use pipes to redirect output
@@ -38,6 +38,14 @@ 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
+ commands can be grouped using the group separator symbol (:::)
42
+
43
+ "echo boil kettle" "sleep 1" ::: "echo make coffee"
44
+
45
+ the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
46
+
47
+ command groups are ran in the sequence you provide them, and if a command group fails
48
+ (if a command fails inside the command group) the rest of the command groups in the sequence are not run
41
49
 
42
50
  options:
43
51
  -h, --help show this help message and exit
@@ -88,6 +96,9 @@ python -m venv .venv && source .venv/bin/activate && pip install . -r requiremen
88
96
 
89
97
  ## TODOs
90
98
 
99
+ - [x] Add support to have commands depend on other commands (some commands must complete
100
+ before a given command can start)
101
+ - [ ] Add support to state how many lines a command can use for it's output in interactive mode
91
102
  - [ ] Maybe add support to allow the user to provide stdin for commands that request it
92
103
  (such as a REPL)
93
104
  - [ ] 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.1.0"
3
+ version = "1.2.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"
@@ -8,23 +8,23 @@ from pyallel import constants
8
8
  from pyallel.colours import Colours
9
9
  from pyallel.errors import InvalidExecutableErrors
10
10
  from pyallel.parser import Arguments, create_parser
11
- from pyallel.process import ProcessGroup
11
+ from pyallel.process_group_manager import ProcessGroupManager
12
12
 
13
13
 
14
14
  def main_loop(
15
- *commands: str,
15
+ *args: str,
16
16
  colours: Colours,
17
17
  interactive: bool = False,
18
18
  timer: bool = False,
19
19
  ) -> int:
20
- process_group = ProcessGroup.from_commands(
21
- *commands,
20
+ process_group_manager = ProcessGroupManager.from_args(
21
+ *args,
22
22
  colours=colours,
23
23
  interactive=interactive,
24
24
  timer=timer,
25
25
  )
26
26
 
27
- return process_group.stream()
27
+ return process_group_manager.stream()
28
28
 
29
29
 
30
30
  def run(*args: str) -> int:
@@ -19,10 +19,10 @@ class Arguments:
19
19
  return msg
20
20
 
21
21
 
22
- COMMANDS_HELP = r"""list of quoted commands to run e.g "mypy ." "black ."
22
+ COMMANDS_HELP = r"""list of quoted commands to run in parallel e.g "mypy ." "black ."
23
23
 
24
24
  each command is executed inside a shell, so shell syntax is supported as
25
- if you were running the command directly in a shell, some examples are below:
25
+ if you were running the command directly in a shell, some examples are below
26
26
 
27
27
  "MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
28
28
  "mypy | tee -a mypy.log" <- use pipes to redirect output
@@ -31,6 +31,14 @@ 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
+ commands can be grouped using the group separator symbol (:::)
35
+
36
+ "echo boil kettle" "sleep 1" ::: "echo make coffee"
37
+
38
+ the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
39
+
40
+ command groups are ran in the sequence you provide them, and if a command group fails
41
+ (if a command fails inside the command group) the rest of the command groups in the sequence are not run
34
42
  """
35
43
 
36
44
 
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import signal
4
+ import subprocess
5
+ import tempfile
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from typing import BinaryIO
9
+
10
+
11
+ @dataclass
12
+ class Process:
13
+ id: int
14
+ command: str
15
+ start: float = 0.0
16
+ end: float = 0.0
17
+ _fd: BinaryIO | None = field(init=False, repr=False, compare=False, default=None)
18
+ _process: subprocess.Popen[bytes] | None = field(
19
+ init=False, repr=False, compare=False, default=None
20
+ )
21
+
22
+ def run(self) -> None:
23
+ self.start = time.perf_counter()
24
+ fd, fd_name = tempfile.mkstemp()
25
+ self._fd = open(fd_name, "rb")
26
+ self._process = subprocess.Popen(
27
+ self.command,
28
+ stdin=subprocess.DEVNULL,
29
+ stdout=fd,
30
+ stderr=subprocess.STDOUT,
31
+ shell=True,
32
+ )
33
+
34
+ def __del__(self) -> None:
35
+ if self._fd:
36
+ self._fd.close()
37
+
38
+ def poll(self) -> int | None:
39
+ if self._process:
40
+ poll = self._process.poll()
41
+ if poll is not None and not self.end:
42
+ self.end = time.perf_counter()
43
+ return poll
44
+ return None
45
+
46
+ def read(self) -> bytes:
47
+ if self._fd:
48
+ return self._fd.read()
49
+ return b""
50
+
51
+ def readline(self) -> bytes:
52
+ if self._fd:
53
+ return self._fd.readline()
54
+ return b""
55
+
56
+ def return_code(self) -> int | None:
57
+ if self._process:
58
+ return self._process.returncode
59
+ return None
60
+
61
+ def interrupt(self) -> None:
62
+ if self._process:
63
+ self._process.send_signal(signal.SIGINT)
64
+
65
+ def kill(self) -> None:
66
+ if self._process:
67
+ self._process.send_signal(signal.SIGKILL)
68
+
69
+ def wait(self) -> int:
70
+ if self._process:
71
+ return self._process.wait()
72
+ return -1
73
+
74
+ @classmethod
75
+ def from_command(cls, id: int, command: str) -> Process:
76
+ return cls(id=id, command=command)
@@ -1,17 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- import signal
4
- import subprocess
5
- import tempfile
6
3
  import time
7
4
  from collections import defaultdict
8
5
  from dataclasses import dataclass, field
9
- from typing import Any, BinaryIO
10
- from uuid import UUID, uuid4
11
6
 
12
7
  from pyallel import constants
13
8
  from pyallel.colours import Colours
14
9
  from pyallel.errors import InvalidExecutableError, InvalidExecutableErrors
10
+ from pyallel.process import Process
15
11
 
16
12
 
17
13
  def get_num_lines(output: str, columns: int | None = None) -> int:
@@ -36,9 +32,9 @@ class ProcessGroup:
36
32
  processes: list[Process]
37
33
  interactive: bool = False
38
34
  timer: bool = False
39
- output: dict[UUID, list[str]] = field(default_factory=lambda: defaultdict(list))
35
+ output: dict[int, list[str]] = field(default_factory=lambda: defaultdict(list))
40
36
  process_lines: list[int] = field(default_factory=list)
41
- completed_processes: set[UUID] = field(default_factory=set)
37
+ completed_processes: set[int] = field(default_factory=set)
42
38
  exit_code: int = 0
43
39
  interrupt_count: int = 0
44
40
  passed: bool = True
@@ -83,11 +79,6 @@ class ProcessGroup:
83
79
  running_process = None
84
80
  interrupted = False
85
81
 
86
- print(
87
- f"{self.colours.dim_on}=>{self.colours.dim_off} {self.colours.white_bold}Running commands...{self.colours.reset_colour}\n{self.colours.dim_on}=>{self.colours.dim_off} ",
88
- flush=True,
89
- )
90
-
91
82
  while True:
92
83
  output = ""
93
84
  for process in self.processes:
@@ -212,7 +203,7 @@ class ProcessGroup:
212
203
 
213
204
  return output
214
205
 
215
- def _handle_signal(self, signum: int, _frame: Any) -> None:
206
+ def handle_signal(self, signum: int) -> None:
216
207
  for process in self.processes:
217
208
  if self.interrupt_count == 0:
218
209
  process.interrupt()
@@ -226,16 +217,17 @@ class ProcessGroup:
226
217
  def from_commands(
227
218
  cls,
228
219
  *commands: str,
229
- colours: Colours,
220
+ colours: Colours | None = None,
230
221
  interactive: bool = False,
231
222
  timer: bool = False,
232
223
  ) -> ProcessGroup:
224
+ colours = colours or Colours()
233
225
  processes: list[Process] = []
234
226
  errors: list[InvalidExecutableError] = []
235
227
 
236
- for command in commands:
228
+ for i, command in enumerate(commands):
237
229
  try:
238
- processes.append(Process.from_command(command))
230
+ processes.append(Process.from_command(i + 1, command))
239
231
  except InvalidExecutableError as e:
240
232
  errors.append(e)
241
233
 
@@ -249,9 +241,6 @@ class ProcessGroup:
249
241
  colours=colours,
250
242
  )
251
243
 
252
- signal.signal(signal.SIGINT, process_group._handle_signal)
253
- signal.signal(signal.SIGTERM, process_group._handle_signal)
254
-
255
244
  return process_group
256
245
 
257
246
  def complete_output(self, tail: int = 20, all: bool = False) -> str:
@@ -319,71 +308,3 @@ class ProcessGroup:
319
308
  output += f"\n{self.colours.red_bold}Abort!{self.colours.reset_colour}"
320
309
 
321
310
  return output
322
-
323
-
324
- @dataclass
325
- class Process:
326
- id: UUID = field(repr=False, compare=False)
327
- command: str
328
- start: float = 0.0
329
- end: float = 0.0
330
- _fd: BinaryIO | None = field(init=False, repr=False, compare=False, default=None)
331
- _process: subprocess.Popen[bytes] | None = field(
332
- init=False, repr=False, compare=False, default=None
333
- )
334
-
335
- def run(self) -> None:
336
- self.start = time.perf_counter()
337
- fd, fd_name = tempfile.mkstemp()
338
- self._fd = open(fd_name, "rb")
339
- self._process = subprocess.Popen(
340
- self.command,
341
- stdin=subprocess.DEVNULL,
342
- stdout=fd,
343
- stderr=subprocess.STDOUT,
344
- shell=True,
345
- )
346
-
347
- def __del__(self) -> None:
348
- if self._fd:
349
- self._fd.close()
350
-
351
- def poll(self) -> int | None:
352
- if self._process:
353
- poll = self._process.poll()
354
- if poll is not None and not self.end:
355
- self.end = time.perf_counter()
356
- return poll
357
- return None
358
-
359
- def read(self) -> bytes:
360
- if self._fd:
361
- return self._fd.read()
362
- return b""
363
-
364
- def readline(self) -> bytes:
365
- if self._fd:
366
- return self._fd.readline()
367
- return b""
368
-
369
- def return_code(self) -> int | None:
370
- if self._process:
371
- return self._process.returncode
372
- return None
373
-
374
- def interrupt(self) -> None:
375
- if self._process:
376
- self._process.send_signal(signal.SIGINT)
377
-
378
- def kill(self) -> None:
379
- if self._process:
380
- self._process.send_signal(signal.SIGKILL)
381
-
382
- def wait(self) -> int:
383
- if self._process:
384
- return self._process.wait()
385
- return -1
386
-
387
- @classmethod
388
- def from_command(cls, command: str) -> Process:
389
- return cls(id=uuid4(), command=command)
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import signal
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from pyallel.colours import Colours
8
+ from pyallel.process_group import ProcessGroup
9
+
10
+
11
+ @dataclass
12
+ class ProcessGroupManager:
13
+ process_groups: list[ProcessGroup]
14
+ interactive: bool = False
15
+ colours: Colours = field(default_factory=Colours)
16
+
17
+ def stream(self) -> int:
18
+ exit_code = 0
19
+
20
+ if not self.interactive:
21
+ print(
22
+ f"{self.colours.dim_on}=>{self.colours.dim_off} {self.colours.white_bold}Running commands...{self.colours.reset_colour}\n{self.colours.dim_on}=>{self.colours.dim_off} ",
23
+ flush=True,
24
+ )
25
+
26
+ for process_group in self.process_groups:
27
+ exit_code = process_group.stream()
28
+ if exit_code > 0:
29
+ break
30
+
31
+ return exit_code
32
+
33
+ def handle_signal(self, signum: int, _frame: Any) -> None:
34
+ for process_group in self.process_groups:
35
+ process_group.handle_signal(signum)
36
+
37
+ @classmethod
38
+ def from_args(
39
+ cls,
40
+ *args: str,
41
+ colours: Colours | None = None,
42
+ interactive: bool = False,
43
+ timer: bool = False,
44
+ ) -> ProcessGroupManager:
45
+ colours = colours or Colours()
46
+ last_separator_index = 0
47
+ commands: list[str] = []
48
+ process_groups: list[ProcessGroup] = []
49
+
50
+ for i, arg in enumerate(args):
51
+ if arg == ":::":
52
+ if i - 1 == 0:
53
+ process_groups.append(
54
+ ProcessGroup.from_commands(
55
+ args[0],
56
+ colours=colours,
57
+ interactive=interactive,
58
+ timer=timer,
59
+ )
60
+ )
61
+ else:
62
+ process_groups.append(
63
+ ProcessGroup.from_commands(
64
+ *commands[last_separator_index:],
65
+ colours=colours,
66
+ interactive=interactive,
67
+ timer=timer,
68
+ )
69
+ )
70
+
71
+ last_separator_index = i
72
+ continue
73
+
74
+ commands.append(arg)
75
+
76
+ if len(process_groups) > 1:
77
+ last_separator_index -= 1
78
+
79
+ process_groups.append(
80
+ ProcessGroup.from_commands(
81
+ *commands[last_separator_index:],
82
+ colours=colours,
83
+ interactive=interactive,
84
+ timer=timer,
85
+ )
86
+ )
87
+
88
+ process_group_manager = cls(
89
+ process_groups=process_groups, interactive=interactive, colours=colours
90
+ )
91
+
92
+ signal.signal(signal.SIGINT, process_group_manager.handle_signal)
93
+ signal.signal(signal.SIGTERM, process_group_manager.handle_signal)
94
+
95
+ return process_group_manager
File without changes
File without changes
File without changes
File without changes
File without changes