Skip to content

Module quickpython.cli

View Source
import asyncio

import builtins

import os

import pydoc

import sys

import types

from asyncio import Future, ensure_future

from datetime import datetime

from functools import partial

from pathlib import Path

from typing import Optional

import black

import isort

from prompt_toolkit import Application

from prompt_toolkit.completion import PathCompleter

from prompt_toolkit.filters import Condition

from prompt_toolkit.formatted_text import AnyFormattedText, Template

from prompt_toolkit.key_binding.key_bindings import KeyBindings

from prompt_toolkit.layout.containers import (

    AnyContainer,

    ConditionalContainer,

    Container,

    DynamicContainer,

    Float,

    HSplit,

    VSplit,

    Window,

)

from prompt_toolkit.layout.dimension import AnyDimension, Dimension

from prompt_toolkit.layout.layout import Layout

from prompt_toolkit.layout.menus import CompletionsMenu

from prompt_toolkit.output.color_depth import ColorDepth

from prompt_toolkit.search import start_search

from prompt_toolkit.shortcuts import clear, message_dialog

from prompt_toolkit.styles import Style

from prompt_toolkit.widgets import (

    Dialog,

    MenuContainer,

    MenuItem,

    SearchToolbar,

    TextArea,

)

from prompt_toolkit.widgets.base import Border, Button, Label

from quickpython import __version__, extensions

ABOUT_MESSAGE = f"""QuickPython version {__version__}

Copyright (c) 2020 Timothy Crosley. Few rights reserved. MIT Licensed.

Simultanously distributed to the US and Canada.

And you know, the rest of the world.

A productive parody.

Made in Seattle.

"""

kb = KeyBindings()

eb = KeyBindings()

current_file: Optional[Path] = None

default_isort_config = isort.Config(settings_path=os.getcwd())

if default_isort_config == isort.settings.DEFAULT_CONFIG:

    default_isort_config = isort.Config(profile="black", float_to_top=True)

default_black_config_file = black.find_pyproject_toml((os.getcwd(),))

if default_black_config_file:

    default_black_config = black.parse_pyproject_toml(default_black_config_file)

else:

    default_black_config = {}

default_black_config = {}

isort_config: isort.Config = default_isort_config

black_config: dict = default_black_config

code_frame_style = Style.from_dict({"frame.label": "bg:#AAAAAA fg:#0000aa"})

style = Style.from_dict(

    {

        "menu-bar": "bg:#aaaaaa black bold",

        "menu-bar.selected-item": "bg:black #aaaaaa bold",

        "menu": "bg:#aaaaaa black bold",

        "menu.border shadow": "black",

        "shadow": "bg:black",

        "dialog": "bg:#0000AA",

        "frame.label": "fg:#AAAAAA bold",

        "dialog frame.label": "fg:black bold bg:#AAAAAA",

        "code-frame frame.label": "bg:#AAAAAA fg:#0000aa",

        "dialog.body": "bg:#AAAAAA fg:#000000",

        "dialog shadow": "bg:#000000",

        "scrollbar.background": "bg:#AAAAAA",

        "scrollbar.button": "bg:black fg:black",

        "scrollbar.arrow": "bg:#AAAAAA fg:black bold",

        "": "bg:#0000AA fg:#AAAAAA bold",

    }

)

@kb.add("escape")

def _(event):

    """Focus the menu"""

    if event.app.layout.has_focus(root_container.window):

        event.app.layout.focus(code)

    else:

        event.app.layout.focus(root_container.window)

def format_code(contents: str) -> str:

    return black_format_code(isort_format_code(contents))

def isort_format_code(contents: str) -> str:

    return isort.code(contents, config=isort_config)

class TextInputDialog:

    def __init__(self, title="", label_text="", completer=None):

        self.future = Future()

        def accept_text(buf):

            app.layout.focus(ok_button)

            buf.complete_state = None

            return True

        def accept():

            self.future.set_result(self.text_area.text)

        def cancel():

            self.future.set_result(None)

        self.text_area = TextArea(

            completer=completer,

            multiline=False,

            width=Dimension(preferred=40),

            accept_handler=accept_text,

        )

        ok_button = Button(text="OK", handler=accept)

        cancel_button = Button(text="Cancel", handler=cancel)

        self.dialog = Dialog(

            title=title,

            body=HSplit([Label(text=label_text), self.text_area]),

            buttons=[ok_button, cancel_button],

            width=Dimension(preferred=80),

            modal=True,

        )

    def __pt_container__(self):

        return self.dialog

class MessageDialog:

    def __init__(self, title, text):

        self.future = Future()

        def set_done():

            self.future.set_result(None)

        ok_button = Button(text="OK", handler=(lambda: set_done()))

        self.dialog = Dialog(

            title=title,

            body=HSplit([Label(text=text)]),

            buttons=[ok_button],

            width=Dimension(preferred=80),

            modal=True,

        )

    def __pt_container__(self):

        return self.dialog

async def show_dialog_as_float(dialog):

    " Coroutine. "

    float_ = Float(content=dialog)

    root_container.floats.insert(0, float_)

    focused_before = app.layout.current_window

    app.layout.focus(dialog)

    result = await dialog.future

    app.layout.focus(focused_before)

    if float_ in root_container.floats:

        root_container.floats.remove(float_)

    return result

@kb.add("c-o")

def open_file(event=None):

    async def coroutine():

        global current_file

        global isort_config

        global black_config

        open_dialog = TextInputDialog(

            title="Open file",

            label_text="Enter the path of a file:",

            completer=PathCompleter(),

        )

        filename = await show_dialog_as_float(open_dialog)

        if filename is not None:

            current_file = Path(filename).resolve()

            isort_config = isort.Config(settings_path=current_file.parent)

            black_config_file = black.find_pyproject_toml((current_file,))

            if black_config_file:

                black_config = black.parse_pyproject_toml(black_config_file)

            else:

                black_config = {}

            try:

                with open(current_file, "r", encoding="utf8") as new_file_conent:

                    code.buffer.text = new_file_conent.read()

                    open_file_frame.title = current_file.name

                feedback(f"Successfully opened {current_file}")

            except IOError as error:

                feedback(f"Error: {error}")

    ensure_future(coroutine())

def save_as_file():

    async def coroutine():

        global current_file

        global isort_config

        global black_config

        save_dialog = TextInputDialog(

            title="Save file",

            label_text="Enter the path of a file:",

            completer=PathCompleter(),

        )

        filename = await show_dialog_as_float(save_dialog)

        if filename is not None:

            current_file = Path(filename).resolve()

            isort_config = isort.Config(settings_path=current_file.parent)

            black_config_file = black.find_pyproject_toml((current_file,))

            if black_config_file:

                black_config = black.parse_pyproject_toml(black_config_file)

            else:

                black_config = {}

            if not current_file.suffixes and not current_file.exists():

                current_file = current_file.with_suffix(".py")

            open_file_frame.title = current_file.name

            save_file()

    ensure_future(coroutine())

def feedback(text):

    immediate.buffer.text = text

def black_format_code(contents: str) -> str:

    """Formats the given import section using black."""

    try:

        immediate.buffer.text = ""

        return black.format_file_contents(

            contents, fast=True, mode=black.FileMode(**black_config)

        )

    except black.NothingChanged:

        return contents

    except Exception as error:

        immediate.buffer.text = str(error)

        return contents

def new(content=""):

    """Creates a new file buffer."""

    global current_file

    global isort_config

    global black_config

    current_file = None

    isort_config = default_isort_config

    black_config = default_black_config

    code.buffer.text = content

    open_file_frame.title = "Untitled"

    feedback("")

@kb.add("c-q")

def exit(event=None):

    """Triggers the request to close QPython cleanly."""

    app.exit()

@Condition

def is_code_focused() -> bool:

    return app.layout.has_focus(code)

@kb.add("tab")

def indent(event):

    event.app.current_buffer.insert_text("    ")

@kb.add("enter", filter=is_code_focused)

def enter(event):

    buffer = event.app.current_buffer

    buffer.insert_text("\n")

    if (

        current_file

        and ".py" not in current_file.suffixes

        and ".pyi" not in current_file.suffixes

    ):

        return

    old_cursor_position = buffer.cursor_position

    if old_cursor_position == 0:

        return

    end_position = buffer.text.rfind("\n", 0, old_cursor_position) + 1

    code, rest = buffer.text[:end_position], buffer.text[end_position:]

    if len(code) < 2 or (code[-1] == "\n" and code[-2] == "\n"):

        return

    formatted_code = format_code(code)

    difference = len(formatted_code) - len(code)

    buffer.text = formatted_code + rest

    buffer.cursor_position = old_cursor_position + difference

@kb.add("c-s")

def save_file(event=None):

    if not current_file:

        save_as_file()

        return

    buffer = app.current_buffer

    buffer.text = format_code(buffer.text)

    current_file.write_text(buffer.text, encoding="utf8")

    immediate.buffer.text = f"Successfully saved {current_file}"

async def _run_buffer(debug: bool = False):

    buffer_filename = f"{current_file or 'buffer'}.qpython"

    with open(buffer_filename, "w") as buffer_file:

        user_code = app.current_buffer.text

        if not user_code.endswith("\n"):

            user_code += "\n"

        with_qpython_injected = isort.code(

            user_code, add_imports=["import quickpython.extensions"]

        )

        buffer_file.write(isort_format_code(with_qpython_injected))

        if debug:

            buffer_file.write("breakpoint()")

    try:

        clear()

        await app.run_system_command(

            f'PYTHONBREAKPOINT=ipdb.set_trace {sys.executable} "{buffer_filename}"'

        )

    finally:

        os.remove(buffer_filename)

async def _view_buffer():

    clear()

    await app.run_system_command("echo ''")

@kb.add("c-r")

@kb.add("f5")

def run_buffer(event=None):

    asyncio.ensure_future(_run_buffer())

def debug():

    asyncio.ensure_future(_run_buffer(debug=True))

def view_buffer(event=None):

    asyncio.ensure_future(_view_buffer())

@kb.add("c-z")

def undo(event=None):

    code.buffer.undo()

def cut():

    data = code.buffer.cut_selection()

    app.clipboard.set_data(data)

def copy():

    data = code.buffer.copy_selection()

    app.clipboard.set_data(data)

def delete():

    code.buffer.cut_selection()

def paste():

    code.buffer.paste_clipboard_data(app.clipboard.get_data())

@kb.add("c-a")

def select_all(event=None):

    code.buffer.cursor_position = 0

    code.buffer.start_selection()

    code.buffer.cursor_position = len(code.buffer.text)

def insert_time_and_date():

    code.buffer.insert_text(datetime.now().isoformat())

search_toolbar = SearchToolbar()

code = TextArea(

    scrollbar=True,

    wrap_lines=False,

    focus_on_click=True,

    line_numbers=True,

    search_field=search_toolbar,

)

code.window.right_margins[0].up_arrow_symbol = "↑"  # type: ignore

code.window.right_margins[0].down_arrow_symbol = "↓"  # type: ignore

class CodeFrame:

    """A custom frame for the quick python code container to match desired styling"""

    def __init__(

        self,

        body: AnyContainer,

        title: AnyFormattedText = "",

        style: str = "",

        width: AnyDimension = None,

        height: AnyDimension = None,

        key_bindings: Optional[KeyBindings] = None,

        modal: bool = False,

    ) -> None:

        self.title = title

        self.body = body

        fill = partial(Window, style="class:frame.border")

        style = "class:frame " + style

        top_row_with_title = VSplit(

            [

                fill(width=1, height=1, char=Border.TOP_LEFT),

                fill(char=Border.HORIZONTAL),

                Label(

                    lambda: Template(" {} ").format(self.title),

                    style="class:frame.label",

                    dont_extend_width=True,

                ),

                fill(char=Border.HORIZONTAL),

                fill(width=1, height=1, char=Border.TOP_RIGHT),

            ],

            height=1,

        )

        top_row_without_title = VSplit(

            [

                fill(width=1, height=1, char=Border.TOP_LEFT),

                fill(char=Border.HORIZONTAL),

                fill(width=1, height=1, char=Border.TOP_RIGHT),

            ],

            height=1,

        )

        @Condition

        def has_title() -> bool:

            return bool(self.title)

        self.container = HSplit(

            [

                ConditionalContainer(content=top_row_with_title, filter=has_title),

                ConditionalContainer(content=top_row_without_title, filter=~has_title),

                VSplit(

                    [

                        fill(width=1, char=Border.VERTICAL),

                        DynamicContainer(lambda: self.body),

                        fill(width=1, char=Border.VERTICAL),

                        # Padding is required to make sure that if the content is

                        # too small, the right frame border is still aligned.

                    ],

                    padding=0,

                ),

            ],

            width=width,

            height=height,

            style=style,

            key_bindings=key_bindings,

            modal=modal,

        )

    def __pt_container__(self) -> Container:

        return self.container

class ImmediateFrame:

    """

    Draw a border around any container, optionally with a title text.

    Changing the title and body of the frame is possible at runtime by

    assigning to the `body` and `title` attributes of this class.

    :param body: Another container object.

    :param title: Text to be displayed in the top of the frame (can be formatted text).

    :param style: Style string to be applied to this widget.

    """

    def __init__(

        self,

        body: AnyContainer,

        title: AnyFormattedText = "",

        style: str = "",

        width: AnyDimension = None,

        height: AnyDimension = None,

        key_bindings: Optional[KeyBindings] = None,

        modal: bool = False,

    ) -> None:

        self.title = title

        self.body = body

        fill = partial(Window, style="class:frame.border")

        style = "class:frame " + style

        top_row_with_title = VSplit(

            [

                fill(width=1, height=1, char="├"),

                fill(char=Border.HORIZONTAL),

                # Notice: we use `Template` here, because `self.title` can be an

                # `HTML` object for instance.

                Label(

                    lambda: Template(" {} ").format(self.title),

                    style="class:frame.label",

                    dont_extend_width=True,

                ),

                fill(char=Border.HORIZONTAL),

                fill(width=1, height=1, char="┤"),

            ],

            height=1,

        )

        top_row_without_title = VSplit(

            [

                fill(width=1, height=1, char=Border.TOP_LEFT),

                fill(char=Border.HORIZONTAL),

                fill(width=1, height=1, char=Border.TOP_RIGHT),

            ],

            height=1,

        )

        @Condition

        def has_title() -> bool:

            return bool(self.title)

        self.container = HSplit(

            [

                ConditionalContainer(content=top_row_with_title, filter=has_title),

                ConditionalContainer(content=top_row_without_title, filter=~has_title),

                VSplit(

                    [

                        fill(width=1, char=Border.VERTICAL),

                        DynamicContainer(lambda: self.body),

                        fill(width=1, char=Border.VERTICAL),

                        # Padding is required to make sure that if the content is

                        # too small, the right frame border is still aligned.

                    ],

                    padding=0,

                ),

            ],

            width=width,

            height=height,

            style=style,

            key_bindings=key_bindings,

            modal=modal,

        )

    def __pt_container__(self) -> Container:

        return self.container

open_file_frame = CodeFrame(

    HSplit(

        [

            # One window that holds the BufferControl with the default buffer on

            # the left.

            code,

            # A vertical line in the middle. We explicitly specify the width, to

            # make sure that the layout engine will not try to divide the whole

            # width by three for all these windows. The window will simply fill its

            # content by repeating this character.

        ],

    ),

    title="Untitled",

    style="class:code-frame",

)

@kb.add("c-g")

def goto(event=None):

    async def coroutine():

        dialog = TextInputDialog(title="Go to line", label_text="Line number:")

        line_number = await show_dialog_as_float(dialog)

        if line_number is None:

            return

        try:

            line_number = int(line_number)

        except ValueError:

            feedback("Invalid line number")

        else:

            code.buffer.cursor_position = (

                code.buffer.document.translate_row_col_to_index(line_number - 1, 0)

            )

    ensure_future(coroutine())

def replace_text():

    async def coroutine():

        to_replace_dialog = TextInputDialog(

            title="Text to Replace", label_text="original:"

        )

        replacement_dialog = TextInputDialog(

            title="Replace With", label_text="replacement:"

        )

        to_replace = await show_dialog_as_float(to_replace_dialog)

        if to_replace is None:

            return

        replacement = await show_dialog_as_float(replacement_dialog)

        if replacement is None:

            return

        code.buffer.text = format_code(

            code.buffer.text.replace(to_replace, replacement)

        )

    ensure_future(coroutine())

def about():

    async def coroutine():

        await show_dialog_as_float(MessageDialog("About QuickPython", ABOUT_MESSAGE))

    ensure_future(coroutine())

def add_function():

    async def coroutine():

        dialog = TextInputDialog(title="Add Function", label_text="Function name:")

        function_name = await show_dialog_as_float(dialog)

        if not function_name:

            return

        code.buffer.insert_text(

            f"""

def {function_name}():

    pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

def add_class():

    async def coroutine():

        dialog = TextInputDialog(title="Add Class", label_text="Class name:")

        class_name = await show_dialog_as_float(dialog)

        if not class_name:

            return

        code.buffer.insert_text(

            f"""

class {class_name}:

    def __init__(self):

        pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

def add_data_class():

    async def coroutine():

        dialog = TextInputDialog(title="Add Data Class", label_text="Class name:")

        class_name = await show_dialog_as_float(dialog)

        if not class_name:

            return

        code.buffer.insert_text(

            f'''

@dataclass

class {class_name}:

    """Comment"""

'''

        )

        code.buffer.text = isort.code(

            code.buffer.text,

            add_imports=["from dataclasses import dataclass"],

            float_to_top=True,

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

def add_method():

    async def coroutine():

        dialog = TextInputDialog(title="Add Method", label_text="Method name:")

        method_name = await show_dialog_as_float(dialog)

        if not method_name:

            return

        code.buffer.insert_text(

            f"""

    def {method_name}(self):

        pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

def add_static_method():

    async def coroutine():

        dialog = TextInputDialog(title="Add Static Method", label_text="Method name:")

        method_name = await show_dialog_as_float(dialog)

        if not method_name:

            return

        code.buffer.insert_text(

            f"""

    @staticmethod

    def {method_name}():

        pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

def add_class_method():

    async def coroutine():

        dialog = TextInputDialog(title="Add Class Method", label_text="Method name:")

        method_name = await show_dialog_as_float(dialog)

        if not method_name:

            return

        code.buffer.insert_text(

            f"""

    @classmethod

    def {method_name}(cls):

        pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

@kb.add("c-f")

def search(event=None):

    start_search(code.control)

def search_next(event=None):

    search_state = app.current_search_state

    cursor_position = code.buffer.get_search_position(

        search_state, include_current_position=False

    )

    code.buffer.cursor_position = cursor_position

def example(game_name: str):

    import inspect

    from quickpython import examples

    def expand_example():

        new(inspect.getsource(getattr(examples, game_name)))

    return expand_example

def built_in_functions():

    docs = [

        pydoc.render_doc(builtin, renderer=pydoc.plaintext).split("\n", 1)[1]

        for builtin_name, builtin in vars(builtins).items()

        if type(builtin) in (types.FunctionType, types.BuiltinFunctionType)

        and not builtin_name.startswith("_")

    ]

    new("\n".join(docs))

QLabel = partial(Label, dont_extend_width=True)

SPACE = QLabel(" ")

immediate = TextArea()

root_container = MenuContainer(

    body=HSplit(

        [

            open_file_frame,

            search_toolbar,

            ImmediateFrame(

                immediate,

                title="Immediate",

                height=5,

                style="fg:#AAAAAA bold",

            ),

            VSplit(

                [

                    QLabel("<F1=Help>"),

                    SPACE,

                    QLabel("<F5=Run>"),

                    SPACE,

                    QLabel("<CTRL+R=Run>"),

                ],

                style="bg:#00AAAA fg:white bold",

                height=1,

            ),

        ]

    ),

    menu_items=[

        MenuItem(

            " File ",

            children=[

                MenuItem("New...", handler=new),

                MenuItem("Open...", handler=open_file),

                MenuItem("Save", handler=save_file),

                MenuItem("Save as...", handler=save_as_file),

                MenuItem("-", disabled=True),

                MenuItem("Exit", handler=exit),

            ],

        ),

        MenuItem(

            " Edit ",

            children=[

                MenuItem("Undo", handler=undo),

                MenuItem("Cut", handler=cut),

                MenuItem("Copy", handler=copy),

                MenuItem("Paste", handler=paste),

                MenuItem("Delete", handler=delete),

                MenuItem("-", disabled=True),

                MenuItem("Go To", handler=goto),

                MenuItem("Select All", handler=select_all),

                MenuItem("Add Time/Date", handler=insert_time_and_date),

                MenuItem("-", disabled=True),

                MenuItem("New Function", handler=add_function),

                MenuItem("New Class", handler=add_class),

                MenuItem("New Data Class", handler=add_data_class),

                MenuItem("New Method", handler=add_method),

                MenuItem("New Static Method", handler=add_static_method),

                MenuItem("New Class Method", handler=add_class_method),

            ],

        ),

        MenuItem(

            " View ",

            children=[MenuItem("Output Screen", handler=view_buffer)],

        ),

        MenuItem(

            " Search ",

            children=[

                MenuItem("Find (CTRL+F)", handler=search),

                MenuItem("Repeat last find", handler=search_next),

                MenuItem("Change", handler=replace_text),

            ],

        ),

        MenuItem(

            " Run ",

            children=[

                MenuItem("Start (F5)", handler=run_buffer),

                MenuItem("Debug", handler=debug),

            ],

        ),

        MenuItem(

            " Examples ",

            children=[

                MenuItem("Connect", handler=example("connect")),

                MenuItem("Eight Puzzle", handler=example("eightpuzzle")),

                MenuItem("Hang Man", handler=example("hangman")),

                MenuItem("Memory", handler=example("memory")),

                MenuItem("Minesweeper", handler=example("minesweeper")),

                MenuItem("Simon", handler=example("simon")),

                MenuItem("Tic Tac Toe", handler=example("tictactoe")),

                MenuItem("Towers", handler=example("towers")),

                MenuItem("Zig Zag", handler=example("zigzag")),

                MenuItem("Uno", handler=example("uno")),

            ],

        ),

        MenuItem(

            " Help ",

            children=[

                MenuItem("About", handler=about),

                MenuItem("Built-in Functions", handler=built_in_functions),

            ],

        ),

    ],

    floats=[

        Float(

            xcursor=True,

            ycursor=True,

            content=CompletionsMenu(max_height=16, scroll_offset=1),

        ),

    ],

    key_bindings=kb,

)

layout = Layout(root_container)

app: Application = Application(

    layout=layout,

    full_screen=True,

    mouse_support=True,

    style=style,

    enable_page_navigation_bindings=True,

    color_depth=ColorDepth.DEPTH_8_BIT,

)

def start(argv=None):

    global current_file

    global isort_config

    argv = sys.argv if argv is None else argv

    if len(sys.argv) > 2:

        sys.exit("Usage: qpython [filename]")

    elif len(sys.argv) == 2:

        current_file = Path(sys.argv[1]).resolve()

        isort_config = isort.Config(settings_path=current_file.parent)

        open_file_frame.title = current_file.name

        if current_file.exists():

            with current_file.open(encoding="utf8") as open_file:

                code.buffer.text = open_file.read()

    else:

        message_dialog(

            title="Welcome to",

            text=ABOUT_MESSAGE,

            style=style,

        ).run()

    app.layout.focus(code.buffer)

    app.run()

if __name__ == "__main__":

    start()

Variables

ABOUT_MESSAGE
QLabel
SPACE
black_config
code
code_frame_style
current_file
default_black_config
default_black_config_file
default_isort_config
eb
immediate
kb
layout
open_file_frame
root_container
search_toolbar
style

Functions

about

def about(

)
View Source
def about():

    async def coroutine():

        await show_dialog_as_float(MessageDialog("About QuickPython", ABOUT_MESSAGE))

    ensure_future(coroutine())

add_class

def add_class(

)
View Source
def add_class():

    async def coroutine():

        dialog = TextInputDialog(title="Add Class", label_text="Class name:")

        class_name = await show_dialog_as_float(dialog)

        if not class_name:

            return

        code.buffer.insert_text(

            f"""

class {class_name}:

    def __init__(self):

        pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

add_class_method

def add_class_method(

)
View Source
def add_class_method():

    async def coroutine():

        dialog = TextInputDialog(title="Add Class Method", label_text="Method name:")

        method_name = await show_dialog_as_float(dialog)

        if not method_name:

            return

        code.buffer.insert_text(

            f"""

    @classmethod

    def {method_name}(cls):

        pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

add_data_class

def add_data_class(

)
View Source
def add_data_class():

    async def coroutine():

        dialog = TextInputDialog(title="Add Data Class", label_text="Class name:")

        class_name = await show_dialog_as_float(dialog)

        if not class_name:

            return

        code.buffer.insert_text(

            f'''

@dataclass

class {class_name}:

    """Comment"""

'''

        )

        code.buffer.text = isort.code(

            code.buffer.text,

            add_imports=["from dataclasses import dataclass"],

            float_to_top=True,

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

add_function

def add_function(

)
View Source
def add_function():

    async def coroutine():

        dialog = TextInputDialog(title="Add Function", label_text="Function name:")

        function_name = await show_dialog_as_float(dialog)

        if not function_name:

            return

        code.buffer.insert_text(

            f"""

def {function_name}():

    pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

add_method

def add_method(

)
View Source
def add_method():

    async def coroutine():

        dialog = TextInputDialog(title="Add Method", label_text="Method name:")

        method_name = await show_dialog_as_float(dialog)

        if not method_name:

            return

        code.buffer.insert_text(

            f"""

    def {method_name}(self):

        pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

add_static_method

def add_static_method(

)
View Source
def add_static_method():

    async def coroutine():

        dialog = TextInputDialog(title="Add Static Method", label_text="Method name:")

        method_name = await show_dialog_as_float(dialog)

        if not method_name:

            return

        code.buffer.insert_text(

            f"""

    @staticmethod

    def {method_name}():

        pass

"""

        )

        code.buffer.text = format_code(code.buffer.text)

    ensure_future(coroutine())

black_format_code

def black_format_code(
    contents: str
) -> str

Formats the given import section using black.

View Source
def black_format_code(contents: str) -> str:

    """Formats the given import section using black."""

    try:

        immediate.buffer.text = ""

        return black.format_file_contents(

            contents, fast=True, mode=black.FileMode(**black_config)

        )

    except black.NothingChanged:

        return contents

    except Exception as error:

        immediate.buffer.text = str(error)

        return contents

built_in_functions

def built_in_functions(

)
View Source
def built_in_functions():

    docs = [

        pydoc.render_doc(builtin, renderer=pydoc.plaintext).split("\n", 1)[1]

        for builtin_name, builtin in vars(builtins).items()

        if type(builtin) in (types.FunctionType, types.BuiltinFunctionType)

        and not builtin_name.startswith("_")

    ]

    new("\n".join(docs))

copy

def copy(

)
View Source
def copy():

    data = code.buffer.copy_selection()

    app.clipboard.set_data(data)

cut

def cut(

)
View Source
def cut():

    data = code.buffer.cut_selection()

    app.clipboard.set_data(data)

debug

def debug(

)
View Source
def debug():

    asyncio.ensure_future(_run_buffer(debug=True))

delete

def delete(

)
View Source
def delete():

    code.buffer.cut_selection()

enter

def enter(
    event
)
View Source
@kb.add("enter", filter=is_code_focused)

def enter(event):

    buffer = event.app.current_buffer

    buffer.insert_text("\n")

    if (

        current_file

        and ".py" not in current_file.suffixes

        and ".pyi" not in current_file.suffixes

    ):

        return

    old_cursor_position = buffer.cursor_position

    if old_cursor_position == 0:

        return

    end_position = buffer.text.rfind("\n", 0, old_cursor_position) + 1

    code, rest = buffer.text[:end_position], buffer.text[end_position:]

    if len(code) < 2 or (code[-1] == "\n" and code[-2] == "\n"):

        return

    formatted_code = format_code(code)

    difference = len(formatted_code) - len(code)

    buffer.text = formatted_code + rest

    buffer.cursor_position = old_cursor_position + difference

example

def example(
    game_name: str
)
View Source
def example(game_name: str):

    import inspect

    from quickpython import examples

    def expand_example():

        new(inspect.getsource(getattr(examples, game_name)))

    return expand_example

exit

def exit(
    event=None
)

Triggers the request to close QPython cleanly.

View Source
@kb.add("c-q")

def exit(event=None):

    """Triggers the request to close QPython cleanly."""

    app.exit()

feedback

def feedback(
    text
)
View Source
def feedback(text):

    immediate.buffer.text = text

format_code

def format_code(
    contents: str
) -> str
View Source
def format_code(contents: str) -> str:

    return black_format_code(isort_format_code(contents))

goto

def goto(
    event=None
)
View Source
@kb.add("c-g")

def goto(event=None):

    async def coroutine():

        dialog = TextInputDialog(title="Go to line", label_text="Line number:")

        line_number = await show_dialog_as_float(dialog)

        if line_number is None:

            return

        try:

            line_number = int(line_number)

        except ValueError:

            feedback("Invalid line number")

        else:

            code.buffer.cursor_position = (

                code.buffer.document.translate_row_col_to_index(line_number - 1, 0)

            )

    ensure_future(coroutine())

indent

def indent(
    event
)
View Source
@kb.add("tab")

def indent(event):

    event.app.current_buffer.insert_text("    ")

insert_time_and_date

def insert_time_and_date(

)
View Source
def insert_time_and_date():

    code.buffer.insert_text(datetime.now().isoformat())

isort_format_code

def isort_format_code(
    contents: str
) -> str
View Source
def isort_format_code(contents: str) -> str:

    return isort.code(contents, config=isort_config)

new

def new(
    content=''
)

Creates a new file buffer.

View Source
def new(content=""):

    """Creates a new file buffer."""

    global current_file

    global isort_config

    global black_config

    current_file = None

    isort_config = default_isort_config

    black_config = default_black_config

    code.buffer.text = content

    open_file_frame.title = "Untitled"

    feedback("")

open_file

def open_file(
    event=None
)
View Source
@kb.add("c-o")

def open_file(event=None):

    async def coroutine():

        global current_file

        global isort_config

        global black_config

        open_dialog = TextInputDialog(

            title="Open file",

            label_text="Enter the path of a file:",

            completer=PathCompleter(),

        )

        filename = await show_dialog_as_float(open_dialog)

        if filename is not None:

            current_file = Path(filename).resolve()

            isort_config = isort.Config(settings_path=current_file.parent)

            black_config_file = black.find_pyproject_toml((current_file,))

            if black_config_file:

                black_config = black.parse_pyproject_toml(black_config_file)

            else:

                black_config = {}

            try:

                with open(current_file, "r", encoding="utf8") as new_file_conent:

                    code.buffer.text = new_file_conent.read()

                    open_file_frame.title = current_file.name

                feedback(f"Successfully opened {current_file}")

            except IOError as error:

                feedback(f"Error: {error}")

    ensure_future(coroutine())

paste

def paste(

)
View Source
def paste():

    code.buffer.paste_clipboard_data(app.clipboard.get_data())

replace_text

def replace_text(

)
View Source
def replace_text():

    async def coroutine():

        to_replace_dialog = TextInputDialog(

            title="Text to Replace", label_text="original:"

        )

        replacement_dialog = TextInputDialog(

            title="Replace With", label_text="replacement:"

        )

        to_replace = await show_dialog_as_float(to_replace_dialog)

        if to_replace is None:

            return

        replacement = await show_dialog_as_float(replacement_dialog)

        if replacement is None:

            return

        code.buffer.text = format_code(

            code.buffer.text.replace(to_replace, replacement)

        )

    ensure_future(coroutine())

run_buffer

def run_buffer(
    event=None
)
View Source
@kb.add("c-r")

@kb.add("f5")

def run_buffer(event=None):

    asyncio.ensure_future(_run_buffer())

save_as_file

def save_as_file(

)
View Source
def save_as_file():

    async def coroutine():

        global current_file

        global isort_config

        global black_config

        save_dialog = TextInputDialog(

            title="Save file",

            label_text="Enter the path of a file:",

            completer=PathCompleter(),

        )

        filename = await show_dialog_as_float(save_dialog)

        if filename is not None:

            current_file = Path(filename).resolve()

            isort_config = isort.Config(settings_path=current_file.parent)

            black_config_file = black.find_pyproject_toml((current_file,))

            if black_config_file:

                black_config = black.parse_pyproject_toml(black_config_file)

            else:

                black_config = {}

            if not current_file.suffixes and not current_file.exists():

                current_file = current_file.with_suffix(".py")

            open_file_frame.title = current_file.name

            save_file()

    ensure_future(coroutine())

save_file

def save_file(
    event=None
)
View Source
@kb.add("c-s")

def save_file(event=None):

    if not current_file:

        save_as_file()

        return

    buffer = app.current_buffer

    buffer.text = format_code(buffer.text)

    current_file.write_text(buffer.text, encoding="utf8")

    immediate.buffer.text = f"Successfully saved {current_file}"
def search(
    event=None
)
View Source
@kb.add("c-f")

def search(event=None):

    start_search(code.control)

search_next

def search_next(
    event=None
)
View Source
def search_next(event=None):

    search_state = app.current_search_state

    cursor_position = code.buffer.get_search_position(

        search_state, include_current_position=False

    )

    code.buffer.cursor_position = cursor_position

select_all

def select_all(
    event=None
)
View Source
@kb.add("c-a")

def select_all(event=None):

    code.buffer.cursor_position = 0

    code.buffer.start_selection()

    code.buffer.cursor_position = len(code.buffer.text)

show_dialog_as_float

def show_dialog_as_float(
    dialog
)

Coroutine.

View Source
async def show_dialog_as_float(dialog):

    " Coroutine. "

    float_ = Float(content=dialog)

    root_container.floats.insert(0, float_)

    focused_before = app.layout.current_window

    app.layout.focus(dialog)

    result = await dialog.future

    app.layout.focus(focused_before)

    if float_ in root_container.floats:

        root_container.floats.remove(float_)

    return result

start

def start(
    argv=None
)
View Source
def start(argv=None):

    global current_file

    global isort_config

    argv = sys.argv if argv is None else argv

    if len(sys.argv) > 2:

        sys.exit("Usage: qpython [filename]")

    elif len(sys.argv) == 2:

        current_file = Path(sys.argv[1]).resolve()

        isort_config = isort.Config(settings_path=current_file.parent)

        open_file_frame.title = current_file.name

        if current_file.exists():

            with current_file.open(encoding="utf8") as open_file:

                code.buffer.text = open_file.read()

    else:

        message_dialog(

            title="Welcome to",

            text=ABOUT_MESSAGE,

            style=style,

        ).run()

    app.layout.focus(code.buffer)

    app.run()

undo

def undo(
    event=None
)
View Source
@kb.add("c-z")

def undo(event=None):

    code.buffer.undo()

view_buffer

def view_buffer(
    event=None
)
View Source
def view_buffer(event=None):

    asyncio.ensure_future(_view_buffer())

Classes

CodeFrame

class CodeFrame(
    body: Union[prompt_toolkit.layout.containers.Container, ForwardRef('MagicContainer')],
    title: Union[str, ForwardRef('MagicFormattedText'), List[Union[Tuple[str, str], Tuple[str, str, Callable[[prompt_toolkit.mouse_events.MouseEvent], NoneType]]]], Callable[[], Any], NoneType] = '',
    style: str = '',
    width: Union[NoneType, int, prompt_toolkit.layout.dimension.Dimension, Callable[[], Any]] = None,
    height: Union[NoneType, int, prompt_toolkit.layout.dimension.Dimension, Callable[[], Any]] = None,
    key_bindings: Union[prompt_toolkit.key_binding.key_bindings.KeyBindings, NoneType] = None,
    modal: bool = False
)

A custom frame for the quick python code container to match desired styling

View Source
class CodeFrame:

    """A custom frame for the quick python code container to match desired styling"""

    def __init__(

        self,

        body: AnyContainer,

        title: AnyFormattedText = "",

        style: str = "",

        width: AnyDimension = None,

        height: AnyDimension = None,

        key_bindings: Optional[KeyBindings] = None,

        modal: bool = False,

    ) -> None:

        self.title = title

        self.body = body

        fill = partial(Window, style="class:frame.border")

        style = "class:frame " + style

        top_row_with_title = VSplit(

            [

                fill(width=1, height=1, char=Border.TOP_LEFT),

                fill(char=Border.HORIZONTAL),

                Label(

                    lambda: Template(" {} ").format(self.title),

                    style="class:frame.label",

                    dont_extend_width=True,

                ),

                fill(char=Border.HORIZONTAL),

                fill(width=1, height=1, char=Border.TOP_RIGHT),

            ],

            height=1,

        )

        top_row_without_title = VSplit(

            [

                fill(width=1, height=1, char=Border.TOP_LEFT),

                fill(char=Border.HORIZONTAL),

                fill(width=1, height=1, char=Border.TOP_RIGHT),

            ],

            height=1,

        )

        @Condition

        def has_title() -> bool:

            return bool(self.title)

        self.container = HSplit(

            [

                ConditionalContainer(content=top_row_with_title, filter=has_title),

                ConditionalContainer(content=top_row_without_title, filter=~has_title),

                VSplit(

                    [

                        fill(width=1, char=Border.VERTICAL),

                        DynamicContainer(lambda: self.body),

                        fill(width=1, char=Border.VERTICAL),

                        # Padding is required to make sure that if the content is

                        # too small, the right frame border is still aligned.

                    ],

                    padding=0,

                ),

            ],

            width=width,

            height=height,

            style=style,

            key_bindings=key_bindings,

            modal=modal,

        )

    def __pt_container__(self) -> Container:

        return self.container

ImmediateFrame

class ImmediateFrame(
    body: Union[prompt_toolkit.layout.containers.Container, ForwardRef('MagicContainer')],
    title: Union[str, ForwardRef('MagicFormattedText'), List[Union[Tuple[str, str], Tuple[str, str, Callable[[prompt_toolkit.mouse_events.MouseEvent], NoneType]]]], Callable[[], Any], NoneType] = '',
    style: str = '',
    width: Union[NoneType, int, prompt_toolkit.layout.dimension.Dimension, Callable[[], Any]] = None,
    height: Union[NoneType, int, prompt_toolkit.layout.dimension.Dimension, Callable[[], Any]] = None,
    key_bindings: Union[prompt_toolkit.key_binding.key_bindings.KeyBindings, NoneType] = None,
    modal: bool = False
)

Draw a border around any container, optionally with a title text. Changing the title and body of the frame is possible at runtime by assigning to the body and title attributes of this class. :param body: Another container object. :param title: Text to be displayed in the top of the frame (can be formatted text). :param style: Style string to be applied to this widget.

View Source
class ImmediateFrame:

    """

    Draw a border around any container, optionally with a title text.

    Changing the title and body of the frame is possible at runtime by

    assigning to the `body` and `title` attributes of this class.

    :param body: Another container object.

    :param title: Text to be displayed in the top of the frame (can be formatted text).

    :param style: Style string to be applied to this widget.

    """

    def __init__(

        self,

        body: AnyContainer,

        title: AnyFormattedText = "",

        style: str = "",

        width: AnyDimension = None,

        height: AnyDimension = None,

        key_bindings: Optional[KeyBindings] = None,

        modal: bool = False,

    ) -> None:

        self.title = title

        self.body = body

        fill = partial(Window, style="class:frame.border")

        style = "class:frame " + style

        top_row_with_title = VSplit(

            [

                fill(width=1, height=1, char="├"),

                fill(char=Border.HORIZONTAL),

                # Notice: we use `Template` here, because `self.title` can be an

                # `HTML` object for instance.

                Label(

                    lambda: Template(" {} ").format(self.title),

                    style="class:frame.label",

                    dont_extend_width=True,

                ),

                fill(char=Border.HORIZONTAL),

                fill(width=1, height=1, char="┤"),

            ],

            height=1,

        )

        top_row_without_title = VSplit(

            [

                fill(width=1, height=1, char=Border.TOP_LEFT),

                fill(char=Border.HORIZONTAL),

                fill(width=1, height=1, char=Border.TOP_RIGHT),

            ],

            height=1,

        )

        @Condition

        def has_title() -> bool:

            return bool(self.title)

        self.container = HSplit(

            [

                ConditionalContainer(content=top_row_with_title, filter=has_title),

                ConditionalContainer(content=top_row_without_title, filter=~has_title),

                VSplit(

                    [

                        fill(width=1, char=Border.VERTICAL),

                        DynamicContainer(lambda: self.body),

                        fill(width=1, char=Border.VERTICAL),

                        # Padding is required to make sure that if the content is

                        # too small, the right frame border is still aligned.

                    ],

                    padding=0,

                ),

            ],

            width=width,

            height=height,

            style=style,

            key_bindings=key_bindings,

            modal=modal,

        )

    def __pt_container__(self) -> Container:

        return self.container

MessageDialog

class MessageDialog(
    title,
    text
)
View Source
class MessageDialog:

    def __init__(self, title, text):

        self.future = Future()

        def set_done():

            self.future.set_result(None)

        ok_button = Button(text="OK", handler=(lambda: set_done()))

        self.dialog = Dialog(

            title=title,

            body=HSplit([Label(text=text)]),

            buttons=[ok_button],

            width=Dimension(preferred=80),

            modal=True,

        )

    def __pt_container__(self):

        return self.dialog

TextInputDialog

class TextInputDialog(
    title='',
    label_text='',
    completer=None
)
View Source
class TextInputDialog:

    def __init__(self, title="", label_text="", completer=None):

        self.future = Future()

        def accept_text(buf):

            app.layout.focus(ok_button)

            buf.complete_state = None

            return True

        def accept():

            self.future.set_result(self.text_area.text)

        def cancel():

            self.future.set_result(None)

        self.text_area = TextArea(

            completer=completer,

            multiline=False,

            width=Dimension(preferred=40),

            accept_handler=accept_text,

        )

        ok_button = Button(text="OK", handler=accept)

        cancel_button = Button(text="Cancel", handler=cancel)

        self.dialog = Dialog(

            title=title,

            body=HSplit([Label(text=label_text), self.text_area]),

            buttons=[ok_button, cancel_button],

            width=Dimension(preferred=80),

            modal=True,

        )

    def __pt_container__(self):

        return self.dialog