Skip to content

Instantly share code, notes, and snippets.

@jonathanslenders
Created October 7, 2021 21:24
Show Gist options
  • Select an option

  • Save jonathanslenders/758d00ec6aed2b323f08e2e6871325c2 to your computer and use it in GitHub Desktop.

Select an option

Save jonathanslenders/758d00ec6aed2b323f08e2e6871325c2 to your computer and use it in GitHub Desktop.
Test prompt_toolkit application
#!/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