Created
October 7, 2021 21:24
-
-
Save jonathanslenders/758d00ec6aed2b323f08e2e6871325c2 to your computer and use it in GitHub Desktop.
Test prompt_toolkit application
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| """ | |
| A simple example of a calculator program. | |
| This could be used as inspiration for a REPL. | |
| """ | |
| import asyncio | |
| from typing import Tuple | |
| from prompt_toolkit.application import Application, create_app_session | |
| from prompt_toolkit.document import Document | |
| from prompt_toolkit.filters import has_focus | |
| from prompt_toolkit.input import DummyInput | |
| from prompt_toolkit.key_binding import KeyBindings, KeyPress | |
| from prompt_toolkit.keys import Keys | |
| from prompt_toolkit.layout.containers import HSplit, Window | |
| from prompt_toolkit.layout.layout import Layout | |
| from prompt_toolkit.output import DummyOutput | |
| from prompt_toolkit.styles import Style | |
| from prompt_toolkit.widgets import SearchToolbar, TextArea | |
| help_text = """ | |
| Type any expression (e.g. "4 + 4") followed by enter to execute. | |
| Press Control-C to exit. | |
| """ | |
| def create_app() -> Tuple[Application, TextArea]: | |
| # The layout. | |
| search_field = SearchToolbar() # For reverse search. | |
| output_field = TextArea(style="class:output-field", text=help_text) | |
| input_field = TextArea( | |
| height=1, | |
| prompt=">>> ", | |
| style="class:input-field", | |
| multiline=False, | |
| wrap_lines=False, | |
| search_field=search_field, | |
| ) | |
| container = HSplit( | |
| [ | |
| output_field, | |
| Window(height=1, char="-", style="class:line"), | |
| input_field, | |
| search_field, | |
| ] | |
| ) | |
| # Attach accept handler to the input field. We do this by assigning the | |
| # handler to the `TextArea` that we created earlier. it is also possible to | |
| # pass it to the constructor of `TextArea`. | |
| # NOTE: It's better to assign an `accept_handler`, rather then adding a | |
| # custom ENTER key binding. This will automatically reset the input | |
| # field and add the strings to the history. | |
| def accept(buff): | |
| # Evaluate "calculator" expression. | |
| try: | |
| output = "\n\nIn: {}\nOut: {}".format( | |
| input_field.text, eval(input_field.text) | |
| ) # Don't do 'eval' in real code! | |
| except BaseException as e: | |
| output = "\n\n{}".format(e) | |
| new_text = output_field.text + output | |
| # Add text to output buffer. | |
| output_field.buffer.document = Document( | |
| text=new_text, cursor_position=len(new_text) | |
| ) | |
| input_field.accept_handler = accept | |
| # The key bindings. | |
| kb = KeyBindings() | |
| @kb.add("c-c") | |
| @kb.add("c-q") | |
| def _(event): | |
| "Pressing Ctrl-Q or Ctrl-C will exit the user interface." | |
| event.app.exit() | |
| # Style. | |
| style = Style( | |
| [ | |
| ("output-field", "bg:#000044 #ffffff"), | |
| ("input-field", "bg:#000000 #ffffff"), | |
| ("line", "#004400"), | |
| ] | |
| ) | |
| # Run application. | |
| application = Application( | |
| layout=Layout(container, focused_element=input_field), | |
| key_bindings=kb, | |
| style=style, | |
| mouse_support=True, | |
| full_screen=True, | |
| ) | |
| return application, output_field | |
| def main() -> None: | |
| application, output_field = create_app() | |
| application.run() | |
| # This can be a test, marked with `@pytest.mark.asyncio`. | |
| async def _test() -> None: | |
| # Test application in a dummy session. | |
| input = DummyInput() | |
| output = DummyOutput() | |
| with create_app_session(output=output, input=input): | |
| app, output_field = create_app() | |
| # Run the application. | |
| # We run it by scheduling the run_async coroutine in the current event | |
| # loop. | |
| task = asyncio.create_task(app.run_async()) | |
| # Wait for the application to be ready. (There needs to be one `await` | |
| # at least, before the scheduled task is able to run). We wait until | |
| # the `run_async` has progressed to the point that the output was drawn | |
| # for the first time. Then it's sufficiently ready to receive input. | |
| # (If we don't do this, at least the `exit()` at the end will fail.) | |
| # We could also do a short sleep, but that less reliable. | |
| ready_event = asyncio.Event() | |
| app.after_render += lambda _: ready_event.set() | |
| await ready_event.wait() | |
| # Feed input data to the application. | |
| # Note that we don't feed it into the `DummyInput`, because there is no | |
| # `drain()` method there. Instead, we send the keys straight into the | |
| # key processor, which will handle them right away. | |
| # `process_keys` will execute the corresponding key bindings. | |
| app.key_processor.feed(KeyPress("3", "3")) | |
| app.key_processor.feed(KeyPress(Keys.ControlM, "\r")) | |
| app.key_processor.process_keys() | |
| # Test the state of our application. | |
| assert "Out: 3" in output_field.text | |
| # Exit application. | |
| app.key_processor.feed(KeyPress(Keys.ControlC, "\03")) | |
| app.key_processor.process_keys() | |
| # Wait for the application to properly terminate. | |
| await task | |
| print('Test OK') | |
| def test() -> None: | |
| asyncio.run(_test()) | |
| if __name__ == "__main__": | |
| test() | |
| ~ | |
| ~ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment