Python: Как написать систему модов для игры / плагинов для программы

от автора

Итак, всех приветствую.

Я думаю, что практически каждый, кто программирует на python достаточно долго, хотел сделать так, чтобы любой сторонний разработчик смог добавить функционал в Ваше приложение без изменения его исходного кода. Поэтому, я хочу сделать гайд для всех новичков — как сделать систему плагинов для программы. Начнем.

А начнем мы с того, что установим importlib в ваше виртуальное окружение.

pip install importlib

Если установка прошла успешно, движемся дальше.

Теперь напишем небольшое приложение, которое будет считывать и выполнять команду пользователя:

import importlib  while True:     command = str(input(">>> "))      if command == "hello":         print("Hello, world!")     elif command == "help":         print("hello - displays Hello, world!")     else:         print(f"Unknown command: {command}")         

Если мы запустим скрипт и выполним несколько команд, это будет выглядеть примерно так;

>>> help hello - displays Hello, world! >>> hello Hello, world! >>> wewewewew Unknown command: wewewewew >>> 

Отлично, все работает. Теперь немного погрузимся в теорию и подумаем, как эта система может работать. Я придумал такой вариант:

  • Программа ищет все папки в папке plugins

  • Программа поочередно пытается открыть файл с информацией о плагине из каждой папки

  • Прочитав конфигурационный файл, программа попытается импортировать из указанного файла указанный класс

С эти разобрались. Теперь попробуем применить теорию на практике. Сделаем перед основным циклом перебор всех папок в папке plugins. Это можно сделать списочным выражением:

import os  plugins_dirs =  [name for name in os.listdir("./plugins") if os.path.isdir(os.path.join("./plugins", name))]

или с помощью простого цикла (Что в принципе одно и тоже):

import os   plugins_dirs = []  for name in os.listdir("./plugins"):     if os.path.isdir(os.path.join("./plugins", name)):         plugins_dirs.append(name)  print(plugins_dirs)

Я выберу второй вариант, т.к. он более читаемый и понятный для человека.

Немного изменим цикл, чтобы он не добавлял папки в список, но читал конфигурационный файл в каждой из папок (Если он там, конечно, есть) и выводил результат на печать:

import os import json   for name in os.listdir("./plugins"):     if os.path.isdir(os.path.join("./plugins", name)):         with open(f"./plugins/{name}/metadata.json") as f:             plugin_data = json.load(f)             print(plugin_data) 

Пока что не будем запускать код, а создадим свой первый плагин. Просто в папке plugins создаем любую папку. Я назвал ее Test_plugin. Структура в ней должна быть следующая:

+ Test_plugin | +---- metadata.json | +---- plugin.py

Содержимое plugin.py пока пустое, а metadata.json такое:

{   "Plugin_name": "TestPlugin",   "Plugin_file": "plugin",   "Plugin_main_class": "Plugin_class" }

В этом файле будет храниться следующая информация о плагине:

  • Plugin_name — Имя плагина. Может быть любым

  • Plugin_file — Путь к основному файлу плагина (Без .py в конце)

  • Plugin_main_class — Основной класс плагина, содержащийся в файле Plugin_file

Теперь можем запустить наш основной скрипт и увидеть что-то подобное:

{'Plugin_name': 'TestPlugin', 'Plugin_file': 'plugin', 'Plugin_main_class': 'Plugin_class'} >>> 

Отлично, это означает, что мы успешно загрузили данные плагина в приложение.

С этого момента начинаются трудности

Сейчас нам нужно как-то импортировать из файла с плагином класс. Как же это сделать если мы понятия не имеем о том, какие плагины загрузит пользователь? Тут-то нам и придет на помощь importlib. Сейчас код должен выглядеть примерно так:

import importlib import os import json   for name in os.listdir("./plugins"):     if os.path.isdir(os.path.join("./plugins", name)):         with open(f"./plugins/{name}/metadata.json") as f:             plugin_data = json.load(f)             print(plugin_data)  while True:     command = str(input(">>> "))      if command == "hello":         print("Hello, world!")     elif command == "help":         print("hello - displays Hello, world!")     else:         print(f"Unknown command: {command}") 

Заменим первую строчку на

from importlib import __import__

Данная функция поможет импортировать файлы, название и путь к которым мы не знаем на этапе разработки приложения. Добавим в наш цикл перебора плагинов строку

imported = __import__(f"plugins.{name}.{plugin_data['Plugin_file']}")

Она обозначает, что нам нужно импортировать файл plugins.Имя_папки_с_плагином.Имя_файла_класса_из_конфига !(ОБЯЗАТЕЛЬНО ЧЕРЕЗ ТОЧКИ)! Посмотрим, что мы импортировали, обернув это в print(dir()).

print(dir(imported))

Добавим такой код в plugin.py:

class Plugin_class:     def __init__(self):         pass      def test_func(self, a, b):         return a + b 

Сделав это и запустив главный файл, мы получим примерно такое:

['Test_plugin', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

Если вы сделали все правильно, то тогда где-то в списке будет имя вашей папки с плагином. Попробуем получить ее атрибут с файлом плагина с помощью:

print(dir(getattr(imported, name)))

Увидим следующее:

['__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'plugin']

И чтобы уже наконец-то добраться до заветного класса с плагином добавим еще немного букв:

print(dir(getattr(getattr(getattr(imported, name), plugin_data['Plugin_file']), plugin_data['Plugin_main_class'])))

Да — да, я знаю что тут больше 121 символа в строке, и что вы мне сделаете? Выполнив этот код, мы увидим все функции нашего класса. У меня это:

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'test_func']

Здесь мы видим __init__ и test_func, которые, как мы помним, мы определяли в классе Plugin_class. На остальное можно не обращать внимания — это встроенные функции и они есть у всех классов python по умолчанию. Попробуем вместо print и dir просто инициализировать этот класс и вызвать у него test_func, записав значение которое он вернет в var. Делается это так:

var = (getattr(getattr(getattr(imported, name), plugin_data['Plugin_file']), plugin_data['Plugin_main_class']))().test_func(150, 150) print(var)

Видим следующий вывод и радуемся жизни:

300 >>>

Это конечно все хорошо, но как нам дать пользователю возможность взаимодействовать с плагином? Все достаточно просто: нам нужно лишь чуть — чуть переписать обработчик функций. Сейчас он выглядит так

from importlib import __import__ import os import json   for name in os.listdir("./plugins"):     if os.path.isdir(os.path.join("./plugins", name)):         with open(f"./plugins/{name}/metadata.json") as f:             plugin_data = json.load(f)             print(plugin_data)             var = (getattr(getattr(getattr(imported, name), plugin_data['Plugin_file']), plugin_data['Plugin_main_class']))().test_func(150, 150)             print(var)  while True:     command = str(input(">>> "))      if command == "hello":         print("Hello, world!")     elif command == "help":         print("hello - displays Hello, world!")     else:         print(f"Unknown command: {command}")

Мы же сделаем так:

from importlib import __import__ import os import json   compiled_plugins = {}  for name in os.listdir("./plugins"):     if os.path.isdir(os.path.join("./plugins", name)):         with open(f"./plugins/{name}/metadata.json") as f:             plugin_data = json.load(f)             print(plugin_data)             imported = __import__(f"plugins.{name}.{plugin_data['Plugin_file']}")             compiled_plugins[plugin_data['Plugin_name']] = (getattr(getattr(getattr(imported, name), plugin_data['Plugin_file']), plugin_data['Plugin_main_class']))()  while True:     command = str(input(">>> "))      if command == "hello":         print("Hello, world!")     elif command == "help":         print("hello - displays Hello, world!")     elif command.split()[0] in compiled_plugins.keys():         worker = compiled_plugins[command.split()[0]]         if len(command.split()) > 1:             worker.execute(command.split()[1:])         else:             print("Syntax error")     else:         print(f"Unknown command: {command}")         

И добавим метод execute в класс нашего плагина:

    def execute(self, com):         if com[0] == "test":             print("Hello from your first plugin!")         else:             print(f"Unknown options: {com}")

Ну, что ж, хочу вас поздравить: Запустив программу сейчас и написав, подставив вместо TestPlugin имя, указанное в json, в терминал это:

TestPlugin test

Вы получите это:

Hello from your first plugin!

В заключение хочу сказать, что то, что мы сейчас написали — это очень плохая система:

Нарушено много правил хорошего кода (pep-8), нет обработки исключений при загрузке и использовании, не предусмотрен конфликт имен плагинов, в общем, если вы хотите сделать что-то хоть немного рабочее, то учитывайте все эти факторы при написании кода. Надеюсь, что вам понравилась моя статья и вы оцените ее. Всем удачи!


ссылка на оригинал статьи https://habr.com/ru/articles/827176/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *