|
| 1 | +# Evrone Python Guidelines |
1 | 2 |
|
| 3 | + |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | +## Содержание |
| 8 | +- [Про код](#про-код) |
| 9 | + - [Основные принципы](#основные-принципы) |
| 10 | + - [Атомарность операций](#атомарность-операций) |
| 11 | + - [Логические блоки](#логические-блоки) |
| 12 | + - [Размеры методов, функций и модулей](#размеры-методов-функций-и-модулей) |
| 13 | + - [Импорты](#импорты) |
| 14 | + - [Файлы `__init__.py`](#файлы-__init__py) |
| 15 | + - [Докстринги](#докстринги) |
| 16 | +- [Про Pull Request](#про-pull-request) |
| 17 | + - [Создание Pull Request](#создание-pull-request) |
| 18 | + - [Рефакторинг и Pull Request](#рефакторинг-и-pull-request) |
| 19 | + - [Размер Pull Request](#размер-pull-request) |
| 20 | +- [Про тулинг](#про-тулинг) |
| 21 | + - [Тестирование (pytest)](#тестирование-pytest) |
| 22 | + - [Пакетный менеджер (poetry)](#пакетный-менеджер-poetry) |
| 23 | + - [Форматирование кода (Black)](#форматирование-кода-black) |
| 24 | + - [Форматирование импортов (isort)](#форматирование-импортов-isort) |
| 25 | + - [Линтер (flake8)](#линтер-flake8) |
| 26 | + - [Тайп-чекер (mypy)](#тайп-чекер-mypy) |
| 27 | + - [Pre-commit хуки (pre-commit)](#pre-commit-хуки-pre-commit) |
| 28 | +- [Прочее](#прочее) |
| 29 | + - [Документация к REST API](#документация-к-rest-api) |
| 30 | + |
| 31 | + |
| 32 | +## Про код |
| 33 | + |
| 34 | +### Основные принципы |
| 35 | +- **Поддерживаемость** (представьте, сможете ли вы понять свой код через год или через два) |
| 36 | +- **Простота** (между сложным и простым решением следует выбрать простое) |
| 37 | +- **Очевидность** (представьте, когда подключится новый программист, насколько ему будет понятно, почему именно **так** написан этот код) |
| 38 | + |
| 39 | + |
| 40 | +### Атомарность операций |
| 41 | +**1 действие ~ 1 строка** |
| 42 | + |
| 43 | +Постарайтесь делать атомарные операции в коде - на каждой строке ровно **одно** действие. |
| 44 | + |
| 45 | +Плохо ❌: |
| 46 | +```python |
| 47 | +# 1. 3 действия на одной строке - 3 вызова функции |
| 48 | +foo_result = foo(bar(spam(x))) |
| 49 | + |
| 50 | +# 2. 3 действия на одной строке - вызов функции foo, get_c, from_b |
| 51 | +foo_result = foo(a=a, b=b, c=get_c(from_b()) |
| 52 | + |
| 53 | +# 3. 3 действия на одной строке - фильтрация по аргументам, условное получение элементов (через or), вызов метода .value |
| 54 | +result = [(a.value() or A, b or B) for a, b in iterator if a < b] |
| 55 | + |
| 56 | +# 4. 4 действия на одной строке - из библиотеки / переменной foo идет получение атрибута bar, получение атрибута spam, получение атрибута hello и вызов calculate_weather |
| 57 | +result = calculate_weather(foo.bar.spam.hello) |
| 58 | +``` |
| 59 | + |
| 60 | +Хорошо ✅: |
| 61 | +```python |
| 62 | +# 1. делаем поочередный вызов каждой функции |
| 63 | +spam_result = spam(x) |
| 64 | +bar_result = bar(spam_result) |
| 65 | +foo_result = foo(bar_result) |
| 66 | + |
| 67 | +# 2. поочередно вызываем функции, результат пишем в переменную и используем ее при вызове foo |
| 68 | +from_b_result = from_b() |
| 69 | +c = get_c(from_b_result) |
| 70 | +foo_result = foo(a=a, b=b, c=c) |
| 71 | + |
| 72 | +# 3. последовательно проводим действия над списком - вначале фильтруем, вызываем метод .value у a, выбираем между элементами (or) |
| 73 | +filtered_result = ((a, b) for a, b in iterator if a < b) |
| 74 | +intermediate_result = ((a.value(), b) for a, b in filtered_result) |
| 75 | +result = [(a or A, b or B) for a, b in intermediate_result] |
| 76 | + |
| 77 | +# 4 . последовательно читаем атрибуты bar, spam, hello и вызываем функцию calculate_weather |
| 78 | +bar = foo.bar |
| 79 | +spam = bar.spam |
| 80 | +hello = spam.hello |
| 81 | +result = calculate_weather(hello) |
| 82 | +``` |
| 83 | + |
| 84 | + |
| 85 | +**Почему?** Потому что код становится более читабельным, не нужно исполнять несколько выражений в голове во время чтения кода. Разбитый до простых атомных операций код воспринимается гораздо лучше, чем сложный уан-лайнер. Постарайтесь упростить свой код настолько, насколько это возможно - код чаще читается, чем пишется. |
| 86 | + |
| 87 | + |
| 88 | +### Логические блоки |
| 89 | + |
| 90 | +Постарайтесь делить код на логические блоки - так глазу программиста будет в разы проще прочитать и уловить суть. |
| 91 | + |
| 92 | +Плохо ❌: |
| 93 | +```python |
| 94 | +def register_model(self, app_label, model): |
| 95 | + model_name = model._meta.model_name |
| 96 | + app_models = self.all_models[app_label] |
| 97 | + if model_name in app_models: |
| 98 | + if (model.__name__ == app_models[model_name].__name__ and |
| 99 | + model.__module__ == app_models[model_name].__module__): |
| 100 | + warnings.warn( |
| 101 | + "Model '%s.%s' was already registered. " |
| 102 | + "Reloading models is not advised as it can lead to inconsistencies, " |
| 103 | + "most notably with related models." % (app_label, model_name), |
| 104 | + RuntimeWarning, stacklevel=2) |
| 105 | + else: |
| 106 | + raise RuntimeError( |
| 107 | + "Conflicting '%s' models in application '%s': %s and %s." % |
| 108 | + (model_name, app_label, app_models[model_name], model)) |
| 109 | + app_models[model_name] = model |
| 110 | + self.do_pending_operations(model) |
| 111 | + self.clear_cache() |
| 112 | +``` |
| 113 | + |
| 114 | +Хорошо ✅: |
| 115 | +```python |
| 116 | +def register_model(self, app_label, model): |
| 117 | + model_name = model._meta.model_name |
| 118 | + app_models = self.all_models[app_label] |
| 119 | + |
| 120 | + if model_name in app_models: |
| 121 | + if ( |
| 122 | + model.__name__ == app_models[model_name].__name__ and |
| 123 | + model.__module__ == app_models[model_name].__module__ |
| 124 | + ): |
| 125 | + warnings.warn( |
| 126 | + "Model '%s.%s' was already registered. " |
| 127 | + "Reloading models is not advised as it can lead to inconsistencies, " |
| 128 | + "most notably with related models." % (app_label, model_name), |
| 129 | + RuntimeWarning, stacklevel=2) |
| 130 | + |
| 131 | + else: |
| 132 | + raise RuntimeError( |
| 133 | + "Conflicting '%s' models in application '%s': %s and %s." % |
| 134 | + (model_name, app_label, app_models[model_name], model)) |
| 135 | + |
| 136 | + app_models[model_name] = model |
| 137 | + |
| 138 | + self.do_pending_operations(model) |
| 139 | + self.clear_cache() |
| 140 | +``` |
| 141 | + |
| 142 | +**Почему?** Кроме того, что это повышает читабельность, [Zen of Python](https://www.python.org/dev/peps/pep-0020/) рассказывает нам о том, как надо писать идиоматический код на Python. |
| 143 | +Одно из высказываний звучит как "Sparse is better than dense." - "Разреженное лучше чем сжатое". Сжатый код сложнее прочитать чем разреженный. |
| 144 | + |
| 145 | + |
| 146 | +### Размеры методов, функций и модулей |
| 147 | + |
| 148 | +Предельный размер метода или функции - **50** строк. |
| 149 | +Достижение предельного размера говорит о том, что функция (метод) делает слишком много - декомпозируйте действия внутри функции (метода). |
| 150 | + |
| 151 | + |
| 152 | +Предельный размер модуля - **300** строк. |
| 153 | +Достижение предельного размера говорит о том, что модуль получил слишком много логики - декомпозируйте модуль на несколько. |
| 154 | + |
| 155 | +Длина строки - 100 символов. |
| 156 | + |
| 157 | + |
| 158 | +### Импорты |
| 159 | + |
| 160 | +Рекомендуемый способ импортирования - абсолютный. |
| 161 | + |
| 162 | +Плохо ❌: |
| 163 | +```python |
| 164 | +# spam.py |
| 165 | +from . import foo, bar |
| 166 | +``` |
| 167 | + |
| 168 | +Хорошо ✅: |
| 169 | +```python |
| 170 | + |
| 171 | +# spam.py |
| 172 | +from some.absolute.path import foo, bar |
| 173 | +``` |
| 174 | + |
| 175 | +**Почему?** Потому что абсолютный импорт явно определяет локацию (путь) модуля, который импортируется. При релативном |
| 176 | +импорте всегда нужно помнить путь и вычислять в уме локацию модулей `foo.py`, `bar.py` относительно `spam.py` |
| 177 | + |
| 178 | + |
| 179 | +### Файлы `__init__.py` |
| 180 | + |
| 181 | +В `__init__.py` файлах пишем только импорты. |
| 182 | + |
| 183 | +**Почему?** Потому что `__init__.py` - последнее место, в которое посмотрит программист, который будет читать код в будущем. |
| 184 | + |
| 185 | + |
| 186 | +### Докстринги |
| 187 | +Рекомендуем добавлять докстринги в функции, методы и классы. |
| 188 | + |
| 189 | +**Почему?** Потому что программист, который впервые увидит ваш код, сможет быстрее понять, что в нем происходит. |
| 190 | +Код читается намного больше, чем пишется. |
| 191 | + |
| 192 | + |
| 193 | +## Про Pull Request |
| 194 | + |
| 195 | +### Создание Pull Request |
| 196 | +**1 Pull Request = 1 issue** |
| 197 | + |
| 198 | +Один Pull Request должен решать ровно одно issue. |
| 199 | + |
| 200 | +**Почему?** Потому что ревьюверу сложнее держать контекст нескольких задач в голове и переключаться между ними. Когда PR содержит несколько issue - это часто приводит к тому, что PR увеличивается и требует больше времени и сил на ревью от ревьювера. |
| 201 | + |
| 202 | + |
| 203 | +### Рефакторинг и Pull Request |
| 204 | +Рефакторинг лучше всего выносить в отдельный Pull Request. |
| 205 | + |
| 206 | +**Почему?** Когда рефакторинг идет вместе с решением определенного issue, то рефакторинг размывает контекст issue и вводит правки, которые не имеют отношения к данному PR. |
| 207 | + |
| 208 | + |
| 209 | +### Размер Pull Request |
| 210 | +Итоговый дифф PR не должен превышать +/- 600 измененных строк. |
| 211 | + |
| 212 | +Плохо ❌: |
| 213 | + |
| 214 | + |
| 215 | +``` |
| 216 | +Дифф 444 + 333 = 777 |
| 217 | +``` |
| 218 | + |
| 219 | +Хорошо ✅: |
| 220 | + |
| 221 | + |
| 222 | +``` |
| 223 | +Дифф 222 + 111 = 333 |
| 224 | +``` |
| 225 | + |
| 226 | + |
| 227 | +**Почему?** Потому что чем больше PR - тем более он становится неконтролируемым и мерж производится "закрыв глаза и заткнув уши". |
| 228 | +Также, большинству ревьюверов будет сложно воспринять такой объем изменений за один раз. |
| 229 | + |
| 230 | + |
| 231 | +## Про тулинг |
| 232 | + |
| 233 | +### Тестирование (pytest) |
| 234 | +[pytest](https://pytest.org) - фреймворк для тестирования кода |
| 235 | + |
| 236 | +Рекомендуемый конфиг в `pytest.ini`: |
| 237 | +```ini |
| 238 | +[pytest] |
| 239 | +DJANGO_SETTINGS_MODULE = settings.local |
| 240 | +python_files = tests.py test_*.py *_tests.py |
| 241 | + |
| 242 | +``` |
| 243 | + |
| 244 | +### Пакетный менеджер (poetry) |
| 245 | + |
| 246 | +[poetry](https://python-poetry.org) - менеджер зависимостей и сборщик пакетов |
| 247 | + |
| 248 | + |
| 249 | +### Форматирование кода (Black) |
| 250 | +[Black](https://black.readthedocs.io/en/stable/) - автоформаттер кода по PEP8 |
| 251 | + |
| 252 | +Рекомендуемый конфиг в `pyproject.toml`: |
| 253 | +```toml |
| 254 | + |
| 255 | +[tool.black] |
| 256 | +line-length = 100 |
| 257 | +target-version = ['py38'] |
| 258 | +exclude = ''' |
| 259 | +( |
| 260 | + \.eggs |
| 261 | + |\.git |
| 262 | + |\.hg |
| 263 | + |\.mypy_cache |
| 264 | + |\.nox |
| 265 | + |\.tox |
| 266 | + |\.venv |
| 267 | + |_build |
| 268 | + |buck-out |
| 269 | + |build |
| 270 | + |dist |
| 271 | +) |
| 272 | +''' |
| 273 | + |
| 274 | +``` |
| 275 | + |
| 276 | + |
| 277 | +### Форматирование импортов (isort) |
| 278 | +[isort](https://pycqa.github.io/isort/) - автоформаттер блока импортов |
| 279 | + |
| 280 | +Рекомендуемый конфиг в `pyproject.toml`: |
| 281 | +```toml |
| 282 | + |
| 283 | +[tool.isort] |
| 284 | +line_length = 100 |
| 285 | +sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] |
| 286 | +multi_line_output = 3 |
| 287 | +known_django = "django" |
| 288 | +profile = "django" |
| 289 | +src_paths = "app" |
| 290 | +lines_after_imports = 2 |
| 291 | + |
| 292 | +``` |
| 293 | + |
| 294 | + |
| 295 | +### Линтер (flake8) |
| 296 | +[flake8](https://flake8.pycqa.org/en/latest/) - валидатор соответствия PEP8 |
| 297 | + |
| 298 | +Рекомендуемый конфиг в `.flake8`: |
| 299 | +```ini |
| 300 | +[flake8] |
| 301 | +max-line-length = 100 |
| 302 | +max-complexity = 5 |
| 303 | +exclude = .venv,venv,**/migrations/*,snapshots |
| 304 | +per-file-ignores = |
| 305 | + tests/**: S101 |
| 306 | + **/tests/**: S101 |
| 307 | +``` |
| 308 | + |
| 309 | + |
| 310 | +### Тайп-чекер (mypy) |
| 311 | + |
| 312 | +[mypy](http://mypy.readthedocs.io) - чекер для статической типизации |
| 313 | + |
| 314 | +Рекомендуемый конфиг `mypy.ini`: |
| 315 | + |
| 316 | +```ini |
| 317 | +[mypy] |
| 318 | +ignore_missing_imports = True |
| 319 | +allow_untyped_globals = True |
| 320 | + |
| 321 | +[mypy-*.migrations.*] |
| 322 | +ignore_errors = True |
| 323 | + |
| 324 | +``` |
| 325 | + |
| 326 | + |
| 327 | +### Pre-commit хуки (pre-commit) |
| 328 | + |
| 329 | +[pre-commit](https://pre-commit.com) - фреймворк для управления `pre-commit` хуками |
| 330 | + |
| 331 | +Рекомендуемый конфиг `.pre-commit-config.yaml`: |
| 332 | + |
| 333 | +```yaml |
| 334 | +default_language_version: |
| 335 | + python: python3.8 |
| 336 | + |
| 337 | +repos: |
| 338 | + - repo: local |
| 339 | + hooks: |
| 340 | + - id: black |
| 341 | + name: black |
| 342 | + entry: black app |
| 343 | + language: python |
| 344 | + types: [python] |
| 345 | + |
| 346 | + - id: isort |
| 347 | + name: isort |
| 348 | + entry: isort app |
| 349 | + language: python |
| 350 | + types: [python] |
| 351 | + |
| 352 | + - id: flake8 |
| 353 | + name: flake8 |
| 354 | + entry: flake8 server |
| 355 | + language: python |
| 356 | + types: [python] |
| 357 | +``` |
| 358 | + |
| 359 | + |
| 360 | +## Прочее |
| 361 | + |
| 362 | +### Документация к REST API |
| 363 | +Рекомендуемый формат документации - [OpenAPI](https://www.openapis.org). |
| 364 | +Схема для OpenAPI должна генерироваться "на лету", чтобы обеспечивать клиентов API свежими изменениями. |
| 365 | + |
| 366 | +**Почему?** Потому что это один из распространенных форматов для документирования REST API, который вышел из Swagger. Данный формат документации поддерживается большим количеством клиентов (Swagger, Postman, Insomnia Designer и многие другие). Также, рукописная документация имеет свойство быстро устаревать, а документация, которая генерируется напрямую из кода позволяет не думать о постоянном обновлении документации. |
| 367 | + |
| 368 | +## Спонсор |
| 369 | +[<img src="https://evrone.com/logo/evrone-sponsored-logo.png" width=300>](https://evrone.com/?utm_source=github.com&utm_campaign=evrone-python-codestyle) |
0 commit comments