Skip to content

Commit

Permalink
GH-#32: plugin system to load custom commands
Browse files Browse the repository at this point in the history
  • Loading branch information
schettino72 committed Mar 24, 2015
1 parent d321241 commit 8091428
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 6 deletions.
4 changes: 3 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ Changes
0.28.0
======

- fix GH-#22: Allow to customize how file_dep are checked
- BACKWARD INCOMPATIBLE: signature for custom DB backend changed
- GH-#22: Allow to customize how file_dep are checked
- GH-#31: Add IPython `%doit` magic-function loading tasks from its global
namespace
- GH-#32 plugin system
- read configuration options from INI files
- fix issue when using unicode strings to specify `minversion` on python 2
- fix GH-#27 auto command in conjunction with task arguments

Expand Down
49 changes: 47 additions & 2 deletions doit/doit_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import os
import sys
import itertools
import traceback
import importlib
from collections import defaultdict
import six
from six.moves import configparser

from .version import VERSION
from .exceptions import InvalidDodoFile, InvalidCommand, InvalidTask
Expand Down Expand Up @@ -37,14 +41,55 @@ def set_var(name, value):



class PluginRegistry(object):
"""simple plugin system to load code dynamically
Plugins must be explicitly loaded (no scanning of files and folders is done
to find plugins).
There is no requirements of interface or checking of loaded plugins (that's
up to application code if desirable). The registry is responsible only for
loading a piece of code and keeping a reference to it.
"""
def __init__(self):
self._plugins = defaultdict(list) # category: value

def __getitem__(self, key):
"""get all plugins from a given category"""
return self._plugins[key]

def add(self, category, module_name, obj_name):
"""get reference to obj from named module/obj"""
module = importlib.import_module(module_name)
self._plugins[category].append(getattr(module, obj_name))



class DoitMain(object):
DOIT_CMDS = (Help, Run, List, Info, Clean, Forget, Ignore, Auto, DumpDB,
Strace, TabCompletion)
TASK_LOADER = DodoTaskLoader

def __init__(self, task_loader=None):
def __init__(self, task_loader=None, config_filenames='doit.cfg'):

This comment has been minimized.

Copy link
@ankostis

ankostis Mar 24, 2015

Contributor

Shouldn't it be either "hidden" file (.doit.cfg) or (dodo.cfg)?

This comment has been minimized.

Copy link
@schettino72

schettino72 Mar 24, 2015

Author Member

To use doit.cfg or .doit.cfg is a matter of taste. We could support both to avoid the bikeshedding :)

dodo.cfg doesn't make sense to me. You are configuring doit. dodo.py is the file where you write tasks for doit, but this name is just a conventions. And soon using doit.cfg you will be able to change the default name for the file where you write tasks...

This comment has been minimized.

Copy link
@ankostis

ankostis Mar 24, 2015

Contributor

Well, at the moment it is bikeshedding :-), but i came to this issue while i was trying to set the --db-file param on ipython (see @3377a13cf0). Obviously the *cfg file would need to be sourced from the same folder.

Anyway, it might be worthwhile to have a site-wide ~/.doit.cfg and a project-wide dodo.cfg [EDIT: or doit.cfg or .doit.cfg, not arguing here].
Unless a setuptools plugin architecture is used.

self.task_loader = task_loader if task_loader else self.TASK_LOADER()
self.sub_cmds = {} # dict with available sub-commands
self.plugins = PluginRegistry()
self.config = configparser.SafeConfigParser(allow_no_value=True)
self.config.optionxform = str # preserve case of option names
self.load_config_ini(config_filenames)


def load_config_ini(self, filenames):
"""read config from INI files
:param files: str or list of str.
Like ConfigParser.read() param filenames
"""
self.config.read(filenames)
for name, _ in self.config.items('command'):
obj_name, mod_name = name.split('@')
self.plugins.add('command', mod_name, obj_name)


@staticmethod
def print_version():
Expand All @@ -57,7 +102,7 @@ def get_commands(self):
"""get all sub-commands"""
sub_cmds = {}
# core doit commands
for cmd_cls in (self.DOIT_CMDS):
for cmd_cls in itertools.chain(self.DOIT_CMDS, self.plugins['command']):
if issubclass(cmd_cls, DoitCmdBase):
cmd = cmd_cls(task_loader=self.task_loader)
cmd.doit_app = self # hack used by Help/TabComplete command
Expand Down
3 changes: 2 additions & 1 deletion doit/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import sys
import inspect
import importlib
import six
from collections import OrderedDict

Expand Down Expand Up @@ -93,7 +94,7 @@ def exist_or_raise(path):
os.chdir(full_cwd)

# get module containing the tasks
return __import__(os.path.splitext(file_name)[0])
return importlib.import_module(os.path.splitext(file_name)[0])



Expand Down
4 changes: 4 additions & 0 deletions tests/sample.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[command]

MyCmd@tests.sample_plugin

10 changes: 10 additions & 0 deletions tests/sample_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from doit.cmd_base import Command

class MyCmd(Command):
name = 'mycmd'
doc_purpose = 'test extending doit commands'
doc_usage = '[XXX]'
doc_description = 'my command description'

def execute(self, opt_values, pos_args): # pragma: no cover
print("this command does nothing!")
35 changes: 33 additions & 2 deletions tests/test_doit_cmd.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
import os

import pytest
from mock import Mock

from doit import get_var
from doit.exceptions import InvalidCommand
from doit.doit_cmd import DoitMain
from doit.cmd_run import Run
from doit.cmd_list import List
from doit import doit_cmd



class TestPluginRegistry(object):
def test_get_empty_list_for_whatever_category(self):
plugins = doit_cmd.PluginRegistry()
assert [] == plugins['foo']
assert [] == plugins['whatever name']

def test_add_many(self):
plugins = doit_cmd.PluginRegistry()
plugins.add('category1', 'pytest', 'raises')
plugins.add('category1', 'mock', 'Mock')
plugins.add('category2', 'doit.cmd_run', 'Run')
assert 2 == len(plugins['category1'])
assert pytest.raises is plugins['category1'][0]
assert Mock is plugins['category1'][1]
assert 1 == len(plugins['category2'])
assert Run is plugins['category2'][0]


class TestLoadINI(object):
def test_load_plugins_command(self):
config_filename = os.path.join(os.path.dirname(__file__), 'sample.cfg')
main = doit_cmd.DoitMain(config_filenames=config_filename)
assert 1 == len(main.plugins['command'])
assert main.plugins['command'][0].name == 'mycmd'
# test loaded plugin command is actually used
assert 'mycmd' in main.get_commands()


def cmd_main(args):
return DoitMain().run(args)
return doit_cmd.DoitMain().run(args)


class TestRun(object):
Expand Down

0 comments on commit 8091428

Please sign in to comment.