2011-08-26 21 views
10

Estoy buscando técnicas que permitan a los usuarios sobrescribir módulos en una aplicación o ampliar una aplicación con módulos nuevos.¿Cuáles son las técnicas más utilizadas para habilitar las extensiones de código de usuario en Python?

Imagine una aplicación llamada pydraw. Actualmente proporciona una clase Circle, que hereda Shape. El árbol de paquetes podría ser:

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

Ahora supongamos que me gustaría para permitir el descubrimiento dinámico y carga de los módulos de usuario que implementan una nueva forma, o tal vez incluso la propia clase Shape. Parece más sencillo para el árbol de un usuario que tiene la misma estructura que el árbol de aplicaciones, tales como:

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

En otras palabras, me gustaría que desea combinar y enmascarar un árbol de la aplicación con el mismo nombre los archivos de la árbol del usuario, o al menos obtener esa estructura aparente desde el punto de vista de Python.

Luego, al configurar sys.path o PYTHONPATH, pydraw.shapes.square podría ser detectable. Sin embargo, la búsqueda del camino del módulo de Python no encuentra módulos como square.py. Supongo que esto se debe a que __method__ ya contiene un módulo principal en otra ruta.

¿Cómo lograrías esta tarea con Python?

Respuesta

1

Si desea cargar el código Python de forma dinámica desde diferentes ubicaciones, puede ampliar la búsqueda __path__ atributos utilizando la pkgutil module:

Al colocar estas líneas en cada pydraw/__init__.py y pydraw/shapes/__init__.py:

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

Podrá escribir la instrucción de importación como si tuviera un paquete único:

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

Puede pensar en el registro automático de sus complementos. Todavía puede usar el código python básico para eso estableciendo una variable de módulo (que actuará como un tipo de patrón singleton).

Añadir la última línea de cada archivo pydraw/shapes/__init__.py:

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

# your shape registry 
__shapes__ = [] 

Ahora puede registrar una forma en la parte superior de su módulo correspondiente (circle.py o square.py aquí).

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

último control:

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

Gracias Julien- extend_path es exactamente lo que estaba buscando! Creí erróneamente que un paquete se podía cargar desde un solo árbol de directorios. – Reece

3

El descubrimiento de extensiones puede ser un poco frágil y complejo, y también requiere que revises todo el PYTHONPATH, que puede ser muy grande.

En su lugar, tiene un archivo de configuración que enumera los complementos que se deben cargar. Esto se puede hacer listándolos como nombres de módulo, y también requiriendo que estén ubicados en PYTHONPATH, o simplemente enumerando las rutas completas.

Si quiere configuraciones de nivel de usuario, tendría tanto un archivo de configuración global que enumera los módulos, como uno por usuario, y simplemente lea estos archivos de configuración en lugar de probar algún mecanismo de descubrimiento.

Además, intenta no solo agregar complementos, sino también anular los componentes de su aplicación. Para eso usaría la arquitectura de componentes Zope. Sin embargo, aún no está completamente portado a Python 3, pero está diseñado para ser utilizado exactamente en este tipo de casos, aunque por su descripción, este parece ser un caso simple, y el ZCA podría ser excesivo. Pero míralo de todos modos.

1

Un método que utiliza para manejar un problema tal era con un provider pattern.

En su shape.py módulo:

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

y en forma del usuario.módulo py:

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

con provider método de hacer algo como esto:

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

Y cuando es necesario instanciar un objeto, utilice un ProvideFactory como esto

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

Usted probablemente puede limpiar esto un poco mediante el uso de decoradores. Realmente no veo de dónde viene el término "patrón del proveedor"; esto me parece una fábrica abstracta bastante ordinaria. –

+0

@Karl Knechtel: sí, podríamos usar decoradores de clase, pero en este caso traté de explicar la idea, no realmente implementar todo;). Para el patrón de proveedor, tienes razón, es más o menos cerca del patrón Abstract Factory, solía nombrarlo patrón de proveedor, ya que es la forma en que me han enseñado sobre ... –

+0

Esto es similar a lo que hace ZCA, pero por supuesto con muchas menos características. Podría tener sentido en casos simples cuando no desee dependencias externas. –

0

Puede detectar archivo cambios dentro de un cierto directorio con métodos específicos del sistema operativo. Para todos los sistemas operativos, existen herramientas de monitoreo de archivos que producen eventos cuando se cambia un archivo o directorio. Alternativamente, puede buscar continuamente archivos más nuevos que la hora de la última búsqueda. Hay varias soluciones posibles, pero en cualquier caso:

  • configurar un directorio de complementos facilita mucho más las cosas que la supervisión de su sistema de archivos completo.
  • en busca de cambios en los archivos en un hilo separado es probablemente la mejor solución
  • si se encuentra un nuevo archivo .py, se puede importar utilizando el __import__ función integrada de
  • si un .py es cambiado, puede volver a importar con el reload función integrada de
  • Si se cambia una clase, las instancias de esa clase todavía se comportará como instancias de la clase de edad, así que asegúrese de recrear casos cuando sea necesario

EDITAR:

Si agrega su directorio de complementos como el primer directorio en PYTHONPATH, ese directorio tendrá prioridad sobre los otros directorios en pythonpath, p.

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