Tutoriales

Cómo usar el depurador de Python (pdb)

Todos sabemos que se pueden cometer errores al escribir programas. Errores de gramática, errores tipográficos, incluso secciones de código olvidadas; todo es posible. A veces, estos problemas son fáciles de detectar. Aquí hay un ejemplo:

$ python3
Python 3.9.9 (main, Nov 19 2021, 00:00:00) 
[GCC 10.3.1 20210422 (Red Hat 10.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> DEBUG: bool = True
>>> 
>>> def print_var_function(val: str):
...     print(f"This is a simple dummy function that prints val={val}")
... 
>>> if __name__ == "__main__":
...     print(f"What is your name?")
...     name = input()
...     if DEBUG:
...         print_var_function(name)
... 
What is your name?
jose
This is a simple dummy function that prints val=jose

Incluso si escribe pequeños programas en Python, pronto descubrirá que trucos como este no son suficientes para depurar el programa. En su lugar, puede aprovechar el depurador de Python (pdb) y obtener una mejor comprensión de cómo se comporta su aplicación.

empezando

Antes de comenzar este tutorial, debe tener:

  • Conocimiento práctico de Python (objetos, flujos de trabajo, estructuras de datos)
  • Curiosidad por saber cómo solucionar los problemas de los scripts en tiempo real
  • Una máquina que pueda ejecutar Python 3 (por ejemplo, estoy usando Fedora Linux)

notas: Usaré una versión moderna de Python (3.7+) para este tutorial, pero puede encontrar una sintaxis antigua para algunas operaciones en la documentación oficial de pdb.

Estudio de caso: un script simple para generar un gráfico de red

Un amigo tuyo te dio un pequeño script de Python para probar. Dice que lo escribió a toda prisa y que puede contener errores (de hecho, admite que intentó ejecutarlo, pero está bastante seguro de que la prueba de concepto es buena). También dijo que el guión depende del módulo Diagramas. Es hora de probar su guión.

Primero, cree un entorno virtual e instale algunas dependencias:

python3 -m venv ~/virtualenv/pythondebugger
. ~/virtualenv/pythondebugger/bin/activate
pip install --upgrade pip diagrams

A continuación, descargue e instale los siguientes scripts:

$ pushd $HOME
$ git clone [email protected]:josevnz/tutorials.git
$ pushd tutorials/PythonDebugger

Desafortunadamente, cuando ejecuta el script, se bloquea:

(pythondebugger) $ ./simple_diagram.py --help
Traceback (most recent call last):
  File "/home/josevnz/tutorials/PythonDebugger/./simple_diagram.py", line 8, in 
    from diagrams.onprem.queue import Celeri
ImportError: cannot import name 'Celeri' from 'diagrams.onprem.queue' (/home/josevnz/virtualenv/pythondebugger/lib64/python3.9/site-packages/diagrams/onprem/queue.py)

Entonces, al menos una importación es incorrecta. sospechas que es apio (deletreado por tu amigo) Céleri), por lo que inicia el script nuevamente con el modo depurador de Python habilitado:

(pythondebugger) $ python3 -m pdb simple_diagram.py --help
> /home/josevnz/tutorials/PythonDebugger/simple_diagram.py(2)()
-> """
(Pdb)

Sin fallas, el indicador (Pdb) le indica que actualmente se encuentra en la línea 2 del programa:

(pythondebugger) $ python3 -m pdb simple_diagram.py --help
> /home/josevnz/tutorials/PythonDebugger/simple_diagram.py(2)()
-> """
(Pdb) l
  1  	#!/usr/bin/env python
  2  ->	"""
  3  	Script that show a basic Airflow + Celery Topology
  4  	"""
  5  	import argparse
  6  	from diagrams import Cluster, Diagram
  7  	from diagrams.onprem.workflow import Airflow
  8  	from diagrams.onprem.queue import Celeri
  9  	
 10  	
 11  	def generate_diagram(diagram_file: str, workers_n: int):

puedes presionar c (continuar) hasta que se produzca un punto de interrupción:

(Pdb) c
Traceback (most recent call last):
  File "/usr/lib64/python3.9/pdb.py", line 1723, in main
    pdb._runscript(mainpyfile)
  File "/usr/lib64/python3.9/pdb.py", line 1583, in _runscript
    self.run(statement)
  File "/usr/lib64/python3.9/bdb.py", line 580, in run
    exec(cmd, globals, locals)
  File "", line 1, in 
  File "/home/josevnz/tutorials/PythonDebugger/simple_diagram.py", line 2, in 
    """
ImportError: cannot import name 'Celeri' from 'diagrams.onprem.queue' (/home/josevnz/virtualenv/pythondebugger/lib64/python3.9/site-packages/diagrams/onprem/queue.py)
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/josevnz/tutorials/PythonDebugger/simple_diagram.py(2)()
-> """

El problema es: la importación incorrecta no permitirá que el script continúe.Entonces, desde n (siguiente paso) y muévase línea por línea:

Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(2)()
-> """
(Pdb) step
Post mortem debugger finished. The /home/josevnz/tutorials/PythonDebugger//simple_diagram.py will be restarted
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(2)()
-> """
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(5)()
-> import argparse
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(6)()
-> from diagrams import Cluster, Diagram
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(7)()
-> from diagrams.onprem.workflow import Airflow
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(8)()
-> from diagrams.onprem.queue import Celeri
(Pdb) n
ImportError: cannot import name 'Celeri' from 'diagrams.onprem.queue' (/home/josevnz/virtualenv/pythondebugger/lib64/python3.9/site-packages/diagrams/onprem/queue.py)
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(8)()
-> from diagrams.onprem.queue import Celeri

No puede continuar a menos que corrija la instrucción rota en la línea 8.necesitas reemplazar import Celeri y import Celery:

(Pdb) from diagrams.onprem.queue import Celery
(Pdb) exit()

Por el bien del argumento, tal vez desee que su programa entre en modo de depuración cuando falte un módulo (en realidad, insiste en tener un Céleri módulo).Los cambios de código son simples; simplemente capture ImportError y llama breakpoint() Función (antes de mostrar el seguimiento de la pila con el seguimiento inverso):

#!/usr/bin/env python
"""
Script that show a basic Airflow + Celery Topology
"""
try:
    import argparse
    from diagrams import Cluster, Diagram
    from diagrams.onprem.workflow import Airflow
    from diagrams.onprem.queue import Celeri
except ImportError:
    breakpoint()

Si llama al script normalmente y falta una de las importaciones, el depurador se iniciará después de imprimir el seguimiento de la pila:

(pythondebugger) $ python3 simple_diagram.py --help
Exception while importing modules:
------------------------------------------------------------
Traceback (most recent call last):
  File "/home/josevnz/tutorials/PythonDebugger//simple_diagram.py", line 11, in 
    from diagrams.onprem.queue import Celeri
ImportError: cannot import name 'Celeri' from 'diagrams.onprem.queue' (/home/josevnz/virtualenv/pythondebugger/lib64/python3.9/site-packages/diagrams/onprem/queue.py)
------------------------------------------------------------
Starting the debugger
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(21)()
-> def generate_diagram(diagram_file: str, workers_n: int):
(Pdb) 

Solucione la importación incorrecta y continúe para ver qué puede hacer el script:

(pythondebugger) $ python3 simple_diagram.py --help
usage: /home/josevnz/tutorials/PythonDebugger//simple_diagram.py [-h] [--workers WORKERS] diagram

Generate network diagrams for examples used on this tutorial

positional arguments:
  diagram            Name of the network diagram to generate

optional arguments:
  -h, --help         show this help message and exit
  --workers WORKERS  Number of workers
  
# Generate a diagram with 3 workers
(pythondebugger) [[email protected] ]$ python3 simple_diagram.py --workers 3 my_airflow.png

El gráfico resultante se ve así:

(José Vicente Nunes, CC BY-SA 4.0)

Mire de cerca el código con paso, continuar, argumentos y puntos de interrupción

Ejecute el script nuevamente, pero con trabajadores negativos:

$ python3 simple_diagram.py --workers -3 my_airflow2.png

Esto produce una imagen extraña sin trabajadores de Celery:

(José Vicente Nunes, CC BY-SA 4.0)

Esto es inesperado.Use un depurador para entender lo que está pasando y descubra una forma de evitarlo; primero pida ver el código fuente completo ll:

(pythondebugger) $ python3 -m pdb simple_diagram.py --workers -3 my_airflow2.png
> /home/josevnz/tutorials/PythonDebugger/simple_diagram.py(2)()
-> """
(Pdb) ll
  1  	#!/usr/bin/env python
  2  ->	"""
  3  	Script that show a basic Airflow + Celery Topology
  4  	"""
  5  	try:
  6  	    import sys
  7  	    import argparse
  8  	    import traceback
  9  	    from diagrams import Cluster, Diagram
 10  	    from diagrams.onprem.workflow import Airflow
 11  	    from diagrams.onprem.queue import Celery
 12  	except ImportError:
 13  	    print("Exception while importing modules:")
 14  	    print("-"*60)
 15  	    traceback.print_exc(file=sys.stderr)
 16  	    print("-"*60)
 17  	    print("Starting the debugger", file=sys.stderr)
 18  	    breakpoint()
 19  	
 20  	
 21  	def generate_diagram(diagram_file: str, workers_n: int):
 22  	    """
 23  	    Generate the network diagram for the given number of workers
 24  	    @param diagram_file: Where to save the diagram
 25  	    @param workers_n: Number of workers
 26  	    """
 27  	    with Diagram("Airflow topology", filename=diagram_file, show=False):
 28  	        with Cluster("Airflow"):
 29  	            airflow = Airflow("Airflow")
 30  	
 31  	        with Cluster("Celery workers"):
 32  	            workers = []
 33  	            for i in range(workers_n):
 34  	                workers.append(Celery(f"Worker {i}"))
 35  	        airflow - workers
 36  	
 37  	
 38  	if __name__ == "__main__":
 39  	    PARSER = argparse.ArgumentParser(
 40  	        description="Generate network diagrams for examples used on this tutorial",
 41  	        prog=__file__
 42  	    )
 43  	    PARSER.add_argument(
 44  	        '--workers',
 45  	        action='store',
 46  	        type=int,
 47  	        default=1,
 48  	        help="Number of workers"
 49  	    )
 50  	    PARSER.add_argument(
 51  	        'diagram',
 52  	        action='store',
 53  	        help="Name of the network diagram to generate"
 54  	    )
 55  	    ARGS = PARSER.parse_args()
 56  	
 57  	    generate_diagram(ARGS.diagram, ARGS.workers)
(Pdb)

No es muy eficiente ir paso a paso aquí, así que simplemente profundice en el código hasta la línea 57 y vea el efecto de pasar el número de trabajadores. == -1 (imprime bastante la variable ARGS):

> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(57)()
-> generate_diagram(ARGS.diagram, ARGS.workers)
(Pdb) pp ARGS
Namespace(workers=-3, diagram='my_airflow2.png')

Además, conocer el tipo de objeto es muy útil. Compruebe para ver el tipo ARGS:

(Pdb) whatis ARGS

Parece que el siguiente paso lógico es profundizar en la función que genera el gráfico. l (lista) para confirmar su ubicación:

(Pdb) l
 52  	        action='store',
 53  	        help="Name of the network diagram to generate"
 54  	    )
 55  	    ARGS = PARSER.parse_args()
 56  	
 57  ->	    generate_diagram(ARGS.diagram, ARGS.workers)
[EOF]

buceando uno s (paso) entra y muévete n (Siguiente) una instrucción:

(Pdb) s
--Call--
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(21)generate_diagram()
-> def generate_diagram(diagram_file: str, workers_n: int):
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(27)generate_diagram()
-> with Diagram("Airflow topology", filename=diagram_file, show=False):
(Pdb) l
 22  	    """
 23  	    Generate the network diagram for the given number of workers
 24  	    @param diagram_file: Where to save the diagram
 25  	    @param workers_n: Number of workers
 26  	    """
 27  ->	    with Diagram("Airflow topology", filename=diagram_file, show=False):
 28  	        with Cluster("Airflow"):
 29  	            airflow = Airflow("Airflow")
 30  	
 31  	        with Cluster("Celery workers"):
 32  	            workers = []

Promete decirte que estás dentro generate_diagram Características. Confirme qué (parámetros) se pasan:

(Pdb) a generate_diagram
diagram_file="my_airflow2.png"
workers_n = -3
(Pdb) 

Verifique el código nuevamente y estudie dónde itera workers_n Número de veces para agregar trabajadores de apio al gráfico:

(Pdb) l
 33  	            for i in range(workers_n):
 34  	                workers.append(Celery(f"Worker {i}"))
 35  	        airflow - workers
 36  	
 37  	
 38  	if __name__ == "__main__":
 39  	    PARSER = argparse.ArgumentParser(
 40  	        description="Generate network diagrams for examples used on this tutorial",
 41  	        prog=__file__
 42  	    )
 43  	    PARSER.add_argument(

Líneas 33-34 relleno workers Lista con objetos de apio.puedes ver lo que está pasando range() Una función al intentar crear un iterador al pasar un número negativo:

(Pdb) p range(workers_n)
range(0, -3)

La teoría dice que esto generará un iterador vacío, lo que significa que el ciclo nunca se ejecutará.Confirmar estado antes y después workers Cambiando.Para ello, establezca dos b (punto de ruptura):

  1. después workers Las variables se inicializan en la línea 33.
  2. después de llenar el bucle workers Salida, en la línea 35.

No desea ejecutar todo el código a la vez, por lo que c (continuar) a través de b (punto de interrupción) a:

(Pdb) b simple_diagram.py:33
Breakpoint 1 at /home/josevnz/tutorials/PythonDebugger//simple_diagram.py:33
(Pdb) b simple_diagram.py:35
Breakpoint 2 at /home/josevnz/tutorials/PythonDebugger//simple_diagram.py:35
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /home/josevnz/tutorials/PythonDebugger//simple_diagram.py:33
2   breakpoint   keep yes   at /home/josevnz/tutorials/PythonDebugger//simple_diagram.py:35

Ahora es el momento de imprimir el contenido. workers y continuar como prometí:

(Pdb) c
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(33)generate_diagram()
-> for i in range(workers_n):
(Pdb) p workers
[]
(Pdb) c
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(35)generate_diagram()
-> airflow - workers
(Pdb) workers
[]

si presionas c (continuar) el programa saldrá, o r (regresar) te traerá de vuelta principal.utilizar devoluciones:

(Pdb) r
--Return--
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(35)generate_diagram()->None
-> airflow - workers
(Pdb) c
The program finished and will be restarted
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(2)()
-> """
(Pdb) exit()

Puede arreglar la aplicación agregando algo de codificación defensiva a la aplicación --workers Argumento que acepta un valor entre 1 y 10 (los gráficos con más de 10 trabajadores pueden no verse bien). A continuación, es hora de cambiar el código para agregar validación:

def valid_range(value: str, upper: int  = 10):
    try:
        int_val = int(value)
        if 1 <= int_val <= upper:
            return int_val
        raise ArgumentTypeError(f"Not true: 1<= {value} <= {upper}")
    except ValueError:
        raise ArgumentTypeError(f"'{value}' is not an Integer")


if __name__ == "__main__":
    PARSER = argparse.ArgumentParser(
        description="Generate network diagrams for examples used on this tutorial",
        prog=__file__
    )
    PARSER.add_argument(
        '--workers',
        action='store',
        type=valid_range,
        default=1,
        help="Number of workers"
    )

Por supuesto, desea saber si esto funciona, así que reinicie el depurador y establezca un punto de interrupción en la línea 39:

(Pdb) b simple_diagram.py:39
Breakpoint 1 at /home/josevnz/tutorials/PythonDebugger//simple_diagram.py:39
(Pdb) c
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(39)valid_range()
-> try:
(Pdb) l
 34  	            for i in range(workers_n):
 35  	                workers.append(Celery(f"Worker {i}"))
 36  	        airflow - workers
 37  	
 38  	def valid_range(value: str, upper: int  = 10):
 39 B->	    try:
 40  	        int_val = int(value)
 41  	        if 1 <= int_val <= upper:
 42  	            return int_val
 43  	        raise ArgumentTypeError(f"Not true: 1<= {value} <= {upper}")
 44  	    except ValueError:
(Pdb) a valid_range
value="-3"
upper = 10
(Pdb) p int(value)
-3
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(40)valid_range()
-> int_val = int(value)
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(41)valid_range()
-> if 1 <= int_val <= upper:
(Pdb) p 1 <= int_val <= upper
False
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(43)valid_range()
-> raise ArgumentTypeError(f"Not true: 1<= {value} <= {upper}")
(Pdb) n
argparse.ArgumentTypeError: Not true: 1<= -3 <= 10
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(43)valid_range()
-> raise ArgumentTypeError(f"Not true: 1<= {value} <= {upper}")
(Pdb) n
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(44)valid_range()
-> except ValueError:
(Pdb) n
--Return--
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(44)valid_range()->None
-> except ValueError:
(Pdb) n
--Call--
> /usr/lib64/python3.9/argparse.py(744)__init__()
-> def __init__(self, argument, message):
(Pdb) c
usage: /home/josevnz/tutorials/PythonDebugger//simple_diagram.py [-h] [--workers WORKERS] diagram
/home/josevnz/tutorials/PythonDebugger//simple_diagram.py: error: argument --workers: Not true: 1<= -3 <= 10
The program exited via sys.exit(). Exit status: 2
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(2)()
-> """

Están sucediendo muchas cosas aquí cuando usas n y c:

  1. La función se llama como se esperaba y se llama al punto de interrupción en la línea 39.
  2. Imprimiste los argumentos de la función de validación.
  3. calificaste (p) verificación de integridad y confirmación -3 No se pasó la verificación de rango, por lo que se lanzó una excepción.
  4. El programa salió con un error.

[ Learn how to modernize your IT with managed cloud services. ]

Sin un depurador, puede confirmar lo que verificó anteriormente:

pythondebugger) $ python3 simple_diagram.py --workers -3 my_airflow2.png
usage: /home/josevnz/tutorials/PythonDebugger//simple_diagram.py [-h] [--workers WORKERS] diagram
/home/josevnz/tutorials/PythonDebugger//simple_diagram.py: error: argument --workers: Not true: 1<= -3 <= 10

# A good call
(pythondebugger) [[email protected] ]$ python3 simple_diagram.py --workers 7 my_airflow2.png && echo "OK"
OK
(José Vicente Nunes, CC BY-SA 4.0)

Aprende a saltar antes de correr

Puede saltar a través del código saltando con el depurador. Tenga en cuenta que dependiendo de dónde salte, puede deshabilitar la ejecución de su código (pero esto es útil para comprender el flujo de trabajo de su programa).Por ejemplo, establezca un punto de interrupción en la línea 32, justo después de crear workers lista, entonces j (Salta) a la línea 36 e imprime el valor del trabajador:

(pythondebugger) $ python3 -m pdb simple_diagram.py --workers 2 my_airflow4.png
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(2)()
-> """
(Pdb) b simple_diagram.py:32
Breakpoint 1 at /home/josevnz/tutorials/PythonDebugger//simple_diagram.py:32
(Pdb) c
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(32)generate_diagram()
-> with Cluster("Celery workers"):
(Pdb) j 36
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(36)generate_diagram()
-> airflow - workers
(Pdb) workers
*** NameError: name 'workers' is not defined

¡Vaya! workers nunca inicializado. Regrese a la línea 32, establezca un punto de interrupción en la línea 36 y continúe para ver qué sucede:

(Pdb) j 32
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(32)generate_diagram()
-> with Cluster("Celery workers"):
(Pdb) b simple_diagram.py:36
Breakpoint 2 at /home/josevnz/tutorials/PythonDebugger//simple_diagram.py:36
(Pdb) c
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(36)generate_diagram()
-> airflow - workers
(Pdb) p workers
[, ]
(Pdb) 

¡Sí, viajar en el tiempo también es confuso!

probar código con interacción

Si observaste de cerca, es posible que hayas notado que las etiquetas en los trabajadores de Celery iban desde Worker 0 llegar Worker N-1. tener un Worker 0 No intuitivo. La solución es fácil, así que vea si puede "arreglar" el código:

(pythondebugger) $ python3 -m pdb simple_diagram.py --workers 2 my_airflow4.png
> /home/josevnz/tutorials/PythonDebugger//simple_diagram.py(2)()
-> """
(Pdb) b simple_diagram.py:35
Breakpoint 1 at /home/josevnz/tutorials/PythonDebugger//simple_diagram.py:35
(Pdb) c
> /home/josevnz/tutorials/PythonDebugger/simple_diagram.py(35)generate_diagram()
-> workers.append(Celery(f"Worker {i}"))
(Pdb) p i
0
(Pdb) p f"Worker {i}"
'Worker 0'
(Pdb) interact
*interactive*
>>> f"Worker {i}"
'Worker 0'
>>> f"Worker {i+1}"
'Worker 1'
>>> workers.append(Celery(f"Worker {i+1}"))
>>> workers
[]

Esto es lo que sucedió:

  1. Establezca un punto de interrupción en la línea 35, que es donde crea una instancia del objeto Celery.
  2. valor de impresión ives que es 0 y pasará workers_n - 1.
  3. Evaluar expresiones f"Worker {i}".
  4. Inicie una sesión interactiva. Esta sesión hereda todas las variables y el contexto hasta el momento del punto de interrupción.Esto significa que puede acceder i y workers lista.
  5. Pruebe nuevas expresiones agregando i+1 y confirme que la solución funciona.

Esto es más barato que reiniciar el programa con una solución. Además, imagine el valor de hacer esto si su función es mucho más compleja y obtiene datos de un recurso remoto, como una base de datos. ¡Oro puro!

Volviendo a la solución anterior, reemplace la línea 35 con:

            for i in range(workers_n):
                workers.append(Celery(f"Worker {i+1}"))
(José Vicente Nunes, CC BY-SA 4.0)

¿Qué aprendiste?

Hay muchas cosas que puede hacer con el depurador de Python. En este tutorial, usted:

  • Ejecute la aplicación en modo de depuración
  • Ejecutar el script paso a paso
  • Inspeccione el contenido de las variables y conozca los miembros de un módulo
  • Usar puntos de interrupción y saltos
  • Agregue un depurador a su aplicación
  • Revisión aplicada en el código en ejecución

Ahora sabe que no necesita un entorno de desarrollo integrado (IDE) para depurar y solucionar problemas de aplicaciones de Python, y pdb es una herramienta muy poderosa para tener en su conjunto de herramientas.

Publicaciones relacionadas

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Botón volver arriba