CodeStyle

Lev Kovalenko

Почему это важно?

Начинающий программист

Опытный программист

Основная мотивация появления CodeStyle

  • Ускорить понимания и ревью кода.
  • Уменьшить стилистическое разнообразие кода.
  • Уменьшить сложность кода.
  • Запретить использование плохих практик.

Читаемость кода

Есть 2 способа ускорить чтение и понимания кода:

  1. Постоянно наращивать базу знаний разработчиков по тому, как может выглядеть код.
  2. Привести весь код к одному стандарту, задать тот самый code style

Pep8

AutoFormatters

Linters

Linter Category Description
Pylint Logical & Stylistic Checks for errors, tries to enforce a coding standard, looks for code smells
PyFlakes Logical Analyzes programs and detects various errors
Pycodestyle Stylistic Checks against some of the style conventions in PEP8
Pydocstyle Stylistic Checks compliance with Python docstring conventions
Bandit Logical Analyzes code to find common security issues
MyPy Logical Checks for optionally-enforced static types

Flake8 plugins

Анализаторы кода - Radon

  • Цикломатическая сложность
  • Метрика Холстеда
  • Индекс поддерживаемости кода

Mypy проверка типов

From Python…

def fib(n):
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a+b

…to statically typed Python

def fib(n: int) -> Iterator[int]:
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a+b

Pre-commit автоматизация

.pre-commit-config.yaml

default_stages: [commit, push]
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files
    -   id: check-ast
    -   id: check-merge-conflict
-   repo: https://github.com/python/black
    rev: 22.3.0
    hooks:
      - id: black
-   repo: https://github.com/pycqa/isort
    rev: 5.10.1
    hooks:
    -   id: isort
        name: isort (python)
-   repo: https://github.com/PyCQA/flake8
    rev: 4.0.1
    hooks:
    -   id: flake8
        additional_dependencies: [
            yastyleguide==0.1.0
        ]

Выводы

  • Читаемость и качество важны.
  • За качеством нужно следить.
  • Нужно автоматизировать рутинные операции.

Как это работает?

Как работает python?

Abstract syntax tree



AST

import ast

code = "one_plus_two = 1 + 2"
tree = ast.parse(code)
print(ast.dump(tree, indent=4))
Module(
    body=[
        Assign(
            targets=[
                Name(id='one_plus_two', ctx=Store())],
            value=BinOp(
                left=Constant(value=1),
                op=Add(),
                right=Constant(value=2)))],
    type_ignores=[])

AST

AST visit functions

import ast

code = "one_plus_two = 1 + 2"
tree = ast.parse(code)
for node in ast.walk(tree):
        print(node.__class__.__name__)
Module
Assign
Name
BinOp
Store
Constant
Add
Constant
for name, value in ast.iter_fields(tree):
        print(name, value)
body [<ast.Assign object at 0x7fa888f0a6b0>]
type_ignores []
for node in ast.iter_child_nodes(tree):
        print(node.__class__.__name__)
Assign

NodeVisitor

import ast

class BinOpVisitor(ast.NodeVisitor):

    def visit_BinOp(self, node):
        print(f"found BinOp at line: {node.lineno}")
        self.generic_visit(node)

code = "one_plus_two = 1 + 2"
tree = ast.parse(code)
BinOpVisitor().visit(tree)
found BinOp at line: 1

А что если написать свой линтер?

from typing import Any, Generator, Tuple

__version__ = "0.1.0"
ERROR = Tuple[int, int, str, Any]
ERROR_GENERATOR = Generator[ERROR, None, None]

class NoConstantPlugin:
    name = __name__
    version = __version__

    def __init__(self, tree: ast.AST):
        self._tree = tree

    def run(self) -> ERROR_GENERATOR:
        visitor = Visitor()
        visitor.visit(self._tree)

        for line, col, msg in visitor.errors:
            yield line, col, msg, type(self)

Plugin visitor

import ast

class Visitor(ast.NodeVisitor):
    errors: list[tuple[int, int, str]] = []
   
    def visit_Constant(self, node: ast.AST):
        if node.value not in (0, 1, 10, 100):
            self.errors.append((node.lineno, node.col_offset, "NAC100 Not allowed CONSTANT in code"))
        self.generic_visit(node)

Теперь запустим

code = "one_plus_two = 1 + 2"
tree = ast.parse(code)
next(NoConstantPlugin(tree).run())
(1, 19, 'NAC100 Not allowed CONSTANT in code', __main__.NoConstantPlugin)

Добавляем плагин Flake8

setup.py

flake8_entry_point = # ...

setuptools.setup(
    # snip ...
    entry_points={
        flake8_entry_point: [
            'NAC = flake8_example:NoConstantPlugin',
        ],
    },
    # snip ...
)

pyproject.toml

[tool.poetry.plugins."flake8.extension"]
NAC = "flake8_example:NoConstantPlugin"