Sublime Forum

What is the best way for a command to call another command in a different package?

#1

I have a TextCommand in one package (call it A), and it’s working fine. I have different package (B) that uses some heuristics to construct arguments for A’s command and then calls it.

It’s in a different package, so I don’t really want to have B call some python function in A, because if I change the implementation of A, I don’t want to have different packages expecting the old interface.

In B’s TextCommand, I have:

self.view.run_command(‘command_defined_in_a’, <args> } )

This sort of works, but not quite. What I find is that while I’m doing development, everything is fine, but if I restart sublime, this command doesn’t seem to work. If I open up a.py and re-save it, however, then everything starts working.

I think what is happening is that B is being loaded before A, and at the time B’s command is constructed, A’s doesn’t exist. That’s just a guess, though. Is there something that needs to be done with the package load order? Or is there a different way that one command should call another?

0 Likes

#2

What you’re trying to do is the correct way to go about this; from the sounds of your problem it sounds like you’re executing code at the wrong time, particularly it sounds like perhaps you have code that executes at the point where the module is loaded, like top level code or code in a class that gets invoked from there.

The API isn’t generally available at startup because the plugin host runs in it’s own external process. Until the API is ready to go, most API commands will not work; only a small handful such as those that tell you what platform Sublime is running on or the version are available right away.

When the plugin host and the API is ready, the top level plugin_loaded() module function will be called to tell you. This happens at some point after startup once the API is immediately available, and then immediately after plugin load (or reload) while Sublime is running.

def plugin_loaded():
    print("My plugin just loaded")

You can also use plugin_unloaded() to know when your plugin is being unloaded, which happens prior to it being reloaded and also when the package it’s in is added to ignored_packages.

1 Like

#3

Thanks for your help. I’m not making any API calls before things get started, but now that I know that I’m calling it the right way, I was able to figure out the problem. There’s still an issue, though, I’m still not sure what’s wrong.

It turns out that a.py’s TextCommand does run, but it raises an exception. I had messed up an exception handler, which is why I missed it.

The question, then, is why a.py’s command has an exception. It turns out that package A has two files, a.py and a_settings.py. a.py contains:

from .a_settings import Settings

while a_settings.py has:

Settings = {}

def plugin_loaded():
      global Settings  # Don't think this is necessary
      # fill in Settings with actual settings

In a.py, the TextCommand uses Settings. When I run the command, I get a KeyError, because Settings is empty.

If I cause a.py to get reloaded, however, then Settings is fully populated and and everything works fine.

Perhaps this is more of a python question than a sublime question (I’m not a python expert).

It looks to me that if a.py is loaded after a_settings.py’s plugin_loaded is called, then everything is fine. If a.py is loaded before that, though, then its commands see an empty Settings, even if a_settings.py’s plugin_loaded() is called before the command is called.

This seems weird to me. It looks like the object that Settings gets bound to in a.py depends on whether or not a_settings.py’s plugin_loaded() has been called. From my (admittedly limited) knowledge of python, I wouldn’t expect that to be the case.

0 Likes

#4

This would definitely seem to be down to plugin load order; for example:

a.py

import sublime
import sublime_plugin

from .a_settings import Settings
print("==> Settings import:", id(Settings))


class ExampleCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        print("Settings run: ", id(Settings))

a_settings.py

Settings = {}
print("==> Settings init:", id(Settings))

def plugin_loaded():
    print("==> Settings set:", id(Settings))

On a fresh start, you see this in the console (redacting uninteresting bits):

reloading plugin A.a
==> Settings init: 2154390146440
==> Settings import: 2154390146440
reloading plugin A.a_settings
==> Settings init: 2154390145416
plugins loaded
==> Settings set: 2154390145416

>>> view.run_command("example")
Settings run:  2154390146440

When a.py is loaded, it does an import which causes a_settings.py to initialize a dictionary which is given to a; then Sublime loads the a_settings.py plugin which replaces the dictionary with a new one.

When plugin_loaded is called, it’s the new one that gets initialized, but the one in the command is still the original imported one. Saving the other file causes it to reload in a more controlled manner so that things work as expected.

Plugins load in lexical order and an import of an already loaded plugin won’t cause it to reload (which causes problems all its own), so changing the names of the files would solve the problem, I would think. On the other hand I’m not 100% sure that the load order is officially documented versus just an implementation detail, so it may or may not be safe to rely on that if it works (on the other hand, PackageControl relies on this to set up dependency load order).

One potential solution would be to use a standard sublime-settings file for this and let the core handle the setup of the settings, but that may not fit with your use case depending on what the Settings dictionary here represents.

Another solution would be to have something in the second plugin return the instantiated object from a function call, then import the function instead:

a.py

import sublime
import sublime_plugin

from .a_settings import settings


class ExampleCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        print("Settings run: ", id(settings()))
        print(settings()["first"])

a_settings.py

def settings():
    print("==> Settings get:", id(settings.obj))
    return settings.obj

def plugin_loaded():
    settings.obj = {"first": "test"}

Now plugin_loaded() still sets up the settings object, but it stashes it in a property of the settings() function, and invoking the function returns the same object:

plugins loaded
>>> view.run_command("example")
==> Settings get: 2412655836296
Settings run:  2412655836296
==> Settings get: 2412655836296
test

There are likely other better solutions as well. :slight_smile:

3 Likes

#5

Got it. Yes, it seems that a_settings.py is being loaded twice: once by a.py and once by sublime, and that’s the cause of the problem.

I guess what I ought to do is put my files in a subdirectory and write a plugin that just exists to import what I want so sublime doesn’t get involved. I was hoping that such a simple two-file package wouldn’t need that kind of structure, but oh well. Either that, or I’ll use a function instead of an exported dictionary, as you suggest.

Regarding the purpose of the Settings dictionary, it’s just to cache the contents of a sublime-settings file. I don’t know how necessary that is, but I pattern-matched a plugin that did that. :slight_smile:

Thanks for your help!

0 Likes

#6

Sublime caches settings internally, so it’s virtually never necessary to manually cache or re-use a Settings object (merely at times convenient).

0 Likes

#7

Thanks. Can you mention when it would be convenient? In the plugin that I pattern-matched, I see that it creates a settings object in plugin_loaded so it can call add_on_change() on it. In function called by add_on_change, it copies the settings from the settings object to a dictionary, and the rest of the plugin uses the dictionary.

Is there a reason to do such a thing?

0 Likes

#8

The only reasons that spring immediately to mind are related to code clarity (though there are quite likely others). For example, you could populate the dictionary with defaults for missing settings so that the rest of the code can be sure that when it accesses the setting it will get a sensible value (e.g. you could say cached_settings["setting"] instead of settings().get("setting", "default").

One paradigm that I tend to use for clarity in this regard is something like this:

def plugin_loaded():
    pkg_setting.obj = sublime.load_settings("MyPackage.sublime-settings")
    pkg_setting.default = {
        "my_setting": True,
    }

def pkg_setting(key):
    default = pkg_setting.default.get(key, None)
    return pkg_setting.obj.get(key, default)

Here the pkg_setting() function takes the settings key and returns a value without having to use load_settings() and/or cache or pass around a settings object. It also allows for putting defaults for settings in place (even though those should already be in the base settings) which is something like an extra safeguard and documentation in the code for what the defaults are.

0 Likes

#9

Note that in @OdatNurd’s Version you wouldn’t get a live value for the setting for obvious reasons.

A better way to centralize default settings in code imo is to use the settings abstraction provided by the sublime_lib dependency along with a chainmap (mentioned in the docs).

1 Like

#10

What I mean is that if I’m grabbing three values out of the same settings, I’ll write it like this:

settings = sublime.load_settings('MySettings.sublime-settings')
foo = settings.get('foo')
bar = settings.get('bar')
baz = settings.get('baz')

Rather than:

foo = sublime.load_settings('MySettings.sublime-settings').get('foo')
bar = sublime.load_settings('MySettings.sublime-settings').get('bar')
baz = sublime.load_settings('MySettings.sublime-settings').get('baz')

I might even put it in a global in plugin_loaded, but trying to import a Settings constant from another module is iffy. OdatNurd said basically everything I would have.

In the plugin that I pattern-matched, I see that it creates a settings object in plugin_loaded so it can call add_on_change() on it. In function called by add_on_change, it copies the settings from the settings object to a dictionary, and the rest of the plugin uses the dictionary.

Is there a reason to do such a thing?

Not that I can think of. It sounds like an attempt to avoid repeated file IO, but Sublime caches all of it anyway so it’s just making things more complicated with no obvious benefit.

If you want defaults for the settings name MySettings.sublime-settings, it’s almost certainly best to just provide a MySettings.sublime-settings file with those defaults. Sublime will merge these defaults with a user’s MySettings.sublime-settings file automatically.

The only time you have to manually manage defaults is if you want to consider settings objects from different sources. For instance, suppose you wanted to check the view settings, then the window settings, and then MySettings.sublime-settings. In this case, I would second FichteFoll’s recommendation of using a ChainMap of SettingsDicts.


As an aside, sublime_lib might be missing a use case here. Often when you add custom settings to a view or window, you prefix them to avoid conflicts. So in the above example, we might check:

  • view.settings().get('MySettings-foo'), then
  • window.settings().get('MySettings-foo'), then
  • sublime.load_settings('MySettings.sublime-settings').get('foo')

ChainMap/SettingsDict won’t quite get you there, because it will try the same keys on each map. We could solve this problem by providing a dict wrapper that prefixes keys:

settings = ChainMap(
    PrefixedDict(SettingsDict(view.settings()), 'MySettings-'),
    PrefixedDict(SettingsDict(window.settings()), 'MySettings-'),
    NamedSettingsDict('MySettings')
)

A convenience class might also be in order, e.g.:

settings = SettingsChain(view, window, 'MySettings')

# subscribe to each member and notify when the chained result changes.
unsubscribe = settings.subscribe(...)
1 Like

#11

Thanks. I’m pretty sure I understand what’s going on regarding caching (or not caching) a package settings file now.

I’m confused by your aside, though. How would a view or a window get settings for something like “MySettings”? It’s my understanding that sublime will load files called Preferences.sublime-settings and <syntax_name>.sublime-settings on its own, but that anything else was package-specific and up to the plugin to do itself. Is that not correct?

0 Likes

#12

What I mean is that some packages have “global” settings defined in MyPackage.sublime-settings, but allow those to be overridden in a specific view or window. A package would have to do this deliberately (and manually), because there is no inherent connection between the view settings, the window settings, and MyPackage.sublime-settings.

This is unrelated to having multiple MyPackage.sublime-settings files for Sublime to merge; it’s an additional thing that a package might do on top of that. Suppose that you have, say, a linter package named ExampleLinter, and you generally want it to run, but you want to disable it specifically for a certain view because that view is full of awful legacy code. To support this, you would have a regular ExampleLinter.sublime-settings file with "enabled": true, but you might also have a TextCommand called “Disable linter for this view“ that would set "ExampleLinter-enabled": false in the view settings. The package code would have to check both settings objects to know whether the linter was enabled for that view.

Checking settings from different sources isn’t complicated, but it’s arguably boilerplate-y. You can use ChainMap along with sublime_lib’s SettingsDict to create a single object you can check (that will in turn check the underlying settings objects as needed), but as-is that only works if the settings keys are exactly the same (e.g. both “enabled”, not “enabled” for MyPackage.sublime-settings but “MyPackage-enabled” for the view settings). Therefore, there is an opportunity for sublime_lib to provide slightly more sophisticated functionality to accommodate that use case.

1 Like