Skip to content

Commit 3f4935c

Browse files
authored
Merge pull request #321 from wolph/quality-audit-3-internals
Quality audit PR 3/6: bar.py internals, decompositions, start() race fix
2 parents b261492 + 39c09cf commit 3f4935c

9 files changed

Lines changed: 509 additions & 197 deletions

File tree

progressbar/__main__.py

Lines changed: 147 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
271271
return parser
272272

273273

274-
def main(argv: list[str] | None = None) -> None: # noqa: C901
274+
def main(argv: list[str] | None = None) -> None:
275275
"""
276276
Main function for the `progressbar` command.
277277
@@ -289,56 +289,10 @@ def main(argv: list[str] | None = None) -> None: # noqa: C901
289289
args.output, args.line_mode, stack
290290
)
291291

292-
input_paths: list[BinaryIO | TextIO | Path | IO[typing.Any]] = []
293-
total_size: int = 0
294-
filesize_available: bool = True
295-
for filename in args.input:
296-
input_path: typing.IO[typing.Any] | pathlib.Path
297-
if filename == '-':
298-
if args.line_mode:
299-
input_path = sys.stdin
300-
else:
301-
input_path = sys.stdin.buffer
302-
303-
filesize_available = False
304-
else:
305-
input_path = pathlib.Path(filename)
306-
if not input_path.exists():
307-
parser.error(f'File not found: {filename}')
308-
309-
if not args.size:
310-
total_size += input_path.stat().st_size
311-
312-
input_paths.append(input_path)
313-
314-
# Determine the size for the progress bar (if provided)
315-
if args.size:
316-
total_size = size_to_bytes(args.size)
317-
filesize_available = True
318-
319-
if filesize_available:
320-
# Create the progress bar components
321-
widgets = [
322-
progressbar.Percentage(),
323-
' ',
324-
progressbar.Bar(),
325-
' ',
326-
progressbar.Timer(),
327-
' ',
328-
progressbar.FileTransferSpeed(),
329-
]
330-
else:
331-
widgets = [
332-
progressbar.SimpleProgress(),
333-
' ',
334-
progressbar.DataSize(),
335-
' ',
336-
progressbar.Timer(),
337-
]
338-
339-
if args.eta:
340-
widgets.append(' ')
341-
widgets.append(progressbar.AdaptiveETA())
292+
input_paths, total_size, filesize_available = _resolve_inputs(
293+
args, parser
294+
)
295+
widgets = _build_widgets(args, filesize_available)
342296

343297
# Initialize the progress bar
344298
bar = progressbar.ProgressBar(
@@ -347,55 +301,150 @@ def main(argv: list[str] | None = None) -> None: # noqa: C901
347301
max_error=False,
348302
)
349303

350-
# Data processing and updating the progress bar
351-
buffer_size = (
352-
size_to_bytes(args.buffer_size) if args.buffer_size else 1024
353-
)
354-
total_transferred = 0
355-
356-
bar.start()
357-
with contextlib.suppress(KeyboardInterrupt, BrokenPipeError):
358-
for input_path in input_paths:
359-
if isinstance(input_path, pathlib.Path):
360-
if args.line_mode:
361-
# newline='' disables universal-newline
362-
# translation so the byte count matches the file
363-
# size for CRLF files as well
364-
input_stream = stack.enter_context(
365-
input_path.open('r', newline=''),
366-
)
367-
else:
368-
input_stream = stack.enter_context(
369-
input_path.open('rb'),
370-
)
304+
_transfer(bar, input_paths, output_stream, args, stack)
305+
306+
307+
def _resolve_inputs(
308+
args: argparse.Namespace,
309+
parser: argparse.ArgumentParser,
310+
) -> tuple[list[BinaryIO | TextIO | Path | IO[typing.Any]], int, bool]:
311+
"""
312+
Resolve the input arguments into concrete streams/paths and the total size.
313+
314+
Returns the list of inputs (stdin streams or file paths), the total size in
315+
bytes and whether that size is known (``filesize_available``).
316+
"""
317+
input_paths: list[BinaryIO | TextIO | Path | IO[typing.Any]] = []
318+
total_size: int = 0
319+
filesize_available: bool = True
320+
for filename in args.input:
321+
input_path: typing.IO[typing.Any] | pathlib.Path
322+
if filename == '-':
323+
if args.line_mode:
324+
input_path = sys.stdin
325+
else:
326+
input_path = sys.stdin.buffer
327+
328+
filesize_available = False
329+
else:
330+
input_path = pathlib.Path(filename)
331+
if not input_path.exists():
332+
parser.error(f'File not found: {filename}')
333+
334+
if not args.size:
335+
total_size += input_path.stat().st_size
336+
337+
input_paths.append(input_path)
338+
339+
# An explicit ``--size`` overrides the detected file sizes entirely.
340+
if args.size:
341+
total_size = size_to_bytes(args.size)
342+
filesize_available = True
343+
344+
return input_paths, total_size, filesize_available
345+
346+
347+
def _build_widgets(
348+
args: argparse.Namespace,
349+
filesize_available: bool,
350+
) -> list[typing.Any]:
351+
"""
352+
Select the widget set for the progress bar.
353+
354+
When the total size is known a percentage/bar layout is used, otherwise a
355+
size-based layout is used. An adaptive ETA is appended when requested.
356+
"""
357+
widgets: list[typing.Any]
358+
if filesize_available:
359+
# Create the progress bar components
360+
widgets = [
361+
progressbar.Percentage(),
362+
' ',
363+
progressbar.Bar(),
364+
' ',
365+
progressbar.Timer(),
366+
' ',
367+
progressbar.FileTransferSpeed(),
368+
]
369+
else:
370+
widgets = [
371+
progressbar.SimpleProgress(),
372+
' ',
373+
progressbar.DataSize(),
374+
' ',
375+
progressbar.Timer(),
376+
]
377+
378+
if args.eta:
379+
widgets.append(' ')
380+
widgets.append(progressbar.AdaptiveETA())
381+
382+
return widgets
383+
384+
385+
def _transfer(
386+
bar: progressbar.ProgressBar,
387+
input_paths: list[BinaryIO | TextIO | Path | IO[typing.Any]],
388+
output_stream: typing.IO[typing.Any],
389+
args: argparse.Namespace,
390+
stack: contextlib.ExitStack,
391+
) -> None:
392+
"""
393+
Copy every input through the progress bar into ``output_stream``.
394+
395+
Opened files are registered on ``stack`` so they are closed when the caller
396+
exits its ``ExitStack`` context.
397+
"""
398+
# Data processing and updating the progress bar
399+
buffer_size = size_to_bytes(args.buffer_size) if args.buffer_size else 1024
400+
total_transferred = 0
401+
402+
bar.start()
403+
with contextlib.suppress(KeyboardInterrupt, BrokenPipeError):
404+
for input_path in input_paths:
405+
if isinstance(input_path, pathlib.Path):
406+
if args.line_mode:
407+
# newline='' disables universal-newline
408+
# translation so the byte count matches the file
409+
# size for CRLF files as well
410+
input_stream = stack.enter_context(
411+
input_path.open('r', newline=''),
412+
)
371413
else:
372-
input_stream = input_path
373-
374-
while True:
375-
data: str | bytes
376-
if args.line_mode:
377-
data = input_stream.readline(buffer_size)
378-
else:
379-
data = input_stream.read(buffer_size)
380-
381-
if not data:
382-
break
383-
384-
output_stream.write(data)
385-
if isinstance(data, str):
386-
# The total size is measured in bytes, so progress
387-
# must be tracked in bytes as well
388-
encoding = (
389-
getattr(input_stream, 'encoding', None) or 'utf-8'
390-
)
391-
total_transferred += len(
392-
data.encode(encoding, errors='replace'),
393-
)
394-
else:
395-
total_transferred += len(data)
396-
397-
bar.update(total_transferred)
414+
input_stream = stack.enter_context(
415+
input_path.open('rb'),
416+
)
417+
else:
418+
input_stream = input_path
419+
420+
while True:
421+
data: str | bytes
422+
if args.line_mode:
423+
data = input_stream.readline(buffer_size)
424+
else:
425+
data = input_stream.read(buffer_size)
426+
427+
if not data:
428+
break
429+
430+
output_stream.write(data)
431+
if isinstance(data, str):
432+
# The total size is measured in bytes, so progress
433+
# must be tracked in bytes as well
434+
encoding = (
435+
getattr(input_stream, 'encoding', None) or 'utf-8'
436+
)
437+
total_transferred += len(
438+
data.encode(encoding, errors='replace'),
439+
)
440+
else:
441+
total_transferred += len(data)
442+
443+
bar.update(total_transferred)
398444

445+
# Inside the suppress block on purpose (matching the historical
446+
# behavior): on interrupt/broken pipe the finish is skipped and a
447+
# BrokenPipeError from a closed stderr cannot crash shutdown.
399448
bar.finish(dirty=True)
400449

401450

0 commit comments

Comments
 (0)