Examples in README
Kontekst
Mieliśmy dyskusję na temat przykładów użycia obrazu kontenera. Wstępny wniosek był mniej więcej taki, że są poprawne, ale mogą być mylące.
Cele, intencje i marzenia
Nie chciałem się tu odnosić tylko do samego README, ale szerzej do zagadnienia testowania kodu w Pythonie. Wydaje mi się, że jeśli ustalimy tu coś konkretnego, to można będzie to umieścić gdzieś poza tym projektem, np. włączyć do ogólnych zasad pisania kodu.
Przypadki użycia
Mamy trzy narzędzia do testowania kodu w Pythonie: Flake8, Pylint, mypy.
Sposób i zakres użycia tych narzędzi może się różnić między projektami (np. jakiś projekt nie używa typowania), ale w wersji "maksymalnej" testowanie kodu obejmuje trzy przypadki użycia:
-
Sprawdzenie kodu w CI uruchamiane automatycznie dla każdego commitu.
-
Sprawdzenie kodu lokalnie bez wykorzystania kontenera.
-
Sprawdzenie kodu lokalnie przy pomocy kontenera.
Analiza problemu
Stworzyłem projekt przykładowy z bardzo błędnym pythonowym kodem: https://projects.task.gda.pl/tziol/kosmogonia-wartburgow
Struktura projektu:
├── .flake8
├── .gitlab-ci.yml
├── .pylintrc
├── README.md
├── mypy.ini
├── scripts
│ ├── test-style-in-container.sh
│ └── test-style.sh
└── src
├── package
│ ├── module1.py
│ ├── module2.py
│ └── subpackage
│ └── module3.py
└── script.py
Uwagi ogólne:
- projekt zawiera przykłady sprawdzania kodu spełniające wszystkie przypadki użycia zdefiniowane powyżej,
- do skryptu
test-style.sh
można by było dodać warunki pozwalające wybrać, który test ma być uruchomiony - to by było przydatne, np. do podzielenia testów w CI, ale to prosta i niezależna od tej dyskusji zmiana, nie chciało już mi się tego wprowadzać, - czasem używam skryptów pythonowych bez rozszerzenia
.py
(dla skryptów typu entrypoint, czy jak je tam zwać), ale tu takich nie wrzucałem, bo część narzędzi rozpoznaje pliki źródłowe po rozszerzeniu.
Flake8
Tu jest najprościej, bo wystarczy wywołać:
flake8
I dostajemy piękny wynik:
./src/script.py:3:1: F401 'datetime' imported but unused
./src/script.py:8:1: E302 expected 2 blank lines, found 1
./src/script.py:9:37: E202 whitespace before ')'
./src/package/module1.py:3:1: E302 expected 2 blank lines, found 0
./src/package/module1.py:7:1: W391 blank line at end of file
./src/package/module2.py:1:1: F401 'os' imported but unused
./src/package/module2.py:2:14: E225 missing whitespace around operator
./src/package/module2.py:4:1: W391 blank line at end of file
./src/package/subpackage/module3.py:2:1: W293 blank line contains whitespace
./src/package/subpackage/module3.py:3:1: W293 blank line contains whitespace
./src/package/subpackage/module3.py:4:5: E303 too many blank lines (2)
Pylint
Tu też jest dość prosto, bo wystarczy podać katalog, który ma sprawdzić:
pylint "src"
Wynik:
************* Module src.script
src/script.py:3:0: W0611: Unused import datetime (unused-import)
************* Module src.package.module1
src/package/module1.py:7:0: C0305: Trailing newlines (trailing-newlines)
src/package/module1.py:3:12: W0613: Unused argument 'paramytr' (unused-argument)
************* Module src.package.module2
src/package/module2.py:4:0: C0305: Trailing newlines (trailing-newlines)
src/package/module2.py:1:0: W0611: Unused import os (unused-import)
************* Module src.package.subpackage.module3
src/package/subpackage/module3.py:2:0: C0303: Trailing whitespace (trailing-whitespace)
src/package/subpackage/module3.py:3:0: C0303: Trailing whitespace (trailing-whitespace)
------------------------------------------------------------------
Your code has been rated at 5.62/10 (previous run: 5.00/10, +0.62)
mypy
Łooopanie.
Trzeba pamiętać o tym, że mypy nie testuje plików indywidualnie, a zbiorczo, więc jeśli wywołamy jedno z poniższych superpoleceń:
mypy "src"
mypy "src/"*
To dostaniemy ładne może-nie-tym-razem:
src/package/module2.py: error: Source file found twice under different module names: 'module2' and 'package.module2'
Found 1 error in 1 file (errors prevented further checking)
Nie znalazłem na to jednego, pięknego, uniwersalnego rozwiązania - pozostawiam to Czytelnikowi lub Czytelniczce.
Poniżej moje propozycje.
Rozwiązanie α: kulturalne
Definiujemy w mypy.ini
katalog ze źródłami (można też z linii komend lub w zmiennej środowiskowej) i włączamy wsparcie dla PEP 420:
[mypy]
mypy_path = src
namespace_packages = True
Przy wywołaniu podajemy wszystkie pakiety/moduły do sprawdzenia:
mypy --package "package" --package "script"
Dostajemy ładny wynik:
src/package/subpackage/module3.py:4: error: No return value expected
src/package/module2.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")
src/package/module1.py:4: error: "funktion" does not return a value
src/package/module1.py:5: error: Incompatible return value type (got "str", expected "int")
src/script.py:10: error: Incompatible return value type (got "str", expected "int")
Found 5 errors in 4 files (checked 6 source files)
Rozwiązanie β: nowoczesne, ale podchwytliwe
Za radą Wyroczni, ustawiamy mypy.ini
:
[mypy]
namespace_packages = True
Przy wywołaniu podajemy tylko główny katalog:
mypy --package "src"
Dostajemy wynik:
src/package/subpackage/module3.py:4: error: No return value expected
src/package/module2.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")
src/package/module1.py:5: error: Incompatible return value type (got "str", expected "int")
src/script.py:10: error: Incompatible return value type (got "str", expected "int")
Found 4 errors in 4 files (checked 7 source files)
Jak widać, w takiej konfiguracji mypy:
- znalazł o jeden błąd mniej (
src/package/module1.py:4: error: "funktion" does not return a value
), - sprawdził o jeden plik źródłowy więcej.
Dlaczego? Nie wiem.
Rozwiązanie γ: antyczne, ale działa
Nie używamy PEP 420 - dodajemy do pakietów pliki __init__.py
.
mypy.ini
:
[mypy]
Wywołanie:
mypy "src"
Wynik:
src/package/module2.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")
src/package/subpackage/module3.py:4: error: No return value expected
src/package/module1.py:4: error: "funktion" does not return a value
src/package/module1.py:5: error: Incompatible return value type (got "str", expected "int")
src/script.py:10: error: Incompatible return value type (got "str", expected "int")
Found 5 errors in 4 files (checked 6 source files)
Przykłady w README
Po tym krótkim wprowadzeniu mogę wyartykułować moje wątpliwości co do przykładów w README:
-
wyglądają na gotowe do skopiowania i użycia w projekcie, a to niekoniecznie jest takie proste - może jakieś ostrzeżenie?,
-
używają sporo globów (
*.py
), co, jak widać, niekoniecznie musi się pokrywać ze "standardowym" sposobem użycia, -
w szczególności, glob przy wywołaniu kontenera (
docker run ... pylint *.py
) zostanie rozwiązany po stronie hosta, a nie kontenera, co w tym przykładzie zadziała, ale może być mylące/niejasne, -
drobnostka:
pip3 install --requirement constraints.txt
jest oczywiście poprawne, ale nie jest nijak standardowe, więc myślę, że w przykładzie jednakpip3 install --requirement requirements.txt
.