Summary
I am experimenting with a plugin pattern for a generic Python application (not necessarily web, desktop, or console) which would allow packages dropped into a plugin folder to be used according to the contract they would need to follow. In my case, this contract is simply to have a function calleddo_plugin_stuff(). I'd like the pattern to make sense for a system that sells plugins like the plugin store in Wordpress.
Minimal Python plugin mechanism is a decent question (despite being 4 years old) with some very good discussion about Django (which I haven't used) and how it allows for a plugin to be installed anywhere via pip. I'd see that as a phase two, because it seems like a pip-based plugin pattern is (sweeping generalization probably not always true) most valuable for purely free (as in money) plugin store. If free (as in open source) plugins are sold for money in a store, if seems that pip would be a poor choice for installation because which people might pay for something they'reabout to get the source code for and use freely / redistribute, they might be unlikely to pay / donate for something they've already installed.
Project Structure
Code
It'salso on GitHub under my same username (PaluMacil) and I made arelease tag of v1.0.0 to freeze the the repo at the code shown below.
app/plugins/blog/__init__.py
def do_plugin_stuff(): print("I'm a blog!")app/plugins/toaster/__init__.py
def do_plugin_stuff(): print("I'm a toaster!")app/plugins/__init__.py
(empty)app/__init__.py
from importlib import import_modulefrom os import path, listdirdef create_app(): app = Application() plugin_dir = path.join(path.dirname(__file__), 'plugins') import_string_list = [''.join(['.plugins.', d]) for d in listdir(plugin_dir) if path.isdir(path.join(plugin_dir, d)) and not d.startswith('__')] print(str(len(import_string_list)) + " imports to do...") for import_string in import_string_list: module = import_module(import_string, __package__) app.plugins.update({module.__name__.split('.')[2]: module}) print(str(len(app.plugins)) + " plugins in the app") return appclass Application: def __init__(self): self.plugins = {}The linenot d.startswith('__') eliminated my pychache dir from Pycharm.
run.py
from app import create_appfrom pprint import PrettyPrinterapp = create_app()app.plugins['toaster'].do_plugin_stuff()printer = PrettyPrinter(indent=4)printer.pprint(app.plugins.__repr__())Points for Review
I'm new enough to Python (very new but coming from a decent C# background, and I read PEP8 before attempting this) that I've never written a Python 2 application. I think my method of importing requires Python 3.3 or 3.4, though I'm not certain. Commentary on this might be nice. Ways of making this code accessible to earlier versions of Python seem to be messy; they involve conditional imports and such, which are verbose and ugly. If there is a trick or two that would make my code better for different versions of Python with minimal cruft, that would be great to see.
Am I missing anything that makes my code much more verbose than it should be? For instance, I'm iterating twice through the directories--once to make a list of packages, and again to make my dictionary. Would it be cleaner to make both parts one loop? The one-loop alternative seems verbose, but there could be further improvements, perhaps:
# Alternative to current code which uses a single loop:for d in listdir(plugin_dir): if path.isdir(path.join(plugin_dir, d)) and not d.startswith('__'): module = import_module(''.join(['.plugins.', d]), __package__) app.plugins.update({module.__name__.split('.')[2]: module})Ismodule.__name__.split('.')[2] a fragile way to get the value for my plugin dictionary? Would [-1] be a better index to use on the result of the split?
I'm having trouble understanding why I might chose to usepkgutil.iter_modules instead of my approach, but I'm wondering if there might be some benefit.It seems to be based onimportlib since Python 3.3 (PEP 302). Would the only difference be that I wouldn't pull in a folder that doesn't have an__init__.py inside it to make it a package?
- \$\begingroup\$Your import syntax looks fine to me and the imports seem to run in Python2.7. Why are you concerned they might be version specific?\$\endgroup\$SuperBiasedMan– SuperBiasedMan2015-09-04 14:22:40 +00:00CommentedSep 4, 2015 at 14:22
- \$\begingroup\$Thanks for the 2.7 run, @SuperBiasedMan. I think I came to that incorrect conclusion based upon
importlib.reloadbeing new in Python 3.4. I had been considering expanding this code to include a way of reloading plugins using that. I decided to wait on that becauseimportlib.invalidate_caches()(new in 3.3) seems to be a package I would need to understand and play with first, which could take a while. In short, I think, my intention to use those to libraries is the culprit of my compatibility question, and I should have installed 2.7 to check myself before posting that part of the question.\$\endgroup\$Palu Macil– Palu Macil2015-09-04 14:29:34 +00:00CommentedSep 4, 2015 at 14:29 - \$\begingroup\$Ah I see, you meant the stuff you were doing with
importlib. I'm not as familiar with that. I can tell you that Python 2.7 does have it and can import modules that way but I don't know enough to say it's as compatible as you need.\$\endgroup\$SuperBiasedMan– SuperBiasedMan2015-09-04 14:31:52 +00:00CommentedSep 4, 2015 at 14:31
1 Answer1
You shouldn't callstr on theint returned fromlen, instead usestr.format.
"{} plugins in the app".format(len(app.plugins))Format will coerce the int to a string implicitly and is neater to read.
Also you're callingrepr backwards. The whole point of an object having a__repr__ function is that it allows an object to be passed torepr(). So you could changeapp.plugins.__repr__() torepr(app.plugins).
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.
