Programing

파이썬에서 최소 플러그인 아키텍처 구축

lottogame 2020. 5. 19. 08:04
반응형

파이썬에서 최소 플러그인 아키텍처 구축


필자는 상당히 기술적 인 청중 (과학자)이 사용하는 Python으로 작성된 응용 프로그램을 가지고 있습니다.

사용자가 응용 프로그램을 확장 가능하게 만드는 좋은 방법, 즉 스크립팅 / 플러그인 아키텍처를 찾고 있습니다.

나는 매우 가벼운 것을 찾고 있습니다. 대부분의 스크립트 또는 플러그인은 타사에서 개발 및 배포하지 않고 몇 분만에 반복 작업을 자동화하고 파일 형식에 대한 지원을 추가하기 위해 사용자가 채 웁니다 따라서 플러그인은 절대 최소 상용구 코드를 가져야하며 폴더에 복사하는 것 외에 '설치'가 필요하지 않습니다 (따라서 setuptools 진입 점 또는 Zope 플러그인 아키텍처와 같은 것).

이와 같은 시스템이 이미 있거나 아이디어 / 영감을 살펴 봐야하는 유사한 체계를 구현하는 프로젝트가 있습니까?


Mine은 기본적으로 "plugins"라는 디렉토리로, 메인 앱이 폴링 한 다음 imp.load_module사용 하여 파일을 픽업하고, 모듈 수준 구성 매개 변수를 사용하여 잘 알려진 진입 점을 찾은 다음 거기서 갈 수있는 디렉토리 입니다. 플러그인이 활성화 된 특정 양의 역 동성을 위해 파일 모니터링 기능을 사용하지만 사용하기 편리합니다.

물론, "[복잡한 것 X은 필요하지 않습니다. 단지 가벼운 것을 원합니다."라고 말하는 모든 요구 사항은 한 번에 하나의 발견 된 요구 사항을 다시 구현할 위험이 있습니다. 그러나 그것은 당신이 어쨌든 재미있는 일을 할 수 없다고 말하는 것은 아닙니다 :)


module_example.py:

def plugin_main(*args, **kwargs):
    print args, kwargs

loader.py:

def load_plugin(name):
    mod = __import__("module_%s" % name)
    return mod

def call_plugin(name, *args, **kwargs):
    plugin = load_plugin(name)
    plugin.plugin_main(*args, **kwargs)

call_plugin("example", 1234)

확실히 "최소"이며, 오류 검사가 전혀 없으며, 수많은 보안 문제가있을 수 있으며, 매우 유연하지는 않습니다. 그러나 파이썬의 플러그인 시스템이 얼마나 간단한 지 보여줍니다 ..

당신은 아마 보길 원하는 꼬마 도깨비 당신이 단지와 많이 할 수 있지만, 너무 모듈 __import__, os.listdir일부 문자열 조작을.


기존 플러그인 프레임 워크 / 라이브러리에 대한이 개요를 살펴보면 좋은 출발점이됩니다. 나는 yapsy를 매우 좋아 하지만 사용 사례에 달려 있습니다.


그 질문은 정말 흥미롭지 만 자세한 내용은 대답하기가 상당히 어렵다고 생각합니다. 어떤 종류의 응용입니까? GUI가 있습니까? 명령 줄 도구입니까? 스크립트 세트? 독특한 진입 점 등을 가진 프로그램 ...

내가 가진 작은 정보를 감안할 때, 나는 매우 일반적인 방식으로 답변 할 것입니다.

플러그인을 추가해야하는 것은 무엇입니까?

  • 로드 할 경로 / 디렉토리를 나열하는 구성 파일을 추가해야합니다.
  • 다른 방법은 "플러그인 / 디렉토리에있는 모든 파일이로드됩니다"라고 말하지만 사용자가 파일을 이동해야하는 불편 함이 있습니다.
  • 마지막으로 중간 옵션은 모든 플러그인이 동일한 plugin / 폴더에 있어야하고 구성 파일의 상대 경로를 사용하여 활성화 / 비활성화하는 것입니다.

순수한 코드 / 디자인 실습에서는 사용자가 확장 할 행동 / 특정 동작을 명확하게 결정해야합니다. 항상 대체 될 공통 진입 점 / 기능 세트를 식별하고 이러한 조치 내의 그룹을 결정하십시오. 이 작업이 완료되면 응용 프로그램을 쉽게 확장 할 수 있어야합니다.

MediaWiki에서 영감을 얻은 hooks 사용 예제 (PHP이지만 언어는 실제로 중요합니까?) :

import hooks

# In your core code, on key points, you allow user to run actions:
def compute(...):
    try:
        hooks.runHook(hooks.registered.beforeCompute)
    except hooks.hookException:
        print('Error while executing plugin')

    # [compute main code] ...

    try:
        hooks.runHook(hooks.registered.afterCompute)
    except hooks.hookException:
        print('Error while executing plugin')

# The idea is to insert possibilities for users to extend the behavior 
# where it matters.
# If you need to, pass context parameters to runHook. Remember that
# runHook can be defined as a runHook(*args, **kwargs) function, not
# requiring you to define a common interface for *all* hooks. Quite flexible :)

# --------------------

# And in the plugin code:
# [...] plugin magic
def doStuff():
    # ....
# and register the functionalities in hooks

# doStuff will be called at the end of each core.compute() call
hooks.registered.afterCompute.append(doStuff)

수은에서 영감을 얻은 또 다른 예. 여기서 확장은 hg 명령 줄 실행 파일 에만 명령을 추가 하여 동작을 확장합니다.

def doStuff(ui, repo, *args, **kwargs):
    # when called, a extension function always receives:
    # * an ui object (user interface, prints, warnings, etc)
    # * a repository object (main object from which most operations are doable)
    # * command-line arguments that were not used by the core program

    doMoreMagicStuff()
    obj = maybeCreateSomeObjects()

# each extension defines a commands dictionary in the main extension file
commands = { 'newcommand': doStuff }

두 가지 방법 모두 확장에 대한 공통 초기화마무리 가 필요할 수 있습니다 . 모든 확장이 구현해야하는 공통 인터페이스를 사용하거나 (두 번째 접근 방식에 더 적합합니다. 머큐리얼은 모든 확장에 대해 호출되는 reposetup (ui, repo)를 사용합니다) 또는 훅 종류의 접근 방식을 사용할 수 있습니다. hooks.setup 후크.

그러나 더 유용한 답변을 원한다면 질문 범위를 좁혀 야합니다.)


Marty Allchin의 간단한 플러그인 프레임 워크 는 내가 필요로하는 기반입니다. 나는 그것을 보라고 정말로 권합니다. 단순하고 쉽게 해킹 할 수있는 것을 원한다면 정말 좋은 시작이라고 생각합니다. Django Snippets 로도 찾을 수 있습니다 .


저는 디지털 마이크로 그래프를 다루고 SGi 머신에서 실행하기 위해 이미지 처리 및 분석 패키지 (기술적으로 라이브러리가 아님)를 작성해야하는 은퇴 한 생물 학자입니다. C로 코드를 작성하고 스크립팅 언어로 Tcl을 사용했습니다. GUI는 Tk를 사용하여 수행되었습니다. Tcl에 나타난 명령은 "extensionName commandName arg0 arg1 ... param0 param1 ..."형식, 즉 공백으로 구분 된 간단한 단어와 숫자입니다. Tcl이 "extensionName"하위 문자열을 보았을 때 제어는 C 패키지로 전달되었습니다. 그런 다음 lexer / parser (lex / yacc에서 수행)를 통해 명령을 실행 한 다음 필요에 따라 C 루틴을 호출했습니다.

패키지를 조작하는 명령은 GUI의 창을 통해 하나씩 실행될 수 있지만 배치 작업은 유효한 Tcl 스크립트 인 텍스트 파일을 편집하여 수행되었습니다. 원하는 파일 수준 작업을 수행 한 템플릿을 선택한 다음 실제 디렉토리 및 파일 이름과 패키지 명령을 포함하도록 복사본을 편집합니다. 그것은 매력처럼 작동했습니다. ...까지

1) 세계는 PC로 바뀌었고 2) Tcl의 iffy 조직 구성 기능이 실제로 불편 해지기 시작했을 때 스크립트는 약 500 줄을 넘었습니다. 시간이 지났다 ...

나는 은퇴했고, 파이썬은 발명되었고, Tcl의 완벽한 후계자처럼 보였다. PC에서 C 프로그램을 컴파일하고 C 패키지로 Python을 확장하며 Python / Gt? / Tk? /? ?. 그러나 편집 가능한 템플릿 스크립트를 사용한다는 오래된 아이디어는 여전히 실행 가능한 것으로 보입니다. 또한 다음과 같이 네이티브 파이썬 형식으로 패키지 명령을 입력하는 것이 너무 큰 부담이되어서는 안됩니다.

packageName.command (arg0, arg1, ..., param0, param1, ...)

몇 가지 추가 점, 괄호 및 쉼표가 있지만 표시되지 않습니다.

누군가 파이썬에서 lex 및 yacc 버전을 시도한 것을 기억합니다 (시도 : http://www.dabeaz.com/ply/ ). 그래도 필요한 경우 주변에 있습니다.

이 혼란의 요점은 파이썬 자체가 과학자들이 사용할 수있는 원하는 "경량"프론트 엔드 인 것 같습니다. 왜 그렇지 않다고 생각하는지 궁금합니다. 진심입니다.


나중에 추가 : 응용 프로그램 gedit 은 플러그인이 추가 될 것으로 예상하며 해당 사이트에는 몇 분 동안 둘러 본 간단한 플러그인 절차에 대한 가장 명확한 설명이 있습니다. 시험:

https://wiki.gnome.org/Apps/Gedit/PythonPluginHowToOld

여전히 귀하의 질문을 더 잘 이해하고 싶습니다. 1) 과학자가 (Python) 응용 프로그램을 다양한 방식으로 매우 간단하게 사용할 수 있기를 원하는지 또는 2) 과학자가 응용 프로그램에 새로운 기능을 추가 할 수 있는지 여부는 확실하지 않습니다. 선택 # 1은 이미지에 직면 한 상황으로, 현재의 필요에 맞게 수정 된 일반 스크립트를 사용하게되었습니다. 플러그인 아이디어로 연결되는 선택 # 2입니까, 아니면 명령을 실행하는 것이 불가능한 응용 프로그램의 일부입니까?


Python Decorators를 검색 할 때 간단하지만 유용한 코드 스 니펫을 찾았습니다. 그것은 당신의 필요에 맞지 않지만 매우 고무적 일 수 있습니다.

Scipy 고급 Python # Plugin 등록 시스템

class TextProcessor(object):
    PLUGINS = []

    def process(self, text, plugins=()):
        if plugins is ():
            for plugin in self.PLUGINS:
                text = plugin().process(text)
        else:
            for plugin in plugins:
                text = plugin().process(text)
        return text

    @classmethod
    def plugin(cls, plugin):
        cls.PLUGINS.append(plugin)
        return plugin


@TextProcessor.plugin
class CleanMarkdownBolds(object):
    def process(self, text):
        return text.replace('**', '')

용법:

processor = TextProcessor()
processed = processor.process(text="**foo bar**", plugins=(CleanMarkdownBolds, ))
processed = processor.process(text="**foo bar**")

Pycon 2009의 Andre Roberge 박사가 제공 한 다양한 플러그인 아키텍처에 대한 멋진 토론을 즐겼습니다. 그는 플러그인을 구현하는 다양한 방법에 대한 훌륭한 개요를 제공합니다.

A와 사용 가능한 팟 캐스트 일련의 동반 (원숭이 패치에 대한 설명은 다음 두 번째 부분) 여섯 개 블로그 항목 .

결정을 내리기 전에 빨리 들어 보는 것이 좋습니다.


나는 최소한의 플러그인 아키텍처를 찾기 위해 여기에 도착했고, 모든 것이 나에게 과장된 것처럼 보였습니다. 그래서 저는 Super Simple Python Plugins를 구현했습니다 . 이를 사용하려면 하나 이상의 디렉토리를 작성 __init__.py하고 각 디렉토리에 특수 파일을 삭제하십시오 . 해당 디렉토리를 가져 오면 다른 모든 Python 파일이 하위 모듈로로드되고 해당 이름이 __all__목록에 배치됩니다 . 그런 다음 해당 모듈의 유효성을 검사 / 초기화 / 등록하는 것은 사용자의 책임입니다. README 파일에 예제가 있습니다.


실제로 setuptools 는 프로젝트 문서에서 가져온 다음 예제와 같이 "plugins directory"와 함께 작동합니다. http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins

사용법 예 :

plugin_dirs = ['foo/plugins'] + sys.path
env = Environment(plugin_dirs)
distributions, errors = working_set.find_plugins(env)
map(working_set.add, distributions)  # add plugins+libs to sys.path
print("Couldn't load plugins due to: %s" % errors)

장기적으로 setuptools 는 충돌이나 요구 사항없이 플러그인을로드 할 수 있기 때문에 훨씬 안전합니다.

또 다른 이점은 원래 응용 프로그램이 신경 쓰지 않아도 동일한 메커니즘을 사용하여 플러그인 자체를 확장 할 수 있다는 것입니다.


플러그인 시스템에 대한 또 다른 접근 방식으로 Extend Me project를 확인할 수 있습니다 .

예를 들어 간단한 클래스와 그 확장을 정의 해 봅시다

# Define base class for extensions (mount point)
class MyCoolClass(Extensible):
    my_attr_1 = 25
    def my_method1(self, arg1):
        print('Hello, %s' % arg1)

# Define extension, which implements some aditional logic
# or modifies existing logic of base class (MyCoolClass)
# Also any extension class maby be placed in any module You like,
# It just needs to be imported at start of app
class MyCoolClassExtension1(MyCoolClass):
    def my_method1(self, arg1):
        super(MyCoolClassExtension1, self).my_method1(arg1.upper())

    def my_method2(self, arg1):
        print("Good by, %s" % arg1)

그리고 그것을 사용하십시오 :

>>> my_cool_obj = MyCoolClass()
>>> print(my_cool_obj.my_attr_1)
25
>>> my_cool_obj.my_method1('World')
Hello, WORLD
>>> my_cool_obj.my_method2('World')
Good by, World

그리고 장면 뒤에 숨겨진 것을 보여주십시오.

>>> my_cool_obj.__class__.__bases__
[MyCoolClassExtension1, MyCoolClass]

extend_me 따라서 예에서, 메타 클래스를 통해 라이브러리를 조작 클래스 생성 과정을, 위의 새로운 인스턴스를 생성 할 때 MyCoolClass, 우리 모두의 서브 클래스 새 클래스의 인스턴스를 가지고 MyCoolClassExtensionMyCoolClass파이썬에 모두의 필요 기능 덕분에 다중 상속

클래스 작성을보다 잘 제어하기 위해이 lib에 정의 된 메타 클래스가 거의 없습니다.

  • ExtensibleType -서브 클래 싱으로 간단한 확장 성을 허용

  • ExtensibleByHashType -ExtensibleType과 유사하지만 특수 버전의 클래스를 구축 할 수있어 기본 클래스의 글로벌 확장 및 특수 버전의 클래스 확장 가능

이 lib는 OpenERP Proxy Project 에서 사용되며 충분히 작동하는 것 같습니다!

실제 사용 예를 보려면 OpenERP 프록시 'field_datetime'확장자를 확인하십시오 .

from ..orm.record import Record
import datetime

class RecordDateTime(Record):
    """ Provides auto conversion of datetime fields from
        string got from server to comparable datetime objects
    """

    def _get_field(self, ftype, name):
        res = super(RecordDateTime, self)._get_field(ftype, name)
        if res and ftype == 'date':
            return datetime.datetime.strptime(res, '%Y-%m-%d').date()
        elif res and ftype == 'datetime':
            return datetime.datetime.strptime(res, '%Y-%m-%d %H:%M:%S')
        return res

Record여기에 extesible 객체가 있습니다. RecordDateTime확장입니다.

확장 기능을 사용하려면 확장 클래스가 포함 된 모듈을 가져 오십시오. 위의 경우 Record생성 된 모든 객체는 기본 클래스에 확장 클래스가 있으므로 모든 기능을 갖습니다.

이 라이브러리의 주요 장점은 확장 가능한 객체를 운영하는 코드는 확장에 대해 알 필요가 없으며 확장은 확장 가능한 객체의 모든 것을 변경할 수 있다는 것입니다.


setuptools에는 EntryPoint가 있습니다 .

진입 점은 다른 배포판에서 사용할 수 있도록 배포에서 Python 객체 (예 : 함수 또는 클래스)를 "광고"하는 간단한 방법입니다. 확장 가능한 응용 프로그램 및 프레임 워크는 특정 배포 또는 sys.path의 모든 활성 배포에서 특정 이름 또는 그룹의 진입 점을 검색 한 다음 원하는대로 광고 된 개체를 검사하거나로드 할 수 있습니다.

AFAIK이 패키지는 pip 또는 virtualenv를 사용하는 경우 항상 사용 가능합니다.


에서 @ edomaur의 대답에 확장하면 내가 한 번 봐 복용 제안 할 수 simple_plugins 에 의해 영감을 간단한 플러그인 프레임 워크 (뻔뻔한 플러그), 마티 Alchin의 일을 .

프로젝트의 README에 기반한 간단한 사용법 예 :

# All plugin info
>>> BaseHttpResponse.plugins.keys()
['valid_ids', 'instances_sorted_by_id', 'id_to_class', 'instances',
 'classes', 'class_to_id', 'id_to_instance']

# Plugin info can be accessed using either dict...
>>> BaseHttpResponse.plugins['valid_ids']
set([304, 400, 404, 200, 301])

# ... or object notation
>>> BaseHttpResponse.plugins.valid_ids
set([304, 400, 404, 200, 301])

>>> BaseHttpResponse.plugins.classes
set([<class '__main__.NotFound'>, <class '__main__.OK'>,
     <class '__main__.NotModified'>, <class '__main__.BadRequest'>,
     <class '__main__.MovedPermanently'>])

>>> BaseHttpResponse.plugins.id_to_class[200]
<class '__main__.OK'>

>>> BaseHttpResponse.plugins.id_to_instance[200]
<OK: 200>

>>> BaseHttpResponse.plugins.instances_sorted_by_id
[<OK: 200>, <MovedPermanently: 301>, <NotModified: 304>, <BadRequest: 400>, <NotFound: 404>]

# Coerce the passed value into the right instance
>>> BaseHttpResponse.coerce(200)
<OK: 200>

파이썬에서 플러그인 프레임 워크를 검색하는 동안이 스레드를 읽는 데 시간을 보냈습니다. 나는 몇 가지를 사용했지만 그들에게 단점 이있었습니다. 다음은 인터페이스가없고 느슨하게 연결된 플러그인 관리 시스템 인 2017 년의 조사를 위해 제시 한 내용입니다 . 나중에로드 . 사용 방법에 대한 자습서 는 다음과 같습니다 .


pluginlib 를 사용할 수 있습니다 .

플러그인은 작성하기 쉽고 다른 패키지, 파일 경로 또는 진입 점에서로드 할 수 있습니다.

필요한 메소드를 정의하여 플러그인 상위 클래스를 작성하십시오.

import pluginlib

@pluginlib.Parent('parser')
class Parser(object):

    @pluginlib.abstractmethod
    def parse(self, string):
        pass

상위 클래스를 상속하여 플러그인을 작성하십시오.

import json

class JSON(Parser):
    _alias_ = 'json'

    def parse(self, string):
        return json.loads(string)

플러그인을로드하십시오.

loader = pluginlib.PluginLoader(modules=['sample_plugins'])
plugins = loader.plugins
parser = plugins.parser.json()
print(parser.parse('{"json": "test"}'))

필자는 필자의 요구에 맞는 작은 플러그인 시스템을 찾으려고 많은 시간을 보냈다. 그러나 나는 자연스럽고 유연한 상속이 이미 있다면 그것을 사용하지 않을 것이라고 생각했습니다.

플러그인에 상속을 사용할 때의 유일한 문제는 가장 구체적인 (상속 트리에서 가장 낮은) 플러그인 클래스가 무엇인지 모른다는 것입니다.

그러나 이것은 기본 클래스의 상속을 추적하는 메타 클래스로 해결할 수 있으며 대부분의 특정 플러그인에서 상속되는 클래스를 작성할 수 있습니다 (아래 그림의 'Root extended')

여기에 이미지 설명을 입력하십시오

그래서 나는 그러한 메타 클래스를 코딩하여 해결책을 찾았습니다.

class PluginBaseMeta(type):
    def __new__(mcls, name, bases, namespace):
        cls = super(PluginBaseMeta, mcls).__new__(mcls, name, bases, namespace)
        if not hasattr(cls, '__pluginextensions__'):  # parent class
            cls.__pluginextensions__ = {cls}  # set reflects lowest plugins
            cls.__pluginroot__ = cls
            cls.__pluginiscachevalid__ = False
        else:  # subclass
            assert not set(namespace) & {'__pluginextensions__',
                                         '__pluginroot__'}     # only in parent
            exts = cls.__pluginextensions__
            exts.difference_update(set(bases))  # remove parents
            exts.add(cls)  # and add current
            cls.__pluginroot__.__pluginiscachevalid__ = False
        return cls

    @property
    def PluginExtended(cls):
        # After PluginExtended creation we'll have only 1 item in set
        # so this is used for caching, mainly not to create same PluginExtended
        if cls.__pluginroot__.__pluginiscachevalid__:
            return next(iter(cls.__pluginextensions__))  # only 1 item in set
        else:
            name = cls.__pluginroot__.__name__ + 'PluginExtended'
            extended = type(name, tuple(cls.__pluginextensions__), {})
            cls.__pluginroot__.__pluginiscachevalid__ = True
return extended

So when you have Root base, made with metaclass, and have tree of plugins which inherit from it, you could automatically get class, which inherits from the most specific plugins by just subclassing:

class RootExtended(RootBase.PluginExtended):
    ... your code here ...

Code base is pretty small (~30 lines of pure code) and as flexible as inheritance allows.

If you're interested, get involved @ https://github.com/thodnev/pluginlib


You may also have a look at Groundwork.

The idea is to build applications around reusable components, called patterns and plugins. Plugins are classes that derive from GwBasePattern. Here's a basic example:

from groundwork import App
from groundwork.patterns import GwBasePattern

class MyPlugin(GwBasePattern):
    def __init__(self, app, **kwargs):
        self.name = "My Plugin"
        super().__init__(app, **kwargs)

    def activate(self): 
        pass

    def deactivate(self):
        pass

my_app = App(plugins=[MyPlugin])       # register plugin
my_app.plugins.activate(["My Plugin"]) # activate it

There are also more advanced patterns to handle e.g. command line interfaces, signaling or shared objects.

Groundwork finds its plugins either by programmatically binding them to an app as shown above or automatically via setuptools. Python packages containing plugins must declare these using a special entry point groundwork.plugin.

Here are the docs.

Disclaimer: I'm one of the authors of Groundwork.


In our current healthcare product we have a plugin architecture implemented with interface class. Our tech stack are Django on top of Python for API and Nuxtjs on top of nodejs for frontend.

We have a plugin manager app written for our product which is basically pip and npm package in adherence with Django and Nuxtjs.

For new plugin development(pip and npm) we made plugin manager as dependency.

In Pip package: With the help of setup.py you can add entrypoint of the plugin to do something with plugin manager(registry, initiations, ...etc.) https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation

In npm package: Similar to pip there are hooks in npm scripts to handle the installation. https://docs.npmjs.com/misc/scripts

Our usecase:

plugin development team is separate from core devopment team now. The scope of plugin development is for integrating with 3rd party apps which are defined in any of the categories of the product. The plugin interfaces are categorised for eg:- Fax, phone, email ...etc plugin manager can be enhanced to new categories.

In your case: Maybe you can have one plugin written and reuse the same for doing stuffs.

플러그인 개발자가 핵심 객체를 재사용해야하는 경우 플러그인 관리자에서 추상화 수준을 수행하여 해당 플러그인을 사용할 수 있도록 해당 객체를 사용할 수 있습니다.

제품에 구현 된 방식을 공유하는 것만으로도 약간의 아이디어가 제공되기를 바랍니다.

참고 URL : https://stackoverflow.com/questions/932069/building-a-minimal-plugin-architecture-in-python

반응형