pyallel 2.0.5__tar.gz → 2.1.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.
Files changed (39) hide show
  1. pyallel-2.1.0/.vim/project.shada +0 -0
  2. {pyallel-2.0.5 → pyallel-2.1.0}/PKG-INFO +4 -3
  3. {pyallel-2.0.5 → pyallel-2.1.0}/README.md +3 -2
  4. {pyallel-2.0.5 → pyallel-2.1.0}/benchmark_process.py +3 -10
  5. pyallel-2.1.0/pyproject.toml +133 -0
  6. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/colours.py +1 -1
  7. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/constants.py +4 -4
  8. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/errors.py +3 -3
  9. pyallel-2.1.0/src/pyallel/logging.py +14 -0
  10. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/main.py +15 -10
  11. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/parser.py +17 -8
  12. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/printer.py +93 -59
  13. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/process.py +12 -31
  14. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/process_group.py +7 -12
  15. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/process_group_manager.py +13 -13
  16. pyallel-2.1.0/tests/conftest.py +13 -0
  17. {pyallel-2.0.5 → pyallel-2.1.0}/tests/test_main.py +39 -173
  18. {pyallel-2.0.5 → pyallel-2.1.0}/tests/test_printer.py +17 -19
  19. {pyallel-2.0.5 → pyallel-2.1.0}/tests/test_process.py +6 -39
  20. {pyallel-2.0.5 → pyallel-2.1.0}/tests/test_process_group.py +12 -19
  21. {pyallel-2.0.5 → pyallel-2.1.0}/tests/test_process_manager.py +66 -27
  22. {pyallel-2.0.5 → pyallel-2.1.0}/uv.lock +28 -1
  23. pyallel-2.0.5/.vim/project.shada +0 -0
  24. pyallel-2.0.5/pyproject.toml +0 -48
  25. {pyallel-2.0.5 → pyallel-2.1.0}/.base_branch +0 -0
  26. {pyallel-2.0.5 → pyallel-2.1.0}/.github/workflows/build.yml +0 -0
  27. {pyallel-2.0.5 → pyallel-2.1.0}/.github/workflows/test.yml +0 -0
  28. {pyallel-2.0.5 → pyallel-2.1.0}/.gitignore +0 -0
  29. {pyallel-2.0.5 → pyallel-2.1.0}/Dockerfile +0 -0
  30. {pyallel-2.0.5 → pyallel-2.1.0}/Dockerfile.alpine +0 -0
  31. {pyallel-2.0.5 → pyallel-2.1.0}/LICENSE +0 -0
  32. {pyallel-2.0.5 → pyallel-2.1.0}/bin/ldd +0 -0
  33. {pyallel-2.0.5 → pyallel-2.1.0}/bin/pyinstaller.sh +0 -0
  34. {pyallel-2.0.5 → pyallel-2.1.0}/build.sh +0 -0
  35. {pyallel-2.0.5 → pyallel-2.1.0}/build_all.sh +0 -0
  36. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/__init__.py +0 -0
  37. {pyallel-2.0.5 → pyallel-2.1.0}/src/pyallel/py.typed +0 -0
  38. {pyallel-2.0.5 → pyallel-2.1.0}/tests/assets/test_handle_multiple_signals.sh +0 -0
  39. {pyallel-2.0.5 → pyallel-2.1.0}/tests/assets/test_output.sh +0 -0
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyallel
3
- Version: 2.0.5
3
+ Version: 2.1.0
4
4
  Summary: Run and handle the output of multiple executables in pyallel (as in parallel)
5
5
  Project-URL: Homepage, https://github.com/Danthewaann/pyallel
6
6
  Project-URL: Repository, https://github.com/Danthewaann/pyallel
@@ -39,7 +39,7 @@ pipx install pyallel
39
39
  Once installed, you can run `pyallel` to see usage information, like so:
40
40
 
41
41
  ```
42
- usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
42
+ usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [--debug] [commands ...]
43
43
 
44
44
  run and handle the output of multiple executables in pyallel (as in parallel)
45
45
 
@@ -98,6 +98,7 @@ options:
98
98
  -V, --version print version and exit
99
99
  --colour {yes,no,auto}
100
100
  colour terminal output, defaults to "auto"
101
+ --debug enable debug mode, which logs debug info to a "pyallel.log" file in the current directory
101
102
  ```
102
103
 
103
104
  Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
@@ -158,7 +159,7 @@ pipx install uv && \
158
159
  before a given command can start)
159
160
  - [x] Add support to state how many lines a command can use for it's output in interactive mode
160
161
  - [x] Improve printing of output performance by only printing lines that have changed
161
- - [ ] Add a debug mode that logs debug information to a log file
162
+ - [x] Add a debug mode that logs debug information to a log file
162
163
  - [ ] Maybe add support to allow the user to provide stdin for commands that request it
163
164
  (such as a REPL)
164
165
  - [ ] Add custom parsing of command output to support filtering for errors (like vim's
@@ -21,7 +21,7 @@ pipx install pyallel
21
21
  Once installed, you can run `pyallel` to see usage information, like so:
22
22
 
23
23
  ```
24
- usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
24
+ usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [--debug] [commands ...]
25
25
 
26
26
  run and handle the output of multiple executables in pyallel (as in parallel)
27
27
 
@@ -80,6 +80,7 @@ options:
80
80
  -V, --version print version and exit
81
81
  --colour {yes,no,auto}
82
82
  colour terminal output, defaults to "auto"
83
+ --debug enable debug mode, which logs debug info to a "pyallel.log" file in the current directory
83
84
  ```
84
85
 
85
86
  Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
@@ -140,7 +141,7 @@ pipx install uv && \
140
141
  before a given command can start)
141
142
  - [x] Add support to state how many lines a command can use for it's output in interactive mode
142
143
  - [x] Improve printing of output performance by only printing lines that have changed
143
- - [ ] Add a debug mode that logs debug information to a log file
144
+ - [x] Add a debug mode that logs debug information to a log file
144
145
  - [ ] Maybe add support to allow the user to provide stdin for commands that request it
145
146
  (such as a REPL)
146
147
  - [ ] Add custom parsing of command output to support filtering for errors (like vim's
@@ -1,6 +1,4 @@
1
- """
2
- Ad-hoc benchmark comparing Process's stdout reading approach against two
3
- scenarios:
1
+ """Ad-hoc benchmark comparing Process's stdout reading approach against two scenarios.
4
2
 
5
3
  1. A chatty command that produces a lot of output while it's running.
6
4
  2. A quick command that exits almost immediately, followed by a period of
@@ -26,9 +24,7 @@ def cpu_time() -> float:
26
24
  return usage.ru_utime + usage.ru_stime
27
25
 
28
26
 
29
- def run_scenario(
30
- name: str, command: str, poll_seconds: float, extra_idle_seconds: float
31
- ) -> None:
27
+ def run_scenario(name: str, command: str, poll_seconds: float, extra_idle_seconds: float) -> None:
32
28
  process = Process(1, command)
33
29
 
34
30
  start_wall = time.perf_counter()
@@ -49,10 +45,7 @@ def run_scenario(
49
45
  end_wall = time.perf_counter()
50
46
  end_cpu = cpu_time()
51
47
 
52
- print(
53
- f"{name:12} wall={end_wall - start_wall:6.3f}s "
54
- f"cpu={end_cpu - start_cpu:6.3f}s bytes={total_bytes}"
55
- )
48
+ print(f"{name:12} wall={end_wall - start_wall:6.3f}s cpu={end_cpu - start_cpu:6.3f}s bytes={total_bytes}")
56
49
 
57
50
 
58
51
  def main() -> None:
@@ -0,0 +1,133 @@
1
+ [project]
2
+ name = "pyallel"
3
+ version = "2.1.0"
4
+ description = "Run and handle the output of multiple executables in pyallel (as in parallel)"
5
+ authors = [{ name = "Daniel Black", email = "danielcrblack@gmail.com" }]
6
+ requires-python = ">=3.8"
7
+ readme = "README.md"
8
+ license = "MIT"
9
+ keywords = [
10
+ "parallel",
11
+ "command",
12
+ "runner",
13
+ "executable",
14
+ "shell",
15
+ "terminal",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Programming Language :: Python :: 3",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: MacOS :: MacOS X",
22
+ "Operating System :: POSIX :: Linux",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/Danthewaann/pyallel"
27
+ Repository = "https://github.com/Danthewaann/pyallel"
28
+
29
+ [project.scripts]
30
+ pyallel = "pyallel.main:entry_point"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=7.4.3,<8",
35
+ "mypy>=1.7.0,<2",
36
+ "pyinstaller==6.11.0",
37
+ "ruff>=0.15.20",
38
+ ]
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.mypy]
45
+ strict = true
46
+ show_error_codes = true
47
+
48
+ [tool.pytest.ini_options]
49
+ addopts = "-vvv"
50
+
51
+ [tool.ruff]
52
+ # Allow lines to be as long as 120 columns
53
+ line-length = 120
54
+
55
+ [tool.ruff.lint]
56
+ # Ruff linting rules are listed within: https://beta.ruff.rs/docs/rules
57
+ #
58
+ # Globally enable all rules by default. We ignore ones we don't want below
59
+ select = [
60
+ "ALL"
61
+ ]
62
+ # Rules to ignore globally
63
+ ignore = [
64
+ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed
65
+ "BLE001", # Do not catch blind exceptions
66
+ "B904", # Within an except* clause, raise exceptions with raise ... from err or raise ... from None
67
+ "COM812", # Trailing comma missing
68
+ "COM819", # Trailing comma prohibited
69
+ "D1", # pydocstyle: undocumented code
70
+ "D203", # pydocstyle: 1 blank line required before class docstring
71
+ "D213", # pydocstyle: Multi-line docstring summary should start at the second line
72
+ "D401", # pydocstyle: First line of docstring should be in imperative mood
73
+ "D413", # pydocstyle: Missing blank line after last section
74
+ "E501", # Line too long
75
+ "EM", # flake8-errmsg: https://docs.astral.sh/ruff/rules/#flake8-errmsg-em
76
+ "FIX002", # Line contains TODO, consider resolving the issue
77
+ "PLR0912", # Too many branches
78
+ "S106", # Possible hardcoded password assigned to argument
79
+ "S603", # subprocess call: check for execution of untrusted input
80
+ "S604", # Function call with shell=True parameter identified, security issue
81
+ "T20", # flake8-print: https://docs.astral.sh/ruff/rules/#flake8-print-t20
82
+ "TD002", # Missing author in TODO
83
+ "TRY003", # Avoid specifying long messages outside the exception class
84
+ "TRY201", # Use raise without specifying exception name
85
+ "TRY400", # Use logging.exception instead of logging.error
86
+ "PLR0913", # Too many arguments in function definition (> 5)
87
+ ]
88
+
89
+ # Rules to ignore on a per file basis
90
+ [tool.ruff.lint.per-file-ignores]
91
+ "**/{tests}/*" = [
92
+ "D", # ignore pydocstyle docstring rules in tests
93
+ "FBT", # flake8-boolean-trap: https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt
94
+ "INP001", # File is part of an implicit namespace package. Add an __init__.py
95
+ "N801", # Class name should use CapWords convention
96
+ "PLR0913", # Too many arguments in function definition
97
+ "PLR2004", # Magic value used in comparison, consider replacing with a constant variable
98
+ "PT019", # Fixture without value is injected as parameter, use @pytest.mark.usefixtures instead
99
+ "S101", # Use of assert detected
100
+ "S104", # Possible binding to all interfaces
101
+ "SLF001", # Private member accessed
102
+ "S108", # Probable insecure usage of temporary file or directory
103
+ "S607", # Starting a process with a partial executable path
104
+ ]
105
+
106
+ # Linter plugin configurations
107
+ #
108
+ # From: https://docs.astral.sh/ruff/settings
109
+ [tool.ruff.lint.flake8-tidy-imports]
110
+ # Disallow all relative imports.
111
+ ban-relative-imports = "all"
112
+
113
+ [tool.ruff.lint.isort]
114
+ combine-as-imports = true
115
+ force-wrap-aliases = true
116
+ known-first-party = ["pizzabox", "tests"]
117
+
118
+ [tool.ruff.lint.mccabe]
119
+ # The maximum function complexity to allow before triggering C901 errors
120
+ max-complexity = 20
121
+
122
+ # Configure the ruff formatter
123
+ [tool.ruff.format]
124
+ # Like Black, use double quotes for strings
125
+ quote-style = "double"
126
+ # Like Black, indent with spaces, rather than tabs
127
+ indent-style = "space"
128
+ # Like Black, respect magic trailing commas
129
+ skip-magic-trailing-comma = false
130
+ # Like Black, automatically detect the appropriate line ending
131
+ line-ending = "auto"
132
+ # Enable reformatting of code snippets in docstrings
133
+ docstring-code-format = true
@@ -21,7 +21,7 @@ class Colours:
21
21
  def from_colour(cls, colour: Literal["yes", "no", "auto"]) -> Colours:
22
22
  colours = cls()
23
23
 
24
- if colour == "no" or colour == "auto" and not constants.IN_TTY:
24
+ if colour == "no" or (colour == "auto" and not constants.IN_TTY):
25
25
  for field in fields(colours):
26
26
  setattr(colours, field.name, "")
27
27
 
@@ -12,18 +12,18 @@ ANSI_ESCAPE = re.compile(r"(\x9B|\x1B\[|\x1B\()[0-?]*[ -\/]*[@-~]")
12
12
 
13
13
  if IN_TTY:
14
14
 
15
- def COLUMNS() -> int:
15
+ def columns() -> int:
16
16
  return shutil.get_terminal_size().columns
17
17
 
18
- def LINES() -> int:
18
+ def lines() -> int:
19
19
  return shutil.get_terminal_size().lines
20
20
 
21
21
  else:
22
22
 
23
- def COLUMNS() -> int:
23
+ def columns() -> int:
24
24
  return sys.maxsize
25
25
 
26
- def LINES() -> int:
26
+ def lines() -> int:
27
27
  return sys.maxsize
28
28
 
29
29
 
@@ -1,10 +1,10 @@
1
1
  class PyallelError(Exception):
2
- """Base error for issues raised by pyallel"""
2
+ """Base error for issues raised by pyallel."""
3
3
 
4
4
 
5
5
  class InvalidLinesModifierError(PyallelError):
6
- """Raised when the lines modifier is invalid"""
6
+ """Raised when the lines modifier is invalid."""
7
7
 
8
8
 
9
9
  class NoCommandsForProcessGroupError(PyallelError):
10
- """Raised when no commands have been provided for a process group"""
10
+ """Raised when no commands have been provided for a process group."""
@@ -0,0 +1,14 @@
1
+ import logging
2
+ import os
3
+
4
+
5
+ def configure_logging(*, debug: bool = False) -> None:
6
+ if debug:
7
+ logging.basicConfig(
8
+ filename="pyallel.log",
9
+ format="%(asctime)s:%(name)s:%(lineno)d:%(message)s",
10
+ datefmt="%Y-%m-%dT%H:%M:%S",
11
+ level=logging.DEBUG,
12
+ )
13
+ else:
14
+ logging.basicConfig(handlers=[logging.StreamHandler(stream=open(os.devnull, "w"))]) # noqa: PTH123, SIM115
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import importlib.metadata
4
+ import logging
4
5
  import sys
5
6
  import time
6
7
  import traceback
@@ -8,6 +9,7 @@ import traceback
8
9
  from pyallel import constants
9
10
  from pyallel.colours import Colours
10
11
  from pyallel.errors import PyallelError
12
+ from pyallel.logging import configure_logging
11
13
  from pyallel.parser import Arguments, create_parser
12
14
  from pyallel.printer import (
13
15
  InteractiveConsolePrinter,
@@ -16,6 +18,8 @@ from pyallel.printer import (
16
18
  )
17
19
  from pyallel.process_group_manager import ProcessGroupManager
18
20
 
21
+ logger = logging.getLogger(__name__)
22
+
19
23
 
20
24
  def entry_point(*args: str) -> int:
21
25
  args = args or tuple(sys.argv[1:])
@@ -31,6 +35,8 @@ def entry_point(*args: str) -> int:
31
35
  parser.print_help()
32
36
  return 2
33
37
 
38
+ configure_logging(debug=parsed_args.debug)
39
+
34
40
  colours = Colours.from_colour(parsed_args.colour)
35
41
  printer: Printer
36
42
  if not parsed_args.interactive or not constants.IN_TTY:
@@ -41,28 +47,27 @@ def entry_point(*args: str) -> int:
41
47
  try:
42
48
  process_group_manager = ProcessGroupManager.from_args(*parsed_args.commands)
43
49
  except PyallelError as e:
44
- print(f"{colours.red_bold}Error: {str(e)}{colours.reset_colour}")
50
+ print(f"{colours.red_bold}Error: {e!s}{colours.reset_colour}")
45
51
  return 1
46
52
 
53
+ logger.debug("starting run with arguments:\n%s", parsed_args)
47
54
  try:
48
55
  exit_code = run(process_group_manager, printer)
49
56
  except Exception:
50
- print(
51
- f"{colours.red_bold}Error: {traceback.format_exc()}{colours.reset_colour}"
52
- )
57
+ logger.exception("failed run with arguments:\n%s", parsed_args)
58
+ print(f"{colours.red_bold}Error: {traceback.format_exc()}{colours.reset_colour}")
53
59
  return 1
54
60
 
55
61
  if exit_code == 1:
62
+ logger.error("failed run with arguments:\n%s", parsed_args)
56
63
  process_group = process_group_manager.get_cur_process_group_output()
57
- print(
58
- f"\n{colours.red_bold}ERROR: the following commands failed{colours.reset_colour}"
59
- )
64
+ print(f"\n{colours.red_bold}ERROR: the following commands failed{colours.reset_colour}")
60
65
  for process_output in process_group.processes:
61
66
  process_poll = process_output.process.poll()
62
67
  if process_poll and process_poll > 0:
63
- print(
64
- f" {colours.red_bold}{process_output.process.command}{colours.reset_colour}"
65
- )
68
+ print(f" {colours.red_bold}{process_output.process.command}{colours.reset_colour}")
69
+ else:
70
+ logger.debug("finished run with arguments:\n%s", parsed_args)
66
71
 
67
72
  return exit_code
68
73
 
@@ -10,13 +10,16 @@ class Arguments:
10
10
  interactive: bool
11
11
  timer: bool
12
12
  version: bool
13
+ debug: bool
13
14
 
14
15
  def __repr__(self) -> str:
15
- msg = "Arguments:\n"
16
+ msg = ""
16
17
  padding = len(sorted(self.__dict__.keys(), key=len, reverse=True)[0]) + 1
17
18
  for field, value in self.__dict__.items():
18
- msg += f" {field: <{padding}}: {value}\n"
19
- return msg
19
+ if field == "commands":
20
+ value = " ".join(value) # noqa: PLW2901
21
+ msg += f"{field: <{padding}}: {value}\n"
22
+ return msg.strip()
20
23
 
21
24
 
22
25
  DESCRIPTION = r"""run and handle the output of multiple executables in %(prog)s (as in parallel)
@@ -24,11 +27,11 @@ DESCRIPTION = r"""run and handle the output of multiple executables in %(prog)s
24
27
  RUNNING COMMANDS
25
28
  ================
26
29
  to run multiple commands you must separate them using the command separator symbol (::)
27
-
30
+
28
31
  %(prog)s mypy . :: black .
29
32
 
30
- if you want to provide options to a command you need to use the double dash symbol (--) to indicate that
31
- any options provided after this symbol should not be interpreted by %(prog)s
33
+ if you want to provide options to a command you need to use the double dash symbol (--) to indicate that
34
+ any options provided after this symbol should not be interpreted by %(prog)s
32
35
 
33
36
  %(prog)s -n -- mypy -V :: black --version
34
37
 
@@ -45,7 +48,7 @@ modifiers can also be set for commands to augment their behaviour using the comm
45
48
 
46
49
  lines (only used in interactive mode):
47
50
  the lines modifier allows you to specify how many lines the command output can take up on the screen
48
-
51
+
49
52
  %(prog)s lines=90 :::: echo running long command... :: echo running other command...
50
53
 
51
54
  90 is expressed as a percentage value, which must be between 1 and 100 inclusive
@@ -54,7 +57,7 @@ SHELL SYNTAX
54
57
  ============
55
58
  each command is executed inside its own shell, this means shell syntax is supported.
56
59
  it is important to note that certain shell syntax must be escaped using backslashes (\)
57
- or wrapped in single quotes (''), otherwise it will be evaluated in your current
60
+ or wrapped in single quotes (''), otherwise it will be evaluated in your current
58
61
  shell immediately instead of the shell that your command will run within.
59
62
 
60
63
  some examples of using shell syntax are below (single quotes are used only if required)
@@ -108,5 +111,11 @@ def create_parser() -> ArgumentParser:
108
111
  choices=("yes", "no", "auto"),
109
112
  default="auto",
110
113
  )
114
+ parser.add_argument(
115
+ "--debug",
116
+ help='enable debug mode, which logs debug info to a "pyallel.log" file in the current directory',
117
+ action="store_true",
118
+ default=False,
119
+ )
111
120
 
112
121
  return parser