Динамический импорт модулей с последующей реализацией объектов с определенным базовым классом из указанных модулей

Я пишу приложение. Нет графического интерфейса: s или что-то еще, просто старое консольное приложение. Это приложение, позвонив ему в приложение, должно иметь возможность загружать плагины при запуске. Итак, естественно, я создал класс для наследуемых плагинов:

class PluginBase(object):
    def on_load(self):
        pass
    def on_unload(self):
        pass
    def do_work(self, data):
        pass

Идея заключается в том, что при запуске приложение будет проходить через текущий каталог, включая поддиры, и искать модули, содержащие классы, которые сами по себе являются подклассами PluginBase.

Больше кода:

class PluginLoader(object):
    def __init__(self, path, cls):
        """ path=path to search (unused atm), cls=baseclass """
        self.path=path
    def search(self):
        for root, dirs, files in os.walk('.'):
            candidates = [fname for fname in files if fname.endswith('.py') \
                                    and not fname.startswith('__')]
        ## this only works if the modules happen to be in the current working dir
        ## that is not important now, i'll fix that later
        if candidates:
            basename = os.path.split(os.getcwd())[1]
            for c in candidates:
                modname = os.path.splitext(c)[0]
                modname = '{0}.{1}'.format(basename, modname)
                __import__(mod)
                module = sys.modules[mod]

После этой последней строки в search я хотел бы как-то a) найти все классы в недавно загруженном модуле, b) проверить, являются ли один или несколько из этих классов подклассами PluginBase и c) (если b ) создайте экземпляр этого/этих классов и добавьте в список загруженных модулей приложения.

Я пробовал различные комбинации issubclass и других, за которым следовал период интенсивного dir: ing и около часа панически запущенного. Я нашел аналогичный подход к моему здесь, и я попробовал просто скопировать его, но получил сообщение о том, что Python не поддерживает импорт по имени файла, после чего я вид потерял мою концентрацию, и в результате этого этот пост был написан.

Я нахожусь на своем конце, все помогают оценить.

Ответ 1

Вы можете сделать что-то вроде этого:

for c in candidates:
    modname = os.path.splitext(c)[0]
    try:
        module=__import__(modname)   #<-- You can get the module this way
    except (ImportError,NotImplementedError):
        continue
    for cls in dir(module):          #<-- Loop over all objects in the module namespace
        cls=getattr(module,cls)
        if (inspect.isclass(cls)                # Make sure it is a class 
            and inspect.getmodule(cls)==module  # Make sure it was defined in module, not just imported
            and issubclass(cls,base)):          # Make sure it is a subclass of base
            # print('found in {f}: {c}'.format(f=module.__name__,c=cls))
            classList.append(cls)

Чтобы проверить вышеизложенное, мне пришлось немного изменить код; ниже приведен полный script.

import sys
import inspect
import os

class PluginBase(object): pass

def search(base):
    for root, dirs, files in os.walk('.'):
        candidates = [fname for fname in files if fname.endswith('.py') 
                      and not fname.startswith('__')]
        classList=[]
        if candidates:
            for c in candidates:
                modname = os.path.splitext(c)[0]
                try:
                    module=__import__(modname)
                except (ImportError,NotImplementedError):
                    continue
                for cls in dir(module):
                    cls=getattr(module,cls)
                    if (inspect.isclass(cls)
                        and inspect.getmodule(cls)==module
                        and issubclass(cls,base)):
                        # print('found in {f}: {c}'.format(f=module.__name__,c=cls))
                        classList.append(cls)
        print(classList)

search(PluginBase)

Ответ 2

Вы сделали бы это намного проще, если бы вы наложили ограничения на плагинов, например, что все плагины должны быть пакетами, которые содержат функцию load_plugin( app, config), которая возвращает экземпляр Plugin. Затем все, что вам нужно сделать, это попытаться импортировать эти пакеты и запустить функцию.

Ответ 3

Вот мета-классный способ регистрации плагинов:

Определите PluginBase как тип PluginType. PluginType автоматически регистрирует любой экземпляр (класс) в наборе plugins.

plugin.py:

plugins=set()
class PluginType(type):
    def __init__(cls, name, bases, attrs):
        super(PluginType, cls).__init__(name, bases, attrs)
        # print(cls, name,cls.__module__)
        plugins.add(cls)

class PluginBase(object):
    __metaclass__=PluginType
    pass

Это часть, которую пользователь пишет. Обратите внимание, что здесь нет ничего особенного.

плагинов, /myplugin.py:

import plugin
class Foo(plugin.PluginBase):
    pass

Вот что может выглядеть функция поиска:

test.py:

import plugin
import os
import imp

def search(plugindir):
    for root, dirs, files in os.walk(plugindir):
        for fname in files:
            modname = os.path.splitext(fname)[0]
            try:
                module=imp.load_source(modname,os.path.join(root,fname))
            except Exception: continue

search('pluginDir')
print(plugin.plugins)

Запуск результатов test.py

set([<class 'myplugin.Foo'>])

Ответ 4

Можете ли вы использовать execfile() вместо импорта с указанным пространством имен dict, затем перебрать это пространство имен с помощью isubclass и т.д.?