6

I have a project in pure Python with a rudimentary plugin system: you write a module that defines a class with a specific interface and name, and the program imports the module and subsequently instantiates the class as needed.

Currently, the plugins all come from a specific folder (subdirectory of where the main .py file is located). I would like to be able to have them elsewhere on disk, and instruct the program to look for plugins in a specific place. Can I do this, for one-off dynamic imports, in a cleaner way than modifying sys.path? I don't want to pollute this global.

Related: can I count on sys.path[0] being the path to the script, even if that differs from the current working directory (os.getcwd())?

EDIT: I forgot to mention - I want to be able to get plugins from several different folders, with the user specifying paths to plugin folders. Currently, each of these folders is set up as a package (with an __init__.py); I can trivially scrap this if it causes a problem.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • 2
    For path to current script you could use `__file__` – Owen Aug 28 '11 at 02:36
  • Or, if what you mean is, can you rely on always being able to import modules in the same directory as the script, I have run across cases where this failed, and I wish I could say exactly why, but I was doing some freaky stuff. – Owen Aug 28 '11 at 02:40
  • Using `__file__` for that part seems cleaner, thank you. It gives a relative path including the file name itself, but that can easily be massaged with `os.path` functionality. – Karl Knechtel Aug 28 '11 at 02:46
  • I think your question was answered [here](http://stackoverflow.com/questions/67631/how-to-import-a-module-given-the-full-path) – rodrigomanhaes Aug 28 '11 at 02:55
  • @rodrigomanhaes I thought of that a bit later, but I'd prefer if possible not to have to re-implement the "look for a .pyc; failing that, look for a .py" logic. – Karl Knechtel Aug 28 '11 at 03:15
  • The "just modify `sys.path`" approach seems to work much more cleanly without the packaging. I also realized that I'll eventually need to resolve conflicts, i.e. multiple folders with identically-named plugins. – Karl Knechtel Aug 28 '11 at 07:07

1 Answers1

6

This might seem weird, but you can modify a module's __path__ variable and then import from it. Then you're not messing with the global import space in sys.path.

Edit: If the directories are loaded at run time, then you don't need a plugins.py file to store them. You can create the module dynamically:

main.py:

#create the plugins module (pseudo-package)

import sys, os

sys.modules['plugins'] = plugins = type(sys)('plugins')

plugins.__path__ = []
for plugin_dir in ['plugins1', 'plugins2']:
    path = os.path.join(sys.path[0], 'addons', plugin_dir)
    plugins.__path__.append(path)

After creating the dynamic module, you can load the plugins as before, using either import_module or __import__:

from importlib import import_module

myplugins = []
for plugin in ['myplugin1', 'myplugin2']:
    myplugins.append(import_module('plugins.' + plugin))
    myplugins[-1].init()

##or using __import__:

myplugins = []
for plugin in ['myplugin1', 'myplugin2']:
    myplugins.append(getattr(__import__('plugins.' + plugin), plugin))
    myplugins[-1].init()

addons/plugins1/myplugin1.py:

def init():
    print('myplugin1')

addons/plugins2/myplugin2.py:

def init():
    print('myplugin2')

I've never used this, but it does work in both Python 2 & 3.

Eryk Sun
  • 33,190
  • 5
  • 92
  • 111
  • It might not have been clear - I want to be able to specify multiple paths and have each of them searched when looking for a plugin. – Karl Knechtel Aug 28 '11 at 04:27
  • I think I'll have to experiment with this as well. – Karl Knechtel Aug 28 '11 at 04:37
  • `importlib` appears to be specific to 3.x. – Karl Knechtel Aug 28 '11 at 05:43
  • Ok, now I see how this works, and it doesn't seem to address my needs. I need to be able to set the list of import locations at runtime, and modifying `__path__` for an already-imported module at runtime doesn't seem to work. – Karl Knechtel Aug 28 '11 at 06:38
  • Much thanks, and sorry to give you such a run-around >_< My tests at the REPL with modifying `__path__` on a blank module didn't work (haven't been able to figure out why exactly), but it does work in script with the `__path__` of an `__init__.py`. Creating the module dynamically is a nice trick but I don't need it; there is one "base" directory of plugins that I can treat as a package, and modify its `__path__` at runtime to refer to additional plugin folders. – Karl Knechtel Aug 28 '11 at 09:48