Sublime Forum

API and Project Settings Hierarchy

#1

I did some searching and didn’t find anything that seemed relevant here in the forum. I am experimenting with retrieving settings in a command and finding that the hierarchy seems inconsistent.

So, according to the docs, the hierarchy should be (for Preferences)

• Packages/Default/Preferences.sublime-settings
• Packages/Default/Preferences (Windows).sublime-settings
• Packages/AnyOtherPackage/Preferences.sublime-settings
• Packages/AnyOtherPackage/Preferences (Windows).sublime-settings
• Packages/User/Preferences.sublime-settings
• Settings from the current project
(and then some from language specific)

So, I figure I’ll experiment with obtaining the tab size. It’s set to 4 in default Preferences.sublime-settings. I then have an activated project file with

    "settings":
    {
        "tab_size": 9
    },

I can tell that Sublime is paying attention to this because my tab stops immediately go to 9 characters when I save the file.

However in code I have the following:

class vhdlModeSettingSniffer(sublime_plugin.TextCommand):
    def run(self, edit)
        pref_settings = sublime.load_settings('Preferences.sublime-settings')
        print('vhdl-mode: Tab size: {}'.format(pref_settings.get("tab_size")))

When I do this, it prints out that the tab size is 4.

In another instance when I’m using my own vhdl_mode.sublime-settings file, I can correctly override the default setting (provided in the package) with my own settings and load_settings picks this up correctly.

So, are the settings from the current project actually factored into the hierarchy using load_settings? Like I said, I can tell the editor pays attention, I just don’t know if the API call is that automated. I may have to manually extract project settings and apply an override there if I want to pick up that behavior.

0 Likes

#2

I believe not, one has to check the View settings to find the actual applied preferences

1 Like

#3

The hierarchy you mention is indeed how the settings are finally applied (on a view by view basis), but when you do:

pref_settings = sublime.load_settings('Preferences.sublime-settings')

You’re specifically loading only the Preferences.sublime-settings settings. That gives you Sublime’s combined view of all similarly named files using some of the hierarchy you mentioned (e.g… Default, then all packages (including their OS specific) and User last), but it doesn’t include things like your syntax specific settings or project settings.

To see the settings as they’re actually applied to the view, you should use view.settings() to see what settings are applied.

Another way to look at it is that anything that’s supplied in Preferences, in a syntax specific settings file (e.g. Python.sublime-settings) or in your project specific settings are settings that are supposed to apply to everything, so they are directly applied on a view by view basis, whereas settings in my_package_name.sublime-settings are settings that only apply to your particular package, and so it’s up to you to use load_settings() to load those settings when you need them.

3 Likes

#4

Thanks so much. This actually changes a lot (for the better) of things I had in place. If there’s multiple ways to go about something I always manage to find the most roundabout method. For the package I had created my own settings file and had been using that with load_settings and instead I could have just put new keys in Preferences and been done with it. (I may still keep that model – at least I can be assured of having some default values there for a few things, but using view.settings() I can keep the applied hierarchy!)

I dislike the proliferation and maze of settings files, but I established a Package Settings command to edit a basic package preferences, and then project settings are the most appropriate way to override behavior on a case by case basis.

0 Likes

#5

I am now running into this little scope nuance and it makes me wonder if I really should have a sublime-settings file or not now. For example, I created some settings keys in my Package Settings with a default value. I then set my own override for these settings. All well and good. If I do a load_settings for my package sublime-settings file I can print these fields out and they are correct.

Now I move to the project file and in settings there, I create alternate versions of these settings (just to make sure I can see a difference.) I retrieve the settings on the view with view.settings() and then print them. They do print out correctly there.

However, then I went and commented out one of the project settings. Now when I print these settings, instead of falling back onto what I’d hoped would be my personal setting value for the key, it reads None. So just as you said, Preferences are applied to everything. The project settings key is a child of the main Preferences key and not the Package keys. I will have to decide what behavior I want for the user here. As noted at least having some default values in my package settings is nice, however they won’t be the backstop default if someone chooses not to define a local value in a project file. I think maybe the best thing I can do is to write a method that performs project hierarchy for my package settings. That way, it should be relatively transparent to the user.

0 Likes

#6

The settings object is sadly not iterable! That’s unfortunate.

0 Likes

#7

Yeah, for that you need to do some jiggering behind the scenes.

I’m doing the thing you’re probably thinking about in OverrideAudit for example. All of the settings are specific only to my own package, so instead of going the preferences route I instead have an OverrideAudit.sublime-settings file that ships with the package and a menu item that opens it side by side with your own custom version of the file to override what you want/need.

In the actual code of the package, I have something like this:

def plugin_loaded():
    oa_setting.obj = sublime.load_settings("OverrideAudit.sublime-settings")
    oa_setting.default = {
        "reuse_views": True,
        "clear_existing": True,
        "ignore_overrides_in": [],
        "diff_unchanged": "diff",
        "diff_context_lines": 3,
        "diff_empty_hdr": False,
        "save_on_diff": False,
        "confirm_deletion": True,
        "confirm_freshen": True,
        "report_on_unignore": True,

        # Inherits from user preferences
        "binary_file_patterns": None
    }

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

So anywhere in the code that I want to query a particular setting, I can just call oa_setting to get it. If the settings object (which can be overriden by the user by blending their own preferences with it) doesn’t have the setting I asked for, a potential default is automatically supplied.

The binary_file_patterns is a special case here; the code knows that for that setting (which is a default preference) it should only try to do something with it if you’ve actually specified it, so at runtime it checks here to see if it’s set and defaults to the returned value from view.settings().get() for the same preference if this returns None.

Now if I decided that I wanted per-project settings to be allowed, all settings access is via a single route, so this one method could first try to query the settings from somewhere else first (say the view settings or a unique key in the project data), then fall back to the settings object and finally to the default and everything remains transparent at the call site.

It’s also a little redundant to have defaults here when they’re theoretically also in the default settings file that I ship, but I’m a paranoid kind of guy, so that’s probably overkill in the general case.

Indeed it is not, which has made me sad many a time. The last time that was mentioned (on Discord in the #general channel) Will said:

I believe the reason not is performance/locking, but I could be wrong about that

1 Like

#8

Sorry to keep spamming this topic but I think there’s still something going on that I don’t understand. When we last left our intrepid hack, it seemed like project keys would not degrade to package specific keys, but rather their parent was Preferences keys. I believed this because I could do a load_settings for my package settings file (and override) and then print the same keys after doing a settings() on the view, if I commented out one of the keys, it would go to None rather than the default value.

So I wrote myself a little method to attempt to read the package settings and then parse the view settings.

def mode_settings(cmd_obj):
    # Load the defaults, or user overridden defaults.
    vhdl_settings = sublime.load_settings('vhdl_mode.sublime-settings')
    # Load the views settings
    view_settings = cmd_obj.view.settings()
    # Sadly the settings object is not iterable.  I will have to check
    # for each setting I use to see if it's been set in the Preferences
    # tree (most likely the project file.)  I can create a list of
    # settings keys and iterate over that.
    keys = ["vhdl-user",
            "vhdl-company",
            "vhdl-project-name",
            "vhdl-platform",
            "vhdl-standard"]
    for key in keys:
        if view_settings.has(key):
            vhdl_settings.set(key, view_settings.get(key))

    return vhdl_settings

This seems to work. However I started to play with using my test command which basically just prints out all the settings from my package settings and the all the settings from my view updated settings.

class vhdlModeSettingSniffer(sublime_plugin.TextCommand):
    '''
    Creating a command to check settings in various
    contexts
    '''
    def run(self, edit):
        '''
        Standard TextCommand Run Method
        '''
        vhdl_settings = sublime.load_settings('vhdl_mode.sublime-settings')
        print('Package Settings')
        print('vhdl-mode: Package Username: {}'.format(vhdl_settings.get("vhdl-user")))
        print('vhdl-mode: Package Company: {}'.format(vhdl_settings.get("vhdl-company")))
        print('vhdl-mode: Package Project: {}'.format(vhdl_settings.get("vhdl-project-name")))
        print('vhdl-mode: Package Part: {}'.format(vhdl_settings.get("vhdl-platform")))
        print('vhdl-mode: Package Standard: {}'.format(vhdl_settings.get("vhdl-standard")))
        new_settings = util.mode_settings(self)
        print('vhdl-mode: View Username: {}'.format(new_settings.get("vhdl-user")))
        print('vhdl-mode: View Company: {}'.format(new_settings.get("vhdl-company")))
        print('vhdl-mode: View Project: {}'.format(new_settings.get("vhdl-project-name")))
        print('vhdl-mode: View Part: {}'.format(new_settings.get("vhdl-platform")))
        print('vhdl-mode: View Standard: {}'.format(new_settings.get("vhdl-standard")))

So, the first time I run this, I get exactly what I expected. The first five lines printed my default values from my package settings. The next five lines printed out the settings as overridden in the project settings field MINUS the one I had commented out to make sure it degraded back to the default.

The following iterations got weird. I found that each time I ran it, the load_settings version was listing off the previous settings iteration, and then the project checking method would update it correctly. So, to sort of summarize

Round 1:
Prints 5 default values.
Prints 5 new values (A).

(edit project file)

Round 2:
Prints the A values.
Prints 5 new values (B)

(edit project file)

Round 3:
Prints the B values
Prints the 5 new values (C)

And so forth.

So what’s going on here? If I had to guess, the view object has a single settings object, and somehow I’m updating all these fields just by calling the routines and they are already providing the override functionality internally? Seems like something like that, because I was expecting each time I called load_settings on my package settings file I would see the defaults.

0 Likes

#9

The Settings instance that you get from load_settings is cached in memory and always the same object (or possibly more correctly wraps the same internal data structure) every time you make a load_settings call with the same argument. If you call save_settings with the same base name as you used to load settings, the contents of the setting object are written out back to disk (this replaces the existing sublime-settings file, which reorders the keys and throws away comments). Otherwise the settings just persist in memory while Sublime is running and revert to defaults when you restart it.

The function you have defined changes the settings in that object which changes them in memory, and so the next time you call load_settings you get the same modified object back again, which still reflects the changes you made last time around.

0 Likes

#10

Aha. Okay, that explains it. If I had created a new settings object, taking the key/value pair from the loaded settings object and also using the view settings key/value pair, I might have seen something a little different.

I’ll have to think about this and decide if the current behavior is desired behavior. It definitely updates correctly, just does not degrade quite the way one would think (until ST restarts) which could be confusing. On the second hand, it’s a small window of possibility that someone would be fussing with the project file this way – most likely it’d be set and forgotten for the rest of the project. Still, that small window of possibility bugs me.

There is something I don’t understand in your own example. You have:

def plugin_loaded():
    oa_setting.obj = sublime.load_settings("OverrideAudit.sublime-settings")
    oa_setting.default = {
    # Stuff
    }

def oa_setting(key):
    # Stuff

The only way I can parse the .obj portion of the first assignment is assuming you have created a settings class, oa_setting is an instance of that class and that class has a Sublime settings child object named obj underneath it. However I get lost when you also then define a method called the same name as what I think is the instance name. Using the oa_setting instantiation inside the method makes sense again to me though. Is that a correct interpretation?

0 Likes

#11

Actually that code is fully self-contained (excluding how it requires that it be run from inside of sublime) so you should be able to just pull it directly into your own code as-is and get it to do something if you wanted to.

Python is a language in which everything is an object of some description, and functions are no different in this respect (you may be familiar with the term “first class” used in this sort of context). For example, you can try running dir(dir) or dir(42) in the Sublime console.

You can think of the line that defines the function as actually creating and initializing an instance of some internal class that represents functions and assigning it as a regular variable using the name of the function. At runtime you treat that value as a function, but it’s still just an object as far as the interpreter is concerned.

So here in this code oa_setting is just a regular function, but obj and default are attributes of the function object. The call to plugin_loaded assigns a settings object to obj and a dictionary of default values to default, and oa_setting itself references those same attributes. All accesses use the name of the function object directly as the specific object to set/query attributes of.

The “tricky” bit is that as the code stands, if you call oa_setting before you call plugin_loaded, the code will throw an AttributeError exception because the oa_setting object has no attributes by those names until plugin_loaded sets them.

In this case that’s fine because Sublime guarantees that this will happen. In a regular python program you could use hasattr to see if the attribute is there first, and set it if it’s not or just leave without doing anything special or whatever.

Note that not all objects allow you to assign arbitrary attributes to them (built-in functions and tuples, for example), so for more complete information you probably want to dig around a bit in the python documentation.

2 Likes

#12

Wow. I knew classes had attributes (or class variables – I still tend to think of it in C++ terms since that’s what I learned a long time ago, even if I don’t currently use it much), but I did not know that you could create attributes on functions as well. That’s actually potentially neat.

In all honesty I’m just a hardware guy who is wandering out into the Pythonic weeds here so that’s the first time I’d seen that trick. I do like the internal consistency of it. Thank you for the explanation.

1 Like