Sublime Forum

Better Input Handlers

#1

I think input() method of the various Command subtypes should initially get the arguments of the corresponding run() method and not an empty dictionary , this way we can implement much more behaviours based on the rest of the arguments.

an example usage of this:

class BaseFileInputHandler(sublime_plugin.ListInputHandler):
    def __init__(self, pattern):
        self.pattern = pattern

    def list_items(self):
        # return a list of all the file matching the given pattern
        ...

class EditSettingsCommand(sublime_plugin.WindowCommand):
    def input(self, args):
        if "base_file" not in args:
            return BasFileInputHandler(args["pattern"])
        else:
            return None

    def run(base_file, user_file=None, default=None, pattern="*.sublime-settings"):
        ...

this implementation enables different behaviours for the edit_settings command based on the specified pattern (settings, keymaps, menus …)

and this makes InputHandlers more useful .

0 Likes

#2

Is this behaviour intended or it only doesn’t work for me ?
As i look at the code in sublime_plugin.py it should work the way i want it but it doesn’t, why ?
Do you have an explanation @OdatNurd ?

0 Likes

#3

Dictionary like arguments are passed via **kwargs. The args contains only positional arguments.

Even though I am not sure, whether I tried it yet, something like def input(self, **kwargs) should make kwargs["pattern"] return the correct argument’s value.

0 Likes

#4

Do you have an example of something that you’re trying to do that doesn’t work?

0 Likes

#5

The code snippet already shows what he tries. He tries to access the args list as dictionary by calling args["pattern"] which of course returns nothing. The value of the pattern argument is to be forwarded to the BaseFileInputHandler to let it use that information for something.

0 Likes

#6

@deathaxe I already tried that before, it doesn’t work.

0 Likes

#7

The ApplicationCommand, WindowCommand and TextCommand classes all have an internal method named run_(), which is the one that Sublime invokes internally. Part of the implementation of that method is calling the run() method that is the “usual” entry point for the command.

Since you mentioned looking in sublime_plugin.py, for reference purposes here’s what the current internal implementation of ApplicationCommand is (but all of them have similar code):

class ApplicationCommand(Command):
    def run_(self, edit_token, args):
        args = self.filter_args(args)
        try:
            if args:
                return self.run(**args)
            else:
                return self.run()
        except (TypeError) as e:
            if 'required positional argument' in str(e):
                if sublime_api.can_accept_input(self.name(), args):
                    sublime.active_window().run_command(
                        'show_overlay',
                        {
                            'overlay': 'command_palette',
                            'command': self.name(),
                            'args': args
                        }
                    )
                    return
            raise

    def run(self):
        pass

If you trace through the code, you can see that the implementation tries to invoke run() with the arguments that it was given, but if a TypeError exception is thrown that indicates that a missing argument was provided, that’s what triggers the input handling code.

Note also that it invokes the command from inside that point with the same args that it called run() with. Since we know that it only got that far because an argument was missing, we can also infer that input() is being called with the arguments provided to run when it was executed and not all arguments that it expects, including default values.

Or, TL;DR: input() is only invoked with the arguments that run() got, and since you did not provide the argument pattern to run(), it’s not given to input() either.

As such the easiest solution in your case is to not provide a default value for pattern in run() and instead add it to the entry in the command palette.

Redesigns of the code to not have default arguments aside, with additional code you can introspect the run() method to see what it’s default arguments would have been, then apply to that the arguments that were actually provided to come up with what you’re expecting; a full dictionary of all arguments to the command, filled out with known defaults.

Here’s an example of that, although I’m not a Python master so there is probably a better way to achieve the same effect (and runtime introspection may or may not be a good thing to do, etc).

import sublime
import sublime_plugin

import inspect


def _get_cmd_defaults(cmd_class, received=None):
    params = inspect.signature(cmd_class.run).parameters
    defaults = {p: params[p].default for p in params
        if params[p].default != inspect._empty}

    if received:
        defaults.update(received)

    return defaults


class BaseFileInputHandler(sublime_plugin.ListInputHandler):
    def __init__(self, pattern):
        self.pattern = pattern

    def list_items(self):
        return sublime.find_resources(self.pattern)


class NewEditSettingsCommand(sublime_plugin.WindowCommand):
    def input(self, args):
        args = _get_cmd_defaults(self.__class__, args)

        if "base_file" not in args:
            return BaseFileInputHandler(args["pattern"])

        return None

    def run(self, base_file, user_file=None, default=None, pattern="*.sublime-settings"):
        print("base_file: %s" % base_file)
        print("user_file: %s" % user_file)
        print("default: %s" % default)
        print("pattern: %s" % pattern)

0 Likes

#8

@OdatNurd Did you get the code running? I tried both the first and your example. The input() method never gets called, while another plugin of mine looks pretty much the same way but works.

0 Likes

#9

The example I posted above works for me, but remember that Input handlers only get used for commands that are in the command palette. So you also have to add it to a sublime-commands file as well.

0 Likes