2009-05-31 14 views
158

Tengo una aplicación, escrita en Python, que es utilizada por un público bastante técnico (científicos).Creación de una arquitectura de complementos mínima en Python

Estoy buscando una buena manera de hacer que la aplicación sea extensible por los usuarios, es decir, una arquitectura de scripting/plugin.

Estoy buscando algo extremadamente ligero. La mayoría de las secuencias de comandos, o complementos, no van a ser desarrolladas y distribuidas por un tercero e instaladas, sino que serán algo acelerado por un usuario en unos pocos minutos para automatizar una tarea repetitiva, agregar soporte para un formato de archivo, etc. Por lo tanto, los complementos deben tener el código mínimo absoluto y no requieren 'instalación' más que copiar en una carpeta (así que algo como setuptools puntos de entrada, o la arquitectura del complemento Zope parece demasiado.)

¿Hay alguna? sistemas como este que ya existen, o cualquier proyecto que implemente un esquema similar que debería buscar ideas/inspiración?

Respuesta

137

Mine es, básicamente, un directorio llamado "complementos" que la aplicación principal puede sondear y luego usar imp.load_module para recoger archivos, buscar un punto de entrada conocido posiblemente con parámetros de configuración a nivel de módulo, y continuar desde allí . Utilizo material de monitoreo de archivos para una cierta cantidad de dinamismo en el que los complementos están activos, pero eso es bueno tenerlo.

Por supuesto, cualquier requisito que surja diciendo "No necesito [cosas grandes y complicadas] X, solo quiero algo ligero" corre el riesgo de volver a implementar X un requisito descubierto a la vez. Pero eso no quiere decir que no pueda divertirse haciéndolo de todos modos :)

+54

+1 para el segundo párrafo. Tan verdadero. :-) –

+22

¡Muchas gracias! Escribí un pequeño tutorial basado en su publicación: http://lkubuntu.wordpress.com/2012/10/02/writing-a-python-plugin-api/ – MiJyn

+2

El módulo 'imp' está en desuso en favor de' importlib 'a partir de python 3.4 – b0fh

22

Si bien esa pregunta es realmente interesante, creo que es bastante difícil de responder, sin más detalles. ¿Qué tipo de aplicación es esta? ¿Tiene una GUI? ¿Es una herramienta de línea de comandos? Un conjunto de scripts? Un programa con un punto de entrada único, etc ...

Dada la poca información que tengo, responderé de manera muy general.

¿Qué significa que tiene que agregar complementos?

  • Probablemente tengas que agregar un archivo de configuración, que listará las rutas/directorios para cargar.
  • Otra forma sería decir "se cargarán los archivos en ese complemento/directorio", pero tiene el inconveniente de requerir que los usuarios muevan los archivos.
  • Una última opción intermedia sería requerir que todos los complementos estén en el mismo complemento/carpeta, y luego activarlos/desactivarlos mediante rutas relativas en un archivo de configuración.

En una práctica de código/diseño puro, tendrá que determinar claramente qué comportamiento/acciones específicas desea que extiendan sus usuarios. Identifique el punto de entrada común/un conjunto de funcionalidades que siempre serán anuladas, y determine los grupos dentro de estas acciones. Una vez hecho esto, debería ser fácil ampliar su aplicación,

Ejemplo utilizando ganchos, inspirado en MediaWiki (PHP, pero, ¿realmente importa el idioma?):

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) 

Otro ejemplo, inspirado en mercurial. Aquí, las extensiones solo agregan comandos al ejecutable de la línea de comandos hg, extendiendo el comportamiento.

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 } 

Para ambos enfoques, es posible que necesite común inicializar y ultimar para su extensión. Puede usar una interfaz común que toda su extensión deberá implementar (se ajusta mejor con el segundo enfoque; mercurial usa un repositorio (ui, repo) que se llama para todas las extensiones), o utilice un enfoque tipo gancho, con un gancho hooks.setup.

Pero, de nuevo, si quieres respuestas más útiles, que tendrá que reducir su pregunta;)

11

Soy biólogo retirado que ocupó de micrograqphs digitales y se encontró tener que escribir un procesamiento y análisis de imágenes paquete (no técnicamente una biblioteca) para ejecutar en una máquina SGi. Escribí el código en C y usé Tcl para el lenguaje de scripting. La GUI, tal como estaba, se hizo usando Tk. Los comandos que aparecían en Tcl eran de la forma "nombre-extensión-mandatoNombre arg0-arg1 ... param0-param1 ...", es decir, palabras y números simples separados por espacios. Cuando Tcl vio la subcadena "extensionName", el control pasó al paquete C. Eso a su vez ejecutaba el comando a través de un lexer/analizador (hecho en lex/yacc) y luego llamaba a las rutinas C según fuera necesario.

Los comandos para operar el paquete se podían ejecutar uno por uno a través de una ventana en la GUI, pero los trabajos por lotes se realizaban editando archivos de texto que eran scripts Tcl válidos; usted elegiría la plantilla que hizo el tipo de operación a nivel de archivo que quería hacer y luego editaría una copia para contener el directorio real y los nombres de archivo más los comandos del paquete. Funcionó a las mil maravillas. Hasta ...

1) El mundo recurrió a las PC y 2) los scripts obtuvieron más de 500 líneas, cuando las capacidades organizacionales dudosas de Tcl comenzaron a convertirse en un inconveniente real. El tiempo pasó ...

Me retiré, Python se inventó, y parecía el sucesor perfecto de Tcl. Ahora, nunca he hecho el puerto, porque nunca he enfrentado los desafíos de compilar (bastante grandes) programas C en una PC, extender Python con un paquete C y hacer GUI en Python/Gt?/Tk? /? ?. Sin embargo, la vieja idea de tener scripts de plantillas editables parece factible. Además, no debe ser una carga demasiado grande para entrar comandos de paquetes en una forma nativa de Python, por ejemplo:

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

Algunos puntos adicionales, parens y comas, pero esos no son increíbles.

Recuerdo haber visto que alguien había hecho versiones de lex y yacc en Python (prueba: http://www.dabeaz.com/ply/), por lo que si aún se necesitan, están cerca.

El punto de este laberinto es que me ha parecido que el propio Python ES el frontal "ligero" deseado utilizable por los científicos. Tengo curiosidad por saber por qué piensas que no es así, y lo digo en serio.


añadido después: La aplicación gedit anticipa que se agregan complementos y tiene su sitio sobre la explicación más clara de un procedimiento simple plug-in que he encontrado en unos pocos minutos de mirar a su alrededor.Trato:

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

todavía me gustaría entender mejor a su pregunta. No estoy seguro de si 1) quiere que los científicos puedan usar su aplicación (Python) simplemente de varias maneras o 2) desea permitir que los científicos agreguen nuevas capacidades a su aplicación. La opción n. ° 1 es la situación que enfrentamos con las imágenes y que nos llevó a usar scripts genéricos que modificamos para adaptarlos a la necesidad del momento. ¿Es la Elección n. ° 2 lo que te lleva a la idea de los complementos, o es algún aspecto de tu aplicación lo que hace que la emisión de comandos sea impracticable?

+2

+1 para el enlace a http://live.gnome.org/Gedit/PythonPluginHowTo, una buena lectura. – synthesizerpatel

+2

Reparación de la rotura del enlace: el complemento de Gedit es ahora - https: //wiki.gnome.org/Apps/Gedit/PythonPluginHowTo – ohhorob

+1

Esta es una publicación hermosa, porque muestra clara y concisamente cuán afortunados somos los biólogos modernos. Para él/ella, python es el lenguaje de scripting modular usado para dar algo de abstracción a los desarrolladores de módulos para que no necesiten analizar el código C principal. Hoy en día, sin embargo, pocos biólogos aprenderán C, sino que lo harán todo en Python. ¿Cómo abstraemos las complejidades de nuestros principales programas Python al escribir módulos? En 10 años a partir de ahora, tal vez los programas se escriban en Emoji y los módulos serán solo archivos de audio que contengan una serie de gruñidos. Y tal vez eso está bien. –

6

Disfruté de la agradable discusión sobre las diferentes arquitecturas de plugins ofrecida por el Dr. Andre Roberge en Pycon 2009. Ofrece una buena visión general de las diferentes formas de implementar complementos, comenzando por algo realmente simple.

Está disponible como podcast (segunda parte después de una explicación de mono-parchado) acompañado de una serie de six blog entries.

Recomiendo escucharlo antes de tomar una decisión.

45

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) 

Es sin duda "mínimo", no tiene absolutamente ninguna comprobación de errores, probablemente, un sinnúmero de problemas de seguridad, no es muy flexible - pero debe mostrar qué tan simple puede ser un sistema de complemento en Python.

Es probable que desee ver el módulo imp t oo, aunque puede hacer mucho con solo __import__, os.listdir y alguna manipulación de cadenas.

+3

Creo que es posible que desee cambiar 'def call_plugin (name, * args)' a 'def call_plugin (name, * args, ** kwargs)', y luego 'plugin.plugin_main (* args)' a 'plugin.plugin_main (* args, ** kwargs) ' –

+0

@RonKlein Buen punto, hecho. – dbr

+4

En python 3, 'imp' está en desuso en favor de' importlib' –

11

Marty Allchin's simple plugin framework es la base que uso para mis propias necesidades. Realmente recomiendo echarle un vistazo, creo que es realmente un buen comienzo si quieres algo simple y fácil de hackear. Puede encontrarlo también en as a Django Snippets.

+0

Estoy tratando de hacer algo así con pyduck como base. – edomaur

+0

Es muy específico de Django por lo que puedo decir. –

+3

@ZoranPavlovic: en absoluto, algunas líneas de Python estándar, no tiene que usar Django. – edomaur

2

setuptools has an EntryPoint:

puntos de entrada son una manera simple para distribuciones a “anuncian” Python objetos (tales como funciones o clases) para su uso por otras distribuciones. Las aplicaciones y marcos extensibles pueden buscar los puntos de entrada con un nombre o grupo particular, ya sea de una distribución específica o de todas las distribuciones activas en sys.path, y luego inspeccionar 0 cargar los objetos anunciados a voluntad.

AFAIK este paquete está siempre disponible si usa pip o virtualenv.

3

Llegué aquí en busca de una arquitectura mínima de complementos, y encontré muchas cosas que me parecieron exageradas. Por lo tanto, he implementado Super Simple Python Plugins. Para usarlo, puede crear uno o más directorios y soltar un archivo especial __init__.py en cada uno. La importación de esos directorios hará que todos los demás archivos de Python se carguen como submódulos, y su nombre (s) se colocará en la lista __all__. Entonces depende de usted validar/inicializar/registrar esos módulos. Hay un ejemplo en el archivo README.

9

Al buscar decoradores de Python, encontré un fragmento de código simple pero útil. Puede no encajar en tus necesidades pero muy inspirador.

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('**', '') 

Uso:

processor = TextProcessor() 
processed = processor.process(text="**foo bar**, plugins=(CleanMarkdownBolds,)) 
processed = processor.process(text="**foo bar**") 
+1

Nota: en este ejemplo, 'WordProcessor.plugin' no devuelve nada (' None'), por lo que importar la clase 'CleanMdashesExtension' luego solo importa' None'. Si las clases de complemento son útiles por sí mismas, haga que el método de clase '.plugin'' return plugin'. – jkmacc

+0

@jkmacc Tienes razón. Modifiqué el fragmento 13 días después de tu comentario. Gracias. – guneysus

2

Como otro enfoque de sistema de complemento, es posible comprobar Extend Me project.

Por ejemplo, vamos a definir la clase sencilla y su extensión

# 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) 

y tratar de usarlo:

>>> 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 

Y mostrar lo que se esconde detrás de la escena:

>>> my_cool_obj.__class__.__bases__ 
[MyCoolClassExtension1, MyCoolClass] 

extend_me biblioteca manipula clase creación pr oceso a través de metaclases, por tanto, en el ejemplo anterior, al crear nueva instancia de MyCoolClass llegamos instancia de la nueva clase que es subclase de ambos MyCoolClassExtension y MyCoolClass que tiene funcionalidad de los dos, gracias a Python multiple inheritance

Para un mejor control sobre la creación de clases hay pocas metaclases definidos en este lib:

  • ExtensibleType - permite la extensibilidad sencilla subclasificando

  • ExtensibleByHashType - similar a eXten sibleType, pero la capacidad para construir versiones especializadas de clase, lo que permite la extensión mundial de clase base y la extensión de versiones especializadas de clase

Este lib se utiliza en OpenERP Proxy Project, y parece estar funcionando lo suficientemente bueno haveing!

Por ejemplo real del uso, busque en 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 aquí es objeto extesible. RecordDateTime es la extensión.

Para habilitar la extensión, solo importe el módulo que contiene la clase de extensión, y (en el caso anterior) todos los objetos Record creados después tendrán clases de extensión en clases base, teniendo así toda su funcionalidad.

La principal ventaja de esta biblioteca es que, el código que opera objetos extensibles, no necesita saber acerca de la extensión y las extensiones podrían cambiar todo en objetos extensibles.

+0

Creo que quiere crear instancias de la subclase, es decir 'my_cool_obj = MyCoolClassExtension1()' en lugar de 'my_cool_obj = MyCoolClass()' – pylang

+0

no, la clase Extensible ha anulado el método '' '__new__''', por lo que automáticamente encuentra todas las subclases , y compilar una nueva clase, que es la subclase de todos ellos, y devolver una nueva instancia de esta clase creada. Por lo tanto, la aplicación original no necesita conocer todas las extensiones. este enfoque es útil cuando se construye una biblioteca, para permitir que el usuario final modifique o amplíe su comportamiento fácilmente. en el ejemplo anterior, MyCoolClass se puede definir en la biblioteca y usarlo, y MyCoolClassExtension podría ser definido por el usuario final. – FireMage

+0

Se agregó un ejemplo más a la respuesta – FireMage

4

En realidad setuptools trabaja con un "directorio de plugins", como el siguiente ejemplo tomado de la documentación del proyecto: el uso http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins

Ejemplo:

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) 

A la larga, setuptools es una opción mucho más segura ya que puede cargar complementos sin conflictos o sin requisitos.

Otra ventaja es que los complementos pueden extenderse utilizando el mismo mecanismo, sin que las aplicaciones originales tengan que preocuparse por ello.

2

Ampliando la respuesta de @ edomaur, puedo sugerir que eche un vistazo a simple_plugins (plug sin vergüenza), que es un marco de plugin simple inspirado en el work of Marty Alchin.

Un ejemplo de uso a corto basado en README del proyecto:

# 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> 
0

He pasado mucho tiempo tratando de encontrar sistema de plugins pequeña para Python, que se ajuste a mis necesidades. Pero luego pensé: si ya hay una herencia, que es natural y flexible, ¿por qué no usarla?

El único problema con el uso de herencia para complementos es que no se sabe cuáles son las clases de plugins más específicas (la más baja en el árbol de herencia).

Pero esto podría ser resuelto con metaclase, que realiza un seguimiento de la herencia de la clase base, y posiblemente podría construir la clase, que hereda de plugins más específicos ('Raíz extendida' en la figura siguiente)

enter image description here

Así que vinieron con una solución mediante la codificación de una metaclase tales:

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

Así que cuando usted tiene base de la raíz, hecha con metaclase, y tienen el árbol de plugins que heredan de ella, se puede obtener de forma automática de clases, que i nherits de los plugins más específicos por simplemente la subclasificación:

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

código base es bastante pequeña (~ 30 líneas de código puro) y tan flexible como la herencia permite.

Si está interesado, involucrarse @https://github.com/thodnev/pluginlib

0

He pasado el tiempo de leer este hilo, mientras yo estaba buscando un marco plug-in en Python de vez en cuando. Tengo used some but there were shortcomings con ellos. Esto es lo que se me ocurrió para su análisis en 2017, un sistema de administración de complementos sin acoplamiento, libremente acoplado: Load me later. Aquí están tutorials sobre cómo usarlo.

Cuestiones relacionadas