- 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
[wip] ProgressBar with callable remove_when_done #1025
Conversation
| Ah, so progress bars are juggling two event loops, the main loop ( A slight modification was introduced to python-prompt-toolkit/prompt_toolkit/application/application.py Lines 613 to 618 in 7d2ef19
So should we be using |
| In the case of the progress bars, there should be only one event loop running, which is the one in the UI thread. |
| Two things:
|
| This allows for more creative removals. I'm personally interested in removal policies like keeping the last X counters and keeping each counter for Y minutes. |
| Pinging this - just don't want to lose steam on this one. @jonathanslenders, let me know if there is anything I can change or further clarify. |
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.
I have a few change requests here.
But, I'm still wondering too why we need a callback for this. Could you please share some example code for how this would be used? I think it would be good to have examples in the examples/ directory anyway.
| | ||
| async def _remove_when_done_async(self): | ||
| def run_remove_when_done_thread() -> None: | ||
| return self.remove_when_done(self) |
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 doesn't correspond with the type signature, I think it's fine to pass self, but the Callable in __init__ should match.
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.
hm I think I got this figured out
| def run_remove_when_done_thread() -> None: | ||
| return self.remove_when_done(self) | ||
| | ||
| 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I borrowed almost all of this code from how validation and ThreadedValidators are implemented. I do see that the current implementation appears to have some issue with cleaning up tasks quickly upon completion (you'll see this if you run the a-lot-of-parallel-tasks.py example that I updated).
Would it be better to allow remove_when_done to be either synchronous or asynchronous and then it's up to the user to design any expensive async functions correctly (with asyncio.sleep, etc.)?
| lock = threading.Lock() | ||
| completed = [] | ||
| | ||
| # Remove a completed counter after 5 seconds but keep the last 5. |
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:
@Condition def ten_seconds_passed() -> bool: counter = get_counter() # context local return time.time() - counter.stop_time > 10 @Condition def at_least_five_bars() -> bool: pb = get_progress_bar() # context local return len(pb.counters) > 10 for i in pb(range(total), label=label, remove_when_done=ten_seconds_passed & at_least_five_bars):If this condition evaluates during every render, we can remove the bar when this turns False.
| closing this in favor of #1042 |
Driven by the conversation in #974
Working to add the ability to provide a callable for
remove_when_donewhich is run in the background and once complete, if the return value isTrue, then the counter in question is deleted.Example use case:
@jonathanslenders I'm still trying to wrap my head around Python's async, I don't think I'm understanding tasks correctly so I haven't been able to get this concept to work yet. The above example generates the progress bars as expected and the background tasks appear to be created however the actual removals themselves are not occurring. I'm also getting a RuntimeError when the application attempts to finalize/cleanup:
I've been testing with 3.8.