Repository: pallets/click (python)

Issue:
Avoid rendering on highly frequent updates in progressbar
I am using a progressbar in a loop with short iterations, which negatively affects performance. It would be good to supply a chunk size such that the heavy part of an update is only performed every nth time. 

Currently I am using a small wrapper around it:

```
class ChunkProgressbar:
    def __init__(self, iterator, chunk_size=100, enabled=True, **kwargs):
        self.iterator = iterator
        self.chunk_size = chunk_size
        self.bar = progressbar(iterator, **kwargs)
        self.missed_updates = 0

    def __enter__(self):
        if self.bar is not None:
            self.bar.__enter__()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.bar.update(self.missed_updates)
        self.bar.__exit__(exc_type, exc_val, exc_tb)

    def __iter__(self):
        for index, elem in enumerate(self.iterator):
            yield elem
            self.missed_updates += 1
            if index % self.chunk_size == self.chunk_size - 1:
                self.bar.update(self.missed_updates)
                self.missed_updates = 0
```

One could also throttle the rendering based on timing information (like the eta calculation), but that might be costly again due to timing calls or mess with heterogeneous iteration times.


Pull request description:
Add update interval for progress bar
Adds an optional `update_interval` for progress bars that allows the user to skip unnecessary renders for frequently updated iterators.

- Fixes #676


Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index d86e7ce..4845673 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -149,6 +149,8 @@ Unreleased
     type like an input value would be. :pr:`1517`
 -   Automatically generated short help messages will stop at the first
     ending of a phrase or double linebreak. :issue:`1082`
+-   Skip progress bar render steps for efficiency with very fast
+    iterators by setting ``update_min_steps``. :issue:`676`
 
 
 Version 7.1.2
diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py
index 5394140..3104409 100644
--- a/src/click/_termui_impl.py
+++ b/src/click/_termui_impl.py
@@ -62,6 +62,7 @@ class ProgressBar:
         label=None,
         file=None,
         color=None,
+        update_min_steps=1,
         width=30,
     ):
         self.fill_char = fill_char
@@ -77,6 +78,8 @@ class ProgressBar:
             file = _default_text_stdout()
         self.file = file
         self.color = color
+        self.update_min_steps = update_min_steps
+        self._completed_intervals = 0
         self.width = width
         self.autowidth = width == 0
 
@@ -290,13 +293,23 @@ class ProgressBar:
         :param current_item: Optional item to set as ``current_item``
             for the updated position.
 
-        .. versionadded:: 8.0
+        .. versionchanged:: 8.0
             Added the ``current_item`` optional parameter.
+
+        .. versionchanged:: 8.0
+            Only render when the number of steps meets the
+            ``update_min_steps`` threshold.
         """
-        self.make_step(n_steps)
-        if current_item is not None:
-            self.current_item = current_item
-        self.render_progress()
+        self._completed_intervals += n_steps
+
+        if self._completed_intervals >= self.update_min_steps:
+            self.make_step(self._completed_intervals)
+
+            if current_item is not None:
+                self.current_item = current_item
+
+            self.render_progress()
+            self._completed_intervals = 0
 
     def finish(self):
         self.eta_known = 0
diff --git a/src/click/termui.py b/src/click/termui.py
index 84442ee..0801af3 100644
--- a/src/click/termui.py
+++ b/src/click/termui.py
@@ -299,6 +299,7 @@ def progressbar(
     width=36,
     file=None,
     color=None,
+    update_min_steps=1,
 ):
     """This function creates an iterable context manager that can be used
     to iterate over something while showing a progress bar.  It will
@@ -353,12 +354,6 @@ def progressbar(
                 archive.extract()
                 bar.update(archive.size, archive)
 
-    .. versionadded:: 2.0
-
-    .. versionadded:: 4.0
-       Added the `color` parameter.  Added a `update` method to the
-       progressbar object.
-
     :param iterable: an iterable to iterate over.  If not provided the length
                      is required.
     :param length: the number of items to iterate over.  By default the
@@ -397,6 +392,17 @@ def progressbar(
                   default is autodetection.  This is only needed if ANSI
                   codes are included anywhere in the progress bar output
                   which is not the case by default.
+    :param update_min_steps: Render only when this many updates have
+        completed. This allows tuning for very fast iterators.
+
+    .. versionadded:: 8.0
+       Added the ``update_min_steps`` parameter.
+
+    .. versionchanged:: 4.0
+        Added the ``color`` parameter. Added the ``update`` method to
+        the object.
+
+    .. versionadded:: 2.0
     """
     from ._termui_impl import ProgressBar
 
@@ -416,6 +422,7 @@ def progressbar(
         label=label,
         width=width,
         color=color,
+        update_min_steps=update_min_steps,
     )
 
 


Test patch:
diff --git a/tests/test_termui.py b/tests/test_termui.py
index e199516..956de77 100644
--- a/tests/test_termui.py
+++ b/tests/test_termui.py
@@ -344,6 +344,16 @@ def test_progressbar_update_with_item_show_func(runner, monkeypatch):
     assert "Custom 4" in lines[2]
 
 
+def test_progress_bar_update_min_steps(runner):
+    bar = _create_progress(update_min_steps=5)
+    bar.update(3)
+    assert bar._completed_intervals == 3
+    assert bar.pos == 0
+    bar.update(2)
+    assert bar._completed_intervals == 0
+    assert bar.pos == 5
+
+
 @pytest.mark.parametrize("key_char", ("h", "H", "é", "À", " ", "字", "àH", "àR"))
 @pytest.mark.parametrize("echo", [True, False])
 @pytest.mark.skipif(not WIN, reason="Tests user-input using the msvcrt module.")