Search code examples
python-3.xsublimetext3sublime-text-plugin

Sublime Text 3.1 Build 3170 ViewEventListener Super Class methods


I updated to Sublime Text build 3170 today from the last stable build which I think was 3143. I had been using sublime_plugin.EventListener to do some tasks related to .scpt files, etc. To do that I had created a class with basic functionality for viewing binary or encoded files. To use that class I then would subclass it and subclass sublime_plugin.EventListener so that sublime would call the appropriate methods (on_modified, on_load, etc).
However, I was hoping to change to sublime_plugin.ViewEventListener after the update (which expanded the api for the ViewEventListener) because that would enable me to accomplish more things in my plugin.
The problem is that my super class methods were not being called by sublime. Below is some code that will hopefully explain my problem. Thanks in advance.

#this does work
class test:
  def on_modified(self, view):
    print("mod")

class test2(test, sublime_plugin.EventListener):
  pass

That will call on_modified when changes are made to a view and will print "mod" every time.

#this does not work
class test:
  def on_modified(self):
    print("mod")

class test2(test, sublime_plugin.ViewEventListener):
  pass

This on the other hand will not work. "mod" is never printed. I have searched google and tried debugging. I can confirm that a ViewEventListener instance is being created and that if on_modified is in test2 then "mod" is printed.
Any suggestions would be greatly appreciated. Thanks again.


Solution

  • This is an interesting little problem because on the face of it, it seems that this should work just fine. If you trace around enough in the internals of the Sublime plugin system however, the reason for this becomes more evident.

    For the purposes of this answer I'm using a plugin that matches the non-working example in your question, which I have named test_plugin.py:

    import sublime
    import sublime_plugin
    
    class test:
        def on_modified(self):
            print("mod")
    
    class test2(test, sublime_plugin.ViewEventListener):
        pass
    

    To begin with, the manner by which EventListener and ViewEventListener are used by Sublime have different mechanisms internally when it comes to plugins.

    EventListener events apply equally to all views everywhere, and so when Sublime loads a plugin and finds an EventListener class, it creates an instance of it right away and then checks to see what events that instance supports. The relevant code for this is in sublime_plugin.py in the reload_plugin() method around line 142:

    if issubclass(t, EventListener):
        obj = t()
        for p in all_callbacks.items():
            if p[0] in dir(obj):
                p[1].append(obj)
    

    all_callbacks is a dictionary where the keys are the names of events and the values are arrays; thus this checks to see if the dir() of an instance of an event listener contains an event, and if so it is added to the list of classes that support that event.

    On the other hand, ViewEventListener applies only to certain views based on the is_applicable and applies_to_primary_view_only class methods. This means that it instead has to store the class instead of an instance, so that as new views are created it can create an instance specific for that view.

    The relevant code is just below the above, at line 156 of the sublime_plugin.py file:

    if issubclass(t, ViewEventListener):
        view_event_listener_classes.append(t)
        module_view_event_listener_classes.append(t)
    

    Now lets look at what happens when an event needs to be raised. In our example we're looking at the on_modified event, which is handled by the on_modified module function in sublime_plugin.py at line 566 (but all events work similarly):

    def on_modified(view_id):
        v = sublime.View(view_id)
        for callback in all_callbacks['on_modified']:
            run_callback('on_modified', callback, lambda: callback.on_modified(v))
        run_view_listener_callback(v, 'on_modified')
    

    The first parts are for the regular EventListener; it finds all of the object instances that it found earlier that have an on_modified handler, and then directly invokes on_modified() on them (the run_callback() wrapper is responsible for timing the execution for the profiling you can see in Tools > Developer > Profile Plugins).

    The handling for ViewEventListener happens in run_view_listener_callback(), which is around line 480 in sublime_plugin.py:

    def run_view_listener_callback(view, name):
        for vel in event_listeners_for_view(view):
            if name in vel.__class__.__dict__:
                run_callback(name, vel, lambda: vel.__class__.__dict__[name](vel))
    

    This is the part where things start to go pear shaped for you if you've defined the classes as in your example because it's specifically asking the __dict__ attribute if it contains the event to invoke, and only calling it if it does.

    Note the following from the Sublime console based on the sample plugin above:

    >>> from User.test_plugin import test, test2
    >>> "on_modified" in test.__dict__
    True
    >>> "on_modified" in test2.__dict__
    False
    

    In particular, test2 doesn't directly contain the on_modified method, so it's not in the __dict__. In a regular Python program if you tried to invoke on_modified, it would notice that it's not in the __dict__ of the object and then start searching the hierarchy, finding it in test and everything is gravy.

    In the case of an EventListener instance, this is what happens; the dir() function knows that one of the superclasses contains on_modified and returns it, and the call to invoke it goes up the chain and everything works as you would expect.

    Since the call to run_view_listener_callback isn't directly trying to invoke the method, it doesn't find it in the direct __dict__ lookup and thus does nothing because it believes that the class doesn't handle that event even though it does.

    So the upshot of all of this wall of text is that for ViewEventListener, the events have to exist directly in the class that is subclassing ViewEventListener or it doesn't work.

    In your example, one way to do that would be to restructure your code and define those methods directly inside test2 instead of in test.

    Another way would be to "proxy" them by defining the method in your subclass, but having the body defer to the super class version instead. That puts the appropriate method in the __dict__ of the class but still lets the implementation appear in another class.

    class test2(test, sublime_plugin.ViewEventListener):
        def on_modified(self):
            super().on_modified()
    

    I'm not a Python guru but this one doesn't seem overly Pythonic to me (at least for the example as laid out) and there are likely other ways to achieve the same thing.