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.37k stars 716 forks source link

Better way to send input to a prompt programatically #610

Open asmeurer opened 6 years ago

asmeurer commented 6 years ago

I've been using a queue, as described at https://github.com/jonathanslenders/python-prompt-toolkit/issues/519#issuecomment-318978114, to make it so that I can send text to a prompt programatically. For instance, I have a -c startup flag that allows to run a command as the first prompt. I also use this in the bracketed paste handler, as described in that issue.

The way I've done this is to pass items in the command queue through a PipeInput and use that as the input to the cli when there is a queue (and a normal input=None otherwise).

However, I'm not a fan of this approach, because it means that I have to manipulate the input command so that it gets accepted by the cli, for instance, by appending a key code that maps to accept_line.

I would much rather just set the prompt text to the command manually, and have it accept immediately. Is there a way to do this? I basically want to cli.current_buffer.insert_text(command) then have cli.run() bypass the eventloop and do a quick exit.

jonathanslenders commented 6 years ago

Hi @asmeurer,

I forgot to reply to this one. The use case makes sense indeed. I'll consider adding an argument to the Prompt.prompt function for accepting the input right away. If the purpose is to actually render the prompt to the output for feedback, then this does make sense. (Just give me some time for this. I'm preparing the 2.0 release and don't want to push too many new features right now.)

asmeurer commented 6 years ago

Great. Actually I figured out that the overabundance of CPR codes printed to my terminal is mostly coming from me using PipeInput. The stdout Output class basically implicitly assumes that the corresponding Input class is a stdin class. I'm still working on transferring to 2.0, so I can't say yet if this is fixed there. The separation of the two classes does still exist there, as far as I know. To me, this indicates that one should never use PipeInput for interactive applications (it should only be used for unit testing basically). Or if you do use it, you need to stub out the cpr request in the output class simultaneously. So in short, if a feature like this could let me completely eliminate PipeInput from my non-testing code, it should hopefully get rid of the CPR bugs for me.

asmeurer commented 6 years ago

The use case makes sense indeed. I'll consider adding an argument to the Prompt.prompt function for accepting the input right away. If the purpose is to actually render the prompt to the output for feedback, then this does make sense. (Just give me some time for this. I'm preparing the 2.0 release and don't want to push too many new features right now.)

How exactly would you do this? I would implement this myself, but I'm completely stumped on how it would be done. Maybe I'm missing something obvious. The eventloop code is still pretty opaque to me.

jonathanslenders commented 6 years ago

I'll try something today. Are you at PyCon?

Something like this should work (some details are still missing):


$ git diff
diff --git a/prompt_toolkit/shortcuts/prompt.py b/prompt_toolkit/shortcuts/prompt.py
index f5e7530..56eb90f 100644
--- a/prompt_toolkit/shortcuts/prompt.py
+++ b/prompt_toolkit/shortcuts/prompt.py
@@ -692,11 +692,15 @@ class PromptSession(object):
             for name in self._fields:
                 setattr(self, name, backup[name])

+        def pre_run():
+            self.default_buffer.text = 'hello'
+            self.app.exit(result='hello')
+
         def run_sync():
             with self._auto_refresh_context():
                 try:
                     self.default_buffer.reset(Document(self.default))
-                    return self.app.run(inputhook=self.inputhook)
+                    return self.app.run(inputhook=self.inputhook, pre_run=pre_run)
                 finally:
                     restore()
asmeurer commented 6 years ago

No I wasn't able to make it to PyCon this year.

asmeurer commented 6 years ago

That looks like it works. The self.default_buffer.text part is unnecessary (just use the default argument to set the text). Something like

diff --git a/prompt_toolkit/shortcuts/prompt.py b/prompt_toolkit/shortcuts/prompt.py
index 6ba8320..1d2bc3b 100644
--- a/prompt_toolkit/shortcuts/prompt.py
+++ b/prompt_toolkit/shortcuts/prompt.py
@@ -667,7 +667,7 @@ class PromptSession(object):
             reserve_space_for_menu=None, enable_system_prompt=None,
             enable_suspend=None, enable_open_in_editor=None,
             tempfile_suffix=None, inputhook=None,
-            async_=False):
+            async_=False, accept_immedietly=False):
         """
         Display the prompt. All the arguments are the same as for the
         :class:`~.PromptSession` class.
@@ -692,11 +692,15 @@ class PromptSession(object):
             for name in self._fields:
                 setattr(self, name, backup[name])

+        def pre_run():
+            if accept_immedietly:
+                self.app.exit(result=default)
+
         def run_sync():
             with self._auto_refresh_context():
                 try:
                     self.default_buffer.reset(Document(self.default))
-                    return self.app.run(inputhook=self.inputhook)
+                    return self.app.run(inputhook=self.inputhook, pre_run=pre_run)
                 finally:
                     restore()
asmeurer commented 6 years ago

Except the accept_handler is not called. If I try adding self.default_buffer.validate_and_handle() I get

Traceback (most recent call last):
  File "/Users/aaronmeurer/Documents/prompt-toolkit/prompt_toolkit/application/application.py", line 616, in _run_async2
    result = yield f
  File "/Users/aaronmeurer/Documents/prompt-toolkit/prompt_toolkit/eventloop/coroutine.py", line 86, in step_next
    new_f = coroutine.send(None)
  File "/Users/aaronmeurer/Documents/prompt-toolkit/prompt_toolkit/application/application.py", line 510, in _run_async
    self._pre_run(pre_run)
  File "/Users/aaronmeurer/Documents/prompt-toolkit/prompt_toolkit/application/application.py", line 483, in _pre_run
    pre_run()
  File "/Users/aaronmeurer/Documents/prompt-toolkit/prompt_toolkit/shortcuts/prompt.py", line 698, in pre_run
    self.app.exit(result=default)
  File "/Users/aaronmeurer/Documents/prompt-toolkit/prompt_toolkit/application/application.py", line 696, in exit
    raise Exception('Return value already set.')
Exception: Return value already set.
jonathanslenders commented 6 years ago

In that case, self.app.exit doesn't need to be called. There is also a typo in immediately. Further, I think, similar code needs to be added to run_async.

asmeurer commented 6 years ago

Yeah I figured that I misspelled it :) That's probably a sign that the flag should be called something else.

asmeurer commented 6 years ago

Hmm, well validate_and_handle isn't actually what you want, because that adds a pre_run callable to reset the buffer, which would then clear the text when the app is actually run.

I'm trying to wrap my mind around the control flow here and what the right way to do this is.

asmeurer commented 6 years ago

To clarify, it's because _pre_run calls pre_run then the pre_run_callables: https://github.com/jonathanslenders/python-prompt-toolkit/blob/f255927556353e6a0a2339947cceb8bf4d31f30e/prompt_toolkit/application/application.py#L480-L488

This patch "fixes it", but I don't think it's a necessarily a good one.

diff --git a/prompt_toolkit/application/application.py b/prompt_toolkit/application/application.py
index 34ae7ee..e51e623 100644
--- a/prompt_toolkit/application/application.py
+++ b/prompt_toolkit/application/application.py
@@ -483,7 +483,7 @@ class Application(object):
             pre_run()

         # Process registered "pre_run_callables" and clear list.
-        for c in self.pre_run_callables:
+        for c in self.pre_run_callables[:]:
             c()
         del self.pre_run_callables[:]
diff --git a/prompt_toolkit/shortcuts/prompt.py b/prompt_toolkit/shortcuts/prompt.py
index f5e7530..a61e9c3 100644
--- a/prompt_toolkit/shortcuts/prompt.py
+++ b/prompt_toolkit/shortcuts/prompt.py
@@ -667,7 +667,7 @@ class PromptSession(object):
             reserve_space_for_menu=None, enable_system_prompt=None,
             enable_suspend=None, enable_open_in_editor=None,
             tempfile_suffix=None, inputhook=None,
-            async_=False):
+            async_=False, accept_immediately=False):
         """
         Display the prompt. All the arguments are the same as for the
         :class:`~.PromptSession` class.
@@ -692,10 +692,15 @@ class PromptSession(object):
             for name in self._fields:
                 setattr(self, name, backup[name])

+        def pre_run():
+            if accept_immediately:
+                self.default_buffer.validate_and_handle()
+
         def run_sync():
             with self._auto_refresh_context():
                 try:
                     self.default_buffer.reset(Document(self.default))
+                    self.app.pre_run_callables.append(pre_run)
                     return self.app.run(inputhook=self.inputhook)
                 finally:
                     restore()
asmeurer commented 6 years ago

@jonathanslenders any thoughts on this? This is the only thing keeping me from updating to 2.0.

jonathanslenders commented 6 years ago

Yes, this is one of the next things to do. I didn't want to postpone the 2.0 release any longer, but I'm coming back to this issue.

jonathanslenders commented 6 years ago

@asmeurer: Can you have a look at this pull request? https://github.com/jonathanslenders/python-prompt-toolkit/pull/641

It's close to the code that you proposed, with a couple of small changes.

asmeurer commented 6 years ago

I'll take a look.

eode commented 1 year ago

I believe this is resolved, or can at least be closed as stale.