pyallel 2.0.5__tar.gz → 2.0.6__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-2.0.6/.vim/project.shada +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/PKG-INFO +1 -1
- {pyallel-2.0.5 → pyallel-2.0.6}/benchmark_process.py +3 -10
- pyallel-2.0.6/pyproject.toml +133 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/colours.py +1 -1
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/constants.py +4 -4
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/errors.py +3 -3
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/main.py +16 -11
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/parser.py +5 -5
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/printer.py +51 -56
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/process.py +12 -17
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/process_group.py +7 -12
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/process_group_manager.py +11 -11
- {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_main.py +45 -91
- {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_printer.py +17 -19
- {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_process.py +1 -3
- {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_process_group.py +7 -16
- {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_process_manager.py +10 -17
- {pyallel-2.0.5 → pyallel-2.0.6}/uv.lock +28 -1
- pyallel-2.0.5/.vim/project.shada +0 -0
- pyallel-2.0.5/pyproject.toml +0 -48
- {pyallel-2.0.5 → pyallel-2.0.6}/.base_branch +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/.github/workflows/build.yml +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/.github/workflows/test.yml +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/.gitignore +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/Dockerfile +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/Dockerfile.alpine +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/LICENSE +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/README.md +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/bin/ldd +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/bin/pyinstaller.sh +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/build.sh +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/build_all.sh +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/__init__.py +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/py.typed +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/tests/assets/test_handle_multiple_signals.sh +0 -0
- {pyallel-2.0.5 → pyallel-2.0.6}/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.
|
|
3
|
+
Version: 2.0.6
|
|
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
|
|
@@ -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.0.6"
|
|
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
|
|
15
|
+
def columns() -> int:
|
|
16
16
|
return shutil.get_terminal_size().columns
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def lines() -> int:
|
|
19
19
|
return shutil.get_terminal_size().lines
|
|
20
20
|
|
|
21
21
|
else:
|
|
22
22
|
|
|
23
|
-
def
|
|
23
|
+
def columns() -> int:
|
|
24
24
|
return sys.maxsize
|
|
25
25
|
|
|
26
|
-
def
|
|
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."""
|
|
@@ -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
|
|
@@ -16,6 +17,15 @@ from pyallel.printer import (
|
|
|
16
17
|
)
|
|
17
18
|
from pyallel.process_group_manager import ProcessGroupManager
|
|
18
19
|
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
filename="pyallel.log",
|
|
22
|
+
format="%(asctime)s:%(levelname)s:%(name)s:%(lineno)d[%(threadName)s]:%(message)s",
|
|
23
|
+
datefmt="%Y-%m-%dT%H:%M:%S",
|
|
24
|
+
level=logging.DEBUG,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
19
29
|
|
|
20
30
|
def entry_point(*args: str) -> int:
|
|
21
31
|
args = args or tuple(sys.argv[1:])
|
|
@@ -41,28 +51,22 @@ def entry_point(*args: str) -> int:
|
|
|
41
51
|
try:
|
|
42
52
|
process_group_manager = ProcessGroupManager.from_args(*parsed_args.commands)
|
|
43
53
|
except PyallelError as e:
|
|
44
|
-
print(f"{colours.red_bold}Error: {
|
|
54
|
+
print(f"{colours.red_bold}Error: {e!s}{colours.reset_colour}")
|
|
45
55
|
return 1
|
|
46
56
|
|
|
47
57
|
try:
|
|
48
58
|
exit_code = run(process_group_manager, printer)
|
|
49
59
|
except Exception:
|
|
50
|
-
print(
|
|
51
|
-
f"{colours.red_bold}Error: {traceback.format_exc()}{colours.reset_colour}"
|
|
52
|
-
)
|
|
60
|
+
print(f"{colours.red_bold}Error: {traceback.format_exc()}{colours.reset_colour}")
|
|
53
61
|
return 1
|
|
54
62
|
|
|
55
63
|
if exit_code == 1:
|
|
56
64
|
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
|
-
)
|
|
65
|
+
print(f"\n{colours.red_bold}ERROR: the following commands failed{colours.reset_colour}")
|
|
60
66
|
for process_output in process_group.processes:
|
|
61
67
|
process_poll = process_output.process.poll()
|
|
62
68
|
if process_poll and process_poll > 0:
|
|
63
|
-
print(
|
|
64
|
-
f" {colours.red_bold}{process_output.process.command}{colours.reset_colour}"
|
|
65
|
-
)
|
|
69
|
+
print(f" {colours.red_bold}{process_output.process.command}{colours.reset_colour}")
|
|
66
70
|
|
|
67
71
|
return exit_code
|
|
68
72
|
|
|
@@ -77,7 +81,8 @@ def run(process_group_manager: ProcessGroupManager, printer: Printer) -> int:
|
|
|
77
81
|
if poll is not None:
|
|
78
82
|
# If we still have new output to print after the process group manager has completed,
|
|
79
83
|
# make sure to print it here before continuing
|
|
80
|
-
|
|
84
|
+
has_output = process_group_manager.stream().has_output()
|
|
85
|
+
if has_output:
|
|
81
86
|
printer.print(process_group_manager)
|
|
82
87
|
|
|
83
88
|
if poll > 0:
|
|
@@ -24,11 +24,11 @@ DESCRIPTION = r"""run and handle the output of multiple executables in %(prog)s
|
|
|
24
24
|
RUNNING COMMANDS
|
|
25
25
|
================
|
|
26
26
|
to run multiple commands you must separate them using the command separator symbol (::)
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
%(prog)s mypy . :: black .
|
|
29
29
|
|
|
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
|
|
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
|
|
32
32
|
|
|
33
33
|
%(prog)s -n -- mypy -V :: black --version
|
|
34
34
|
|
|
@@ -45,7 +45,7 @@ modifiers can also be set for commands to augment their behaviour using the comm
|
|
|
45
45
|
|
|
46
46
|
lines (only used in interactive mode):
|
|
47
47
|
the lines modifier allows you to specify how many lines the command output can take up on the screen
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
%(prog)s lines=90 :::: echo running long command... :: echo running other command...
|
|
50
50
|
|
|
51
51
|
90 is expressed as a percentage value, which must be between 1 and 100 inclusive
|
|
@@ -54,7 +54,7 @@ SHELL SYNTAX
|
|
|
54
54
|
============
|
|
55
55
|
each command is executed inside its own shell, this means shell syntax is supported.
|
|
56
56
|
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
|
|
57
|
+
or wrapped in single quotes (''), otherwise it will be evaluated in your current
|
|
58
58
|
shell immediately instead of the shell that your command will run within.
|
|
59
59
|
|
|
60
60
|
some examples of using shell syntax are below (single quotes are used only if required)
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from typing import Protocol
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol
|
|
5
5
|
|
|
6
6
|
from pyallel import constants
|
|
7
7
|
from pyallel.colours import Colours
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from pyallel.
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pyallel.process import Process, ProcessOutput
|
|
11
|
+
from pyallel.process_group import ProcessGroupOutput
|
|
12
|
+
from pyallel.process_group_manager import ProcessGroupManager
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class Printer(Protocol):
|
|
@@ -20,7 +22,7 @@ class Printer(Protocol):
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class ConsolePrinter:
|
|
23
|
-
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
25
|
+
def __init__(self, colours: Colours | None = None, *, timer: bool = False) -> None:
|
|
24
26
|
self._colours = colours or Colours()
|
|
25
27
|
self._timer = timer
|
|
26
28
|
self._prefix = f"{self._colours.dim_on}=>{self._colours.dim_off} "
|
|
@@ -30,6 +32,7 @@ class ConsolePrinter:
|
|
|
30
32
|
def write(
|
|
31
33
|
self,
|
|
32
34
|
line: str,
|
|
35
|
+
*,
|
|
33
36
|
include_prefix: bool = False,
|
|
34
37
|
end: str = "\n",
|
|
35
38
|
flush: bool = False,
|
|
@@ -38,7 +41,7 @@ class ConsolePrinter:
|
|
|
38
41
|
) -> None:
|
|
39
42
|
truncate_num = 0
|
|
40
43
|
prefix = self._prefix if include_prefix else ""
|
|
41
|
-
columns = columns or constants.
|
|
44
|
+
columns = columns or constants.columns()
|
|
42
45
|
if prefix:
|
|
43
46
|
truncate_num = 6
|
|
44
47
|
if prefix and truncate:
|
|
@@ -50,6 +53,7 @@ class ConsolePrinter:
|
|
|
50
53
|
def generate_process_output(
|
|
51
54
|
self,
|
|
52
55
|
output: ProcessOutput,
|
|
56
|
+
*,
|
|
53
57
|
tail_output: bool = False,
|
|
54
58
|
include_cmd: bool = True,
|
|
55
59
|
include_output: bool = True,
|
|
@@ -65,7 +69,7 @@ class ConsolePrinter:
|
|
|
65
69
|
|
|
66
70
|
if include_cmd:
|
|
67
71
|
status = self.generate_process_output_status(
|
|
68
|
-
output, include_progress, include_timer
|
|
72
|
+
output, include_progress=include_progress, include_timer=include_timer
|
|
69
73
|
)
|
|
70
74
|
line_parts = (False, status, "\n")
|
|
71
75
|
out.append(line_parts)
|
|
@@ -76,10 +80,7 @@ class ConsolePrinter:
|
|
|
76
80
|
|
|
77
81
|
if tail_output:
|
|
78
82
|
output_lines = output.process.lines - 1
|
|
79
|
-
if output_lines == 0:
|
|
80
|
-
lines = []
|
|
81
|
-
else:
|
|
82
|
-
lines = lines[-output_lines:]
|
|
83
|
+
lines = [] if output_lines == 0 else lines[-output_lines:]
|
|
83
84
|
|
|
84
85
|
for line in lines:
|
|
85
86
|
prefix = True
|
|
@@ -87,7 +88,7 @@ class ConsolePrinter:
|
|
|
87
88
|
if append_newlines and end != "\n":
|
|
88
89
|
end = "\n"
|
|
89
90
|
else:
|
|
90
|
-
line = line[:-1]
|
|
91
|
+
line = line[:-1] # noqa: PLW2901
|
|
91
92
|
|
|
92
93
|
try:
|
|
93
94
|
prev_line = self._to_print[-1]
|
|
@@ -106,12 +107,13 @@ class ConsolePrinter:
|
|
|
106
107
|
def generate_process_output_status(
|
|
107
108
|
self,
|
|
108
109
|
output: ProcessOutput,
|
|
110
|
+
*,
|
|
109
111
|
include_progress: bool = True,
|
|
110
112
|
include_timer: bool | None = None,
|
|
111
113
|
columns: int | None = None,
|
|
112
114
|
) -> str:
|
|
113
115
|
include_timer = include_timer if include_timer is not None else self._timer
|
|
114
|
-
columns = columns or constants.
|
|
116
|
+
columns = columns or constants.columns()
|
|
115
117
|
|
|
116
118
|
passed = None
|
|
117
119
|
icon = ""
|
|
@@ -159,13 +161,14 @@ class ConsolePrinter:
|
|
|
159
161
|
def generate_process_group_output(
|
|
160
162
|
self,
|
|
161
163
|
output: ProcessGroupOutput,
|
|
164
|
+
*,
|
|
162
165
|
interrupt_count: int = 0,
|
|
163
166
|
tail_output: bool = True,
|
|
164
167
|
) -> list[tuple[bool, str, str]]:
|
|
165
168
|
self.set_process_lines(output, interrupt_count)
|
|
166
169
|
|
|
167
170
|
for out in output.processes:
|
|
168
|
-
self.generate_process_output(out, tail_output, append_newlines=True)
|
|
171
|
+
self.generate_process_output(out, tail_output=tail_output, append_newlines=True)
|
|
169
172
|
|
|
170
173
|
if interrupt_count == 1:
|
|
171
174
|
self._to_print.append((False, "", "\n"))
|
|
@@ -176,7 +179,7 @@ class ConsolePrinter:
|
|
|
176
179
|
"\n",
|
|
177
180
|
)
|
|
178
181
|
)
|
|
179
|
-
elif interrupt_count == 2:
|
|
182
|
+
elif interrupt_count == 2: # noqa: PLR2004
|
|
180
183
|
self._to_print.append((False, "", "\n"))
|
|
181
184
|
self._to_print.append(
|
|
182
185
|
(
|
|
@@ -198,7 +201,7 @@ class ConsolePrinter:
|
|
|
198
201
|
interrupt_count: int = 0,
|
|
199
202
|
lines: int = 0,
|
|
200
203
|
) -> None:
|
|
201
|
-
lines = lines or constants.
|
|
204
|
+
lines = lines or constants.lines() - 1
|
|
202
205
|
if interrupt_count:
|
|
203
206
|
lines -= 2
|
|
204
207
|
|
|
@@ -212,9 +215,7 @@ class ConsolePrinter:
|
|
|
212
215
|
processes_with_dynamic_lines.append(process_output)
|
|
213
216
|
continue
|
|
214
217
|
|
|
215
|
-
process_output.process.lines = int(
|
|
216
|
-
lines * process_output.process.percentage_lines
|
|
217
|
-
)
|
|
218
|
+
process_output.process.lines = int(lines * process_output.process.percentage_lines)
|
|
218
219
|
used_lines += process_output.process.lines
|
|
219
220
|
|
|
220
221
|
# Remove the used lines from the total available lines
|
|
@@ -269,7 +270,7 @@ class ConsolePrinter:
|
|
|
269
270
|
|
|
270
271
|
def get_num_lines(self, line: str, columns: int | None = None) -> int:
|
|
271
272
|
lines = 0
|
|
272
|
-
columns = columns or constants.
|
|
273
|
+
columns = columns or constants.columns()
|
|
273
274
|
line = constants.ANSI_ESCAPE.sub("", line)
|
|
274
275
|
length = len(line)
|
|
275
276
|
line_lines = 1
|
|
@@ -282,7 +283,7 @@ class ConsolePrinter:
|
|
|
282
283
|
return lines
|
|
283
284
|
|
|
284
285
|
def truncate_line(self, line: str, columns: int | None = None) -> str:
|
|
285
|
-
columns = columns or constants.
|
|
286
|
+
columns = columns or constants.columns()
|
|
286
287
|
escaped_line = constants.ANSI_ESCAPE.sub("", line)
|
|
287
288
|
return "".join(escaped_line[:columns]) + "..."
|
|
288
289
|
|
|
@@ -294,31 +295,32 @@ class ConsolePrinter:
|
|
|
294
295
|
|
|
295
296
|
|
|
296
297
|
class InteractiveConsolePrinter(ConsolePrinter):
|
|
297
|
-
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
298
|
-
super().__init__(colours, timer)
|
|
298
|
+
def __init__(self, colours: Colours | None = None, *, timer: bool = False) -> None:
|
|
299
|
+
super().__init__(colours, timer=timer)
|
|
299
300
|
self._last_printed: list[tuple[bool, str, str]] = []
|
|
300
301
|
|
|
301
302
|
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
302
303
|
output = process_group_manager.get_cur_process_group_output()
|
|
303
|
-
self.print_process_group_output(output, process_group_manager.
|
|
304
|
+
self.print_process_group_output(output, interrupt_count=process_group_manager.interrupt_count)
|
|
304
305
|
|
|
305
306
|
poll = process_group_manager.poll()
|
|
306
307
|
if poll is not None:
|
|
307
308
|
self.clear_last_printed_lines()
|
|
308
309
|
self.reset()
|
|
309
310
|
self.print_process_group_output(
|
|
310
|
-
output, process_group_manager.
|
|
311
|
+
output, interrupt_count=process_group_manager.interrupt_count, tail_output=False
|
|
311
312
|
)
|
|
312
313
|
self.reset()
|
|
313
314
|
|
|
314
315
|
def print_process_group_output(
|
|
315
316
|
self,
|
|
316
317
|
output: ProcessGroupOutput,
|
|
318
|
+
*,
|
|
317
319
|
interrupt_count: int = 0,
|
|
318
320
|
tail_output: bool = True,
|
|
319
321
|
) -> None:
|
|
320
|
-
columns = constants.
|
|
321
|
-
self.generate_process_group_output(output, interrupt_count, tail_output)
|
|
322
|
+
columns = constants.columns()
|
|
323
|
+
self.generate_process_group_output(output, interrupt_count=interrupt_count, tail_output=tail_output)
|
|
322
324
|
|
|
323
325
|
num_lines_to_print = len(self._to_print)
|
|
324
326
|
num_last_printed_lines = len(self._last_printed)
|
|
@@ -327,9 +329,7 @@ class InteractiveConsolePrinter(ConsolePrinter):
|
|
|
327
329
|
# we just print all the new lines
|
|
328
330
|
if not num_last_printed_lines or not tail_output:
|
|
329
331
|
for include_prefix, line, end in self._to_print:
|
|
330
|
-
self.write(
|
|
331
|
-
line, include_prefix, end, truncate=tail_output, columns=columns
|
|
332
|
-
)
|
|
332
|
+
self.write(line, include_prefix=include_prefix, end=end, truncate=tail_output, columns=columns)
|
|
333
333
|
else:
|
|
334
334
|
# Compare the number of last lines and new lines and only update what has changed.
|
|
335
335
|
#
|
|
@@ -337,9 +337,7 @@ class InteractiveConsolePrinter(ConsolePrinter):
|
|
|
337
337
|
# comparing the last printed lines with the new lines that were generated
|
|
338
338
|
print(f"\033[{num_last_printed_lines}A", end="")
|
|
339
339
|
cursor_line = 0
|
|
340
|
-
for cur_line, line_parts in enumerate(
|
|
341
|
-
self._last_printed[:num_lines_to_print]
|
|
342
|
-
):
|
|
340
|
+
for cur_line, line_parts in enumerate(self._last_printed[:num_lines_to_print]):
|
|
343
341
|
# If the current line is not the same as it's newly generated version, we update the line
|
|
344
342
|
if line_parts[1] != self._to_print[cur_line][1]:
|
|
345
343
|
include_prefix, line, end = self._to_print[cur_line]
|
|
@@ -350,9 +348,7 @@ class InteractiveConsolePrinter(ConsolePrinter):
|
|
|
350
348
|
# Clear the current line
|
|
351
349
|
print(f"{constants.CLEAR_LINE}\r", end="")
|
|
352
350
|
# Write the new line, this will move the cursor to the next line automatically
|
|
353
|
-
self.write(
|
|
354
|
-
line, include_prefix, end, truncate=tail_output, columns=columns
|
|
355
|
-
)
|
|
351
|
+
self.write(line, include_prefix=include_prefix, end=end, truncate=tail_output, columns=columns)
|
|
356
352
|
# Need to set the cursor_line to be the current line + 1 as the above write
|
|
357
353
|
# will move the cursor to the next line
|
|
358
354
|
cursor_line = cur_line + 1
|
|
@@ -366,17 +362,18 @@ class InteractiveConsolePrinter(ConsolePrinter):
|
|
|
366
362
|
# Just print the new lines as normal
|
|
367
363
|
for line_parts in self._to_print[num_last_printed_lines:]:
|
|
368
364
|
include_prefix, line, end = line_parts
|
|
369
|
-
self.write(
|
|
370
|
-
|
|
371
|
-
|
|
365
|
+
self.write(line, include_prefix=include_prefix, end=end, truncate=tail_output, columns=columns)
|
|
366
|
+
elif num_last_printed_lines > num_lines_to_print:
|
|
367
|
+
# Make sure to clear the remaining last printed lines at the end of the screen so they don't get left behind
|
|
368
|
+
print("\033[0J", end="")
|
|
372
369
|
else:
|
|
373
|
-
# Jump to the end of the output
|
|
370
|
+
# Jump to the end of the output since the num of lines printed hasn't changed
|
|
374
371
|
lines_to_jump = num_lines_to_print - cursor_line
|
|
375
372
|
if lines_to_jump:
|
|
376
373
|
print(f"\033[{lines_to_jump}B\r", end="")
|
|
377
374
|
|
|
378
375
|
# Force a flush to return the cursor to the bottom immediately
|
|
379
|
-
print(
|
|
376
|
+
print(end="", flush=True)
|
|
380
377
|
|
|
381
378
|
self._last_printed = self._to_print.copy()
|
|
382
379
|
self._to_print.clear()
|
|
@@ -384,8 +381,7 @@ class InteractiveConsolePrinter(ConsolePrinter):
|
|
|
384
381
|
def clear_last_printed_lines(self) -> None:
|
|
385
382
|
# Clear all the lines that were just printed
|
|
386
383
|
print(
|
|
387
|
-
f"{constants.CLEAR_LINE}{constants.UP_LINE}{constants.CLEAR_LINE}"
|
|
388
|
-
* len(self._last_printed),
|
|
384
|
+
f"{constants.CLEAR_LINE}{constants.UP_LINE}{constants.CLEAR_LINE}" * len(self._last_printed),
|
|
389
385
|
end="",
|
|
390
386
|
)
|
|
391
387
|
|
|
@@ -395,8 +391,8 @@ class InteractiveConsolePrinter(ConsolePrinter):
|
|
|
395
391
|
|
|
396
392
|
|
|
397
393
|
class NonInteractiveConsolePrinter(ConsolePrinter):
|
|
398
|
-
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
399
|
-
super().__init__(colours, timer)
|
|
394
|
+
def __init__(self, colours: Colours | None = None, *, timer: bool = False) -> None:
|
|
395
|
+
super().__init__(colours, timer=timer)
|
|
400
396
|
self._current_process: Process | None = None
|
|
401
397
|
|
|
402
398
|
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
@@ -405,10 +401,8 @@ class NonInteractiveConsolePrinter(ConsolePrinter):
|
|
|
405
401
|
for output in pg.processes:
|
|
406
402
|
if self._current_process is None:
|
|
407
403
|
self._current_process = output.process
|
|
408
|
-
|
|
409
|
-
self.print_process_output(
|
|
410
|
-
output, include_progress=False, include_timer=False
|
|
411
|
-
)
|
|
404
|
+
process_output = process_group_manager.get_process(output.id)
|
|
405
|
+
self.print_process_output(process_output, include_progress=False, include_timer=False)
|
|
412
406
|
elif self._current_process is not output.process:
|
|
413
407
|
continue
|
|
414
408
|
else:
|
|
@@ -421,6 +415,7 @@ class NonInteractiveConsolePrinter(ConsolePrinter):
|
|
|
421
415
|
def print_process_output(
|
|
422
416
|
self,
|
|
423
417
|
output: ProcessOutput,
|
|
418
|
+
*,
|
|
424
419
|
tail_output: bool = False,
|
|
425
420
|
include_cmd: bool = True,
|
|
426
421
|
include_output: bool = True,
|
|
@@ -429,13 +424,13 @@ class NonInteractiveConsolePrinter(ConsolePrinter):
|
|
|
429
424
|
) -> None:
|
|
430
425
|
for include_prefix, line, end in self.generate_process_output(
|
|
431
426
|
output,
|
|
432
|
-
tail_output,
|
|
433
|
-
include_cmd,
|
|
434
|
-
include_output,
|
|
435
|
-
include_progress,
|
|
436
|
-
include_timer,
|
|
427
|
+
tail_output=tail_output,
|
|
428
|
+
include_cmd=include_cmd,
|
|
429
|
+
include_output=include_output,
|
|
430
|
+
include_progress=include_progress,
|
|
431
|
+
include_timer=include_timer,
|
|
437
432
|
):
|
|
438
|
-
self.write(line, include_prefix, end)
|
|
433
|
+
self.write(line, include_prefix=include_prefix, end=end)
|
|
439
434
|
|
|
440
435
|
# Force a flush otherwise lines that don't end in a newline character will not get printed as they are read
|
|
441
|
-
print(
|
|
436
|
+
print(end="", flush=True)
|