diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e1a20c1..0165c8ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Added `progress.SpeedColumn` a generic speed column with customizable units + ## [13.9.2] - 2024-10-04 ### Fixed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d8985ca13..4edc03cc1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -91,3 +91,4 @@ The following people have contributed to the development of Rich: - [L. Yeung](https://github.com/lewis-yeung) - [chthollyphile](https://github.com/chthollyphile) - [Jonathan Helmus](https://github.com/jjhelmus) +- [Sven Augustin](https://github.com/nichtjens) diff --git a/docs/source/progress.rst b/docs/source/progress.rst index baeb3a25a..62f669c1a 100644 --- a/docs/source/progress.rst +++ b/docs/source/progress.rst @@ -161,6 +161,7 @@ The following column objects are available: - :class:`~rich.progress.TotalFileSizeColumn` Displays total file size (assumes the steps are bytes). - :class:`~rich.progress.DownloadColumn` Displays download progress (assumes the steps are bytes). - :class:`~rich.progress.TransferSpeedColumn` Displays transfer speed (assumes the steps are bytes). +- :class:`~rich.progress.SpeedColumn` Displays generic speed in customizable units (defaults to it/s). - :class:`~rich.progress.SpinnerColumn` Displays a "spinner" animation. - :class:`~rich.progress.RenderableColumn` Displays an arbitrary Rich renderable in the column. - :class:`~rich.progress.IterationSpeedColumn` Displays iteration speed in it/s (iterations per second). diff --git a/rich/progress.py b/rich/progress.py index 1e92eb6b1..c8bb8ef41 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -711,6 +711,7 @@ class TaskProgressColumn(TextColumn): 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. + speed_units (str, optional): Units of the speed. Defaults to it/s. """ def __init__( @@ -723,9 +724,11 @@ def __init__( highlighter: Optional[Highlighter] = None, table_column: Optional[Column] = None, show_speed: bool = False, + speed_units: str = "it/s", ) -> None: self.text_format_no_percentage = text_format_no_percentage self.show_speed = show_speed + self.speed_units = speed_units super().__init__( text_format=text_format, style=style, @@ -736,11 +739,12 @@ def __init__( ) @classmethod - def render_speed(cls, speed: Optional[float]) -> Text: - """Render the speed in iterations per second. + def render_speed(cls, speed: Optional[float], units: str = "it/s") -> Text: + """Render the speed in iterations per second or the supplied units. Args: - task (Task): A Task object. + speed (float): Current value of the speed. + units (str): Units of the speed. Defaults to it/s. Returns: Text: Text object containing the task speed. @@ -753,11 +757,13 @@ def render_speed(cls, speed: Optional[float]) -> Text: 1000, ) data_speed = speed / unit - return Text(f"{data_speed:.1f}{suffix} it/s", style="progress.percentage") + return Text(f"{data_speed:.1f}{suffix} {units}", 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) + return self.render_speed( + task.finished_speed or task.speed, units=self.speed_units + ) text_format = ( self.text_format_no_percentage if task.total is None else self.text_format ) @@ -925,6 +931,24 @@ def render(self, task: "Task") -> Text: return Text(f"{data_speed}/s", style="progress.data.speed") +class SpeedColumn(ProgressColumn): + """Renders human readable speed. + + Args: + table_column (Optional[Column], optional): Table Column to use. Defaults to None. + units (str, optional): Units of the speed. Defaults to it/s. + """ + + def __init__(self, *args, units: str = "it/s", **kwargs) -> None: + self.units = units + super().__init__(*args, **kwargs) + + def render(self, task: "Task") -> Text: + """Show speed.""" + speed = task.finished_speed or task.speed + return TaskProgressColumn.render_speed(speed, units=self.units) + + class ProgressSample(NamedTuple): """Sample of progress for a given time.""" diff --git a/tests/test_progress.py b/tests/test_progress.py index 0be683c3e..a546364cf 100644 --- a/tests/test_progress.py +++ b/tests/test_progress.py @@ -17,6 +17,7 @@ MofNCompleteColumn, Progress, RenderableColumn, + SpeedColumn, SpinnerColumn, Task, TaskID, @@ -356,6 +357,7 @@ def test_columns() -> None: TotalFileSizeColumn(), DownloadColumn(), TransferSpeedColumn(), + SpeedColumn(), MofNCompleteColumn(), MofNCompleteColumn(separator=" of "), transient=True, @@ -377,7 +379,7 @@ def test_columns() -> None: result = replace_link_ids(console.file.getvalue()) print(repr(result)) - expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kfoo\ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:34\x1b[0m \x1b[32m12 \x1b[0m \x1b[32m10 \x1b[0m \x1b[32m12/10 \x1b[0m \x1b[31m1 \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of 10\x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbyte/s \x1b[0m \ntest bar \x1b[38;2;114;156;31m━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:29\x1b[0m \x1b[32m16 \x1b[0m \x1b[32m7 bytes\x1b[0m \x1b[32m16/7 \x1b[0m \x1b[31m2 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7 \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbytes/s\x1b[0m \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:34\x1b[0m \x1b[32m12 \x1b[0m \x1b[32m10 \x1b[0m \x1b[32m12/10 \x1b[0m \x1b[31m1 \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of 10\x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbyte/s \x1b[0m \ntest bar \x1b[38;2;114;156;31m━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:29\x1b[0m \x1b[32m16 \x1b[0m \x1b[32m7 bytes\x1b[0m \x1b[32m16/7 \x1b[0m \x1b[31m2 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7 \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbytes/s\x1b[0m \n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K" + expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 \x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\n \x1b[32mbytes \x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:19\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Kfoo\ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 \x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\n \x1b[32mbytes \x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:19\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 \x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\n \x1b[32mbytes \x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:19\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 \x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\n \x1b[32mbytes \x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:19\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:…\x1b[0m \x1b[33m0:00:…\x1b[0m \x1b[32m12 \x1b[0m \x1b[32m10 \x1b[0m \x1b[32m12/10 \x1b[0m \x1b[31m1 \x1b[0m \x1b[35m1.3 \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbyte/s\x1b[0m \x1b[35mit/s \x1b[0m \x1b[32m10 \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:…\x1b[0m \x1b[33m0:00:…\x1b[0m \x1b[32m16 \x1b[0m \x1b[32m7 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[31m2 \x1b[0m \x1b[35m2.0 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7\x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbytes…\x1b[0m \x1b[35mit/s \x1b[0m \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:…\x1b[0m \x1b[33m0:00:…\x1b[0m \x1b[32m12 \x1b[0m \x1b[32m10 \x1b[0m \x1b[32m12/10 \x1b[0m \x1b[31m1 \x1b[0m \x1b[35m1.3 \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbyte/s\x1b[0m \x1b[35mit/s \x1b[0m \x1b[32m10 \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:…\x1b[0m \x1b[33m0:00:…\x1b[0m \x1b[32m16 \x1b[0m \x1b[32m7 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[31m2 \x1b[0m \x1b[35m2.0 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7\x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbytes…\x1b[0m \x1b[35mit/s \x1b[0m \n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K" assert result == expected @@ -659,6 +661,15 @@ def test_task_progress_column_speed() -> None: speed_text = TaskProgressColumn.render_speed(8888888) assert speed_text.plain == "8.9×10⁶ it/s" + speed_text = TaskProgressColumn.render_speed(5, units="Hz") + assert speed_text.plain == "5.0 Hz" + + speed_text = TaskProgressColumn.render_speed(5000, units="Hz") + assert speed_text.plain == "5.0×10³ Hz" + + speed_text = TaskProgressColumn.render_speed(8888888, units="Hz") + assert speed_text.plain == "8.9×10⁶ Hz" + if __name__ == "__main__": _render = render_progress()