- Notifications
You must be signed in to change notification settings - Fork 750
[wip] ProgressBar with callable remove_when_done #1025
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -15,6 +15,7 @@ | |
| import traceback | ||
| from asyncio import get_event_loop, new_event_loop, set_event_loop | ||
| from typing import ( | ||
| Callable, | ||
| Generic, | ||
| Iterable, | ||
| List, | ||
| | @@ -23,11 +24,13 @@ | |
| Sized, | ||
| TextIO, | ||
| TypeVar, | ||
| Union, | ||
| cast, | ||
| ) | ||
| | ||
| from prompt_toolkit.application import Application | ||
| from prompt_toolkit.application import Application, get_app | ||
| from prompt_toolkit.application.current import get_app_session | ||
| from prompt_toolkit.eventloop import run_in_executor_with_context | ||
| from prompt_toolkit.filters import Condition, is_done, renderer_height_is_known | ||
| from prompt_toolkit.formatted_text import ( | ||
| AnyFormattedText, | ||
| | @@ -56,12 +59,10 @@ | |
| try: | ||
| import contextvars | ||
| except ImportError: | ||
| from prompt_toolkit.eventloop import dummy_contextvars as contextvars # type: ignore | ||
| import prompt_toolkit.eventloop.dummy_contextvars as contextvars # type: ignore | ||
| | ||
| | ||
| __all__ = [ | ||
| "ProgressBar", | ||
| ] | ||
| __all__ = ["ProgressBar"] | ||
| | ||
| E = KeyPressEvent | ||
| | ||
| | @@ -250,15 +251,19 @@ def __call__( | |
| self, | ||
| data: Optional[Iterable[_T]] = None, | ||
| label: AnyFormattedText = "", | ||
| remove_when_done: bool = False, | ||
| remove_when_done: Union[ | ||
| Callable[["ProgressBarCounter[_T]"], bool], bool | ||
| ] = False, | ||
| total: Optional[int] = None, | ||
| ) -> "ProgressBarCounter[_T]": | ||
| """ | ||
| Start a new counter. | ||
| | ||
| :param label: Title text or description for this progress. (This can be | ||
| formatted text as well). | ||
| :param remove_when_done: When `True`, hide this progress bar. | ||
| :param remove_when_done: When `True`, hide this progress bar. Can be a | ||
| callable accepting a ProgressBarCounter instance. The callable is | ||
| run in the background allowing for delayed removals. | ||
| :param total: Specify the maximum value if it can't be calculated by | ||
| calling ``len``. | ||
| """ | ||
| | @@ -319,7 +324,9 @@ def __init__( | |
| progress_bar: ProgressBar, | ||
| data: Optional[Iterable[_CounterItem]] = None, | ||
| label: AnyFormattedText = "", | ||
| remove_when_done: bool = False, | ||
| remove_when_done: Union[ | ||
| Callable[["ProgressBarCounter[_CounterItem]"], bool], bool | ||
| ] = False, | ||
| total: Optional[int] = None, | ||
| ) -> None: | ||
| | ||
| | @@ -359,6 +366,13 @@ def item_completed(self) -> None: | |
| self.items_completed += 1 | ||
| self.progress_bar.invalidate() | ||
| | ||
| async def _remove_when_done_async(self) -> None: | ||
| def run_remove_when_done_thread() -> bool: | ||
| return self.remove_when_done(self) | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't correspond with the type signature, I think it's fine to pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hm I think I got this figured out | ||
| | ||
| if await run_in_executor_with_context(run_remove_when_done_thread): | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still thinking about whether this is the right thing to do. By default, I'd like as much as possible for prompt_toolkit to have asynchronous APIs, and possibly create an synchronous abstraction on top of that. If we expect a blocking synchronous function here, then we exclude people that would like to include it in an async app. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I borrowed almost all of this code from how validation and Would it be better to allow | ||
| self.progress_bar.counters.remove(self) | ||
| | ||
| @property | ||
| def done(self) -> bool: | ||
| return self._done | ||
| | @@ -370,8 +384,17 @@ def done(self, value: bool) -> None: | |
| # If done then store the stop_time, otherwise clear. | ||
| self.stop_time = datetime.datetime.now() if value else None | ||
| | ||
| if value and self.remove_when_done: | ||
| self.progress_bar.counters.remove(self) | ||
| if value: | ||
| if callable(self.remove_when_done): | ||
| # Register a background task to run the remove_when_done callable. | ||
kenodegard marked this conversation as resolved. Show resolved Hide resolved | ||
| self.progress_bar._app_loop.call_soon_threadsafe( | ||
| lambda: self.progress_bar.app.create_background_task( | ||
| self._remove_when_done_async() | ||
| ) | ||
| ) | ||
| elif self.remove_when_done: | ||
| # Non-callable that is True, remove bar. | ||
| self.progress_bar.counters.remove(self) | ||
| | ||
| @property | ||
| def percentage(self) -> float: | ||
| | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nice. That's a great use case.
I'm still not convinced though that a thread is the right approach here. We will get many threads that never finish and can't be joined anywhere. I'm thinking about whether we could reuse the prompt_toolkit filters in this case too, and do something like:
If this condition evaluates during every render, we can remove the bar when this turns False.