diff --git a/docs/_theme/flask_theme_support.py b/docs/_theme/flask_theme_support.py
index c11997c..8174712 100644
--- a/docs/_theme/flask_theme_support.py
+++ b/docs/_theme/flask_theme_support.py
@@ -1,18 +1,18 @@
# flasky extensions. flasky pygments style based on tango style
from pygments.style import Style
from pygments.token import (
- Keyword,
- Name,
Comment,
- String,
Error,
+ Generic,
+ Keyword,
+ Literal,
+ Name,
Number,
Operator,
- Generic,
- Whitespace,
- Punctuation,
Other,
- Literal,
+ Punctuation,
+ String,
+ Whitespace,
)
diff --git a/docs/conf.py b/docs/conf.py
index 8912b99..c4ed327 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
#
# Progress Bar documentation build configuration file, created by
# sphinx-quickstart on Tue Aug 20 11:47:33 2013.
@@ -11,16 +10,16 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
+import datetime
import os
import sys
-import datetime
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
-from progressbar import __about__ as metadata # noqa: E402
+from progressbar import __about__ as metadata
# -- General configuration -----------------------------------------------
@@ -59,7 +58,7 @@
master_doc = 'index'
# General information about the project.
-project = u'Progress Bar'
+project = 'Progress Bar'
project_slug = ''.join(project.capitalize().split())
copyright = f'{datetime.date.today().year}, {metadata.__author__}'
diff --git a/docs/progressbar.algorithms.rst b/docs/progressbar.algorithms.rst
new file mode 100644
index 0000000..bf239d7
--- /dev/null
+++ b/docs/progressbar.algorithms.rst
@@ -0,0 +1,7 @@
+progressbar.algorithms module
+=============================
+
+.. automodule:: progressbar.algorithms
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/progressbar.env.rst b/docs/progressbar.env.rst
new file mode 100644
index 0000000..a818e0b
--- /dev/null
+++ b/docs/progressbar.env.rst
@@ -0,0 +1,7 @@
+progressbar.env module
+======================
+
+.. automodule:: progressbar.env
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/progressbar.terminal.os_specific.posix.rst b/docs/progressbar.terminal.os_specific.posix.rst
deleted file mode 100644
index 7d1ec49..0000000
--- a/docs/progressbar.terminal.os_specific.posix.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-progressbar.terminal.os\_specific.posix module
-==============================================
-
-.. automodule:: progressbar.terminal.os_specific.posix
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/progressbar.terminal.os_specific.rst b/docs/progressbar.terminal.os_specific.rst
index 456ef9c..b00648e 100644
--- a/docs/progressbar.terminal.os_specific.rst
+++ b/docs/progressbar.terminal.os_specific.rst
@@ -7,9 +7,6 @@ Submodules
.. toctree::
:maxdepth: 4
- progressbar.terminal.os_specific.posix
- progressbar.terminal.os_specific.windows
-
Module contents
---------------
diff --git a/docs/progressbar.terminal.os_specific.windows.rst b/docs/progressbar.terminal.os_specific.windows.rst
deleted file mode 100644
index 0595e93..0000000
--- a/docs/progressbar.terminal.os_specific.windows.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-progressbar.terminal.os\_specific.windows module
-================================================
-
-.. automodule:: progressbar.terminal.os_specific.windows
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/examples.py b/examples.py
index 8b7247c..00e565e 100644
--- a/examples.py
+++ b/examples.py
@@ -1,7 +1,9 @@
#!/usr/bin/python
-# -*- coding: utf-8 -*-
+from __future__ import annotations
+import contextlib
import functools
+import os
import random
import sys
import time
@@ -9,7 +11,7 @@
import progressbar
-examples: typing.List[typing.Callable[[typing.Any], typing.Any]] = []
+examples: list[typing.Callable[[typing.Any], typing.Any]] = []
def example(fn):
@@ -40,19 +42,83 @@ def fast_example():
@example
def shortcut_example():
- for i in progressbar.progressbar(range(10)):
+ for _ in progressbar.progressbar(range(10)):
time.sleep(0.1)
@example
def prefixed_shortcut_example():
- for i in progressbar.progressbar(range(10), prefix='Hi: '):
+ for _ in progressbar.progressbar(range(10), prefix='Hi: '):
time.sleep(0.1)
+@example
+def parallel_bars_multibar_example():
+ if os.name == 'nt':
+ print('Skipping multibar example on Windows due to threading '
+ 'incompatibilities with the example code.')
+ return
+
+ BARS = 5
+ N = 50
+
+ def do_something(bar):
+ for _ in bar(range(N)):
+ # Sleep up to 0.1 seconds
+ time.sleep(random.random() * 0.1)
+
+ with progressbar.MultiBar() as multibar:
+ bar_labels = []
+ for i in range(BARS):
+ # Get a progressbar
+ bar_label = 'Bar #%d' % i
+ bar_labels.append(bar_label)
+ multibar[bar_label]
+
+ for _ in range(N * BARS):
+ time.sleep(0.005)
+
+ bar_i = random.randrange(0, BARS)
+ bar_label = bar_labels[bar_i]
+ # Increment one of the progress bars at random
+ multibar[bar_label].increment()
+
+
+@example
+def multiple_bars_line_offset_example():
+ BARS = 5
+ N = 100
+
+ bars = [
+ progressbar.ProgressBar(
+ max_value=N,
+ # We add 1 to the line offset to account for the `print_fd`
+ line_offset=i + 1,
+ max_error=False,
+ )
+ for i in range(BARS)
+ ]
+ # Create a file descriptor for regular printing as well
+ print_fd = progressbar.LineOffsetStreamWrapper(lines=0, stream=sys.stdout)
+ assert print_fd
+
+ # The progress bar updates, normally you would do something useful here
+ for _ in range(N * BARS):
+ time.sleep(0.005)
+
+ # Increment one of the progress bars at random
+ bars[random.randrange(0, BARS)].increment()
+
+ # Cleanup the bars
+ for bar in bars:
+ bar.finish()
+ # Add a newline to make sure the next print starts on a new line
+ print()
+
+
@example
def templated_shortcut_example():
- for i in progressbar.progressbar(range(10), suffix='{seconds_elapsed:.1}'):
+ for _ in progressbar.progressbar(range(10), suffix='{seconds_elapsed:.1}'):
time.sleep(0.1)
@@ -62,7 +128,7 @@ def job_status_example():
redirect_stdout=True,
widgets=[progressbar.widgets.JobStatusBar('status')],
) as bar:
- for i in range(30):
+ for _ in range(30):
print('random', random.random())
# Roughly 1/3 probability for each status ;)
# Yes... probability is confusing at times
@@ -141,7 +207,7 @@ def multi_range_bar_example():
'\x1b[31m.\x1b[39m', # Scheduling
' ', # Not started
]
- widgets = [progressbar.MultiRangeBar("amounts", markers=markers)]
+ widgets = [progressbar.MultiRangeBar('amounts', markers=markers)]
amounts = [0] * (len(markers) - 1) + [25]
with progressbar.ProgressBar(widgets=widgets, max_value=10).start() as bar:
@@ -149,7 +215,7 @@ def multi_range_bar_example():
incomplete_items = [
idx
for idx, amount in enumerate(amounts)
- for i in range(amount)
+ for _ in range(amount)
if idx != 0
]
if not incomplete_items:
@@ -167,7 +233,7 @@ def multi_progress_bar_example(left=True):
jobs = [
# Each job takes between 1 and 10 steps to complete
[0, random.randint(1, 10)]
- for i in range(25) # 25 jobs total
+ for _ in range(25) # 25 jobs total
]
widgets = [
@@ -197,17 +263,17 @@ def multi_progress_bar_example(left=True):
@example
def granular_progress_example():
widgets = [
- progressbar.GranularBar(markers=" ▏▎▍▌▋▊▉█", left='', right='|'),
- progressbar.GranularBar(markers=" ▁▂▃▄▅▆▇█", left='', right='|'),
- progressbar.GranularBar(markers=" ▖▌▛█", left='', right='|'),
- progressbar.GranularBar(markers=" ░▒▓█", left='', right='|'),
- progressbar.GranularBar(markers=" ⡀⡄⡆⡇⣇⣧⣷⣿", left='', right='|'),
- progressbar.GranularBar(markers=" .oO", left='', right=''),
+ progressbar.GranularBar(markers=' ▏▎▍▌▋▊▉█', left='', right='|'),
+ progressbar.GranularBar(markers=' ▁▂▃▄▅▆▇█', left='', right='|'),
+ progressbar.GranularBar(markers=' ▖▌▛█', left='', right='|'),
+ progressbar.GranularBar(markers=' ░▒▓█', left='', right='|'),
+ progressbar.GranularBar(markers=' ⡀⡄⡆⡇⣇⣧⣷⣿', left='', right='|'),
+ progressbar.GranularBar(markers=' .oO', left='', right=''),
]
- for i in progressbar.progressbar(list(range(100)), widgets=widgets):
+ for _ in progressbar.progressbar(list(range(100)), widgets=widgets):
time.sleep(0.03)
- for i in progressbar.progressbar(iter(range(100)), widgets=widgets):
+ for _ in progressbar.progressbar(iter(range(100)), widgets=widgets):
time.sleep(0.03)
@@ -237,6 +303,7 @@ def file_transfer_example():
bar = progressbar.ProgressBar(widgets=widgets, max_value=1000).start()
for i in range(100):
# do something
+ time.sleep(0.01)
bar.update(10 * i + 1)
bar.finish()
@@ -270,6 +337,7 @@ def update(self, bar):
bar.start()
for i in range(200):
# do something
+ time.sleep(0.01)
bar.update(5 * i + 1)
bar.finish()
@@ -286,8 +354,8 @@ def double_bar_example():
bar = progressbar.ProgressBar(widgets=widgets, max_value=1000).start()
for i in range(100):
# do something
- bar.update(10 * i + 1)
time.sleep(0.01)
+ bar.update(10 * i + 1)
bar.finish()
@@ -337,7 +405,7 @@ def basic_progress():
def progress_with_automatic_max():
# Progressbar can guess max_value automatically.
bar = progressbar.ProgressBar()
- for i in bar(range(8)):
+ for _ in bar(range(8)):
time.sleep(0.1)
@@ -345,7 +413,7 @@ def progress_with_automatic_max():
def progress_with_unavailable_max():
# Progressbar can't guess max_value.
bar = progressbar.ProgressBar(max_value=8)
- for i in bar((i for i in range(8))):
+ for _ in bar(i for i in range(8)):
time.sleep(0.1)
@@ -354,7 +422,7 @@ def animated_marker():
bar = progressbar.ProgressBar(
widgets=['Working: ', progressbar.AnimatedMarker()]
)
- for i in bar((i for i in range(5))):
+ for _ in bar(i for i in range(5)):
time.sleep(0.1)
@@ -367,7 +435,7 @@ def filling_bar_animated_marker():
),
]
)
- for i in bar(range(15)):
+ for _ in bar(range(15)):
time.sleep(0.1)
@@ -381,7 +449,7 @@ def counter_and_timer():
')',
]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar((i for i in range(15))):
+ for _ in bar(i for i in range(15)):
time.sleep(0.1)
@@ -391,7 +459,7 @@ def format_label():
progressbar.FormatLabel('Processed: %(value)d lines (in: %(elapsed)s)')
]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar((i for i in range(15))):
+ for _ in bar(i for i in range(15)):
time.sleep(0.1)
@@ -399,7 +467,7 @@ def format_label():
def animated_balloons():
widgets = ['Balloon: ', progressbar.AnimatedMarker(markers='.oO@* ')]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar((i for i in range(24))):
+ for _ in bar(i for i in range(24)):
time.sleep(0.1)
@@ -409,7 +477,7 @@ def animated_arrows():
try:
widgets = ['Arrows: ', progressbar.AnimatedMarker(markers='←↖↑↗→↘↓↙')]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar((i for i in range(24))):
+ for _ in bar(i for i in range(24)):
time.sleep(0.1)
except UnicodeError:
sys.stdout.write('Unicode error: skipping example')
@@ -421,7 +489,7 @@ def animated_filled_arrows():
try:
widgets = ['Arrows: ', progressbar.AnimatedMarker(markers='◢◣◤◥')]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar((i for i in range(24))):
+ for _ in bar(i for i in range(24)):
time.sleep(0.1)
except UnicodeError:
sys.stdout.write('Unicode error: skipping example')
@@ -433,7 +501,7 @@ def animated_wheels():
try:
widgets = ['Wheels: ', progressbar.AnimatedMarker(markers='◐◓◑◒')]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar((i for i in range(24))):
+ for _ in bar(i for i in range(24)):
time.sleep(0.1)
except UnicodeError:
sys.stdout.write('Unicode error: skipping example')
@@ -446,7 +514,7 @@ def format_label_bouncer():
progressbar.BouncingBar(),
]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar((i for i in range(100))):
+ for _ in bar(i for i in range(100)):
time.sleep(0.01)
@@ -458,14 +526,14 @@ def format_label_rotating_bouncer():
]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar((i for i in range(18))):
+ for _ in bar(i for i in range(18)):
time.sleep(0.1)
@example
def with_right_justify():
with progressbar.ProgressBar(
- max_value=10, term_width=20, left_justify=False
+ max_value=10, term_width=20, left_justify=False
) as progress:
assert progress.term_width is not None
for i in range(10):
@@ -474,20 +542,16 @@ def with_right_justify():
@example
def exceeding_maximum():
- with progressbar.ProgressBar(max_value=1) as progress:
- try:
- progress.update(2)
- except ValueError:
- pass
+ with progressbar.ProgressBar(max_value=1) as progress, contextlib.suppress(
+ ValueError):
+ progress.update(2)
@example
def reaching_maximum():
progress = progressbar.ProgressBar(max_value=1)
- try:
+ with contextlib.suppress(RuntimeError):
progress.update(1)
- except RuntimeError:
- pass
@example
@@ -504,20 +568,11 @@ def stderr_redirection():
progress.update(0)
-@example
-def negative_maximum():
- try:
- with progressbar.ProgressBar(max_value=-1) as progress:
- progress.start()
- except ValueError:
- pass
-
-
@example
def rotating_bouncing_marker():
widgets = [progressbar.BouncingBar(marker=progressbar.RotatingMarker())]
with progressbar.ProgressBar(
- widgets=widgets, max_value=20, term_width=10
+ widgets=widgets, max_value=20, term_width=10
) as progress:
for i in range(20):
time.sleep(0.1)
@@ -529,7 +584,7 @@ def rotating_bouncing_marker():
)
]
with progressbar.ProgressBar(
- widgets=widgets, max_value=20, term_width=10
+ widgets=widgets, max_value=20, term_width=10
) as progress:
for i in range(20):
time.sleep(0.1)
@@ -545,7 +600,7 @@ def incrementing_bar():
],
max_value=10,
).start()
- for i in range(10):
+ for _ in range(10):
# do something
time.sleep(0.1)
bar += 1
@@ -581,13 +636,17 @@ def eta_types_demonstration():
progressbar.Percentage(),
' ETA: ',
progressbar.ETA(),
- ' Adaptive ETA: ',
+ ' Adaptive : ',
progressbar.AdaptiveETA(),
- ' Absolute ETA: ',
+ ' Smoothing(a=0.1): ',
+ progressbar.SmoothingETA(smoothing_parameters=dict(alpha=0.1)),
+ ' Smoothing(a=0.9): ',
+ progressbar.SmoothingETA(smoothing_parameters=dict(alpha=0.9)),
+ ' Absolute: ',
progressbar.AbsoluteETA(),
- ' Transfer Speed: ',
+ ' Transfer: ',
progressbar.FileTransferSpeed(),
- ' Adaptive Transfer Speed: ',
+ ' Adaptive T: ',
progressbar.AdaptiveTransferSpeed(),
' ',
progressbar.Bar(),
@@ -617,7 +676,7 @@ def adaptive_eta_without_value_change():
poll_interval=0.0001,
)
bar.start()
- for i in range(100):
+ for _ in range(100):
bar.update(1)
time.sleep(0.1)
bar.finish()
@@ -628,9 +687,9 @@ def iterator_with_max_value():
# Testing using progressbar as an iterator with a max value
bar = progressbar.ProgressBar()
- for n in bar(iter(range(100)), 100):
+ for _ in bar(iter(range(100)), 100):
# iter range is a way to get an iterator in both python 2 and 3
- pass
+ time.sleep(0.01)
@example
@@ -698,13 +757,13 @@ def user_variables():
num_subtasks = sum(len(x) for x in tasks.values())
with progressbar.ProgressBar(
- prefix='{variables.task} >> {variables.subtask}',
- variables={'task': '--', 'subtask': '--'},
- max_value=10 * num_subtasks,
+ prefix='{variables.task} >> {variables.subtask}',
+ variables={'task': '--', 'subtask': '--'},
+ max_value=10 * num_subtasks,
) as bar:
for tasks_name, subtasks in tasks.items():
for subtask_name in subtasks:
- for i in range(10):
+ for _ in range(10):
bar.update(
bar.value + 1, task=tasks_name, subtask=subtask_name
)
@@ -736,14 +795,14 @@ def format_custom_text():
@example
def simple_api_example():
bar = progressbar.ProgressBar(widget_kwargs=dict(fill='█'))
- for i in bar(range(200)):
+ for _ in bar(range(200)):
time.sleep(0.02)
@example
-def ETA_on_generators():
+def eta_on_generators():
def gen():
- for x in range(200):
+ for _ in range(200):
yield None
widgets = [
@@ -755,14 +814,14 @@ def gen():
]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar(gen()):
+ for _ in bar(gen()):
time.sleep(0.02)
@example
def percentage_on_generators():
def gen():
- for x in range(200):
+ for _ in range(200):
yield None
widgets = [
@@ -775,19 +834,22 @@ def gen():
]
bar = progressbar.ProgressBar(widgets=widgets)
- for i in bar(gen()):
+ for _ in bar(gen()):
time.sleep(0.02)
def test(*tests):
if tests:
+ no_tests = True
for example in examples:
for test in tests:
if test in example.__name__:
example()
+ no_tests = False
break
- else:
+ if no_tests:
+ for example in examples:
print('Skipping', example.__name__)
else:
for example in examples:
diff --git a/progressbar/__about__.py b/progressbar/__about__.py
index 6279c36..dee8e02 100644
--- a/progressbar/__about__.py
+++ b/progressbar/__about__.py
@@ -21,7 +21,7 @@
'''.strip().split(),
)
__email__ = 'wolph@wol.ph'
-__version__ = '4.3.2'
+__version__ = '4.4.0'
__license__ = 'BSD'
__copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)'
__url__ = 'https://github.com/WoLpH/python-progressbar'
diff --git a/progressbar/__init__.py b/progressbar/__init__.py
index 4382499..ff76ff4 100644
--- a/progressbar/__init__.py
+++ b/progressbar/__init__.py
@@ -1,6 +1,11 @@
from datetime import date
from .__about__ import __author__, __version__
+from .algorithms import (
+ DoubleExponentialMovingAverage,
+ ExponentialMovingAverage,
+ SmoothingAlgorithm,
+)
from .bar import DataTransferBar, NullBar, ProgressBar
from .base import UnknownLength
from .multi import MultiBar, SortKey
@@ -32,6 +37,7 @@
ReverseBar,
RotatingMarker,
SimpleProgress,
+ SmoothingETA,
Timer,
Variable,
VariableMixin,
@@ -46,6 +52,10 @@
'ETA',
'AdaptiveETA',
'AbsoluteETA',
+ 'SmoothingETA',
+ 'SmoothingAlgorithm',
+ 'ExponentialMovingAverage',
+ 'DoubleExponentialMovingAverage',
'DataSize',
'FileTransferSpeed',
'AdaptiveTransferSpeed',
diff --git a/progressbar/__main__.py b/progressbar/__main__.py
new file mode 100644
index 0000000..431aa31
--- /dev/null
+++ b/progressbar/__main__.py
@@ -0,0 +1,396 @@
+from __future__ import annotations
+
+import argparse
+import contextlib
+import pathlib
+import sys
+import typing
+from pathlib import Path
+from typing import BinaryIO, TextIO
+
+import progressbar
+
+
+def size_to_bytes(size_str: str) -> int:
+ '''
+ Convert a size string with suffixes 'k', 'm', etc., to bytes.
+
+ Note: This function also supports '@' as a prefix to a file path to get the
+ file size.
+
+ >>> size_to_bytes('1024k')
+ 1048576
+ >>> size_to_bytes('1024m')
+ 1073741824
+ >>> size_to_bytes('1024g')
+ 1099511627776
+ >>> size_to_bytes('1024')
+ 1024
+ >>> size_to_bytes('1024p')
+ 1125899906842624
+ '''
+
+ # Define conversion rates
+ suffix_exponent = {
+ 'k': 1,
+ 'm': 2,
+ 'g': 3,
+ 't': 4,
+ 'p': 5,
+ }
+
+ # Initialize the default exponent to 0 (for bytes)
+ exponent = 0
+
+ # Check if the size starts with '@' (for file sizes, not handled here)
+ if size_str.startswith('@'):
+ return pathlib.Path(size_str[1:]).stat().st_size
+
+ # Check if the last character is a known suffix and adjust the multiplier
+ if size_str[-1].lower() in suffix_exponent:
+ # Update exponent based on the suffix
+ exponent = suffix_exponent[size_str[-1].lower()]
+ # Remove the suffix from the size_str
+ size_str = size_str[:-1]
+
+ # Convert the size_str to an integer and apply the exponent
+ return int(size_str) * (1024**exponent)
+
+
+def create_argument_parser() -> argparse.ArgumentParser:
+ '''
+ Create the argument parser for the `progressbar` command.
+ '''
+
+ parser = argparse.ArgumentParser(
+ description='''
+ Monitor the progress of data through a pipe.
+
+ Note that this is a Python implementation of the original `pv` command
+ that is functional but not yet feature complete.
+ '''
+ )
+
+ # Display switches
+ parser.add_argument(
+ '-p',
+ '--progress',
+ action='store_true',
+ help='Turn the progress bar on.',
+ )
+ parser.add_argument(
+ '-t', '--timer', action='store_true', help='Turn the timer on.'
+ )
+ parser.add_argument(
+ '-e', '--eta', action='store_true', help='Turn the ETA timer on.'
+ )
+ parser.add_argument(
+ '-I',
+ '--fineta',
+ action='store_true',
+ help='Display the ETA as local time of arrival.',
+ )
+ parser.add_argument(
+ '-r', '--rate', action='store_true', help='Turn the rate counter on.'
+ )
+ parser.add_argument(
+ '-a',
+ '--average-rate',
+ action='store_true',
+ help='Turn the average rate counter on.',
+ )
+ parser.add_argument(
+ '-b',
+ '--bytes',
+ action='store_true',
+ help='Turn the total byte counter on.',
+ )
+ parser.add_argument(
+ '-8',
+ '--bits',
+ action='store_true',
+ help='Display total bits instead of bytes.',
+ )
+ parser.add_argument(
+ '-T',
+ '--buffer-percent',
+ action='store_true',
+ help='Turn on the transfer buffer percentage display.',
+ )
+ parser.add_argument(
+ '-A',
+ '--last-written',
+ type=int,
+ help='Show the last NUM bytes written.',
+ )
+ parser.add_argument(
+ '-F',
+ '--format',
+ type=str,
+ help='Use the format string FORMAT for output format.',
+ )
+ parser.add_argument(
+ '-n', '--numeric', action='store_true', help='Numeric output.'
+ )
+ parser.add_argument(
+ '-q', '--quiet', action='store_true', help='No output.'
+ )
+
+ # Output modifiers
+ parser.add_argument(
+ '-W',
+ '--wait',
+ action='store_true',
+ help='Wait until the first byte has been transferred.',
+ )
+ parser.add_argument('-D', '--delay-start', type=float, help='Delay start.')
+ parser.add_argument(
+ '-s', '--size', type=str, help='Assume total data size is SIZE.'
+ )
+ parser.add_argument(
+ '-l',
+ '--line-mode',
+ action='store_true',
+ help='Count lines instead of bytes.',
+ )
+ parser.add_argument(
+ '-0',
+ '--null',
+ action='store_true',
+ help='Count lines terminated with a zero byte.',
+ )
+ parser.add_argument(
+ '-i', '--interval', type=float, help='Interval between updates.'
+ )
+ parser.add_argument(
+ '-m',
+ '--average-rate-window',
+ type=int,
+ help='Window for average rate calculation.',
+ )
+ parser.add_argument(
+ '-w',
+ '--width',
+ type=int,
+ help='Assume terminal is WIDTH characters wide.',
+ )
+ parser.add_argument(
+ '-H', '--height', type=int, help='Assume terminal is HEIGHT rows high.'
+ )
+ parser.add_argument(
+ '-N', '--name', type=str, help='Prefix output information with NAME.'
+ )
+ parser.add_argument(
+ '-f', '--force', action='store_true', help='Force output.'
+ )
+ parser.add_argument(
+ '-c',
+ '--cursor',
+ action='store_true',
+ help='Use cursor positioning escape sequences.',
+ )
+
+ # Data transfer modifiers
+ parser.add_argument(
+ '-L',
+ '--rate-limit',
+ type=str,
+ help='Limit transfer to RATE bytes per second.',
+ )
+ parser.add_argument(
+ '-B',
+ '--buffer-size',
+ type=str,
+ help='Use transfer buffer size of BYTES.',
+ )
+ parser.add_argument(
+ '-C', '--no-splice', action='store_true', help='Never use splice.'
+ )
+ parser.add_argument(
+ '-E', '--skip-errors', action='store_true', help='Ignore read errors.'
+ )
+ parser.add_argument(
+ '-Z',
+ '--error-skip-block',
+ type=str,
+ help='Skip block size when ignoring errors.',
+ )
+ parser.add_argument(
+ '-S',
+ '--stop-at-size',
+ action='store_true',
+ help='Stop transferring after SIZE bytes.',
+ )
+ parser.add_argument(
+ '-Y',
+ '--sync',
+ action='store_true',
+ help='Synchronise buffer caches to disk after writes.',
+ )
+ parser.add_argument(
+ '-K',
+ '--direct-io',
+ action='store_true',
+ help='Set O_DIRECT flag on all inputs/outputs.',
+ )
+ parser.add_argument(
+ '-X',
+ '--discard',
+ action='store_true',
+ help='Discard input data instead of transferring it.',
+ )
+ parser.add_argument(
+ '-d', '--watchfd', type=str, help='Watch file descriptor of process.'
+ )
+ parser.add_argument(
+ '-R',
+ '--remote',
+ type=int,
+ help='Remote control another running instance of pv.',
+ )
+
+ # General options
+ parser.add_argument(
+ '-P', '--pidfile', type=pathlib.Path, help='Save process ID in FILE.'
+ )
+ parser.add_argument(
+ 'input',
+ help='Input file path. Uses stdin if not specified.',
+ default='-',
+ nargs='*',
+ )
+ parser.add_argument(
+ '-o',
+ '--output',
+ default='-',
+ help='Output file path. Uses stdout if not specified.',
+ )
+
+ return parser
+
+
+def main(argv: list[str] | None = None): # noqa: C901
+ '''
+ Main function for the `progressbar` command.
+
+ Args:
+ argv (list[str] | None): Command-line arguments passed to the script.
+
+ Returns:
+ None
+ '''
+ parser: argparse.ArgumentParser = create_argument_parser()
+ args: argparse.Namespace = parser.parse_args(argv)
+
+ with contextlib.ExitStack() as stack:
+ output_stream: typing.IO[typing.Any] = _get_output_stream(
+ args.output, args.line_mode, stack
+ )
+
+ input_paths: list[BinaryIO | TextIO | Path] = []
+ total_size: int = 0
+ filesize_available: bool = True
+ for filename in args.input:
+ input_path: typing.IO[typing.Any] | pathlib.Path
+ if filename == '-':
+ if args.line_mode:
+ input_path = sys.stdin
+ else:
+ input_path = sys.stdin.buffer
+
+ filesize_available = False
+ else:
+ input_path = pathlib.Path(filename)
+ if not input_path.exists():
+ parser.error(f'File not found: {filename}')
+
+ if not args.size:
+ total_size += input_path.stat().st_size
+
+ input_paths.append(input_path)
+
+ # Determine the size for the progress bar (if provided)
+ if args.size:
+ total_size = size_to_bytes(args.size)
+ filesize_available = True
+
+ if filesize_available:
+ # Create the progress bar components
+ widgets = [
+ progressbar.Percentage(),
+ ' ',
+ progressbar.Bar(),
+ ' ',
+ progressbar.Timer(),
+ ' ',
+ progressbar.FileTransferSpeed(),
+ ]
+ else:
+ widgets = [
+ progressbar.SimpleProgress(),
+ ' ',
+ progressbar.DataSize(),
+ ' ',
+ progressbar.Timer(),
+ ]
+
+ if args.eta:
+ widgets.append(' ')
+ widgets.append(progressbar.AdaptiveETA())
+
+ # Initialize the progress bar
+ bar = progressbar.ProgressBar(
+ # widgets=widgets,
+ max_value=total_size or None,
+ max_error=False,
+ )
+
+ # Data processing and updating the progress bar
+ buffer_size = (
+ size_to_bytes(args.buffer_size) if args.buffer_size else 1024
+ )
+ total_transferred = 0
+
+ bar.start()
+ with contextlib.suppress(KeyboardInterrupt):
+ 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')
+ )
+ else:
+ input_stream = input_path
+
+ while True:
+ data: str | bytes
+ if args.line_mode:
+ data = input_stream.readline(buffer_size)
+ else:
+ data = input_stream.read(buffer_size)
+
+ if not data:
+ break
+
+ output_stream.write(data)
+ total_transferred += len(data)
+ bar.update(total_transferred)
+
+ bar.finish(dirty=True)
+
+
+def _get_output_stream(
+ output: str | None,
+ line_mode: bool,
+ 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
+ elif line_mode:
+ return sys.stdout
+ else:
+ return sys.stdout.buffer
+
+
+if __name__ == '__main__':
+ main()
diff --git a/progressbar/algorithms.py b/progressbar/algorithms.py
new file mode 100644
index 0000000..bb8586e
--- /dev/null
+++ b/progressbar/algorithms.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+import abc
+from datetime import timedelta
+
+
+class SmoothingAlgorithm(abc.ABC):
+ @abc.abstractmethod
+ def __init__(self, **kwargs):
+ raise NotImplementedError
+
+ @abc.abstractmethod
+ def update(self, new_value: float, elapsed: timedelta) -> float:
+ '''Updates the algorithm with a new value and returns the smoothed
+ value.
+ '''
+ raise NotImplementedError
+
+
+class ExponentialMovingAverage(SmoothingAlgorithm):
+ '''
+ The Exponential Moving Average (EMA) is an exponentially weighted moving
+ average that reduces the lag that's typically associated with a simple
+ moving average. It's more responsive to recent changes in data.
+ '''
+
+ def __init__(self, alpha: float = 0.5) -> None:
+ self.alpha = alpha
+ self.value = 0
+
+ def update(self, new_value: float, elapsed: timedelta) -> float:
+ self.value = self.alpha * new_value + (1 - self.alpha) * self.value
+ return self.value
+
+
+class DoubleExponentialMovingAverage(SmoothingAlgorithm):
+ '''
+ The Double Exponential Moving Average (DEMA) is essentially an EMA of an
+ EMA, which reduces the lag that's typically associated with a simple EMA.
+ It's more responsive to recent changes in data.
+ '''
+
+ def __init__(self, alpha: float = 0.5) -> None:
+ self.alpha = alpha
+ self.ema1 = 0
+ self.ema2 = 0
+
+ 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
+ return 2 * self.ema1 - self.ema2
diff --git a/progressbar/bar.py b/progressbar/bar.py
index d722100..ca46fd3 100644
--- a/progressbar/bar.py
+++ b/progressbar/bar.py
@@ -127,7 +127,12 @@ def finish(self): # pragma: no cover
def __del__(self):
if not self._finished and self._started: # pragma: no cover
- self.finish()
+ # We're not using contextlib.suppress here because during teardown
+ # contextlib is not available anymore.
+ try: # noqa: SIM105
+ self.finish()
+ except AttributeError:
+ pass
def __getstate__(self):
return self.__dict__
@@ -162,13 +167,13 @@ class DefaultFdMixin(ProgressBarMixinBase):
#: Set the terminal to be ANSI compatible. If a terminal is ANSI
#: compatible we will automatically enable `colors` and disable
#: `line_breaks`.
- is_ansi_terminal: bool = False
+ is_ansi_terminal: bool | None = False
#: Whether the file descriptor is a terminal or not. This is used to
#: determine whether to use ANSI escape codes or not.
- is_terminal: bool
+ is_terminal: bool | None
#: Whether to print line breaks. This is useful for logging the
#: progressbar. When disabled the current line is overwritten.
- line_breaks: bool = True
+ line_breaks: bool | None = True
#: Specify the type and number of colors to support. Defaults to auto
#: detection based on the file descriptor type (i.e. interactive terminal)
#: environment variables such as `COLORTERM` and `TERM`. Color output can
@@ -179,9 +184,7 @@ class DefaultFdMixin(ProgressBarMixinBase):
#: For true (24 bit/16M) color support you can use `COLORTERM=truecolor`.
#: For 256 color support you can use `TERM=xterm-256color`.
#: For 16 colorsupport you can use `TERM=xterm`.
- enable_colors: progressbar.env.ColorSupport | bool | None = (
- progressbar.env.COLOR_SUPPORT
- )
+ enable_colors: progressbar.env.ColorSupport = progressbar.env.COLOR_SUPPORT
def __init__(
self,
@@ -200,7 +203,7 @@ def __init__(
fd = self._apply_line_offset(fd, line_offset)
self.fd = fd
self.is_ansi_terminal = progressbar.env.is_ansi_terminal(fd)
- self.is_terminal = self._determine_is_terminal(fd, is_terminal)
+ self.is_terminal = progressbar.env.is_terminal(fd, is_terminal)
self.line_breaks = self._determine_line_breaks(line_breaks)
self.enable_colors = self._determine_enable_colors(enable_colors)
@@ -219,29 +222,47 @@ def _apply_line_offset(
else:
return fd
- def _determine_is_terminal(
- self,
- fd: base.TextIO,
- is_terminal: bool | None,
- ) -> bool:
- if is_terminal is not None:
- return progressbar.env.is_terminal(fd, is_terminal)
- else:
- return progressbar.env.is_ansi_terminal(fd)
-
- def _determine_line_breaks(self, line_breaks: bool | None) -> bool:
+ def _determine_line_breaks(self, line_breaks: bool | None) -> bool | None:
if line_breaks is None:
return progressbar.env.env_flag(
'PROGRESSBAR_LINE_BREAKS',
not self.is_terminal,
)
else:
- return bool(line_breaks)
+ return line_breaks
def _determine_enable_colors(
self,
enable_colors: progressbar.env.ColorSupport | None,
) -> progressbar.env.ColorSupport:
+ '''
+ Determines the color support for the progress bar.
+
+ This method checks the `enable_colors` parameter and the environment
+ variables `PROGRESSBAR_ENABLE_COLORS` and `FORCE_COLOR` to determine
+ the color support.
+
+ If `enable_colors` is:
+ - `None`, it checks the environment variables and the terminal
+ compatibility to ANSI.
+ - `True`, it sets the color support to XTERM_256.
+ - `False`, it sets the color support to NONE.
+ - For different values that are not instances of
+ `progressbar.env.ColorSupport`, it raises a ValueError.
+
+ Args:
+ enable_colors (progressbar.env.ColorSupport | None): The color
+ support setting from the user. It can be None, True, False,
+ or an instance of `progressbar.env.ColorSupport`.
+
+ Returns:
+ progressbar.env.ColorSupport: The determined color support.
+
+ Raises:
+ ValueError: If `enable_colors` is not None, True, False, or an
+ instance of `progressbar.env.ColorSupport`.
+ '''
+ color_support: progressbar.env.ColorSupport
if enable_colors is None:
colors = (
progressbar.env.env_flag('PROGRESSBAR_ENABLE_COLORS'),
@@ -252,23 +273,23 @@ def _determine_enable_colors(
for color_enabled in colors:
if color_enabled is not None:
if color_enabled:
- enable_colors = progressbar.env.COLOR_SUPPORT
+ color_support = progressbar.env.COLOR_SUPPORT
else:
- enable_colors = progressbar.env.ColorSupport.NONE
+ color_support = progressbar.env.ColorSupport.NONE
break
- else: # pragma: no cover
- # This scenario should never occur because `is_ansi_terminal`
- # should always be `True` or `False`
- raise ValueError('Unable to determine color support')
+ else:
+ color_support = progressbar.env.ColorSupport.NONE
elif enable_colors is True:
- enable_colors = progressbar.env.ColorSupport.XTERM_256
+ color_support = progressbar.env.ColorSupport.XTERM_256
elif enable_colors is False:
- enable_colors = progressbar.env.ColorSupport.NONE
- elif not isinstance(enable_colors, progressbar.env.ColorSupport):
+ color_support = progressbar.env.ColorSupport.NONE
+ elif isinstance(enable_colors, progressbar.env.ColorSupport):
+ color_support = enable_colors
+ else:
raise ValueError(f'Invalid color support value: {enable_colors}')
- return enable_colors
+ return color_support
def print(self, *args: types.Any, **kwargs: types.Any) -> None:
print(*args, file=self.fd, **kwargs)
@@ -366,8 +387,12 @@ def __init__(self, term_width: int | None = None, **kwargs):
self._handle_resize()
import signal
- self._prev_handle = signal.getsignal(signal.SIGWINCH)
- signal.signal(signal.SIGWINCH, self._handle_resize)
+ self._prev_handle = signal.getsignal(
+ signal.SIGWINCH # type: ignore
+ )
+ signal.signal(
+ signal.SIGWINCH, self._handle_resize # type: ignore
+ )
self.signal_set = True
def _handle_resize(self, signum=None, frame=None):
@@ -381,7 +406,9 @@ def finish(self): # pragma: no cover
with contextlib.suppress(Exception):
import signal
- signal.signal(signal.SIGWINCH, self._prev_handle)
+ signal.signal(
+ signal.SIGWINCH, self._prev_handle # type: ignore
+ )
class StdRedirectMixin(DefaultFdMixin):
@@ -776,7 +803,7 @@ def default_widgets(self):
' ',
widgets.Timer(**self.widget_kwargs),
' ',
- widgets.AdaptiveETA(**self.widget_kwargs),
+ widgets.SmoothingETA(**self.widget_kwargs),
]
else:
return [
@@ -876,7 +903,7 @@ def update(self, value=None, force=False, **kwargs):
if (
value is not None
and value is not base.UnknownLength
- and isinstance(value, int)
+ and isinstance(value, (int, float))
):
if self.max_value is base.UnknownLength:
# Can't compare against unknown lengths so just update
@@ -1071,7 +1098,7 @@ def default_widgets(self):
' ',
widgets.Timer(),
' ',
- widgets.AdaptiveETA(),
+ widgets.SmoothingETA(),
]
else:
return [
diff --git a/progressbar/env.py b/progressbar/env.py
index 07e6666..e29f6fb 100644
--- a/progressbar/env.py
+++ b/progressbar/env.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import contextlib
import enum
import os
import re
@@ -9,13 +10,11 @@
@typing.overload
-def env_flag(name: str, default: bool) -> bool:
- ...
+def env_flag(name: str, default: bool) -> bool: ...
@typing.overload
-def env_flag(name: str, default: bool | None = None) -> bool | None:
- ...
+def env_flag(name: str, default: bool | None = None) -> bool | None: ...
def env_flag(name, default=None):
@@ -41,6 +40,7 @@ class ColorSupport(enum.IntEnum):
XTERM = 16
XTERM_256 = 256
XTERM_TRUECOLOR = 16777216
+ WINDOWS = 8
@classmethod
def from_env(cls):
@@ -51,8 +51,8 @@ def from_env(cls):
will enable 256 color/8 bit support. If they contain `xterm`, we will
enable 16 color support. Otherwise, we will assume no color support.
- If `JUPYTER_COLUMNS` or `JUPYTER_LINES` is set, we will assume true
- color support.
+ If `JUPYTER_COLUMNS` or `JUPYTER_LINES` or `JPY_PARENT_PID` is set, we
+ will assume true color support.
Note that the highest available value will be used! Having
`COLORTERM=truecolor` will override `TERM=xterm-256color`.
@@ -64,11 +64,22 @@ def from_env(cls):
'TERM',
)
- if os.environ.get('JUPYTER_COLUMNS') or os.environ.get(
- 'JUPYTER_LINES',
- ):
+ if JUPYTER:
# Jupyter notebook always supports true color.
return cls.XTERM_TRUECOLOR
+ elif os.name == 'nt':
+ # We can't reliably detect true color support on Windows, so we
+ # will assume it is supported if the console is configured to
+ # support it.
+ from .terminal.os_specific import windows
+
+ if (
+ windows.get_console_mode()
+ & windows.WindowsConsoleModeFlags.ENABLE_PROCESSED_OUTPUT
+ ):
+ return cls.XTERM_TRUECOLOR
+ else:
+ return cls.WINDOWS # pragma: no cover
support = cls.NONE
for variable in variables:
@@ -90,15 +101,15 @@ def from_env(cls):
def is_ansi_terminal(
fd: base.IO,
is_terminal: bool | None = None,
-) -> bool: # pragma: no cover
+) -> bool | None: # pragma: no cover
if is_terminal is None:
- # Jupyter Notebooks define this variable and support progress bars
- if 'JPY_PARENT_PID' in os.environ:
+ # Jupyter Notebooks support progress bars
+ if JUPYTER:
is_terminal = True
# This works for newer versions of pycharm only. With older versions
# there is no way to check.
elif os.environ.get('PYCHARM_HOSTED') == '1' and not os.environ.get(
- 'PYTEST_CURRENT_TEST',
+ 'PYTEST_CURRENT_TEST'
):
is_terminal = True
@@ -108,7 +119,7 @@ def is_ansi_terminal(
# isatty has not been defined we have no way of knowing so we will not
# use ansi. ansi terminals will typically define one of the 2
# environment variables.
- try:
+ with contextlib.suppress(Exception):
is_tty = fd.isatty()
# Try and match any of the huge amount of Linux/Unix ANSI consoles
if is_tty and ANSI_TERM_RE.match(os.environ.get('TERM', '')):
@@ -116,15 +127,20 @@ def is_ansi_terminal(
# ANSICON is a Windows ANSI compatible console
elif 'ANSICON' in os.environ:
is_terminal = True
+ elif os.name == 'nt':
+ from .terminal.os_specific import windows
+
+ return bool(
+ windows.get_console_mode()
+ & windows.WindowsConsoleModeFlags.ENABLE_PROCESSED_OUTPUT,
+ )
else:
is_terminal = None
- except Exception:
- is_terminal = False
- return bool(is_terminal)
+ return is_terminal
-def is_terminal(fd: base.IO, is_terminal: bool | None = None) -> bool:
+def is_terminal(fd: base.IO, is_terminal: bool | None = None) -> bool | None:
if is_terminal is None:
# Full ansi support encompasses what we expect from a terminal
is_terminal = is_ansi_terminal(fd) or None
@@ -141,9 +157,20 @@ def is_terminal(fd: base.IO, is_terminal: bool | None = None) -> bool:
except Exception:
is_terminal = False
- return bool(is_terminal)
+ return is_terminal
+
+# Enable Windows full color mode if possible
+if os.name == 'nt':
+ from .terminal import os_specific
+ os_specific.set_console_mode()
+
+JUPYTER = bool(
+ os.environ.get('JUPYTER_COLUMNS')
+ or os.environ.get('JUPYTER_LINES')
+ or os.environ.get('JPY_PARENT_PID')
+)
COLOR_SUPPORT = ColorSupport.from_env()
ANSI_TERMS = (
'([xe]|bv)term',
diff --git a/progressbar/multi.py b/progressbar/multi.py
index be1ca7d..ae3dd23 100644
--- a/progressbar/multi.py
+++ b/progressbar/multi.py
@@ -129,7 +129,8 @@ def __setitem__(self, key: str, bar: bar.ProgressBar):
bar.label = key
bar.fd = stream.LastLineStream(self.fd)
bar.paused = True
- # Essentially `bar.print = self.print`, but `mypy` doesn't like that
+ # Essentially `bar.print = self.print`, but `mypy` doesn't
+ # like that
bar.print = self.print # type: ignore
# Just in case someone is using a progressbar with a custom
diff --git a/progressbar/terminal/base.py b/progressbar/terminal/base.py
index 8c9b262..895887b 100644
--- a/progressbar/terminal/base.py
+++ b/progressbar/terminal/base.py
@@ -3,6 +3,7 @@
import abc
import collections
import colorsys
+import enum
import threading
from collections import defaultdict
@@ -178,6 +179,79 @@ def column(self, stream):
return column
+class WindowsColors(enum.Enum):
+ BLACK = 0, 0, 0
+ BLUE = 0, 0, 128
+ GREEN = 0, 128, 0
+ CYAN = 0, 128, 128
+ RED = 128, 0, 0
+ MAGENTA = 128, 0, 128
+ YELLOW = 128, 128, 0
+ GREY = 192, 192, 192
+ INTENSE_BLACK = 128, 128, 128
+ INTENSE_BLUE = 0, 0, 255
+ INTENSE_GREEN = 0, 255, 0
+ INTENSE_CYAN = 0, 255, 255
+ INTENSE_RED = 255, 0, 0
+ INTENSE_MAGENTA = 255, 0, 255
+ INTENSE_YELLOW = 255, 255, 0
+ INTENSE_WHITE = 255, 255, 255
+
+ @staticmethod
+ def from_rgb(rgb: types.Tuple[int, int, int]):
+ '''
+ Find the closest WindowsColors to the given RGB color.
+
+ >>> WindowsColors.from_rgb((0, 0, 0))
+
+
+ >>> WindowsColors.from_rgb((255, 255, 255))
+
+
+ >>> WindowsColors.from_rgb((0, 255, 0))
+
+
+ >>> WindowsColors.from_rgb((45, 45, 45))
+
+
+ >>> WindowsColors.from_rgb((128, 0, 128))
+
+ '''
+
+ def color_distance(rgb1, rgb2):
+ return sum((c1 - c2) ** 2 for c1, c2 in zip(rgb1, rgb2))
+
+ return min(
+ WindowsColors,
+ key=lambda color: color_distance(color.value, rgb),
+ )
+
+
+class WindowsColor:
+ '''
+ Windows compatible color class for when ANSI is not supported.
+ Currently a no-op because it is not possible to buffer these colors.
+
+ >>> WindowsColor(WindowsColors.RED)('test')
+ 'test'
+ '''
+
+ __slots__ = ('color',)
+
+ def __init__(self, color: Color):
+ self.color = color
+
+ def __call__(self, text):
+ return text
+ ## In the future we might want to use this, but it requires direct
+ ## printing to stdout and all of our surrounding functions expect
+ ## buffered output so it's not feasible right now. Additionally,
+ ## recent Windows versions all support ANSI codes without issue so
+ ## there is little need.
+ # from progressbar.terminal.os_specific import windows
+ # windows.print_color(text, WindowsColors.from_rgb(self.color.rgb))
+
+
class RGB(collections.namedtuple('RGB', ['red', 'green', 'blue'])):
__slots__ = ()
@@ -207,6 +281,14 @@ def to_ansi_256(self):
blue = round(self.blue / 255 * 5)
return 16 + 36 * red + 6 * green + blue
+ @property
+ def to_windows(self):
+ '''
+ Convert an RGB color (0-255 per channel) to the closest color in the
+ Windows 16 color scheme.
+ '''
+ return WindowsColors.from_rgb((self.red, self.green, self.blue))
+
def interpolate(self, end: RGB, step: float) -> RGB:
return RGB(
int(self.red + (end.red - self.red) * step),
@@ -286,15 +368,24 @@ def __call__(self, value: str) -> str:
@property
def fg(self):
- return SGRColor(self, 38, 39)
+ if env.COLOR_SUPPORT is env.ColorSupport.WINDOWS:
+ return WindowsColor(self)
+ else:
+ return SGRColor(self, 38, 39)
@property
def bg(self):
- return SGRColor(self, 48, 49)
+ if env.COLOR_SUPPORT is env.ColorSupport.WINDOWS:
+ return DummyColor()
+ else:
+ return SGRColor(self, 48, 49)
@property
def underline(self):
- return SGRColor(self, 58, 59)
+ if env.COLOR_SUPPORT is env.ColorSupport.WINDOWS:
+ return DummyColor()
+ else:
+ return SGRColor(self, 58, 59)
@property
def ansi(self) -> types.Optional[str]:
@@ -335,21 +426,21 @@ def __hash__(self):
class Colors:
- by_name: ClassVar[
- defaultdict[str, types.List[Color]]
- ] = collections.defaultdict(list)
- by_lowername: ClassVar[
- defaultdict[str, types.List[Color]]
- ] = collections.defaultdict(list)
- by_hex: ClassVar[
- defaultdict[str, types.List[Color]]
- ] = collections.defaultdict(list)
- by_rgb: ClassVar[
- defaultdict[RGB, types.List[Color]]
- ] = collections.defaultdict(list)
- by_hls: ClassVar[
- defaultdict[HSL, types.List[Color]]
- ] = collections.defaultdict(list)
+ by_name: ClassVar[defaultdict[str, types.List[Color]]] = (
+ collections.defaultdict(list)
+ )
+ by_lowername: ClassVar[defaultdict[str, types.List[Color]]] = (
+ collections.defaultdict(list)
+ )
+ by_hex: ClassVar[defaultdict[str, types.List[Color]]] = (
+ collections.defaultdict(list)
+ )
+ by_rgb: ClassVar[defaultdict[RGB, types.List[Color]]] = (
+ collections.defaultdict(list)
+ )
+ by_hls: ClassVar[defaultdict[HSL, types.List[Color]]] = (
+ collections.defaultdict(list)
+ )
by_xterm: ClassVar[dict[int, Color]] = dict()
@classmethod
@@ -475,6 +566,14 @@ def apply_colors(
return text
+class DummyColor:
+ def __call__(self, text):
+ return text
+
+ def __repr__(self):
+ return 'DummyColor()'
+
+
class SGR(CSI):
_start_code: int
_end_code: int
diff --git a/progressbar/terminal/os_specific/__init__.py b/progressbar/terminal/os_specific/__init__.py
index 3d27cf5..833feeb 100644
--- a/progressbar/terminal/os_specific/__init__.py
+++ b/progressbar/terminal/os_specific/__init__.py
@@ -1,7 +1,8 @@
-import sys
+import os
-if sys.platform.startswith('win'):
+if os.name == 'nt':
from .windows import (
+ get_console_mode as _get_console_mode,
getch as _getch,
reset_console_mode as _reset_console_mode,
set_console_mode as _set_console_mode,
@@ -10,13 +11,17 @@
else:
from .posix import getch as _getch
- def _reset_console_mode():
+ def _reset_console_mode() -> None:
pass
- def _set_console_mode():
- pass
+ def _set_console_mode() -> bool:
+ return False
+
+ def _get_console_mode() -> int:
+ return 0
getch = _getch
reset_console_mode = _reset_console_mode
set_console_mode = _set_console_mode
+get_console_mode = _get_console_mode
diff --git a/progressbar/terminal/os_specific/posix.py b/progressbar/terminal/os_specific/posix.py
index e9bd475..52a9560 100644
--- a/progressbar/terminal/os_specific/posix.py
+++ b/progressbar/terminal/os_specific/posix.py
@@ -5,11 +5,11 @@
def getch():
fd = sys.stdin.fileno()
- old_settings = termios.tcgetattr(fd)
+ old_settings = termios.tcgetattr(fd) # type: ignore
try:
- tty.setraw(sys.stdin.fileno())
+ tty.setraw(sys.stdin.fileno()) # type: ignore
ch = sys.stdin.read(1)
finally:
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) # type: ignore
return ch
diff --git a/progressbar/terminal/os_specific/windows.py b/progressbar/terminal/os_specific/windows.py
index f294845..425d349 100644
--- a/progressbar/terminal/os_specific/windows.py
+++ b/progressbar/terminal/os_specific/windows.py
@@ -5,7 +5,10 @@
Note that the naming convention here is non-pythonic because we are
matching the Windows API naming.
'''
+from __future__ import annotations
+
import ctypes
+import enum
from ctypes.wintypes import (
BOOL as _BOOL,
CHAR as _CHAR,
@@ -19,14 +22,31 @@
_kernel32 = ctypes.windll.Kernel32 # type: ignore
-_ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
-_ENABLE_PROCESSED_OUTPUT = 0x0001
-_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
-
_STD_INPUT_HANDLE = _DWORD(-10)
_STD_OUTPUT_HANDLE = _DWORD(-11)
+class WindowsConsoleModeFlags(enum.IntFlag):
+ ENABLE_ECHO_INPUT = 0x0004
+ ENABLE_EXTENDED_FLAGS = 0x0080
+ ENABLE_INSERT_MODE = 0x0020
+ ENABLE_LINE_INPUT = 0x0002
+ ENABLE_MOUSE_INPUT = 0x0010
+ ENABLE_PROCESSED_INPUT = 0x0001
+ ENABLE_QUICK_EDIT_MODE = 0x0040
+ ENABLE_WINDOW_INPUT = 0x0008
+ ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
+
+ ENABLE_PROCESSED_OUTPUT = 0x0001
+ ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+ DISABLE_NEWLINE_AUTO_RETURN = 0x0008
+ ENABLE_LVB_GRID_WORLDWIDE = 0x0010
+
+ def __str__(self):
+ return f'{self.name} (0x{self.value:04X})'
+
+
_GetConsoleMode = _kernel32.GetConsoleMode
_GetConsoleMode.restype = _BOOL
@@ -39,7 +59,6 @@
_ReadConsoleInput = _kernel32.ReadConsoleInputA
_ReadConsoleInput.restype = _BOOL
-
_h_console_input = _GetStdHandle(_STD_INPUT_HANDLE)
_input_mode = _DWORD()
_GetConsoleMode(_HANDLE(_h_console_input), ctypes.byref(_input_mode))
@@ -54,7 +73,7 @@ class _COORD(ctypes.Structure):
class _FOCUS_EVENT_RECORD(ctypes.Structure):
- _fields_ = (('bSetFocus', _BOOL), )
+ _fields_ = (('bSetFocus', _BOOL),)
class _KEY_EVENT_RECORD(ctypes.Structure):
@@ -72,7 +91,7 @@ class _uchar(ctypes.Union):
class _MENU_EVENT_RECORD(ctypes.Structure):
- _fields_ = (('dwCommandId', _UINT), )
+ _fields_ = (('dwCommandId', _UINT),)
class _MOUSE_EVENT_RECORD(ctypes.Structure):
@@ -85,7 +104,7 @@ class _MOUSE_EVENT_RECORD(ctypes.Structure):
class _WINDOW_BUFFER_SIZE_RECORD(ctypes.Structure):
- _fields_ = (('dwSize', _COORD), )
+ _fields_ = (('dwSize', _COORD),)
class _INPUT_RECORD(ctypes.Structure):
@@ -101,21 +120,38 @@ class _Event(ctypes.Union):
_fields_ = (('EventType', _WORD), ('Event', _Event))
-def reset_console_mode():
+def reset_console_mode() -> None:
_SetConsoleMode(_HANDLE(_h_console_input), _DWORD(_input_mode.value))
_SetConsoleMode(_HANDLE(_h_console_output), _DWORD(_output_mode.value))
-def set_console_mode():
- mode = _input_mode.value | _ENABLE_VIRTUAL_TERMINAL_INPUT
+def set_console_mode() -> bool:
+ mode = (
+ _input_mode.value
+ | WindowsConsoleModeFlags.ENABLE_VIRTUAL_TERMINAL_INPUT
+ )
_SetConsoleMode(_HANDLE(_h_console_input), _DWORD(mode))
mode = (
_output_mode.value
- | _ENABLE_PROCESSED_OUTPUT
- | _ENABLE_VIRTUAL_TERMINAL_PROCESSING
+ | WindowsConsoleModeFlags.ENABLE_PROCESSED_OUTPUT
+ | WindowsConsoleModeFlags.ENABLE_VIRTUAL_TERMINAL_PROCESSING
)
- _SetConsoleMode(_HANDLE(_h_console_output), _DWORD(mode))
+ return bool(_SetConsoleMode(_HANDLE(_h_console_output), _DWORD(mode)))
+
+
+def get_console_mode() -> int:
+ return _input_mode.value
+
+
+def set_text_color(color) -> None:
+ _kernel32.SetConsoleTextAttribute(_h_console_output, color)
+
+
+def print_color(text, color):
+ set_text_color(color)
+ print(text) # noqa: T201
+ set_text_color(7) # Reset to default color, grey
def getch():
diff --git a/progressbar/widgets.py b/progressbar/widgets.py
index 40f2972..e5046b6 100644
--- a/progressbar/widgets.py
+++ b/progressbar/widgets.py
@@ -13,7 +13,7 @@
from python_utils import containers, converters, types
-from . import base, terminal, utils
+from . import algorithms, base, terminal, utils
from .terminal import colors
if types.TYPE_CHECKING:
@@ -604,7 +604,17 @@ class AdaptiveETA(ETA, SamplesMixin):
Very convenient for resuming the progress halfway.
'''
- def __init__(self, **kwargs):
+ exponential_smoothing: bool
+ exponential_smoothing_factor: float
+
+ def __init__(
+ self,
+ exponential_smoothing=True,
+ exponential_smoothing_factor=0.1,
+ **kwargs,
+ ):
+ self.exponential_smoothing = exponential_smoothing
+ self.exponential_smoothing_factor = exponential_smoothing_factor
ETA.__init__(self, **kwargs)
SamplesMixin.__init__(self, **kwargs)
@@ -628,6 +638,50 @@ def __call__(
return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
+class SmoothingETA(ETA):
+ '''
+ WidgetBase which attempts to estimate the time of arrival using an
+ exponential moving average (EMA) of the speed.
+
+ EMA applies more weight to recent data points and less to older ones,
+ and doesn't require storing all past values. This approach works well
+ with varying data points and smooths out fluctuations effectively.
+ '''
+
+ smoothing_algorithm: algorithms.SmoothingAlgorithm
+ smoothing_parameters: dict[str, float]
+
+ def __init__(
+ self,
+ smoothing_algorithm: type[
+ algorithms.SmoothingAlgorithm
+ ] = algorithms.ExponentialMovingAverage,
+ smoothing_parameters: dict[str, float] | None = None,
+ **kwargs,
+ ):
+ self.smoothing_parameters = smoothing_parameters or {}
+ self.smoothing_algorithm = smoothing_algorithm(
+ **(self.smoothing_parameters or {}),
+ )
+ ETA.__init__(self, **kwargs)
+
+ def __call__(
+ self,
+ progress: ProgressBarMixinBase,
+ data: Data,
+ value=None,
+ elapsed=None,
+ ):
+ if value is None: # pragma: no branch
+ value = data['value']
+
+ if elapsed is None: # pragma: no branch
+ elapsed = data['time_elapsed']
+
+ self.smoothing_algorithm.update(value, elapsed)
+ return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
+
+
class DataSize(FormatWidgetMixin, WidgetBase):
'''
Widget for showing an amount of data transferred/processed.
@@ -1202,7 +1256,8 @@ def get_values(self, progress: ProgressBarMixinBase, data: Data):
if not 0 <= value <= 1:
raise ValueError(
- f'Range value needs to be in the range [0..1], got {value}',
+ 'Range value needs to be in the range [0..1], '
+ f'got {value}',
)
range_ = value * (len(ranges) - 1)
diff --git a/pyproject.toml b/pyproject.toml
index c4c2a95..904e717 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -108,8 +108,8 @@ exclude = ['docs*', 'tests*']
[tool.setuptools]
include-package-data = true
-# [project.scripts]
-# progressbar2 = 'progressbar.cli:main'
+[project.scripts]
+progressbar = 'progressbar.cli:main'
[project.optional-dependencies]
docs = ['sphinx>=1.8.5', 'sphinx-autodoc-typehints>=1.6.0']
@@ -121,6 +121,7 @@ tests = [
'pytest-mypy',
'pytest>=4.6.9',
'sphinx>=1.8.5',
+ 'pywin32; sys_platform == "win32"',
]
[project.urls]
@@ -180,6 +181,7 @@ exclude_lines = [
'if __name__ == .__main__.:',
'if types.TYPE_CHECKING:',
'@typing.overload',
+ 'if os.name == .nt.:',
]
[tool.pyright]
diff --git a/ruff.toml b/ruff.toml
index 083e321..bd1b288 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -5,7 +5,7 @@ target-version = 'py38'
src = ['progressbar']
-ignore = [
+lint.ignore = [
'A001', # Variable {name} is shadowing a Python builtin
'A002', # Argument {name} is shadowing a Python builtin
'A003', # Class attribute {name} is shadowing a Python builtin
@@ -21,9 +21,14 @@ ignore = [
'C408', # Unnecessary {obj_type} call (rewrite as a literal)
'SIM114', # Combine `if` branches using logical `or` operator
'RET506', # Unnecessary `else` after `raise` statement
+ 'Q001', # Remove bad quotes
+ 'Q002', # Remove bad quotes
+ 'COM812', # Missing trailing comma in a list
+ 'ISC001', # String concatenation with implicit str conversion
+ 'SIM108', # Ternary operators are not always more readable
]
line-length = 80
-select = [
+lint.select = [
'A', # flake8-builtins
'ASYNC', # flake8 async checker
'B', # flake8-bugbear
@@ -56,20 +61,42 @@ select = [
'UP', # pyupgrade
]
-[per-file-ignores]
+[lint.per-file-ignores]
'tests/*' = ['INP001', 'T201', 'T203']
-'examples.py' = ['T201']
+'examples.py' = ['T201', 'N806']
+'docs/conf.py' = ['E501', 'INP001']
+'docs/_theme/flask_theme_support.py' = ['RUF012', 'INP001']
-[pydocstyle]
+[lint.pydocstyle]
convention = 'google'
-ignore-decorators = ['typing.overload']
+ignore-decorators = [
+ 'typing.overload',
+ 'typing.override',
+]
-[isort]
+[lint.isort]
case-sensitive = true
combine-as-imports = true
force-wrap-aliases = true
-[flake8-quotes]
+[lint.flake8-quotes]
docstring-quotes = 'single'
inline-quotes = 'single'
multiline-quotes = 'single'
+
+[format]
+line-ending = 'lf'
+indent-style = 'space'
+quote-style = 'single'
+docstring-code-format = true
+skip-magic-trailing-comma = false
+exclude = [
+ '__init__.py',
+]
+
+[lint.pycodestyle]
+max-line-length = 79
+
+[lint.flake8-pytest-style]
+mark-parentheses = true
+
diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py
new file mode 100644
index 0000000..85027ce
--- /dev/null
+++ b/tests/test_algorithms.py
@@ -0,0 +1,47 @@
+from datetime import timedelta
+
+import pytest
+from progressbar import algorithms
+
+
+def test_ema_initialization():
+ ema = algorithms.ExponentialMovingAverage()
+ assert ema.alpha == 0.5
+ assert ema.value == 0
+
+@pytest.mark.parametrize('alpha, new_value, expected', [
+ (0.5, 10, 5),
+ (0.1, 20, 2),
+ (0.9, 30, 27),
+ (0.3, 15, 4.5),
+ (0.7, 40, 28),
+ (0.5, 0, 0),
+ (0.2, 100, 20),
+ (0.8, 50, 40),
+])
+def test_ema_update(alpha, new_value, expected):
+ ema = algorithms.ExponentialMovingAverage(alpha)
+ result = ema.update(new_value, timedelta(seconds=1))
+ assert result == expected
+
+def test_dema_initialization():
+ dema = algorithms.DoubleExponentialMovingAverage()
+ assert dema.alpha == 0.5
+ assert dema.ema1 == 0
+ assert dema.ema2 == 0
+
+@pytest.mark.parametrize('alpha, new_value, expected', [
+ (0.5, 10, 7.5),
+ (0.1, 20, 3.8),
+ (0.9, 30, 29.7),
+ (0.3, 15, 7.65),
+ (0.5, 0, 0),
+ (0.2, 100, 36.0),
+ (0.8, 50, 48.0),
+])
+def test_dema_update(alpha, new_value, expected):
+ dema = algorithms.DoubleExponentialMovingAverage(alpha)
+ result = dema.update(new_value, timedelta(seconds=1))
+ assert result == expected
+
+# Additional test functions can be added here as needed.
diff --git a/tests/test_color.py b/tests/test_color.py
index 1a6657e..dc7c2bb 100644
--- a/tests/test_color.py
+++ b/tests/test_color.py
@@ -1,14 +1,33 @@
from __future__ import annotations
+import os
import typing
import progressbar
-import progressbar.env
import progressbar.terminal
import pytest
from progressbar import env, terminal, widgets
from progressbar.terminal import Colors, apply_colors, colors
+ENVIRONMENT_VARIABLES = [
+ 'PROGRESSBAR_ENABLE_COLORS',
+ 'FORCE_COLOR',
+ 'COLORTERM',
+ 'TERM',
+ 'JUPYTER_COLUMNS',
+ 'JUPYTER_LINES',
+ 'JPY_PARENT_PID',
+]
+
+
+@pytest.fixture(autouse=True)
+def clear_env(monkeypatch: pytest.MonkeyPatch):
+ # Clear all environment variables that might affect the tests
+ for variable in ENVIRONMENT_VARIABLES:
+ monkeypatch.delenv(variable, raising=False)
+
+ monkeypatch.setattr(env, 'JUPYTER', False)
+
@pytest.mark.parametrize(
'variable',
@@ -17,18 +36,30 @@
'FORCE_COLOR',
],
)
-def test_color_environment_variables(monkeypatch, variable):
+def test_color_environment_variables(monkeypatch: pytest.MonkeyPatch,
+ variable):
+ if os.name == 'nt':
+ # Windows has special handling so we need to disable that to make the
+ # tests work properly
+ monkeypatch.setattr(os, 'name', 'posix')
+
monkeypatch.setattr(
env,
'COLOR_SUPPORT',
- progressbar.env.ColorSupport.XTERM_256,
+ env.ColorSupport.XTERM_256,
)
- monkeypatch.setenv(variable, '1')
+ monkeypatch.setenv(variable, 'true')
bar = progressbar.ProgressBar()
+ assert not env.is_ansi_terminal(bar.fd)
+ assert not bar.is_ansi_terminal
assert bar.enable_colors
- monkeypatch.setenv(variable, '0')
+ monkeypatch.setenv(variable, 'false')
+ bar = progressbar.ProgressBar()
+ assert not bar.enable_colors
+
+ monkeypatch.setenv(variable, '')
bar = progressbar.ProgressBar()
assert not bar.enable_colors
@@ -54,11 +85,13 @@ def test_color_environment_variables(monkeypatch, variable):
],
)
def test_color_support_from_env(monkeypatch, variable, value):
- monkeypatch.setenv('JUPYTER_COLUMNS', '')
- monkeypatch.setenv('JUPYTER_LINES', '')
+ if os.name == 'nt':
+ # Windows has special handling so we need to disable that to make the
+ # tests work properly
+ monkeypatch.setattr(os, 'name', 'posix')
monkeypatch.setenv(variable, value)
- progressbar.env.ColorSupport.from_env()
+ env.ColorSupport.from_env()
@pytest.mark.parametrize(
@@ -69,8 +102,15 @@ def test_color_support_from_env(monkeypatch, variable, value):
],
)
def test_color_support_from_env_jupyter(monkeypatch, variable):
- monkeypatch.setenv(variable, '80')
- progressbar.env.ColorSupport.from_env()
+ monkeypatch.setattr(env, 'JUPYTER', True)
+ assert env.ColorSupport.from_env() == env.ColorSupport.XTERM_TRUECOLOR
+
+ # Sanity check
+ monkeypatch.setattr(env, 'JUPYTER', False)
+ if os.name == 'nt':
+ assert env.ColorSupport.from_env() == env.ColorSupport.WINDOWS
+ else:
+ assert env.ColorSupport.from_env() == env.ColorSupport.NONE
def test_enable_colors_flags():
@@ -81,7 +121,7 @@ def test_enable_colors_flags():
assert not bar.enable_colors
bar = progressbar.ProgressBar(
- enable_colors=progressbar.env.ColorSupport.XTERM_TRUECOLOR,
+ enable_colors=env.ColorSupport.XTERM_TRUECOLOR,
)
assert bar.enable_colors
@@ -166,7 +206,7 @@ def test_no_color_widgets(widget):
).uses_colors
-def test_colors():
+def test_colors(monkeypatch):
for colors_ in Colors.by_rgb.values():
for color in colors_:
rgb = color.rgb
@@ -174,18 +214,27 @@ def test_colors():
assert rgb.hex
assert rgb.to_ansi_16 is not None
assert rgb.to_ansi_256 is not None
- assert color.underline
+ assert rgb.to_windows is not None
+
+ with monkeypatch.context() as context:
+ context.setattr(env,'COLOR_SUPPORT', env.ColorSupport.XTERM)
+ assert color.underline
+ context.setattr(env,'COLOR_SUPPORT', env.ColorSupport.WINDOWS)
+ assert color.underline
+
assert color.fg
assert color.bg
assert str(color)
assert str(rgb)
+ assert color('test')
def test_color():
color = colors.red
- assert color('x') == color.fg('x') != 'x'
- assert color.fg('x') != color.bg('x') != 'x'
- assert color.fg('x') != color.underline('x') != 'x'
+ if os.name != 'nt':
+ assert color('x') == color.fg('x') != 'x'
+ assert color.fg('x') != color.bg('x') != 'x'
+ assert color.fg('x') != color.underline('x') != 'x'
# Color hashes are based on the RGB value
assert hash(color) == hash(terminal.Color(color.rgb, None, None, None))
Colors.register(color.rgb)
@@ -287,7 +336,7 @@ def test_apply_colors(text, fg, bg, fg_none, bg_none, percentage, expected,
monkeypatch.setattr(
env,
'COLOR_SUPPORT',
- progressbar.env.ColorSupport.XTERM_256,
+ env.ColorSupport.XTERM_256,
)
assert (
apply_colors(
@@ -302,6 +351,22 @@ def test_apply_colors(text, fg, bg, fg_none, bg_none, percentage, expected,
)
+def test_windows_colors(monkeypatch):
+ monkeypatch.setattr(env, 'COLOR_SUPPORT', env.ColorSupport.WINDOWS)
+ assert (
+ apply_colors(
+ 'test',
+ fg=colors.red,
+ bg=colors.red,
+ fg_none=colors.red,
+ bg_none=colors.red,
+ percentage=1,
+ )
+ == 'test'
+ )
+ colors.red.underline('test')
+
+
def test_ansi_color(monkeypatch):
color = progressbar.terminal.Color(
colors.red.rgb,
diff --git a/tests/test_failure.py b/tests/test_failure.py
index cee84b7..4c10546 100644
--- a/tests/test_failure.py
+++ b/tests/test_failure.py
@@ -1,10 +1,12 @@
+import logging
import time
import progressbar
import pytest
-def test_missing_format_values():
+def test_missing_format_values(caplog):
+ caplog.set_level(logging.CRITICAL, logger='progressbar.widgets')
with pytest.raises(KeyError):
p = progressbar.ProgressBar(
widgets=[progressbar.widgets.FormatLabel('%(x)s')],
diff --git a/tests/test_monitor_progress.py b/tests/test_monitor_progress.py
index 7105254..0769391 100644
--- a/tests/test_monitor_progress.py
+++ b/tests/test_monitor_progress.py
@@ -80,20 +80,18 @@ def test_list_example(testdir):
line.rstrip() for line in _non_empty_lines(result.stderr.lines)
]
pprint.pprint(result.stderr.lines, width=70)
- result.stderr.fnmatch_lines(
- [
- ' 0% (0 of 9) | | Elapsed Time: ?:00:00 ETA: --:--:--',
- ' 11% (1 of 9) |# | Elapsed Time: ?:00:01 ETA: ?:00:08',
- ' 22% (2 of 9) |## | Elapsed Time: ?:00:02 ETA: ?:00:07',
- ' 33% (3 of 9) |#### | Elapsed Time: ?:00:03 ETA: ?:00:06',
- ' 44% (4 of 9) |##### | Elapsed Time: ?:00:04 ETA: ?:00:05',
- ' 55% (5 of 9) |###### | Elapsed Time: ?:00:05 ETA: ?:00:04',
- ' 66% (6 of 9) |######## | Elapsed Time: ?:00:06 ETA: ?:00:03',
- ' 77% (7 of 9) |######### | Elapsed Time: ?:00:07 ETA: ?:00:02',
- ' 88% (8 of 9) |########## | Elapsed Time: ?:00:08 ETA: ?:00:01',
- '100% (9 of 9) |############| Elapsed Time: ?:00:09 Time: ?:00:09',
- ],
- )
+ result.stderr.fnmatch_lines([
+ ' 0% (0 of 9) | | Elapsed Time: ?:00:00 ETA: --:--:--',
+ ' 11% (1 of 9) |# | Elapsed Time: ?:00:01 ETA: ?:00:08',
+ ' 22% (2 of 9) |## | Elapsed Time: ?:00:02 ETA: ?:00:07',
+ ' 33% (3 of 9) |#### | Elapsed Time: ?:00:03 ETA: ?:00:06',
+ ' 44% (4 of 9) |##### | Elapsed Time: ?:00:04 ETA: ?:00:05',
+ ' 55% (5 of 9) |###### | Elapsed Time: ?:00:05 ETA: ?:00:04',
+ ' 66% (6 of 9) |######## | Elapsed Time: ?:00:06 ETA: ?:00:03',
+ ' 77% (7 of 9) |######### | Elapsed Time: ?:00:07 ETA: ?:00:02',
+ ' 88% (8 of 9) |########## | Elapsed Time: ?:00:08 ETA: ?:00:01',
+ '100% (9 of 9) |############| Elapsed Time: ?:00:09 Time: ?:00:09',
+ ])
def test_generator_example(testdir):
@@ -140,20 +138,19 @@ def test_rapid_updates(testdir):
)
result.stderr.lines = _non_empty_lines(result.stderr.lines)
pprint.pprint(result.stderr.lines, width=70)
- result.stderr.fnmatch_lines(
- [
- ' 0% (0 of 10) | | Elapsed Time: ?:00:00 ETA: --:--:--',
- ' 10% (1 of 10) | | Elapsed Time: ?:00:01 ETA: ?:00:09',
- ' 20% (2 of 10) |# | Elapsed Time: ?:00:02 ETA: ?:00:08',
- ' 30% (3 of 10) |# | Elapsed Time: ?:00:03 ETA: ?:00:07',
- ' 40% (4 of 10) |## | Elapsed Time: ?:00:04 ETA: ?:00:06',
- ' 50% (5 of 10) |### | Elapsed Time: ?:00:05 ETA: ?:00:05',
- ' 60% (6 of 10) |### | Elapsed Time: ?:00:07 ETA: ?:00:06',
- ' 70% (7 of 10) |#### | Elapsed Time: ?:00:09 ETA: ?:00:06',
- ' 80% (8 of 10) |#### | Elapsed Time: ?:00:11 ETA: ?:00:04',
- ' 90% (9 of 10) |##### | Elapsed Time: ?:00:13 ETA: ?:00:02',
- '100% (10 of 10) |#####| Elapsed Time: ?:00:15 Time: ?:00:15',
- ],
+ result.stderr.fnmatch_lines([
+ ' 0% (0 of 10) | | Elapsed Time: 0:00:00 ETA: --:--:--',
+ ' 10% (1 of 10) | | Elapsed Time: 0:00:01 ETA: 0:00:09',
+ ' 20% (2 of 10) |# | Elapsed Time: 0:00:02 ETA: 0:00:08',
+ ' 30% (3 of 10) |# | Elapsed Time: 0:00:03 ETA: 0:00:07',
+ ' 40% (4 of 10) |## | Elapsed Time: 0:00:04 ETA: 0:00:06',
+ ' 50% (5 of 10) |### | Elapsed Time: 0:00:05 ETA: 0:00:05',
+ ' 60% (6 of 10) |### | Elapsed Time: 0:00:07 ETA: 0:00:04',
+ ' 70% (7 of 10) |#### | Elapsed Time: 0:00:09 ETA: 0:00:03',
+ ' 80% (8 of 10) |#### | Elapsed Time: 0:00:11 ETA: 0:00:02',
+ ' 90% (9 of 10) |##### | Elapsed Time: 0:00:13 ETA: 0:00:01',
+ '100% (10 of 10) |#####| Elapsed Time: 0:00:15 Time: 0:00:15',
+ ],
)
diff --git a/tests/test_progressbar.py b/tests/test_progressbar.py
index d418d4c..d329424 100644
--- a/tests/test_progressbar.py
+++ b/tests/test_progressbar.py
@@ -68,3 +68,9 @@ def test_dirty():
bar.finish(dirty=True)
assert bar.finished()
assert bar.started()
+
+
+def test_negative_maximum():
+ with pytest.raises(ValueError), progressbar.ProgressBar(
+ max_value=-1) as progress:
+ progress.start()
diff --git a/tests/test_progressbar_command.py b/tests/test_progressbar_command.py
new file mode 100644
index 0000000..05a3ab0
--- /dev/null
+++ b/tests/test_progressbar_command.py
@@ -0,0 +1,102 @@
+import io
+
+import progressbar.__main__ as main
+import pytest
+
+
+def test_size_to_bytes():
+ assert main.size_to_bytes('1') == 1
+ assert main.size_to_bytes('1k') == 1024
+ assert main.size_to_bytes('1m') == 1048576
+ assert main.size_to_bytes('1g') == 1073741824
+ assert main.size_to_bytes('1p') == 1125899906842624
+
+ assert main.size_to_bytes('1024') == 1024
+ assert main.size_to_bytes('1024k') == 1048576
+ assert main.size_to_bytes('1024m') == 1073741824
+ assert main.size_to_bytes('1024g') == 1099511627776
+ assert main.size_to_bytes('1024p') == 1152921504606846976
+
+
+def test_filename_to_bytes(tmp_path):
+ file = tmp_path / 'test'
+ file.write_text('test')
+ assert main.size_to_bytes(f'@{file}') == 4
+
+ with pytest.raises(FileNotFoundError):
+ main.size_to_bytes(f'@{tmp_path / "nonexistent"}')
+
+
+def test_create_argument_parser():
+ parser = main.create_argument_parser()
+ args = parser.parse_args(
+ ['-p', '-t', '-e', '-r', '-a', '-b', '-8', '-T', '-n', '-q',
+ 'input', '-o', 'output'])
+ assert args.progress is True
+ assert args.timer is True
+ assert args.eta is True
+ assert args.rate is True
+ assert args.average_rate is True
+ assert args.bytes is True
+ assert args.bits is True
+ assert args.buffer_percent is True
+ assert args.last_written is None
+ assert args.format is None
+ assert args.numeric is True
+ assert args.quiet is True
+ assert args.input == ['input']
+ assert args.output == 'output'
+
+
+def test_main_binary(capsys):
+ # Call the main function with different command line arguments
+ main.main(
+ ['-p', '-t', '-e', '-r', '-a', '-b', '-8', '-T', '-n', '-q', __file__])
+
+ captured = capsys.readouterr()
+ assert 'test_main(capsys):' in captured.out
+
+
+def test_main_lines(capsys):
+ # Call the main function with different command line arguments
+ main.main(
+ ['-p', '-t', '-e', '-r', '-a', '-b', '-8', '-T', '-n', '-q', '-l',
+ '-s', f'@{__file__}',
+ __file__])
+
+ captured = capsys.readouterr()
+ assert 'test_main(capsys):' in captured.out
+
+
+class Input(io.StringIO):
+ buffer: io.BytesIO
+
+ @classmethod
+ def create(cls, text: str):
+ instance = cls(text)
+ instance.buffer = io.BytesIO(text.encode())
+ return instance
+
+
+def test_main_lines_output(monkeypatch, tmp_path):
+ text = 'my input'
+ monkeypatch.setattr('sys.stdin', Input.create(text))
+ output_filename = tmp_path / 'output'
+ main.main(['-l', '-o', str(output_filename)])
+
+ assert output_filename.read_text() == text
+
+
+def test_main_bytes_output(monkeypatch, tmp_path):
+ text = 'my input'
+
+ monkeypatch.setattr('sys.stdin', Input.create(text))
+ output_filename = tmp_path / 'output'
+ main.main(['-o', str(output_filename)])
+
+ assert output_filename.read_text() == f'{text}'
+
+
+def test_missing_input(tmp_path):
+ with pytest.raises(SystemExit):
+ main.main([str(tmp_path / 'output')])
diff --git a/tests/test_stream.py b/tests/test_stream.py
index c92edf7..1803ffd 100644
--- a/tests/test_stream.py
+++ b/tests/test_stream.py
@@ -1,4 +1,5 @@
import io
+import os
import sys
import progressbar
@@ -98,6 +99,7 @@ def test_no_newlines():
@pytest.mark.parametrize('stream', [sys.__stdout__, sys.__stderr__])
+@pytest.mark.skipif(os.name == 'nt', reason='Windows does not support this')
def test_fd_as_standard_streams(stream):
with progressbar.ProgressBar(fd=stream) as pb:
for i in range(101):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 34bd0da..8003204 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -38,17 +38,17 @@ def test_is_terminal(monkeypatch):
fd = io.StringIO()
monkeypatch.delenv('PROGRESSBAR_IS_TERMINAL', raising=False)
- monkeypatch.delenv('JPY_PARENT_PID', raising=False)
+ monkeypatch.setattr(progressbar.env, 'JUPYTER', False)
assert progressbar.env.is_terminal(fd) is False
assert progressbar.env.is_terminal(fd, True) is True
assert progressbar.env.is_terminal(fd, False) is False
- monkeypatch.setenv('JPY_PARENT_PID', '123')
+ monkeypatch.setattr(progressbar.env, 'JUPYTER', True)
assert progressbar.env.is_terminal(fd) is True
- monkeypatch.delenv('JPY_PARENT_PID')
# Sanity check
+ monkeypatch.setattr(progressbar.env, 'JUPYTER', False)
assert progressbar.env.is_terminal(fd) is False
monkeypatch.setenv('PROGRESSBAR_IS_TERMINAL', 'true')
@@ -65,27 +65,27 @@ def test_is_ansi_terminal(monkeypatch):
fd = io.StringIO()
monkeypatch.delenv('PROGRESSBAR_IS_TERMINAL', raising=False)
- monkeypatch.delenv('JPY_PARENT_PID', raising=False)
+ monkeypatch.setattr(progressbar.env, 'JUPYTER', False)
- assert progressbar.env.is_ansi_terminal(fd) is False
+ assert not progressbar.env.is_ansi_terminal(fd)
assert progressbar.env.is_ansi_terminal(fd, True) is True
assert progressbar.env.is_ansi_terminal(fd, False) is False
- monkeypatch.setenv('JPY_PARENT_PID', '123')
+ monkeypatch.setattr(progressbar.env, 'JUPYTER', True)
assert progressbar.env.is_ansi_terminal(fd) is True
- monkeypatch.delenv('JPY_PARENT_PID')
+ monkeypatch.setattr(progressbar.env, 'JUPYTER', False)
# Sanity check
- assert progressbar.env.is_ansi_terminal(fd) is False
+ assert not progressbar.env.is_ansi_terminal(fd)
monkeypatch.setenv('PROGRESSBAR_IS_TERMINAL', 'true')
- assert progressbar.env.is_ansi_terminal(fd) is False
+ assert not progressbar.env.is_ansi_terminal(fd)
monkeypatch.setenv('PROGRESSBAR_IS_TERMINAL', 'false')
- assert progressbar.env.is_ansi_terminal(fd) is False
+ assert not progressbar.env.is_ansi_terminal(fd)
monkeypatch.delenv('PROGRESSBAR_IS_TERMINAL')
# Sanity check
- assert progressbar.env.is_ansi_terminal(fd) is False
+ assert not progressbar.env.is_ansi_terminal(fd)
# Fake TTY mode for environment testing
fd.isatty = lambda: True
@@ -102,9 +102,9 @@ def test_is_ansi_terminal(monkeypatch):
monkeypatch.setenv('ANSICON', 'true')
assert progressbar.env.is_ansi_terminal(fd) is True
monkeypatch.delenv('ANSICON')
- assert progressbar.env.is_ansi_terminal(fd) is False
+ assert not progressbar.env.is_ansi_terminal(fd)
def raise_error():
raise RuntimeError('test')
fd.isatty = raise_error
- assert progressbar.env.is_ansi_terminal(fd) is False
+ assert not progressbar.env.is_ansi_terminal(fd)
diff --git a/tests/test_windows.py b/tests/test_windows.py
new file mode 100644
index 0000000..be2e2a9
--- /dev/null
+++ b/tests/test_windows.py
@@ -0,0 +1,84 @@
+import os
+import sys
+import time
+
+import pytest
+
+if os.name == 'nt':
+ import win32console # "pip install pypiwin32" to get this
+else:
+ pytest.skip('skipping windows-only tests', allow_module_level=True)
+
+import progressbar
+
+pytest_plugins = 'pytester'
+_WIDGETS = [progressbar.Percentage(), ' ',
+ progressbar.Bar(), ' ',
+ progressbar.FileTransferSpeed(), ' ',
+ progressbar.ETA()]
+_MB = 1024 * 1024
+
+
+# ---------------------------------------------------------------------------
+def scrape_console(line_count):
+ pcsb = win32console.GetStdHandle(win32console.STD_OUTPUT_HANDLE)
+ csbi = pcsb.GetConsoleScreenBufferInfo()
+ col_max = csbi['Size'].X
+ row_max = csbi['CursorPosition'].Y
+
+ line_count = min(line_count, row_max)
+ lines = []
+ for row in range(line_count):
+ pct = win32console.PyCOORDType(0, row + row_max - line_count)
+ line = pcsb.ReadConsoleOutputCharacter(col_max, pct)
+ lines.append(line.rstrip())
+ return lines
+
+
+# ---------------------------------------------------------------------------
+def runprogress():
+ print('***BEGIN***')
+ b = progressbar.ProgressBar(
+ widgets=['example.m4v: ', *_WIDGETS],
+ max_value=10 * _MB,
+ )
+ for i in range(10):
+ b.update((i + 1) * _MB)
+ time.sleep(0.25)
+ b.finish()
+ print('***END***')
+ return 0
+
+
+# ---------------------------------------------------------------------------
+def find(lines, x):
+ try:
+ return lines.index(x)
+ except ValueError:
+ return -sys.maxsize
+
+
+# ---------------------------------------------------------------------------
+def test_windows(testdir: pytest.Testdir) -> None:
+ testdir.run(sys.executable, '-c',
+ 'import progressbar; print(progressbar.__file__)')
+
+
+def main():
+ runprogress()
+
+ scraped_lines = scrape_console(100)
+ # reverse lines so we find the LAST instances of output.
+ scraped_lines.reverse()
+ index_begin = find(scraped_lines, '***BEGIN***')
+ index_end = find(scraped_lines, '***END***')
+
+ if index_end + 2 != index_begin:
+ print('ERROR: Unexpected multi-line output from progressbar')
+ print(f'{index_begin=} {index_end=}')
+ return 1
+ return 0
+
+
+if __name__ == '__main__':
+ main()