Plugins en Python
Siempre me ha gustado escribir aplicaciones extensibles, pero picarme todo un sistema de plugins me ha parecido tedioso.
Por eso he intentado buscar librerías que me ayuden a crear plugins, aunque siempre he tenido problemas para la distribución de éstos, como me ha pasado con Yapsy.
La verdad es que me he sentido como un auténtico estúpido al descubrir que Python tiene un sistema para escribir plugins muy sencillo de usar. Vamos a ver cómo.
Puedes encontrar el artículo original en: MagMax Blog.
Qué voy a hacer
Básicamente, vamos a hacer una pequeña aplicación y dos plugins. Dependiendo de una opción se usará uno u otro.
Estructura:
.
├── app
│ └── app.py
├── plugin1
│ ├── plugin1
│ │ └── __init__.py
│ └── setup.py
└── plugin2
├── plugin2
│ └── __init__.py
└── setup.py
Plugin 1
Vamos a comenzar escribiendo un plugin. Va a ser algo muy sencillo. Para
ello, creamos dos ficheros; el primero será el plugin propiamente dicho,
en el fichero plugin1/plugin1/__init__.py
:
def example():
print("I'm plugin one")
Y aquí está el truco: en el archivo plugin1/setup.py
:
from setuptools import setup, find_packages
setup(
name='plugin1',
version='0.0.6',
description="This is the plugin 1",
packages=find_packages('.'),
entry_points={
'plugin_system': 'example = plugin1:example'
},
)
Lo he reducido al mínimo. La parte importante es la de entry_points
,
ya que estoy definiendo un entry point llamado plugin_system
que,
básicamente, asigna a una variable la función anterior.
Con esto ya tenemos el plugin. Vamos a compilarlo (para ahorrar
problemas, lo compilaremos como source
):
$ python setup.py sdist
lo que generará el archivo dist/plugin1-0.0.6.tar.gz
. La versión 0.0.6
es porque 0.0.1 era muy sosa XD
Aplicación
Vamos ahora con la aplicación principal (app/app.py
):
import argparse
import pkg_resources
def main():
parser = argparse.ArgumentParser(description='Loads a plugin')
parser.add_argument('action', choices=['run', 'list'],
help='action to be performed')
parser.add_argument('-p', '--plugin',
help='plugin to be loaded')
args = parser.parse_args()
if args.action == 'list':
full_env = pkg_resources.Environment()
dists, errors = pkg_resources.WorkingSet().find_plugins(full_env)
for dist in dists:
if 'plugin_system' in dist.get_entry_map():
print(' %s (%s)' % (dist.project_name, dist.version))
elif args.action == 'run':
requirement = pkg_resources.Requirement(args.plugin)
plugin = pkg_resources.WorkingSet().find(requirement)
example = plugin.load_entry_point('plugin_system', 'example')
example()
if __name__ == '__main__':
main()
Como se puede observar, hago uso intensivo de pkg_resources
. Podemos
probar a listar los plugins instalados:
$ python app/app.py list
$
Y no tendremos nada. Claro, falta instalar el plugin. Para ello,
simplemente usamos pip
, pero me voy a crear un virtualenv
para no
engorrinarme el sistema:
$ virtualenv venv
[...]
$ . venv/bin/activate
(venv) $ pip install plugin1/dist/plugin1-0.0.6.tar.gz
[...]
(venv) $ python app.py list
plugin1 (0.0.6)
(venv) $
Mucho mejor. Ahora vamos a ejecutarlo:
(venv) $ python app.py run -p plugin1
I'm plugin one
(venv) $
Plugin2
El lector avispado no tendrá problema en crearlo a partir del plugin1 :) Es más, podéis crear todos los que queráis XD
Notas
La gracia es que podemos tener más de un entrypoint
, o agruparlos por
clave.
Con esto ya no necesito Yapsy ni PluginBase ni ningún otro sistema enrevesado.
Para más información, podéis leer Dynamic Discovery of Services and Plugins.