From b3f1c4db78e3cd85ec187da866a71822805912e7 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 9 Sep 2020 15:25:18 +0200 Subject: [PATCH 01/66] Incrementing version to v3.53.1 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index 2dd04577..339835e2 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '3.53.0' +__version__ = '3.53.1' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From 2e0e47422b6912cbb2071d32b9ef08c0ff4a0b9a Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 15 Mar 2021 01:40:40 +0100 Subject: [PATCH 02/66] Update readme to fix #245 --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index cdda4301..eb155c54 100644 --- a/README.rst +++ b/README.rst @@ -37,6 +37,8 @@ Introduction A text progress bar is typically used to display the progress of a long running operation, providing a visual cue that processing is underway. +The progressbar is based on the old Python progressbar package that was published on the now defunct Google Code. Since that project was completely abandoned by its developer and the developer did not respond to email, I decided to fork the package. This package is still backwards compatible with the original progressbar package so you can safely use it as a drop-in replacement for existing project. + The ProgressBar class manages the current progress, and the format of the line is given by a number of widgets. A widget is an object that may display differently depending on the state of the progress bar. There are many types From af28ce13c8ad2b485599af1867dd467ab9545895 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Sun, 1 Aug 2021 18:40:24 +0200 Subject: [PATCH 03/66] switch to flake8 only --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index b23190a7..d5462630 100644 --- a/setup.py +++ b/setup.py @@ -45,8 +45,6 @@ def run_tests(self): 'flake8>=3.7.7', 'pytest>=4.6.9', 'pytest-cov>=2.6.1', - 'pytest-flakes>=4.0.0', - 'pytest-pep8>=1.0.6', 'freezegun>=0.3.11', 'sphinx>=1.8.5', ] From fbb2f4d18703f387349e77c126214d8eb9bd89c1 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 9 Sep 2021 03:11:58 +0200 Subject: [PATCH 04/66] applied isatty fix thanks to @piotrbartman to fix #254 --- progressbar/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/progressbar/utils.py b/progressbar/utils.py index 7ef1790a..f1e2dd42 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -191,6 +191,9 @@ def __init__(self, target, capturing=False, listeners=set()): self.listeners = listeners self.needs_clear = False + def isatty(self): + return self.target.isatty() + def write(self, value): if self.capturing: self.buffer.write(value) From 1828744e6884be1fedf0e5b980ff201f9c2f7b36 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 9 Sep 2021 03:12:09 +0200 Subject: [PATCH 05/66] Incrementing version to v3.53.2 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index 339835e2..4294f43e 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '3.53.1' +__version__ = '3.53.2' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From 236d2f6985e364dcf30c0a20660ffbe374beec3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 13 Sep 2021 21:20:49 +0200 Subject: [PATCH 06/66] Stop using deprecated distutils.util Using distutils emits a DeprecationWarning with python 3.10 at runtime. (And the module is slated for removal in python 3.12.) The list of accepted strings is taken from https://docs.python.org/3/distutils/apiref.html#distutils.util.strtobool --- progressbar/utils.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/progressbar/utils.py b/progressbar/utils.py index f1e2dd42..a9dc6f54 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -1,5 +1,4 @@ from __future__ import absolute_import -import distutils.util import atexit import io import os @@ -173,13 +172,15 @@ def env_flag(name, default=None): Accepts environt variables formatted as y/n, yes/no, 1/0, true/false, on/off, and returns it as a boolean - If the environt variable is not defined, or has an unknown value, returns - `default` + If the environment variable is not defined, or has an unknown value, + returns `default` ''' - try: - return bool(distutils.util.strtobool(os.environ.get(name, ''))) - except ValueError: - return default + v = os.getenv(name) + if v and v.lower() in ('y', 'yes', 't', 'true', 'on', '1'): + return True + if v and v.lower() in ('n', 'no', 'f', 'false', 'off', '0'): + return False + return default class WrappingIO: From 8a96eef31924487132d8f5dc58c3eb8bb1add269 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 15 Sep 2021 00:42:42 +0200 Subject: [PATCH 07/66] Incrementing version to v3.53.3 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index 4294f43e..6832fe2a 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '3.53.2' +__version__ = '3.53.3' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From 8ba5064a5e0c65bc5b59c4020d24d4af7cdb6fbb Mon Sep 17 00:00:00 2001 From: Zannick Date: Mon, 4 Oct 2021 13:02:02 -0700 Subject: [PATCH 08/66] Allow customizing the N/A% string in Percentage. Move the selection of whether to use N/A% to a separate method. --- progressbar/widgets.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index f514f7dd..02ef4824 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -628,17 +628,20 @@ def __call__(self, progress, data, format=None): class Percentage(FormatWidgetMixin, WidgetBase): '''Displays the current percentage as a number with a percent sign.''' - def __init__(self, format='%(percentage)3d%%', **kwargs): + def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs): + self.na = na FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, format=format, **kwargs) def __call__(self, progress, data, format=None): + return FormatWidgetMixin.__call__(self, progress, data, self.get_format(progress, data)) + + def get_format(self, progress, data): # If percentage is not available, display N/A% if 'percentage' in data and not data['percentage']: - return FormatWidgetMixin.__call__(self, progress, data, - format='N/A%%') + return self.na - return FormatWidgetMixin.__call__(self, progress, data) + return self.format class SimpleProgress(FormatWidgetMixin, WidgetBase): From 0eae5ef7cd5fe36262cb399c137082f6168e1ce8 Mon Sep 17 00:00:00 2001 From: Zannick Date: Mon, 4 Oct 2021 13:03:46 -0700 Subject: [PATCH 09/66] Show 0% instead of N/A% when percentage is 0. --- progressbar/widgets.py | 10 ++++++---- tests/test_monitor_progress.py | 10 +++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 02ef4824..33e81272 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -634,14 +634,16 @@ def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs): WidgetBase.__init__(self, format=format, **kwargs) def __call__(self, progress, data, format=None): - return FormatWidgetMixin.__call__(self, progress, data, self.get_format(progress, data)) + return FormatWidgetMixin.__call__(self, progress, data, + self.get_format(progress, data)) - def get_format(self, progress, data): + def get_format(self, progress, data, format=None): # If percentage is not available, display N/A% - if 'percentage' in data and not data['percentage']: + if ('percentage' in data and not data['percentage'] and + data['percentage'] != 0): return self.na - return self.format + return format or self.format class SimpleProgress(FormatWidgetMixin, WidgetBase): diff --git a/tests/test_monitor_progress.py b/tests/test_monitor_progress.py index d55c86ab..097261c6 100644 --- a/tests/test_monitor_progress.py +++ b/tests/test_monitor_progress.py @@ -65,7 +65,7 @@ def test_list_example(testdir): if l.strip()] pprint.pprint(result.stderr.lines, width=70) result.stderr.fnmatch_lines([ - 'N/A% (0 of 9) | | Elapsed Time: ?:00:00 ETA: --:--:--', + ' 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', @@ -117,7 +117,7 @@ def test_rapid_updates(testdir): result.stderr.lines = [l for l in result.stderr.lines if l.strip()] pprint.pprint(result.stderr.lines, width=70) result.stderr.fnmatch_lines([ - 'N/A% (0 of 10) | | Elapsed Time: ?:00:00 ETA: --:--:--', + ' 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', @@ -139,7 +139,7 @@ def test_non_timed(testdir): result.stderr.lines = [l for l in result.stderr.lines if l.strip()] pprint.pprint(result.stderr.lines, width=70) result.stderr.fnmatch_lines([ - 'N/A%| |', + ' 0%| |', ' 20%|########## |', ' 40%|##################### |', ' 60%|################################ |', @@ -156,7 +156,7 @@ def test_line_breaks(testdir): ))) pprint.pprint(result.stderr.str(), width=70) assert result.stderr.str() == u'\n'.join(( - u'N/A%| |', + u' 0%| |', u' 20%|########## |', u' 40%|##################### |', u' 60%|################################ |', @@ -175,7 +175,7 @@ def test_no_line_breaks(testdir): pprint.pprint(result.stderr.lines, width=70) assert result.stderr.lines == [ u'', - u'N/A%| |', + u' 0%| |', u' 20%|########## |', u' 40%|##################### |', u' 60%|################################ |', From 0dca2aa5e62a1a81ad7f6141951ba97b3498d5ff Mon Sep 17 00:00:00 2001 From: Zannick Date: Mon, 4 Oct 2021 14:19:11 -0700 Subject: [PATCH 10/66] Add a bar that shows a label in the center. Include a specialization with a percentage in the center. --- progressbar/__init__.py | 4 ++++ progressbar/widgets.py | 31 +++++++++++++++++++++++++++++++ tests/test_monitor_progress.py | 20 ++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/progressbar/__init__.py b/progressbar/__init__.py index b108c419..ef504514 100644 --- a/progressbar/__init__.py +++ b/progressbar/__init__.py @@ -26,6 +26,8 @@ VariableMixin, MultiRangeBar, MultiProgressBar, + FormatLabelBar, + PercentageLabelBar, Variable, DynamicMessage, FormatCustomText, @@ -72,6 +74,8 @@ 'VariableMixin', 'MultiRangeBar', 'MultiProgressBar', + 'FormatLabelBar', + 'PercentageLabelBar', 'Variable', 'DynamicMessage', 'FormatCustomText', diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 33e81272..65d1c99b 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -909,6 +909,37 @@ def get_values(self, progress, data): return ranges +class FormatLabelBar(FormatLabel, Bar): + '''A bar which has a formatted label in the center.''' + def __init__(self, format, **kwargs): + FormatLabel.__init__(self, format, **kwargs) + Bar.__init__(self, **kwargs) + + def __call__(self, progress, data, width, format=None): + center = FormatLabel.__call__(self, progress, data, format=format) + bar = Bar.__call__(self, progress, data, width) + + # Aligns the center of the label to the center of the bar + center_len = progress.custom_len(center) + center_left = int((width - center_len) / 2) + center_right = center_left + center_len + return bar[:center_left] + center + bar[center_right:] + + +class PercentageLabelBar(Percentage, FormatLabelBar): + '''A bar which displays the current percentage in the center.''' + # %3d adds an extra space that makes it look off-center + # %2d keeps the label somewhat consistently in-place + def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs): + Percentage.__init__(self, format, na=na, **kwargs) + FormatLabelBar.__init__(self, format, **kwargs) + + def __call__(self, progress, data, width, format=None): + return FormatLabelBar.__call__( + self, progress, data, width, + format=Percentage.get_format(self, progress, data, format=None)) + + class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): '''Displays a custom variable.''' diff --git a/tests/test_monitor_progress.py b/tests/test_monitor_progress.py index 097261c6..36ce89c6 100644 --- a/tests/test_monitor_progress.py +++ b/tests/test_monitor_progress.py @@ -186,6 +186,26 @@ def test_no_line_breaks(testdir): ] +def test_percentage_label_bar(testdir): + result = testdir.runpython(testdir.makepyfile(_create_script( + widgets='[progressbar.PercentageLabelBar()]', + line_breaks=False, + items=list(range(5)), + ))) + pprint.pprint(result.stderr.lines, width=70) + assert result.stderr.lines == [ + u'', + u'| 0% |', + u'|########### 20% |', + u'|####################### 40% |', + u'|###########################60%#### |', + u'|###########################80%################ |', + u'|###########################100%###########################|', + u'', + u'|###########################100%###########################|' + ] + + def test_colors(testdir): kwargs = dict( items=range(1), From 67a8889167cbb520f70b68ba10782b3743576f0c Mon Sep 17 00:00:00 2001 From: Zannick Date: Mon, 4 Oct 2021 14:34:54 -0700 Subject: [PATCH 11/66] Add new widgets to README and examples. --- README.rst | 2 ++ examples.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/README.rst b/README.rst index eb155c54..f91d97d5 100644 --- a/README.rst +++ b/README.rst @@ -58,7 +58,9 @@ of widgets: - `FileTransferSpeed `_ - `FormatCustomText `_ - `FormatLabel `_ + - `FormatLabelBar `_ - `Percentage `_ + - `PercentageLabelBar `_ - `ReverseBar `_ - `RotatingMarker `_ - `SimpleProgress `_ diff --git a/examples.py b/examples.py index 379cb11c..a3300440 100644 --- a/examples.py +++ b/examples.py @@ -177,6 +177,17 @@ def multi_progress_bar_example(left=True): time.sleep(0.02) +@example +def percentage_label_bar_example(): + widgets = [progressbar.PercentageLabelBar()] + bar = progressbar.ProgressBar(widgets=widgets, max_value=10).start() + for i in range(10): + # do something + time.sleep(0.1) + bar.update(i + 1) + bar.finish() + + @example def file_transfer_example(): widgets = [ From 3e3f475cc4f165d0ae146e04606367c6c5b531d8 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 11 Oct 2021 00:02:34 +0200 Subject: [PATCH 12/66] added get_format method to FormatWidgetMixin --- progressbar/base.py | 4 ++++ progressbar/widgets.py | 25 ++++++++++--------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/progressbar/base.py b/progressbar/base.py index 6383cfcf..a8c8e714 100644 --- a/progressbar/base.py +++ b/progressbar/base.py @@ -15,3 +15,7 @@ def __cmp__(self, other): # pragma: no cover class UnknownLength(six.with_metaclass(FalseMeta, object)): pass + + +class Undefined(six.with_metaclass(FalseMeta, object)): + pass diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 65d1c99b..5e24c3de 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -114,15 +114,19 @@ def __init__(self, format, new_style=False, **kwargs): self.new_style = new_style self.format = format + def get_format(self, progress, data, format=None): + return format or self.format + def __call__(self, progress, data, format=None): '''Formats the widget into a string''' + format = self.get_format(progress, data, format) try: if self.new_style: - return (format or self.format).format(**data) + return format.format(**data) else: - return (format or self.format) % data + return format % data except (TypeError, KeyError): - print('Error while formatting %r' % self.format, file=sys.stderr) + print('Error while formatting %r' % format, file=sys.stderr) pprint.pprint(data, stream=sys.stderr) raise @@ -633,17 +637,13 @@ def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, format=format, **kwargs) - def __call__(self, progress, data, format=None): - return FormatWidgetMixin.__call__(self, progress, data, - self.get_format(progress, data)) - def get_format(self, progress, data, format=None): # If percentage is not available, display N/A% - if ('percentage' in data and not data['percentage'] and - data['percentage'] != 0): + percentage = data.get('percentage', base.Undefined) + if not percentage and percentage != 0: return self.na - return format or self.format + return FormatWidgetMixin.get_format(self, progress, data, format) class SimpleProgress(FormatWidgetMixin, WidgetBase): @@ -934,11 +934,6 @@ def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs): Percentage.__init__(self, format, na=na, **kwargs) FormatLabelBar.__init__(self, format, **kwargs) - def __call__(self, progress, data, width, format=None): - return FormatLabelBar.__call__( - self, progress, data, width, - format=Percentage.get_format(self, progress, data, format=None)) - class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): '''Displays a custom variable.''' From 30034c6f89eedaf029117f70ed1035dcd4661b36 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 12 Oct 2021 23:36:29 +0200 Subject: [PATCH 13/66] added github workflow --- .github/workflows/main.yml | 27 +++++++++++++++++++++++++++ README.rst | 7 ++++--- 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..02709dc7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,27 @@ +name: tox + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox diff --git a/README.rst b/README.rst index f91d97d5..18c2bec9 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,11 @@ Text progress bar library for Python. ############################################################################## -Travis status: +Build status: -.. image:: https://travis-ci.org/WoLpH/python-progressbar.svg?branch=master - :target: https://travis-ci.org/WoLpH/python-progressbar +.. image:: https://github.com/WoLpH/python-progressbar/actions/workflows/main.yml/badge.svg + :alt: python-progressbar test status + :target: https://github.com/WoLpH/python-progressbar/actions Coverage: From 36f59c1dac133365741facd4fca885382a073b9e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 12 Oct 2021 23:42:38 +0200 Subject: [PATCH 14/66] Incrementing version to v3.54.0 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index 6832fe2a..880be3eb 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '3.53.3' +__version__ = '3.54.0' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From 13a1d7a2fe56be4ad4c8a9b852caa742d05ae2fe Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Wed, 13 Oct 2021 22:51:04 -0400 Subject: [PATCH 15/66] Add GranularBar widget `GranularBar` is a widget that displays progress at a sub-character granularity by using multiple marker characters. Using the `GranularBar` in its default configuration will show a smooth progress bar using unicode block characters. More examples are provided in `examples.py` --- README.rst | 1 + examples.py | 13 ++++++++ progressbar/__init__.py | 2 ++ progressbar/widgets.py | 56 ++++++++++++++++++++++++++++++++++ tests/test_monitor_progress.py | 19 ++++++++++++ 5 files changed, 91 insertions(+) diff --git a/README.rst b/README.rst index 18c2bec9..b485fb99 100644 --- a/README.rst +++ b/README.rst @@ -60,6 +60,7 @@ of widgets: - `FormatCustomText `_ - `FormatLabel `_ - `FormatLabelBar `_ + - `GranularBar `_ - `Percentage `_ - `PercentageLabelBar `_ - `ReverseBar `_ diff --git a/examples.py b/examples.py index a3300440..605ef5d9 100644 --- a/examples.py +++ b/examples.py @@ -176,6 +176,19 @@ def multi_progress_bar_example(left=True): bar.update(progress, jobs=jobs, force=True) time.sleep(0.02) +@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=''), + ] + for i in progressbar.progressbar(range(100), widgets=widgets): + time.sleep(0.03) + @example def percentage_label_bar_example(): diff --git a/progressbar/__init__.py b/progressbar/__init__.py index ef504514..f93ab86d 100644 --- a/progressbar/__init__.py +++ b/progressbar/__init__.py @@ -26,6 +26,7 @@ VariableMixin, MultiRangeBar, MultiProgressBar, + GranularBar, FormatLabelBar, PercentageLabelBar, Variable, @@ -74,6 +75,7 @@ 'VariableMixin', 'MultiRangeBar', 'MultiProgressBar', + 'GranularBar', 'FormatLabelBar', 'PercentageLabelBar', 'Variable', diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 5e24c3de..449a09d6 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -909,6 +909,62 @@ def get_values(self, progress, data): return ranges +class GranularBar(AutoWidthWidgetBase): + '''A progressbar that can display progress at a sub-character granularity + by using multiple marker characters. + + Examples of markers: + - Smooth: ` ▏▎▍▌▋▊▉█` (default) + - Bar: ` ▁▂▃▄▅▆▇█` + - Snake: ` ▖▌▛█` + - Fade in: ` ░▒▓█` + - Dots: ` ⡀⡄⡆⡇⣇⣧⣷⣿` + - Growing circles: ` .oO` + ''' + + def __init__(self, markers=' ▏▎▍▌▋▊▉█', left='|', right='|', **kwargs): + '''Creates a customizable progress bar. + + markers - string of characters to use as granular progress markers. The + first character should represent 0% and the last 100%. + Ex: ` .oO` + left - string or callable object to use as a left border + right - string or callable object to use as a right border + ''' + self.markers = markers + self.left = string_or_lambda(left) + self.right = string_or_lambda(right) + + AutoWidthWidgetBase.__init__(self, **kwargs) + + def __call__(self, progress, data, width): + left = converters.to_unicode(self.left(progress, data, width)) + right = converters.to_unicode(self.right(progress, data, width)) + width -= progress.custom_len(left) + progress.custom_len(right) + + if progress.max_value is not base.UnknownLength \ + and progress.max_value > 0: + percent = progress.value / progress.max_value + else: + percent = 0 + + num_chars = percent * width + + marker = self.markers[-1] * int(num_chars) + + marker_idx = int((num_chars % 1) * (len(self.markers) - 1)) + if marker_idx: + marker += self.markers[marker_idx] + + marker = converters.to_unicode(marker) + + # Make sure we ignore invisible characters when filling + width += len(marker) - progress.custom_len(marker) + marker = marker.ljust(width, self.markers[0]) + + return left + marker + right + + class FormatLabelBar(FormatLabel, Bar): '''A bar which has a formatted label in the center.''' def __init__(self, format, **kwargs): diff --git a/tests/test_monitor_progress.py b/tests/test_monitor_progress.py index 36ce89c6..943af85a 100644 --- a/tests/test_monitor_progress.py +++ b/tests/test_monitor_progress.py @@ -206,6 +206,25 @@ def test_percentage_label_bar(testdir): ] +def test_granular_bar(testdir): + result = testdir.runpython(testdir.makepyfile(_create_script( + widgets='[progressbar.GranularBar(markers=" .oO")]', + line_breaks=False, + items=list(range(5)), + ))) + pprint.pprint(result.stderr.lines, width=70) + assert result.stderr.lines == [u'', + u'| |', + u'|OOOOOOOOOOO. |', + u'|OOOOOOOOOOOOOOOOOOOOOOO |', + u'|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo |', + u'|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO. |', + u'|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO|', + u'', + u'|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO|' + ] + + def test_colors(testdir): kwargs = dict( items=range(1), From 4228693e8369d4567a2f8a58cd9220ab66192ea0 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Fri, 15 Oct 2021 11:44:17 +0200 Subject: [PATCH 16/66] Added class for easy granular marker configuration --- examples.py | 3 ++- progressbar/utils.py | 2 +- progressbar/widgets.py | 17 +++++++++++++++-- tests/test_monitor_progress.py | 3 ++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/examples.py b/examples.py index 605ef5d9..0c6948da 100644 --- a/examples.py +++ b/examples.py @@ -176,9 +176,10 @@ def multi_progress_bar_example(left=True): bar.update(progress, jobs=jobs, force=True) time.sleep(0.02) + @example def granular_progress_example(): - widgets=[ + widgets = [ progressbar.GranularBar(markers=" ▏▎▍▌▋▊▉█", left='', right='|'), progressbar.GranularBar(markers=" ▁▂▃▄▅▆▇█", left='', right='|'), progressbar.GranularBar(markers=" ▖▌▛█", left='', right='|'), diff --git a/progressbar/utils.py b/progressbar/utils.py index a9dc6f54..258249ff 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -192,7 +192,7 @@ def __init__(self, target, capturing=False, listeners=set()): self.listeners = listeners self.needs_clear = False - def isatty(self): + def isatty(self): # pragma: no cover return self.target.isatty() def write(self, value): diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 449a09d6..e9c03cab 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -909,6 +909,15 @@ def get_values(self, progress, data): return ranges +class GranularMarkers: + smooth = ' ▏▎▍▌▋▊▉█' + bar = ' ▁▂▃▄▅▆▇█' + snake = ' ▖▌▛█' + fade_in = ' ░▒▓█' + dots = ' ⡀⡄⡆⡇⣇⣧⣷⣿' + growing_circles = ' .oO' + + class GranularBar(AutoWidthWidgetBase): '''A progressbar that can display progress at a sub-character granularity by using multiple marker characters. @@ -920,14 +929,18 @@ class GranularBar(AutoWidthWidgetBase): - Fade in: ` ░▒▓█` - Dots: ` ⡀⡄⡆⡇⣇⣧⣷⣿` - Growing circles: ` .oO` + + The markers can be accessed through GranularMarkers. GranularMarkers.dots + for example ''' - def __init__(self, markers=' ▏▎▍▌▋▊▉█', left='|', right='|', **kwargs): + def __init__(self, markers=GranularMarkers.smooth, left='|', right='|', + **kwargs): '''Creates a customizable progress bar. markers - string of characters to use as granular progress markers. The first character should represent 0% and the last 100%. - Ex: ` .oO` + Ex: ` .oO`. left - string or callable object to use as a left border right - string or callable object to use as a right border ''' diff --git a/tests/test_monitor_progress.py b/tests/test_monitor_progress.py index 943af85a..5dd6f5ee 100644 --- a/tests/test_monitor_progress.py +++ b/tests/test_monitor_progress.py @@ -213,7 +213,8 @@ def test_granular_bar(testdir): items=list(range(5)), ))) pprint.pprint(result.stderr.lines, width=70) - assert result.stderr.lines == [u'', + assert result.stderr.lines == [ + u'', u'| |', u'|OOOOOOOOOOO. |', u'|OOOOOOOOOOOOOOOOOOOOOOO |', From e5d37e3ded1b39a6b08421d471f987422b58a020 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Fri, 15 Oct 2021 14:20:11 +0200 Subject: [PATCH 17/66] Incrementing version to v3.55.0 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index 880be3eb..35b1c9de 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '3.54.0' +__version__ = '3.55.0' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From db8c944577189945ac0f08b8693b4a78b8b17e5e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 03:41:31 +0100 Subject: [PATCH 18/66] updated to python 3 only support --- .github/workflows/main.yml | 2 +- examples.py | 11 ++---- progressbar/__init__.py | 80 ++++++++++++++++---------------------- progressbar/bar.py | 24 ++++-------- progressbar/base.py | 7 +--- progressbar/utils.py | 17 ++++---- progressbar/widgets.py | 23 ++++------- setup.py | 68 +++++++------------------------- tests/test_flush.py | 2 - tests/test_stream.py | 2 - tests/test_terminal.py | 2 - tox.ini | 11 +++--- 12 files changed, 84 insertions(+), 165 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 02709dc7..7b19e5d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff --git a/examples.py b/examples.py index 0c6948da..1c033839 100644 --- a/examples.py +++ b/examples.py @@ -1,12 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import with_statement - import functools import random import sys @@ -187,7 +181,10 @@ def granular_progress_example(): progressbar.GranularBar(markers=" ⡀⡄⡆⡇⣇⣧⣷⣿", left='', right='|'), progressbar.GranularBar(markers=" .oO", left='', right=''), ] - for i in progressbar.progressbar(range(100), widgets=widgets): + for i in progressbar.progressbar(list(range(100)), widgets=widgets): + time.sleep(0.03) + + for i in progressbar.progressbar(iter(range(100)), widgets=widgets): time.sleep(0.03) diff --git a/progressbar/__init__.py b/progressbar/__init__.py index f93ab86d..33d7c719 100644 --- a/progressbar/__init__.py +++ b/progressbar/__init__.py @@ -1,52 +1,40 @@ from datetime import date -from .utils import ( - len_color, - streams -) -from .shortcuts import progressbar - -from .widgets import ( - Timer, - ETA, - AdaptiveETA, - AbsoluteETA, - DataSize, - FileTransferSpeed, - AdaptiveTransferSpeed, - AnimatedMarker, - Counter, - Percentage, - FormatLabel, - SimpleProgress, - Bar, - ReverseBar, - BouncingBar, - RotatingMarker, - VariableMixin, - MultiRangeBar, - MultiProgressBar, - GranularBar, - FormatLabelBar, - PercentageLabelBar, - Variable, - DynamicMessage, - FormatCustomText, - CurrentTime -) - -from .bar import ( - ProgressBar, - DataTransferBar, - NullBar, -) +from .__about__ import __author__ +from .__about__ import __version__ +from .bar import DataTransferBar +from .bar import NullBar +from .bar import ProgressBar from .base import UnknownLength - - -from .__about__ import ( - __author__, - __version__, -) +from .shortcuts import progressbar +from .utils import len_color +from .utils import streams +from .widgets import AbsoluteETA +from .widgets import AdaptiveETA +from .widgets import AdaptiveTransferSpeed +from .widgets import AnimatedMarker +from .widgets import Bar +from .widgets import BouncingBar +from .widgets import Counter +from .widgets import CurrentTime +from .widgets import DataSize +from .widgets import DynamicMessage +from .widgets import ETA +from .widgets import FileTransferSpeed +from .widgets import FormatCustomText +from .widgets import FormatLabel +from .widgets import FormatLabelBar +from .widgets import GranularBar +from .widgets import MultiProgressBar +from .widgets import MultiRangeBar +from .widgets import Percentage +from .widgets import PercentageLabelBar +from .widgets import ReverseBar +from .widgets import RotatingMarker +from .widgets import SimpleProgress +from .widgets import Timer +from .widgets import Variable +from .widgets import VariableMixin __date__ = str(date.today()) __all__ = [ diff --git a/progressbar/bar.py b/progressbar/bar.py index b5980e23..7848b776 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -1,17 +1,13 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals -from __future__ import with_statement - -import sys +import logging import math import os +import sys import time import timeit -import logging import warnings -from datetime import datetime from copy import deepcopy +from datetime import datetime + try: # pragma: no cover from collections import abc except ImportError: # pragma: no cover @@ -19,14 +15,11 @@ from python_utils import converters -import six - from . import widgets from . import widgets as widgets_module # Avoid name collision from . import base from . import utils - logger = logging.getLogger(__name__) @@ -77,7 +70,7 @@ def __init__(self, fd=sys.stderr, is_terminal=None, line_breaks=None, # iteractive terminals) or write line breaks (suitable for log files) if line_breaks is None: line_breaks = utils.env_flag('PROGRESSBAR_LINE_BREAKS', not - self.is_terminal) + self.is_terminal) self.line_breaks = line_breaks # Check if ANSI escape characters are enabled (suitable for iteractive @@ -197,7 +190,6 @@ def finish(self, end='\n'): class ProgressBar(StdRedirectMixin, ResizableMixin, ProgressBarBase): - '''The ProgressBar class which updates and prints the bar. Args: @@ -489,7 +481,7 @@ def data(self): total_seconds_elapsed=total_seconds_elapsed, # The seconds since the bar started modulo 60 seconds_elapsed=(elapsed.seconds % 60) + - (elapsed.microseconds / 1000000.), + (elapsed.microseconds / 1000000.), # The minutes since the bar started modulo 60 minutes_elapsed=(elapsed.seconds / 60) % 60, # The hours since the bar started modulo 24 @@ -585,7 +577,7 @@ def _format_widgets(self): elif isinstance(widget, widgets.AutoWidthWidgetBase): result.append(widget) expanding.insert(0, index) - elif isinstance(widget, six.string_types): + elif isinstance(widget, str): result.append(widget) width -= self.custom_len(widget) else: @@ -795,6 +787,7 @@ class DataTransferBar(ProgressBar): This assumes that the values its given are numbers of bytes. ''' + def default_widgets(self): if self.max_value: return [ @@ -813,7 +806,6 @@ def default_widgets(self): class NullBar(ProgressBar): - ''' Progress bar that does absolutely nothing. Useful for single verbosity flags diff --git a/progressbar/base.py b/progressbar/base.py index a8c8e714..df278e59 100644 --- a/progressbar/base.py +++ b/progressbar/base.py @@ -1,7 +1,4 @@ # -*- mode: python; coding: utf-8 -*- -from __future__ import absolute_import -import six - class FalseMeta(type): def __bool__(self): # pragma: no cover @@ -13,9 +10,9 @@ def __cmp__(self, other): # pragma: no cover __nonzero__ = __bool__ -class UnknownLength(six.with_metaclass(FalseMeta, object)): +class UnknownLength(metaclass=FalseMeta): pass -class Undefined(six.with_metaclass(FalseMeta, object)): +class Undefined(metaclass=FalseMeta): pass diff --git a/progressbar/utils.py b/progressbar/utils.py index 258249ff..e589105f 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -1,17 +1,16 @@ -from __future__ import absolute_import import atexit +import datetime import io +import logging import os import re import sys -import logging -import datetime -from python_utils.time import timedelta_to_seconds, epoch, format_time + from python_utils.converters import scale_1024 from python_utils.terminal import get_terminal_size - -import six - +from python_utils.time import epoch +from python_utils.time import format_time +from python_utils.time import timedelta_to_seconds assert timedelta_to_seconds assert get_terminal_size @@ -19,7 +18,6 @@ assert scale_1024 assert epoch - ANSI_TERMS = ( '([xe]|bv)term', '(sco)?ansi', @@ -186,7 +184,7 @@ def env_flag(name, default=None): class WrappingIO: def __init__(self, target, capturing=False, listeners=set()): - self.buffer = six.StringIO() + self.buffer = io.StringIO() self.target = target self.capturing = capturing self.listeners = listeners @@ -402,6 +400,7 @@ class AttributeDict(dict): ... AttributeError: No such attribute: spam ''' + def __getattr__(self, name): if name in self: return self[name] diff --git a/progressbar/widgets.py b/progressbar/widgets.py index e9c03cab..1eaa0c09 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -1,20 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import with_statement - import abc -import sys -import pprint import datetime import functools +import pprint +import sys from python_utils import converters -import six - from . import base from . import utils @@ -24,7 +16,7 @@ def string_or_lambda(input_): - if isinstance(input_, six.string_types): + if isinstance(input_, str): def render_input(progress, data, width): return input_ % data @@ -50,7 +42,7 @@ def create_wrapper(wrapper): elif not wrapper: return - if isinstance(wrapper, six.string_types): + if isinstance(wrapper, str): assert '{}' in wrapper, 'Expected string with {} for formatting' else: raise RuntimeError('Pass either a begin/end string as a tuple or a' @@ -84,7 +76,7 @@ def _marker(progress, data, width): else: return marker - if isinstance(marker, six.string_types): + if isinstance(marker, str): marker = converters.to_unicode(marker) assert utils.len_color(marker) == 1, \ 'Markers are required to be 1 char' @@ -811,7 +803,7 @@ class VariableMixin(object): '''Mixin to display a custom user variable ''' def __init__(self, name, **kwargs): - if not isinstance(name, six.string_types): + if not isinstance(name, str): raise TypeError('Variable(): argument must be a string') if len(name.split()) > 1: raise ValueError('Variable(): argument must be single word') @@ -980,6 +972,7 @@ def __call__(self, progress, data, width): class FormatLabelBar(FormatLabel, Bar): '''A bar which has a formatted label in the center.''' + def __init__(self, format, **kwargs): FormatLabel.__init__(self, format, **kwargs) Bar.__init__(self, **kwargs) @@ -997,6 +990,7 @@ def __call__(self, progress, data, width, format=None): class PercentageLabelBar(Percentage, FormatLabelBar): '''A bar which displays the current percentage in the center.''' + # %3d adds an extra space that makes it look off-center # %2d keeps the label somewhat consistently in-place def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs): @@ -1070,4 +1064,3 @@ def current_datetime(self): def current_time(self): return self.current_datetime().time() - diff --git a/setup.py b/setup.py index d5462630..3f4f7bdf 100644 --- a/setup.py +++ b/setup.py @@ -4,55 +4,16 @@ import os import sys -from setuptools.command.test import test as TestCommand - -try: - from setuptools import setup, find_packages -except ImportError: - from distutils.core import setup, find_packages - - -# Not all systems use utf8 encoding by default, this works around that issue -if sys.version_info > (3,): - from functools import partial - open = partial(open, encoding='utf8') - +from setuptools import setup, find_packages # To prevent importing about and thereby breaking the coverage info we use this # exec hack about = {} -with open("progressbar/__about__.py") as fp: +with open('progressbar/__about__.py', encoding='utf8') as fp: exec(fp.read(), about) -class PyTest(TestCommand): - user_options = [('pytest-args=', 'a', 'Arguments to pass to pytest')] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.pytest_args = '' - - def run_tests(self): - import shlex - # import here, cause outside the eggs aren't loaded - import pytest - errno = pytest.main(shlex.split(self.pytest_args)) - sys.exit(errno) - - install_reqs = [] -tests_require = [ - 'flake8>=3.7.7', - 'pytest>=4.6.9', - 'pytest-cov>=2.6.1', - 'freezegun>=0.3.11', - 'sphinx>=1.8.5', -] - -if sys.version_info < (2, 7): - tests_require += ['unittest2'] - - if sys.argv[-1] == 'info': for k, v in about.items(): print('%s: %s' % (k, v)) @@ -65,7 +26,6 @@ def run_tests(self): readme = \ 'See http://pypi.python.org/pypi/%(__package_name__)s/' % about - if __name__ == '__main__': setup( name='progressbar2', @@ -80,32 +40,32 @@ def run_tests(self): long_description=readme, include_package_data=True, install_requires=[ - 'python-utils>=2.3.0', - 'six', + 'python-utils>=3.0.0', ], - tests_require=tests_require, setup_requires=['setuptools'], zip_safe=False, - cmdclass={'test': PyTest}, extras_require={ 'docs': [ - 'sphinx>=1.7.4', + 'sphinx>=1.8.5', + ], + 'tests': [ + 'flake8>=3.7.7', + 'pytest>=4.6.9', + 'pytest-cov>=2.6.1', + 'freezegun>=0.3.11', + 'sphinx>=1.8.5', ], - 'tests': tests_require, }, + python_requires='>=3.7.0', classifiers=[ 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', - "Programming Language :: Python :: 2", - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: PyPy', ], ) diff --git a/tests/test_flush.py b/tests/test_flush.py index 7f4317eb..69dc4e30 100644 --- a/tests/test_flush.py +++ b/tests/test_flush.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import time import progressbar diff --git a/tests/test_stream.py b/tests/test_stream.py index 235f174a..6dcfcf7c 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import io import sys import pytest diff --git a/tests/test_terminal.py b/tests/test_terminal.py index f55f3df9..997bb0d6 100644 --- a/tests/test_terminal.py +++ b/tests/test_terminal.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys import time import signal diff --git a/tox.ini b/tox.ini index 9af9ca8b..a36a8ddd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,15 @@ [tox] -envlist = py27, py33, py34, py35, py36, py37, py38, pypy, flake8, docs +envlist = py37, py38, py39, py310, pypy3, flake8, docs skip_missing_interpreters = True [testenv] basepython = - py27: python2.7 - py34: python3.4 - py35: python3.5 py36: python3.6 py37: python3.7 py38: python3.8 - pypy: pypy + py39: python3.9 + py310: python3.10 + pypy3: pypy3 deps = -r{toxinidir}/tests/requirements.txt commands = py.test --basetemp="{envtmpdir}" --confcutdir=.. {posargs} @@ -18,7 +17,7 @@ changedir = tests [testenv:flake8] changedir = -basepython = python2.7 +basepython = python3 deps = flake8 commands = flake8 {toxinidir}/progressbar {toxinidir}/tests {toxinidir}/examples.py From 74910cae5f5f2f62afeaab82ea0b8a1f7d211dda Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 04:26:45 +0100 Subject: [PATCH 19/66] added some type hinting --- progressbar/bar.py | 15 ++++++--- progressbar/utils.py | 71 +++++++++++++++++++++++++----------------- progressbar/widgets.py | 4 +-- setup.py | 1 + 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index 7848b776..a884dcab 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -8,6 +8,8 @@ from copy import deepcopy from datetime import datetime +from python_utils import types + try: # pragma: no cover from collections import abc except ImportError: # pragma: no cover @@ -51,8 +53,10 @@ class ProgressBarBase(abc.Iterable, ProgressBarMixinBase): class DefaultFdMixin(ProgressBarMixinBase): - def __init__(self, fd=sys.stderr, is_terminal=None, line_breaks=None, - enable_colors=None, **kwargs): + def __init__(self, fd: types.IO = sys.stderr, + is_terminal: bool | None = None, + line_breaks: bool | None = None, + enable_colors: bool | None = None, **kwargs): if fd is sys.stdout: fd = utils.streams.original_stdout @@ -115,7 +119,7 @@ def finish(self, *args, **kwargs): # pragma: no cover class ResizableMixin(ProgressBarMixinBase): - def __init__(self, term_width=None, **kwargs): + def __init__(self, term_width: int | None = None, **kwargs): ProgressBarMixinBase.__init__(self, **kwargs) self.signal_set = False @@ -149,7 +153,8 @@ def finish(self): # pragma: no cover class StdRedirectMixin(DefaultFdMixin): - def __init__(self, redirect_stderr=False, redirect_stdout=False, **kwargs): + def __init__(self, redirect_stderr: bool=False, redirect_stdout: + bool=False, **kwargs): DefaultFdMixin.__init__(self, **kwargs) self.redirect_stderr = redirect_stderr self.redirect_stdout = redirect_stdout @@ -172,7 +177,7 @@ def start(self, *args, **kwargs): utils.streams.start_capturing(self) DefaultFdMixin.start(self, *args, **kwargs) - def update(self, value=None): + def update(self, value: float=None): if not self.line_breaks and utils.streams.needs_clear(): self.fd.write('\r' + ' ' * self.term_width + '\r') diff --git a/progressbar/utils.py b/progressbar/utils.py index e589105f..101fac7e 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import atexit import datetime import io @@ -5,7 +7,11 @@ import os import re import sys +from typing import Optional +from typing import Set +from typing import Union +from python_utils import types from python_utils.converters import scale_1024 from python_utils.terminal import get_terminal_size from python_utils.time import epoch @@ -32,7 +38,8 @@ ANSI_TERM_RE = re.compile('^({})'.format('|'.join(ANSI_TERMS)), re.IGNORECASE) -def is_ansi_terminal(fd, is_terminal=None): # pragma: no cover +def is_ansi_terminal(fd: types.IO, is_terminal: bool | None = None) \ + -> bool: # pragma: no cover if is_terminal is None: # Jupyter Notebooks define this variable and support progress bars if 'JPY_PARENT_PID' in os.environ: @@ -64,7 +71,7 @@ def is_ansi_terminal(fd, is_terminal=None): # pragma: no cover return is_terminal -def is_terminal(fd, is_terminal=None): +def is_terminal(fd: types.IO, is_terminal: bool | None = None) -> bool: if is_terminal is None: # Full ansi support encompasses what we expect from a terminal is_terminal = is_ansi_terminal(True) or None @@ -84,7 +91,8 @@ def is_terminal(fd, is_terminal=None): return is_terminal -def deltas_to_seconds(*deltas, **kwargs): # default=ValueError): +def deltas_to_seconds(*deltas, **kwargs) -> Optional[ + Union[float, int]]: # default=ValueError): ''' Convert timedeltas and seconds as int to seconds as float while coalescing @@ -128,7 +136,7 @@ def deltas_to_seconds(*deltas, **kwargs): # default=ValueError): return default -def no_color(value): +def no_color(value: Union[str, bytes]) -> Union[str, bytes]: ''' Return the `value` without ANSI escape codes @@ -151,7 +159,7 @@ def no_color(value): return re.sub(pattern, replace, value) -def len_color(value): +def len_color(value: Union[str, bytes]) -> int: ''' Return the length of `value` without ANSI escape codes @@ -165,7 +173,7 @@ def len_color(value): return len(no_color(value)) -def env_flag(name, default=None): +def env_flag(name: str, default: Optional[bool] = None) -> Optional[bool]: ''' Accepts environt variables formatted as y/n, yes/no, 1/0, true/false, on/off, and returns it as a boolean @@ -183,7 +191,8 @@ def env_flag(name, default=None): class WrappingIO: - def __init__(self, target, capturing=False, listeners=set()): + def __init__(self, target: types.IO, capturing: bool = False, listeners: + Set['progressbar.ProgressBar'] = set()) -> None: self.buffer = io.StringIO() self.target = target self.capturing = capturing @@ -193,7 +202,7 @@ def __init__(self, target, capturing=False, listeners=set()): def isatty(self): # pragma: no cover return self.target.isatty() - def write(self, value): + def write(self, value: str) -> None: if self.capturing: self.buffer.write(value) if '\n' in value: # pragma: no branch @@ -205,10 +214,10 @@ def write(self, value): if '\n' in value: # pragma: no branch self.flush_target() - def flush(self): + def flush(self) -> None: self.buffer.flush() - def _flush(self): + def _flush(self) -> None: value = self.buffer.getvalue() if value: self.flush() @@ -220,12 +229,12 @@ def _flush(self): # when explicitly flushing, always flush the target as well self.flush_target() - def flush_target(self): # pragma: no cover + def flush_target(self) -> None: # pragma: no cover if not self.target.closed and getattr(self.target, 'flush'): self.target.flush() -class StreamWrapper(object): +class StreamWrapper: '''Wrap stdout and stderr globally''' def __init__(self): @@ -244,14 +253,20 @@ def __init__(self): if env_flag('WRAP_STDERR', default=False): # pragma: no cover self.wrap_stderr() - def start_capturing(self, bar=None): + def start_capturing( + self, + bar: types.U['progressbar.ProgressBar', + 'progressbar.DataTransferBar', None] = None, + ) -> None: if bar: # pragma: no branch self.listeners.add(bar) self.capturing += 1 self.update_capturing() - def stop_capturing(self, bar=None): + def stop_capturing(self, bar: Optional[ + Union[ + 'progressbar.ProgressBar', 'progressbar.DataTransferBar']] = None) -> None: if bar: # pragma: no branch try: self.listeners.remove(bar) @@ -261,7 +276,7 @@ def stop_capturing(self, bar=None): self.capturing -= 1 self.update_capturing() - def update_capturing(self): # pragma: no cover + def update_capturing(self) -> None: # pragma: no cover if isinstance(self.stdout, WrappingIO): self.stdout.capturing = self.capturing > 0 @@ -271,14 +286,14 @@ def update_capturing(self): # pragma: no cover if self.capturing <= 0: self.flush() - def wrap(self, stdout=False, stderr=False): + def wrap(self, stdout: bool = False, stderr: bool = False) -> None: if stdout: self.wrap_stdout() if stderr: self.wrap_stderr() - def wrap_stdout(self): + def wrap_stdout(self) -> types.IO: self.wrap_excepthook() if not self.wrapped_stdout: @@ -288,7 +303,7 @@ def wrap_stdout(self): return sys.stdout - def wrap_stderr(self): + def wrap_stderr(self) -> types.IO: self.wrap_excepthook() if not self.wrapped_stderr: @@ -298,44 +313,44 @@ def wrap_stderr(self): return sys.stderr - def unwrap_excepthook(self): + def unwrap_excepthook(self) -> None: if self.wrapped_excepthook: self.wrapped_excepthook -= 1 sys.excepthook = self.original_excepthook - def wrap_excepthook(self): + def wrap_excepthook(self) -> None: if not self.wrapped_excepthook: logger.debug('wrapping excepthook') self.wrapped_excepthook += 1 sys.excepthook = self.excepthook - def unwrap(self, stdout=False, stderr=False): + def unwrap(self, stdout: bool = False, stderr: bool = False) -> None: if stdout: self.unwrap_stdout() if stderr: self.unwrap_stderr() - def unwrap_stdout(self): + def unwrap_stdout(self) -> None: if self.wrapped_stdout > 1: self.wrapped_stdout -= 1 else: sys.stdout = self.original_stdout self.wrapped_stdout = 0 - def unwrap_stderr(self): + def unwrap_stderr(self) -> None: if self.wrapped_stderr > 1: self.wrapped_stderr -= 1 else: sys.stderr = self.original_stderr self.wrapped_stderr = 0 - def needs_clear(self): # pragma: no cover + def needs_clear(self) -> bool: # pragma: no cover stdout_needs_clear = getattr(self.stdout, 'needs_clear', False) stderr_needs_clear = getattr(self.stderr, 'needs_clear', False) return stderr_needs_clear or stdout_needs_clear - def flush(self): + def flush(self) -> None: if self.wrapped_stdout: # pragma: no branch try: self.stdout._flush() @@ -401,16 +416,16 @@ class AttributeDict(dict): AttributeError: No such attribute: spam ''' - def __getattr__(self, name): + def __getattr__(self, name: str) -> int: if name in self: return self[name] else: raise AttributeError("No such attribute: " + name) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: int) -> None: self[name] = value - def __delattr__(self, name): + def __delattr__(self, name: str) -> None: if name in self: del self[name] else: diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 1eaa0c09..285c51e9 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -152,7 +152,7 @@ def __init__(self, min_width=None, max_width=None, **kwargs): self.min_width = min_width self.max_width = max_width - def check_size(self, progress): + def check_size(self, progress: 'progressbar.ProgressBar'): if self.min_width and self.min_width > progress.term_width: return False elif self.max_width and self.max_width < progress.term_width: @@ -247,7 +247,7 @@ class FormatLabel(FormatWidgetMixin, WidgetBase): 'value': ('value', None), } - def __init__(self, format, **kwargs): + def __init__(self, format: str, **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, **kwargs) diff --git a/setup.py b/setup.py index 3f4f7bdf..f72abd08 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ 'flake8>=3.7.7', 'pytest>=4.6.9', 'pytest-cov>=2.6.1', + 'pytest-mypy', 'freezegun>=0.3.11', 'sphinx>=1.8.5', ], From fbde8e200c54c510d8a1ddc8b3bb113596af2670 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 12:13:41 +0100 Subject: [PATCH 20/66] simplified types --- progressbar/utils.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/progressbar/utils.py b/progressbar/utils.py index 101fac7e..d8e9f2a3 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -7,9 +7,7 @@ import os import re import sys -from typing import Optional from typing import Set -from typing import Union from python_utils import types from python_utils.converters import scale_1024 @@ -91,8 +89,8 @@ def is_terminal(fd: types.IO, is_terminal: bool | None = None) -> bool: return is_terminal -def deltas_to_seconds(*deltas, **kwargs) -> Optional[ - Union[float, int]]: # default=ValueError): +def deltas_to_seconds(*deltas, + **kwargs) -> int | float | None: # default=ValueError): ''' Convert timedeltas and seconds as int to seconds as float while coalescing @@ -136,7 +134,7 @@ def deltas_to_seconds(*deltas, **kwargs) -> Optional[ return default -def no_color(value: Union[str, bytes]) -> Union[str, bytes]: +def no_color(value: types.StringTypes) -> types.StringTypes: ''' Return the `value` without ANSI escape codes @@ -159,7 +157,7 @@ def no_color(value: Union[str, bytes]) -> Union[str, bytes]: return re.sub(pattern, replace, value) -def len_color(value: Union[str, bytes]) -> int: +def len_color(value: types.StringTypes) -> int: ''' Return the length of `value` without ANSI escape codes @@ -173,7 +171,7 @@ def len_color(value: Union[str, bytes]) -> int: return len(no_color(value)) -def env_flag(name: str, default: Optional[bool] = None) -> Optional[bool]: +def env_flag(name: str, default: bool | None = None) -> bool | None: ''' Accepts environt variables formatted as y/n, yes/no, 1/0, true/false, on/off, and returns it as a boolean @@ -264,9 +262,9 @@ def start_capturing( self.capturing += 1 self.update_capturing() - def stop_capturing(self, bar: Optional[ - Union[ - 'progressbar.ProgressBar', 'progressbar.DataTransferBar']] = None) -> None: + def stop_capturing( + self, bar: 'progressbar.ProgressBar' + | 'progressbar.DataTransferBar' | None = None) -> None: if bar: # pragma: no branch try: self.listeners.remove(bar) From f1dc90ba65dad07284fa300cb289825da8142966 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 12:16:54 +0100 Subject: [PATCH 21/66] fixed python 3.7 support --- progressbar/bar.py | 8 +++++--- tox.ini | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index a884dcab..fd538dcd 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import math import os @@ -153,8 +155,8 @@ def finish(self): # pragma: no cover class StdRedirectMixin(DefaultFdMixin): - def __init__(self, redirect_stderr: bool=False, redirect_stdout: - bool=False, **kwargs): + def __init__(self, redirect_stderr: bool = False, redirect_stdout: + bool = False, **kwargs): DefaultFdMixin.__init__(self, **kwargs) self.redirect_stderr = redirect_stderr self.redirect_stdout = redirect_stdout @@ -177,7 +179,7 @@ def start(self, *args, **kwargs): utils.streams.start_capturing(self) DefaultFdMixin.start(self, *args, **kwargs) - def update(self, value: float=None): + def update(self, value: float = None): if not self.line_breaks and utils.streams.needs_clear(): self.fd.write('\r' + ' ' * self.term_width + '\r') diff --git a/tox.ini b/tox.ini index a36a8ddd..1ed7e3c6 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ commands = flake8 {toxinidir}/progressbar {toxinidir}/tests {toxinidir}/examples [testenv:docs] changedir = -basepython = python3 +basepython = python3.7 deps = -r{toxinidir}/docs/requirements.txt whitelist_externals = rm From 3759b2633185c83239d95dec677bbfae2ec8f2a1 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 12:17:32 +0100 Subject: [PATCH 22/66] any modern python installation should be able to build the docs --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1ed7e3c6..a36a8ddd 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ commands = flake8 {toxinidir}/progressbar {toxinidir}/tests {toxinidir}/examples [testenv:docs] changedir = -basepython = python3.7 +basepython = python3 deps = -r{toxinidir}/docs/requirements.txt whitelist_externals = rm From fcc0d794d1c44e7513cdcf2d46073269c80f2349 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 12:27:29 +0100 Subject: [PATCH 23/66] simplified types --- progressbar/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/progressbar/utils.py b/progressbar/utils.py index d8e9f2a3..da7d3955 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -7,7 +7,6 @@ import os import re import sys -from typing import Set from python_utils import types from python_utils.converters import scale_1024 @@ -190,7 +189,7 @@ def env_flag(name: str, default: bool | None = None) -> bool | None: class WrappingIO: def __init__(self, target: types.IO, capturing: bool = False, listeners: - Set['progressbar.ProgressBar'] = set()) -> None: + types.Set['progressbar.ProgressBar'] = set()) -> None: self.buffer = io.StringIO() self.target = target self.capturing = capturing @@ -253,8 +252,8 @@ def __init__(self): def start_capturing( self, - bar: types.U['progressbar.ProgressBar', - 'progressbar.DataTransferBar', None] = None, + bar: 'progressbar.ProgressBar' | 'progressbar.DataTransferBar' + | None = None, ) -> None: if bar: # pragma: no branch self.listeners.add(bar) From a952bec0ab8d995372142f43e971ae7abab97ba7 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 12:33:02 +0100 Subject: [PATCH 24/66] fixed documentation styling issues --- progressbar/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/progressbar/utils.py b/progressbar/utils.py index da7d3955..7fed5f50 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -378,12 +378,14 @@ class AttributeDict(dict): >>> attrs = AttributeDict(spam=123) # Reading + >>> attrs['spam'] 123 >>> attrs.spam 123 # Read after update using attribute + >>> attrs.spam = 456 >>> attrs['spam'] 456 @@ -391,6 +393,7 @@ class AttributeDict(dict): 456 # Read after update using dict access + >>> attrs['spam'] = 123 >>> attrs['spam'] 123 @@ -398,6 +401,7 @@ class AttributeDict(dict): 123 # Read after update using dict access + >>> del attrs.spam >>> attrs['spam'] Traceback (most recent call last): From 7f83c59d132705798f67d401a18320155cd8fc37 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 12:41:05 +0100 Subject: [PATCH 25/66] small type hinting improvements --- progressbar/utils.py | 19 ++++++++----------- progressbar/widgets.py | 6 +++++- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/progressbar/utils.py b/progressbar/utils.py index 7fed5f50..b1be944f 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -15,6 +15,9 @@ from python_utils.time import format_time from python_utils.time import timedelta_to_seconds +if types.TYPE_CHECKING: + from .bar import ProgressBar + assert timedelta_to_seconds assert get_terminal_size assert format_time @@ -188,12 +191,12 @@ def env_flag(name: str, default: bool | None = None) -> bool | None: class WrappingIO: - def __init__(self, target: types.IO, capturing: bool = False, listeners: - types.Set['progressbar.ProgressBar'] = set()) -> None: + def __init__(self, target: types.IO, capturing: bool = False, + listeners: types.Set[ProgressBar] = None) -> None: self.buffer = io.StringIO() self.target = target self.capturing = capturing - self.listeners = listeners + self.listeners = listeners or set() self.needs_clear = False def isatty(self): # pragma: no cover @@ -250,20 +253,14 @@ def __init__(self): if env_flag('WRAP_STDERR', default=False): # pragma: no cover self.wrap_stderr() - def start_capturing( - self, - bar: 'progressbar.ProgressBar' | 'progressbar.DataTransferBar' - | None = None, - ) -> None: + def start_capturing(self, bar: ProgressBar | None = None) -> None: if bar: # pragma: no branch self.listeners.add(bar) self.capturing += 1 self.update_capturing() - def stop_capturing( - self, bar: 'progressbar.ProgressBar' - | 'progressbar.DataTransferBar' | None = None) -> None: + def stop_capturing(self, bar: ProgressBar | None = None) -> None: if bar: # pragma: no branch try: self.listeners.remove(bar) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 285c51e9..9ffb68af 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -6,10 +6,14 @@ import sys from python_utils import converters +from python_utils import types from . import base from . import utils +if types.TYPE_CHECKING: + from .bar import ProgressBar + MAX_DATE = datetime.date.max MAX_TIME = datetime.time.max MAX_DATETIME = datetime.datetime.max @@ -152,7 +156,7 @@ def __init__(self, min_width=None, max_width=None, **kwargs): self.min_width = min_width self.max_width = max_width - def check_size(self, progress: 'progressbar.ProgressBar'): + def check_size(self, progress: 'ProgressBar'): if self.min_width and self.min_width > progress.term_width: return False elif self.max_width and self.max_width < progress.term_width: From 475d690588e871b6f1e2d96bc4481dc57b5b19e7 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 12:45:03 +0100 Subject: [PATCH 26/66] small type hinting improvements --- progressbar/bar.py | 12 ++++++------ tox.ini | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index fd538dcd..025471e9 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -75,8 +75,8 @@ def __init__(self, fd: types.IO = sys.stderr, # Check if it should overwrite the current line (suitable for # iteractive terminals) or write line breaks (suitable for log files) if line_breaks is None: - line_breaks = utils.env_flag('PROGRESSBAR_LINE_BREAKS', not - self.is_terminal) + line_breaks = utils.env_flag('PROGRESSBAR_LINE_BREAKS', + not self.is_terminal) self.line_breaks = line_breaks # Check if ANSI escape characters are enabled (suitable for iteractive @@ -155,8 +155,8 @@ def finish(self): # pragma: no cover class StdRedirectMixin(DefaultFdMixin): - def __init__(self, redirect_stderr: bool = False, redirect_stdout: - bool = False, **kwargs): + def __init__(self, redirect_stderr: bool = False, + redirect_stdout: bool = False, **kwargs): DefaultFdMixin.__init__(self, **kwargs) self.redirect_stderr = redirect_stderr self.redirect_stdout = redirect_stdout @@ -487,8 +487,8 @@ def data(self): # The seconds since the bar started total_seconds_elapsed=total_seconds_elapsed, # The seconds since the bar started modulo 60 - seconds_elapsed=(elapsed.seconds % 60) + - (elapsed.microseconds / 1000000.), + seconds_elapsed=(elapsed.seconds % 60) + + (elapsed.microseconds / 1000000.), # The minutes since the bar started modulo 60 minutes_elapsed=(elapsed.seconds / 60) % 60, # The hours since the bar started modulo 24 diff --git a/tox.ini b/tox.ini index a36a8ddd..99d982dd 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ commands = sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html {posargs} [flake8] -ignore = W391, W504, E741 +ignore = W391, W504, E741, W503, E131 exclude = docs, progressbar/six.py From e84e2678c247b34e3e03a9a29ce93e28f1a0cfcf Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 5 Jan 2022 14:41:27 +0100 Subject: [PATCH 27/66] Incrementing version to v4.0.0 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index 35b1c9de..98474085 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '3.55.0' +__version__ = '4.0.0' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From 3a70922ccba34bb3394b6cd63bf35e290648dc43 Mon Sep 17 00:00:00 2001 From: William Andre Date: Wed, 8 Jun 2022 17:04:32 +0200 Subject: [PATCH 28/66] Delegate unknown attrs to target in WrappingIO Same idea as fbb2f4d18703f387349e77c126214d8eb9bd89c1 but more generic. Only the flushing should be wrapped, everything else should behave like it would on the target. The issue arises while trying to access attributes like `encoding` or `fileno`. --- progressbar/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/progressbar/utils.py b/progressbar/utils.py index b1be944f..6a322db4 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -199,8 +199,8 @@ def __init__(self, target: types.IO, capturing: bool = False, self.listeners = listeners or set() self.needs_clear = False - def isatty(self): # pragma: no cover - return self.target.isatty() + def __getattr__(self, name): # pragma: no cover + return getattr(self.target, name) def write(self, value: str) -> None: if self.capturing: From c0c2f2cd10c79d0dd5864451d4c9126c37726ca3 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 18 Oct 2022 12:47:23 +0200 Subject: [PATCH 29/66] added basic (still broken) typing tests --- .coveragerc | 1 + .github/workflows/main.yml | 1 - progressbar/py.typed | 0 tox.ini | 6 ++++++ 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 progressbar/py.typed diff --git a/.coveragerc b/.coveragerc index 995b08fe..e5ec1f57 100644 --- a/.coveragerc +++ b/.coveragerc @@ -22,3 +22,4 @@ exclude_lines = raise NotImplementedError if 0: if __name__ == .__main__.: + if types.TYPE_CHECKING: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b19e5d8..4c86a063 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,5 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions - name: Test with tox run: tox diff --git a/progressbar/py.typed b/progressbar/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/tox.ini b/tox.ini index 99d982dd..afba92f9 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,12 @@ basepython = python3 deps = flake8 commands = flake8 {toxinidir}/progressbar {toxinidir}/tests {toxinidir}/examples.py +[testenv:pyright] +changedir = +basepython = python3 +deps = pyright +commands = pyright {toxinidir}/progressbar + [testenv:docs] changedir = basepython = python3 From 1ba23eea00b6eb108cbc1868da16002d5c6680b3 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 18 Oct 2022 13:04:18 +0200 Subject: [PATCH 30/66] added tox to requirements for github --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c86a063..e534cab5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,6 +21,6 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip tox - name: Test with tox run: tox From 11cfea0ae2a78828c0b7cc9ba4e80b733ea719a7 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 18 Oct 2022 13:41:26 +0200 Subject: [PATCH 31/66] fixing (some) github actions warnings --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e534cab5..84ccac34 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,9 +14,9 @@ jobs: python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From c65e4b428e503aadf766eff5cd9716715a43d87b Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 18 Oct 2022 13:51:52 +0200 Subject: [PATCH 32/66] added multiple threaded progress bars example --- README.rst | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.rst b/README.rst index b485fb99..94b33333 100644 --- a/README.rst +++ b/README.rst @@ -237,3 +237,68 @@ Bar with wide Chinese (or other multibyte) characters ) for i in bar(range(10)): time.sleep(0.1) + +Showing multiple (threaded) independent progress bars in parallel +============================================================================== + +While this method works fine and will continue to work fine, a smarter and +fully automatic version of this is currently being made: +https://github.com/WoLpH/python-progressbar/issues/176 + +.. code:: python + + import random + import sys + import threading + import time + + import progressbar + + output_lock = threading.Lock() + + + class LineOffsetStreamWrapper: + UP = '\033[F' + DOWN = '\033[B' + + def __init__(self, lines=0, stream=sys.stderr): + self.stream = stream + self.lines = lines + + def write(self, data): + with output_lock: + self.stream.write(self.UP * self.lines) + self.stream.write(data) + self.stream.write(self.DOWN * self.lines) + self.stream.flush() + + def __getattr__(self, name): + return getattr(self.stream, name) + + + bars = [] + for i in range(5): + bars.append( + progressbar.ProgressBar( + fd=LineOffsetStreamWrapper(i), + max_value=1000, + ) + ) + + if i: + print('Reserve a line for the progressbar') + + + class Worker(threading.Thread): + def __init__(self, bar): + super().__init__() + self.bar = bar + + def run(self): + for i in range(1000): + time.sleep(random.random() / 100) + self.bar.update(i) + + + for bar in bars: + Worker(bar).start() From 57a4a6580e46d6b2fd37467853217262f36078f0 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 18 Oct 2022 13:54:28 +0200 Subject: [PATCH 33/66] Incrementing version to v4.1.0 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index 98474085..fdbcba78 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '4.0.0' +__version__ = '4.1.0' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From 78a58e208c3fe916a454f9d13f15d596a4a3d7a3 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 18 Oct 2022 14:44:57 +0200 Subject: [PATCH 34/66] Fixed backwards compatibility with original progressbar library --- progressbar/widgets.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 9ffb68af..8d81bb6d 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -272,6 +272,9 @@ class Timer(FormatLabel, TimeSensitiveWidgetBase): '''WidgetBase which displays the elapsed seconds.''' def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs): + if '%s' in format and '%(elapsed)s' not in format: + format = format.replace('%s', '%(elapsed)s') + FormatLabel.__init__(self, format=format, **kwargs) TimeSensitiveWidgetBase.__init__(self, **kwargs) @@ -373,6 +376,9 @@ def __init__( format_NA='ETA: N/A', **kwargs): + if '%s' in format and '%(eta)s' not in format: + format = format.replace('%s', '%(eta)s') + Timer.__init__(self, **kwargs) self.format_not_started = format_not_started self.format_finished = format_finished From 5ac9a2c990dbac3e483dfadb1586fd575a218a77 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 18 Oct 2022 14:45:27 +0200 Subject: [PATCH 35/66] Incrementing version to v4.1.1 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index fdbcba78..f964fbda 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '4.1.0' +__version__ = '4.1.1' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From c83cef9a59d0dbd9e1971e6174f23241b497f1dc Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 20 Oct 2022 01:20:41 +0200 Subject: [PATCH 36/66] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index f3aaf432..38887b7c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015, Rick van Hattem (Wolph) +Copyright (c) 2022, Rick van Hattem (Wolph) All rights reserved. Redistribution and use in source and binary forms, with or without From 37d389772c87ae1a18066db92bcd489601889774 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 26 Oct 2022 13:54:42 +0200 Subject: [PATCH 37/66] Small documentation tweaks --- docs/index.rst | 2 ++ docs/progressbar.bar.rst | 1 + progressbar/bar.py | 24 ++++++++++++++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f505cbaa..58b16d58 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,7 @@ Welcome to Progress Bar's documentation! .. toctree:: :maxdepth: 4 + usage examples contributing installation @@ -13,6 +14,7 @@ Welcome to Progress Bar's documentation! progressbar.base progressbar.utils progressbar.widgets + history .. include:: ../README.rst diff --git a/docs/progressbar.bar.rst b/docs/progressbar.bar.rst index 971fa84e..7b7a0a39 100644 --- a/docs/progressbar.bar.rst +++ b/docs/progressbar.bar.rst @@ -5,3 +5,4 @@ progressbar.bar module :members: :undoc-members: :show-inheritance: + :member-order: bysource diff --git a/progressbar/bar.py b/progressbar/bar.py index 025471e9..cd094a0a 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -27,6 +27,9 @@ logger = logging.getLogger(__name__) +T = types.TypeVar('T') + + class ProgressBarMixinBase(object): def __init__(self, **kwargs): @@ -265,16 +268,21 @@ class ProgressBar(StdRedirectMixin, ResizableMixin, ProgressBarBase): the current progress bar. As a result, you have access to the ProgressBar's methods and attributes. Although there is nothing preventing you from changing the ProgressBar you should treat it as read only. - - Useful methods and attributes include (Public API): - - value: current progress (min_value <= value <= max_value) - - max_value: maximum (and final) value - - end_time: not None if the bar has finished (reached 100%) - - start_time: the time when start() method of ProgressBar was called - - seconds_elapsed: seconds elapsed since start_time and last call to - update ''' + #: Current progress (min_value <= value <= max_value) + value: T + #: Maximum (and final) value. Beyond this value an error will be raised + #: unless the `max_error` parameter is `False`. + max_value: T + #: The time the progressbar reached `max_value` or when `finish()` was + #: called. + end_time: datetime + #: The time `start()` was called or iteration started. + start_time: datetime + #: Seconds between `start_time` and last call to `update()` + seconds_elapsed: float + _DEFAULT_MAXVAL = base.UnknownLength # update every 50 milliseconds (up to a 20 times per second) _MINIMUM_UPDATE_INTERVAL = 0.050 From 7ecc510f274fa2402e9e9c6676718407fdd8c340 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 26 Oct 2022 13:54:59 +0200 Subject: [PATCH 38/66] added currval support for legacy progressbar users --- progressbar/bar.py | 10 ++++++++++ tests/test_failure.py | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/progressbar/bar.py b/progressbar/bar.py index cd094a0a..36043303 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -796,6 +796,16 @@ def finish(self, end='\n', dirty=False): ResizableMixin.finish(self) ProgressBarBase.finish(self) + @property + def currval(self): + ''' + Legacy method to make progressbar-2 compatible with the original + progressbar package + ''' + warnings.warn('The usage of `currval` is deprecated, please use ' + '`value` instead', DeprecationWarning) + return self.value + class DataTransferBar(ProgressBar): '''A progress bar with sensible defaults for downloads etc. diff --git a/tests/test_failure.py b/tests/test_failure.py index 40fee23c..030ab292 100644 --- a/tests/test_failure.py +++ b/tests/test_failure.py @@ -99,6 +99,13 @@ def test_deprecated_poll(): progressbar.ProgressBar(poll=5) +def test_deprecated_currval(): + with pytest.warns(DeprecationWarning): + bar = progressbar.ProgressBar(max_value=5) + bar.update(2) + assert bar.currval == 2 + + def test_unexpected_update_keyword_arg(): p = progressbar.ProgressBar(max_value=10) with pytest.raises(TypeError): From 35639c4a8d2513eccfbf207f97fd92ba8d35c7dd Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 26 Oct 2022 14:14:41 +0200 Subject: [PATCH 39/66] added method for easy incrementing --- progressbar/bar.py | 5 ++++- tests/test_iterators.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index 36043303..bb3db497 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -576,7 +576,10 @@ def __enter__(self): def __iadd__(self, value): 'Updates the ProgressBar by adding a new value.' - self.update(self.value + value) + return self.increment(value) + + def increment(self, value, *args, **kwargs): + self.update(self.value + value, *args, **kwargs) return self def _format_widgets(self): diff --git a/tests/test_iterators.py b/tests/test_iterators.py index 188809a3..b32c529e 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -51,7 +51,8 @@ def test_adding_value(): p = progressbar.ProgressBar(max_value=10) p.start() p.update(5) - p += 5 + p += 2 + p.increment(2) with pytest.raises(ValueError): p += 5 From 706bf58122b43ad5e5716b0478a83b08ad7a828c Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 26 Oct 2022 15:39:36 +0200 Subject: [PATCH 40/66] added usage doc --- docs/usage.rst | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/usage.rst diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 00000000..6aa03303 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,70 @@ +======== +Usage +======== + +There are many ways to use Python Progressbar, you can see a few basic examples +here but there are many more in the :doc:`examples` file. + +Wrapping an iterable +------------------------------------------------------------------------------ +:: + + import time + import progressbar + + bar = progressbar.ProgressBar() + for i in bar(range(100)): + time.sleep(0.02) + +Context wrapper +------------------------------------------------------------------------------ +:: + + import time + import progressbar + + with progressbar.ProgressBar(max_value=10) as bar: + for i in range(10): + time.sleep(0.1) + bar.update(i) + +Combining progressbars with print output +------------------------------------------------------------------------------ +:: + + import time + import progressbar + + bar = progressbar.ProgressBar(redirect_stdout=True) + for i in range(100): + print 'Some text', i + time.sleep(0.1) + bar.update(i) + +Progressbar with unknown length +------------------------------------------------------------------------------ +:: + + import time + import progressbar + + bar = progressbar.ProgressBar(max_value=progressbar.UnknownLength) + for i in range(20): + time.sleep(0.1) + bar.update(i) + +Bar with custom widgets +------------------------------------------------------------------------------ +:: + + import time + import progressbar + + bar = progressbar.ProgressBar(widgets=[ + ' [', progressbar.Timer(), '] ', + progressbar.Bar(), + ' (', progressbar.ETA(), ') ', + ]) + for i in bar(range(20)): + time.sleep(0.1) + From b1dd4853a53331d6e910c997216f2dfbf481591d Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 26 Oct 2022 15:42:38 +0200 Subject: [PATCH 41/66] added history doc --- docs/history.rst | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/history.rst diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 00000000..91f04cb8 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,8 @@ +.. _history: + +======= +History +======= + +.. include:: ../CHANGES.rst + :start-line: 5 From a567b8e7f7f1716abd7c6ce7322544dcd0e70c3a Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 26 Oct 2022 15:45:18 +0200 Subject: [PATCH 42/66] added default value to increment method --- progressbar/bar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index bb3db497..691a61ea 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -578,7 +578,7 @@ def __iadd__(self, value): 'Updates the ProgressBar by adding a new value.' return self.increment(value) - def increment(self, value, *args, **kwargs): + def increment(self, value=1, *args, **kwargs): self.update(self.value + value, *args, **kwargs) return self From 3e997e81bee021f60b3fc2adcd313f972b489a7b Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 26 Oct 2022 15:47:23 +0200 Subject: [PATCH 43/66] Incrementing version to v4.2.0 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index f964fbda..bc898708 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -19,7 +19,7 @@ long running operations. '''.strip().split()) __email__ = 'wolph@wol.ph' -__version__ = '4.1.1' +__version__ = '4.2.0' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From f19bb2e0553d1361b33c91e2e36dd1d8e6fa96d9 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 27 Oct 2022 12:23:03 +0200 Subject: [PATCH 44/66] reformatted and added more type hinting --- progressbar/bar.py | 189 ++++++++++++++++++++++++++++----------------- 1 file changed, 120 insertions(+), 69 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index 691a61ea..4cb79f5d 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -1,42 +1,40 @@ from __future__ import annotations import logging -import math import os import sys import time import timeit import warnings +from abc import ABC from copy import deepcopy from datetime import datetime -from python_utils import types - -try: # pragma: no cover - from collections import abc -except ImportError: # pragma: no cover - import collections as abc - -from python_utils import converters +import math +from python_utils import converters, types -from . import widgets -from . import widgets as widgets_module # Avoid name collision -from . import base -from . import utils +from . import ( + base, + utils, + widgets, + widgets as widgets_module, # Avoid name collision +) logger = logging.getLogger(__name__) - T = types.TypeVar('T') class ProgressBarMixinBase(object): + _started = False + _finished = False + term_width: int = 80 def __init__(self, **kwargs): - self._finished = False + pass def start(self, **kwargs): - pass + self._started = True def update(self, value=None): pass @@ -45,23 +43,36 @@ def finish(self): # pragma: no cover self._finished = True def __del__(self): - if not self._finished: # pragma: no cover + if not self._finished and self._started: # pragma: no cover try: self.finish() except Exception: - pass + # Never raise during cleanup. We're too late now + logging.debug( + 'Exception raised during ProgressBar cleanup', + exc_info=True + ) + def __getstate__(self): + return self.__dict__ -class ProgressBarBase(abc.Iterable, ProgressBarMixinBase): + +class ProgressBarBase(types.Iterable, ProgressBarMixinBase, ABC): pass class DefaultFdMixin(ProgressBarMixinBase): - - def __init__(self, fd: types.IO = sys.stderr, - is_terminal: bool | None = None, - line_breaks: bool | None = None, - enable_colors: bool | None = None, **kwargs): + fd: types.IO = sys.stderr + is_ansi_terminal: bool = False + line_breaks: bool = True + enable_colors: bool = False + + def __init__( + self, fd: types.IO = sys.stderr, + is_terminal: bool | None = None, + line_breaks: bool | None = None, + enable_colors: bool | None = None, **kwargs + ): if fd is sys.stdout: fd = utils.streams.original_stdout @@ -73,22 +84,27 @@ def __init__(self, fd: types.IO = sys.stderr, # Check if this is an interactive terminal self.is_terminal = utils.is_terminal( - fd, is_terminal or self.is_ansi_terminal) + fd, is_terminal or self.is_ansi_terminal + ) # Check if it should overwrite the current line (suitable for # iteractive terminals) or write line breaks (suitable for log files) if line_breaks is None: - line_breaks = utils.env_flag('PROGRESSBAR_LINE_BREAKS', - not self.is_terminal) - self.line_breaks = line_breaks + line_breaks = utils.env_flag( + 'PROGRESSBAR_LINE_BREAKS', + not self.is_terminal + ) + self.line_breaks = bool(line_breaks) # Check if ANSI escape characters are enabled (suitable for iteractive # terminals), or should be stripped off (suitable for log files) if enable_colors is None: - enable_colors = utils.env_flag('PROGRESSBAR_ENABLE_COLORS', - self.is_ansi_terminal) + enable_colors = utils.env_flag( + 'PROGRESSBAR_ENABLE_COLORS', + self.is_ansi_terminal + ) - self.enable_colors = enable_colors + self.enable_colors = bool(enable_colors) ProgressBarMixinBase.__init__(self, **kwargs) @@ -121,6 +137,16 @@ def finish(self, *args, **kwargs): # pragma: no cover self.fd.flush() + def _format_line(self): + 'Joins the widgets and justifies the line' + + widgets = ''.join(self._to_unicode(self._format_widgets())) + + if self.left_justify: + return widgets.ljust(self.term_width) + else: + return widgets.rjust(self.term_width) + class ResizableMixin(ProgressBarMixinBase): @@ -157,9 +183,17 @@ def finish(self): # pragma: no cover class StdRedirectMixin(DefaultFdMixin): - - def __init__(self, redirect_stderr: bool = False, - redirect_stdout: bool = False, **kwargs): + redirect_stderr: bool = False + redirect_stdout: bool = False + stdout: types.IO + stderr: types.IO + _stdout: types.IO + _stderr: types.IO + + def __init__( + self, redirect_stderr: bool = False, + redirect_stdout: bool = False, **kwargs + ): DefaultFdMixin.__init__(self, **kwargs) self.redirect_stderr = redirect_stderr self.redirect_stdout = redirect_stdout @@ -199,7 +233,12 @@ def finish(self, end='\n'): utils.streams.unwrap_stderr() -class ProgressBar(StdRedirectMixin, ResizableMixin, ProgressBarBase): +class ProgressBar( + StdRedirectMixin, + ResizableMixin, + ProgressBarBase, + types.Generic[T], +): '''The ProgressBar class which updates and prints the bar. Args: @@ -287,11 +326,13 @@ class ProgressBar(StdRedirectMixin, ResizableMixin, ProgressBarBase): # update every 50 milliseconds (up to a 20 times per second) _MINIMUM_UPDATE_INTERVAL = 0.050 - def __init__(self, min_value=0, max_value=None, widgets=None, - left_justify=True, initial_value=0, poll_interval=None, - widget_kwargs=None, custom_len=utils.len_color, - max_error=True, prefix=None, suffix=None, variables=None, - min_poll_interval=None, **kwargs): + def __init__( + self, min_value=0, max_value=None, widgets=None, + left_justify=True, initial_value=0, poll_interval=None, + widget_kwargs=None, custom_len=utils.len_color, + max_error=True, prefix=None, suffix=None, variables=None, + min_poll_interval=None, **kwargs + ): ''' Initializes a progress bar with sane defaults ''' @@ -299,19 +340,25 @@ def __init__(self, min_value=0, max_value=None, widgets=None, ResizableMixin.__init__(self, **kwargs) ProgressBarBase.__init__(self, **kwargs) if not max_value and kwargs.get('maxval') is not None: - warnings.warn('The usage of `maxval` is deprecated, please use ' - '`max_value` instead', DeprecationWarning) + warnings.warn( + 'The usage of `maxval` is deprecated, please use ' + '`max_value` instead', DeprecationWarning + ) max_value = kwargs.get('maxval') if not poll_interval and kwargs.get('poll'): - warnings.warn('The usage of `poll` is deprecated, please use ' - '`poll_interval` instead', DeprecationWarning) + warnings.warn( + 'The usage of `poll` is deprecated, please use ' + '`poll_interval` instead', DeprecationWarning + ) poll_interval = kwargs.get('poll') if max_value: if min_value > max_value: - raise ValueError('Max value needs to be bigger than the min ' - 'value') + raise ValueError( + 'Max value needs to be bigger than the min ' + 'value' + ) self.min_value = min_value self.max_value = max_value self.max_error = max_error @@ -346,10 +393,13 @@ def __init__(self, min_value=0, max_value=None, widgets=None, # comparison is run for _every_ update. With billions of updates # (downloading a 1GiB file for example) this adds up. poll_interval = utils.deltas_to_seconds(poll_interval, default=None) - min_poll_interval = utils.deltas_to_seconds(min_poll_interval, - default=None) + min_poll_interval = utils.deltas_to_seconds( + min_poll_interval, + default=None + ) self._MINIMUM_UPDATE_INTERVAL = utils.deltas_to_seconds( - self._MINIMUM_UPDATE_INTERVAL) + self._MINIMUM_UPDATE_INTERVAL + ) # Note that the _MINIMUM_UPDATE_INTERVAL sets the minimum in case of # low values. @@ -520,7 +570,8 @@ def default_widgets(self): widgets.Percentage(**self.widget_kwargs), ' ', widgets.SimpleProgress( format='(%s)' % widgets.SimpleProgress.DEFAULT_FORMAT, - **self.widget_kwargs), + **self.widget_kwargs + ), ' ', widgets.Bar(**self.widget_kwargs), ' ', widgets.Timer(**self.widget_kwargs), ' ', widgets.AdaptiveETA(**self.widget_kwargs), @@ -590,7 +641,7 @@ def _format_widgets(self): for index, widget in enumerate(self.widgets): if isinstance(widget, widgets.WidgetBase) \ - and not widget.check_size(self): + and not widget.check_size(self): continue elif isinstance(widget, widgets.AutoWidthWidgetBase): result.append(widget) @@ -621,16 +672,6 @@ def _to_unicode(cls, args): for arg in args: yield converters.to_unicode(arg) - def _format_line(self): - 'Joins the widgets and justifies the line' - - widgets = ''.join(self._to_unicode(self._format_widgets())) - - if self.left_justify: - return widgets.ljust(self.term_width) - else: - return widgets.rjust(self.term_width) - def _needs_update(self): 'Returns whether the ProgressBar should redraw the line.' delta = timeit.default_timer() - self._last_update_timer @@ -671,7 +712,8 @@ def update(self, value=None, force=False, **kwargs): elif self.max_error: raise ValueError( 'Value %s is out of range, should be between %s and %s' - % (value, self.min_value, self.max_value)) + % (value, self.min_value, self.max_value) + ) else: self.max_value = value @@ -684,7 +726,8 @@ def update(self, value=None, force=False, **kwargs): if key not in self.variables: raise TypeError( 'update() got an unexpected keyword ' + - 'argument {0!r}'.format(key)) + 'argument {0!r}'.format(key) + ) elif self.variables[key] != kwargs[key]: self.variables[key] = kwargs[key] variables_changed = True @@ -738,15 +781,21 @@ def start(self, max_value=None, init=True): self.widgets = self.default_widgets() if self.prefix: - self.widgets.insert(0, widgets.FormatLabel( - self.prefix, new_style=True)) + self.widgets.insert( + 0, widgets.FormatLabel( + self.prefix, new_style=True + ) + ) # Unset the prefix variable after applying so an extra start() # won't keep copying it self.prefix = None if self.suffix: - self.widgets.append(widgets.FormatLabel( - self.suffix, new_style=True)) + self.widgets.append( + widgets.FormatLabel( + self.suffix, new_style=True + ) + ) # Unset the suffix variable after applying so an extra start() # won't keep copying it self.suffix = None @@ -805,8 +854,10 @@ def currval(self): Legacy method to make progressbar-2 compatible with the original progressbar package ''' - warnings.warn('The usage of `currval` is deprecated, please use ' - '`value` instead', DeprecationWarning) + warnings.warn( + 'The usage of `currval` is deprecated, please use ' + '`value` instead', DeprecationWarning + ) return self.value From b2e2777becb868864a0a914009c389ae502986f4 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Fri, 28 Oct 2022 20:03:43 +0200 Subject: [PATCH 45/66] Much more type hinting --- progressbar/widgets.py | 426 ++++++++++++++++++++++++++++------------- 1 file changed, 291 insertions(+), 135 deletions(-) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 8d81bb6d..30ca01a5 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- +from __future__ import annotations import abc import datetime import functools import pprint import sys +import typing -from python_utils import converters -from python_utils import types +from python_utils import converters, types -from . import base -from . import utils +from . import base, utils if types.TYPE_CHECKING: from .bar import ProgressBar @@ -18,6 +18,9 @@ MAX_TIME = datetime.time.max MAX_DATETIME = datetime.datetime.max +Data = types.Dict[str, types.Any] +FormatString = typing.Optional[str] + def string_or_lambda(input_): if isinstance(input_, str): @@ -49,24 +52,26 @@ def create_wrapper(wrapper): if isinstance(wrapper, str): assert '{}' in wrapper, 'Expected string with {} for formatting' else: - raise RuntimeError('Pass either a begin/end string as a tuple or a' - ' template string with {}') + raise RuntimeError( + 'Pass either a begin/end string as a tuple or a' + ' template string with {}' + ) return wrapper -def wrapper(function, wrapper): +def wrapper(function, wrapper_): '''Wrap the output of a function in a template string or a tuple with begin/end strings ''' - wrapper = create_wrapper(wrapper) - if not wrapper: + wrapper_ = create_wrapper(wrapper_) + if not wrapper_: return function @functools.wraps(function) def wrap(*args, **kwargs): - return wrapper.format(function(*args, **kwargs)) + return wrapper_.format(function(*args, **kwargs)) return wrap @@ -74,7 +79,7 @@ def wrap(*args, **kwargs): def create_marker(marker, wrap=None): def _marker(progress, data, width): if progress.max_value is not base.UnknownLength \ - and progress.max_value > 0: + and progress.max_value > 0: length = int(progress.value / progress.max_value * width) return (marker * length) else: @@ -89,7 +94,7 @@ def _marker(progress, data, width): return wrapper(marker, wrap) -class FormatWidgetMixin(object): +class FormatWidgetMixin(abc.ABC): '''Mixin to format widgets using a formatstring Variables available: @@ -104,16 +109,25 @@ class FormatWidgetMixin(object): days - percentage: Percentage as a float ''' - required_values = [] - def __init__(self, format, new_style=False, **kwargs): + def __init__(self, format: str, new_style: bool = False, **kwargs): self.new_style = new_style self.format = format - def get_format(self, progress, data, format=None): + def get_format( + self, + progress: ProgressBar, + data: Data, + format: types.Optional[str] = None + ) -> str: return format or self.format - def __call__(self, progress, data, format=None): + def __call__( + self, + progress: ProgressBar, + data: Data, + format: types.Optional[str] = None, + ): '''Formats the widget into a string''' format = self.get_format(progress, data, format) try: @@ -127,7 +141,7 @@ def __call__(self, progress, data, format=None): raise -class WidthWidgetMixin(object): +class WidthWidgetMixin(abc.ABC): '''Mixing to make sure widgets are only visible if the screen is within a specified size range so the progressbar fits on both large and small screens.. @@ -136,7 +150,7 @@ class WidthWidgetMixin(object): - min_width: Only display the widget if at least `min_width` is left - max_width: Only display the widget if at most `max_width` is left - >>> class Progress(object): + >>> class Progress: ... term_width = 0 >>> WidthWidgetMixin(5, 10).check_size(Progress) @@ -165,8 +179,7 @@ def check_size(self, progress: 'ProgressBar'): return True -class WidgetBase(WidthWidgetMixin): - __metaclass__ = abc.ABCMeta +class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta): '''The base class for all widgets The ProgressBar will call the widget's update value when the widget should @@ -196,14 +209,14 @@ class WidgetBase(WidthWidgetMixin): copy = True @abc.abstractmethod - def __call__(self, progress, data): + def __call__(self, progress: ProgressBar, data: Data): '''Updates the widget. progress - a reference to the calling ProgressBar ''' -class AutoWidthWidgetBase(WidgetBase): +class AutoWidthWidgetBase(WidgetBase, metaclass=abc.ABCMeta): '''The base class for all variable width widgets. This widget is much like the \\hfill command in TeX, it will expand to @@ -212,7 +225,12 @@ class AutoWidthWidgetBase(WidgetBase): ''' @abc.abstractmethod - def __call__(self, progress, data, width): + def __call__( + self, + progress: ProgressBar, + data: Data, + width: int = 0, + ): '''Updates the widget providing the total width the widget must fill. progress - a reference to the calling ProgressBar @@ -220,7 +238,7 @@ def __call__(self, progress, data, width): ''' -class TimeSensitiveWidgetBase(WidgetBase): +class TimeSensitiveWidgetBase(WidgetBase, metaclass=abc.ABCMeta): '''The base class for all time sensitive widgets. Some widgets like timers would become out of date unless updated at least @@ -233,7 +251,7 @@ class FormatLabel(FormatWidgetMixin, WidgetBase): '''Displays a formatted label >>> label = FormatLabel('%(value)s', min_width=5, max_width=10) - >>> class Progress(object): + >>> class Progress: ... pass >>> label = FormatLabel('{value} :: {value:^6}', new_style=True) >>> str(label(Progress, dict(value='test'))) @@ -255,7 +273,12 @@ def __init__(self, format: str, **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, **kwargs) - def __call__(self, progress, data, **kwargs): + def __call__( + self, + progress: ProgressBar, + data: Data, + format: types.Optional[str] = None, + ): for name, (key, transform) in self.mapping.items(): try: if transform is None: @@ -265,7 +288,7 @@ def __call__(self, progress, data, **kwargs): except (KeyError, ValueError, IndexError): # pragma: no cover pass - return FormatWidgetMixin.__call__(self, progress, data, **kwargs) + return FormatWidgetMixin.__call__(self, progress, data, format) class Timer(FormatLabel, TimeSensitiveWidgetBase): @@ -282,7 +305,7 @@ def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs): format_time = staticmethod(utils.format_time) -class SamplesMixin(TimeSensitiveWidgetBase): +class SamplesMixin(TimeSensitiveWidgetBase, metaclass=abc.ABCMeta): ''' Mixing for widgets that average multiple measurements @@ -314,19 +337,21 @@ class SamplesMixin(TimeSensitiveWidgetBase): True ''' - def __init__(self, samples=datetime.timedelta(seconds=2), key_prefix=None, - **kwargs): + def __init__( + self, samples=datetime.timedelta(seconds=2), key_prefix=None, + **kwargs, + ): self.samples = samples self.key_prefix = (self.__class__.__name__ or key_prefix) + '_' TimeSensitiveWidgetBase.__init__(self, **kwargs) - def get_sample_times(self, progress, data): + def get_sample_times(self, progress: ProgressBar, data: Data): return progress.extra.setdefault(self.key_prefix + 'sample_times', []) - def get_sample_values(self, progress, data): + def get_sample_values(self, progress: ProgressBar, data: Data): return progress.extra.setdefault(self.key_prefix + 'sample_values', []) - def __call__(self, progress, data, delta=False): + def __call__(self, progress: ProgressBar, data: Data, delta: bool = False): sample_times = self.get_sample_times(progress, data) sample_values = self.get_sample_values(progress, data) @@ -368,13 +393,14 @@ class ETA(Timer): '''WidgetBase which attempts to estimate the time of arrival.''' def __init__( - self, - format_not_started='ETA: --:--:--', - format_finished='Time: %(elapsed)8s', - format='ETA: %(eta)8s', - format_zero='ETA: 00:00:00', - format_NA='ETA: N/A', - **kwargs): + self, + format_not_started='ETA: --:--:--', + format_finished='Time: %(elapsed)8s', + format='ETA: %(eta)8s', + format_zero='ETA: 00:00:00', + format_NA='ETA: N/A', + **kwargs, + ): if '%s' in format and '%(eta)s' not in format: format = format.replace('%s', '%(eta)s') @@ -386,7 +412,7 @@ def __init__( self.format_zero = format_zero self.format_NA = format_NA - def _calculate_eta(self, progress, data, value, elapsed): + def _calculate_eta(self, progress: ProgressBar, data: Data, value, elapsed): '''Updates the widget to show the ETA or total time when finished.''' if elapsed: # The max() prevents zero division errors @@ -398,7 +424,13 @@ def _calculate_eta(self, progress, data, value, elapsed): return eta_seconds - def __call__(self, progress, data, value=None, elapsed=None): + def __call__( + self, + progress: ProgressBar, + data: Data, + value=None, + elapsed=None, + ): '''Updates the widget to show the ETA or total time when finished.''' if value is None: value = data['value'] @@ -409,7 +441,8 @@ def __call__(self, progress, data, value=None, elapsed=None): ETA_NA = False try: data['eta_seconds'] = self._calculate_eta( - progress, data, value=value, elapsed=elapsed) + progress, data, value=value, elapsed=elapsed + ) except TypeError: data['eta_seconds'] = None ETA_NA = True @@ -438,7 +471,7 @@ def __call__(self, progress, data, value=None, elapsed=None): class AbsoluteETA(ETA): '''Widget which attempts to estimate the absolute time of arrival.''' - def _calculate_eta(self, progress, data, value, elapsed): + def _calculate_eta(self, progress: ProgressBar, data: Data, value, elapsed): eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed) now = datetime.datetime.now() try: @@ -447,13 +480,16 @@ def _calculate_eta(self, progress, data, value, elapsed): return datetime.datetime.max def __init__( - self, - format_not_started='Estimated finish time: ----/--/-- --:--:--', - format_finished='Finished at: %(elapsed)s', - format='Estimated finish time: %(eta)s', - **kwargs): - ETA.__init__(self, format_not_started=format_not_started, - format_finished=format_finished, format=format, **kwargs) + self, + format_not_started='Estimated finish time: ----/--/-- --:--:--', + format_finished='Finished at: %(elapsed)s', + format='Estimated finish time: %(eta)s', + **kwargs, + ): + ETA.__init__( + self, format_not_started=format_not_started, + format_finished=format_finished, format=format, **kwargs, + ) class AdaptiveETA(ETA, SamplesMixin): @@ -467,9 +503,17 @@ def __init__(self, **kwargs): ETA.__init__(self, **kwargs) SamplesMixin.__init__(self, **kwargs) - def __call__(self, progress, data): - elapsed, value = SamplesMixin.__call__(self, progress, data, - delta=True) + def __call__( + self, + progress: ProgressBar, + data: Data, + value=None, + elapsed=None, + ): + elapsed, value = SamplesMixin.__call__( + self, progress, data, + delta=True + ) if not elapsed: value = None elapsed = 0 @@ -486,17 +530,23 @@ class DataSize(FormatWidgetMixin, WidgetBase): ''' def __init__( - self, variable='value', - format='%(scaled)5.1f %(prefix)s%(unit)s', unit='B', - prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), - **kwargs): + self, variable='value', + format='%(scaled)5.1f %(prefix)s%(unit)s', unit='B', + prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), + **kwargs, + ): self.variable = variable self.unit = unit self.prefixes = prefixes FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, **kwargs) - def __call__(self, progress, data): + def __call__( + self, + progress: ProgressBar, + data: Data, + format: types.Optional[str] = None, + ): value = data[self.variable] if value is not None: scaled, power = utils.scale_1024(value, len(self.prefixes)) @@ -507,7 +557,7 @@ def __call__(self, progress, data): data['prefix'] = self.prefixes[power] data['unit'] = self.unit - return FormatWidgetMixin.__call__(self, progress, data) + return FormatWidgetMixin.__call__(self, progress, data, format) class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): @@ -516,10 +566,11 @@ class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): ''' def __init__( - self, format='%(scaled)5.1f %(prefix)s%(unit)-s/s', - inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', unit='B', - prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), - **kwargs): + self, format='%(scaled)5.1f %(prefix)s%(unit)-s/s', + inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', unit='B', + prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), + **kwargs, + ): self.unit = unit self.prefixes = prefixes self.inverse_format = inverse_format @@ -530,17 +581,24 @@ def _speed(self, value, elapsed): speed = float(value) / elapsed return utils.scale_1024(speed, len(self.prefixes)) - def __call__(self, progress, data, value=None, total_seconds_elapsed=None): + def __call__( + self, + progress: ProgressBar, + data, + value=None, + total_seconds_elapsed=None + ): '''Updates the widget with the current SI prefixed speed.''' if value is None: value = data['value'] elapsed = utils.deltas_to_seconds( total_seconds_elapsed, - data['total_seconds_elapsed']) + data['total_seconds_elapsed'] + ) if value is not None and elapsed is not None \ - and elapsed > 2e-6 and value > 2e-6: # =~ 0 + and elapsed > 2e-6 and value > 2e-6: # =~ 0 scaled, power = self._speed(value, elapsed) else: scaled = power = 0 @@ -551,8 +609,10 @@ def __call__(self, progress, data, value=None, total_seconds_elapsed=None): scaled = 1 / scaled data['scaled'] = scaled data['prefix'] = self.prefixes[0] - return FormatWidgetMixin.__call__(self, progress, data, - self.inverse_format) + return FormatWidgetMixin.__call__( + self, progress, data, + self.inverse_format + ) else: data['scaled'] = scaled data['prefix'] = self.prefixes[power] @@ -567,9 +627,17 @@ def __init__(self, **kwargs): FileTransferSpeed.__init__(self, **kwargs) SamplesMixin.__init__(self, **kwargs) - def __call__(self, progress, data): - elapsed, value = SamplesMixin.__call__(self, progress, data, - delta=True) + def __call__( + self, + progress: ProgressBar, + data, + value=None, + total_seconds_elapsed=None + ): + elapsed, value = SamplesMixin.__call__( + self, progress, data, + delta=True + ) return FileTransferSpeed.__call__(self, progress, data, value, elapsed) @@ -578,8 +646,10 @@ class AnimatedMarker(TimeSensitiveWidgetBase): it were rotating. ''' - def __init__(self, markers='|/-\\', default=None, fill='', - marker_wrap=None, fill_wrap=None, **kwargs): + def __init__( + self, markers='|/-\\', default=None, fill='', + marker_wrap=None, fill_wrap=None, **kwargs, + ): self.markers = markers self.marker_wrap = create_wrapper(marker_wrap) self.default = default or markers[0] @@ -587,7 +657,7 @@ def __init__(self, markers='|/-\\', default=None, fill='', self.fill = create_marker(fill, self.fill_wrap) if fill else None WidgetBase.__init__(self, **kwargs) - def __call__(self, progress, data, width=None): + def __call__(self, progress: ProgressBar, data: Data, width=None): '''Updates the widget to show the next marker or the first marker when finished''' @@ -600,8 +670,11 @@ def __call__(self, progress, data, width=None): if self.fill: # Cut the last character so we can replace it with our marker - fill = self.fill(progress, data, width - progress.custom_len( - marker)) + fill = self.fill( + progress, data, width - progress.custom_len( + marker + ) + ) else: fill = '' @@ -627,7 +700,7 @@ def __init__(self, format='%(value)d', **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, format=format, **kwargs) - def __call__(self, progress, data, format=None): + def __call__(self, progress: ProgressBar, data: Data, format=None): return FormatWidgetMixin.__call__(self, progress, data, format) @@ -639,7 +712,7 @@ def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, format=format, **kwargs) - def get_format(self, progress, data, format=None): + def get_format(self, progress: ProgressBar, data: Data, format=None): # If percentage is not available, display N/A% percentage = data.get('percentage', base.Undefined) if not percentage and percentage != 0: @@ -658,7 +731,7 @@ def __init__(self, format=DEFAULT_FORMAT, **kwargs): WidgetBase.__init__(self, format=format, **kwargs) self.max_width_cache = dict(default=self.max_width) - def __call__(self, progress, data, format=None): + def __call__(self, progress: ProgressBar, data: Data, format=None): # If max_value is not available, display N/A if data.get('max_value'): data['max_value_s'] = data.get('max_value') @@ -671,8 +744,10 @@ def __call__(self, progress, data, format=None): else: data['value_s'] = 0 - formatted = FormatWidgetMixin.__call__(self, progress, data, - format=format) + formatted = FormatWidgetMixin.__call__( + self, progress, data, + format=format + ) # Guess the maximum width from the min and max value key = progress.min_value, progress.max_value @@ -684,8 +759,11 @@ def __call__(self, progress, data, format=None): continue temporary_data['value'] = value - width = progress.custom_len(FormatWidgetMixin.__call__( - self, progress, temporary_data, format=format)) + width = progress.custom_len( + FormatWidgetMixin.__call__( + self, progress, temporary_data, format=format + ) + ) if width: # pragma: no branch max_width = max(max_width or 0, width) @@ -701,8 +779,10 @@ def __call__(self, progress, data, format=None): class Bar(AutoWidthWidgetBase): '''A progress bar which stretches to fill the line.''' - def __init__(self, marker='#', left='|', right='|', fill=' ', - fill_left=True, marker_wrap=None, **kwargs): + def __init__( + self, marker='#', left='|', right='|', fill=' ', + fill_left=True, marker_wrap=None, **kwargs, + ): '''Creates a customizable progress bar. The callable takes the same parameters as the `__call__` method @@ -722,7 +802,12 @@ def __init__(self, marker='#', left='|', right='|', fill=' ', AutoWidthWidgetBase.__init__(self, **kwargs) - def __call__(self, progress, data, width): + def __call__( + self, + progress: ProgressBar, + data: Data, + width: int = 0, + ): '''Updates the progress bar and its subcomponents''' left = converters.to_unicode(self.left(progress, data, width)) @@ -745,8 +830,10 @@ def __call__(self, progress, data, width): class ReverseBar(Bar): '''A bar which has a marker that goes from right to left''' - def __init__(self, marker='#', left='|', right='|', fill=' ', - fill_left=False, **kwargs): + def __init__( + self, marker='#', left='|', right='|', fill=' ', + fill_left=False, **kwargs, + ): '''Creates a customizable progress bar. marker - string or updatable object to use as a marker @@ -755,8 +842,10 @@ def __init__(self, marker='#', left='|', right='|', fill=' ', fill - character to use for the empty part of the progress bar fill_left - whether to fill from the left or the right ''' - Bar.__init__(self, marker=marker, left=left, right=right, fill=fill, - fill_left=fill_left, **kwargs) + Bar.__init__( + self, marker=marker, left=left, right=right, fill=fill, + fill_left=fill_left, **kwargs, + ) class BouncingBar(Bar, TimeSensitiveWidgetBase): @@ -764,7 +853,12 @@ class BouncingBar(Bar, TimeSensitiveWidgetBase): INTERVAL = datetime.timedelta(milliseconds=100) - def __call__(self, progress, data, width): + def __call__( + self, + progress: ProgressBar, + data: Data, + width: int = 0, + ): '''Updates the progress bar and its subcomponents''' left = converters.to_unicode(self.left(progress, data, width)) @@ -776,7 +870,8 @@ def __call__(self, progress, data, width): if width: # pragma: no branch value = int( - data['total_seconds_elapsed'] / self.INTERVAL.total_seconds()) + data['total_seconds_elapsed'] / self.INTERVAL.total_seconds() + ) a = value % width b = width - a - 1 @@ -792,24 +887,35 @@ def __call__(self, progress, data, width): class FormatCustomText(FormatWidgetMixin, WidgetBase): - mapping = {} + mapping: types.Dict[str, types.Any] = {} copy = False - def __init__(self, format, mapping=mapping, **kwargs): + def __init__( + self, + format: str, + mapping: types.Dict[str, types.Any] = None, + **kwargs, + ): self.format = format - self.mapping = mapping + self.mapping = mapping or self.mapping FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, **kwargs) - def update_mapping(self, **mapping): + def update_mapping(self, **mapping: types.Dict[str, types.Any]): self.mapping.update(mapping) - def __call__(self, progress, data): + def __call__( + self, + progress: ProgressBar, + data: Data, + format: types.Optional[str] = None, + ): return FormatWidgetMixin.__call__( - self, progress, self.mapping, self.format) + self, progress, self.mapping, format or self.format + ) -class VariableMixin(object): +class VariableMixin: '''Mixin to display a custom user variable ''' def __init__(self, name, **kwargs): @@ -843,10 +949,15 @@ def __init__(self, name, markers, **kwargs): for marker in markers ] - def get_values(self, progress, data): + def get_values(self, progress: ProgressBar, data: Data): return data['variables'][self.name] or [] - def __call__(self, progress, data, width): + def __call__( + self, + progress: ProgressBar, + data: Data, + width: int = 0, + ): '''Updates the progress bar and its subcomponents''' left = converters.to_unicode(self.left(progress, data, width)) @@ -877,37 +988,43 @@ def __call__(self, progress, data, width): class MultiProgressBar(MultiRangeBar): - def __init__(self, - name, - # NOTE: the markers are not whitespace even though some - # terminals don't show the characters correctly! - markers=' ▁▂▃▄▅▆▇█', - **kwargs): - MultiRangeBar.__init__(self, name=name, - markers=list(reversed(markers)), **kwargs) - - def get_values(self, progress, data): + def __init__( + self, + name, + # NOTE: the markers are not whitespace even though some + # terminals don't show the characters correctly! + markers=' ▁▂▃▄▅▆▇█', + **kwargs, + ): + MultiRangeBar.__init__( + self, name=name, + markers=list(reversed(markers)), **kwargs, + ) + + def get_values(self, progress: ProgressBar, data: Data): ranges = [0] * len(self.markers) - for progress in data['variables'][self.name] or []: - if not isinstance(progress, (int, float)): + for value in data['variables'][self.name] or []: + if not isinstance(value, (int, float)): # Progress is (value, max) - progress_value, progress_max = progress - progress = float(progress_value) / float(progress_max) + progress_value, progress_max = value + value = float(progress_value) / float(progress_max) - if progress < 0 or progress > 1: + if not 0 <= value <= 1: raise ValueError( 'Range value needs to be in the range [0..1], got %s' % - progress) + value + ) - range_ = progress * (len(ranges) - 1) + range_ = value * (len(ranges) - 1) pos = int(range_) frac = range_ % 1 - ranges[pos] += (1 - frac) - if (frac): - ranges[pos + 1] += (frac) + ranges[pos] += 1 - frac + if frac: + ranges[pos + 1] += frac if self.fill_left: ranges = list(reversed(ranges)) + return ranges @@ -936,8 +1053,10 @@ class GranularBar(AutoWidthWidgetBase): for example ''' - def __init__(self, markers=GranularMarkers.smooth, left='|', right='|', - **kwargs): + def __init__( + self, markers=GranularMarkers.smooth, left='|', right='|', + **kwargs, + ): '''Creates a customizable progress bar. markers - string of characters to use as granular progress markers. The @@ -952,13 +1071,18 @@ def __init__(self, markers=GranularMarkers.smooth, left='|', right='|', AutoWidthWidgetBase.__init__(self, **kwargs) - def __call__(self, progress, data, width): + def __call__( + self, + progress: ProgressBar, + data: Data, + width: int = 0, + ): left = converters.to_unicode(self.left(progress, data, width)) right = converters.to_unicode(self.right(progress, data, width)) width -= progress.custom_len(left) + progress.custom_len(right) if progress.max_value is not base.UnknownLength \ - and progress.max_value > 0: + and progress.max_value > 0: percent = progress.value / progress.max_value else: percent = 0 @@ -987,7 +1111,13 @@ def __init__(self, format, **kwargs): FormatLabel.__init__(self, format, **kwargs) Bar.__init__(self, **kwargs) - def __call__(self, progress, data, width, format=None): + def __call__( # type: ignore + self, + progress: ProgressBar, + data: Data, + width: int = 0, + format: FormatString = None, + ): center = FormatLabel.__call__(self, progress, data, format=format) bar = Bar.__call__(self, progress, data, width) @@ -1007,12 +1137,25 @@ def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs): Percentage.__init__(self, format, na=na, **kwargs) FormatLabelBar.__init__(self, format, **kwargs) + def __call__( # type: ignore + self, + progress: ProgressBar, + data: Data, + width: int = 0, + format: FormatString = None, + ): + return super().__call__(progress, data, width, format=format) + + + class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): '''Displays a custom variable.''' - def __init__(self, name, format='{name}: {formatted_value}', - width=6, precision=3, **kwargs): + def __init__( + self, name, format='{name}: {formatted_value}', + width=6, precision=3, **kwargs, + ): '''Creates a Variable associated with the given name.''' self.format = format self.width = width @@ -1020,7 +1163,12 @@ def __init__(self, name, format='{name}: {formatted_value}', VariableMixin.__init__(self, name=name) WidgetBase.__init__(self, **kwargs) - def __call__(self, progress, data): + def __call__( + self, + progress: ProgressBar, + data: Data, + format: types.Optional[str] = None + ): value = data['variables'][self.name] context = data.copy() context['value'] = value @@ -1037,7 +1185,8 @@ def __call__(self, progress, data): except (TypeError, ValueError): if value: context['formatted_value'] = '{value:{width}}'.format( - **context) + **context + ) else: context['formatted_value'] = '-' * self.width @@ -1053,17 +1202,24 @@ class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): '''Widget which displays the current (date)time with seconds resolution.''' INTERVAL = datetime.timedelta(seconds=1) - def __init__(self, format='Current Time: %(current_time)s', - microseconds=False, **kwargs): + def __init__( + self, format='Current Time: %(current_time)s', + microseconds=False, **kwargs, + ): self.microseconds = microseconds FormatWidgetMixin.__init__(self, format=format, **kwargs) TimeSensitiveWidgetBase.__init__(self, **kwargs) - def __call__(self, progress, data): + def __call__( + self, + progress: ProgressBar, + data: Data, + format: types.Optional[str] = None, + ): data['current_time'] = self.current_time() data['current_datetime'] = self.current_datetime() - return FormatWidgetMixin.__call__(self, progress, data) + return FormatWidgetMixin.__call__(self, progress, data, format=format) def current_datetime(self): now = datetime.datetime.now() From 7dfd7cc2d90575c88f4fcab2e675f492d79cf33e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Sat, 29 Oct 2022 14:09:30 +0200 Subject: [PATCH 46/66] fixed type issues --- progressbar/utils.py | 170 ++++++++++++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 44 deletions(-) diff --git a/progressbar/utils.py b/progressbar/utils.py index 6a322db4..65b9747d 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -7,13 +7,13 @@ import os import re import sys +from types import TracebackType +from typing import Iterable, Iterator, Type from python_utils import types from python_utils.converters import scale_1024 from python_utils.terminal import get_terminal_size -from python_utils.time import epoch -from python_utils.time import format_time -from python_utils.time import timedelta_to_seconds +from python_utils.time import epoch, format_time, timedelta_to_seconds if types.TYPE_CHECKING: from .bar import ProgressBar @@ -39,7 +39,7 @@ def is_ansi_terminal(fd: types.IO, is_terminal: bool | None = None) \ - -> bool: # pragma: no cover + -> bool: # pragma: no cover if is_terminal is None: # Jupyter Notebooks define this variable and support progress bars if 'JPY_PARENT_PID' in os.environ: @@ -68,13 +68,13 @@ def is_ansi_terminal(fd: types.IO, is_terminal: bool | None = None) \ except Exception: is_terminal = False - return is_terminal + return bool(is_terminal) def is_terminal(fd: types.IO, is_terminal: bool | None = None) -> bool: if is_terminal is None: # Full ansi support encompasses what we expect from a terminal - is_terminal = is_ansi_terminal(True) or None + is_terminal = is_ansi_terminal(fd) or None if is_terminal is None: # Allow a environment variable override @@ -88,11 +88,13 @@ def is_terminal(fd: types.IO, is_terminal: bool | None = None) -> bool: except Exception: is_terminal = False - return is_terminal + return bool(is_terminal) -def deltas_to_seconds(*deltas, - **kwargs) -> int | float | None: # default=ValueError): +def deltas_to_seconds( + *deltas, + **kwargs +) -> int | float | None: # default=ValueError): ''' Convert timedeltas and seconds as int to seconds as float while coalescing @@ -148,15 +150,9 @@ def no_color(value: types.StringTypes) -> types.StringTypes: 'abc' ''' if isinstance(value, bytes): - pattern = '\\\u001b\\[.*?[@-~]' - pattern = pattern.encode() - replace = b'' - assert isinstance(pattern, bytes) + return re.sub('\\\u001b\\[.*?[@-~]'.encode(), b'', value) else: - pattern = u'\x1b\\[.*?[@-~]' - replace = '' - - return re.sub(pattern, replace, value) + return re.sub(u'\x1b\\[.*?[@-~]', '', value) def len_color(value: types.StringTypes) -> int: @@ -190,30 +186,37 @@ def env_flag(name: str, default: bool | None = None) -> bool | None: class WrappingIO: - - def __init__(self, target: types.IO, capturing: bool = False, - listeners: types.Set[ProgressBar] = None) -> None: + buffer: io.StringIO + target: types.IO + capturing: bool + listeners: set + needs_clear: bool = False + + def __init__( + self, target: types.IO, capturing: bool = False, + listeners: types.Set[ProgressBar] = None + ) -> None: self.buffer = io.StringIO() self.target = target self.capturing = capturing self.listeners = listeners or set() self.needs_clear = False - def __getattr__(self, name): # pragma: no cover - return getattr(self.target, name) - - def write(self, value: str) -> None: + def write(self, value: str) -> int: + ret = 0 if self.capturing: - self.buffer.write(value) + ret += self.buffer.write(value) if '\n' in value: # pragma: no branch self.needs_clear = True for listener in self.listeners: # pragma: no branch listener.update() else: - self.target.write(value) + ret += self.target.write(value) if '\n' in value: # pragma: no branch self.flush_target() + return ret + def flush(self) -> None: self.buffer.flush() @@ -233,9 +236,80 @@ def flush_target(self) -> None: # pragma: no cover if not self.target.closed and getattr(self.target, 'flush'): self.target.flush() + def __enter__(self) -> WrappingIO: + return self + + def fileno(self) -> int: + return self.target.fileno() + + def isatty(self) -> bool: + return self.target.isatty() + + def read(self, n: int = -1) -> str: + return self.target.read(n) + + def readable(self) -> bool: + return self.target.readable() + + def readline(self, limit: int = -1) -> str: + return self.target.readline(limit) + + def readlines(self, hint: int = -1) -> list[str]: + return self.target.readlines(hint) + + def seek(self, offset: int, whence: int = os.SEEK_SET) -> int: + return self.target.seek(offset, whence) + + def seekable(self) -> bool: + return self.target.seekable() + + def tell(self) -> int: + return self.target.tell() + + def truncate(self, size: types.Optional[int] = None) -> int: + return self.target.truncate(size) + + def writable(self) -> bool: + return self.target.writable() + + def writelines(self, lines: Iterable[str]) -> None: + return self.target.writelines(lines) + + def close(self) -> None: + self.flush() + self.target.close() + + def __next__(self) -> str: + return self.target.__next__() + + def __iter__(self) -> Iterator[str]: + return self.target.__iter__() + + def __exit__( + self, + __t: Type[BaseException] | None, + __value: BaseException | None, + __traceback: TracebackType | None + ) -> None: + self.close() + class StreamWrapper: '''Wrap stdout and stderr globally''' + stdout: types.Union[types.TextIO, WrappingIO] + stderr: types.Union[types.TextIO, WrappingIO] + original_excepthook: types.Callable[ + [ + types.Optional[ + types.Type[BaseException]], + types.Optional[BaseException], + types.Optional[TracebackType], + ], None] + wrapped_stdout: int = 0 + wrapped_stderr: int = 0 + wrapped_excepthook: int = 0 + capturing: int = 0 + listeners: set def __init__(self): self.stdout = self.original_stdout = sys.stdout @@ -291,8 +365,10 @@ def wrap_stdout(self) -> types.IO: self.wrap_excepthook() if not self.wrapped_stdout: - self.stdout = sys.stdout = WrappingIO(self.original_stdout, - listeners=self.listeners) + self.stdout = sys.stdout = WrappingIO( # type: ignore + self.original_stdout, + listeners=self.listeners + ) self.wrapped_stdout += 1 return sys.stdout @@ -301,8 +377,10 @@ def wrap_stderr(self) -> types.IO: self.wrap_excepthook() if not self.wrapped_stderr: - self.stderr = sys.stderr = WrappingIO(self.original_stderr, - listeners=self.listeners) + self.stderr = sys.stderr = WrappingIO( # type: ignore + self.original_stderr, + listeners=self.listeners + ) self.wrapped_stderr += 1 return sys.stderr @@ -346,22 +424,26 @@ def needs_clear(self) -> bool: # pragma: no cover def flush(self) -> None: if self.wrapped_stdout: # pragma: no branch - try: - self.stdout._flush() - except (io.UnsupportedOperation, - AttributeError): # pragma: no cover - self.wrapped_stdout = False - logger.warn('Disabling stdout redirection, %r is not seekable', - sys.stdout) + if isinstance(self.stdout, WrappingIO): # pragma: no branch + try: + self.stdout._flush() + except io.UnsupportedOperation: # pragma: no cover + self.wrapped_stdout = False + logger.warning( + 'Disabling stdout redirection, %r is not seekable', + sys.stdout + ) if self.wrapped_stderr: # pragma: no branch - try: - self.stderr._flush() - except (io.UnsupportedOperation, - AttributeError): # pragma: no cover - self.wrapped_stderr = False - logger.warn('Disabling stderr redirection, %r is not seekable', - sys.stderr) + if isinstance(self.stderr, WrappingIO): # pragma: no branch + try: + self.stderr._flush() + except io.UnsupportedOperation: # pragma: no cover + self.wrapped_stderr = False + logger.warning( + 'Disabling stderr redirection, %r is not seekable', + sys.stderr + ) def excepthook(self, exc_type, exc_value, exc_traceback): self.original_excepthook(exc_type, exc_value, exc_traceback) From 50dffb6969ef54672e2ad7a8912bc090b22597a1 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Sun, 30 Oct 2022 13:14:35 +0200 Subject: [PATCH 47/66] fixed more type issues --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f72abd08..850df2de 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ long_description=readme, include_package_data=True, install_requires=[ - 'python-utils>=3.0.0', + 'python-utils>=3.4.5', ], setup_requires=['setuptools'], zip_safe=False, @@ -55,6 +55,7 @@ 'pytest-mypy', 'freezegun>=0.3.11', 'sphinx>=1.8.5', + 'dill>=0.3.6', ], }, python_requires='>=3.7.0', From 71a1afe49454b4ce4d73df5b7e3bd8aaae937740 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 31 Oct 2022 11:46:12 +0100 Subject: [PATCH 48/66] added backwards compatibility tests --- tests/test_backwards_compatibility.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/test_backwards_compatibility.py diff --git a/tests/test_backwards_compatibility.py b/tests/test_backwards_compatibility.py new file mode 100644 index 00000000..027c3f9e --- /dev/null +++ b/tests/test_backwards_compatibility.py @@ -0,0 +1,16 @@ +import time +import progressbar + + +def test_progressbar_1_widgets(): + widgets = [ + progressbar.AdaptiveETA(format="Time left: %s"), + progressbar.Timer(format="Time passed: %s"), + progressbar.Bar() + ] + + bar = progressbar.ProgressBar(widgets=widgets, max_value=100).start() + + for i in range(1, 101): + bar.update(i) + time.sleep(0.1) From 1df0706d7c6218ea4b94605f14b36908ee75e68b Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 31 Oct 2022 11:48:27 +0100 Subject: [PATCH 49/66] fixed issue with random prints when dill pickling progressbar. Fixes: #263 --- tests/test_dill_pickle.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_dill_pickle.py diff --git a/tests/test_dill_pickle.py b/tests/test_dill_pickle.py new file mode 100644 index 00000000..ce1ee43d --- /dev/null +++ b/tests/test_dill_pickle.py @@ -0,0 +1,17 @@ +import pickle + +import dill + +import progressbar + + +def test_dill(): + bar = progressbar.ProgressBar() + assert bar._started == False + assert bar._finished == False + + assert not dill.pickles(bar) + + assert bar._started == False + # Should be false because it never should have started/initialized + assert bar._finished == False From f9fc657d2252c5510faad73a5f498fae5023bf68 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 31 Oct 2022 11:48:38 +0100 Subject: [PATCH 50/66] more type tests --- tests/test_wrappingio.py | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_wrappingio.py diff --git a/tests/test_wrappingio.py b/tests/test_wrappingio.py new file mode 100644 index 00000000..6a9f105a --- /dev/null +++ b/tests/test_wrappingio.py @@ -0,0 +1,63 @@ +import io +import os +import sys + +import pytest + +from progressbar import utils + + +def test_wrappingio(): + # Test the wrapping of our version of sys.stdout` ` q + fd = utils.WrappingIO(sys.stdout) + assert fd.fileno() + assert not fd.isatty() + + assert not fd.read() + assert not fd.readline() + assert not fd.readlines() + assert fd.readable() + + assert not fd.seek(0) + assert fd.seekable() + assert not fd.tell() + + assert not fd.truncate() + assert fd.writable() + assert fd.write('test') + assert not fd.writelines(['test']) + + with pytest.raises(StopIteration): + next(fd) + with pytest.raises(StopIteration): + next(iter(fd)) + + +def test_wrapping_stringio(): + # Test the wrapping of our version of sys.stdout` ` q + string_io = io.StringIO() + fd = utils.WrappingIO(string_io) + with fd: + with pytest.raises(io.UnsupportedOperation): + fd.fileno() + + assert not fd.isatty() + + assert not fd.read() + assert not fd.readline() + assert not fd.readlines() + assert fd.readable() + + assert not fd.seek(0) + assert fd.seekable() + assert not fd.tell() + + assert not fd.truncate() + assert fd.writable() + assert fd.write('test') + assert not fd.writelines(['test']) + + with pytest.raises(StopIteration): + next(fd) + with pytest.raises(StopIteration): + next(iter(fd)) From 0e21c5b499b8947ad7f40b2b9b0faaf42d280e30 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 31 Oct 2022 12:51:26 +0100 Subject: [PATCH 51/66] small formatting changes --- progressbar/__about__.py | 6 +- progressbar/bar.py | 122 ++++++++++++++++----------- progressbar/base.py | 1 + progressbar/shortcuts.py | 20 ++++- progressbar/utils.py | 34 ++++---- progressbar/widgets.py | 176 +++++++++++++++++++++++++-------------- 6 files changed, 224 insertions(+), 135 deletions(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index bc898708..64d0af06 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -14,10 +14,12 @@ __title__ = 'Python Progressbar' __package_name__ = 'progressbar2' __author__ = 'Rick van Hattem (Wolph)' -__description__ = ' '.join(''' +__description__ = ' '.join( + ''' A Python Progressbar library to provide visual (yet text based) progress to long running operations. -'''.strip().split()) +'''.strip().split() +) __email__ = 'wolph@wol.ph' __version__ = '4.2.0' __license__ = 'BSD' diff --git a/progressbar/bar.py b/progressbar/bar.py index 4cb79f5d..2ec687ee 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -50,7 +50,7 @@ def __del__(self): # Never raise during cleanup. We're too late now logging.debug( 'Exception raised during ProgressBar cleanup', - exc_info=True + exc_info=True, ) def __getstate__(self): @@ -68,10 +68,12 @@ class DefaultFdMixin(ProgressBarMixinBase): enable_colors: bool = False def __init__( - self, fd: types.IO = sys.stderr, + self, + fd: types.IO = sys.stderr, is_terminal: bool | None = None, line_breaks: bool | None = None, - enable_colors: bool | None = None, **kwargs + enable_colors: bool | None = None, + **kwargs, ): if fd is sys.stdout: fd = utils.streams.original_stdout @@ -91,8 +93,7 @@ def __init__( # iteractive terminals) or write line breaks (suitable for log files) if line_breaks is None: line_breaks = utils.env_flag( - 'PROGRESSBAR_LINE_BREAKS', - not self.is_terminal + 'PROGRESSBAR_LINE_BREAKS', not self.is_terminal ) self.line_breaks = bool(line_breaks) @@ -100,8 +101,7 @@ def __init__( # terminals), or should be stripped off (suitable for log files) if enable_colors is None: enable_colors = utils.env_flag( - 'PROGRESSBAR_ENABLE_COLORS', - self.is_ansi_terminal + 'PROGRESSBAR_ENABLE_COLORS', self.is_ansi_terminal ) self.enable_colors = bool(enable_colors) @@ -149,7 +149,6 @@ def _format_line(self): class ResizableMixin(ProgressBarMixinBase): - def __init__(self, term_width: int | None = None, **kwargs): ProgressBarMixinBase.__init__(self, **kwargs) @@ -160,6 +159,7 @@ def __init__(self, term_width: int | None = None, **kwargs): try: self._handle_resize() import signal + self._prev_handle = signal.getsignal(signal.SIGWINCH) signal.signal(signal.SIGWINCH, self._handle_resize) self.signal_set = True @@ -177,6 +177,7 @@ def finish(self): # pragma: no cover if self.signal_set: try: import signal + signal.signal(signal.SIGWINCH, self._prev_handle) except Exception: # pragma no cover pass @@ -191,8 +192,10 @@ class StdRedirectMixin(DefaultFdMixin): _stderr: types.IO def __init__( - self, redirect_stderr: bool = False, - redirect_stdout: bool = False, **kwargs + self, + redirect_stderr: bool = False, + redirect_stdout: bool = False, + **kwargs, ): DefaultFdMixin.__init__(self, **kwargs) self.redirect_stderr = redirect_stderr @@ -327,11 +330,21 @@ class ProgressBar( _MINIMUM_UPDATE_INTERVAL = 0.050 def __init__( - self, min_value=0, max_value=None, widgets=None, - left_justify=True, initial_value=0, poll_interval=None, - widget_kwargs=None, custom_len=utils.len_color, - max_error=True, prefix=None, suffix=None, variables=None, - min_poll_interval=None, **kwargs + self, + min_value=0, + max_value=None, + widgets=None, + left_justify=True, + initial_value=0, + poll_interval=None, + widget_kwargs=None, + custom_len=utils.len_color, + max_error=True, + prefix=None, + suffix=None, + variables=None, + min_poll_interval=None, + **kwargs, ): ''' Initializes a progress bar with sane defaults @@ -342,22 +355,23 @@ def __init__( if not max_value and kwargs.get('maxval') is not None: warnings.warn( 'The usage of `maxval` is deprecated, please use ' - '`max_value` instead', DeprecationWarning + '`max_value` instead', + DeprecationWarning, ) max_value = kwargs.get('maxval') if not poll_interval and kwargs.get('poll'): warnings.warn( 'The usage of `poll` is deprecated, please use ' - '`poll_interval` instead', DeprecationWarning + '`poll_interval` instead', + DeprecationWarning, ) poll_interval = kwargs.get('poll') if max_value: if min_value > max_value: raise ValueError( - 'Max value needs to be bigger than the min ' - 'value' + 'Max value needs to be bigger than the min ' 'value' ) self.min_value = min_value self.max_value = max_value @@ -394,8 +408,7 @@ def __init__( # (downloading a 1GiB file for example) this adds up. poll_interval = utils.deltas_to_seconds(poll_interval, default=None) min_poll_interval = utils.deltas_to_seconds( - min_poll_interval, - default=None + min_poll_interval, default=None ) self._MINIMUM_UPDATE_INTERVAL = utils.deltas_to_seconds( self._MINIMUM_UPDATE_INTERVAL @@ -412,7 +425,7 @@ def __init__( # A dictionary of names that can be used by Variable and FormatWidget self.variables = utils.AttributeDict(variables or {}) - for widget in (self.widgets or []): + for widget in self.widgets or []: if isinstance(widget, widgets_module.VariableMixin): if widget.name not in self.variables: self.variables[widget.name] = None @@ -546,7 +559,7 @@ def data(self): total_seconds_elapsed=total_seconds_elapsed, # The seconds since the bar started modulo 60 seconds_elapsed=(elapsed.seconds % 60) - + (elapsed.microseconds / 1000000.), + + (elapsed.microseconds / 1000000.0), # The minutes since the bar started modulo 60 minutes_elapsed=(elapsed.seconds / 60) % 60, # The hours since the bar started modulo 24 @@ -568,20 +581,27 @@ def default_widgets(self): if self.max_value: return [ widgets.Percentage(**self.widget_kwargs), - ' ', widgets.SimpleProgress( + ' ', + widgets.SimpleProgress( format='(%s)' % widgets.SimpleProgress.DEFAULT_FORMAT, - **self.widget_kwargs + **self.widget_kwargs, ), - ' ', widgets.Bar(**self.widget_kwargs), - ' ', widgets.Timer(**self.widget_kwargs), - ' ', widgets.AdaptiveETA(**self.widget_kwargs), + ' ', + widgets.Bar(**self.widget_kwargs), + ' ', + widgets.Timer(**self.widget_kwargs), + ' ', + widgets.AdaptiveETA(**self.widget_kwargs), ] else: return [ widgets.AnimatedMarker(**self.widget_kwargs), - ' ', widgets.BouncingBar(**self.widget_kwargs), - ' ', widgets.Counter(**self.widget_kwargs), - ' ', widgets.Timer(**self.widget_kwargs), + ' ', + widgets.BouncingBar(**self.widget_kwargs), + ' ', + widgets.Counter(**self.widget_kwargs), + ' ', + widgets.Timer(**self.widget_kwargs), ] def __call__(self, iterable, max_value=None): @@ -640,8 +660,9 @@ def _format_widgets(self): data = self.data() for index, widget in enumerate(self.widgets): - if isinstance(widget, widgets.WidgetBase) \ - and not widget.check_size(self): + if isinstance( + widget, widgets.WidgetBase + ) and not widget.check_size(self): continue elif isinstance(widget, widgets.AutoWidthWidgetBase): result.append(widget) @@ -656,7 +677,7 @@ def _format_widgets(self): count = len(expanding) while expanding: - portion = max(int(math.ceil(width * 1. / count)), 0) + portion = max(int(math.ceil(width * 1.0 / count)), 0) index = expanding.pop() widget = result[index] count -= 1 @@ -725,8 +746,8 @@ def update(self, value=None, force=False, **kwargs): for key in kwargs: if key not in self.variables: raise TypeError( - 'update() got an unexpected keyword ' + - 'argument {0!r}'.format(key) + 'update() got an unexpected keyword ' + + 'argument {0!r}'.format(key) ) elif self.variables[key] != kwargs[key]: self.variables[key] = kwargs[key] @@ -782,9 +803,7 @@ def start(self, max_value=None, init=True): if self.prefix: self.widgets.insert( - 0, widgets.FormatLabel( - self.prefix, new_style=True - ) + 0, widgets.FormatLabel(self.prefix, new_style=True) ) # Unset the prefix variable after applying so an extra start() # won't keep copying it @@ -792,9 +811,7 @@ def start(self, max_value=None, init=True): if self.suffix: self.widgets.append( - widgets.FormatLabel( - self.suffix, new_style=True - ) + widgets.FormatLabel(self.suffix, new_style=True) ) # Unset the suffix variable after applying so an extra start() # won't keep copying it @@ -856,7 +873,8 @@ def currval(self): ''' warnings.warn( 'The usage of `currval` is deprecated, please use ' - '`value` instead', DeprecationWarning + '`value` instead', + DeprecationWarning, ) return self.value @@ -871,16 +889,22 @@ def default_widgets(self): if self.max_value: return [ widgets.Percentage(), - ' of ', widgets.DataSize('max_value'), - ' ', widgets.Bar(), - ' ', widgets.Timer(), - ' ', widgets.AdaptiveETA(), + ' of ', + widgets.DataSize('max_value'), + ' ', + widgets.Bar(), + ' ', + widgets.Timer(), + ' ', + widgets.AdaptiveETA(), ] else: return [ widgets.AnimatedMarker(), - ' ', widgets.DataSize(), - ' ', widgets.Timer(), + ' ', + widgets.DataSize(), + ' ', + widgets.Timer(), ] diff --git a/progressbar/base.py b/progressbar/base.py index df278e59..72f93846 100644 --- a/progressbar/base.py +++ b/progressbar/base.py @@ -1,5 +1,6 @@ # -*- mode: python; coding: utf-8 -*- + class FalseMeta(type): def __bool__(self): # pragma: no cover return False diff --git a/progressbar/shortcuts.py b/progressbar/shortcuts.py index f882a5a2..9e1502dd 100644 --- a/progressbar/shortcuts.py +++ b/progressbar/shortcuts.py @@ -1,11 +1,23 @@ from . import bar -def progressbar(iterator, min_value=0, max_value=None, - widgets=None, prefix=None, suffix=None, **kwargs): +def progressbar( + iterator, + min_value=0, + max_value=None, + widgets=None, + prefix=None, + suffix=None, + **kwargs +): progressbar = bar.ProgressBar( - min_value=min_value, max_value=max_value, - widgets=widgets, prefix=prefix, suffix=suffix, **kwargs) + min_value=min_value, + max_value=max_value, + widgets=widgets, + prefix=prefix, + suffix=suffix, + **kwargs + ) for result in progressbar(iterator): yield result diff --git a/progressbar/utils.py b/progressbar/utils.py index 65b9747d..721f4e6d 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -38,8 +38,9 @@ ANSI_TERM_RE = re.compile('^({})'.format('|'.join(ANSI_TERMS)), re.IGNORECASE) -def is_ansi_terminal(fd: types.IO, is_terminal: bool | None = None) \ - -> bool: # pragma: no cover +def is_ansi_terminal( + fd: types.IO, is_terminal: bool | None = None +) -> bool: # pragma: no cover if is_terminal is None: # Jupyter Notebooks define this variable and support progress bars if 'JPY_PARENT_PID' in os.environ: @@ -92,8 +93,7 @@ def is_terminal(fd: types.IO, is_terminal: bool | None = None) -> bool: def deltas_to_seconds( - *deltas, - **kwargs + *deltas, **kwargs ) -> int | float | None: # default=ValueError): ''' Convert timedeltas and seconds as int to seconds as float while coalescing @@ -193,8 +193,10 @@ class WrappingIO: needs_clear: bool = False def __init__( - self, target: types.IO, capturing: bool = False, - listeners: types.Set[ProgressBar] = None + self, + target: types.IO, + capturing: bool = False, + listeners: types.Set[ProgressBar] = None, ) -> None: self.buffer = io.StringIO() self.target = target @@ -289,22 +291,24 @@ def __exit__( self, __t: Type[BaseException] | None, __value: BaseException | None, - __traceback: TracebackType | None + __traceback: TracebackType | None, ) -> None: self.close() class StreamWrapper: '''Wrap stdout and stderr globally''' + stdout: types.Union[types.TextIO, WrappingIO] stderr: types.Union[types.TextIO, WrappingIO] original_excepthook: types.Callable[ [ - types.Optional[ - types.Type[BaseException]], + types.Optional[types.Type[BaseException]], types.Optional[BaseException], types.Optional[TracebackType], - ], None] + ], + None, + ] wrapped_stdout: int = 0 wrapped_stderr: int = 0 wrapped_excepthook: int = 0 @@ -366,8 +370,7 @@ def wrap_stdout(self) -> types.IO: if not self.wrapped_stdout: self.stdout = sys.stdout = WrappingIO( # type: ignore - self.original_stdout, - listeners=self.listeners + self.original_stdout, listeners=self.listeners ) self.wrapped_stdout += 1 @@ -378,8 +381,7 @@ def wrap_stderr(self) -> types.IO: if not self.wrapped_stderr: self.stderr = sys.stderr = WrappingIO( # type: ignore - self.original_stderr, - listeners=self.listeners + self.original_stderr, listeners=self.listeners ) self.wrapped_stderr += 1 @@ -431,7 +433,7 @@ def flush(self) -> None: self.wrapped_stdout = False logger.warning( 'Disabling stdout redirection, %r is not seekable', - sys.stdout + sys.stdout, ) if self.wrapped_stderr: # pragma: no branch @@ -442,7 +444,7 @@ def flush(self) -> None: self.wrapped_stderr = False logger.warning( 'Disabling stderr redirection, %r is not seekable', - sys.stderr + sys.stderr, ) def excepthook(self, exc_type, exc_value, exc_traceback): diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 30ca01a5..ae8e2723 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -24,6 +24,7 @@ def string_or_lambda(input_): if isinstance(input_, str): + def render_input(progress, data, width): return input_ % data @@ -78,17 +79,20 @@ def wrap(*args, **kwargs): def create_marker(marker, wrap=None): def _marker(progress, data, width): - if progress.max_value is not base.UnknownLength \ - and progress.max_value > 0: + if ( + progress.max_value is not base.UnknownLength + and progress.max_value > 0 + ): length = int(progress.value / progress.max_value * width) - return (marker * length) + return marker * length else: return marker if isinstance(marker, str): marker = converters.to_unicode(marker) - assert utils.len_color(marker) == 1, \ - 'Markers are required to be 1 char' + assert ( + utils.len_color(marker) == 1 + ), 'Markers are required to be 1 char' return wrapper(_marker, wrap) else: return wrapper(marker, wrap) @@ -118,7 +122,7 @@ def get_format( self, progress: ProgressBar, data: Data, - format: types.Optional[str] = None + format: types.Optional[str] = None, ) -> str: return format or self.format @@ -206,6 +210,7 @@ class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta): progressbar can be reused. Some widgets such as the FormatCustomText require the shared state so this needs to be optional ''' + copy = True @abc.abstractmethod @@ -244,6 +249,7 @@ class TimeSensitiveWidgetBase(WidgetBase, metaclass=abc.ABCMeta): Some widgets like timers would become out of date unless updated at least every `INTERVAL` ''' + INTERVAL = datetime.timedelta(milliseconds=100) @@ -338,7 +344,9 @@ class SamplesMixin(TimeSensitiveWidgetBase, metaclass=abc.ABCMeta): ''' def __init__( - self, samples=datetime.timedelta(seconds=2), key_prefix=None, + self, + samples=datetime.timedelta(seconds=2), + key_prefix=None, **kwargs, ): self.samples = samples @@ -368,9 +376,11 @@ def __call__(self, progress: ProgressBar, data: Data, delta: bool = False): if isinstance(self.samples, datetime.timedelta): minimum_time = progress.last_update_time - self.samples minimum_value = sample_values[-1] - while (sample_times[2:] and - minimum_time > sample_times[1] and - minimum_value > sample_values[1]): + while ( + sample_times[2:] + and minimum_time > sample_times[1] + and minimum_value > sample_values[1] + ): sample_times.pop(0) sample_values.pop(0) else: @@ -412,7 +422,9 @@ def __init__( self.format_zero = format_zero self.format_NA = format_NA - def _calculate_eta(self, progress: ProgressBar, data: Data, value, elapsed): + def _calculate_eta( + self, progress: ProgressBar, data: Data, value, elapsed + ): '''Updates the widget to show the ETA or total time when finished.''' if elapsed: # The max() prevents zero division errors @@ -471,7 +483,9 @@ def __call__( class AbsoluteETA(ETA): '''Widget which attempts to estimate the absolute time of arrival.''' - def _calculate_eta(self, progress: ProgressBar, data: Data, value, elapsed): + def _calculate_eta( + self, progress: ProgressBar, data: Data, value, elapsed + ): eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed) now = datetime.datetime.now() try: @@ -487,8 +501,11 @@ def __init__( **kwargs, ): ETA.__init__( - self, format_not_started=format_not_started, - format_finished=format_finished, format=format, **kwargs, + self, + format_not_started=format_not_started, + format_finished=format_finished, + format=format, + **kwargs, ) @@ -511,8 +528,7 @@ def __call__( elapsed=None, ): elapsed, value = SamplesMixin.__call__( - self, progress, data, - delta=True + self, progress, data, delta=True ) if not elapsed: value = None @@ -530,8 +546,10 @@ class DataSize(FormatWidgetMixin, WidgetBase): ''' def __init__( - self, variable='value', - format='%(scaled)5.1f %(prefix)s%(unit)s', unit='B', + self, + variable='value', + format='%(scaled)5.1f %(prefix)s%(unit)s', + unit='B', prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), **kwargs, ): @@ -566,8 +584,10 @@ class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): ''' def __init__( - self, format='%(scaled)5.1f %(prefix)s%(unit)-s/s', - inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', unit='B', + self, + format='%(scaled)5.1f %(prefix)s%(unit)-s/s', + inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', + unit='B', prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), **kwargs, ): @@ -586,19 +606,22 @@ def __call__( progress: ProgressBar, data, value=None, - total_seconds_elapsed=None + total_seconds_elapsed=None, ): '''Updates the widget with the current SI prefixed speed.''' if value is None: value = data['value'] elapsed = utils.deltas_to_seconds( - total_seconds_elapsed, - data['total_seconds_elapsed'] + total_seconds_elapsed, data['total_seconds_elapsed'] ) - if value is not None and elapsed is not None \ - and elapsed > 2e-6 and value > 2e-6: # =~ 0 + if ( + value is not None + and elapsed is not None + and elapsed > 2e-6 + and value > 2e-6 + ): # =~ 0 scaled, power = self._speed(value, elapsed) else: scaled = power = 0 @@ -610,8 +633,7 @@ def __call__( data['scaled'] = scaled data['prefix'] = self.prefixes[0] return FormatWidgetMixin.__call__( - self, progress, data, - self.inverse_format + self, progress, data, self.inverse_format ) else: data['scaled'] = scaled @@ -620,8 +642,7 @@ def __call__( class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): - '''WidgetBase for showing the transfer speed, based on the last X samples - ''' + '''WidgetBase for showing the transfer speed, based on the last X samples''' def __init__(self, **kwargs): FileTransferSpeed.__init__(self, **kwargs) @@ -632,11 +653,10 @@ def __call__( progress: ProgressBar, data, value=None, - total_seconds_elapsed=None + total_seconds_elapsed=None, ): elapsed, value = SamplesMixin.__call__( - self, progress, data, - delta=True + self, progress, data, delta=True ) return FileTransferSpeed.__call__(self, progress, data, value, elapsed) @@ -647,8 +667,13 @@ class AnimatedMarker(TimeSensitiveWidgetBase): ''' def __init__( - self, markers='|/-\\', default=None, fill='', - marker_wrap=None, fill_wrap=None, **kwargs, + self, + markers='|/-\\', + default=None, + fill='', + marker_wrap=None, + fill_wrap=None, + **kwargs, ): self.markers = markers self.marker_wrap = create_wrapper(marker_wrap) @@ -671,9 +696,7 @@ def __call__(self, progress: ProgressBar, data: Data, width=None): if self.fill: # Cut the last character so we can replace it with our marker fill = self.fill( - progress, data, width - progress.custom_len( - marker - ) + progress, data, width - progress.custom_len(marker) ) else: fill = '' @@ -745,8 +768,7 @@ def __call__(self, progress: ProgressBar, data: Data, format=None): data['value_s'] = 0 formatted = FormatWidgetMixin.__call__( - self, progress, data, - format=format + self, progress, data, format=format ) # Guess the maximum width from the min and max value @@ -780,8 +802,14 @@ class Bar(AutoWidthWidgetBase): '''A progress bar which stretches to fill the line.''' def __init__( - self, marker='#', left='|', right='|', fill=' ', - fill_left=True, marker_wrap=None, **kwargs, + self, + marker='#', + left='|', + right='|', + fill=' ', + fill_left=True, + marker_wrap=None, + **kwargs, ): '''Creates a customizable progress bar. @@ -831,8 +859,13 @@ class ReverseBar(Bar): '''A bar which has a marker that goes from right to left''' def __init__( - self, marker='#', left='|', right='|', fill=' ', - fill_left=False, **kwargs, + self, + marker='#', + left='|', + right='|', + fill=' ', + fill_left=False, + **kwargs, ): '''Creates a customizable progress bar. @@ -843,8 +876,13 @@ def __init__( fill_left - whether to fill from the left or the right ''' Bar.__init__( - self, marker=marker, left=left, right=right, fill=fill, - fill_left=fill_left, **kwargs, + self, + marker=marker, + left=left, + right=right, + fill=fill, + fill_left=fill_left, + **kwargs, ) @@ -916,7 +954,7 @@ def __call__( class VariableMixin: - '''Mixin to display a custom user variable ''' + '''Mixin to display a custom user variable''' def __init__(self, name, **kwargs): if not isinstance(name, str): @@ -944,10 +982,7 @@ class MultiRangeBar(Bar, VariableMixin): def __init__(self, name, markers, **kwargs): VariableMixin.__init__(self, name) Bar.__init__(self, **kwargs) - self.markers = [ - string_or_lambda(marker) - for marker in markers - ] + self.markers = [string_or_lambda(marker) for marker in markers] def get_values(self, progress: ProgressBar, data: Data): return data['variables'][self.name] or [] @@ -997,8 +1032,10 @@ def __init__( **kwargs, ): MultiRangeBar.__init__( - self, name=name, - markers=list(reversed(markers)), **kwargs, + self, + name=name, + markers=list(reversed(markers)), + **kwargs, ) def get_values(self, progress: ProgressBar, data: Data): @@ -1011,8 +1048,8 @@ def get_values(self, progress: ProgressBar, data: Data): if not 0 <= value <= 1: raise ValueError( - 'Range value needs to be in the range [0..1], got %s' % - value + 'Range value needs to be in the range [0..1], got %s' + % value ) range_ = value * (len(ranges) - 1) @@ -1054,7 +1091,10 @@ class GranularBar(AutoWidthWidgetBase): ''' def __init__( - self, markers=GranularMarkers.smooth, left='|', right='|', + self, + markers=GranularMarkers.smooth, + left='|', + right='|', **kwargs, ): '''Creates a customizable progress bar. @@ -1081,8 +1121,10 @@ def __call__( right = converters.to_unicode(self.right(progress, data, width)) width -= progress.custom_len(left) + progress.custom_len(right) - if progress.max_value is not base.UnknownLength \ - and progress.max_value > 0: + if ( + progress.max_value is not base.UnknownLength + and progress.max_value > 0 + ): percent = progress.value / progress.max_value else: percent = 0 @@ -1147,14 +1189,16 @@ def __call__( # type: ignore return super().__call__(progress, data, width, format=format) - - class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): '''Displays a custom variable.''' def __init__( - self, name, format='{name}: {formatted_value}', - width=6, precision=3, **kwargs, + self, + name, + format='{name}: {formatted_value}', + width=6, + precision=3, + **kwargs, ): '''Creates a Variable associated with the given name.''' self.format = format @@ -1167,7 +1211,7 @@ def __call__( self, progress: ProgressBar, data: Data, - format: types.Optional[str] = None + format: types.Optional[str] = None, ): value = data['variables'][self.name] context = data.copy() @@ -1195,16 +1239,20 @@ def __call__( class DynamicMessage(Variable): '''Kept for backwards compatibility, please use `Variable` instead.''' + pass class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): '''Widget which displays the current (date)time with seconds resolution.''' + INTERVAL = datetime.timedelta(seconds=1) def __init__( - self, format='Current Time: %(current_time)s', - microseconds=False, **kwargs, + self, + format='Current Time: %(current_time)s', + microseconds=False, + **kwargs, ): self.microseconds = microseconds FormatWidgetMixin.__init__(self, format=format, **kwargs) From cc2cfb727c3b705a36209a168e51d867150e41d3 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 00:29:53 +0100 Subject: [PATCH 52/66] Fixed tests running from pycharm --- progressbar/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/progressbar/utils.py b/progressbar/utils.py index 721f4e6d..c1400ec6 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -47,7 +47,9 @@ def is_ansi_terminal( is_terminal = True # This works for newer versions of pycharm only. older versions there # is no way to check. - elif os.environ.get('PYCHARM_HOSTED') == '1': + elif os.environ.get('PYCHARM_HOSTED') == '1' and not os.environ.get( + 'PYTEST_CURRENT_TEST' + ): is_terminal = True if is_terminal is None: From 7ff98d7f4fe6ededd3137d933dce0fb85a64b23e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 11:42:00 +0100 Subject: [PATCH 53/66] Added more terminal tests --- tests/test_utils.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3f292bfd..0ff4a7a1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -54,3 +54,30 @@ def test_is_terminal(monkeypatch): # Sanity check assert progressbar.utils.is_terminal(fd) is False + + +def test_is_ansi_terminal(monkeypatch): + fd = io.StringIO() + + monkeypatch.delenv('PROGRESSBAR_IS_TERMINAL', raising=False) + monkeypatch.delenv('JPY_PARENT_PID', raising=False) + + assert progressbar.utils.is_ansi_terminal(fd) is False + assert progressbar.utils.is_ansi_terminal(fd, True) is True + assert progressbar.utils.is_ansi_terminal(fd, False) is False + + monkeypatch.setenv('JPY_PARENT_PID', '123') + assert progressbar.utils.is_ansi_terminal(fd) is True + monkeypatch.delenv('JPY_PARENT_PID') + + # Sanity check + assert progressbar.utils.is_ansi_terminal(fd) is False + + monkeypatch.setenv('PROGRESSBAR_IS_TERMINAL', 'true') + assert progressbar.utils.is_ansi_terminal(fd) is False + monkeypatch.setenv('PROGRESSBAR_IS_TERMINAL', 'false') + assert progressbar.utils.is_ansi_terminal(fd) is False + monkeypatch.delenv('PROGRESSBAR_IS_TERMINAL') + + # Sanity check + assert progressbar.utils.is_ansi_terminal(fd) is False From 36c796f2bf1654353df530717629b2ca0e0b7156 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 11:42:37 +0100 Subject: [PATCH 54/66] Added black, mypy and pyright to tox list --- tox.ini | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index afba92f9..df0a7d69 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, py39, py310, pypy3, flake8, docs +envlist = py37, py38, py39, py310, pypy3, flake8, docs, black, mypy, pyright skip_missing_interpreters = True [testenv] @@ -21,12 +21,23 @@ basepython = python3 deps = flake8 commands = flake8 {toxinidir}/progressbar {toxinidir}/tests {toxinidir}/examples.py +[testenv:mypy] +changedir = +basepython = python3 +deps = mypy +commands = mypy {toxinidir}/progressbar + [testenv:pyright] changedir = basepython = python3 deps = pyright commands = pyright {toxinidir}/progressbar +[testenv:black] +basepython = python3 +deps = black +commands = black --skip-string-normalization --line-length 79 {toxinidir}/progressbar + [testenv:docs] changedir = basepython = python3 From a51deccd4969a021ade8567f8966f96fa874c6d4 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 11:43:17 +0100 Subject: [PATCH 55/66] Added type hints to widgets --- progressbar/widgets.py | 103 +++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index ae8e2723..336dfb79 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import annotations + import abc import datetime import functools @@ -12,7 +13,7 @@ from . import base, utils if types.TYPE_CHECKING: - from .bar import ProgressBar + from .bar import ProgressBarMixinBase MAX_DATE = datetime.date.max MAX_TIME = datetime.time.max @@ -120,7 +121,7 @@ def __init__(self, format: str, new_style: bool = False, **kwargs): def get_format( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, format: types.Optional[str] = None, ) -> str: @@ -128,7 +129,7 @@ def get_format( def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, format: types.Optional[str] = None, ): @@ -148,7 +149,7 @@ def __call__( class WidthWidgetMixin(abc.ABC): '''Mixing to make sure widgets are only visible if the screen is within a specified size range so the progressbar fits on both large and small - screens.. + screens. Variables available: - min_width: Only display the widget if at least `min_width` is left @@ -174,7 +175,7 @@ def __init__(self, min_width=None, max_width=None, **kwargs): self.min_width = min_width self.max_width = max_width - def check_size(self, progress: 'ProgressBar'): + def check_size(self, progress: ProgressBarMixinBase): if self.min_width and self.min_width > progress.term_width: return False elif self.max_width and self.max_width < progress.term_width: @@ -190,7 +191,7 @@ class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta): be updated. The widget's size may change between calls, but the widget may display incorrectly if the size changes drastically and repeatedly. - The boolean INTERVAL informs the ProgressBar that it should be + The INTERVAL timedelta informs the ProgressBar that it should be updated more often because it is time sensitive. The widgets are only visible if the screen is within a @@ -214,7 +215,7 @@ class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta): copy = True @abc.abstractmethod - def __call__(self, progress: ProgressBar, data: Data): + def __call__(self, progress: ProgressBarMixinBase, data: Data): '''Updates the widget. progress - a reference to the calling ProgressBar @@ -232,7 +233,7 @@ class AutoWidthWidgetBase(WidgetBase, metaclass=abc.ABCMeta): @abc.abstractmethod def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, width: int = 0, ): @@ -281,7 +282,7 @@ def __init__(self, format: str, **kwargs): def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, format: types.Optional[str] = None, ): @@ -350,16 +351,20 @@ def __init__( **kwargs, ): self.samples = samples - self.key_prefix = (self.__class__.__name__ or key_prefix) + '_' + self.key_prefix = ( + key_prefix if key_prefix else self.__class__.__name__ + ) + '_' TimeSensitiveWidgetBase.__init__(self, **kwargs) - def get_sample_times(self, progress: ProgressBar, data: Data): + def get_sample_times(self, progress: ProgressBarMixinBase, data: Data): return progress.extra.setdefault(self.key_prefix + 'sample_times', []) - def get_sample_values(self, progress: ProgressBar, data: Data): + def get_sample_values(self, progress: ProgressBarMixinBase, data: Data): return progress.extra.setdefault(self.key_prefix + 'sample_values', []) - def __call__(self, progress: ProgressBar, data: Data, delta: bool = False): + def __call__( + self, progress: ProgressBarMixinBase, data: Data, delta: bool = False + ): sample_times = self.get_sample_times(progress, data) sample_values = self.get_sample_values(progress, data) @@ -423,7 +428,7 @@ def __init__( self.format_NA = format_NA def _calculate_eta( - self, progress: ProgressBar, data: Data, value, elapsed + self, progress: ProgressBarMixinBase, data: Data, value, elapsed ): '''Updates the widget to show the ETA or total time when finished.''' if elapsed: @@ -438,7 +443,7 @@ def _calculate_eta( def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, value=None, elapsed=None, @@ -484,7 +489,7 @@ class AbsoluteETA(ETA): '''Widget which attempts to estimate the absolute time of arrival.''' def _calculate_eta( - self, progress: ProgressBar, data: Data, value, elapsed + self, progress: ProgressBarMixinBase, data: Data, value, elapsed ): eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed) now = datetime.datetime.now() @@ -522,7 +527,7 @@ def __init__(self, **kwargs): def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, value=None, elapsed=None, @@ -561,7 +566,7 @@ def __init__( def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, format: types.Optional[str] = None, ): @@ -603,7 +608,7 @@ def _speed(self, value, elapsed): def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data, value=None, total_seconds_elapsed=None, @@ -650,7 +655,7 @@ def __init__(self, **kwargs): def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data, value=None, total_seconds_elapsed=None, @@ -682,7 +687,7 @@ def __init__( self.fill = create_marker(fill, self.fill_wrap) if fill else None WidgetBase.__init__(self, **kwargs) - def __call__(self, progress: ProgressBar, data: Data, width=None): + def __call__(self, progress: ProgressBarMixinBase, data: Data, width=None): '''Updates the widget to show the next marker or the first marker when finished''' @@ -709,7 +714,7 @@ def __call__(self, progress: ProgressBar, data: Data, width=None): # cast fill to the same type as marker fill = type(marker)(fill) - return fill + marker + return fill + marker # type: ignore # Alias for backwards compatibility @@ -723,7 +728,9 @@ def __init__(self, format='%(value)d', **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, format=format, **kwargs) - def __call__(self, progress: ProgressBar, data: Data, format=None): + def __call__( + self, progress: ProgressBarMixinBase, data: Data, format=None + ): return FormatWidgetMixin.__call__(self, progress, data, format) @@ -735,7 +742,9 @@ def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, format=format, **kwargs) - def get_format(self, progress: ProgressBar, data: Data, format=None): + def get_format( + self, progress: ProgressBarMixinBase, data: Data, format=None + ): # If percentage is not available, display N/A% percentage = data.get('percentage', base.Undefined) if not percentage and percentage != 0: @@ -747,6 +756,11 @@ def get_format(self, progress: ProgressBar, data: Data, format=None): class SimpleProgress(FormatWidgetMixin, WidgetBase): '''Returns progress as a count of the total (e.g.: "5 of 47")''' + max_width_cache: dict[ + types.Union[str, tuple[float, float | types.Type[base.UnknownLength]]], + types.Optional[int], + ] + DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s' def __init__(self, format=DEFAULT_FORMAT, **kwargs): @@ -754,7 +768,9 @@ def __init__(self, format=DEFAULT_FORMAT, **kwargs): WidgetBase.__init__(self, format=format, **kwargs) self.max_width_cache = dict(default=self.max_width) - def __call__(self, progress: ProgressBar, data: Data, format=None): + def __call__( + self, progress: ProgressBarMixinBase, data: Data, format=None + ): # If max_value is not available, display N/A if data.get('max_value'): data['max_value_s'] = data.get('max_value') @@ -773,7 +789,9 @@ def __call__(self, progress: ProgressBar, data: Data, format=None): # Guess the maximum width from the min and max value key = progress.min_value, progress.max_value - max_width = self.max_width_cache.get(key, self.max_width) + max_width: types.Optional[int] = self.max_width_cache.get( + key, self.max_width + ) if not max_width: temporary_data = data.copy() for value in key: @@ -832,7 +850,7 @@ def __init__( def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, width: int = 0, ): @@ -893,7 +911,7 @@ class BouncingBar(Bar, TimeSensitiveWidgetBase): def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, width: int = 0, ): @@ -944,7 +962,7 @@ def update_mapping(self, **mapping: types.Dict[str, types.Any]): def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, format: types.Optional[str] = None, ): @@ -984,12 +1002,12 @@ def __init__(self, name, markers, **kwargs): Bar.__init__(self, **kwargs) self.markers = [string_or_lambda(marker) for marker in markers] - def get_values(self, progress: ProgressBar, data: Data): + def get_values(self, progress: ProgressBarMixinBase, data: Data): return data['variables'][self.name] or [] def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, width: int = 0, ): @@ -1038,8 +1056,8 @@ def __init__( **kwargs, ) - def get_values(self, progress: ProgressBar, data: Data): - ranges = [0] * len(self.markers) + def get_values(self, progress: ProgressBarMixinBase, data: Data): + ranges = [0.0] * len(self.markers) for value in data['variables'][self.name] or []: if not isinstance(value, (int, float)): # Progress is (value, max) @@ -1113,7 +1131,7 @@ def __init__( def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, width: int = 0, ): @@ -1121,11 +1139,14 @@ def __call__( right = converters.to_unicode(self.right(progress, data, width)) width -= progress.custom_len(left) + progress.custom_len(right) + max_value = progress.max_value + # mypy doesn't get that the first part of the if statement makes sure + # we get the correct type if ( - progress.max_value is not base.UnknownLength - and progress.max_value > 0 + max_value is not base.UnknownLength + and max_value > 0 # type: ignore ): - percent = progress.value / progress.max_value + percent = progress.value / max_value # type: ignore else: percent = 0 @@ -1155,7 +1176,7 @@ def __init__(self, format, **kwargs): def __call__( # type: ignore self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, width: int = 0, format: FormatString = None, @@ -1181,7 +1202,7 @@ def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs): def __call__( # type: ignore self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, width: int = 0, format: FormatString = None, @@ -1209,7 +1230,7 @@ def __init__( def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, format: types.Optional[str] = None, ): @@ -1260,7 +1281,7 @@ def __init__( def __call__( self, - progress: ProgressBar, + progress: ProgressBarMixinBase, data: Data, format: types.Optional[str] = None, ): From e36b73a75d28d381d8519dfb6d5b1a82bb2cca53 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 11:45:45 +0100 Subject: [PATCH 56/66] Little flake8 cleanup --- tests/test_dill_pickle.py | 12 +++++------- tests/test_wrappingio.py | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_dill_pickle.py b/tests/test_dill_pickle.py index ce1ee43d..bfa1da4b 100644 --- a/tests/test_dill_pickle.py +++ b/tests/test_dill_pickle.py @@ -1,5 +1,3 @@ -import pickle - import dill import progressbar @@ -7,11 +5,11 @@ def test_dill(): bar = progressbar.ProgressBar() - assert bar._started == False - assert bar._finished == False + assert bar._started is False + assert bar._finished is False - assert not dill.pickles(bar) + assert dill.pickles(bar) is False - assert bar._started == False + assert bar._started is False # Should be false because it never should have started/initialized - assert bar._finished == False + assert bar._finished is False diff --git a/tests/test_wrappingio.py b/tests/test_wrappingio.py index 6a9f105a..8a352872 100644 --- a/tests/test_wrappingio.py +++ b/tests/test_wrappingio.py @@ -1,5 +1,4 @@ import io -import os import sys import pytest From ed889f2bb21445247ceb117b8666d06548ced16f Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 11:46:18 +0100 Subject: [PATCH 57/66] Full pyright and mypy compliant type hinting --- progressbar/bar.py | 270 +++++++++++++++++++++++++---------------- progressbar/utils.py | 21 ++-- progressbar/widgets.py | 4 +- 3 files changed, 179 insertions(+), 116 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index 2ec687ee..eaa27a5f 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -1,14 +1,15 @@ from __future__ import annotations +import abc import logging import os import sys import time import timeit import warnings -from abc import ABC from copy import deepcopy from datetime import datetime +from typing import Type import math from python_utils import converters, types @@ -22,13 +23,79 @@ logger = logging.getLogger(__name__) -T = types.TypeVar('T') +# float also accepts integers and longs but we don't want an explicit union +# due to type checking complexity +T = float -class ProgressBarMixinBase(object): +class ProgressBarMixinBase(abc.ABC): _started = False _finished = False + _last_update_time: types.Optional[float] = None + + #: The terminal width. This should be automatically detected but will + #: fall back to 80 if auto detection is not possible. term_width: int = 80 + #: The widgets to render, defaults to the result of `default_widget()` + widgets: types.List[widgets_module.WidgetBase] + #: When going beyond the max_value, raise an error if True or silently + #: ignore otherwise + max_error: bool + #: Prefix the progressbar with the given string + prefix: types.Optional[str] + #: Suffix the progressbar with the given string + suffix: types.Optional[str] + #: Justify to the left if `True` or the right if `False` + left_justify: bool + #: The default keyword arguments for the `default_widgets` if no widgets + #: are configured + widget_kwargs: types.Dict[str, types.Any] + #: Custom length function for multibyte characters such as CJK + # custom_len: types.Callable[[str], int] + custom_len: types.ClassVar[ + types.Callable[['ProgressBarMixinBase', str], int] + ] + #: The time the progress bar was started + initial_start_time: types.Optional[datetime] + #: The interval to poll for updates in seconds if there are updates + poll_interval: types.Optional[float] + #: The minimum interval to poll for updates in seconds even if there are + #: no updates + min_poll_interval: float + + #: Current progress (min_value <= value <= max_value) + value: T + #: The minimum/start value for the progress bar + min_value: T + #: Maximum (and final) value. Beyond this value an error will be raised + #: unless the `max_error` parameter is `False`. + max_value: T | types.Type[base.UnknownLength] + #: The time the progressbar reached `max_value` or when `finish()` was + #: called. + end_time: types.Optional[datetime] + #: The time `start()` was called or iteration started. + start_time: types.Optional[datetime] + #: Seconds between `start_time` and last call to `update()` + seconds_elapsed: float + + #: Extra data for widgets with persistent state. This is used by + #: sampling widgets for example. Since widgets can be shared between + #: multiple progressbars we need to store the state with the progressbar. + extra: types.Dict[str, types.Any] + + def get_last_update_time(self) -> types.Optional[datetime]: + if self._last_update_time: + return datetime.fromtimestamp(self._last_update_time) + else: + return None + + def set_last_update_time(self, value: types.Optional[datetime]): + if value: + self._last_update_time = time.mktime(value.timetuple()) + else: + self._last_update_time = None + + last_update_time = property(get_last_update_time, set_last_update_time) def __init__(self, **kwargs): pass @@ -56,15 +123,25 @@ def __del__(self): def __getstate__(self): return self.__dict__ + def data(self) -> types.Dict[str, types.Any]: + raise NotImplementedError() + -class ProgressBarBase(types.Iterable, ProgressBarMixinBase, ABC): +class ProgressBarBase(types.Iterable, ProgressBarMixinBase): pass class DefaultFdMixin(ProgressBarMixinBase): + # The file descriptor to write to. Defaults to `sys.stderr` fd: types.IO = sys.stderr + #: 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 + #: Whether to print line breaks. This is useful for logging the + #: progressbar. When disabled the current line is overwritten. line_breaks: bool = True + #: Enable or disable colors. Defaults to auto detection enable_colors: bool = False def __init__( @@ -147,6 +224,46 @@ def _format_line(self): else: return widgets.rjust(self.term_width) + def _format_widgets(self): + result = [] + expanding = [] + width = self.term_width + data = self.data() + + for index, widget in enumerate(self.widgets): + if isinstance( + widget, widgets.WidgetBase + ) and not widget.check_size(self): + continue + elif isinstance(widget, widgets.AutoWidthWidgetBase): + result.append(widget) + expanding.insert(0, index) + elif isinstance(widget, str): + result.append(widget) + width -= self.custom_len(widget) + else: + widget_output = converters.to_unicode(widget(self, data)) + result.append(widget_output) + width -= self.custom_len(widget_output) + + count = len(expanding) + while expanding: + portion = max(int(math.ceil(width * 1.0 / count)), 0) + index = expanding.pop() + widget = result[index] + count -= 1 + + widget_output = widget(self, data, portion) + width -= self.custom_len(widget_output) + result[index] = widget_output + + return result + + @classmethod + def _to_unicode(cls, args): + for arg in args: + yield converters.to_unicode(arg) + class ResizableMixin(ProgressBarMixinBase): def __init__(self, term_width: int | None = None, **kwargs): @@ -219,7 +336,7 @@ def start(self, *args, **kwargs): utils.streams.start_capturing(self) DefaultFdMixin.start(self, *args, **kwargs) - def update(self, value: float = None): + def update(self, value: types.Optional[float] = None): if not self.line_breaks and utils.streams.needs_clear(): self.fd.write('\r' + ' ' * self.term_width + '\r') @@ -240,7 +357,6 @@ class ProgressBar( StdRedirectMixin, ResizableMixin, ProgressBarBase, - types.Generic[T], ): '''The ProgressBar class which updates and prints the bar. @@ -312,33 +428,23 @@ class ProgressBar( you from changing the ProgressBar you should treat it as read only. ''' - #: Current progress (min_value <= value <= max_value) - value: T - #: Maximum (and final) value. Beyond this value an error will be raised - #: unless the `max_error` parameter is `False`. - max_value: T - #: The time the progressbar reached `max_value` or when `finish()` was - #: called. - end_time: datetime - #: The time `start()` was called or iteration started. - start_time: datetime - #: Seconds between `start_time` and last call to `update()` - seconds_elapsed: float + _iterable: types.Optional[types.Iterable] - _DEFAULT_MAXVAL = base.UnknownLength + _DEFAULT_MAXVAL: Type[base.UnknownLength] = base.UnknownLength # update every 50 milliseconds (up to a 20 times per second) - _MINIMUM_UPDATE_INTERVAL = 0.050 + _MINIMUM_UPDATE_INTERVAL: float = 0.050 + _last_update_time: types.Optional[float] = None def __init__( self, - min_value=0, - max_value=None, - widgets=None, - left_justify=True, - initial_value=0, - poll_interval=None, - widget_kwargs=None, - custom_len=utils.len_color, + min_value: T = 0, + max_value: T | types.Type[base.UnknownLength] | None = None, + widgets: types.List[widgets_module.WidgetBase] = None, + left_justify: bool = True, + initial_value: T = 0, + poll_interval: types.Optional[float] = None, + widget_kwargs: types.Dict[str, types.Any] = None, + custom_len: types.Callable[[str], int] = utils.len_color, max_error=True, prefix=None, suffix=None, @@ -369,33 +475,33 @@ def __init__( poll_interval = kwargs.get('poll') if max_value: - if min_value > max_value: + # mypy doesn't understand that a boolean check excludes + # `UnknownLength` + if min_value > max_value: # type: ignore raise ValueError( 'Max value needs to be bigger than the min ' 'value' ) self.min_value = min_value - self.max_value = max_value + # Legacy issue, `max_value` can be `None` before execution. After + # that it either has a value or is `UnknownLength` + self.max_value = max_value # type: ignore self.max_error = max_error # Only copy the widget if it's safe to copy. Most widgets are so we # assume this to be true - if widgets is None: - self.widgets = widgets - else: - self.widgets = [] - for widget in widgets: - if getattr(widget, 'copy', True): - widget = deepcopy(widget) - self.widgets.append(widget) + self.widgets = [] + for widget in widgets or []: + if getattr(widget, 'copy', True): + widget = deepcopy(widget) + self.widgets.append(widget) - self.widgets = widgets self.prefix = prefix self.suffix = suffix self.widget_kwargs = widget_kwargs or {} self.left_justify = left_justify self.value = initial_value self._iterable = None - self.custom_len = custom_len + self.custom_len = custom_len # type: ignore self.initial_start_time = kwargs.get('start_time') self.init() @@ -410,8 +516,9 @@ def __init__( min_poll_interval = utils.deltas_to_seconds( min_poll_interval, default=None ) - self._MINIMUM_UPDATE_INTERVAL = utils.deltas_to_seconds( - self._MINIMUM_UPDATE_INTERVAL + self._MINIMUM_UPDATE_INTERVAL = ( + utils.deltas_to_seconds(self._MINIMUM_UPDATE_INTERVAL) + or self._MINIMUM_UPDATE_INTERVAL ) # Note that the _MINIMUM_UPDATE_INTERVAL sets the minimum in case of @@ -421,11 +528,11 @@ def __init__( min_poll_interval or self._MINIMUM_UPDATE_INTERVAL, self._MINIMUM_UPDATE_INTERVAL, float(os.environ.get('PROGRESSBAR_MINIMUM_UPDATE_INTERVAL', 0)), - ) + ) # type: ignore # A dictionary of names that can be used by Variable and FormatWidget self.variables = utils.AttributeDict(variables or {}) - for widget in self.widgets or []: + for widget in self.widgets: if isinstance(widget, widgets_module.VariableMixin): if widget.name not in self.variables: self.variables[widget.name] = None @@ -494,19 +601,7 @@ def percentage(self): return percentage - def get_last_update_time(self): - if self._last_update_time: - return datetime.fromtimestamp(self._last_update_time) - - def set_last_update_time(self, value): - if value: - self._last_update_time = time.mktime(value.timetuple()) - else: - self._last_update_time = None - - last_update_time = property(get_last_update_time, set_last_update_time) - - def data(self): + def data(self) -> types.Dict[str, types.Any]: ''' Returns: @@ -653,46 +748,6 @@ def increment(self, value=1, *args, **kwargs): self.update(self.value + value, *args, **kwargs) return self - def _format_widgets(self): - result = [] - expanding = [] - width = self.term_width - data = self.data() - - for index, widget in enumerate(self.widgets): - if isinstance( - widget, widgets.WidgetBase - ) and not widget.check_size(self): - continue - elif isinstance(widget, widgets.AutoWidthWidgetBase): - result.append(widget) - expanding.insert(0, index) - elif isinstance(widget, str): - result.append(widget) - width -= self.custom_len(widget) - else: - widget_output = converters.to_unicode(widget(self, data)) - result.append(widget_output) - width -= self.custom_len(widget_output) - - count = len(expanding) - while expanding: - portion = max(int(math.ceil(width * 1.0 / count)), 0) - index = expanding.pop() - widget = result[index] - count -= 1 - - widget_output = widget(self, data, portion) - width -= self.custom_len(widget_output) - result[index] = widget_output - - return result - - @classmethod - def _to_unicode(cls, args): - for arg in args: - yield converters.to_unicode(arg) - def _needs_update(self): 'Returns whether the ProgressBar should redraw the line.' delta = timeit.default_timer() - self._last_update_timer @@ -707,8 +762,10 @@ def _needs_update(self): # add more bars to progressbar (according to current # terminal width) try: - divisor = self.max_value / self.term_width # float division - if self.value // divisor != self.previous_value // divisor: + divisor: float = self.max_value / self.term_width # type: ignore + value_divisor = self.value // divisor # type: ignore + pvalue_divisor = self.previous_value // divisor # type: ignore + if value_divisor != pvalue_divisor: return True except Exception: # ignore any division errors @@ -798,7 +855,7 @@ def start(self, max_value=None, init=True): ProgressBarBase.start(self, max_value=max_value) # Constructing the default widgets is only done when we know max_value - if self.widgets is None: + if not self.widgets: self.widgets = self.default_widgets() if self.prefix: @@ -818,10 +875,11 @@ def start(self, max_value=None, init=True): self.suffix = None for widget in self.widgets: - interval = getattr(widget, 'INTERVAL', None) + interval: int | float | None = utils.deltas_to_seconds( + getattr(widget, 'INTERVAL', None), + default=None, + ) if interval is not None: - interval = utils.deltas_to_seconds(interval) - self.poll_interval = min( self.poll_interval or interval, interval, @@ -832,7 +890,11 @@ def start(self, max_value=None, init=True): # https://github.com/WoLpH/python-progressbar/issues/207 self.next_update = 0 - if self.max_value is not base.UnknownLength and self.max_value < 0: + if ( + self.max_value is not base.UnknownLength + and self.max_value is not None + and self.max_value < 0 # type: ignore + ): raise ValueError('max_value out of range, got %r' % self.max_value) now = datetime.now() diff --git a/progressbar/utils.py b/progressbar/utils.py index c1400ec6..d65eeb10 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -16,7 +16,7 @@ from python_utils.time import epoch, format_time, timedelta_to_seconds if types.TYPE_CHECKING: - from .bar import ProgressBar + from .bar import ProgressBar, ProgressBarMixinBase assert timedelta_to_seconds assert get_terminal_size @@ -95,8 +95,9 @@ def is_terminal(fd: types.IO, is_terminal: bool | None = None) -> bool: def deltas_to_seconds( - *deltas, **kwargs -) -> int | float | None: # default=ValueError): + *deltas, + default: types.Optional[types.Type[ValueError]] = ValueError, +) -> int | float | None: ''' Convert timedeltas and seconds as int to seconds as float while coalescing @@ -121,9 +122,6 @@ def deltas_to_seconds( >>> deltas_to_seconds(default=0.0) 0.0 ''' - default = kwargs.pop('default', ValueError) - assert not kwargs, 'Only the `default` keyword argument is supported' - for delta in deltas: if delta is None: continue @@ -137,7 +135,8 @@ def deltas_to_seconds( if default is ValueError: raise ValueError('No valid deltas passed to `deltas_to_seconds`') else: - return default + # mypy doesn't understand the `default is ValueError` check + return default # type: ignore def no_color(value: types.StringTypes) -> types.StringTypes: @@ -301,8 +300,8 @@ def __exit__( class StreamWrapper: '''Wrap stdout and stderr globally''' - stdout: types.Union[types.TextIO, WrappingIO] - stderr: types.Union[types.TextIO, WrappingIO] + stdout: types.TextIO | WrappingIO + stderr: types.TextIO | WrappingIO original_excepthook: types.Callable[ [ types.Optional[types.Type[BaseException]], @@ -333,14 +332,14 @@ def __init__(self): if env_flag('WRAP_STDERR', default=False): # pragma: no cover self.wrap_stderr() - def start_capturing(self, bar: ProgressBar | None = None) -> None: + def start_capturing(self, bar: ProgressBarMixinBase | None = None) -> None: if bar: # pragma: no branch self.listeners.add(bar) self.capturing += 1 self.update_capturing() - def stop_capturing(self, bar: ProgressBar | None = None) -> None: + def stop_capturing(self, bar: ProgressBarMixinBase | None = None) -> None: if bar: # pragma: no branch try: self.listeners.remove(bar) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 336dfb79..fdc074de 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -647,7 +647,9 @@ def __call__( class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): - '''WidgetBase for showing the transfer speed, based on the last X samples''' + ''' + WidgetBase for showing the transfer speed, based on the last X samples + ''' def __init__(self, **kwargs): FileTransferSpeed.__init__(self, **kwargs) From fd7f1fceeaf61ed35269023db949fa5e63f5cf46 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 12:23:38 +0100 Subject: [PATCH 58/66] pypy3 builds are broken again, disabling for now --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index df0a7d69..99be8934 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, py39, py310, pypy3, flake8, docs, black, mypy, pyright +envlist = py37, py38, py39, py310, flake8, docs, black, mypy, pyright skip_missing_interpreters = True [testenv] From b2876025bd8b1b180ac35d7c0b0db1df49ade955 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 12:37:58 +0100 Subject: [PATCH 59/66] docs clarification --- progressbar/widgets.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index fdc074de..7e5b78e9 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -585,7 +585,7 @@ def __call__( class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): ''' - WidgetBase for showing the transfer speed (useful for file transfers). + Widget for showing the current transfer speed (useful for file transfers). ''' def __init__( @@ -647,9 +647,7 @@ def __call__( class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): - ''' - WidgetBase for showing the transfer speed, based on the last X samples - ''' + '''Widget for showing the transfer speed based on the last X samples''' def __init__(self, **kwargs): FileTransferSpeed.__init__(self, **kwargs) From 3edbeb05a292d4b0d210a8cba5e33d0caff89a90 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 13:50:14 +0100 Subject: [PATCH 60/66] Added python 3.7 type hinting compatibility --- progressbar/bar.py | 12 ++++++------ progressbar/base.py | 8 ++++++++ progressbar/utils.py | 18 ++++++++++-------- progressbar/widgets.py | 7 ++++--- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index eaa27a5f..ab067e6c 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -133,7 +133,7 @@ class ProgressBarBase(types.Iterable, ProgressBarMixinBase): class DefaultFdMixin(ProgressBarMixinBase): # The file descriptor to write to. Defaults to `sys.stderr` - fd: types.IO = sys.stderr + fd: base.IO = sys.stderr #: Set the terminal to be ANSI compatible. If a terminal is ANSI #: compatible we will automatically enable `colors` and disable #: `line_breaks`. @@ -146,7 +146,7 @@ class DefaultFdMixin(ProgressBarMixinBase): def __init__( self, - fd: types.IO = sys.stderr, + fd: base.IO = sys.stderr, is_terminal: bool | None = None, line_breaks: bool | None = None, enable_colors: bool | None = None, @@ -303,10 +303,10 @@ def finish(self): # pragma: no cover class StdRedirectMixin(DefaultFdMixin): redirect_stderr: bool = False redirect_stdout: bool = False - stdout: types.IO - stderr: types.IO - _stdout: types.IO - _stderr: types.IO + stdout: base.IO + stderr: base.IO + _stdout: base.IO + _stderr: base.IO def __init__( self, diff --git a/progressbar/base.py b/progressbar/base.py index 72f93846..d3f12d52 100644 --- a/progressbar/base.py +++ b/progressbar/base.py @@ -1,4 +1,5 @@ # -*- mode: python; coding: utf-8 -*- +from python_utils import types class FalseMeta(type): @@ -17,3 +18,10 @@ class UnknownLength(metaclass=FalseMeta): class Undefined(metaclass=FalseMeta): pass + + +try: + IO = types.IO # type: ignore + TextIO = types.TextIO # type: ignore +except AttributeError: + from typing.io import IO # type: ignore diff --git a/progressbar/utils.py b/progressbar/utils.py index d65eeb10..76590096 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -15,6 +15,8 @@ from python_utils.terminal import get_terminal_size from python_utils.time import epoch, format_time, timedelta_to_seconds +from progressbar import base + if types.TYPE_CHECKING: from .bar import ProgressBar, ProgressBarMixinBase @@ -39,7 +41,7 @@ def is_ansi_terminal( - fd: types.IO, is_terminal: bool | None = None + fd: base.IO, is_terminal: bool | None = None ) -> bool: # pragma: no cover if is_terminal is None: # Jupyter Notebooks define this variable and support progress bars @@ -74,7 +76,7 @@ def is_ansi_terminal( return bool(is_terminal) -def is_terminal(fd: types.IO, is_terminal: bool | None = None) -> bool: +def is_terminal(fd: base.IO, is_terminal: bool | None = None) -> bool: if is_terminal is None: # Full ansi support encompasses what we expect from a terminal is_terminal = is_ansi_terminal(fd) or None @@ -188,14 +190,14 @@ def env_flag(name: str, default: bool | None = None) -> bool | None: class WrappingIO: buffer: io.StringIO - target: types.IO + target: base.IO capturing: bool listeners: set needs_clear: bool = False def __init__( self, - target: types.IO, + target: base.IO, capturing: bool = False, listeners: types.Set[ProgressBar] = None, ) -> None: @@ -300,8 +302,8 @@ def __exit__( class StreamWrapper: '''Wrap stdout and stderr globally''' - stdout: types.TextIO | WrappingIO - stderr: types.TextIO | WrappingIO + stdout: base.TextIO | WrappingIO + stderr: base.TextIO | WrappingIO original_excepthook: types.Callable[ [ types.Optional[types.Type[BaseException]], @@ -366,7 +368,7 @@ def wrap(self, stdout: bool = False, stderr: bool = False) -> None: if stderr: self.wrap_stderr() - def wrap_stdout(self) -> types.IO: + def wrap_stdout(self) -> base.IO: self.wrap_excepthook() if not self.wrapped_stdout: @@ -377,7 +379,7 @@ def wrap_stdout(self) -> types.IO: return sys.stdout - def wrap_stderr(self) -> types.IO: + def wrap_stderr(self) -> base.IO: self.wrap_excepthook() if not self.wrapped_stderr: diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 7e5b78e9..56d6b417 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -207,9 +207,10 @@ class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta): - max_width: Only display the widget if at most `max_width` is left - weight: Widgets with a higher `weigth` will be calculated before widgets with a lower one - - copy: Copy this widget when initializing the progress bar so the - progressbar can be reused. Some widgets such as the FormatCustomText - require the shared state so this needs to be optional + - copy: Copy this widget when initializing the progress bar so the + progressbar can be reused. Some widgets such as the FormatCustomText + require the shared state so this needs to be optional + ''' copy = True From 9bda71d9be0728ce1e05a3cffff90e7cabc95dc5 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 13:53:05 +0100 Subject: [PATCH 61/66] Added python 3.7 type hinting compatibility --- progressbar/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/progressbar/base.py b/progressbar/base.py index d3f12d52..9bf4e568 100644 --- a/progressbar/base.py +++ b/progressbar/base.py @@ -24,4 +24,7 @@ class Undefined(metaclass=FalseMeta): IO = types.IO # type: ignore TextIO = types.TextIO # type: ignore except AttributeError: - from typing.io import IO # type: ignore + from typing.io import IO, TextIO # type: ignore + +assert IO +assert TextIO From 3cb06e5e7b0417f45eb4bc9846e7c1be8e4020a5 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 16:23:10 +0100 Subject: [PATCH 62/66] All type issues finally fixed? Maybe? --- progressbar/bar.py | 37 +++++++++++++++++++++---------------- progressbar/base.py | 2 +- progressbar/utils.py | 30 +++++++++++++++++++----------- progressbar/widgets.py | 15 +++++++++------ 4 files changed, 50 insertions(+), 34 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index ab067e6c..806a98a8 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -51,10 +51,10 @@ class ProgressBarMixinBase(abc.ABC): #: are configured widget_kwargs: types.Dict[str, types.Any] #: Custom length function for multibyte characters such as CJK - # custom_len: types.Callable[[str], int] - custom_len: types.ClassVar[ - types.Callable[['ProgressBarMixinBase', str], int] - ] + # mypy and pyright can't agree on what the correct one is... so we'll + # need to use a helper function :( + # custom_len: types.Callable[['ProgressBarMixinBase', str], int] + custom_len: types.Callable[[str], int] #: The time the progress bar was started initial_start_time: types.Optional[datetime] #: The interval to poll for updates in seconds if there are updates @@ -188,7 +188,7 @@ def __init__( def update(self, *args, **kwargs): ProgressBarMixinBase.update(self, *args, **kwargs) - line = converters.to_unicode(self._format_line()) + line: str = converters.to_unicode(self._format_line()) if not self.enable_colors: line = utils.no_color(line) @@ -240,11 +240,11 @@ def _format_widgets(self): expanding.insert(0, index) elif isinstance(widget, str): result.append(widget) - width -= self.custom_len(widget) + width -= self.custom_len(widget) # type: ignore else: widget_output = converters.to_unicode(widget(self, data)) result.append(widget_output) - width -= self.custom_len(widget_output) + width -= self.custom_len(widget_output) # type: ignore count = len(expanding) while expanding: @@ -254,7 +254,7 @@ def _format_widgets(self): count -= 1 widget_output = widget(self, data, portion) - width -= self.custom_len(widget_output) + width -= self.custom_len(widget_output) # type: ignore result[index] = widget_output return result @@ -303,8 +303,8 @@ def finish(self): # pragma: no cover class StdRedirectMixin(DefaultFdMixin): redirect_stderr: bool = False redirect_stdout: bool = False - stdout: base.IO - stderr: base.IO + stdout: utils.WrappingIO | base.IO + stderr: utils.WrappingIO | base.IO _stdout: base.IO _stderr: base.IO @@ -428,7 +428,7 @@ class ProgressBar( you from changing the ProgressBar you should treat it as read only. ''' - _iterable: types.Optional[types.Iterable] + _iterable: types.Optional[types.Iterator] _DEFAULT_MAXVAL: Type[base.UnknownLength] = base.UnknownLength # update every 50 milliseconds (up to a 20 times per second) @@ -439,11 +439,11 @@ def __init__( self, min_value: T = 0, max_value: T | types.Type[base.UnknownLength] | None = None, - widgets: types.List[widgets_module.WidgetBase] = None, + widgets: types.Optional[types.List[widgets_module.WidgetBase]] = None, left_justify: bool = True, initial_value: T = 0, poll_interval: types.Optional[float] = None, - widget_kwargs: types.Dict[str, types.Any] = None, + widget_kwargs: types.Optional[types.Dict[str, types.Any]] = None, custom_len: types.Callable[[str], int] = utils.len_color, max_error=True, prefix=None, @@ -594,7 +594,7 @@ def percentage(self): return None elif self.max_value: todo = self.value - self.min_value - total = self.max_value - self.min_value + total = self.max_value - self.min_value # type: ignore percentage = 100.0 * todo / total else: percentage = 100.0 @@ -631,7 +631,7 @@ def data(self) -> types.Dict[str, types.Any]: ''' self._last_update_time = time.time() self._last_update_timer = timeit.default_timer() - elapsed = self.last_update_time - self.start_time + elapsed = self.last_update_time - self.start_time # type: ignore # For Python 2.7 and higher we have _`timedelta.total_seconds`, but we # want to support older versions as well total_seconds_elapsed = utils.deltas_to_seconds(elapsed) @@ -717,11 +717,16 @@ def __iter__(self): def __next__(self): try: - value = next(self._iterable) + if self._iterable is None: # pragma: no cover + value = self.value + else: + value = next(self._iterable) + if self.start_time is None: self.start() else: self.update(self.value + 1) + return value except StopIteration: self.finish() diff --git a/progressbar/base.py b/progressbar/base.py index 9bf4e568..8639f557 100644 --- a/progressbar/base.py +++ b/progressbar/base.py @@ -20,7 +20,7 @@ class Undefined(metaclass=FalseMeta): pass -try: +try: # pragma: no cover IO = types.IO # type: ignore TextIO = types.TextIO # type: ignore except AttributeError: diff --git a/progressbar/utils.py b/progressbar/utils.py index 76590096..7f84a91d 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -26,6 +26,8 @@ assert scale_1024 assert epoch +StringT = types.TypeVar('StringT', bound=types.StringTypes) + ANSI_TERMS = ( '([xe]|bv)term', '(sco)?ansi', @@ -141,7 +143,7 @@ def deltas_to_seconds( return default # type: ignore -def no_color(value: types.StringTypes) -> types.StringTypes: +def no_color(value: StringT) -> StringT: ''' Return the `value` without ANSI escape codes @@ -153,9 +155,10 @@ def no_color(value: types.StringTypes) -> types.StringTypes: 'abc' ''' if isinstance(value, bytes): - return re.sub('\\\u001b\\[.*?[@-~]'.encode(), b'', value) + pattern: bytes = '\\\u001b\\[.*?[@-~]'.encode() + return re.sub(pattern, b'', value) # type: ignore else: - return re.sub(u'\x1b\\[.*?[@-~]', '', value) + return re.sub(u'\x1b\\[.*?[@-~]', '', value) # type: ignore def len_color(value: types.StringTypes) -> int: @@ -199,7 +202,7 @@ def __init__( self, target: base.IO, capturing: bool = False, - listeners: types.Set[ProgressBar] = None, + listeners: types.Optional[types.Set[ProgressBar]] = None, ) -> None: self.buffer = io.StringIO() self.target = target @@ -306,12 +309,17 @@ class StreamWrapper: stderr: base.TextIO | WrappingIO original_excepthook: types.Callable[ [ - types.Optional[types.Type[BaseException]], - types.Optional[BaseException], - types.Optional[TracebackType], + types.Type[BaseException], + BaseException, + TracebackType | None, ], None, ] + # original_excepthook: types.Callable[ + # [ + # types.Type[BaseException], + # BaseException, TracebackType | None, + # ], None] | None wrapped_stdout: int = 0 wrapped_stderr: int = 0 wrapped_excepthook: int = 0 @@ -368,7 +376,7 @@ def wrap(self, stdout: bool = False, stderr: bool = False) -> None: if stderr: self.wrap_stderr() - def wrap_stdout(self) -> base.IO: + def wrap_stdout(self) -> WrappingIO: self.wrap_excepthook() if not self.wrapped_stdout: @@ -377,9 +385,9 @@ def wrap_stdout(self) -> base.IO: ) self.wrapped_stdout += 1 - return sys.stdout + return sys.stdout # type: ignore - def wrap_stderr(self) -> base.IO: + def wrap_stderr(self) -> WrappingIO: self.wrap_excepthook() if not self.wrapped_stderr: @@ -388,7 +396,7 @@ def wrap_stderr(self) -> base.IO: ) self.wrapped_stderr += 1 - return sys.stderr + return sys.stderr # type: ignore def unwrap_excepthook(self) -> None: if self.wrapped_excepthook: diff --git a/progressbar/widgets.py b/progressbar/widgets.py index 56d6b417..b13d9f33 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -132,7 +132,7 @@ def __call__( progress: ProgressBarMixinBase, data: Data, format: types.Optional[str] = None, - ): + ) -> str: '''Formats the widget into a string''' format = self.get_format(progress, data, format) try: @@ -216,7 +216,7 @@ class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta): copy = True @abc.abstractmethod - def __call__(self, progress: ProgressBarMixinBase, data: Data): + def __call__(self, progress: ProgressBarMixinBase, data: Data) -> str: '''Updates the widget. progress - a reference to the calling ProgressBar @@ -237,7 +237,7 @@ def __call__( progress: ProgressBarMixinBase, data: Data, width: int = 0, - ): + ) -> str: '''Updates the widget providing the total width the widget must fill. progress - a reference to the calling ProgressBar @@ -702,7 +702,9 @@ def __call__(self, progress: ProgressBarMixinBase, data: Data, width=None): if self.fill: # Cut the last character so we can replace it with our marker fill = self.fill( - progress, data, width - progress.custom_len(marker) + progress, + data, + width - progress.custom_len(marker), # type: ignore ) else: fill = '' @@ -767,7 +769,8 @@ class SimpleProgress(FormatWidgetMixin, WidgetBase): def __init__(self, format=DEFAULT_FORMAT, **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, format=format, **kwargs) - self.max_width_cache = dict(default=self.max_width) + self.max_width_cache = dict() + self.max_width_cache['default'] = self.max_width or 0 def __call__( self, progress: ProgressBarMixinBase, data: Data, format=None @@ -950,7 +953,7 @@ class FormatCustomText(FormatWidgetMixin, WidgetBase): def __init__( self, format: str, - mapping: types.Dict[str, types.Any] = None, + mapping: types.Optional[types.Dict[str, types.Any]] = None, **kwargs, ): self.format = format From c83073c30b790cbb29a593ea0a0d1bd0d9158e5e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 16:36:36 +0100 Subject: [PATCH 63/66] added pyright config --- pyrightconfig.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 pyrightconfig.json diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..58d8fa22 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,11 @@ +{ + "include": [ + "progressbar" + ], + "exclude": [ + "examples" + ], + "ignore": [ + "docs" + ], +} From db8727dcf87f440bdd36b3992abc7413162c7a83 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 1 Nov 2022 16:38:47 +0100 Subject: [PATCH 64/66] Incrementing version to v4.3b.0 --- progressbar/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progressbar/__about__.py b/progressbar/__about__.py index 64d0af06..a5d57b2e 100644 --- a/progressbar/__about__.py +++ b/progressbar/__about__.py @@ -21,7 +21,7 @@ '''.strip().split() ) __email__ = 'wolph@wol.ph' -__version__ = '4.2.0' +__version__ = '4.3b.0' __license__ = 'BSD' __copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)' __url__ = 'https://github.com/WoLpH/python-progressbar' From 94f2e789e4008c824dfd5f38f2e0ee694af584c3 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 23 Nov 2023 18:07:50 +0100 Subject: [PATCH 65/66] mypy fixes --- progressbar/bar.py | 15 ++++++++++----- progressbar/base.py | 6 +++--- progressbar/utils.py | 10 +++++----- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/progressbar/bar.py b/progressbar/bar.py index 806a98a8..c1a64328 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -106,7 +106,7 @@ def start(self, **kwargs): def update(self, value=None): pass - def finish(self): # pragma: no cover + def finish(self) -> None: # pragma: no cover self._finished = True def __del__(self): @@ -185,7 +185,7 @@ def __init__( ProgressBarMixinBase.__init__(self, **kwargs) - def update(self, *args, **kwargs): + def update(self, *args: types.Any, **kwargs: types.Any) -> None: ProgressBarMixinBase.update(self, *args, **kwargs) line: str = converters.to_unicode(self._format_line()) @@ -202,7 +202,8 @@ def update(self, *args, **kwargs): except UnicodeEncodeError: # pragma: no cover self.fd.write(line.encode('ascii', 'replace')) - def finish(self, *args, **kwargs): # pragma: no cover + def finish(self, *args: types.Any, **kwargs: types.Any) -> None: # pragma: no cover + if self._finished: return @@ -824,14 +825,18 @@ def update(self, value=None, force=False, **kwargs): # Only flush if something was actually written self.fd.flush() - def start(self, max_value=None, init=True): + def start( # type: ignore[override] + self, + max_value: int | None=None, + init: bool=True, + ): '''Starts measuring time, and prints the bar at 0%. It returns self so you can use it like this: Args: max_value (int): The maximum value of the progressbar - reinit (bool): Initialize the progressbar, this is useful if you + init (bool): (Re)Initialize the progressbar, this is useful if you wish to reuse the same progressbar but can be disabled if data needs to be passed along to the next run diff --git a/progressbar/base.py b/progressbar/base.py index 8639f557..8e007914 100644 --- a/progressbar/base.py +++ b/progressbar/base.py @@ -23,8 +23,8 @@ class Undefined(metaclass=FalseMeta): try: # pragma: no cover IO = types.IO # type: ignore TextIO = types.TextIO # type: ignore -except AttributeError: +except AttributeError: # pragma: no cover from typing.io import IO, TextIO # type: ignore -assert IO -assert TextIO +assert IO is not None +assert TextIO is not None diff --git a/progressbar/utils.py b/progressbar/utils.py index 7f84a91d..6cfc4bb9 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -20,11 +20,11 @@ if types.TYPE_CHECKING: from .bar import ProgressBar, ProgressBarMixinBase -assert timedelta_to_seconds -assert get_terminal_size -assert format_time -assert scale_1024 -assert epoch +assert timedelta_to_seconds is not None +assert get_terminal_size is not None +assert format_time is not None +assert scale_1024 is not None +assert epoch is not None StringT = types.TypeVar('StringT', bound=types.StringTypes) From c66f275d3d8cd71e3911a715a0b77d0ccf558c3e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 23 Nov 2023 18:17:59 +0100 Subject: [PATCH 66/66] fixed all tests? --- .github/workflows/main.yml | 2 +- docs/conf.py | 2 +- progressbar/bar.py | 11 ++++++----- progressbar/shortcuts.py | 4 ++-- progressbar/widgets.py | 1 - tox.ini | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 84ccac34..ddac4b2a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 diff --git a/docs/conf.py b/docs/conf.py index 15d5ba34..a7f5a618 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -330,4 +330,4 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} diff --git a/progressbar/bar.py b/progressbar/bar.py index c1a64328..f1077cb9 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -202,8 +202,9 @@ def update(self, *args: types.Any, **kwargs: types.Any) -> None: except UnicodeEncodeError: # pragma: no cover self.fd.write(line.encode('ascii', 'replace')) - def finish(self, *args: types.Any, **kwargs: types.Any) -> None: # pragma: no cover - + def finish( + self, *args: types.Any, **kwargs: types.Any + ) -> None: # pragma: no cover if self._finished: return @@ -826,9 +827,9 @@ def update(self, value=None, force=False, **kwargs): self.fd.flush() def start( # type: ignore[override] - self, - max_value: int | None=None, - init: bool=True, + self, + max_value: int | None = None, + init: bool = True, ): '''Starts measuring time, and prints the bar at 0%. diff --git a/progressbar/shortcuts.py b/progressbar/shortcuts.py index 9e1502dd..dd61c9cb 100644 --- a/progressbar/shortcuts.py +++ b/progressbar/shortcuts.py @@ -8,7 +8,7 @@ def progressbar( widgets=None, prefix=None, suffix=None, - **kwargs + **kwargs, ): progressbar = bar.ProgressBar( min_value=min_value, @@ -16,7 +16,7 @@ def progressbar( widgets=widgets, prefix=prefix, suffix=suffix, - **kwargs + **kwargs, ) for result in progressbar(iterator): diff --git a/progressbar/widgets.py b/progressbar/widgets.py index b13d9f33..b8215bdf 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -417,7 +417,6 @@ def __init__( format_NA='ETA: N/A', **kwargs, ): - if '%s' in format and '%(eta)s' not in format: format = format.replace('%s', '%(eta)s') diff --git a/tox.ini b/tox.ini index 99be8934..f169ee95 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,14 @@ [tox] -envlist = py37, py38, py39, py310, flake8, docs, black, mypy, pyright +envlist = py38, py39, py310, py311, py312, flake8, docs, black, mypy, pyright skip_missing_interpreters = True [testenv] basepython = - py36: python3.6 - py37: python3.7 py38: python3.8 py39: python3.9 py310: python3.10 + py311: python3.11 + py312: python3.12 pypy3: pypy3 deps = -r{toxinidir}/tests/requirements.txt @@ -42,7 +42,7 @@ commands = black --skip-string-normalization --line-length 79 {toxinidir}/progre changedir = basepython = python3 deps = -r{toxinidir}/docs/requirements.txt -whitelist_externals = +allowlist_externals = rm cd mkdir