pyallel 0.5.0__tar.gz → 0.6.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.5.0
3
+ Version: 0.6.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
@@ -23,6 +23,8 @@ Description-Content-Type: text/markdown
23
23
 
24
24
  Run and handle the output of multiple executables in `pyallel` (as in parallel)
25
25
 
26
+ https://github.com/Danthewaann/pyallel/assets/22531177/be12efc4-439d-416d-8112-dc57cc4c291a
27
+
26
28
  Requires Python >=3.8
27
29
 
28
30
  Tested on Linux and MacOS only
@@ -37,17 +39,43 @@ pip install pyallel
37
39
 
38
40
  Once installed, you can run `pyallel` to see usage information, like so:
39
41
 
40
- ```bash
41
- pyallel
42
+ ```
43
+ usage: pyallel [-h] [-d] [-n] [-s] [-V] [-v] [commands ...]
44
+
45
+ Run and handle the output of multiple executables in pyallel (as in parallel)
46
+
47
+ positional arguments:
48
+ commands list of quoted commands to run e.g "mypy ." "black ."
49
+
50
+ can provide environment variables to each command like so:
51
+
52
+ "MYPY_FORCE_COLOR=1 mypy ."
53
+
54
+ command modes:
55
+
56
+ can also provide modes to commands to do extra things:
57
+
58
+ "tail=10 :: pytest ." <-- only output the last 10 lines, doesn't work in --no-stream mode
59
+
60
+ options:
61
+ -h, --help show this help message and exit
62
+ -d, --debug output debug info for each command
63
+ -n, --non-interactive
64
+ run in non-interactive mode
65
+ -s, --no-stream don't stream output of each command
66
+ -V, --verbose run in verbose mode
67
+ -v, --version print version and exit
42
68
  ```
43
69
 
44
70
  Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
45
71
 
46
72
  > [!IMPORTANT]
47
- > If your need to provide arguments to a command, you must surround the command and it's arguments in quotes!
73
+ > If you need to provide arguments to a command, you must surround the command and it's arguments in quotes!
48
74
 
49
75
  ```bash
50
- pyallel "black --color --check --diff ." "MYPY_FORCE_COLOR=1 mypy ." "ruff check --no-fix ."
76
+ pyallel "MYPY_FORCE_COLOR=1 mypy ." \
77
+ "black --check --diff ." \
78
+ "tail=20 :: pytest ."
51
79
  ```
52
80
 
53
81
  ## TODOs
@@ -55,15 +83,13 @@ pyallel "black --color --check --diff ." "MYPY_FORCE_COLOR=1 mypy ." "ruff check
55
83
  - [x] Allow output for all provided commands to be streamed to stdout (this will require a
56
84
  re-work of how we print command output as we currently just print output once the command
57
85
  finishes)
58
- - [ ] Add CI checks to run the tests and linters against Python versions > 3.8
59
- - [ ] Allow specific command outputs to be streamed to stdout, while all other
60
- commands will only get outputted after the streamed commands have completed (such as running
61
- `pytest` as a streamed command, whilst running `mypy`, `ruff` etc. as non-streamed commands).
62
- This will require adding special arguments/modes to each command so they are treated
63
- differently from other commands
64
- - [ ] Add visual examples of `pyallel` in action
86
+ - [x] Add CI checks to run the tests and linters against Python versions > 3.8
87
+ - [x] Add command mode arguments to support things like only tailing the last 10 lines
88
+ of a command whilst it is running e.g. `"tail=10 :: pytest ."`
89
+ - [x] Add visual examples of `pyallel` in action
65
90
  - [ ] Add custom parsing of command output to support filtering for errors (like vim's
66
91
  `errorformat`)
92
+ - [ ] Add graceful Ctrl-C interrupt handling
67
93
  - [x] Provide a way to set environment variables for each command to run with
68
94
  - [ ] Allow list of files to be provided to supply as input arguments to each command
69
95
  - [ ] Allow input to be piped into `pyallel` via stdin to supply as standard input to each
@@ -0,0 +1,83 @@
1
+ # Pyallel
2
+
3
+ Run and handle the output of multiple executables in `pyallel` (as in parallel)
4
+
5
+ https://github.com/Danthewaann/pyallel/assets/22531177/be12efc4-439d-416d-8112-dc57cc4c291a
6
+
7
+ Requires Python >=3.8
8
+
9
+ Tested on Linux and MacOS only
10
+
11
+ # Quick start
12
+
13
+ `pyallel` can be installed using pip:
14
+
15
+ ```bash
16
+ pip install pyallel
17
+ ```
18
+
19
+ Once installed, you can run `pyallel` to see usage information, like so:
20
+
21
+ ```
22
+ usage: pyallel [-h] [-d] [-n] [-s] [-V] [-v] [commands ...]
23
+
24
+ Run and handle the output of multiple executables in pyallel (as in parallel)
25
+
26
+ positional arguments:
27
+ commands list of quoted commands to run e.g "mypy ." "black ."
28
+
29
+ can provide environment variables to each command like so:
30
+
31
+ "MYPY_FORCE_COLOR=1 mypy ."
32
+
33
+ command modes:
34
+
35
+ can also provide modes to commands to do extra things:
36
+
37
+ "tail=10 :: pytest ." <-- only output the last 10 lines, doesn't work in --no-stream mode
38
+
39
+ options:
40
+ -h, --help show this help message and exit
41
+ -d, --debug output debug info for each command
42
+ -n, --non-interactive
43
+ run in non-interactive mode
44
+ -s, --no-stream don't stream output of each command
45
+ -V, --verbose run in verbose mode
46
+ -v, --version print version and exit
47
+ ```
48
+
49
+ Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
50
+
51
+ > [!IMPORTANT]
52
+ > If you need to provide arguments to a command, you must surround the command and it's arguments in quotes!
53
+
54
+ ```bash
55
+ pyallel "MYPY_FORCE_COLOR=1 mypy ." \
56
+ "black --check --diff ." \
57
+ "tail=20 :: pytest ."
58
+ ```
59
+
60
+ ## TODOs
61
+
62
+ - [x] Allow output for all provided commands to be streamed to stdout (this will require a
63
+ re-work of how we print command output as we currently just print output once the command
64
+ finishes)
65
+ - [x] Add CI checks to run the tests and linters against Python versions > 3.8
66
+ - [x] Add command mode arguments to support things like only tailing the last 10 lines
67
+ of a command whilst it is running e.g. `"tail=10 :: pytest ."`
68
+ - [x] Add visual examples of `pyallel` in action
69
+ - [ ] Add custom parsing of command output to support filtering for errors (like vim's
70
+ `errorformat`)
71
+ - [ ] Add graceful Ctrl-C interrupt handling
72
+ - [x] Provide a way to set environment variables for each command to run with
73
+ - [ ] Allow list of files to be provided to supply as input arguments to each command
74
+ - [ ] Allow input to be piped into `pyallel` via stdin to supply as standard input to each
75
+ command
76
+ - [ ] Add custom config file for `pyallel` to read from as an alternative to providing
77
+ arguments via the command line
78
+ - [ ] Add support for providing config via a `[tool.pyallel]` section in a
79
+ `pyproject.toml` file in the current working directory
80
+ - [ ] Maybe allow command dependencies to be defined in a python file where commands are
81
+ decorated with info that details it's dependencies?
82
+ - [x] Add test suite
83
+ - [x] Improve error handling when parsing provided commands (check they are valid executables)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyallel"
3
- version = "0.5.0"
3
+ version = "0.6.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"
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
+ import traceback
4
5
  import time
5
6
  import importlib.metadata
7
+ from pyallel.errors import InvalidExecutableErrors
6
8
 
7
9
  from pyallel.parser import Arguments, create_parser
8
10
  from pyallel.process import ProcessGroup, format_time_taken
@@ -11,13 +13,12 @@ from pyallel import constants
11
13
 
12
14
  def main_loop(
13
15
  commands: list[str],
14
- fail_fast: bool = False,
15
16
  interactive: bool = False,
16
17
  debug: bool = False,
17
18
  stream: bool = False,
18
19
  ) -> bool:
19
20
  process_group = ProcessGroup.from_commands(
20
- commands, interactive=interactive, fail_fast=fail_fast, debug=debug
21
+ commands, interactive=interactive, debug=debug
21
22
  )
22
23
  if not stream:
23
24
  return process_group.run()
@@ -48,14 +49,16 @@ def run(*args: str) -> int:
48
49
  try:
49
50
  status = main_loop(
50
51
  parsed_args.commands,
51
- parsed_args.fail_fast,
52
52
  parsed_args.interactive,
53
53
  parsed_args.debug,
54
54
  parsed_args.stream,
55
55
  )
56
- except Exception as e:
56
+ except InvalidExecutableErrors as e:
57
57
  status = False
58
58
  message = str(e)
59
+ except Exception:
60
+ status = False
61
+ message = traceback.format_exc()
59
62
 
60
63
  if not status:
61
64
  if not message:
@@ -5,7 +5,6 @@ from argparse import ArgumentParser, RawTextHelpFormatter
5
5
 
6
6
  class Arguments:
7
7
  commands: list[str]
8
- fail_fast: bool
9
8
  interactive: bool
10
9
  debug: bool
11
10
  verbose: bool
@@ -20,6 +19,20 @@ class Arguments:
20
19
  return msg
21
20
 
22
21
 
22
+ COMMANDS_HELP = """list of quoted commands to run e.g "mypy ." "black ."
23
+
24
+ can provide environment variables to each command like so:
25
+
26
+ "MYPY_FORCE_COLOR=1 mypy ."
27
+
28
+ command modes:
29
+
30
+ can also provide modes to commands to do extra things:
31
+
32
+ "tail=10 :: pytest ." <-- only output the last 10 lines, doesn't work in --no-stream mode
33
+ """
34
+
35
+
23
36
  def create_parser() -> ArgumentParser:
24
37
  parser = ArgumentParser(
25
38
  prog="pyallel",
@@ -28,15 +41,13 @@ def create_parser() -> ArgumentParser:
28
41
  )
29
42
  parser.add_argument(
30
43
  "commands",
31
- help='list of quoted commands to run e.g "mypy ." "black ."\n\n'
32
- "can provide environment variables to each command like so:\n\n"
33
- ' "MYPY_FORCE_COLOR=1 mypy ."',
44
+ help=COMMANDS_HELP,
34
45
  nargs="*",
35
46
  )
36
47
  parser.add_argument(
37
- "-f",
38
- "--fail-fast",
39
- help="exit immediately when a command fails",
48
+ "-d",
49
+ "--debug",
50
+ help="output debug info for each command",
40
51
  action="store_true",
41
52
  default=False,
42
53
  )
@@ -48,19 +59,13 @@ def create_parser() -> ArgumentParser:
48
59
  dest="interactive",
49
60
  default=True,
50
61
  )
51
- parser.add_argument(
52
- "-d",
53
- "--debug",
54
- help="output debug info for each command",
55
- action="store_true",
56
- default=False,
57
- )
58
62
  parser.add_argument(
59
63
  "-s",
60
- "--stream",
61
- help="stream output of first command",
62
- action="store_true",
63
- default=False,
64
+ "--no-stream",
65
+ help="don't stream output of each command",
66
+ action="store_false",
67
+ default=True,
68
+ dest="stream",
64
69
  )
65
70
  parser.add_argument(
66
71
  "-V",
@@ -16,7 +16,13 @@ from pyallel.errors import InvalidExecutableErrors, InvalidExecutableError
16
16
 
17
17
 
18
18
  def indent(output: str) -> str:
19
- return "\n".join(" " + line for line in output.splitlines())
19
+ length = 200
20
+ indented_output = []
21
+ for line in output.splitlines():
22
+ if len(line) > length:
23
+ line = line[:length] + "..."
24
+ indented_output.append(line)
25
+ return "\n".join(" " + line for line in indented_output)
20
26
 
21
27
 
22
28
  def format_time_taken(time_taken: float) -> str:
@@ -48,6 +54,7 @@ def get_command_status(
48
54
  icon: str | None = None,
49
55
  passed: bool | None = None,
50
56
  debug: bool = False,
57
+ timer: bool = False,
51
58
  ) -> str:
52
59
  if passed is True:
53
60
  colour = constants.GREEN_BOLD
@@ -71,7 +78,7 @@ def get_command_status(
71
78
 
72
79
  output += f"{constants.NC}]{colour} {msg} "
73
80
 
74
- if debug:
81
+ if timer:
75
82
  end = process.end
76
83
  if not process.end:
77
84
  end = time.perf_counter()
@@ -93,11 +100,11 @@ def run_process(process: Process, debug: bool = False) -> bool:
93
100
  print(f"{constants.CLEAR_LINE}{constants.CR}", end="")
94
101
 
95
102
  if process.return_code() != 0:
96
- print(get_command_status(process, passed=False, debug=debug))
103
+ print(get_command_status(process, passed=False, debug=debug, timer=debug))
97
104
  print_command_output(process)
98
105
  return False
99
106
  else:
100
- print(get_command_status(process, passed=True, debug=debug))
107
+ print(get_command_status(process, passed=True, debug=debug, timer=debug))
101
108
  print_command_output(process)
102
109
  return True
103
110
 
@@ -105,7 +112,6 @@ def run_process(process: Process, debug: bool = False) -> bool:
105
112
  @dataclass
106
113
  class ProcessGroup:
107
114
  processes: list[Process]
108
- fail_fast: bool = False
109
115
  interactive: bool = False
110
116
  debug: bool = False
111
117
  output: dict[UUID, str] = field(default_factory=dict)
@@ -134,9 +140,9 @@ class ProcessGroup:
134
140
  continue
135
141
 
136
142
  completed_processes.add(process.id)
137
- passed = run_process(process, debug=self.debug)
138
- if self.fail_fast and not passed:
139
- return False
143
+ process_passed = run_process(process, debug=self.debug)
144
+ if not process_passed:
145
+ passed = False
140
146
 
141
147
  if len(completed_processes) == len(self.processes):
142
148
  break
@@ -160,19 +166,31 @@ class ProcessGroup:
160
166
  for i, process in enumerate(self.processes, start=1):
161
167
  if process.poll() is not None:
162
168
  completed_processes.add(process.id)
169
+ if process.return_code() != 0:
170
+ passed = False
163
171
  output += get_command_status(
164
- process, passed=process.return_code() == 0, debug=self.debug
172
+ process,
173
+ passed=process.return_code() == 0,
174
+ debug=self.debug,
175
+ timer=self.debug,
165
176
  )
166
177
  output += "\n"
167
178
  else:
168
179
  output += get_command_status(
169
- process, icon=constants.ICONS[icon], debug=self.debug
180
+ process,
181
+ icon=constants.ICONS[icon],
182
+ debug=self.debug,
183
+ timer=self.debug,
170
184
  )
171
185
  output += "\n"
172
186
 
173
187
  process_output = process.read().decode()
174
188
  if process_output:
175
189
  self.output[process.id] = process_output
190
+ if process.tail_mode.enabled:
191
+ process_output = "\n".join(
192
+ process_output.splitlines()[-process.tail_mode.lines :]
193
+ )
176
194
  output += indent(process_output)
177
195
  output += "\n"
178
196
  if i != len(self.processes):
@@ -211,7 +229,7 @@ class ProcessGroup:
211
229
  output = ""
212
230
  for process in self.processes:
213
231
  if running_process is None and process.id not in completed_processes:
214
- output += get_command_status(process)
232
+ output += get_command_status(process, debug=self.debug)
215
233
  output += "\n"
216
234
  running_process = process
217
235
  elif running_process is not process:
@@ -223,6 +241,8 @@ class ProcessGroup:
223
241
  output += "\n"
224
242
 
225
243
  if process.poll() is not None:
244
+ if process.return_code() != 0:
245
+ passed = False
226
246
  process_output_2 = process.readline().decode()
227
247
  while process_output_2:
228
248
  output += indent(process_output_2)
@@ -230,7 +250,10 @@ class ProcessGroup:
230
250
  process_output_2 = process.readline().decode()
231
251
 
232
252
  output += get_command_status(
233
- process, passed=process.return_code() == 0, debug=self.debug
253
+ process,
254
+ passed=process.return_code() == 0,
255
+ debug=self.debug,
256
+ timer=self.debug,
234
257
  )
235
258
  output += "\n\n"
236
259
  completed_processes.add(process.id)
@@ -251,7 +274,6 @@ class ProcessGroup:
251
274
  cls,
252
275
  commands: list[str],
253
276
  interactive: bool = False,
254
- fail_fast: bool = False,
255
277
  debug: bool = False,
256
278
  ) -> ProcessGroup:
257
279
  processes: list[Process] = []
@@ -269,14 +291,19 @@ class ProcessGroup:
269
291
  return cls(
270
292
  processes=processes,
271
293
  interactive=interactive,
272
- fail_fast=fail_fast,
273
294
  debug=debug,
274
295
  )
275
296
 
276
297
 
298
+ @dataclass
299
+ class TailMode:
300
+ enabled: bool = False
301
+ lines: int = 0
302
+
303
+
277
304
  @dataclass
278
305
  class Process:
279
- id: UUID
306
+ id: UUID = field(repr=False, compare=False)
280
307
  name: str
281
308
  args: list[str]
282
309
  env: dict[str, str] = field(default_factory=dict)
@@ -287,6 +314,7 @@ class Process:
287
314
  fd_name: Path | None = None
288
315
  fd_read: BinaryIO | None = None
289
316
  fd: int | None = None
317
+ tail_mode: TailMode = field(default_factory=TailMode)
290
318
 
291
319
  def run(self) -> None:
292
320
  self.start = time.perf_counter()
@@ -302,8 +330,6 @@ class Process:
302
330
  def __del__(self) -> None:
303
331
  if self.fd_read:
304
332
  self.fd_read.close()
305
- if self.fd_name:
306
- self.fd_name.unlink(missing_ok=True)
307
333
 
308
334
  def poll(self) -> int | None:
309
335
  if self.process:
@@ -332,14 +358,19 @@ class Process:
332
358
 
333
359
  @classmethod
334
360
  def from_command(cls, command: str) -> Process:
361
+ tail_mode = TailMode()
335
362
  env = os.environ.copy()
336
363
  if " :: " in command:
337
- command_modes, args = command.split(" :: ")
338
- command_modes = command_modes.split()
339
- args = args.split()
364
+ modes, _args = command.split(" :: ")
365
+ if modes:
366
+ for mode in modes.split():
367
+ name, value = mode.split("=", maxsplit=1)
368
+ if name == "tail":
369
+ tail_mode.enabled = True
370
+ tail_mode.lines = int(value)
371
+ args = _args.split()
340
372
  else:
341
373
  args = command.split()
342
- command_modes = ""
343
374
 
344
375
  parsed_args: list[str] = []
345
376
  for arg in args:
@@ -353,4 +384,6 @@ class Process:
353
384
  raise InvalidExecutableError(parsed_args[0])
354
385
 
355
386
  str_args = shlex.split(" ".join(parsed_args[1:]))
356
- return cls(id=uuid4(), name=parsed_args[0], args=str_args, env=env)
387
+ return cls(
388
+ id=uuid4(), name=parsed_args[0], args=str_args, env=env, tail_mode=tail_mode
389
+ )
pyallel-0.5.0/README.md DELETED
@@ -1,57 +0,0 @@
1
- # Pyallel
2
-
3
- Run and handle the output of multiple executables in `pyallel` (as in parallel)
4
-
5
- Requires Python >=3.8
6
-
7
- Tested on Linux and MacOS only
8
-
9
- # Quick start
10
-
11
- `pyallel` can be installed using pip:
12
-
13
- ```bash
14
- pip install pyallel
15
- ```
16
-
17
- Once installed, you can run `pyallel` to see usage information, like so:
18
-
19
- ```bash
20
- pyallel
21
- ```
22
-
23
- Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
24
-
25
- > [!IMPORTANT]
26
- > If your need to provide arguments to a command, you must surround the command and it's arguments in quotes!
27
-
28
- ```bash
29
- pyallel "black --color --check --diff ." "MYPY_FORCE_COLOR=1 mypy ." "ruff check --no-fix ."
30
- ```
31
-
32
- ## TODOs
33
-
34
- - [x] Allow output for all provided commands to be streamed to stdout (this will require a
35
- re-work of how we print command output as we currently just print output once the command
36
- finishes)
37
- - [ ] Add CI checks to run the tests and linters against Python versions > 3.8
38
- - [ ] Allow specific command outputs to be streamed to stdout, while all other
39
- commands will only get outputted after the streamed commands have completed (such as running
40
- `pytest` as a streamed command, whilst running `mypy`, `ruff` etc. as non-streamed commands).
41
- This will require adding special arguments/modes to each command so they are treated
42
- differently from other commands
43
- - [ ] Add visual examples of `pyallel` in action
44
- - [ ] Add custom parsing of command output to support filtering for errors (like vim's
45
- `errorformat`)
46
- - [x] Provide a way to set environment variables for each command to run with
47
- - [ ] Allow list of files to be provided to supply as input arguments to each command
48
- - [ ] Allow input to be piped into `pyallel` via stdin to supply as standard input to each
49
- command
50
- - [ ] Add custom config file for `pyallel` to read from as an alternative to providing
51
- arguments via the command line
52
- - [ ] Add support for providing config via a `[tool.pyallel]` section in a
53
- `pyproject.toml` file in the current working directory
54
- - [ ] Maybe allow command dependencies to be defined in a python file where commands are
55
- decorated with info that details it's dependencies?
56
- - [x] Add test suite
57
- - [x] Improve error handling when parsing provided commands (check they are valid executables)
File without changes
File without changes
File without changes
File without changes