|
| 1 | +# Evrone Python Guidelines |
| 2 | + |
| 3 | + |
| 4 | +## About the code |
| 5 | + |
| 6 | +### Basic principles |
| 7 | +- **Maintainability** (will you be able to understand your code in a year or two?) |
| 8 | +- **Simplicity** (between a complex and a simple solution, you should choose the simple one) |
| 9 | +- **Plainness** (when a new programmer joins, how clear it will be to them why this code is written in this way?) |
| 10 | + |
| 11 | + |
| 12 | +### Atomicity of operations |
| 13 | +**1 action ~ 1 line** |
| 14 | + |
| 15 | +Try to do atomic operations in your code — there should be exactly one operation on each line. |
| 16 | + |
| 17 | +Bad ❌: |
| 18 | +```python |
| 19 | +# 1. 3 actions on one line - 3 function calls |
| 20 | +foo_result = foo(bar(spam(x))) |
| 21 | + |
| 22 | +# 2. 3 actions on one line - function call foo, get_c, from_b |
| 23 | +foo_result = foo(a=a, b=b, c=get_c(from_b()) |
| 24 | + |
| 25 | +# 3. 3 actions on one line - filtering by arguments, conditionally getting elements (via or), calling a method .value |
| 26 | +result = [(a.value() or A, b or B) for a, b in iterator if a < b] |
| 27 | + |
| 28 | +# 4. 4 actions on one line - from library/variable foo comes bar attribute getting, spam attribute getting, hello attribute getting and calculate_weather call |
| 29 | +result = calculate_weather(foo.bar.spam.hello) |
| 30 | +``` |
| 31 | + |
| 32 | +Good ✅: |
| 33 | +```python |
| 34 | +# 1. make a call to each function in turn |
| 35 | +spam_result = spam(x) |
| 36 | +bar_result = bar(spam_result) |
| 37 | +foo_result = foo(bar_result) |
| 38 | + |
| 39 | +# 2. call the functions one by one, write the result to a variable and use it when calling foo |
| 40 | +from_b_result = from_b() |
| 41 | +c = get_c(from_b_result) |
| 42 | +foo_result = foo(a=a, b=b, c=c) |
| 43 | + |
| 44 | +# 3. sequentially perform actions on the list - first filter, then call the .value method of a, and choose between elements (or) |
| 45 | +filtered_result = ((a, b) for a, b in iterator if a < b) |
| 46 | +intermediate_result = ((a.value(), b) for a, b in filtered_result) |
| 47 | +result = [(a or A, b or B) for a, b in intermediate_result] |
| 48 | + |
| 49 | +# 4 . sequentially read the attributes bar, spam, hello and call the function calculate_weather |
| 50 | +bar = foo.bar |
| 51 | +spam = bar.spam |
| 52 | +hello = spam.hello |
| 53 | +result = calculate_weather(hello) |
| 54 | +``` |
| 55 | + |
| 56 | + |
| 57 | +**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. |
| 58 | + |
| 59 | + |
| 60 | +### Logical blocks |
| 61 | + |
| 62 | +Try to divide the code into logical blocks — this way it will be much easier for the programmer to read and understand the essence. |
| 63 | + |
| 64 | +Bad ❌: |
| 65 | +```python |
| 66 | +def register_model(self, app_label, model): |
| 67 | + model_name = model._meta.model_name |
| 68 | + app_models = self.all_models[app_label] |
| 69 | + if model_name in app_models: |
| 70 | + if (model.__name__ == app_models[model_name].__name__ and |
| 71 | + model.__module__ == app_models[model_name].__module__): |
| 72 | + warnings.warn( |
| 73 | + "Model '%s.%s' was already registered. " |
| 74 | + "Reloading models is not advised as it can lead to inconsistencies, " |
| 75 | + "most notably with related models." % (app_label, model_name), |
| 76 | + RuntimeWarning, stacklevel=2) |
| 77 | + else: |
| 78 | + raise RuntimeError( |
| 79 | + "Conflicting '%s' models in application '%s': %s and %s." % |
| 80 | + (model_name, app_label, app_models[model_name], model)) |
| 81 | + app_models[model_name] = model |
| 82 | + self.do_pending_operations(model) |
| 83 | + self.clear_cache() |
| 84 | +``` |
| 85 | + |
| 86 | +Good ✅: |
| 87 | +```python |
| 88 | +def register_model(self, app_label, model): |
| 89 | + model_name = model._meta.model_name |
| 90 | + app_models = self.all_models[app_label] |
| 91 | + |
| 92 | + if model_name in app_models: |
| 93 | + if ( |
| 94 | + model.__name__ == app_models[model_name].__name__ and |
| 95 | + model.__module__ == app_models[model_name].__module__ |
| 96 | + ): |
| 97 | + warnings.warn( |
| 98 | + "Model '%s.%s' was already registered. " |
| 99 | + "Reloading models is not advised as it can lead to inconsistencies, " |
| 100 | + "most notably with related models." % (app_label, model_name), |
| 101 | + RuntimeWarning, stacklevel=2) |
| 102 | + |
| 103 | + else: |
| 104 | + raise RuntimeError( |
| 105 | + "Conflicting '%s' models in application '%s': %s and %s." % |
| 106 | + (model_name, app_label, app_models[model_name], model)) |
| 107 | + |
| 108 | + app_models[model_name] = model |
| 109 | + |
| 110 | + self.do_pending_operations(model) |
| 111 | + self.clear_cache() |
| 112 | +``` |
| 113 | + |
| 114 | +**Why?** In addition to improving readability, The Zen of Python teaches us how to write idiomatic Python code. One of the statements claims that "sparse is better than dense." Compressed code is harder to read than sparse code. |
| 115 | + |
| 116 | + |
| 117 | +### Sizes of methods, functions, and modules |
| 118 | +The size limit for a method or function is 50 lines. Reaching the size limit indicates that the function (method) is doing too much — so decompose the actions inside the function (method). |
| 119 | +The module size limit is 300 lines. Reaching the size limit indicates that the module has received too much logic — so decompose the module into several ones. |
| 120 | +The line length is 100 characters. |
| 121 | +Imports |
| 122 | +The recommended import method is absolute. |
| 123 | +Bad ❌: |
| 124 | +# spam.py |
| 125 | +from . import foo, bar |
| 126 | +Good ✅: |
| 127 | +# spam.py |
| 128 | +from some.absolute.path import foo, bar |
| 129 | +Why? Because absolute import explicitly defines the location (path) of the module that is being imported. 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 |
| 130 | +Files __init__.py |
| 131 | +Only write imports in __init__.py files. |
| 132 | +Why? Because __init__.py is the last place a programmer will look when they read the code in the future. |
| 133 | +Docstrings |
| 134 | +We recommend adding docstrings to functions, methods, and classes. |
| 135 | +Why? Because the programmer who sees your code for the first time will be able to quickly understand what is happening in it. Code is read much more than it is written. |
| 136 | +About Pull Requests |
| 137 | +Creating Pull Requests |
| 138 | +1 Pull Request = 1 issue |
| 139 | +One Pull Request must solve exactly one issue. |
| 140 | +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. |
| 141 | +Refactoring and Pull Requests |
| 142 | +Refactoring is best done in a separate Pull Request. |
| 143 | +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. |
| 144 | +Pull Request Size |
| 145 | +The resulting PR diff should not exceed +/- 600 changed lines. |
| 146 | +Bad ❌: |
| 147 | + |
| 148 | +Diff 444 + 333 = 777 |
| 149 | + |
| 150 | +Good ✅: |
| 151 | +Diff 222 + 111 = 333 |
| 152 | + |
| 153 | +Why? Because the more PR involves, the more uncontrollable it becomes, and the merge is made "with eyes closed and ears shut." Also, most reviewers will find it difficult to accept a large volume of changes at once. |
| 154 | +About tooling |
| 155 | +Testing (pytest) |
| 156 | +pytest - code testing framework |
| 157 | +Recommended config in pytest.ini: |
| 158 | +[pytest] |
| 159 | +DJANGO_SETTINGS_MODULE = settings.local |
| 160 | +python_files = tests.py test_*.py *_tests.py |
| 161 | + |
| 162 | +Package manager (poetry) |
| 163 | +poetry - dependency manager and package builder |
| 164 | +Code formatting (Black) |
| 165 | +Black - PEP8 code auto-formatter |
| 166 | +Recommended config in pyproject.toml: |
| 167 | +[tool.black] |
| 168 | +line-length = 100 |
| 169 | +target-version = ['py38'] |
| 170 | +exclude = ''' |
| 171 | +( |
| 172 | + \.eggs |
| 173 | + |\.git |
| 174 | + |\.hg |
| 175 | + |\.mypy_cache |
| 176 | + |\.nox |
| 177 | + |\.tox |
| 178 | + |\.venv |
| 179 | + |_build |
| 180 | + |buck-out |
| 181 | + |build |
| 182 | + |dist |
| 183 | +) |
| 184 | +''' |
| 185 | + |
| 186 | + Imports formatting (isort) |
| 187 | +isort - import block auto-formatter |
| 188 | +Recommended config in pyproject.toml: |
| 189 | +[tool.isort] |
| 190 | +line_length = 100 |
| 191 | +sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] |
| 192 | +multi_line_output = 3 |
| 193 | +known_django = "django" |
| 194 | +profile = "django" |
| 195 | +src_paths = "app" |
| 196 | +lines_after_imports = 2 |
| 197 | + |
| 198 | +Linter (flake8) |
| 199 | +flake8 - PEP8 conformance validator |
| 200 | +Recommended config in .flake8: |
| 201 | +[flake8] |
| 202 | +max-line-length = 100 |
| 203 | +max-complexity = 5 |
| 204 | +exclude = .venv,venv,**/migrations/*,snapshots |
| 205 | +per-file-ignores = |
| 206 | + tests/**: S101 |
| 207 | + **/tests/**: S101 |
| 208 | +Type checker (mypy) |
| 209 | +mypy - checker for static typing |
| 210 | +Recommended config mypy.ini: |
| 211 | +[mypy] |
| 212 | +ignore_missing_imports = True |
| 213 | +allow_untyped_globals = True |
| 214 | + |
| 215 | +[mypy-*.migrations.*] |
| 216 | +ignore_errors = True |
| 217 | + |
| 218 | +Pre-commit hooks (pre-commit) |
| 219 | +pre-commit - framework for managing pre-commit hooks |
| 220 | +Recommended config .pre-commit-config.yaml: |
| 221 | +default_language_version: |
| 222 | + python: python3.8 |
| 223 | + |
| 224 | +repos: |
| 225 | + - repo: local |
| 226 | + hooks: |
| 227 | + - id: black |
| 228 | + name: black |
| 229 | + entry: black app |
| 230 | + language: python |
| 231 | + types: [python] |
| 232 | + |
| 233 | + - id: isort |
| 234 | + name: isort |
| 235 | + entry: isort app |
| 236 | + language: python |
| 237 | + types: [python] |
| 238 | + |
| 239 | + - id: flake8 |
| 240 | + name: flake8 |
| 241 | + entry: flake8 server |
| 242 | + language: python |
| 243 | + types: [python] |
| 244 | +Other |
| 245 | +REST API Documentation |
| 246 | +The recommended documentation format is OpenAPI. The schema for OpenAPI should be generated “on the fly” to provide API clients with fresh changes. |
| 247 | +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. |
| 248 | + |
| 249 | + |
0 commit comments