Skip to content

Latest commit

 

History

History
389 lines (281 loc) · 18.2 KB

RU.md

File metadata and controls

389 lines (281 loc) · 18.2 KB

Evrone Python Guidelines (RU)

Содержание

Шаблон правила

Опишите правило или рекомендацию, сопроводив ее необходимыми нюансами и контекстом. По возможности предоставьте примеры нарушения и соблюдения правила в блоках Плохо и Хорошо. Комментарии к фрагментам кода приветствуются.

Плохо ❌:

# Фрагмент кода с нарушением правила

Хорошо ✅:

# Фрагмент кода с соблюдением правила

Почему?

Обязательная секция с аргументами и причинами использования правила. Желательно указать ссылки на issues/статьи/stackoverflow в поддержку аргументов.

Про код

Основные принципы

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

Атомарность операций

1 действие ~ 1 строка

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

Плохо ❌:

# 1. 3 действия на одной строке - 3 вызова функции
foo_result = foo(bar(spam(x)))

# 2. 3 действия на одной строке - вызов функции foo, get_c, from_b
foo_result = foo(a=a, b=b, c=get_c(from_b())

# 3. 3 действия на одной строке - фильтрация по аргументам, условное получение элементов (через or), вызов метода .value
result = [(a.value() or A, b or B) for a, b in iterator if a < b]

# 4. 4 действия на одной строке - из библиотеки / переменной foo идет получение атрибута bar, получение атрибута spam, получение атрибута hello и вызов calculate_weather
result = calculate_weather(foo.bar.spam.hello)

Хорошо ✅:

# 1. делаем поочередный вызов каждой функции
spam_result = spam(x)
bar_result = bar(spam_result)
foo_result = foo(bar_result)

# 2. поочередно вызываем функции, результат пишем в переменную и используем ее при вызове foo
from_b_result = from_b()
c = get_c(from_b_result)
foo_result = foo(a=a, b=b, c=c)

# 3. последовательно проводим действия над списком - вначале фильтруем, вызываем метод .value у a, выбираем между элементами (or)
filtered_result = ((a, b) for a, b in iterator if a < b)
intermediate_result = ((a.value(), b) for a, b in filtered_result)
result = [(a or A, b or B) for a, b in intermediate_result]

# 4 . последовательно читаем атрибуты bar, spam, hello и вызываем функцию calculate_weather
bar = foo.bar
spam = bar.spam
hello = spam.hello
result = calculate_weather(hello)

Почему? Потому что код становится более читабельным, не нужно исполнять несколько выражений в голове во время чтения кода. Разбитый до простых атомных операций код воспринимается гораздо лучше, чем сложный однострочник. Постарайтесь упростить свой код настолько, насколько это возможно - код чаще читается, чем пишется.

Примечания:

  • К ORM синтаксу, например Model.objects.filter(...).select_related(...).distinct(), следует относиться иначе, что будет описано в отдельном правиле
  • Правило было написано до принятия PEP-678. Попробуйте найти ошибку, если возникло исключение в выражении foo.bar.bar.bar.bar.bar.bar.bar (nonetype object has no attribute bar)

Логические блоки

Постарайтесь делить код на логические блоки - так глазу программиста будет в разы проще прочитать и уловить суть.

Плохо ❌:

def register_model(self, app_label, model):
    model_name = model._meta.model_name
    app_models = self.all_models[app_label]
    if model_name in app_models:
        if (model.__name__ == app_models[model_name].__name__ and
                model.__module__ == app_models[model_name].__module__):
            warnings.warn(
                "Model '%s.%s' was already registered. "
                "Reloading models is not advised as it can lead to inconsistencies, "
                "most notably with related models." % (app_label, model_name),
                RuntimeWarning, stacklevel=2)
        else:
            raise RuntimeError(
                "Conflicting '%s' models in application '%s': %s and %s." %
                (model_name, app_label, app_models[model_name], model))
    app_models[model_name] = model
    self.do_pending_operations(model)
    self.clear_cache()

Хорошо ✅:

def register_model(self, app_label, model):
    model_name = model._meta.model_name
    app_models = self.all_models[app_label]
    
    if model_name in app_models:
        if (
            model.__name__ == app_models[model_name].__name__ and
            model.__module__ == app_models[model_name].__module__
        ):
            warnings.warn(
                "Model '%s.%s' was already registered. "
                "Reloading models is not advised as it can lead to inconsistencies, "
                "most notably with related models." % (app_label, model_name),
                RuntimeWarning, stacklevel=2)
            
        else:
            raise RuntimeError(
                "Conflicting '%s' models in application '%s': %s and %s." %
                (model_name, app_label, app_models[model_name], model))
        
    app_models[model_name] = model
    
    self.do_pending_operations(model)
    self.clear_cache()

Почему? Кроме того, что это повышает читабельность, Zen of Python рассказывает нам о том, как надо писать идиоматический код на Python. Одно из высказываний звучит как "Sparse is better than dense." - "Разреженное лучше чем сжатое". Сжатый код сложнее прочитать чем разреженный.

Размеры методов, функций и модулей

Предельный размер метода или функции - 50 строк. Достижение предельного размера говорит о том, что функция (метод) делает слишком много - декомпозируйте действия внутри функции (метода).

Предельный размер модуля - 300 строк. Достижение предельного размера говорит о том, что модуль получил слишком много логики - декомпозируйте модуль на несколько.

Длина строки - 100 символов.

Импорты

Рекомендуемый способ импортирования - абсолютный.

Плохо ❌:

# spam.py
from . import foo, bar

Хорошо ✅:

# spam.py
from some.absolute.path import foo, bar

Почему? Потому что абсолютный импорт явно определяет локацию (путь) модуля, который импортируется. При релативном импорте всегда нужно помнить путь и вычислять в уме локацию модулей foo.py, bar.py относительно spam.py

Файлы __init__.py

В __init__.py файлах пишем только импорты.

Почему? Потому что __init__.py - последнее место, в которое посмотрит программист, который будет читать код в будущем.

Докстринги

Рекомендуем добавлять докстринги в функции, методы и классы.

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

Аннотации типов

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

Почему? Типизированный код является актуальной "документацией". Не нужно угадывать тип объекта или использовать isinstance, современные IDE прекрасно считывают тип аннотированных объектов. К тому же типизированный код снижает число ошибок. Предупреждения и ошибки о неправильном типе возникают при запуске статического анализатора типов во время написания кода, а не в процессе взаимодействия с запущенным проектом. Юнит тесты писать гораздо проще, если знать, какие типы данных ожидаемы.

Про Pull Request

Создание Pull Request

1 Pull Request = 1 issue

Один Pull Request должен решать ровно одно issue.

Почему? Потому что ревьюверу сложнее держать контекст нескольких задач в голове и переключаться между ними. Когда PR содержит несколько issue - это часто приводит к тому, что PR увеличивается и требует больше времени и сил на ревью от ревьювера.

Рефакторинг и Pull Request

Рефакторинг лучше всего выносить в отдельный Pull Request.

Почему? Когда рефакторинг идет вместе с решением определенного issue, то рефакторинг размывает контекст issue и вводит правки, которые не имеют отношения к данному PR.

Размер Pull Request

Итоговый дифф PR не должен превышать +/- 600 измененных строк.

Плохо ❌:

bad

Дифф 444 + 333 = 777

Хорошо ✅:

good

Дифф 222 + 111 = 333

Почему? Потому что чем больше PR - тем более он становится неконтролируемым и мерж производится "закрыв глаза и заткнув уши". Также, большинству ревьюверов будет сложно воспринять такой объем изменений за один раз.

Про тулинг

Тестирование (pytest)

pytest - фреймворк для тестирования кода

Рекомендуемый конфиг в pytest.ini:

[pytest]
DJANGO_SETTINGS_MODULE = settings.local
python_files = tests.py test_*.py *_tests.py

Пакетный менеджер (poetry)

poetry - менеджер зависимостей и сборщик пакетов

Форматирование кода (Black)

Black - автоформаттер кода по PEP8

Рекомендуемый конфиг в pyproject.toml:

[tool.black]
line-length = 100
target-version = ['py38']
exclude = '''
(
  \.eggs
  |\.git
  |\.hg
  |\.mypy_cache
  |\.nox
  |\.tox
  |\.venv
  |_build
  |buck-out
  |build
  |dist
)
'''

Форматирование импортов (isort)

isort - автоформаттер блока импортов

Рекомендуемый конфиг в pyproject.toml:

[tool.isort]
line_length = 100
sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
multi_line_output = 3
known_django = "django"
profile = "django"
src_paths = "app"
lines_after_imports = 2

Линтер (flake8)

flake8 - валидатор соответствия PEP8

Рекомендуемый конфиг в .flake8:

[flake8]
max-line-length = 100
max-complexity = 5
exclude = .venv,venv,**/migrations/*,snapshots
per-file-ignores =
    tests/**: S101
    **/tests/**: S101

Тайп-чекер (mypy)

mypy - чекер для статической типизации

Рекомендуемый конфиг mypy.ini:

[mypy]
ignore_missing_imports = True
allow_untyped_globals = True

[mypy-*.migrations.*]
ignore_errors = True

Pre-commit хуки (pre-commit)

pre-commit - фреймворк для управления pre-commit хуками

Рекомендуемый конфиг .pre-commit-config.yaml:

default_language_version:
    python: python3.8

repos:
  - repo: local
    hooks:
      - id: black
        name: black
        entry: black app
        language: python
        types: [python]

      - id: isort
        name: isort
        entry: isort app
        language: python
        types: [python]

      - id: flake8
        name: flake8
        entry: flake8 server
        language: python
        types: [python]

Прочее

Документация к REST API

Рекомендуемый формат документации - OpenAPI. Схема для OpenAPI должна генерироваться "на лету", чтобы обеспечивать клиентов API свежими изменениями.

Почему? Потому что это один из распространенных форматов для документирования REST API, который вышел из Swagger. Данный формат документации поддерживается большим количеством клиентов (Swagger, Postman, Insomnia Designer и многие другие). Также, рукописная документация имеет свойство быстро устаревать, а документация, которая генерируется напрямую из кода позволяет не думать о постоянном обновлении документации.

Спонсор