pyallel 0.3.3__tar.gz → 0.4.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.
- {pyallel-0.3.3 → pyallel-0.4.0}/PKG-INFO +3 -2
- {pyallel-0.3.3 → pyallel-0.4.0}/README.md +2 -1
- {pyallel-0.3.3 → pyallel-0.4.0}/pyproject.toml +1 -1
- {pyallel-0.3.3 → pyallel-0.4.0}/src/pyallel/main.py +14 -39
- pyallel-0.4.0/src/pyallel/process.py +63 -0
- {pyallel-0.3.3 → pyallel-0.4.0}/LICENSE +0 -0
- {pyallel-0.3.3 → pyallel-0.4.0}/src/pyallel/__init__.py +0 -0
- {pyallel-0.3.3 → pyallel-0.4.0}/src/pyallel/errors.py +0 -0
- {pyallel-0.3.3 → pyallel-0.4.0}/src/pyallel/parser.py +0 -0
- {pyallel-0.3.3 → pyallel-0.4.0}/src/pyallel/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyallel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -53,6 +53,7 @@ pyallel "black --color --check --diff ." "mypy ." "ruff check --no-fix ."
|
|
|
53
53
|
- [ ] Allow a single main command output to be streamed to stdout, while all other
|
|
54
54
|
commands will only get outputted after the main command has completed (such as running
|
|
55
55
|
`pytest` as the main command, whilst running `mypy`, `ruff` etc. as other commands)
|
|
56
|
+
- [x] Provide a way to set environment variables for each command to run with
|
|
56
57
|
- [ ] Allow list of files to be provided to supply as input arguments to each command
|
|
57
58
|
- [ ] Allow input to be piped into `pyallel` via stdin to supply as standard input to each
|
|
58
59
|
command
|
|
@@ -63,6 +64,6 @@ pyallel "black --color --check --diff ." "mypy ." "ruff check --no-fix ."
|
|
|
63
64
|
- [ ] Maybe allow command dependencies to be defined in a python file where commands are
|
|
64
65
|
decorated with info that details it's dependencies?
|
|
65
66
|
- [x] Add test suite
|
|
66
|
-
- [
|
|
67
|
+
- [x] Improve error handling when parsing provided commands (check they are valid executables)
|
|
67
68
|
- [ ] Add visual examples of `pyallel` in action
|
|
68
69
|
|
|
@@ -32,6 +32,7 @@ pyallel "black --color --check --diff ." "mypy ." "ruff check --no-fix ."
|
|
|
32
32
|
- [ ] Allow a single main command output to be streamed to stdout, while all other
|
|
33
33
|
commands will only get outputted after the main command has completed (such as running
|
|
34
34
|
`pytest` as the main command, whilst running `mypy`, `ruff` etc. as other commands)
|
|
35
|
+
- [x] Provide a way to set environment variables for each command to run with
|
|
35
36
|
- [ ] Allow list of files to be provided to supply as input arguments to each command
|
|
36
37
|
- [ ] Allow input to be piped into `pyallel` via stdin to supply as standard input to each
|
|
37
38
|
command
|
|
@@ -42,5 +43,5 @@ pyallel "black --color --check --diff ." "mypy ." "ruff check --no-fix ."
|
|
|
42
43
|
- [ ] Maybe allow command dependencies to be defined in a python file where commands are
|
|
43
44
|
decorated with info that details it's dependencies?
|
|
44
45
|
- [x] Add test suite
|
|
45
|
-
- [
|
|
46
|
+
- [x] Improve error handling when parsing provided commands (check they are valid executables)
|
|
46
47
|
- [ ] Add visual examples of `pyallel` in action
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import sys
|
|
5
4
|
import time
|
|
6
|
-
import shutil
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
import shlex
|
|
9
5
|
import importlib.metadata
|
|
10
|
-
import subprocess
|
|
11
6
|
|
|
12
7
|
from pyallel.errors import InvalidExecutableErrors, InvalidExecutableError
|
|
13
8
|
from pyallel.parser import Arguments, create_parser
|
|
9
|
+
from pyallel.process import Process
|
|
14
10
|
|
|
15
11
|
IN_TTY = sys.__stdin__.isatty()
|
|
16
12
|
|
|
@@ -40,46 +36,22 @@ TICK = "\u2713"
|
|
|
40
36
|
X = "\u2717"
|
|
41
37
|
|
|
42
38
|
|
|
43
|
-
@dataclass
|
|
44
|
-
class Process:
|
|
45
|
-
name: str
|
|
46
|
-
args: list[str]
|
|
47
|
-
start: float
|
|
48
|
-
process: subprocess.Popen[bytes]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def run_command(command: str) -> Process:
|
|
52
|
-
executable, *args = command.split(maxsplit=1)
|
|
53
|
-
if not shutil.which(executable):
|
|
54
|
-
raise InvalidExecutableError(executable)
|
|
55
|
-
args = shlex.split(" ".join(args))
|
|
56
|
-
env = os.environ.copy()
|
|
57
|
-
# TODO: need to provide a way to supply environment variables
|
|
58
|
-
# for each provided command
|
|
59
|
-
env["MYPY_FORCE_COLOR"] = "1" if IN_TTY else "0"
|
|
60
|
-
start = time.perf_counter()
|
|
61
|
-
process = subprocess.Popen(
|
|
62
|
-
[executable, *args],
|
|
63
|
-
stdout=subprocess.PIPE,
|
|
64
|
-
stderr=subprocess.STDOUT,
|
|
65
|
-
env=env,
|
|
66
|
-
)
|
|
67
|
-
return Process(name=executable, args=args, start=start, process=process)
|
|
68
|
-
|
|
69
|
-
|
|
70
39
|
def run_commands(commands: list[str]) -> list[Process]:
|
|
71
40
|
processes: list[Process] = []
|
|
72
41
|
errors: list[InvalidExecutableError] = []
|
|
73
42
|
|
|
74
43
|
for command in commands:
|
|
75
44
|
try:
|
|
76
|
-
processes.append(
|
|
45
|
+
processes.append(Process.from_command(command))
|
|
77
46
|
except InvalidExecutableError as e:
|
|
78
47
|
errors.append(e)
|
|
79
48
|
|
|
80
49
|
if errors:
|
|
81
50
|
raise InvalidExecutableErrors(*errors)
|
|
82
51
|
|
|
52
|
+
for process in processes:
|
|
53
|
+
process.run()
|
|
54
|
+
|
|
83
55
|
return processes
|
|
84
56
|
|
|
85
57
|
|
|
@@ -135,10 +107,9 @@ def print_command_status(process: Process, passed: bool, debug: bool = False) ->
|
|
|
135
107
|
|
|
136
108
|
|
|
137
109
|
def print_command_output(process: Process) -> None:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
print(f"{indent(output.decode())}")
|
|
110
|
+
output = process.stdout().read()
|
|
111
|
+
if output:
|
|
112
|
+
print(f"{indent(output.decode())}")
|
|
142
113
|
print()
|
|
143
114
|
|
|
144
115
|
|
|
@@ -164,13 +135,13 @@ def main_loop(
|
|
|
164
135
|
time.sleep(0.1)
|
|
165
136
|
|
|
166
137
|
for process in processes:
|
|
167
|
-
if process.name in completed_processes or process.
|
|
138
|
+
if process.name in completed_processes or process.poll() is None:
|
|
168
139
|
continue
|
|
169
140
|
|
|
170
141
|
print(f"{CLEAR_LINE}{CR}", end="")
|
|
171
142
|
completed_processes.add(process.name)
|
|
172
143
|
|
|
173
|
-
if process.
|
|
144
|
+
if process.return_code() != 0:
|
|
174
145
|
print_command_status(process, passed=False, debug=debug)
|
|
175
146
|
print_command_output(process)
|
|
176
147
|
passed = False
|
|
@@ -236,3 +207,7 @@ def run(*args: str) -> int:
|
|
|
236
207
|
|
|
237
208
|
def entry_point() -> None:
|
|
238
209
|
sys.exit(run(*sys.argv[1:]))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
entry_point()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import subprocess
|
|
5
|
+
import shlex
|
|
6
|
+
import io
|
|
7
|
+
import shutil
|
|
8
|
+
import os
|
|
9
|
+
from typing import IO
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pyallel.errors import InvalidExecutableError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Process:
|
|
17
|
+
name: str
|
|
18
|
+
args: list[str]
|
|
19
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
20
|
+
start: float = 0.0
|
|
21
|
+
process: subprocess.Popen[bytes] | None = None
|
|
22
|
+
|
|
23
|
+
def run(self) -> None:
|
|
24
|
+
self.start = time.perf_counter()
|
|
25
|
+
self.process = subprocess.Popen(
|
|
26
|
+
[self.name, *self.args],
|
|
27
|
+
stdout=subprocess.PIPE,
|
|
28
|
+
stderr=subprocess.STDOUT,
|
|
29
|
+
env=self.env,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def poll(self) -> int | None:
|
|
33
|
+
if self.process:
|
|
34
|
+
return self.process.poll()
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def stdout(self) -> IO[bytes]:
|
|
38
|
+
if self.process and self.process.stdout:
|
|
39
|
+
return self.process.stdout
|
|
40
|
+
return io.BytesIO(b"")
|
|
41
|
+
|
|
42
|
+
def return_code(self) -> int | None:
|
|
43
|
+
if self.process:
|
|
44
|
+
return self.process.returncode
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_command(cls, command: str) -> Process:
|
|
49
|
+
env = os.environ.copy()
|
|
50
|
+
args = command.split()
|
|
51
|
+
parsed_args: list[str] = []
|
|
52
|
+
for arg in args:
|
|
53
|
+
if "=" in arg:
|
|
54
|
+
name, value = arg.split("=")
|
|
55
|
+
env[name] = value
|
|
56
|
+
else:
|
|
57
|
+
parsed_args.append(arg)
|
|
58
|
+
|
|
59
|
+
if not shutil.which(parsed_args[0]):
|
|
60
|
+
raise InvalidExecutableError(parsed_args[0])
|
|
61
|
+
|
|
62
|
+
args = shlex.split(" ".join(parsed_args[1:]))
|
|
63
|
+
return Process(name=parsed_args[0], args=args, env=env)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|