prompt-toolkit / python-prompt-toolkit

Library for building powerful interactive command line applications in Python
https://python-prompt-toolkit.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
9.4k stars 717 forks source link

Slightly more synchronous completer? #497

Open asmeurer opened 7 years ago

asmeurer commented 7 years ago

Is it possible to make the completer slightly more synchronous. I appreciate that it doesn't block, but sometimes I like to type tab and then more text, or enter, for cases where I know the completion is unique, so that it will fill in the common part automatically. But if you type any text before the completion code finishes, the completion is cancelled and doesn't happen.

I'm probably asking two contradictory things, having it be both nonblocking and synchronous. But maybe instead of an immediate cancel it could use a timeout, so that it could block for a fraction of a second (the actual ideal time would need to be found experimentally), but then be nonblocking after that to avoid issues where the completer hangs.

randy3k commented 7 years ago

I note that the completers are running in other threads asynchronously. I wonder if it is possible to execute them under the main thread. In my use case, my completion functions are written in C and they need to be in the main thread.

class RCompleter(Completer):
    def get_completions(self, document, complete_event):
        # do some calls in C via ctyes.
randy3k commented 7 years ago

Well, I found a dirty trick to disable async code as a whole

    eventloop.run_in_executor = lambda callback: callback()
    eventloop.call_from_executor = lambda callback, _max_postpone_until=None: callback()
jonathanslenders commented 7 years ago

Hi @asmeurer, @randy3k,

There is no option yet to make the completer synchronous (apart from using the readline-line completer. See: https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/examples/autocompletion-like-readline.py )

Right now, you could of course also create a custom key binding for the Tab key.

But it's not the first time that this question is asked. There are some IPython users that wish to have the synchronous completion, because a few libraries fail asynchronous autocompletion with Jedi. In any case, this is on my to-do list.

asmeurer commented 7 years ago

I also found out that it's hard to make the completers timeout, because signals can only be used on the main thread.

jonathanslenders commented 7 years ago

Signals are actually never the right way to add timeouts to any code. Some people use it because it works most of the time, but it's definitely not guaranteed to always work, and sometimes it will break stuff in the most unexpected ways.

Signals handlers are not thread safe and using that to insert an exception in the middle of some code could cause many other bugs. Further, what if this code acquired a lock? Or what if it started another thread on its own? You cannot always use try/finally. And code cannot be expected to deal with exceptions that are not raised by functions it doesn't call.

Also see this tweet from Raymond: https://twitter.com/raymondh/status/673219592815177729 It's also why Rust uses signalfd.

The right way to "kill" a thread is by writing the code in that thread in such a way that the thread itself checks regularly whether it needs to be killed and then does its own clean up. (Note that it doesn't have to be polling: it could be a message queue.)

Back to prompt_toolkit. The completer mechanism is written in a way that it won't start a new completion, until the previous completion is finished. So, there will never be more than one background thread. If the input text was changed by the time that a completer finishes, then it will be restarted to get the latest completions.

For prompt_toolkit 2.0, it is already possible to have both synchronous and asynchronous completions. I still need to add a few examples to demonstrate this, but the implementation is there. By default, the completers are synchronous, but you have to wrap it in a ThreadedCompleter to make it run in a background thread. https://github.com/jonathanslenders/python-prompt-toolkit/blob/2.0/prompt_toolkit/completion.py#L160 If you use the asyncio event loop, then the completer can also be an asyncio coroutine.

asmeurer commented 7 years ago

Does 2.0 still use list(get_completions)? That defeats the purpose of the function being a generator.

Ideally, the behavior I'd like to have is, when I press TAB:

In my code, I have two completers, a dir completer based on rlcompleter that just uses dir and the namespace, and is very fast, and the jedi completer, which is sometimes fast, and sometimes slow. So I want to get the fast completions right away, and if a second or so later I'm still typing and the slow ones come in as well, that's great.

Since this is definitely not possible in pre-2.0, the next best thing for me is to add a timeout to the jedi completer, so that I don't have to wait forever when it is slow. How would you suggest I implement this, given that 2.0 can't currently be used? I've never really done thread programming, and I've only ever implemented timeouts with signals.

jonathanslenders commented 7 years ago

2.0 does still use get_completions. I completely understand your point, but there are several challenges to change to make this work. What is a generator in another thread should map to an asynchronous generator in the main thread. We want the user interface to stay responsive while the completions are coming in.

But, this is actually a very interesting challenge. What we would need is similar to the implementation of the asynchronous generators which we now have in Python 3.6. This approach would also allow us to stop consuming a generator in a clean way if the input changed in the meantime.

So, thanks for the ideas!

edit: at this point and don't have any good suggestion yet for pre-2.0. Maybe I'll come up with something.

dimpase commented 7 years ago

IMHO asynchronous completion is OK if it does not involve importing/initialising modules. In particular, initialising an extension which is not thread-safe (not in sense of GIL, but in the ordinary POSIX threads sense) might lead to crashes.

It is puzzling to me that this appear to be console-only crashes; the same code run in a Jupyter notebook simply works. Does this mean that in the notebook everything is synchronous?

asmeurer commented 6 years ago

The new async completer in 2.0 is awesome. It makes my dir completer actually useful, as it always gives completions instantly, and I can either wait for Jedi or keep going.

Some way to tell in the UI if the completions are all in, and to differentiate between a slow completer and no completions would be nice. I suppose I could work around it by yielding some kind of null completion at the end of my completion function, so that I always know when it's done. I would suggest ringing the bell when there are no completions (for pressing TAB, not complete while typing), and perhaps some sort of small loading indicator on the completions UI when they are still loading, maybe only after some period of some (like a second).

asmeurer commented 6 years ago

I see there's an example that uses the bottom toolbar to show a loading indicator. So that's not hard to do apparently. Though I would add it to the completions popup, not in a bottom toolbar.

jonathanslenders commented 6 years ago

Yes, agreed that it would be nice to show it in the popup. I'm not sure I want to add it for this release, but it definitely makes sense. Regarding that example, I'm also not 100% sure that this implementation is actually correct, probably I bit more testing needs to be done here.

randy3k commented 6 years ago

@asmeurer Ya, I have also switched to v2.0, it's awesome. Thank @jonathanslenders for the amazing work.