├── .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 | 
245 | ```
246 | Diff 444 + 333 = 777
247 | ```
248 |
249 | Good ✅:
250 |
251 | 
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 |
6 |
7 |
8 |
9 |
10 |