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.
Files changed (37) hide show
  1. pyallel-2.0.6/.vim/project.shada +0 -0
  2. {pyallel-2.0.5 → pyallel-2.0.6}/PKG-INFO +1 -1
  3. {pyallel-2.0.5 → pyallel-2.0.6}/benchmark_process.py +3 -10
  4. pyallel-2.0.6/pyproject.toml +133 -0
  5. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/colours.py +1 -1
  6. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/constants.py +4 -4
  7. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/errors.py +3 -3
  8. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/main.py +16 -11
  9. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/parser.py +5 -5
  10. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/printer.py +51 -56
  11. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/process.py +12 -17
  12. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/process_group.py +7 -12
  13. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/process_group_manager.py +11 -11
  14. {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_main.py +45 -91
  15. {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_printer.py +17 -19
  16. {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_process.py +1 -3
  17. {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_process_group.py +7 -16
  18. {pyallel-2.0.5 → pyallel-2.0.6}/tests/test_process_manager.py +10 -17
  19. {pyallel-2.0.5 → pyallel-2.0.6}/uv.lock +28 -1
  20. pyallel-2.0.5/.vim/project.shada +0 -0
  21. pyallel-2.0.5/pyproject.toml +0 -48
  22. {pyallel-2.0.5 → pyallel-2.0.6}/.base_branch +0 -0
  23. {pyallel-2.0.5 → pyallel-2.0.6}/.github/workflows/build.yml +0 -0
  24. {pyallel-2.0.5 → pyallel-2.0.6}/.github/workflows/test.yml +0 -0
  25. {pyallel-2.0.5 → pyallel-2.0.6}/.gitignore +0 -0
  26. {pyallel-2.0.5 → pyallel-2.0.6}/Dockerfile +0 -0
  27. {pyallel-2.0.5 → pyallel-2.0.6}/Dockerfile.alpine +0 -0
  28. {pyallel-2.0.5 → pyallel-2.0.6}/LICENSE +0 -0
  29. {pyallel-2.0.5 → pyallel-2.0.6}/README.md +0 -0
  30. {pyallel-2.0.5 → pyallel-2.0.6}/bin/ldd +0 -0
  31. {pyallel-2.0.5 → pyallel-2.0.6}/bin/pyinstaller.sh +0 -0
  32. {pyallel-2.0.5 → pyallel-2.0.6}/build.sh +0 -0
  33. {pyallel-2.0.5 → pyallel-2.0.6}/build_all.sh +0 -0
  34. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/__init__.py +0 -0
  35. {pyallel-2.0.5 → pyallel-2.0.6}/src/pyallel/py.typed +0 -0
  36. {pyallel-2.0.5 → pyallel-2.0.6}/tests/assets/test_handle_multiple_signals.sh +0 -0
  37. {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.5
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 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."""
@@ -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: {str(e)}{colours.reset_colour}")
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
- if process_group_manager.stream().has_output():
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
- from pyallel.process import Process, ProcessOutput
9
- from pyallel.process_group import ProcessGroupOutput
10
- from pyallel.process_group_manager import ProcessGroupManager
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.COLUMNS()
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.COLUMNS()
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.LINES() - 1
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.COLUMNS()
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.COLUMNS()
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._interrupt_count)
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._interrupt_count, tail_output=False
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.COLUMNS()
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
- line, include_prefix, end, truncate=tail_output, columns=columns
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("", end="", flush=True)
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
- output = process_group_manager.get_process(output.id)
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("", end="", flush=True)
436
+ print(end="", flush=True)