[PYTHON] "Inline" Sublime Text plugin callbacks with a generator

Overview

The process of using various input interfaces of Sublime Text tends to be a callback hell, so I wrote it neatly using a generator! It is a story. I'm using the plug-in I'm writing now.

Background

The Sublime Text 3 API can do what you want through a text editor. It is possible to accept various inputs such as displaying a quick panel with customized menu items and displaying a text input panel, but as you can see, the editor's while accepting input from various users Other operations aren't blocked, right? This is because, of course, the functions that operate these input reception interfaces are executed in a new thread, but if you want to interfere with the processing of the main thread from there, after the callback function passed in advance is input. Call with.

For example, in the quick panel display, it looks like this:

from sublime_plugin import WindowCommand


class ShowMenu(WindowCommand):

    def run(self):
        menu = ["A", "B", "C"]
        def cb(index):
            print(menu[index])
        self.window.show_quick_panel(menu, cb)

Can display a menu containing the elements "A", "B", "C", and the cb () function is passed the index of the selected menu. Well, it's not a big deal because the contents of the callback are small if it is about this, but it becomes troublesome again if the number of rows of the callback and the number of variables passed to the callback increase. Even more troublesome is when you have to call commands that wait for user input in succession, and when you try to call the next user input wait command with an appropriate callback in the callback ... For example:

class Auth(WindowCommand):

    def run(self):
        self.window.show_input_panel("username", "", cb1)
        #When the input panel is displayed, the line on this side will continue without waiting for input.
        #Write the process using input in the callback


def _cb1(username):
    # show_input_panel()If it is as it is, the input character string will be displayed and it is really that, but for the time being I do not care about it
    self.window.show_input_panel(
        "password", "", functools.partial(_cb2, username))


def _cb2(username, password):
    _auth(username, password)


def _auth(username, password):
    #Authentication process using username and password

Well, I can write it, but it's not easy to read. It is tedious and unintuitive to have to separate as a function just to execute it as a callback, even though the unit to be cut out as a function is not appropriate. If there are a lot of parameters to be passed, it will be troublesome and maintainability will be reduced.

Let's write more elegantly

Can't you write more elegantly? ……Can write. With Python.

In principle, it would be nice if the execution of the function could be paused when it came to wait for input from the user. ――Yes, this is where the yield statement comes into play. By calling the yield statement, you can save the running state until thenext ()function or thesend ()function is called for the generator. If there is input from the user, it can be used in the expression in the generator by using the send () function. If not, just call next (). So, first of all, I will prepare a decorator like this.


def chain_callbacks(
    f: Callable[..., Generator[Callable[Callable[...]], Any, Any]
) -> Callable[..., None]:
    @wraps(f)
    def wrapper(*args, **kwargs):
        chain = f(*args, **kwargs)
        try:
            next_f = next(chain)
        except StopIteration:
            return

        def cb(*args, **kwargs):
            nonlocal next_f
            try:
                if len(args) + len(kwargs) != 0:
                    next_f = chain.send(*args, **kwargs)
                else:
                    next_f = next(chain)
                next_f(cb)
            except StopIteration:
                return
        next_f(cb)
    return wrapper

It's a little tricky, but you can use it to "inline" callbacks. In the yield statement, pass" a function that takes one callback function that you want the yield statement and subsequent statements to be executed when called". If any value is passed to that callback function, it can be received as the return value of yield.

from functools import partial

class Auth(WindowCommand):

    @chain_callback
    def run(self):
        # `functools.partial()`With`on_done`Take only one argument
        #In the form of a function, yield
        username = yield partial(
            self.window.show_input_panel, "username", "",
            on_change=None, on_cancel=None)
        password = yield partial(
            self.window.show_input_panel, "password", "",
            on_change=None, on_cancel=None)
        #From here, use username and password to authenticate

This side is refreshing!

This kind of generator usage is also used by web frameworks like Twisted and Tornado.

Recommended Posts

"Inline" Sublime Text plugin callbacks with a generator
Create a python3 build environment with Sublime Text3
Pythonbrew with Sublime Text
Set up a Python development environment with Sublime Text 2
Create a plugin that always highlights arbitrary text in Sublime Text 2
GOTO in Python with Sublime Text 3
Enable Python raw_input with Sublime Text 3
Create a large text file with shellscript
I made a stamp generator with GAN
Create a matrix with PythonGUI (text box)
I made a package like Weblio pop-up English-Japanese dictionary with Sublime Text3
Create a plugin that allows you to search Sublime Text 3 tabs in Python
Speaking Japanese with OpenJtalk (reading a text file)
Speaking Japanese with gTTS (reading a text file)
Make a simple pixel art generator with Flask
Plugin to add variable symbols (Sublime Text) Description
I tried to make it on / off by setting "Create a plug-in that highlights double-byte space with Sublime Text 2".
How to create a submenu with the [Blender] plugin
[Blender] Complement Blender's Python API with a text editor
Links to do what you want with Sublime Text
Using a Python program with fluentd's exec_filter Output Plugin
Use python installed with Pyenv with Sublime REPL of Sublime Text 3
Using a python program with fluentd's exec Output Plugin