web-dev-qa-db-ja.com

Pythonで最小限のプラグインアーキテクチャを構築する

Pythonで書かれたアプリケーションがあり、かなり専門的な聴衆(科学者)によって使用されています。

ユーザーがアプリケーションを拡張できるようにする良い方法、つまりスクリプト/プラグインアーキテクチャを探しています。

私は何かを探しています非常に軽量。ほとんどのスクリプト、またはプラグインは、サードパーティによって開発および配布されてインストールされることはありませんが、繰り返しタスクを自動化し、ファイル形式のサポートを追加するために、数分でユーザーによってホイップされるものになります。そのため、プラグインには最低限のボイラープレートコードが必要であり、フォルダーにコピーする以外に「インストール」は必要ありません(したがって、setuptoolsエントリポイント、またはZopeプラグインアーキテクチャのように見えます)。

すでにこのようなシステムがありますか、またはアイデア/インスピレーションを探す必要がある同様のスキームを実装するプロジェクトはありますか?

177
dF.

私の場合、基本的に「プラグイン」と呼ばれるディレクトリで、メインアプリがポーリングしてから imp.load_module を使用してファイルを取得し、モジュールレベルの構成パラメーターを使用して既知のエントリポイントを探します。 、そこから行きます。プラグインがアクティブになっている一定量のダイナミズムのためにファイルモニタリングスタッフを使用していますが、それは便利です。

もちろん、「[大規模で複雑なもの] Xは必要ありません。軽量なものが必要です」という要件は、Xを一度に1つずつ再実装するリスクを伴います。しかし、それはあなたがとにかくそれを行ういくつかの楽しみを持つことができないということではありません:)

144
TJG

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)

それは確かに「最小」であり、エラーチェックがまったくなく、おそらく無数のセキュリティ問題があり、あまり柔軟性がありませんが、Pythonのプラグインシステムがどれほど簡単かを示す必要があります。

おそらく imp モジュールも検討する必要がありますが、__import__os.listdirおよびいくつかの文字列操作で多くのことができます。

51
dbr

既存のプラグインフレームワーク/ライブラリに関するこの概要で を見てください、それは良い出発点です。 yapsy は非常に好きですが、ユースケースによって異なります。

30
PhilS

その質問は本当に興味深いものですが、詳細なしでは答えるのはかなり難しいと思います。これはどのようなアプリケーションですか? GUIがありますか?コマンドラインツールですか?スクリプトのセット?一意のエントリポイントなどを備えたプログラム.

私が持っている小さな情報を考えると、私は非常に一般的な方法で答えます。

プラグインを追加する必要があるとはどういう意味ですか?

  • おそらく、ロードするパス/ディレクトリをリストする構成ファイルを追加する必要があります。
  • 別の方法は、「そのplugin /ディレクトリ内のすべてのファイルがロードされます」と言うことですが、ユーザーにファイルを移動するように要求するのは不便です。
  • 最後の中間オプションは、すべてのプラグインが同じplugin /フォルダーにあることを要求し、構成ファイルの相対パスを使用してそれらをアクティブ/非アクティブにすることです。

純粋なコード/設計の実践では、ユーザーに拡張する動作/特定のアクションを明確に決定する必要があります。常に上書きされる共通のエントリポイント/機能のセットを特定し、これらのアクション内のグループを決定します。これが完了すると、アプリケーションを簡単に拡張できるはずです。

hooksを使用した例は、MediaWikiに触発されています(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)

別の例は、Mercurialに触発されたものです。ここで、拡張機能は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 }

どちらのアプローチでも、拡張機能に共通のinitializeおよびfinalizeが必要になる場合があります。すべての拡張機能が実装する必要のある共通のインターフェイスを使用するか(2番目のアプローチに適しています。Mercurialはすべての拡張機能に対して呼び出されるreposetup(ui、repo)を使用します)、またはフックのようなアプローチを使用しますhooks.setupフック。

しかし、さらに有用な答えが必要な場合は、質問を絞り込む必要があります;)

25
Nicolas Dumazet

Marty Allchinのシンプルなプラグインフレームワーク は、自分のニーズに合わせて使用​​するベースです。私は本当にそれを見てみることをお勧めします。シンプルで簡単にハッキングできるものが欲しいなら、それは本当に良いスタートだと思います。 Djangoスニペットとして も見つけることができます。

11
edomaur

私は引退した生物学者であり、デジタルマイクログラフを扱っており、SGiマシンで実行するには画像処理および分析パッケージ(厳密にはライブラリーではない)を作成する必要がありました。コードをCで記述し、スクリプト言語にTclを使用しました。 GUIは、Tkを使用して行われました。 Tclに登場したコマンドは、「extensionName commandName arg0 arg1 ... param0 param1 ...」という形式でした。つまり、スペースで区切られた単純な単語と数字です。 Tclが「extensionName」サブストリングを検出すると、制御がCパッケージに渡されました。次に、コマンドをレクサー/パーサー(Lex/yaccで実行)で実行し、必要に応じてCルーチンを呼び出しました。

パッケージを操作するコマンドは、GUIのウィンドウを介して1つずつ実行できますが、有効なTclスクリプトであるテキストファイルを編集してバッチジョブを実行しました。目的の種類のファイルレベルの操作を実行したテンプレートを選択し、実際のディレクトリとファイル名に加えてパッケージコマンドを含むコピーを編集します。それは魅力のように働いた。まで...

1)世界はPCに変わり、2)Tclの不確かな組織能力が本当に不便になり始めたとき、スクリプトは約500行を超えました。時は過ぎた ...

私は引退し、Pythonが発明され、Tclの完全な後継者のように見えました。今では、PCで(非常に大きな)Cプログラムをコンパイルし、PythonをCパッケージで拡張し、Python/GtでGUIを実行するという課題に直面したことがないため、移植を行ったことはありません。 ?/ Tk?/ ??。ただし、編集可能なテンプレートスクリプトを使用するという古い考え方は、まだ実行可能なようです。また、ネイティブのPython形式でパッケージコマンドを入力することは、それほど負担になりません。たとえば:

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

いくつかの余分なドット、括弧、およびコンマがありますが、それらはショートッパーではありません。

誰かがPython(試してみてください: http://www.dabeaz.com/ply/ )でLexとyaccのバージョンを作成したことを覚えています。 、彼らは周りにいます。

このとりとめのない点は、Python自体ISが科学者が使用できる望ましい「軽量」フロントエンドであるように思えたことです。私はあなたがなぜそうではないと思うのか知りたいのですが、真剣に言っています。


後で追加:アプリケーションgeditはプラグインが追加されることを予測し、そのサイトには数分で見つけた簡単なプラグイン手順の最も明確な説明があります周りを探し。試してください:

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

あなたの質問をもっとよく理解したいと思います。 1)科学者が(Python)アプリケーションをさまざまな方法で簡単に使用できるようにするか、2)科学者がアプリケーションに新しい機能を追加できるようにするかどうかは不明です。選択肢#1は、私たちが画像に直面した状況であり、そのため、現時点のニーズに合わせて修正した汎用スクリプトを使用することになりました。プラグインのアイデアに導く選択肢#2ですか、それともコマンドの発行を実行不可能にするアプリケーションの一部ですか?

11
behindthefall

Pythonデコレータを検索すると、シンプルだが便利なコードスニペットが見つかりました。それはあなたのニーズに合わないかもしれませんが、非常に刺激的です。

Scipy Advanced Python#Plugin Registration System

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**")
10
guneysus

私はPycon 2009でAndre Roberge博士によって行われたさまざまなプラグインアーキテクチャに関するニースの議論を楽しみました。彼は本当にシンプルなものから始めて、プラグインを実装するさまざまな方法の概要を説明します。

一連の 6つのブログエントリ を伴う podcast (サルパッチの説明に続く第2部)として利用できます。

決定を下す前に、すぐに試聴することをお勧めします。

7
Jon Mills

プロジェクトのドキュメントから抜粋した次の例のように、実際にはsetuptoolsは「プラグインディレクトリ」で動作します。 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は、競合や要件の欠落なしにプラグインをロードできるため、はるかに安全な選択です。

別の利点は、元のアプリケーションが気にすることなく、同じメカニズムを使用してプラグイン自体を拡張できることです。

4
ankostis

最小限のプラグインアーキテクチャを探してここに到着しましたが、私にはやり過ぎのように思われるものがたくさん見つかりました。そこで、私は Super Simple Python Plugins を実装しました。これを使用するには、1つ以上のディレクトリを作成し、それぞれに特別な__init__.pyファイルをドロップします。これらのディレクトリをインポートすると、他のすべてのPythonファイルがサブモジュールとしてロードされ、それらの名前が__all__リストに配置されます。それから、それらのモジュールを検証/初期化/登録するのはあなた次第です。 READMEファイルに例があります。

4
samwyse

setuptoolsにはEntryPointがあります

エントリポイントは、他のディストリビューションが使用するPythonオブジェクト(関数やクラスなど)をディストリビューションが「アドバタイズ」する簡単な方法です。拡張可能なアプリケーションおよびフレームワークは、特定のディストリビューションまたはsys.path上のすべてのアクティブなディストリビューションから特定の名前またはグループを持つエントリポイントを検索し、アドバタイズされたオブジェクトを自由に検査またはロードできます。

私の知る限り、このパッケージは、pipまたはvirtualenvを使用する場合は常に利用可能です。

3
guettli

プラグインシステムへのもう1つのアプローチとして、 Extend Meプロジェクト を確認できます。

たとえば、単純なクラスとその拡張子を定義しましょう

# 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の新しいインスタンスを作成するときに、新しいクラスのインスタンスが取得されます。 Pythonの 多重継承 のおかげで、両方の機能を持つMyCoolClassExtensionMyCoolClassの両方のサブクラス

クラスの作成をより適切に制御するために、このライブラリにはいくつかのメタクラスが定義されています。

  • ExtensibleType-サブクラス化することで簡単な拡張性を実現します

  • ExtensibleByHashType-ExtensibleTypeに似ていますが、クラスの特殊なバージョンを構築する機能を備えており、基本クラスのグローバル拡張とクラスの特殊なバージョンの拡張を許可します

このライブラリは OpenERP Proxy Project で使用されており、十分に機能しているようです!

実際の使用例については、 OpenERP Proxy 'field_datetime' extension をご覧ください。

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は、拡張可能なオブジェクトです。 RecordDateTimeは拡張子です。

拡張機能を有効にするには、拡張機能クラスを含むモジュールをインポートし、(上記の場合)ベースクラスに拡張機能クラスが含まれる後に作成されるすべてのRecordオブジェクトをインポートします。

このライブラリの主な利点は、拡張可能オブジェクトを操作するコードが拡張機能について知る必要がなく、拡張機能が拡張可能オブジェクトのすべてを変更できることです。

3
FireMage

私はPythonでプラグインフレームワークを探している間、このスレッドを読むことに時間を費やしました。私は いくつか使用したが欠点があった を持っている。 2017年のあなたの精査のために私が思いついたのは、インターフェースのない、疎結合のプラグイン管理システムです Load me later 。ここに tutorials の使い方があります。

2
chfw

@edomaurの答えを拡張して、 simple_plugins (恥知らずなプラグイン)を見てみることをお勧めします。これは マーティ・アルチンの作品

プロジェクトの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>
2
Petar Marić

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"}'))
1
aviso

Groundwork もご覧ください。

アイデアは、パターンやプラグインと呼ばれる再利用可能なコンポーネントを中心にアプリケーションを構築することです。プラグインは、GwBasePatternから派生したクラスです。基本的な例を次に示します。

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

処理するためのより高度なパターンもあります。コマンドラインインターフェース、シグナリングまたは共有オブジェクト。

Groundworkは、上記のようにプログラムでプラグインをアプリにバインドするか、setuptoolsを介して自動的にプラグインを見つけます。 Pythonプラグインを含むパッケージは、特別なエントリポイントgroundwork.pluginを使用してこれらを宣言する必要があります。

docs です。

免責事項:私はGroundworkの著者の一人です。

1
ub_marco

私はPythonの小さなプラグインシステムを見つけるために多くの時間を費やしました。これは私のニーズに合うものです。しかし、その後、自然で柔軟な継承が既にあるのなら、なぜそれを使用しないのかと考えました。

プラグインに継承を使用する場合の唯一の問題は、最も具体的な(継承ツリーで最も低い)プラグインクラスが何かわからないことです。

しかし、これはメタクラスで解決できます。メタクラスは基本クラスの継承を追跡し、おそらく特定のプラグインから継承するクラスを構築できます(下図の「ルート拡張」)

enter image description here

そのため、このようなメタクラスをコーディングすることで解決策を見つけました。

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__.__+ 'PluginExtended'
            extended = type(name, Tuple(cls.__pluginextensions__), {})
            cls.__pluginroot__.__pluginiscachevalid__ = True
return extended

したがって、メタクラスで作成されたルートベースがあり、それを継承するプラグインのツリーがある場合、サブクラス化するだけで最も特定のプラグインから継承するクラスを自動的に取得できます。

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

コードベースは非常に小さく(純粋なコードの最大30行)、継承が許す限り柔軟です。

興味があるなら、参加してください@ https://github.com/thodnev/pluginlib

1
thodnev

現在のヘルスケア製品には、インターフェイスクラスで実装されたプラグインアーキテクチャがあります。技術スタックは、APIのDjangoの上にあるPythonと、フロントエンドのnodejsの上にあるNuxtjsです。

基本的にDjangoおよびNuxtjsに準拠したpipおよびnpmパッケージである製品用に作成されたプラグインマネージャーアプリがあります。

新しいプラグイン開発(pipおよびnpm)では、プラグインマネージャーを依存関係として作成しました。

Pipパッケージの場合:setup.pyを使用すると、プラグインのエントリポイントを追加して、プラグインマネージャー(レジストリ、開始、...など)で何かを行うことができます https://setuptools.readthedocs.io/en /latest/setuptools.html#automatic-script-creation

Npmパッケージ:pipと同様に、インストールを処理するためにnpmスクリプトにフックがあります。 https://docs.npmjs.com/misc/scripts

ユースケース:

現在、プラグイン開発チームはコア開発チームから分離されています。プラグイン開発の範囲は、製品のカテゴリのいずれかで定義されているサードパーティアプリと統合することです。プラグインインターフェイスは、たとえば次のように分類されます。-ファックス、電話、電子メールなど...プラグインマネージャーを新しいカテゴリに拡張できます。

あなたの場合:たぶん、あなたは1つのプラグインを書いて、物事をするために同じものを再利用することができます。

プラグイン開発者が再利用コアオブジェクトを使用する必要がある場合、プラグインマネージャー内で一定レベルの抽象化を行うことにより、そのオブジェクトを使用して、プラグインがそれらのメソッドを継承できるようにします。

製品に実装した方法を共有するだけで、ちょっとしたアイデアが得られることを願っています。

0
shangan