Narzędzia do statycznej analizy kodu źródłowego w PyCharm

Kilkukrotnie wspominałem już o IDE PyCharm i jego możliwościach. Oprócz kolorowania składni i obsługi kilku innych głupotek pozwala na przykład definiować narzędzia, które później możemy uruchamiać w trakcie pisania kodu.

Jako programiści Pythona możemy do tych narzędzi zaliczyć te dbające o jakość pisanego przez nas kodu. Oprócz standardowego PEP8 – który jest tylko czubkiem góry lodowej – osobiście chciałbym polecić także: flake8, pylint oraz radon.

flake8 jako linter sprawdza kod pod kątem zgodności ze standardami – dodatkowo rozszerza wspomniany przed chwilą PEP8 o kilka innych wartościowych usprawnień, np. takie związane z komentarzami, nazewnictwem zmiennych, itp.

Podobnie ma się sytuacja z pylint-em, jednak jego zaletą jest ocena naszego kodu z użyciem skali – każda zmiana może wpływać pozytywnie lub negatywnie zgodnie z następującym wzorem. Satysfakcją jest uzyskać ocenę 10.00/10.00 :). Oprócz oceny liczbowej otrzymujemy także zestawienie i najróżniejsze statystyki.

radon natomiast w całkiem przyjemny sposób prezentuje nam informacje na temat złożoności fragmentów naszego kodu – może nam się wtedy zapalić czerwona lampka, że pewne elementy powstałego kodu mogą potencjalnie stanowić wąskie gardło w aplikacji i są dobrym miejscem zaczepienia jeśli chodzi o refaktoryzację.

Zainstalujmy więc sobie narzędzia w czystym środowisku wirtualnym:


pushd ~/Envs
~/bin/Python-3.6.4/bin/python3 -m venv pytools
source pytools/bin/activate
pip install pylint
pip install radon

flake8 zostanie zainstalowany jako zależność podczas instalacji radon-a.

Klikamy „+” aby dodać nowe narzędzie. W otwartym okienku uzupełniamy jak poniżej:

Oczywiście dobrym pomysłem jest także grupowanie naszych narzędzi, np. ze względu na technologię oraz konfiguracja skrótów klawiaturowych:

Poniżej kilka zrzutów po uruchomieniu:

  • flake8:


~/Envs/pytools/bin/flake8 setup.py
setup.py:5:1: F401 're' imported but unused
setup.py:15:1: E302 expected 2 blank lines, found 1
setup.py:38:1: E305 expected 2 blank lines after class or function definition, found 1
setup.py:52:80: E501 line too long (134 > 79 characters)
setup.py:100:80: E501 line too long (116 > 79 characters)
Process finished with exit code 1

  • pylint


~/Envs/pytools/bin/pylint setup.py
Using config file ~/.pylintrc
************* Module setup
C: 52, 0: Line too long (134/100) (line-too-long)
C:100, 0: Line too long (116/100) (line-too-long)
W: 8, 0: Redefining built-in 'open' (redefined-builtin)
C: 1, 0: Missing module docstring (missing-docstring)
C: 13, 0: Constant name "here" doesn't conform to '(([AZ_][AZ09_]*)|(__.*__))$' pattern (invalid-name)
C: 15, 0: Missing class docstring (missing-docstring)
E: 32, 8: Unable to import 'pytest' (import-error)
W: 22,12: Attribute 'pytest_args' defined outside __init__ (attribute-defined-outside-init)
W: 24,12: Attribute 'pytest_args' defined outside __init__ (attribute-defined-outside-init)
W: 29, 8: Attribute 'test_suite' defined outside __init__ (attribute-defined-outside-init)
C: 43, 0: Constant name "packages" doesn't conform to '(([A-Z_][A-Z0-9_]*)|(__.*__))$' pattern (invalid-name)
C: 45, 0: Constant name "requires" doesn't conform to '(([AZ_][AZ09_]*)|(__.*__))$' pattern (invalid-name)
C: 52, 0: Constant name "test_requirements" doesn't conform to '(([A-Z_][A-Z0-9_]*)|(__.*__))$' pattern (invalid-name)
C: 54, 0: Constant name "about" doesn't conform to '(([AZ_][AZ09_]*)|(__.*__))$' pattern (invalid-name)
W: 56, 4: Use of exec (exec-used)
C: 59, 4: Constant name "readme" doesn't conform to '(([A-Z_][A-Z0-9_]*)|(__.*__))$' pattern (invalid-name)
C: 61, 4: Constant name "history" doesn't conform to '(([AZ_][AZ09_]*)|(__.*__))$' pattern (invalid-name)
W: 5, 0: Unused import re (unused-import)
Report
======
39 statements analysed.
Statistics by type
——————
+———+——-+———–+———–+————+———+
|type |number |old number |difference |%documented |%badname |
+=========+=======+===========+===========+============+=========+
|module |1 |NC |NC |0.00 |0.00 |
+———+——-+———–+———–+————+———+
|class |1 |NC |NC |0.00 |0.00 |
+———+——-+———–+———–+————+———+
|method |3 |NC |NC |100.00 |0.00 |
+———+——-+———–+———–+————+———+
|function |0 |NC |NC |0 |0 |
+———+——-+———–+———–+————+———+
Raw metrics
———–
+———-+——-+——+———+———–+
|type |number |% |previous |difference |
+==========+=======+======+=========+===========+
|code |63 |60.58 |NC |NC |
+———-+——-+——+———+———–+
|docstring |21 |20.19 |NC |NC |
+———-+——-+——+———+———–+
|comment |3 |2.88 |NC |NC |
+———-+——-+——+———+———–+
|empty |17 |16.35 |NC |NC |
+———-+——-+——+———+———–+
Duplication
———–
+————————-+——+———+———–+
| |now |previous |difference |
+=========================+======+=========+===========+
|nb duplicated lines |0 |NC |NC |
+————————-+——+———+———–+
|percent duplicated lines |0.000 |NC |NC |
+————————-+——+———+———–+
Messages by category
——————–
+———–+——-+———+———–+
|type |number |previous |difference |
+===========+=======+=========+===========+
|convention |11 |NC |NC |
+———–+——-+———+———–+
|refactor |0 |NC |NC |
+———–+——-+———+———–+
|warning |6 |NC |NC |
+———–+——-+———+———–+
|error |1 |NC |NC |
+———–+——-+———+———–+
Messages
——–
+——————————-+————+
|message id |occurrences |
+===============================+============+
|invalid-name |7 |
+——————————-+————+
|attribute-defined-outside-init |3 |
+——————————-+————+
|missing-docstring |2 |
+——————————-+————+
|line-too-long |2 |
+——————————-+————+
|unused-import |1 |
+——————————-+————+
|redefined-builtin |1 |
+——————————-+————+
|import-error |1 |
+——————————-+————+
|exec-used |1 |
+——————————-+————+
———————————–
Your code has been rated at 4.36/10
Process finished with exit code 22

  • radon


~/Envs/pytools/bin/radon cc setup.py
setup.py
M 18:4 PyTest.initialize_options – A
C 15:0 PyTest – A
M 26:4 PyTest.finalize_options – A
M 31:4 PyTest.run_tests – A
Process finished with exit code 0

view raw

radon.result.sh

hosted with ❤ by GitHub

Więcej doczytać można w następujących źródłach:

  1. About style guide of python and linter tool. pep8, pyflakes, flake8, haking, Pylint.
  2. What is Flake8 and why we should use it?

Sortowanie po wirtualnej kolumnie w Django

Nie tak dawno temu w ramach jednego z zadań w Django było uporządkowanie wyników wg innego klucza niż najprostsze ORDER BY. Chodziło o nadanie pewnego rodzaju wag dla wierszy, które możliwie najlepiej wpasowały się w poniższe kryteria:

  • pierwszeństwo miały wiersze, które zawierały dokładne dopasowanie szukanej frazy
  • następnie wiersze, które od szukanej frazy się zaczynały
  • kolejno wiersze, które na podaną frazę się kończyły
  • kończąc wierszami, które podaną frazę zawierały w dowolnym miejscu
  • opcjonalnie pozostałe wiersze, jeśli query nie było jedynym kryterium filtrowania.

Przydatny okazał się poniższy kawałek kodu tworzący nieistniejącą w modelu CaseWhen columnę exact w postaci nowych warunków w obiekcie QuerySet  – po której następnie sortował:


queryset = queryset.annotate(
exact=Case(
When(
name__iexact=query,
then=Value('1')
),
When(
name__istartswith=query,
then=Value('2')
),
When(
name__iendswith=query,
then=Value('3')
),
When(
name__icontains=query,
then=Value('4')
),
default=Value('5'),
output_field=CharField(),
)
).order_by('exact', 'name')

Dodatkowe porządkowanie wg kolumny name służy do alfabetycznego uporządkowania wierszy, którym została nadana ta sama waga.

Jeśli nie zostanie przekazana fraza do szukania (parametr q) używana jest fraza beer. Żeby mieć pogląd jakie rzeczywiście wyniki są zwracane – w migracji aplikacji casewhen dodaję kilka wierszy do bazy.

Rozwiązanie zostało wdrożone w ramach integracji z Django REST Framework, ale chodzi tylko o zasadę działania, więc kod można wykorzystać także bez DRF.

Oczywiście całość do przejrzenia bezpośrednio na GitHub.