jueves, 22 de septiembre de 2016

Django: Test en eclipse

El problema

Ejecutar un test automático desde el eclipse sin necesidad de trabajar con una consola aparte. Existen librarias que mejoran el soporte a los test automáticos (seran tema de otro post), pero si no queremos llegar a tanto y pretendemos realizar la ejecución o, también imprescindible, el debug de algunos test no es tan directo como seleccionar simplemente Run As->Python unit-test


Debería ser así, pero la realidad es que no funciona y obtendrán el mensaje...

raise AppRegistryNotReady("Apps aren't loaded yet.")
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.

Seguramente se ejecutan los unit-test sin antes cargar cuestiones básicas del ORM y demás, todavía no me queda claro porque.


La solución


Para resolver este tema, sin instalar librerias adicionales tenemos el siguiente workaround:

1.Ir a Run Configurations...

2.Crear una nueva configuración de ejecución, muy importante estar parado sobre PyDev Django como indica la siguiente imagen.


3.Luego colocar un nombre descriptivo, tener en cuenta que esta es una configuración para correr test, luego seguramente necesitemos una para depurar, por tanto no esta demás indicar esta característica en el nombre


4.Luego indicar para que proyecto estamos armando la configuración


5.Indicar la ubicación del manage.py del proyecto


6.En argumentos colocar la palabra "test", como si lo hiciéramos por consola


7.Salvar y ejecutar el test....




Para debug:


Para debug realizar los mismos pasos pero al utilizar el debug configuration en lugar del run configuration...


Conclusion

Rápidamente se puede acceder a los test automáticos sin necesidad de componentes de terceros y cuando se entiende la idea se configura muy rápido.
Espero haya servido.




martes, 20 de septiembre de 2016

Django: Log de consultas SQL

El problema

La verdad que en el poco tiempo que llevo jugando con Django no he tenido grandes problemas con código SQL generado automáticamente por el ORM de Django, y cuando tuve algun inconveniente el mensaje de error fue lo suficientemente descriptivo como para resolverlo sin mas. 
Esto no me hace creer que no pueden aparecer problemas un poco mas complejos y que no estaría de mas tener un buen registro de todo lo que el ORM esta ejecutando sobre el motor relacional. Tiempo atrás, trabajando con Delphi/DataSnap procuraba guardar toda consulta generada en un tabla para posterior analisis/auditoria. ¿Como hacer los mismo en Django?

La solución

Yo no he pedido hacer en django que las consultas SQL se persistan en una tabla de la base de datos, pero al menos si que se guarden en un archivo del proyecto.
Tan simple como agregar la siguiente entrada en el settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
        'file': {
            'level': 'DEBUG',
            #'class': 'logging.FileHandler',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': os.path.join(BASE_DIR, 'logs/django.log'), #'/path/to/django/debug.log',
            'maxBytes': 16745#16777216, # 16megabytes
            #'formatter': 'verbose'
        },
                 
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['file'],
            'level': 'DEBUG',
        },
    }

Revisar tener la carpeta en el proyecto, en mi caso llamada logs.


Algunas pruebas

Ahora para verificar vamos a realizar algunas pruebas por consola, si aun no sabes como hacerlo en eclipse te recomiendo un post anterior Django: Testeo de escritorio sobre consola de eclipse.

Desde ORM:
Plato.objects.all()
[<Plato: Flan Casero>, <Plato: Omelette>, <Plato: Milanesa de soja>, <Plato: Tortilla>]

Consulta SQL:
(0.001)

SELECT "djSisitia_plato"."id", "djSisitia_plato"."descripcion", "djSisitia_plato"."tipo_id", "djSisitia_plato"."imagen" 
FROM "djSisitia_plato" LIMIT 21; args=()


Plato.objects.all().filter(tipo__descripcion="Postre")

(0.000) 
SELECT "djSisitia_plato"."id", "djSisitia_plato"."descripcion", "djSisitia_plato"."tipo_id", "djSisitia_plato"."imagen" 
FROM "djSisitia_plato" INNER JOIN "djSisitia_tipoplato" ON ("djSisitia_plato"."tipo_id" = "djSisitia_tipoplato"."id") 
WHERE "djSisitia_tipoplato"."descripcion" = 'Postre' 
LIMIT 21; args=('Postre',)

Django: Testeo de escritorio/interactivo sobre consola de eclipse

Trabajar sobre la consola

Seguramente para quienes tengan experiencia en lenguajes dinámicos este post les parecerá trivial, para quienes venimos del mundo de los lenguajes estáticos (En mi caso delphi, C++ o C#) la posibilidad de trabajar sobre una consola no esta contemplada, de hecho no existe la posibilidad y tampoco se me hubiese ocurrido como opción.
Siendo python un lenguaje dinámico técnicamente es posible y esta muy bien tener en mente esta idea porque compensa, al menos un poco, las penurias de no tener un compilador hecho y derecho

La idea general

Al trabajar sobre un lenguaje dinámico no tenemos un intelligent code completion decente, tenemos que trabajar mucho con la memoria y ser muy estrictos con la convenciones. Con todo, la memoria tiene limitaciones, la ide puede ayudar pero si tenemos la posibilidad de probar alguna implementacion sobre la marcha se agradece, y como se puede prever la consola lo permite probar ahí, en caliente.

El ejemplo

Para entender la idea imaginemos un ejemplo muy chico, un ejemplo mínimo de un software gastronómico donde se declara la clase"Seleccion", el objeto de la clase puede contener una serie de platos pero no puede contener mas de un plato del mismo tipo, es decir, puede tener, entrada, plato principal, postre, merienda y tanto como se quiera siempre y cuando se cumpla que no existen 2 del mismo tipo (No se agrega al ejemplo la clase Plato, ni la clase TipoPlato, la cual indica su tipo)
Para asegurarnos el control creamos un método "agregarPlato", solo se debería poder agregar platos a la selección por este método (tristemente python no permite restringirlo verdaderamente)

class Seleccion(models.Model):

    platos = models.ManyToManyField(Plato, related_name="selecciones")
    comenzal = ....
    otros atributos...
  
    def agregarPlato(self, plato):
        """
         Controlar aqui regla de que solo
         un plato de cada tipo!
        """
        pass

La consola

Para abrir la consola de python en eclipse hacer lo siguiente:
Boton derecho en raiz del proyecto->Django->Shell with django environment


El resultado deberia ser el siguiente,,,


El proceso de prueba

Importaciones

Ahora bien, vamos a codificar y probar. Lo primero es importar nuestras clases.


Tip: No es necesario realizar el proceso de importación manualmente, la ide puede ayudar. Simplemente tratemos de instanciar un objeto y con ctrl-space podemos tocar en las sugerencias de importación como indica la imagen


esta técnica puede ahorrar mucho tiempo...

Datos maestros

Volviendo a nuestro tema, vamos a investigar un poco si tenemos los datos que nos permitan realizar la prueba....


plts = Plato.objects.all()
plts

[<Plato: Flan Casero>, <Plato: Omelette>, <Plato: Milanesa de soja>, <Plato: Tortilla>]

Tenemos platos, veamos los tipos de esos platos...

for p in plts:
     print(p.tipo.descripcion)

Postre
Principal
Principal
Principal

Como vemos los ultimo tres platos son del tipo "Principal" si intento meter mas de uno en una "Seleccion" la clase no debería permitirlo

La prueba

Primero creamos una Seleccion:

from djSisitia.models.Seleccion import Seleccion
sel = Seleccion()

Como la relación es many to many tengo que salvar primero (aun no tengo estudiado el tema).

#Completar todos los datos obligatorios y guardar
sel.save()

Ahora dejamos la consola como esta e intentamos codificar el método "agregarPlato"
Para lograr el objetivo vamos a aplicar una logica muy simple, si ya existe un plato del mismo tipo lanzamos un excepción...
Como estamos aprendiendo a desarrollar en django vamos a usar la consola para investigar como seria preguntar si la selección tiene ya un plato de un tipo determinado, primero metemos platos por izquierda, utilizando el add de la coleccion

plts
[<Plato: Flan Casero>, <Plato: Omelette>, <Plato: Milanesa de soja>, <Plato: Tortilla>]

#Agrego un postre
sel.platos.add(plts[0]) 

#Pregunto si en la lista de platos de la seleccion existe un plato del tipo plts[0].tipo (que es postre)
sel.platos.filter(tipo=plts[0].tipo) 
[<Plato: Flan Casero>]

Listo! Funciona, ahora si a implementar...

def agregarPlato(self, plato):
        pl = self.platos.filter(tipo=plato.tipo)
        if not pl:
            self.platos.add(plato)
        else:
            raise Exception("Se esta intentando meter mas de plato del mismo tipo")
        return self.platos.all().count()

probemos....


sel.agregarPlato(plts[0]) #Se agrega un plato del tipo postre

1 #Exito

sel.agregarPlato(plts[0]) #Se otro plato del tipo postre

Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "C:\Users\pabli\git\djSisitia\djSisitia\djSisitia\models\Seleccion.py", line 29, in agregarPlato
    raise Exception("Se esta intentando meter mas de plato del mismo tipo")
Exception: Se esta intentando meter mas de plato del mismo tipo #Error


sel.agregarPlato(plts[1]) #Se agrega un plato del tipo principal


2  #Exito

sel.agregarPlato(plts[1]) #Se otro plato del tipo principal

Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "C:\Users\pabli\git\djSisitia\djSisitia\djSisitia\models\Seleccion.py", line 29, in agregarPlato
    raise Exception("Se esta intentando meter mas de plato del mismo tipo")
Exception: Se esta intentando meter mas de plato del mismo tipo  #Error

Conclusion

Lo mas importante a destacar es que al estar limitados con las capacidades del ide para autocompletar código y especialmente cuando uno esta en la etapa de aprendizaje esta técnica realmente ayuda a probar características, métodos, soluciones. En el ejemplo queda especialmente visible cuando se debió probar las técnicas del filtrado que (aunque son muy básicas) quien esta aprendiendo no domina totalmente.

Tip: Se debe tener en cuenta que ciertos cambios en el código requieren volver a lanzar la consola para que el interprete vuelta a interpretar las implementaciones, desconozco si existe una forma de deshabilitar esto y que reinterprete cualquier cambio, es un tema que voy a investigar y puede ser la próxima entrada dado que me parece que es muy factible que exista.