Sublime Forum

What is the best way to factor out similar code in TextCommands?

#1

I have several TextCommands that are mostly the same, but a little bit different. It’s not easy for me to just write a single TextCommand that takes a bunch of parameters because part of what’s different in my TextCommands is that I need to implement a few utility functions differently.

My first thought is that I should create a generic TextCommand and use that as a superclass for the commands I actually intend to use. My intention would be to not instantiate it directly, but only as a superclass of my concrete commands. I’m running into problems with that, though, because my generic TextCommand is still a subclass of TextCommand. For instance, my generic command takes a few extra initialization parameters (provided by the subclass). Sounds harmless, but the problem is that it seems that sublime wants to construct any TextCommand on its own, and it’s not going to use my specific arguments to it when it does. I’d rather sublime not try to use my generic command directly - only the concrete subclasses, but I don’t know how to get sublime to do that.

Is there a common pattern for dealing with this?

0 Likes

#2

Can you post an example?

0 Likes

#3

Sublime automatically creates instances of special classes, which it does by recognizing them as being subclasses at the point where it’s loading the plugin file. In order to stop it from creating an instance of your command, you need to stop it from seeing it in the first place.

Sublime only automatically loads files in the top level of a package, so you can stop it from seeing commands by implementing them in a subdirectory and then importing only your subclasses in a top level plugin; Sublime would then only see and create those commands but not the base command because it doesn’t know about it.

However, there’s nothing that says that you’re not allowed to just create any class that you want in your plugins, so there’s nothing stopping you from creating a base class that’s not a command subclass, then using that along with the appropriate class as the base classes for your commands.

For example, in OverrideAudit there is a collection of commands that are all meant to be invoked via context menu operations. That generally involves some glue code for being able to determine what’s under the cursor to know if the context is correct, handling contextual operations on tabs, and other code that’s 90% identical across all similar commands.

There is a ContextHelper class that handles all of this abstraction. All command classes that need to be contextually aware inherit from this class as well as the appropriate command class.

Which of these ideas is more pythonic, idiomatic, common, best, etc I don’t know (maybe neither and there’s an even better way); I’m certainly not a Python guru by any stretch.

2 Likes

#4

Thanks. I took your suggestion and used something like ContextHelper (a superclass that is not itself a subclass of TextCommand) and then used multiple inheritance to compose my classes. That worked. I tried using subdirectories, but I’m not a python expert, and I had some trouble getting the importing to work correctly given that I’m not sure how exactly sublime is loading/reloading plugins.

Speaking of sublime loading/reloading plugins, although I put everything in the top-level plugin directory, I’m still having a minor problem. My generic superclass (the ContextHelper equivalent) is in one python file, and the commands (derived classes) are in another. When I modify the superclass, its python file is reloaded, but the derived classes are unaffected unless I manually touch their source file as well. I know that reloading modules is not in general supported by python, so I’m not sure what exactly sublime is doing. Is there a way to get sublime to reload a dependent file? For instance, when superclass.py is modified, I would like sublime to reload superclass.py and derivedclass.py.

0 Likes

#5

Only Python files in the top level of a package are considered to be Plugins; any other .py file has no special significance to Sublime itself. Sublime only loads (and reloads) plugins, so the implication here is that python files not in the top level aren’t loaded unless some other file causes it to happen.

The rest of this post assumes a package structure laid out like as the following, with the given file contents:


User/
├── Preferences.sublime-settings
├── my_plugin.py
└── subdir
    └── base.py

my_plugin.py

import sublime
import sublime_plugin

from .subdir.base import SomeClass

def plugin_loaded():
	SomeClass().stuff()

subdir/base.py

class SomeClass():
	def stuff(self):
		print("I'm doing stuff!")

Sublime only considers my_plugin.py as a plugin, so:

  • That file is loaded at startup
  • Any time the file changes, it will be unloaded and then reloaded
  • If the package User is ignored, it will be unloaded (but don’t ignore your User package :wink:)
  • When the package is unignored, it will be reloaded
  • It’s top level plugin_loaded() and plugin_unloaded() (if any) will be invoked as appropriate

The code in base.py isn’t loaded by Sublime because it’s not considered a plugin, but my_plugin.py is using the import statement to import it and get the symbol, which is used when this plugin is loaded. Hence every time the top level plugin is loaded, I'm doing stuff! is printed to the Sublime console.

The import statement is using a relative import of .subdir.base, or “from the folder named subdir in the current directory, load the file base.py and give me the SomeClass symbol from it”. The import could also be from User.subdir.base import SomeClass if you don’t want to go the relative route.

This also points out that you can import symbols from any other package. One of the more common uses of that is from Default.exec import ExecCommand to get at the exec command from the Default plugin so you can subclass it to extend it, for example.

This is a common problem, and even the code outlined above has the same issue; when base.py is modified, it doesn’t get reloaded at all, so the changes aren’t seen.

Generally the issue is that when Python loads a module, the result of the load is cached in a table internally in the interpreter, and subsequent attempts to load that module cause the loader to just return back the result of the previous load.

In the case of the code above, that means that when my_plugin.py gets reloaded, it tells the interpreter to load base.py, and since it was previously loaded, you just get the same result back as the first time, which thus doesn’t notice that there are any changes.

The same also happens with the two top level files in your example. In that case, since both files are top-level you can get the result you want by first saving the second file to get it to reload, then saving the first file, which will try to import from it and get the new symbols.

You can cause a reload to happen by removing the entry in the cache for the module, which would make it load again the next time, or you can just get the system to reload the file directly.

AutomaticPackageReloader is a package that can make your life easy in this regard; it can ensure that everything gets reloaded, so while you’re working on your package things reload as you want them to.

You can also do something such as the following replacement for my_plugin.py above:

import sublime
import sublime_plugin

import imp
import sys

if "User.subdir.base" in sys.modules:
	print(" -> Reloading sub module")
	imp.reload(sys.modules["User.subdir.base"])

from User.subdir.base import SomeClass

def plugin_loaded():
	SomeClass().stuff()

Here at load time we check to see if the base.py file has already been loaded, and if it has we cause it to be reloaded before the import happens, so that the import takes the new code into account. If the module hasn’t been loaded yet, it does nothing and the import does the initial load.

This is a modified excerpt from the top level override_audit.py plugin, which ensures at load time that any sub-modules are reloaded if they were previously loaded, in case they changed. @ThomSmith is also working on (or at least considering working on) a dependency that abstracts this sort of logic away so you don’t have to re-implement it every time.

I would imagine that you shouldn’t do something like the above directly to cause a plugin to reload, because I think that would subvert the load/unload plugin mechanisms; for something like that you probably want to use the package I outlined above.

1 Like

#6

Thanks for the detailed information! It helps a lot to understand what’s going on.

0 Likes