Repository: Textualize/rich
Pull request #2210 (2022-04-21 15:34:11)

progress speed
Adds a `show_speed` parameter to `TaskProgressColumn` which displays speed in iterations per second when the total is not known in advance.

<img width="731" alt="Screenshot 2022-04-21 at 16 32 58" src="https://user-images.githubusercontent.com/554369/164496682-44c697fd-f061-4858-9370-6dfcd1f27dfe.png">



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b73aaa18..12d99a5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Ability to change terminal window title https://github.com/Textualize/rich/pull/2200
+- Added show_speed parameter to progress.track which will show the speed when the total is not known
 
 ### Fixed
 
diff --git a/rich/progress.py b/rich/progress.py
index b0642687..ca52c4b9 100644
--- a/rich/progress.py
+++ b/rich/progress.py
@@ -115,6 +115,7 @@ def track(
     pulse_style: StyleType = "bar.pulse",
     update_period: float = 0.1,
     disable: bool = False,
+    show_speed: bool = True,
 ) -> Iterable[ProgressType]:
     """Track progress by iterating over a sequence.
 
@@ -132,6 +133,7 @@ def track(
         pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
         update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
         disable (bool, optional): Disable display of progress.
+        show_speed (bool, optional): Show speed if total isn't known. Defaults to True.
     Returns:
         Iterable[ProgressType]: An iterable of the values in the sequence.
 
@@ -148,7 +150,7 @@ def track(
                 finished_style=finished_style,
                 pulse_style=pulse_style,
             ),
-            TaskProgressColumn(),
+            TaskProgressColumn(show_speed=show_speed),
             TimeRemainingColumn(),
         )
     )
@@ -676,7 +678,18 @@ class TimeElapsedColumn(ProgressColumn):
 
 
 class TaskProgressColumn(TextColumn):
-    """A column displaying the progress of a task."""
+    """Show task progress as a percentage.
+
+    Args:
+        text_format (str, optional): Format for percentage display. Defaults to "[progress.percentage]{task.percentage:>3.0f}%".
+        text_format_no_percentage (str, optional): Format if percentage is unknown. Defaults to "".
+        style (StyleType, optional): Style of output. Defaults to "none".
+        justify (JustifyMethod, optional): Text justification. Defaults to "left".
+        markup (bool, optional): Enable markup. Defaults to True.
+        highlighter (Optional[Highlighter], optional): Highlighter to apply to output. Defaults to None.
+        table_column (Optional[Column], optional): Table Column to use. Defaults to None.
+        show_speed (bool, optional): Show speed if total is unknown. Defaults to False.
+    """
 
     def __init__(
         self,
@@ -687,8 +700,11 @@ class TaskProgressColumn(TextColumn):
         markup: bool = True,
         highlighter: Optional[Highlighter] = None,
         table_column: Optional[Column] = None,
+        show_speed: bool = False,
     ) -> None:
+
         self.text_format_no_percentage = text_format_no_percentage
+        self.show_speed = show_speed
         super().__init__(
             text_format=text_format,
             style=style,
@@ -698,7 +714,29 @@ class TaskProgressColumn(TextColumn):
             table_column=table_column,
         )
 
+    @classmethod
+    def render_speed(cls, speed: Optional[float]) -> Text:
+        """Render the speed in iterations per second.
+
+        Args:
+            task (Task): A Task object.
+
+        Returns:
+            Text: Text object containing the task speed.
+        """
+        if speed is None:
+            return Text("", style="progress.percentage")
+        unit, suffix = filesize.pick_unit_and_suffix(
+            int(speed),
+            ["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
+            1000,
+        )
+        data_speed = speed / unit
+        return Text(f"{data_speed:.1f}{suffix} it/s", style="progress.percentage")
+
     def render(self, task: "Task") -> Text:
+        if task.total is None and self.show_speed:
+            return self.render_speed(task.finished_speed or task.speed)
         text_format = (
             self.text_format_no_percentage if task.total is None else self.text_format
         )
@@ -1152,13 +1190,10 @@ class Progress(JupyterMixin):
             Iterable[ProgressType]: An iterable of values taken from the provided sequence.
         """
 
+        task_total: Optional[float] = None
         if total is None:
             if isinstance(sequence, Sized):
                 task_total = float(len(sequence))
-            else:
-                raise ValueError(
-                    f"unable to get size of {sequence!r}, please specify 'total'"
-                )
         else:
             task_total = total
 


Test patch:
diff --git a/tests/test_progress.py b/tests/test_progress.py
index 61e11a9c..8a90ad80 100644
--- a/tests/test_progress.py
+++ b/tests/test_progress.py
@@ -308,10 +308,6 @@ def test_track() -> None:
 
     assert result == expected
 
-    with pytest.raises(ValueError):
-        for n in track(5):
-            pass
-
 
 def test_progress_track() -> None:
     console = Console(
@@ -341,10 +337,6 @@ def test_progress_track() -> None:
 
     assert result == expected
 
-    with pytest.raises(ValueError):
-        for n in progress.track(5):
-            pass
-
 
 def test_columns() -> None:
 
@@ -655,6 +647,20 @@ def test_wrap_file_task_total() -> None:
         os.remove(filename)
 
 
+def test_task_progress_column_speed():
+    speed_text = TaskProgressColumn.render_speed(None)
+    assert speed_text.plain == ""
+
+    speed_text = TaskProgressColumn.render_speed(5)
+    assert speed_text.plain == "5.0 it/s"
+
+    speed_text = TaskProgressColumn.render_speed(5000)
+    assert speed_text.plain == "5.0×10³ it/s"
+
+    speed_text = TaskProgressColumn.render_speed(8888888)
+    assert speed_text.plain == "8.9×10⁶ it/s"
+
+
 if __name__ == "__main__":
     _render = render_progress()
     print(_render)