Esta pagina se ve mejor con JavaScript habilitado

Debugear código Python/Django

 ·  🎃 kr0m

En este artículo aprenderemos como debugear código Python de nuestro proyecto Django, esto nos permitirá inspeccionar valores de variables, ejecutar código paso a paso entre otras muchas funcionalidades.

Antes de nada activamos nuestro venv:

cd /home/kr0m/rxWod/
source bin/activate

Instalamos ipdb :

cd rxWodProject
pip install ipdb

En nuestras vistas debemos importar ipdb y asignar breakpoints con ipdb.set_trace():

vi rxWod/views.py

from django.shortcuts import get_object_or_404, get_list_or_404, render, redirect
from django.http import HttpResponse, HttpResponseRedirect
from .models import Routine, Exercise
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django.utils.translation import gettext as _
from django.utils.translation import ngettext
from django.http import JsonResponse
import math
import json
import random
import ipdb

max_routines_per_user = 25
max_routine_exercises = 10
max_rounds = 4

max_exercise_reps = 12

@login_required()
@require_http_methods(["GET"])
def index(request):
    #print('username: %s' % request.user.username)

    clear_empty_routines(request.user)

    routines = Routine.objects.filter(user=request.user)
    #print('routines: %s' % routines)
    ipdb.set_trace()

    categories = Exercise._meta.get_field('category_id').choices
    categories = [category[1] for category in categories]
    #print('categories: %s' % categories)

    string_categories = ','.join(map(str, categories))
    #print('string_categories: %s' % string_categories)

    context = {
        'routines': routines,
        'categories': string_categories,
    }
    return render(request, 'rxWod/index.html', context)

Arrancamos nuestro servidor:

python manage.py runserver 0.0.0.0:8000

Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

Y cuando carguemos desde el navegador la vista index, aparecerá en la consola donde tenemos el servidor web activo, la siguiente salida:

/usr/home/kr0m/rxWod/lib/python3.7/site-packages/IPython/core/history.py:226: UserWarning: IPython History requires SQLite, your history will not be saved
  warn("IPython History requires SQLite, your history will not be saved")
> /usr/home/kr0m/rxWod/rxWodProject/rxWod/views.py(31)index()
     30 
---> 31     categories = Exercise._meta.get_field('category_id').choices
     32     categories = [category[1] for category in categories]

Podemos ver que la ejecución de código se ha parado en la línea 31 tal como deseamos, para poder ver un poco mas en contexto la zona de código podemos ejecutar la orden ll:

ipdb> ll

     20 @login_required()
     21 @require_http_methods(["GET"])
     22 def index(request):
     23     #print('username: %s' % request.user.username)
     24 
     25     clear_empty_routines(request.user)
     26 
     27     routines = Routine.objects.filter(user=request.user)
     28     #print('routines: %s' % routines)
     29     ipdb.set_trace()
     30 
---> 31     categories = Exercise._meta.get_field('category_id').choices
     32     categories = [category[1] for category in categories]
     33     #print('categories: %s' % categories)
     34 
     35     string_categories = ','.join(map(str, categories))
     36     #print('string_categories: %s' % string_categories)
     37 
     38     context = {
     39         'routines': routines,
     40         'categories': string_categories,
     41     }
     42     return render(request, 'rxWod/index.html', context)
     43 

ipdb> 

Se trata de una shell en el propio debugger, aquí podremos inspeccionar valores de variables, ejecutar código paso a paso entre otras muchas funcionalidades.

Avanzamos un paso n de next, pero sin abandonar la parte del código donde nos encontramos, es decir no seguimos las llamadas a otras funciones:

ipdb> n

> /usr/home/kr0m/rxWod/rxWodProject/rxWod/views.py(32)index()
     31     categories = Exercise._meta.get_field('category_id').choices
---> 32     categories = [category[1] for category in categories]
     33     #print('categories: %s' % categories)

En caso de querer seguir las llamadas deberíamos avanzar con s de step.
Si queremos repetir el último comando sin necesidad de volver a escribirlo tan solo debemos presionar ENTER, igual como en GDB :)

Podemos ver el valor de las variables locales:

ipdb> locals()

{'request': <WSGIRequest: GET '/'>, 'routines': <QuerySet [<Routine: 2021-08-18 07:18:26.887464+00:00>, <Routine: 2021-08-21 09:49:17.355769+00:00>]>, 'categories': [(0, 'Hombros'), (1, 'Espalda'), (2, 'Biceps'), (3, 'Triceps'), (4, 'Pecho'), (5, 'Core'), (6, 'Glúteos'), (7, 'Cuadriceps'), (8, 'Isquiotibial'), (9, 'Cardio'), (10, 'Lumbar'), (11, 'Agarre')]}

Podemos imprimir el valor de una variable en concreto:

ipdb> categories

[(0, 'Hombros'), (1, 'Espalda'), (2, 'Biceps'), (3, 'Triceps'), (4, 'Pecho'), (5, 'Core'), (6, 'Glúteos'), (7, 'Cuadriceps'), (8, 'Isquiotibial'), (9, 'Cardio'), (10, 'Lumbar'), (11, 'Agarre')]

Un modo mas cómodo de visualizar las variables es mediante prettyPrint:

ipdb> pp categories

[(0, 'Hombros'),
 (1, 'Espalda'),
 (2, 'Biceps'),
 (3, 'Triceps'),
 (4, 'Pecho'),
 (5, 'Core'),
 (6, 'Glúteos'),
 (7, 'Cuadriceps'),
 (8, 'Isquiotibial'),
 (9, 'Cardio'),
 (10, 'Lumbar'),
 (11, 'Agarre')]

El debuger nos facilita la vida hasta tal punto que nos permite tabular para que aparezcan las opciones de los valores de la variable o los métodos de los objetos:


Como ejemplo consultamos el valor de las headers recibidas:

ipdb> request.headers

{'Content-Length': '', 'Content-Type': 'text/plain', 'Host': 'localhost:8000', 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0', 'Sec-Ch-Ua': '"Chromium";v="91", " Not;A Brand";v="99"', 'Sec-Ch-Ua-Mobile': '?0', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (X11; FreeBSD amd64; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Dest': 'document', 'Referer': 'http://localhost:8000/', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'es', 'Cookie': 'csrftoken=Yl6erIV4TbC3mRTq1J8wO2UH7d3x474LCwmritEdrTl4DITUdbK18EYsTtgqsjih; sessionid=w3e7z8jxg4yurjl69txju69tobb08vdm'}

Podemos incluso ejecutar código Python sobre los objetos como contar el número de rutinas obtenidas de la base de datos:

ipdb> routines.count()

2

Finalmente le damos a c de continue y el navegador mostrará la web.

En mis pruebas solo he detectado un pequeño inconveniente y es que desde la shell del debugger se pueden asignar breakpoints al vuelo pero esto falla en proyectos Django por alguna razón, deja asignar el breakpoint pero al darle a continuar no se detiene en dicho breakpoint.

En cambio probándolo en un script de Python tradicional funciona sin problemas, si ipdb también sirve para debugear script de Python de toda la vida.

Como solución se pueden asignar varios breakpoints por código ejecutando ipdb.set_trace() en cada punto.

Esta forma de depurar resulta muy útil ya que podemos ver de forma interactiva que partes del código se están ejecutando, si entra en algún if que no debe, si se queda atascado en algún for eternamente, si está realizando alguna petición externa a una API que no funciona en ese momento o cualquier otro malfuncionamiento.

Como nota final dejo un enlace a una web con una hoja resumen de los comandos de ipdb:
https://wangchuan.github.io/coding/2017/07/12/ipdb-cheat-sheet.html

Si te ha gustado el artículo puedes invitarme a un RedBull aquí