Jeżeli miałbym wybrać jedno narzędzie, które potężnie zmieniło moje podejście do tworzenia kodu w pythonie, to byłłby to debugger. PDB, bo o nim mowa, to wbudowany w Pythona debugger.
Do tej pory stosowałem go głównie w obrębie VSCode, którego wsparcie dla takich narzędzi jest wprost genialne. Ale pomyślałem sobie, że fajnie byłoby go poznać od strony bardziej przyziemnej, korzystając z najlepszego interfejsu dla komputera czyli oczywiście terminala :P
Osobiście korzystam właśnie z Neovima, i na jego podstawie będę się tutaj opierał.
Ale najpierw - jak zacząć?
python -m pdb <nazwa_pliku>.py
Po uruchomieniu znajdziemy się w środowisku interaktywnym. Warto w tym momencie poznać kilka podstawowych poleceń:
| Polecenie | Skrót | Opis |
|---|---|---|
| help | h | Wyświetla listę dostępnych poleceń lub szczegółową pomoc dla konkretnego polecenia. |
| list | l | Wyświetla fragment kodu źródłowego wokół aktualnej linii. |
| next | n | Wykonuje następną linię kodu, wchodząc do funkcji. |
| p | Wyświetla wartość wyrażenia. | |
| pretty print | pp | Wyświetla wartość wyrażenia w bardziej czytelny sposób. |
| restart | r | Restartuje program od początku. |
| quit | q | Kończy działanie debuggera i programu. |
Na początek to powinno wystarczyć.
Teraz przydałby się jakiś skrypt do przetestowania, prawda? Dla przykładu, mam skrypt służący do generowania tokenów JWT. Znajdziesz go na Github’ie
Na tym kodzie będę teraz pracował.
Najpierw, przygotumy wszytko:
python3 -m venv venv
source venv/bin/activate
pip install pyjwt
wget <adres_gista> -o jwt_generator.py
I uruchamiamy debugger:
python -m pdb jwt_generator.py --username andrzej --user-level admin --valid-for 15
Na wstępie zobaczysz coś takiego:
> /home/Andrzej/projekty/blog_examples/python_debugger/jwt_generator.py(1)<module>()
-> import argparse
Program zaczyna się od importowania modułu argparse.
Okej, rozejrzyjmy się trochę po tym kodzie. Najpierw ’list':
(Pdb) l
1 -> import argparse
2 import jwt
3 from datetime import datetime, timedelta, timezone
4 from pathlib import Path
5 import sys
6
7 MAX_MINUTES = 30
8 DEFAULT_SECRET_FILE = "secret.txt"
9
10 def load_secret(path: str) -> str:
11 try:
Wszystko się zgadza, program zatrzymał się na pierwszej linii.
Wpisując kilka razy polecenie n/next, dotrzemy w końcu do funkcji main():
I po kilku razach pokaze się błąd: brakuje pliku z sekretem.
(Pdb) n
> /home/Andrzej/projekty/blog_examples/blog_examples/python_debugger/jwt_generator.py(73)<module>()
-> main()
(Pdb) n
Error reading secret file: [Errno 2] No such file or directory: 'secret.txt'
SystemExit: 1
Zwróć uwagę, że mamy tutaj SystemExit, ale program jeszcze się nie zakończył.
Po kolejnych kilku powtórzeniach next:
> /mnt/c/Users/Andrzej/Documents/projekty/blog_examples/python_debugger/jwt_generator.py(73)<module>()
-> main()
(Pdb) n
--Return--
> /mnt/c/Users/Andrzej/Documents/projekty/blog_examples/python_debugger/jwt_generator.py(73)<module>()->None
-> main()
(Pdb) n
SystemExit: 1
> <string>(1)<module>()->None
(Pdb) n
--Return--
> <string>(1)<module>()->None
(Pdb) n
The program exited via sys.exit(). Exit status: 1
> /mnt/c/Users/Andrzej/Documents/projekty/blog_examples/python_debugger/jwt_generator.py(1)<module>()
-> import argparse
I w taki sposób zaczynamy od początku. Dla ułatwienia dodam, że jednokrotne użycie
c/continue wywołałoby taki sam efekt. Program się wysypie.
Nie chcemy, żeby program się sypał, więc dodajmy plik secret.txt:
echo "adf8e10h515nbu91b34j51p0j4nb51bn34jn96n01n6nn460186b1b04h614n61pn0" > secret.txt
Kiedy teraz wrócimy do debuggera:
> /mnt/c/Users/Andrzej/Documents/projekty/blog_examples/python_debugger/jwt_generator.py(1)<module>()
-> import argparse
(Pdb) c
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmRyemVqIiwibHZsIjoiYWRtaW4iLCJpYXQiOjE3NzYyNjE0NDUsImV4cCI6MTc3NjI2MjM0NX0.YQsWyKS1ZIcEAQU6MEWkJCIHK9IvmcV7lYvbt75Gb6A
The program finished and will be restarted
I mamy przykładowy wygnerowany token.
To teraz coś ciekawszego - spróbujmy zobaczyć co się dzieje po drodze.
Przyda nam się kilka kolejnych poleceń:
| Polecenie | Skrót | Opis |
|---|---|---|
| breakpoint | b | ustawia breakpoint |
| clear | cl | usuwa wszystkie breakpointy |
| continue | c | kontynuuje wykonywania programu do kolejnego breakpointu lub końca programu |
Za pomocą polecenia ‘b 12’ ustawiamy breakpoint na linii 12, czyli w momencie odczytu sekretu z pliku.
Używając teraz polecenia c, program dociera do tej właśnie linii
-> return Path(path).read_text().strip()
(Pdb) list
7 MAX_MINUTES = 30
8 DEFAULT_SECRET_FILE = "secret.txt"
9
10 def load_secret(path: str) -> str:
11 try:
12 B-> return Path(path).read_text().strip()
13 except Exception as e:
14 print(f"Error reading secret file: {e}", file=sys.stderr)
15 sys.exit(1)
16
17 def parse_args():
Podejrzyjmy sobie jak wygląda odczyt pliku.
Pamiętaj, że wpisując dane wyrażenie, wykonujesz je:
(Pdb) Path(path)
PosixPath('secret.txt')
(Pdb) Path(path).read_text()
'adf8e10h515nbu91b34j51p0j4nb51bn34jn96n01n6nn460186b1b04h614n61pn0\n'
(Pdb) Path(path).read_text().strip()
'adf8e10h515nbu91b34j51p0j4nb51bn34jn96n01n6nn460186b1b04h614n61pn0'
Wartość sekretu poprawna.
Ale załóżmy, że nie wiem czym jest PosixPath, więc zajrzyjmy do środka tego obiektu:
(Pdb) Path(path).__dir__
<built-in method __dir__ of PosixPath object at 0x7b6990aabb80>
(Pdb) Path(path).__dir__()
['__module__', '__doc__', '__slots__', '_accessor', '__new__', '_make_child_relpath',
'__enter__', <...> '__sizeof__', '__dir__', '__class__', '_flavour']
Powyższą listę metod oczywiście skróciłem dla czytelności.
Okej, wiemy już, że PosixPath ma całkiem sporo metod.
Warto w tym miejscu przypomnieć, że widoczne tutaj metody z dwoma podkreślnikami, tzw. magiczne metody (lub dunder; “double under” - podwójny podkreślnik) których odpowiednie zdefiniowanie pozwala na zmodyfikowania zachowania obiektów, albo też tzw. przeciążenie operatorów.
Przykładem jest widoczny wyżej __doc__ który zawiera docstring - czyli krótki
opis danej funkcjo pozostawiony prze jej twórcę.
Skoro przy okazji przypomnieliśmy sobie o magicznych metodach, to już wiem, że chciałbym podejrzeć docstring:
(Pdb) pp Path(path).__doc__
('Path subclass for non-Windows systems.\n'
'\n'
' On a POSIX system, instantiating a Path should return this object.\n'
' ')
I znowu wszystko się tutaj zgadza.
Bardziej zaawansowane możliwości
Interact
Polecenie interact w PDB pozwala na urchomienie interkatywnej sesji interpretera Pythona zawierającą aktualny zestaw zmiennych programu. Przydatne przy próbach eksperymentowania z kodem na bieżąco i rozwiązania mało oczywistych błędów.
Pamiętaj tylko, że zestaw aktualnie działających zmiennych zależy od miejsca uruchomienia trybu interaktywnego.
Np. uruchomienie interact na breakpointcie w linii 12, tak jak opisałem
wcześniej pozwala na podejrzenie paramteru path z funckji load_secret, ale
z tego miejsca nie zobaczymy np. zmiennej args zdefiniowanej na początku main()
chociaż load_secret jest wywoływane póżniej.
-> import argparse
(Pdb) b12
*** NameError: name 'b12' is not defined
(Pdb) b 12
Breakpoint 1 at /home/andrzej/projekty/blog_examples/python_debugger/jwt_generator.py:12
(Pdb) c
> /home/andrzej/projekty/blog_examples/python_debugger/jwt_generator.py(12)load_secret()
-> return Path(path).read_text().strip()
(Pdb) interact
*interactive*
>>> path
'secret.txt'
>>> args
Traceback (most recent call last):
File "<console>", line 1, in <module>
NameError: name 'args' is not defined
>>>
Z trybu interaktywnego wychodzimy za pomocą exit() lub Ctrl+D.
Alias
Definiowanie aliasów potrafi przyspieszyć pracę, zwłaszcza gdy często używamy tych samych poleceń.
Warto dokładnie zapoznać się z dokumentacją, bo niestety ale sposób działania tego polecenia jest mocno nieoczywisty (prznajmniej dla mnie).
Za pomocą %1, %2 odwołujemy się do parametrów przekazywanych do aliasu.
Na zakończenie
Muszę przyznać, że debugowanie kodu z poziomu terminala jest zaskakująco przyjemne. Żadnych dodatków do edytora, konfiguracji, po prostu działa.
Cudo :)
Oczywiście fakt, że trzeba się przełączać między debuggerem a edytorem co chwilę jest uciążliwy, ale uważam, że jednak warto znać narzędzia których się używa, zamiast tylko bezmyślnei klikać ikonki w GUI (VSCode, patrzę na ciebie :P).
Na zakończenie, w jednym w kolejnych postów chcę pokazać krok po kroku integrację PDB z Neovimem, co realnie zmniejszy ilość powodów do przełączania się na VSCode :)
Miłego!