pyallel 0.2.1__tar.gz → 0.3.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.2.1
3
+ Version: 0.3.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
@@ -50,8 +50,10 @@ pyallel "black --color --check --diff ." "mypy ." "ruff check --no-fix ."
50
50
  command
51
51
  - [ ] Add custom config file for `pyallel` to read from as an alternative to providing
52
52
  arguments via the command line
53
- - [ ] Add support for providing config via a `pyproject.toml` file in the current
54
- working directory
53
+ - [ ] Add support for providing config via a `[tool.pyallel]` section in a
54
+ `pyproject.toml` file in the current working directory
55
+ - [ ] Maybe allow command dependencies to be defined in a python file where commands are
56
+ decorated with info that details it's dependencies?
55
57
  - [ ] Add test suite
56
58
  - [ ] Improve error handling when parsing provided commands (check they are valid executables)
57
59
  - [ ] Add visual examples of `pyallel` in action
@@ -32,8 +32,10 @@ pyallel "black --color --check --diff ." "mypy ." "ruff check --no-fix ."
32
32
  command
33
33
  - [ ] Add custom config file for `pyallel` to read from as an alternative to providing
34
34
  arguments via the command line
35
- - [ ] Add support for providing config via a `pyproject.toml` file in the current
36
- working directory
35
+ - [ ] Add support for providing config via a `[tool.pyallel]` section in a
36
+ `pyproject.toml` file in the current working directory
37
+ - [ ] Maybe allow command dependencies to be defined in a python file where commands are
38
+ decorated with info that details it's dependencies?
37
39
  - [ ] Add test suite
38
40
  - [ ] Improve error handling when parsing provided commands (check they are valid executables)
39
41
  - [ ] Add visual examples of `pyallel` in action
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyallel"
3
- version = "0.2.1"
3
+ version = "0.3.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"
@@ -16,15 +16,22 @@ classifiers = [
16
16
  ]
17
17
 
18
18
  [tool.poetry.scripts]
19
- pyallel = "pyallel.main:run"
19
+ pyallel = "pyallel.main:entry_point"
20
20
 
21
21
  [tool.poetry.dependencies]
22
22
  python = ">=3.11,<4.0"
23
23
 
24
+ [tool.poetry.group.dev.dependencies]
25
+ pytest = "^7.4.3"
26
+ mypy = "^1.7.0"
27
+
24
28
  [tool.mypy]
25
29
  strict = true
26
30
  show_error_codes = true
27
31
 
32
+ [tool.pytest.ini_options]
33
+ addopts = "-vvv"
34
+
28
35
  [build-system]
29
36
  requires = ["poetry-core"]
30
37
  build-backend = "poetry.core.masonry.api"
@@ -1,42 +1,41 @@
1
- import argparse
2
1
  import os
3
2
  import sys
4
3
  import time
5
- from datetime import timedelta
6
4
  from dataclasses import dataclass
5
+ import shlex
7
6
  import importlib.metadata
8
7
  import subprocess
9
8
 
10
- WHITE_BOLD = "\033[1m"
11
- GREEN_BOLD = "\033[1;32m"
12
- BLUE_BOLD = "\033[1;34m"
13
- RED_BOLD = "\033[1;31m"
14
- NC = "\033[0m"
15
- CLEAR_LINE = "\033[2K"
16
- UP_LINE = "\033[1F"
17
- ICONS = ("/", "-", "\\", "|")
9
+ from pyallel.parser import Arguments, create_parser
10
+
11
+ IN_TTY = sys.__stdin__.isatty()
12
+
13
+ if IN_TTY:
14
+ WHITE_BOLD = "\033[1m"
15
+ GREEN_BOLD = "\033[1;32m"
16
+ BLUE_BOLD = "\033[1;34m"
17
+ RED_BOLD = "\033[1;31m"
18
+ CLEAR_LINE = "\033[2K"
19
+ UP_LINE = "\033[1F"
20
+ NC = "\033[0m"
21
+ CR = "\r"
22
+ else:
23
+ WHITE_BOLD = ""
24
+ GREEN_BOLD = ""
25
+ BLUE_BOLD = ""
26
+ RED_BOLD = ""
27
+ CLEAR_LINE = ""
28
+ UP_LINE = ""
29
+ NC = ""
30
+ CR = ""
31
+
18
32
 
33
+ ICONS = ("/", "-", "\\", "|")
19
34
  # Unicode character bytes to render different symbols in the terminal
20
35
  TICK = "\u2713"
21
36
  X = "\u2717"
22
37
 
23
38
 
24
- class Arguments:
25
- commands: list[str]
26
- fail_fast: bool
27
- interactive: bool
28
- debug: bool
29
- verbose: bool
30
- version: bool
31
-
32
- def __repr__(self) -> str:
33
- msg = "Arguments:\n"
34
- padding = len(sorted(self.__dict__.keys(), key=len, reverse=True)[0]) + 1
35
- for field, value in self.__dict__.items():
36
- msg += f" {field: <{padding}}: {value}\n"
37
- return msg
38
-
39
-
40
39
  @dataclass
41
40
  class Process:
42
41
  name: str
@@ -46,7 +45,8 @@ class Process:
46
45
 
47
46
 
48
47
  def run_command(command: str) -> Process:
49
- executable, *args = command.split()
48
+ executable, *args = command.split(maxsplit=1)
49
+ args = shlex.split(" ".join(args))
50
50
  env = os.environ.copy()
51
51
  start = time.perf_counter()
52
52
  process = subprocess.Popen(
@@ -60,52 +60,6 @@ def run_command(command: str) -> Process:
60
60
  return Process(name=executable, args=args, start=start, process=process)
61
61
 
62
62
 
63
- def create_parser() -> argparse.ArgumentParser:
64
- parser = argparse.ArgumentParser(
65
- prog="pyallel",
66
- description="Run and handle the output of multiple executables in pyallel (as in parallel)",
67
- )
68
- parser.add_argument("commands", help="list of commands to run", nargs="*")
69
- parser.add_argument(
70
- "-f",
71
- "--fail-fast",
72
- help="exit immediately when a command fails",
73
- action="store_true",
74
- default=False,
75
- )
76
- parser.add_argument(
77
- "-n",
78
- "--non-interactive",
79
- help="run in non-interactive mode",
80
- action="store_false",
81
- dest="interactive",
82
- default=True,
83
- )
84
- parser.add_argument(
85
- "-d",
86
- "--debug",
87
- help="output debug info for each command",
88
- action="store_true",
89
- default=False,
90
- )
91
- parser.add_argument(
92
- "-V",
93
- "--verbose",
94
- help="run in verbose mode",
95
- action="store_true",
96
- default=False,
97
- )
98
- parser.add_argument(
99
- "-v",
100
- "--version",
101
- help="print version and exit",
102
- action="store_true",
103
- default=False,
104
- )
105
-
106
- return parser
107
-
108
-
109
63
  def run_commands(commands: list[str]) -> list[Process]:
110
64
  return [run_command(command) for command in commands]
111
65
 
@@ -127,13 +81,13 @@ def format_time_taken(time_taken: float) -> str:
127
81
  elif 60 <= time_taken < 3600:
128
82
  msg = f"{minutes}m"
129
83
  if seconds:
130
- msg += f" {seconds}s"
84
+ msg += f" {seconds}s"
131
85
  elif time_taken >= 3600:
132
86
  msg = f"{hour}h"
133
87
  if minutes:
134
- msg += f" {minutes}m"
88
+ msg += f" {minutes}m"
135
89
  if seconds:
136
- msg += f" {seconds}s"
90
+ msg += f" {seconds}s"
137
91
 
138
92
  return msg
139
93
 
@@ -179,20 +133,22 @@ def main_loop(
179
133
  completed_processes: set[str] = set()
180
134
  passed = True
181
135
 
182
- if not interactive:
136
+ if not interactive or not IN_TTY:
183
137
  print(f"{WHITE_BOLD}Running commands...{NC}\n")
184
138
 
185
139
  while True:
186
- if interactive:
140
+ if interactive and IN_TTY:
187
141
  for icon in ICONS:
188
- print(f"{CLEAR_LINE}\r{WHITE_BOLD}Running commands{NC} {icon}", end="")
142
+ print(
143
+ f"{CLEAR_LINE}{CR}{WHITE_BOLD}Running commands{NC} {icon}", end=""
144
+ )
189
145
  time.sleep(0.1)
190
146
 
191
147
  for process in processes:
192
148
  if process.name in completed_processes or process.process.poll() is None:
193
149
  continue
194
150
 
195
- print(f"${CLEAR_LINE}\r", end="")
151
+ print(f"{CLEAR_LINE}{CR}", end="")
196
152
  completed_processes.add(process.name)
197
153
 
198
154
  if process.process.returncode != 0:
@@ -212,35 +168,43 @@ def main_loop(
212
168
  return passed
213
169
 
214
170
 
215
- def run() -> None:
171
+ def run(*args: str) -> int:
216
172
  parser = create_parser()
217
- args = parser.parse_args(namespace=Arguments())
218
- if args.version:
173
+ parsed_args = parser.parse_args(args=args, namespace=Arguments())
174
+
175
+ if parsed_args.version:
219
176
  my_version = importlib.metadata.version("pyallel")
220
177
  print(my_version)
221
- sys.exit(0)
222
-
223
- if not sys.stdin.isatty():
224
- print(sys.stdin.read())
178
+ return 0
225
179
 
226
- if args.verbose:
227
- print(args)
180
+ if parsed_args.verbose:
181
+ print(parsed_args)
228
182
 
229
- if not args.commands:
183
+ if not parsed_args.commands:
230
184
  parser.print_help()
231
- sys.exit(2)
185
+ return 2
232
186
 
233
- start_time = time.perf_counter()
187
+ start = time.perf_counter()
234
188
 
235
189
  exit_code = 0
236
- status = main_loop(args.commands, args.fail_fast, args.interactive, args.debug)
190
+ status = main_loop(
191
+ parsed_args.commands,
192
+ parsed_args.fail_fast,
193
+ parsed_args.interactive,
194
+ parsed_args.debug,
195
+ )
237
196
  if not status:
238
197
  print(f"{RED_BOLD}A command failed!{NC}")
239
198
  exit_code = 1
240
199
  else:
241
200
  print(f"{GREEN_BOLD}Success!{NC}")
242
201
 
243
- elapsed_time = time.perf_counter() - start_time
244
- if args.debug:
245
- print(f"\nTime taken : {format_time_taken(elapsed_time)}")
246
- sys.exit(exit_code)
202
+ if parsed_args.debug:
203
+ elapsed = time.perf_counter() - start
204
+ print(f"\nTime taken : {format_time_taken(elapsed)}")
205
+
206
+ return exit_code
207
+
208
+
209
+ def entry_point() -> None:
210
+ sys.exit(run(*sys.argv[1:]))
@@ -0,0 +1,63 @@
1
+ from argparse import ArgumentParser
2
+
3
+
4
+ class Arguments:
5
+ commands: list[str]
6
+ fail_fast: bool
7
+ interactive: bool
8
+ debug: bool
9
+ verbose: bool
10
+ version: bool
11
+
12
+ def __repr__(self) -> str:
13
+ msg = "Arguments:\n"
14
+ padding = len(sorted(self.__dict__.keys(), key=len, reverse=True)[0]) + 1
15
+ for field, value in self.__dict__.items():
16
+ msg += f" {field: <{padding}}: {value}\n"
17
+ return msg
18
+
19
+
20
+ def create_parser() -> ArgumentParser:
21
+ parser = ArgumentParser(
22
+ prog="pyallel",
23
+ description="Run and handle the output of multiple executables in pyallel (as in parallel)",
24
+ )
25
+ parser.add_argument("commands", help="list of commands to run", nargs="*")
26
+ parser.add_argument(
27
+ "-f",
28
+ "--fail-fast",
29
+ help="exit immediately when a command fails",
30
+ action="store_true",
31
+ default=False,
32
+ )
33
+ parser.add_argument(
34
+ "-n",
35
+ "--non-interactive",
36
+ help="run in non-interactive mode",
37
+ action="store_false",
38
+ dest="interactive",
39
+ default=True,
40
+ )
41
+ parser.add_argument(
42
+ "-d",
43
+ "--debug",
44
+ help="output debug info for each command",
45
+ action="store_true",
46
+ default=False,
47
+ )
48
+ parser.add_argument(
49
+ "-V",
50
+ "--verbose",
51
+ help="run in verbose mode",
52
+ action="store_true",
53
+ default=False,
54
+ )
55
+ parser.add_argument(
56
+ "-v",
57
+ "--version",
58
+ help="print version and exit",
59
+ action="store_true",
60
+ default=False,
61
+ )
62
+
63
+ return parser
File without changes
File without changes
File without changes