grim/pyscovery

3d85384af146
Parents
Children 231ec5dc3d81
initial version, only finds plugins in paths
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.project Sat Mar 23 01:31:37 2013 -0500
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>pyplugin</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.python.pydev.PyDevBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.python.pydev.pythonNature</nature>
+ </natures>
+</projectDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.pydevproject Sat Mar 23 01:31:37 2013 -0500
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?eclipse-pydev version="1.0"?>
+
+<pydev_project>
+<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
+<path>/pyplugin/src</path>
+<path>/pyplugin/tests</path>
+</pydev_pathproperty>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
+</pydev_project>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyplugin.py Sat Mar 23 01:31:37 2013 -0500
@@ -0,0 +1,64 @@
+import importlib
+import inspect
+
+PATHS = []
+
+def add_path(path):
+ """
+ Adds a path to search for plugins under
+ """
+
+ global PATHS # pylint:disable-msg=W0602
+
+ if not path in PATHS:
+ PATHS.append(path)
+
+
+def remove_path(path):
+ """
+ Removes a plugin search path
+ """
+
+ global PATHS # pylint:disable-msg=W0602
+
+ if path in PATHS:
+ PATHS.remove(path)
+
+
+def get_paths():
+ """
+ Returns the list of all plugin paths
+ """
+
+ return PATHS
+
+
+def find(cls):
+ """
+ Find all plugins that are subclasses of cls in the current search paths
+ """
+
+ if not inspect.isclass(cls):
+ raise TypeError('{} is not a class instance')
+
+ cls_name = cls.__name__
+
+ for path in PATHS:
+ mod = importlib.import_module(path)
+
+ for symbol_name in dir(mod):
+ if symbol_name == cls_name:
+ continue
+
+ symbol = getattr(mod, symbol_name, None)
+
+ if not inspect.isclass(symbol):
+ continue
+
+ if inspect.isabstract(symbol):
+ continue
+
+ yield symbol
+
+
+__all__ = [add_path, remove_path, get_paths, find]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/modules/__init__.py Sat Mar 23 01:31:37 2013 -0500
@@ -0,0 +1,3 @@
+"""
+This package contains modules with plugins
+"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/modules/mixed.py Sat Mar 23 01:31:37 2013 -0500
@@ -0,0 +1,46 @@
+"""
+A module with plugins and other stuff
+"""
+
+import abc
+
+from tests import Plugin
+
+CONSTANT = 3.14
+
+def func():
+ """
+ A function
+ """
+
+ pass
+
+class Simple(Plugin): # pylint:disable-msg=R0903
+ """
+ A simple plugin
+ """
+
+ pass
+
+
+class Abstract(Plugin): # pylint:disable-msg=R0903
+ """
+ A plugin with an abstract method
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def abstract(self): # pylint:disable-msg=R0201
+ """ an abstract method """
+ return
+
+
+class Concrete(Abstract): # pylint:disable-msg=R0903
+ """
+ A concrete implementation of Abstract
+ """
+
+ def abstract(self): # pylint:disable-msg=R0201
+ return
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/modules/multiple.py Sat Mar 23 01:31:37 2013 -0500
@@ -0,0 +1,21 @@
+"""
+A module with multiple plugins
+"""
+
+from tests import Plugin
+
+class First(Plugin): # pylint:disable-msg=R0903
+ """
+ The first plugin
+ """
+
+ pass
+
+
+class Second(Plugin): # pylint:disable-msg=R0903
+ """
+ The second plugin
+ """
+
+ pass
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/modules/single.py Sat Mar 23 01:31:37 2013 -0500
@@ -0,0 +1,12 @@
+"""
+Plugins in a module
+"""
+
+from tests import Plugin
+
+class Single(Plugin): # pylint:disable-msg=R0903
+ """
+ A simple plugin
+ """
+
+ pass
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/tests.py Sat Mar 23 01:31:37 2013 -0500
@@ -0,0 +1,195 @@
+"""
+Unit tests for pyplugin
+"""
+
+import inspect
+import unittest
+
+import pyplugin
+
+PATH = 'test'
+
+
+class Plugin(object): # pylint:disable-msg=R0903
+ """
+ The class all the test plugins descend from
+ """
+
+ pass
+
+
+class TestPaths(unittest.TestCase): # pylint:disable-msg=R0904
+ """
+ Unit tests for path manipulation
+ """
+
+ def _test_path_count(self, count):
+ """
+ Modular form to check how many paths are in the search paths
+ """
+
+ paths = pyplugin.get_paths()
+ self.assertEqual(len(paths), count,
+ 'search paths {}'.format(paths))
+
+
+ def setUp(self): # pylint:disable-msg=C0103
+ paths = pyplugin.get_paths()
+ for path in paths:
+ pyplugin.remove_path(path)
+
+
+ def tearDown(self): # pylint:disable-msg=C0103
+ self._test_path_count(0)
+
+
+ def test_add_remove(self):
+ """
+ Add and remove a single plugin path
+ """
+
+ pyplugin.add_path(PATH)
+ self._test_path_count(1)
+ pyplugin.remove_path(PATH)
+
+
+ def test_add_existing(self):
+ """
+ Add the same plugin path twice, and make sure it's only in there once
+ """
+
+ pyplugin.add_path(PATH)
+ pyplugin.add_path(PATH)
+ self._test_path_count(1)
+
+ pyplugin.remove_path(PATH)
+
+
+ def test_remove_nonexistant(self): # pylint:disable-msg=R0201
+ """
+ Try to remove a non-existant plugin path
+ """
+
+ pyplugin.remove_path(PATH)
+
+
+ def test_add_multiple(self):
+ """
+ Add multiple paths to the search paths
+ """
+
+ second = '{}.1'.format(PATH)
+
+ pyplugin.add_path(PATH)
+ self._test_path_count(1)
+
+ pyplugin.add_path(second)
+ self._test_path_count(2)
+
+ pyplugin.remove_path(PATH)
+ pyplugin.remove_path(second)
+
+
+class TestFind(unittest.TestCase): # pylint:disable-msg=R0904
+ """
+ Basic tests for the find function
+ """
+
+ def test_none(self):
+ """
+ Test that a TypeError is raised when find is called with None
+ """
+
+ self.assertRaises(TypeError, pyplugin.find(None))
+
+
+ def test_string(self):
+ """
+ Test that a TypeError is raised when find is called with a string
+ """
+
+ self.assertRaises(TypeError, pyplugin.find(''))
+
+
+ def test_int(self):
+ """
+ Test that a TypeError is raised when find is called with an int
+ """
+
+ self.assertRaises(TypeError, pyplugin.find(0))
+
+
+ def test_old_style_class(self): # pylint:disable-msg=R0201
+ """
+ Test that a TypeError is NOT raised when find is called with an old
+ style class
+ """
+
+ class Test: # pylint:disable-msg=R0903,W0232
+ """ old style class """
+ pass
+
+ pyplugin.find(Test)
+
+
+ def test_new_style_class(self): # pylint:disable-msg=R0201
+ """
+ Test that a TypeError is NOT raised when find is called with an new
+ style class
+ """
+
+ class Test(object): # pylint:disable-msg=R0903
+ """ new style class """
+ pass
+
+ pyplugin.find(Test)
+
+
+class TestModule(unittest.TestCase): # pylint:disable-msg=R0904
+ """
+ Tests the discovery method of pyplugin
+ """
+
+ def _test_path(self, path, count):
+ """
+ Given a module path, test that we find count plugins
+ """
+
+ self.assertEqual(len(pyplugin.get_paths()), 0)
+ pyplugin.add_path(path)
+
+ gen = pyplugin.find(Plugin)
+
+ self.assertTrue(inspect.isgenerator(gen))
+ self.assertEqual(len(list(gen)), count)
+
+
+ def setUp(self): # pylint:disable-msg=C0103
+ paths = pyplugin.get_paths()
+ for path in paths:
+ pyplugin.remove_path(path)
+
+
+ def test_single(self):
+ """
+ Test a module with a single plugin
+ """
+
+ self._test_path('modules.single', 1)
+
+
+ def test_multiple(self):
+ """
+ Test a module with multiple plugins
+ """
+
+ self._test_path('modules.multiple', 2)
+
+
+ def test_mixed(self):
+ """
+ Test a module with more than just plugins
+ """
+
+ self._test_path('modules.mixed', 2)
+