@@ -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