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

Sublime Text 3 Creating Plugin adding external python libraries


So I'm trying to create a plugin and execute it with the context menu. In my plugin, I want to make use of the mechanical soup module.

I have configured the Context.sublime-menu file as follows to create the Context menu option:

[
{
    "id": "SlackSnippets",
    "caption": "SlackSnippets",
    "children":
    [
        {"id": "wrap"},
        { "command": "slacksnippet" }
    ]
}
]

Now, with this, the extreme basic functionality of the context menu works, it shows up when you right-click, and it's child command is clickable: Clickable Content.

However, where I'm running into trouble is when I then try to import the mechanical soup module in the python file where the 'SlackSnippet' command is defined:

from __future__ import print_function
import os
import mechanicalsoup """If Commented out, I can click the command in the context window"""
import sublime
import sublime_plugin


class SlacksnippetCommand(sublime_plugin.TextCommand):

    def run(self, edit):
        ## Code

The command in the Context menu then greys out and I am unable to use it: enter image description here

I am new to creating plugins in Sublime Text 3 and any help with this would be greatly appreciated!


Solution

  • The reason that your command is grayed out in the second case is because Sublime can't load the plugin with the line you mention in place. Thus it doesn't know about the command the menu entry is telling it to execute, and so it is disabled (if you were using the command palette, the command would not appear at all in this case).

    If you open the Sublime console with Ctrl+` or View > Show Console you'll see a stack trace saying this:

    ImportError: No module named 'mechanicalsoup'
    

    The first thing to note is that Sublime Text contains its own embedded version of Python 3.3.6 that is completely distinct from any version of Python you may have installed on your computer.

    As such, it is unable to load system installed Python modules by default because it doesn't know anything about them. It's possible to make Sublime look at system locations, but it's generally not a good idea because it introduces a lot of external dependencies that users of your plugin/package will need to install before your code works (including installing Python 3.3.6 itself).

    Generally speaking, the Python interpreter embedded in Sublime looks for Python files/modules in a few locations, but the only ones that are meant for users to interact with are:

    1. The Sublime Text Packages folder, which you can get to by selecting Preferences > Browse Packages from the menu
    2. A folder named Lib/python3.3 (Lib is stored in the same place as the Packages folder; use that command and go up one folder level)

    You have the option of putting your module (e.g. mechanicalsoup in the Packages folder. However, this is not a particular good idea because Sublime will try to load the top level Python source files automatically (because it thinks it is a package) which is not what you want and may not actually work except in simple cases.

    Sublime ignores any Python files that are not in the top level of a package folder unless you tell it explicitly to load it (i.e. via import). So you can put your modules directly inside of your package folder and import them from there.

    For example, if your package was named SlackSnippets, your package folder might look like this:

    SlackSnippets/
    |-- mechanicalsoup
    |   |-- __init__.py
    |   |-- __version__.py
    |   |-- browser.py
    |   |-- form.py
    |   |-- stateful_browser.py
    |   `-- utils.py
    `-- slack_snippet_cmd.py
    

    Now you can import the module by specifying the module name SlackSnippets.mechanicalsoup:

    from __future__ import print_function
    import os
    import SlackSnippets.mechanicalsoup 
    import sublime
    import sublime_plugin
    
    
    class SlacksnippetCommand(sublime_plugin.TextCommand):
        def run(self, edit):
            pass
    

    (Note: This still won't work because this module requires other modules to be installed, such as requests. I'm not sure how deep that particular rabbit hole goes, however, since I don't use this module.)

    Depending on the module in question and how it's written, there may be more work involved in getting things to work this way, such as if there are operating system specific libraries that are needed.

    That leaves us with the second option mentioned above, which is to put your modules in the Lib/python3.3 folder so Sublime will be able to find them.

    In this case since that folder is in the search path, your code as originally posted will load the module without any changes (although it will still fail to work here due to no requests module being available).

    You (and your users) are still still responsible for putting the module(s) there, though; Sublime doesn't do that sort of thing for you on it's own. If you plan on using Package Control to distribute your package when you're finished, you can get a little respite from this.

    Package Control supports the notion of dependency modules and with some set up will install modules for the user (if they're not already installed) when your package is installed.

    Currently this is handled by Package Control by putting the module into the Packages folder with a special package layout followed by doing some work behind the scenes so that Sublime will see it as a regular module.

    In order to get this to work you need to have the dependencies that you require also be stored in Package Control, which requires more set up work by you if the modules that you're interested in aren't already available.

    Currently mechanicalsoup is not available, although requests is. The list of available dependencies is here.