Skip to content

Commit 7e4f1b9

Browse files
committed
Fix CLI and platform-specific bugs
__main__.py: pass the configured widgets to the bar (E2), track progress in bytes in line mode (E1), suppress BrokenPipeError for early-closing pipes (E3), keep known-size mode for empty files (E8). posix.py: getch() falls back to a plain read when stdin is not a tty (E6). windows.py: explicit argtypes on all kernel32 bindings, invalid console handles are detected and skipped, getch() honors the number of events read and survives non-ASCII keys (E4, E5). base.py: remove the Python 2 era __cmp__/__nonzero__ dead code (E7).
1 parent bae590d commit 7e4f1b9

4 files changed

Lines changed: 80 additions & 29 deletions

File tree

‎progressbar/__main__.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,8 @@ def main(argv: list[str] | None = None) -> None: # noqa: C901
342342

343343
# Initialize the progress bar
344344
bar = progressbar.ProgressBar(
345-
# widgets=widgets,
346-
max_value=total_size or None,
345+
widgets=widgets,
346+
max_value=total_size if filesize_available else None,
347347
max_error=False,
348348
)
349349

@@ -354,7 +354,7 @@ def main(argv: list[str] | None = None) -> None: # noqa: C901
354354
total_transferred = 0
355355

356356
bar.start()
357-
with contextlib.suppress(KeyboardInterrupt):
357+
with contextlib.suppress(KeyboardInterrupt, BrokenPipeError):
358358
for input_path in input_paths:
359359
if isinstance(input_path, pathlib.Path):
360360
input_stream = stack.enter_context(
@@ -374,7 +374,18 @@ def main(argv: list[str] | None = None) -> None: # noqa: C901
374374
break
375375

376376
output_stream.write(data)
377-
total_transferred += len(data)
377+
if isinstance(data, str):
378+
# The total size is measured in bytes, so progress
379+
# must be tracked in bytes as well
380+
encoding = (
381+
getattr(input_stream, 'encoding', None) or 'utf-8'
382+
)
383+
total_transferred += len(
384+
data.encode(encoding, errors='replace'),
385+
)
386+
else:
387+
total_transferred += len(data)
388+
378389
bar.update(total_transferred)
379390

380391
bar.finish(dirty=True)

‎progressbar/base.py‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import typing
43
from typing import IO, TextIO
54

65

@@ -9,12 +8,6 @@ class FalseMeta(type):
98
def __bool__(cls) -> bool: # pragma: no cover
109
return False
1110

12-
@classmethod
13-
def __cmp__(cls, other: typing.Any) -> int: # pragma: no cover
14-
return -1
15-
16-
__nonzero__ = __bool__
17-
1811

1912
class UnknownLength(metaclass=FalseMeta):
2013
pass

‎progressbar/terminal/os_specific/posix.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55

66
def getch() -> str:
7+
if not sys.stdin.isatty():
8+
# Raw mode is unavailable (and unnecessary) without a tty
9+
return sys.stdin.read(1)
10+
711
fd = sys.stdin.fileno()
812
old_settings = termios.tcgetattr(fd) # type: ignore
913
try:

‎progressbar/terminal/os_specific/windows.py‎

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525

2626
_STD_INPUT_HANDLE = _DWORD(-10)
2727
_STD_OUTPUT_HANDLE = _DWORD(-11)
28+
# GetStdHandle returns INVALID_HANDLE_VALUE (-1) when no console is
29+
# attached (piped output, pythonw, services)
30+
_INVALID_HANDLE_VALUE = _HANDLE(-1).value
31+
32+
33+
def _valid_handle(handle) -> bool:
34+
return handle is not None and handle != _INVALID_HANDLE_VALUE
2835

2936

3037
class WindowsConsoleModeFlags(enum.IntFlag):
@@ -48,25 +55,33 @@ def __str__(self) -> str:
4855
return f'{self.name} (0x{self.value:04X})'
4956

5057

58+
# Explicit argtypes are required: without them ctypes passes arguments
59+
# as 32-bit C ints, silently truncating 64-bit HANDLE values
5160
_GetConsoleMode = _kernel32.GetConsoleMode
61+
_GetConsoleMode.argtypes = (_HANDLE, ctypes.POINTER(_DWORD))
5262
_GetConsoleMode.restype = _BOOL
5363

5464
_SetConsoleMode = _kernel32.SetConsoleMode
65+
_SetConsoleMode.argtypes = (_HANDLE, _DWORD)
5566
_SetConsoleMode.restype = _BOOL
5667

5768
_GetStdHandle = _kernel32.GetStdHandle
69+
_GetStdHandle.argtypes = (_DWORD,)
5870
_GetStdHandle.restype = _HANDLE
5971

60-
_ReadConsoleInput = _kernel32.ReadConsoleInputA
61-
_ReadConsoleInput.restype = _BOOL
72+
_SetConsoleTextAttribute = _kernel32.SetConsoleTextAttribute
73+
_SetConsoleTextAttribute.argtypes = (_HANDLE, _WORD)
74+
_SetConsoleTextAttribute.restype = _BOOL
6275

6376
_h_console_input = _GetStdHandle(_STD_INPUT_HANDLE)
6477
_input_mode = _DWORD()
65-
_GetConsoleMode(_HANDLE(_h_console_input), ctypes.byref(_input_mode))
78+
if _valid_handle(_h_console_input):
79+
_GetConsoleMode(_HANDLE(_h_console_input), ctypes.byref(_input_mode))
6680

6781
_h_console_output = _GetStdHandle(_STD_OUTPUT_HANDLE)
6882
_output_mode = _DWORD()
69-
_GetConsoleMode(_HANDLE(_h_console_output), ctypes.byref(_output_mode))
83+
if _valid_handle(_h_console_output):
84+
_GetConsoleMode(_HANDLE(_h_console_output), ctypes.byref(_output_mode))
7085

7186

7287
class _COORD(ctypes.Structure):
@@ -121,17 +136,34 @@ class _Event(ctypes.Union):
121136
_fields_ = (('EventType', _WORD), ('Event', _Event))
122137

123138

139+
_ReadConsoleInput = _kernel32.ReadConsoleInputA
140+
_ReadConsoleInput.argtypes = (
141+
_HANDLE,
142+
ctypes.POINTER(_INPUT_RECORD),
143+
_DWORD,
144+
ctypes.POINTER(_DWORD),
145+
)
146+
_ReadConsoleInput.restype = _BOOL
147+
148+
124149
def reset_console_mode() -> None:
125-
_SetConsoleMode(_HANDLE(_h_console_input), _DWORD(_input_mode.value))
126-
_SetConsoleMode(_HANDLE(_h_console_output), _DWORD(_output_mode.value))
150+
if _valid_handle(_h_console_input):
151+
_SetConsoleMode(_HANDLE(_h_console_input), _DWORD(_input_mode.value))
152+
153+
if _valid_handle(_h_console_output):
154+
_SetConsoleMode(_HANDLE(_h_console_output), _DWORD(_output_mode.value))
127155

128156

129157
def set_console_mode() -> bool:
130-
mode = (
131-
_input_mode.value
132-
| WindowsConsoleModeFlags.ENABLE_VIRTUAL_TERMINAL_INPUT
133-
)
134-
_SetConsoleMode(_HANDLE(_h_console_input), _DWORD(mode))
158+
if not _valid_handle(_h_console_output):
159+
return False
160+
161+
if _valid_handle(_h_console_input):
162+
mode = (
163+
_input_mode.value
164+
| WindowsConsoleModeFlags.ENABLE_VIRTUAL_TERMINAL_INPUT
165+
)
166+
_SetConsoleMode(_HANDLE(_h_console_input), _DWORD(mode))
135167

136168
mode = (
137169
_output_mode.value
@@ -146,7 +178,8 @@ def get_console_mode() -> int:
146178

147179

148180
def set_text_color(color) -> None:
149-
_kernel32.SetConsoleTextAttribute(_h_console_output, color)
181+
if _valid_handle(_h_console_output):
182+
_SetConsoleTextAttribute(_HANDLE(_h_console_output), _WORD(color))
150183

151184

152185
def print_color(text, color) -> None:
@@ -156,19 +189,29 @@ def print_color(text, color) -> None:
156189

157190

158191
def getch():
192+
if not _valid_handle(_h_console_input):
193+
return None
194+
159195
lp_buffer = (_INPUT_RECORD * 2)()
160196
n_length = _DWORD(2)
161197
lp_number_of_events_read = _DWORD()
162198

163-
_ReadConsoleInput(
199+
if not _ReadConsoleInput(
164200
_HANDLE(_h_console_input),
165201
lp_buffer,
166202
n_length,
167203
ctypes.byref(lp_number_of_events_read),
168-
)
169-
170-
char = lp_buffer[1].Event.KeyEvent.uChar.AsciiChar.decode('ascii')
171-
if char == '\x00':
204+
):
172205
return None
173206

174-
return char
207+
# Only the records that were actually read contain valid data, and
208+
# non-ASCII keys must not crash the decode
209+
for i in range(min(lp_number_of_events_read.value, len(lp_buffer))):
210+
char = lp_buffer[i].Event.KeyEvent.uChar.AsciiChar.decode(
211+
'ascii',
212+
errors='replace',
213+
)
214+
if char != '\x00':
215+
return char
216+
217+
return None

0 commit comments

Comments
 (0)