LoginSignup
1
2

Pythonでフレームワークを作ってみる

Last updated at Posted at 2023-08-06

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の大まかな設定をここに記載しています。

setting.json
{
	"plugin":{
		"plugin.Plugin":"Plugin"
	},
	"window":{
		"title":"Framework",
		"geometry":"300x200"
	}
}

5. 実装

Main.py
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()
ILogger.py
from abc import ABCMeta, abstractmethod

class ILogger(metaclass=ABCMeta):
    @abstractmethod
    def log(self, info: str):
        pass
Logger.py
from ILogger import ILogger

class Logger(ILogger):
    def log(self, info: str):
        print(info)
IPlugin.py
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
PluginManager.py
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()
Window.py
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()
Plugin.py
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"というボタンは読み込んだモジュール名を表示したもので、押すと拡張機能が実行されます。
image.png

7. ポイント

  • モジュールの読込にimportlibを使用しました。これにより、JSONから取得したモジュール名・クラス名を使って動的に読込先を指定することができます。
  • ログ出力はprintするだけとなっているので、わざわざクラスとして分離する意味が無いのでは?と思うかもしれません。しかし拡張機能が増えていくことを想定すると、ログ出力機能は1つのクラスとしてまとめた方が後々便利になると思います。例えばGUIアプリを作るのであればログを画面上に表示したい場合があるかもしれませんし、loggingモジュールを使用する場合は各拡張機能それぞれで出力設定を管理するよりは1か所でまとめて設定できた方が保守性が優れていると思います。(どんなフレームワークを作るかによって実装が変わってくるので、ここではprintするだけにしました)
  • GUIの生成にはTkinterを使用しました。GUIライブラリは他にもあるのですが、Pythonのバージョンや依存関係などに制限が発生しないよう、標準ライブラリを使いました。

8. 今後

現状は拡張機能の一覧だけしかGUIで表示できていませんので、ログ出力結果もまとめて表示できるようにしたいです。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2