Каковы распространенные методы включения расширений пользовательского кода в Python?

Я ищу методы, которые позволяют пользователям переопределять модули в приложении или расширять приложение с новыми модулями.

Представьте себе приложение под названием pydraw. В настоящее время он предоставляет класс Circle, который наследует Shape. Дерево пакетов может выглядеть так:

/usr/lib/python/
└── pydraw
    ├── __init__.py
    ├── shape.py
    └── shapes
        ├── circle.py
        └── __init__.py

Теперь предположим, что я хотел бы включить динамическое обнаружение и загрузку пользовательских модулей, которые реализуют новую форму, или, возможно, даже класс Shape. Кажется наиболее простым для дерева пользователей иметь ту же структуру, что и дерево приложений, например:

/home/someuser/python/
└── pydraw
    ├── __init__.py
    ├── shape.py       <-- new superclass
    └── shapes
        ├── __init__.py
        └── square.py   <-- new user class

Другими словами, я хотел бы наложить и замаскировать дерево приложений с одинаковыми именами из дерева пользователей или, по крайней мере, получить эту видимую структуру с точки зрения Python.

Затем, настраивая sys.path или PYTHONPATH, pydraw.shapes.square может быть обнаружен. Тем не менее, поиск путей модуля Python не находит такие модули, как square.py. Я предполагаю, что это потому, что __method__ уже содержит родительский модуль на другом пути.

Как бы вы выполнили эту задачу с Python?

Ответ 1

Если вы хотите динамически загружать код python из разных мест, вы можете расширить атрибуты поиска __path__, используя модуль pkgutil:

Поместив эти строки в каждый pydraw/__init__.py и pydraw/shapes/__init__.py:

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

Вы сможете написать оператор импорта, как если бы у вас был уникальный пакет:

>>> import pydraw.shapes
>>> pydraw.shapes.__path__
['/usr/lib/python/pydraw/shapes', '/home/someuser/python/pydraw/shapes']
>>> from pydraw.shapes import circle, square
>>>

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

Добавьте последнюю строку в каждый файл pydraw/shapes/__init__.py:

 from pkgutil import extend_path
 __path__ = extend_path(__path__, __name__)

 # your shape registry
 __shapes__ = []

Теперь вы можете зарегистрировать фигуру в верхней части связанного с ней модуля (circle.py или square.py здесь).

 from pydraw.shapes import __shapes__
 __shapes__.append(__name__)

Последняя проверка:

 >>> from pydraw.shapes import circle,square
 >>> from pydraw.shapes import circle,square,__shapes__
 >>> __shapes__
 ['pydraw.shapes.circle', 'pydraw.shapes.square']

Ответ 2

Открытие расширений может быть немного хрупким и сложным, а также требует, чтобы вы просмотрели все PYTHONPATH, которые могут быть очень большими.

Вместо этого укажите файл конфигурации, в котором перечислены плагины, которые должны быть загружены. Это можно сделать, указав их как имена модулей, а также потребовав, чтобы они были расположены на PYTHONPATH или просто перечислены полные пути.

Если вы хотите иметь конфигурацию на каждом уровне пользователя, у меня будет как глобальный файл конфигурации, в котором перечислены модули, так и один пользователь, и просто прочитайте эти файлы конфигурации вместо того, чтобы пытаться найти механизм обнаружения.

Также вы пытаетесь не только добавлять плагины, но и переопределять компоненты своего приложения. Для этого я бы использовал архитектуру компонентов Zope. Однако он еще не полностью перенесен на Python 3, но он предназначен для использования именно в таких случаях, хотя из вашего описания это, по-видимому, простой случай, и ZCA может быть излишним. Но посмотрите на это в любом случае.

Ответ 3

Метод, который я использовал для решения такой проблемы, был с шаблоном поставщика.

В вашем модуле shape.py:

class BaseShape:
   def __init__(self):
      pass
provider("BaseShape", BaseShape)

и в модуле user shape.py:

class UserBaseShape:
   def __init__(self):
      pass
provider("BaseShape", UserBaseShape)

с помощью метода provider, выполняющего что-то вроде этого:

def provider(provide_key, provider_class):
   global_providers[provide_key] = provider_class

И когда вам нужно создать объект, используйте ProvideFactory, как этот

class ProvideFactory:
   def get(self, provide_key, *args, **kwargs):
      return global_providers[provide_key](*args, **kwargs)

Ответ 4

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

  • Конфигурирование каталога плагинов упрощает работу над вашей полной файловой системой.
  • поиск изменений файла в отдельном потоке, вероятно, является лучшим решением
  • Если найден новый .py файл, его можно импортировать с помощью встроенной функции __import__
  • Если параметр .py изменен, вы можете повторно импортировать его с помощью встроенной функции reload
  • Если класс изменен, экземпляры этого класса будут по-прежнему вести себя как экземпляры старого класса, поэтому обязательно при необходимости создайте экземпляры

EDIT:

Если вы добавите свой каталог плагинов в качестве первого каталога в PYTHONPATH, этот каталог будет иметь приоритет над другими каталогами в pythonpath, например.

import sys
sys.path.insert(0, 'PLUGIN_DIR')