Как улучшить качество кода на Python

В этой статье мы рассмотрим, как улучшить качество вашего кода на Python с помощью линтеров, инструментов форматирования и сканеров уязвимостей безопасности.

Под качеством кода обычно понимается то, насколько функциональным и удобным для сопровождения является ваш код. Код считается качественным, если:

  1. Он служит своей цели
  2. Его поведение можно легко проверить с помощью автоматизированных тестов
  3. Его стиль последователен
  4. Он понятен
  5. Он не содержит уязвимостей безопасности
  6. Его легко поддерживать
  7. Он хорошо документирован

В этой статье мы сосредоточимся на пунктах с третьего по седьмой.

Содержание

Линтеры

Линтеры (англ. linter) выявляют ошибки программирования, баги, стилистические ошибки и подозрительные конструкции путем анализа исходного кода.

Инструменты линтинга просты в настройке. Они улучшают опыт разработчиков, устраняя трения между коллегами, имеющими разные мнения о стиле.

Хотя линтинг является общепринятой практикой, многие разработчики все еще не одобряют его в силу своего упрямства.

Давайте рассмотрим небольшой пример.

Первая версия:

numbers = []

while True:
    answer = input('Enter a number: ')
    if answer != 'quit':
        numbers.append(answer)
    else:
        break

print('Numbers: %s' % numbers)

Вторая версия:

numbers = []

while (answer := input("Enter a number: ")) != "quit":
    numbers.append(answer)

print(f"Numbers: {numbers}")

Третья версия:

numbers = []

while True:
    answer = input("Enter a number: ")
    if answer == "quit":
        break
    numbers.append(answer)

print(f"Numbers: {numbers}")

Какая из них лучше? С точки зрения функциональности они одинаковы.

Какая из них вам больше нравится? Какую из них предпочтут соавторы вашего проекта?

Как разработчик программного обеспечения вы, скорее всего, работаете в команде. А в команде очень важно, чтобы все разработчики придерживались одних и тех же стандартов написания кода. В противном случае читать чужой код будет гораздо сложнее. При проверке кода основное внимание должно уделяться проблемам более высокого уровня, а не обыденным вопросам синтаксического форматирования.

Возьмем к примеру обычный текст. Если вы как автор будете заканчивать каждое предложение восклицательным знаком, читателю будет очень сложно уловить тон. Если вы к тому же будете игнорировать общепринятые стандарты, касающиеся заглавных букв и интервалов между словами, ваши предложения будет очень трудно читать. Такое чтение потребует гораздо больше мозговых усилий. Вы потеряете читателей и соавторов.

С кодом дело обстоит примерно так же. Мы используем руководства по стилю, чтобы нашим коллегам-разработчикам (да и нам самим) было проще понять замысел и сотрудничать с нами.

Нам, как разработчикам Python, повезло, что в нашем распоряжении есть руководство по стилю PEP-8, которое содержит набор соглашений, рекомендаций и лучших практик для облегчения чтения и сопровождения нашего кода. В нем особое внимание уделяется соглашениям об именовании, комментариям к коду и вопросам оформления (например, отступам и пробельным символам). Несколько примеров:

  • Максимальная длина строки – 79 символов
  • Использование пробелов вместо табуляции
  • Названия функций пишутся в нижнем регистре

Ограничение в 79 символов немного устарело с учетом современных экранов. Вы можете смело игнорировать это правило и использовать более высокое ограничение, например 120 символов. Обычно это делает код более читабельным.

Что касается инструментов для линтинга, то, хотя их существует множество, в основном все они ищут ошибки либо в логике кода, либо в соблюдении стандартов кода:

  1. Логика кода. Линтеры проверяют ошибки программирования, обеспечивают соблюдение стандартов, ищут запахи кода и проверяют сложность кода. Pyflakes и McCabe (проверка сложности) – самые популярные инструменты для линтинга логики кода.
  2. Стиль кода. Линтеры просто обеспечивают соблюдение стандартов кода (собранных в PEP-8). pycodestyle относится к этой категории.

Flake8

Flake8 – это обертка вокруг Pyflakes, pycodestyle и McCabe. Установка такая же, как и у любого другого пакета PyPI:

$ pip install flake8

Допустим, у вас есть следующий код, сохраненный в файле my_module.py:

from requests import *

def get_error_message(error_type):
    if error_type == 404:
        return 'red'
    elif error_type == 403:
        return 'orange'
    elif error_type == 401:
        return 'yellow'
    else:
        return 'blue'


def main():
    res = get('https://api.github.com/events')
    STATUS = res.status_code
    if res.ok:
        print(f'{STATUS}')
    else:
        print(get_error_message(STATUS))




if __name__ == '__main__':
    main()

Чтобы обработать линтером этот файл, вы можете просто выполнить команду:

$ python -m flake8 my_module.py

Это должно привести к следующему результату:

my_module.py:1:1: F403 'from requests import *' used; unable to detect undefined names
my_module.py:3:1: E302 expected 2 blank lines, found 1
my_module.py:15:11: F405 'get' may be undefined, or defined from star imports: requests
my_module.py:25:1: E303 too many blank lines (4)

Вы также можете увидеть ошибку my_module.py:26:11: W292 no newline at end of file (зависит от конфигурации вашего редактора кода).

Для каждого нарушения выводится строка, содержащая следующие данные:

  1. Путь к файлу (относительно директории, из которой запускался Flake8)
  2. Номер строки
  3. Номер столбца
  4. ID нарушенного правила
  5. Описание правила

Нарушения, начинающиеся с F, являются ошибками Pyflakes, в то время как нарушения, начинающиеся с E, являются ошибками pycodestyle.

Исправив нарушения, вы должны получить:

from requests import get


def get_error_message(error_type):
    if error_type == 404:
        return 'red'
    elif error_type == 403:
        return 'orange'
    elif error_type == 401:
        return 'yellow'
    else:
        return 'blue'


def main():
    res = get('https://api.github.com/events')
    STATUS = res.status_code
    if res.ok:
        print(f'{STATUS}')
    else:
        print(get_error_message(STATUS))


if __name__ == '__main__':
    main()

Наряду с PyFlakes и pycodestyle, вы можете использовать Flake8 для проверки цикломатической сложности.

Например, сложность функции get_error_message равна четырем, так как существует четыре возможных ветви (или пути кода):

def get_error_message(error_type):
    if error_type == 404:
        return 'red'
    elif error_type == 403:
        return 'orange'
    elif error_type == 401:
        return 'yellow'
    else:
        return 'blue'

Чтобы ограничить максимальную сложность значением 3 или ниже, выполните команду:

$ python -m flake8 --max-complexity 3 my_module.py

Flake8 должен выдать ошибку:

my_module.py:4:1: C901 'get_error_message' is too complex (4)

Рефакторинг кода выглядит следующим образом:

def get_error_message(error_type):
    colors = {
        404: 'red',
        403: 'orange',
        401: 'yellow',
    }
    return colors[error_type] if error_type in colors else 'blue'

Теперь Flake8 должен пройти:

$ python -m flake8 --max-complexity 3 my_module.py

Вы можете добавить дополнительные проверки во Flake8 с помощью системы плагинов. Например, чтобы обеспечить соблюдение соглашений об именовании PEP-8, установите pep8-naming:

$ pip install pep8-naming

Запустите:

$ python -m flake8 my_module.py

Вы должны увидеть следующее:

my_module.py:15:6: N806 variable 'STATUS' in function should be lowercase

Исправление:

def main():
    res = get('https://api.github.com/events')
    status = res.status_code
    if res.ok:
        print(f'{status}')
    else:
        print(get_error_message(status))

Список самых популярных расширений можно найти в репозитории Awesome Flake8 Extensions.

Pylama – популярный инструмент для линтинга, который, как и Flake8, склеивает несколько линтеров.

Инструменты форматирования кода

В то время как линтеры просто проверяют ваш код на наличие проблем, инструменты форматирования кода (англ. formatter) фактически переформатируют ваш код на основе набора стандартов.

Приведение кода в надлежащий формат – это необходимая, но скучная работа, которую должен выполнять компьютер.

Почему это необходимо?

Хорошо отформатированный код, отвечающий требованиям руководства по стилю, легче читать, а это облегчает поиск ошибок и привлечение новых разработчиков. Это также уменьшает количество конфликтов при слиянии.

Читабельность имеет значение.

The Zen of Python

Опять же, это скучная работа, по поводу которой разработчики часто расходятся во мнениях (табы против пробелов, одинарные кавычки против двойных и т. д.). Поэтому имеет смысл использовать инструмент для форматирования кода, чтобы автоматически переформатировать его, основываясь на определенном наборе стандартов.

Совет: проводя код-ревью, избегайте комментариев о форматировании кода. Для этого существуют автоматические форматировщики и линтеры.

isort

isort используется для автоматического разделения импортов в вашем коде на следующие группы:

  • стандартная библиотека
  • сторонние
  • локальные

В рамках групп импорты располагаются в алфавитном порядке.

# standard library
import datetime
import os

# third-party
import requests
from flask import Flask
from flask.cli import AppGroup

# local
from your_module import some_method

Установка:

$ pip install isort

Если вы предпочитаете плагин Flake8, обратите внимание на flake8-isort. flake8-import-order тоже довольно популярен.

Чтобы запустить isort для работы с файлами в текущем каталоге и подкаталогах:

$ python -m isort .

Чтобы запустить его для одного файла:

$ python -m isort my_module.py

До:

import os
import datetime
from your_module import some_method
from flask.cli import AppGroup
import requests
from flask import Flask

После:

import datetime
import os

import requests
from flask import Flask
from flask.cli import AppGroup

from your_module import some_method

Чтобы, не внося изменений, проверить, правильно ли отсортированы и упорядочены импортируемые файлы, используйте флаг --check-only:

$ python -m isort my_module.py --check-only

ERROR: my_module.py Imports are incorrectly sorted and/or formatted.

Совет: используйте --check-only внутри конвейеров CI/CD.

Чтобы увидеть изменения, не применяя их, используйте флаг --diff:

$ python -m isort my_module.py --diff

--- my_module.py:before      2022-02-28 22:04:45.977272
+++ my_module.py:after       2022-02-28 22:04:48.254686
@@ -1,6 +1,7 @@
+import datetime
 import os
-import datetime
+
+import requests
+from flask import Flask
+from flask.cli import AppGroup
 from your_module import some_method
-from flask.cli import AppGroup
-import requests
-from flask import Flask

При использовании isort с Black следует использовать опцию --profile black, чтобы избежать конфликтов стилей кода:

$ python -m isort --profile black .

Black

Black – это форматировщик кода на Python, который используется для переформатирования вашего кода на основе руководства по стилю кода Black, которое довольно близко к PEP-8.

$ pip install black
Предпочитаете плагин Flake8? Обратите внимание на flake8-black.

Рекурсивное редактирование файлов внутри текущей директории:

$ python -m black .

Black также можно запустить для работы с одним файлом:

$ python -m black my_module.py

До:

import pytest

@pytest.fixture(scope="module")
def authenticated_client(app):
    client = app.test_client()
    client.post("/login", data=dict(email="dummy@email.ai", password="notreal"), follow_redirects=True)
    return client

После:

import pytest


@pytest.fixture(scope="module")
def authenticated_client(app):
    client = app.test_client()
    client.post(
        "/login",
        data=dict(email="dummy@email.ai", password="notreal"),
        follow_redirects=True,
    )
    return client

Если вы просто хотите проверить, соответствует ли ваш код стандартам стиля Black, вы можете использовать флаг --check:

$ python -m black my_module.py --check

would reformat my_module.py
Oh no! 💥 💔 💥
1 file would be reformatted.

Совет: используйте --check в конвейерах CI/CD.

Флаг --diff показывает разницу между вашим текущим кодом и переформатированным кодом:

$ python -m black my_module.py --diff

--- my_module.py        2022-02-28 22:04:45.977272 +0000
+++ my_module.py        2022-02-28 22:05:15.124565 +0000
@@ -1,7 +1,12 @@
 import pytest
+

 @pytest.fixture(scope="module")
 def authenticated_client(app):
     client = app.test_client()
-    client.post("/login", data=dict(email="dummy@email.ai", password="notreal"), follow_redirects=True)
-    return client
\ No newline at end of file
+    client.post(
+        "/login",
+        data=dict(email="dummy@email.ai", password="notreal"),
+        follow_redirects=True,
+    )
+    return client
would reformat my_module.py

All done! ✨ 🍰 ✨
1 file would be reformatted.
YAPF и autopep8 - инструменты форматирования кода, похожие на Black. На них тоже стоит обратить внимание.

Ruff

Стоит упомянуть еще один инструмент под названием Ruff, который набирает обороты в сообществе Python (например, FastAPI начал использовать Ruff в качестве линтера для своего проекта).

Это “чрезвычайно быстрый линтер и форматировщик кода на Python, написанный на языке Rust”. Выполняет как линтинг, так и форматирование. Поддерживает различные наборы правил для линтинга и форматирования:

  • Pyflakes
  • pycodestyle
  • McCabe
  • pep8-naming
  • isort
  • pydocstyle

Полный список поддерживаемых правил можно посмотреть здесь.

Вы можете использовать его для замены как Flake8, так и isort. Очень скоро вы сможете и Black заменить на Ruff. Автоформатирование находится в бета-версии. Подробнее см. в этом GitHub issue.

Как и другие инструменты, вы можете установить его как пакет Python:

$ pip install ruff

Ruff можно запускать для работы с файлами в текущем каталоге и подкаталогах:

$ python -m ruff . --select F,W,E

Его также можно запустить для одного файла:

$ python -m ruff my_module.py --select F,W,E

Чтобы исправить порядок импорта, можно запустить ruff с флагом --fix:

$ python -m ruff --fix . --select F,W,E

Сканеры уязвимостей в безопасности

Уязвимости безопасности – это, пожалуй, самый важный аспект качества кода, и все же их часто игнорируют. Ваш код безопасен лишь настолько, насколько надежно его самое слабое звено. К счастью, существует ряд инструментов, которые могут помочь обнаружить возможные уязвимости в нашем коде. Давайте рассмотрим парочку.

Bandit

Bandit – это инструмент, предназначенный для поиска распространенных проблем безопасности в коде Python, таких как жестко закодированные строки паролей, десериализация недоверенного кода, использование pass в блоках except и т. д.

$ pip install bandit
Предпочитаете плагин для Flake8? Обратите внимание на flake8-bandit.

Запуск выглядит так:

$ bandit my_module.py

Код:

evaluate = 'print("Hi!")'
eval(evaluate)


evaluate = 'open("secret_file.txt").read()'
eval(evaluate)

Вы должны увидеть следующее предупреждение:

>> Issue: [B307:blacklist] Use of possibly insecure function - consider using safer
    ast.literal_eval.
   Severity: Medium   Confidence: High
   Location: my_module.py:2
   More Info:
    https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b307-eval
1   evaluate = 'print("Hi!")'
2   eval(evaluate)
3

--------------------------------------------------
>> Issue: [B307:blacklist] Use of possibly insecure function - consider using safer
    ast.literal_eval.
   Severity: Medium   Confidence: High
   Location: my_module.py:6
   More Info:
    https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b307-eval
5   evaluate = 'open("secret_file.txt").read()'
6   eval(evaluate)

--------------------------------------------------

Safety

Safety – еще один инструмент, который пригодится вам для защиты вашего кода от проблем с безопасностью.

Он используется для проверки установленных зависимостей на наличие известных уязвимостей безопасности. Список известных уязвимостей безопасности в пакетах Python хранится в базе данных Safety DB.

$ pip install safety

Активировав виртуальное окружение, запустите Safity следующим образом:

$ safety check

Пример вывода при установке Flask v0.12.2:

+==============================================================================+
|                                                                              |
|                               /$$$$$$            /$$                         |
|                              /$$__  $$          | $$                         |
|           /$$$$$$$  /$$$$$$ | $$  \__//$$$$$$  /$$$$$$   /$$   /$$           |
|          /$$_____/ |____  $$| $$$$   /$$__  $$|_  $$_/  | $$  | $$           |
|         |  $$$$$$   /$$$$$$$| $$_/  | $$$$$$$$  | $$    | $$  | $$           |
|          \____  $$ /$$__  $$| $$    | $$_____/  | $$ /$$| $$  | $$           |
|          /$$$$$$$/|  $$$$$$$| $$    |  $$$$$$$  |  $$$$/|  $$$$$$$           |
|         |_______/  \_______/|__/     \_______/   \___/   \____  $$           |
|                                                          /$$  | $$           |
|                                                         |  $$$$$$/           |
|  by pyup.io                                              \______/            |
|                                                                              |
+==============================================================================+
| REPORT                                                                       |
| checked 37 packages, using default DB                                        |
+============================+===========+==========================+==========+
| package                    | installed | affected                 | ID       |
+============================+===========+==========================+==========+
| flask                      | 0.12.2    | <0.12.3                  | 36388    |
| flask                      | 0.12.2    | <1.0                     | 38654    |
+==============================================================================+

Запуск инструментов качества кода

Теперь, когда вы познакомились с инструментами, давайте разберем, когда их следует использовать.

Как правило, инструменты запускаются:

  1. При написании кода (в вашей IDE или редакторе кода)
  2. Во время коммита (с помощью pre-commit хуков)
  3. Когда код проверяется в системе контроля исходного кода (через конвейер CI).

Запуск инструментов внутри IDE или редактора кода

Поиск возможных проблем лучше начинать как можно раньше и проводить как можно чаще. Поэтому настоятельно рекомендуется выполнять линтинг и форматирование кода во время разработки. Многие популярные IDE имеют встроенные линтеры и форматеры. Для большинства вышеупомянутых инструментов вы сможете найти соответствующий плагин для вашего редактора кода. Такие плагины в режиме реального времени предупреждают вас о нарушениях стиля кода и потенциальных ошибках программирования.

Ресурсы:

Pre-commit хуки

Поскольку в процессе работы над кодом вы неизбежно пропустите то или иное предупреждение, хорошей практикой является проверка качества кода во время коммита с помощью pre-commit git-хуков. Прежде чем прогонять код через линтер. можно его сперва отформатировать. Таким образом вы сможете избежать коммита кода, который не пройдет проверку качества в вашем CI-конвейере.

Для управления git-хуками рекомендуется использовать фреймворк pre-commit.

$ pip install pre-commit

После установки добавьте в проект файл конфигурации pre-commit под названием .pre-commit-config.yaml. Чтобы запустить Flake8, добавьте следующий конфиг:

repos:
-   repo: https://github.com/pycqa/flake8
    rev: 6.1.0
    hooks:
    -   id: flake8

Наконец, чтобы настроить скрипты git-хуков, выполните следующие действия:

(venv)$ pre-commit install

Теперь каждый раз, когда вы запускаете git commit, Flake8 будет запускаться до того, как будет выполнен коммит. И если возникнут какие-либо проблемы, коммит будет прерван.

Запуск инструментов в конвейере CI

Хотя вы можете использовать инструменты контроля качества кода в своем редакторе и с помощью pre-commit хуков, вы не всегда можете рассчитывать на то, что ваши товарищи по команде и другие сотрудники будут делать то же самое. Поэтому стоит запускать проверки качества кода в конвейере CI. На этом этапе вы должны запустить линтеры и детекторы уязвимостей безопасности, а также убедиться, что код соответствует определенному стилю. Такие проверки можно выполнять параллельно с тестами.

Реальный проект

Давайте создадим простой проект, чтобы посмотреть, как все это работает.

Сначала создайте новую папку:

$ mkdir flask_example
$ cd flask_example

Затем инициализируйте проект с помощью Poetry:

$ poetry init

Package name [flask_example]:
Version [0.1.0]:
Description []:
Author [Your name <your@email.com>, n to skip]:
License []:
Compatible Python versions [^3.12]:

Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Do you confirm generation? (yes/no) [yes]

После этого добавьте Flask, pytest, Flake8, Black, isort, Bandit и Safety:

$ poetry add flask
$ poetry add --group dev pytest flake8 black isort safety bandit

Создайте файл test_app.py для хранения тестов:

from app import app
import pytest


@pytest.fixture
def client():
    app.config['TESTING'] = True

    with app.test_client() as client:
        yield client


def test_home(client):
    response = client.get('/')

    assert response.status_code == 200

Затем добавьте файл app.py для приложения Flask:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def home():
    return 'OK'


if __name__ == '__main__':
    app.run()

Теперь мы готовы добавить pre-commit конфигурацию.

Сначала инициализируйте новый git-репозиторий:

$ git init

Затем установите pre-commit и настройте скрипты git hook:

$ poetry add --group dev pre-commit
$ poetry run pre-commit install

Для конфигурации создайте файл .pre-commit-config.yaml:

repos:
-   repo: https://github.com/pycqa/flake8
    rev: 6.1.0
    hooks:
    -   id: flake8

Перед коммитом запустите isort и Black:

$ poetry run isort . --profile black
$ poetry run black .

Сделайте коммит изменений, чтобы запустить pre-commit хук:

$ git add .
$ git commit -m 'Initial commit'

Наконец, давайте настроим CI-конвейер через GitHub Actions.

Создайте следующие файлы и папки:

.github
└── workflows
    └── main.yaml

.github/workflows/main.yaml:

name: CI
on: [push]

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        python-version: [3.12.0]
        poetry-version: [1.7.0]
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
      - name: Run image
        uses: abatilo/actions-poetry@v2
        with:
          poetry-version: ${{ matrix.poetry-version }}
      - name: Install dependencies
        run: poetry install
      - name: Run tests
        run: poetry run pytest
  code-quality:
    strategy:
      fail-fast: false
      matrix:
        python-version: [3.12.0]
        poetry-version: [1.7.0]
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
      - name: Run image
        uses: abatilo/actions-poetry@v2
        with:
          poetry-version: ${{ matrix.poetry-version }}
      - name: Install dependencies
        run: poetry install
      - name: Run black
        run: poetry run black . --check
      - name: Run isort
        run: poetry run isort . --check-only --profile black
      - name: Run flake8
        run: poetry run flake8 .
      - name: Run bandit
        run: poetry run bandit .
      - name: Run saftey
        run: poetry run safety check

Эта конфигурация:

  • запускается при каждом push – on: [push]
  • запускается на последней версии Ubuntu – ubuntu-latest
  • использует Python 3.12.0 – python-version: [3.12.0], python-version: ${{ matrix.python-version }}
  • используется Poetry версии 1.7.0 – poetry-version: [1.7.0], poetry-version: ${{ matrix.poetry-version }}

В конфигурации определены два задания (jobs): test и code-quality. Как следует из названий, в задаче test выполняются тесты, а в code-quality – проверка качества кода.

Добавьте CI-конфиг в git и сделайте коммит:

$ git add .github/workflows/main.yaml
$ git commit -m 'Add CI config'

Создайте новый репозиторий на GitHub и разместите там свой проект.

Например:

$ git remote add origin git@github.com:<your-github-username>/flask_example.git
$ git branch -M main
$ git push -u origin main

На вкладке Actions вашего репозитория GitHub вы должны увидеть, что ваш рабочий процесс запущен.

Заключение

Качество кода – одна из самых обсуждаемых тем в разработке программного обеспечения. Стиль кода, в частности, является чувствительным вопросом среди разработчиков, поскольку мы проводим большую часть рабочего времени за чтением кода. Код, имеющий последовательный стиль, соответствующий стандартам PEP-8, намного легче читать и понимать. Поскольку доведение кода до такого читабельного состояния это скучный, рутинный процесс, он должен выполняться компьютером с помощью инструментов, таких как Black и isort. А Flake8, Bandit и Safety помогут убедиться, что ваш код безопасен и не содержит ошибок.

Перевод статьи «Python Code Quality».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *