1. 概要
Pythonを使ってフレームワークを作ってみようと思います。開発環境にはVisual Studio 2019, Python 3.7を使用しています。
ここでのフレームワークとは、以下の条件を満たすものとします。
- エントリポイント(プログラムの開始地点)がフレームワーク側に存在する。
- 利用者が拡張機能を追加する際は、設定ファイル更新とソースコード追加だけで済む。
※2023/8/16更新:GUIで拡張機能一覧を表示する仕組みを追加しました。
2. ファイル構成
以下の構成で作成しました。拡張機能を追加する場合は、setting.jsonを更新してpluginフォルダにソースファイルを追加します。
Framework
├── ILogger.py
├── IPlugin.py
├── Logger.py
├── Main.py
├── PluginManager.py
├── plugin
│ └── Plugin.py
├── setting.json
└── Window.py
3. クラス構成
以下の構成で実装します。
- Main
- エントリポイントとして利用するクラス。設定ファイルの読込はここで行う。
- ILogger
- ログ出力を行うためのメソッドを定義したインターフェース。拡張機能はこれを使ってログ出力を行う。
- Logger
- ログ出力機能を実装したクラス。ILoggerを継承。
- IPlugin
- 拡張機能に必要なメソッドを定義したインターフェース。利用者が機能を追加する際は、これを継承したクラスを作成する必要がある。
- PluginManager
- 拡張機能を管理するクラス。
- Window
- 拡張機能一覧をGUIとして表示するためのクラス。
- Plugin
- 拡張機能の実装クラス。利用者が作成する。
クラス図はこんな感じです。拡張機能を追加する場合はIPluginを継承してILoggerを利用することになります。
4. 設定ファイル
JSON形式で記述し、読み込むモジュールの配置場所と拡張機能を実行するためのクラスを指定します。また、GUIの大まかな設定をここに記載しています。
{
"plugin":{
"plugin.Plugin":"Plugin"
},
"window":{
"title":"Framework",
"geometry":"300x200"
}
}
5. 実装
import json
from Logger import Logger
from PluginManager import PluginManager
from Window import Window
class Main:
def __init__(self):
self.logger = Logger()
self.pluginManager = PluginManager(self.logger)
self.window = Window(self.pluginManager)
self.load_setting()
def load_setting(self):
with open('./setting.json') as _file:
_json: dict = json.load(_file)
self.pluginManager.load(_json.get('plugin'))
self.window.setup(_json.get('window'))
if __name__ == "__main__":
_main = Main()
_main.window.mainloop()
_main.pluginManager.shutdown()
from abc import ABCMeta, abstractmethod
class ILogger(metaclass=ABCMeta):
@abstractmethod
def log(self, info: str):
pass
from ILogger import ILogger
class Logger(ILogger):
def log(self, info: str):
print(info)
from abc import ABCMeta, abstractmethod
from ILogger import ILogger
class IPlugin(metaclass=ABCMeta):
@abstractmethod
def get_name(self) -> str:
pass
def is_shutdown_enable(self) -> bool:
return True
def setup(self, logger: ILogger):
return
def start(self):
return
def shutdown(self):
return
from importlib import import_module
from Logger import Logger
from IPlugin import IPlugin
class PluginManager():
def __init__(self, logger: Logger):
self.executedIndex = -1
self.logger = logger
self.pluginList: list[IPlugin] = []
def load(self, plugins: dict):
for module, plugin in plugins.items():
_plugin = getattr(import_module(module), plugin)
if issubclass(_plugin, IPlugin):
_instance: IPlugin = _plugin()
self.logger.log('load ' + _instance.get_name())
_instance.setup(self.logger)
self.pluginList.append(_instance)
def get_catalog(self) -> list:
_list: list = []
for plugin in self.pluginList:
_list.append(plugin.get_name())
return _list
def start(self, index: int):
if 0 <= index and index < len(self.pluginList):
_isShutdownDone = False
if self.executedIndex < 0:
_isShutdownDone = True
elif self.pluginList[self.executedIndex].is_shutdown_enable():
self.pluginList[self.executedIndex].shutdown()
_isShutdownDone = True
if _isShutdownDone:
self.executedIndex = index
self.pluginList[index].start()
else:
self.logger.log('can not shutdown ' + self.pluginList[index].get_name())
else:
self.logger.log('invalid index ' + str(index))
def shutdown(self):
if 0 <= self.executedIndex:
self.pluginList[self.executedIndex].shutdown()
import tkinter
from PluginManager import PluginManager
class Window():
def __init__(self, pluginManager: PluginManager):
self.pluginManager = pluginManager
def setup(self, setting: dict):
self.root = tkinter.Tk()
self.root.title(setting.get('title', ''))
self.root.geometry(setting.get('geometry', '100x100'))
_message = tkinter.Label(self.root)
_message.config(text='実行する機能を選択してください')
_message.pack()
_catalog = self.pluginManager.get_catalog()
for index, title in enumerate(_catalog):
_button = tkinter.Button(self.root)
_button.config(text=title, command=self.start_plugin(index))
_button.pack()
def start_plugin(self, index: int):
def func():
self.pluginManager.start(index)
return func
def mainloop(self):
self.root.mainloop()
from ILogger import ILogger
from IPlugin import IPlugin
class Plugin(IPlugin):
def get_name(self) -> str:
return 'Plugin'
def is_shutdown_enable(self) -> bool:
return True
def setup(self, logger: ILogger):
self.logger = logger
def start(self):
self.logger.log('start ' + self.get_name())
def shutdown(self):
self.logger.log('shutdown ' + self.get_name())
6. 実行結果
以下のGUIが表示されます。この"Plugin"というボタンは読み込んだモジュール名を表示したもので、押すと拡張機能が実行されます。
7. ポイント
- モジュールの読込にimportlibを使用しました。これにより、JSONから取得したモジュール名・クラス名を使って動的に読込先を指定することができます。
- ログ出力はprintするだけとなっているので、わざわざクラスとして分離する意味が無いのでは?と思うかもしれません。しかし拡張機能が増えていくことを想定すると、ログ出力機能は1つのクラスとしてまとめた方が後々便利になると思います。例えばGUIアプリを作るのであればログを画面上に表示したい場合があるかもしれませんし、loggingモジュールを使用する場合は各拡張機能それぞれで出力設定を管理するよりは1か所でまとめて設定できた方が保守性が優れていると思います。(どんなフレームワークを作るかによって実装が変わってくるので、ここではprintするだけにしました)
- GUIの生成にはTkinterを使用しました。GUIライブラリは他にもあるのですが、Pythonのバージョンや依存関係などに制限が発生しないよう、標準ライブラリを使いました。
8. 今後
現状は拡張機能の一覧だけしかGUIで表示できていませんので、ログ出力結果もまとめて表示できるようにしたいです。