├── .gitignore ├── EN.md ├── LICENSE ├── README.md └── RU.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Mac 132 | .DS_Store 133 | .AppleDouble 134 | .LSOverride 135 | .DocumentRevisions-V100 136 | .fseventsd 137 | .Spotlight-V100gs 138 | .TemporaryItems 139 | .Trashes 140 | .VolumeIcon.icns 141 | 142 | # PyCharm 143 | .idea/ 144 | .idea_modules/ -------------------------------------------------------------------------------- /EN.md: -------------------------------------------------------------------------------- 1 | # Evrone Python Guidelines (EN) 2 | 3 | 4 | ## Table of Contents 5 | - [About the code](#about-the-code) 6 | - [Rule template](#rule-template) 7 | - [Basic principles](#basic-principles) 8 | - [Atomicity of operations](#atomicity-of-operations) 9 | - [Logical blocks](#logical-blocks) 10 | - [Sizes of methods, functions, and modules](#sizes-of-methods-functions-and-modules) 11 | - [Imports](#imports) 12 | - [Files `__init__.py`](#files-__init__py) 13 | - [Docstrings](#docstrings) 14 | - [About Pull Requests](#about-pull-requests) 15 | - [Creating Pull Requests](#creating-pull-requests) 16 | - [Refactoring and Pull Requests](#refactoring-and-pull-requests) 17 | - [Pull Request Size](#pull-request-size) 18 | - [About tooling](#about-tooling) 19 | - [Testing (pytest)](#testing-pytest) 20 | - [Package manager (poetry)](#package-manager-poetry) 21 | - [Code formatting (Black)](#code-formatting-black) 22 | - [Imports formatting (isort)](#imports-formatting-isort) 23 | - [Linter (flake8)](#linter-flake8) 24 | - [Type checker (mypy)](#type-checker-mypy) 25 | - [Pre-commit hooks (pre-commit)](#pre-commit-hooks-pre-commit) 26 | - [Other](#other) 27 | - [REST API Documentation](#rest-api-documentation) 28 | 29 | ## Rule template 30 | 31 | Describe the rule with necessary details and context. Provide `bad`, `good` with examples if possible. Helpful comments to snippets are highly appreciated. 32 | 33 | Bad ❌: 34 | 35 | ```python 36 | # snippet with bad example 37 | ``` 38 | 39 | Good ✅: 40 | 41 | ```python 42 | # snippet with good example 43 | ``` 44 | 45 | **Why?** 46 | 47 | Section is required and should contain reasons and arguments. It is recommended to provide links issues/resources/stackoveflow for specified reasons and arguments. 48 | 49 | 50 | ## About the code 51 | 52 | ### Basic principles 53 | - **Maintainability** (will you be able to understand your code in a year or two?) 54 | - **Simplicity** (between a complex and a simple solution, you should choose the simple one) 55 | - **Plainness** (when a new programmer joins, how clear it will be to them **why** this code is written in this way?) 56 | 57 | 58 | ### Atomicity of operations 59 | **1 action ~ 1 line** 60 | 61 | Try to do atomic operations in your code — there should be exactly **one** operation on each line. 62 | 63 | Bad ❌: 64 | ```python 65 | # 1. 3 actions on one line - 3 function calls 66 | foo_result = foo(bar(spam(x))) 67 | 68 | # 2. 3 actions on one line - function call foo, get_c, from_b 69 | foo_result = foo(a=a, b=b, c=get_c(from_b()) 70 | 71 | # 3. 3 actions on one line - filtering by arguments, conditionally getting elements (via or), calling a method .value 72 | result = [(a.value() or A, b or B) for a, b in iterator if a < b] 73 | 74 | # 4. 4 actions on one line - from library/variable foo comes bar attribute getting, spam attribute getting, hello attribute getting and calculate_weather call 75 | result = calculate_weather(foo.bar.spam.hello) 76 | ``` 77 | 78 | Good ✅: 79 | ```python 80 | # 1. make a call to each function in turn 81 | spam_result = spam(x) 82 | bar_result = bar(spam_result) 83 | foo_result = foo(bar_result) 84 | 85 | # 2. call the functions one by one, write the result to a variable and use it when calling foo 86 | from_b_result = from_b() 87 | c = get_c(from_b_result) 88 | foo_result = foo(a=a, b=b, c=c) 89 | 90 | # 3. sequentially perform actions on the list - first filter, then call the .value method of a, and choose between elements (or) 91 | filtered_result = ((a, b) for a, b in iterator if a < b) 92 | intermediate_result = ((a.value(), b) for a, b in filtered_result) 93 | result = [(a or A, b or B) for a, b in intermediate_result] 94 | 95 | # 4 . sequentially read the attributes bar, spam, hello and call the function calculate_weather 96 | bar = foo.bar 97 | spam = bar.spam 98 | hello = spam.hello 99 | result = calculate_weather(hello) 100 | ``` 101 | 102 | 103 | **Why?** Because the code becomes more readable, and there is no need to execute several statements in your head while reading the code. Code broken down into simple atomic operations is perceived much better than complex one-liners. Try to simplify your code as much as possible — code is more often read than written. 104 | 105 | 106 | **Notes**: 107 | 108 | * `ORM` syntax like `Model.objects.filter(...).select_related(...).distinct()` is a different story and it will be discussed in separate rule 109 | * Rule was written before [PEP-678](https://peps.python.org/pep-0678/). Try to figure out the error if an exception is thrown in such a chain `foo.bar.bar.bar.bar.bar.bar.bar` (`nonetype object has no attribute bar`) 110 | 111 | 112 | ### Logical blocks 113 | 114 | Try to divide the code into logical blocks — this way it will be much easier for the programmer to read and understand the essence. 115 | 116 | Bad ❌: 117 | ```python 118 | def register_model(self, app_label, model): 119 | model_name = model._meta.model_name 120 | app_models = self.all_models[app_label] 121 | if model_name in app_models: 122 | if (model.__name__ == app_models[model_name].__name__ and 123 | model.__module__ == app_models[model_name].__module__): 124 | warnings.warn( 125 | "Model '%s.%s' was already registered. " 126 | "Reloading models is not advised as it can lead to inconsistencies, " 127 | "most notably with related models." % (app_label, model_name), 128 | RuntimeWarning, stacklevel=2) 129 | else: 130 | raise RuntimeError( 131 | "Conflicting '%s' models in application '%s': %s and %s." % 132 | (model_name, app_label, app_models[model_name], model)) 133 | app_models[model_name] = model 134 | self.do_pending_operations(model) 135 | self.clear_cache() 136 | ``` 137 | 138 | Good ✅: 139 | ```python 140 | def register_model(self, app_label, model): 141 | model_name = model._meta.model_name 142 | app_models = self.all_models[app_label] 143 | 144 | if model_name in app_models: 145 | if ( 146 | model.__name__ == app_models[model_name].__name__ and 147 | model.__module__ == app_models[model_name].__module__ 148 | ): 149 | warnings.warn( 150 | "Model '%s.%s' was already registered. " 151 | "Reloading models is not advised as it can lead to inconsistencies, " 152 | "most notably with related models." % (app_label, model_name), 153 | RuntimeWarning, stacklevel=2) 154 | 155 | else: 156 | raise RuntimeError( 157 | "Conflicting '%s' models in application '%s': %s and %s." % 158 | (model_name, app_label, app_models[model_name], model)) 159 | 160 | app_models[model_name] = model 161 | 162 | self.do_pending_operations(model) 163 | self.clear_cache() 164 | ``` 165 | 166 | **Why?** In addition to improving readability, [The Zen of Python](https://www.python.org/dev/peps/pep-0020/) teaches us how to write idiomatic Python code. 167 | One of the statements claims that "sparse is better than dense." Compressed code is harder to read than sparse code. 168 | 169 | 170 | ### Sizes of methods, functions, and modules 171 | 172 | The size limit for a method or function is 50 lines. 173 | Reaching the size limit indicates that the function (method) is doing too much — so decompose the actions inside the function (method). 174 | 175 | 176 | The module size limit is 300 lines. 177 | Reaching the size limit indicates that the module has received too much logic — so decompose the module into several ones. 178 | 179 | The line length is 100 characters. 180 | 181 | 182 | ### Imports 183 | 184 | The recommended import method is absolute. 185 | 186 | Bad ❌: 187 | ```python 188 | # spam.py 189 | from . import foo, bar 190 | ``` 191 | 192 | Good ✅: 193 | ```python 194 | # spam.py 195 | from some.absolute.path import foo, bar 196 | ``` 197 | 198 | **Why?** Because absolute import explicitly defines the location (path) of the module that is being imported. 199 | With relative imports, you always need to remember the path and calculate in your mind the location of the modules `foo.py`, `bar.py` relative to `spam.py` 200 | 201 | 202 | ### Files `__init__.py` 203 | 204 | Only write imports in `__init__.py` files. 205 | 206 | **Why?** Because `__init__.py` is the last place a programmer will look when they read the code in the future. 207 | 208 | 209 | ### Docstrings 210 | We recommend adding docstrings to functions, methods, and classes. 211 | 212 | **Why?** Because the programmer who sees your code for the first time will be able to quickly understand what is happening in it. 213 | Code is read much more than it is written. 214 | 215 | 216 | ### Type Annotations 217 | 218 | Annotating new code is strongly encouraged. Existing codebase is recommended to annotate gradually. It is also advised to use `static type checker` on `pre-commit` or `CI` stage but allow proceeding with invalid annotations because sometimes it takes a lot of effort to debug and annotate code. 219 | 220 | **Why?** Typed code is better self-documented. There is no need to guess the object's type or use `isinstance`, modern IDEs work perfectly with annotated object types. Moreover, annotations reduce error rate. Warnings and errors are shown by `static type checker` during coding instead of catching errors on running project. Unit testing is also much easier if you know what types are expected. 221 | 222 | 223 | ## About Pull Requests 224 | 225 | ### Creating Pull Requests 226 | **1 Pull Request = 1 issue** 227 | 228 | One Pull Request must solve exactly one issue. 229 | 230 | **Why?** Because it is more difficult for a reviewer to keep the context of several tasks in their head and switch between them. When a PR contains several issues, then the PR often increases and requires more time and effort for the review from the reviewer. 231 | 232 | 233 | ### Refactoring and Pull Requests 234 | Refactoring is best done in a separate Pull Request. 235 | 236 | **Why?** When refactoring goes along with resolving a specific issue, the refactoring blurs the context of the issue and introduces changes that are not related to that PR. 237 | 238 | 239 | ### Pull Request Size 240 | The resulting PR diff should not exceed +/- 600 changed lines. 241 | 242 | Bad ❌: 243 | 244 | ![bad](https://user-images.githubusercontent.com/8825727/113953748-6fc7ba80-9853-11eb-9673-827995e54f73.png) 245 | ``` 246 | Diff 444 + 333 = 777 247 | ``` 248 | 249 | Good ✅: 250 | 251 | ![good](https://user-images.githubusercontent.com/8825727/113953831-a30a4980-9853-11eb-854b-d4c4f6559f2c.png) 252 | ``` 253 | Diff 222 + 111 = 333 254 | ``` 255 | 256 | 257 | **Why?** Because the more PR involves, the more uncontrollable it becomes, and the merge is made "with eyes closed and ears shut." 258 | Also, most reviewers will find it difficult to accept a large volume of changes at once. 259 | 260 | 261 | ## About tooling 262 | 263 | ### Testing (pytest) 264 | [pytest](https://pytest.org) - code testing framework 265 | 266 | Recommended config in `pytest.ini`: 267 | ```ini 268 | [pytest] 269 | DJANGO_SETTINGS_MODULE = settings.local 270 | python_files = tests.py test_*.py *_tests.py 271 | ``` 272 | 273 | ### Package manager (poetry) 274 | [poetry](https://python-poetry.org) - dependency manager and package builder 275 | 276 | 277 | ### Code formatting (Black) 278 | Black - PEP8 code auto-formatter 279 | 280 | Recommended config in `pyproject.toml`: 281 | ```toml 282 | [tool.black] 283 | line-length = 100 284 | target-version = ['py38'] 285 | exclude = ''' 286 | ( 287 | \.eggs 288 | |\.git 289 | |\.hg 290 | |\.mypy_cache 291 | |\.nox 292 | |\.tox 293 | |\.venv 294 | |_build 295 | |buck-out 296 | |build 297 | |dist 298 | ) 299 | ''' 300 | ``` 301 | 302 | 303 | ### Imports formatting (isort) 304 | [isort](https://pycqa.github.io/isort/) - import block auto-formatter 305 | 306 | Recommended config in `pyproject.toml`: 307 | ```toml 308 | [tool.isort] 309 | line_length = 100 310 | sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] 311 | multi_line_output = 3 312 | known_django = "django" 313 | profile = "django" 314 | src_paths = "app" 315 | lines_after_imports = 2 316 | ``` 317 | 318 | 319 | ### Linter (flake8) 320 | [flake8](https://flake8.pycqa.org/en/latest/) - PEP8 conformance validator 321 | 322 | Recommended config in `.flake8`: 323 | ```ini 324 | [flake8] 325 | max-line-length = 100 326 | max-complexity = 5 327 | exclude = .venv,venv,**/migrations/*,snapshots 328 | per-file-ignores = 329 | tests/**: S101 330 | **/tests/**: S101 331 | ``` 332 | 333 | 334 | ### Type checker (mypy) 335 | [mypy](http://mypy.readthedocs.io) - checker for static typing 336 | 337 | Recommended config `mypy.ini`: 338 | ```ini 339 | [mypy] 340 | ignore_missing_imports = True 341 | allow_untyped_globals = True 342 | 343 | [mypy-*.migrations.*] 344 | ignore_errors = True 345 | ``` 346 | 347 | 348 | ### Pre-commit hooks (pre-commit) 349 | 350 | [pre-commit](https://pre-commit.com) - framework for managing `pre-commit` hooks 351 | 352 | Recommended config `.pre-commit-config.yaml`: 353 | 354 | ```yaml 355 | default_language_version: 356 | python: python3.8 357 | 358 | repos: 359 | - repo: local 360 | hooks: 361 | - id: black 362 | name: black 363 | entry: black app 364 | language: python 365 | types: [python] 366 | 367 | - id: isort 368 | name: isort 369 | entry: isort app 370 | language: python 371 | types: [python] 372 | 373 | - id: flake8 374 | name: flake8 375 | entry: flake8 server 376 | language: python 377 | types: [python] 378 | ``` 379 | 380 | 381 | ## Other 382 | 383 | ### REST API Documentation 384 | The recommended documentation format is [OpenAPI](https://www.openapis.org). 385 | The schema for OpenAPI should be generated “on the fly” to provide API clients with fresh changes. 386 | 387 | **Why?** Because it's one of the common formats for documenting REST APIs that come out of Swagger. This documentation format is supported by a large number of clients (Swagger, Postman, Insomnia Designer, and many others). Also, handwritten documentation tends to quickly become outdated, and documentation that is generated directly from the code allows you to avoid constantly thinking about updating the documentation. 388 | 389 | 390 | ## Sponsor 391 | [](https://evrone.com/?utm_source=github.com&utm_campaign=evrone-python-codestyle) 392 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Evrone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | Stars 7 | 8 | License 9 | 10 |

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