Skip to content
Merged
11 changes: 11 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ jobs:
tox-env: py313
- python-version: '3.14'
tox-env: py314
# Python 3.15 is a pre-release and currently unsupported by
# typing_extensions (no released version survives
# `from typing_extensions import *` on 3.15 because
# no_type_check_decorator is still listed in __all__ after its
# removal from typing), which breaks the python_utils import.
# Failures are advisory until upstream catches up.
- python-version: '3.15-dev'
tox-env: py315
experimental: true
- python-version: '3.14'
tox-env: docs
- python-version: '3.14'
Expand All @@ -44,4 +51,8 @@ jobs:
run: |
python -m pip install --upgrade pip tox
- name: Test with tox
# Step-level continue-on-error keeps the job green for
# experimental (pre-release Python) environments while still
# showing the failing step in the logs
continue-on-error: ${{ matrix.experimental || false }}
run: tox -e ${{ matrix.tox-env }}
5 changes: 5 additions & 0 deletions examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ def do_something(bar):
# Increment one of the progress bars at random
multibar[bar_label].increment()

# The multibar context manager waits for all bars to finish on
# exit, so finish them explicitly
for bar_label in bar_labels:
multibar[bar_label].finish()


@example
def multiple_bars_line_offset_example() -> None:
Expand Down
43 changes: 34 additions & 9 deletions progressbar/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,8 @@ def main(argv: list[str] | None = None) -> None: # noqa: C901

# Initialize the progress bar
bar = progressbar.ProgressBar(
# widgets=widgets,
max_value=total_size or None,
widgets=widgets,
max_value=total_size if filesize_available else None,
max_error=False,
)

Expand All @@ -354,12 +354,20 @@ def main(argv: list[str] | None = None) -> None: # noqa: C901
total_transferred = 0

bar.start()
with contextlib.suppress(KeyboardInterrupt):
with contextlib.suppress(KeyboardInterrupt, BrokenPipeError):
for input_path in input_paths:
if isinstance(input_path, pathlib.Path):
input_stream = stack.enter_context(
input_path.open('r' if args.line_mode else 'rb')
)
if args.line_mode:
# newline='' disables universal-newline
# translation so the byte count matches the file
# size for CRLF files as well
input_stream = stack.enter_context(
input_path.open('r', newline=''),
)
else:
input_stream = stack.enter_context(
input_path.open('rb'),
)
else:
input_stream = input_path

Expand All @@ -374,7 +382,18 @@ def main(argv: list[str] | None = None) -> None: # noqa: C901
break

output_stream.write(data)
total_transferred += len(data)
if isinstance(data, str):
# The total size is measured in bytes, so progress
# must be tracked in bytes as well
encoding = (
getattr(input_stream, 'encoding', None) or 'utf-8'
)
total_transferred += len(
data.encode(encoding, errors='replace'),
)
Comment thread
wolph marked this conversation as resolved.
else:
total_transferred += len(data)

bar.update(total_transferred)

bar.finish(dirty=True)
Expand All @@ -386,8 +405,14 @@ def _get_output_stream(
stack: contextlib.ExitStack,
) -> typing.IO[typing.Any]:
if output and output != '-':
mode = 'w' if line_mode else 'wb'
return stack.enter_context(open(output, mode)) # noqa: SIM115
if line_mode:
# newline='' passes the data through without newline
# translation, mirroring the input handling
return stack.enter_context(
open(output, 'w', newline=''), # noqa: SIM115
Comment thread
wolph marked this conversation as resolved.
Dismissed
)

return stack.enter_context(open(output, 'wb')) # noqa: SIM115
Comment thread
wolph marked this conversation as resolved.
Dismissed
elif line_mode:
return sys.stdout
else:
Expand Down
22 changes: 16 additions & 6 deletions progressbar/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ class ExponentialMovingAverage(SmoothingAlgorithm):

def __init__(self, alpha: float = 0.5) -> None:
self.alpha = alpha
self.value = 0
self.value: float | None = None

def update(self, new_value: float, elapsed: timedelta) -> float:
self.value = self.alpha * new_value + (1 - self.alpha) * self.value
if self.value is None:
# Seed with the first observation instead of biasing towards 0
self.value = new_value
else:
self.value = self.alpha * new_value + (1 - self.alpha) * self.value

return self.value


Expand All @@ -43,10 +48,15 @@ class DoubleExponentialMovingAverage(SmoothingAlgorithm):

def __init__(self, alpha: float = 0.5) -> None:
self.alpha = alpha
self.ema1 = 0
self.ema2 = 0
self.ema1: float | None = None
self.ema2: float | None = None

def update(self, new_value: float, elapsed: timedelta) -> float:
self.ema1 = self.alpha * new_value + (1 - self.alpha) * self.ema1
self.ema2 = self.alpha * self.ema1 + (1 - self.alpha) * self.ema2
if self.ema1 is None or self.ema2 is None:
# Seed with the first observation instead of biasing towards 0
self.ema1 = self.ema2 = new_value
else:
self.ema1 = self.alpha * new_value + (1 - self.alpha) * self.ema1
self.ema2 = self.alpha * self.ema1 + (1 - self.alpha) * self.ema2

return 2 * self.ema1 - self.ema2
Loading
Loading