Максим Мазаев. Как прокачать линтер

Максим Мазаев. Как прокачать линтер

В ЦИАН мы постоянно пользуемся линтером для проверки качества кода. Но случаются моменты, когда хочется проверять код на соответствие внутренним соглашениям, про которые линтер ничего не знает. Разработчикам приходится держать все соглашения в голове и самостоятельно проверять качество кода. Это усложняет и затягивает код-ревью. Я расскажу о том, как мы решили эту проблему с помощью Pylint, который имеет мощную систему создания плагинов. Покажу на примерах как плагины могут упростить процесс код-ревью. Рассмотрим процесс написания плагинов и попутно разберемся как работает сам Pylint изнутри.

Transcript

  1. Как прокачать линтер Максим Мазаев

  2. • Программист в ЦИАН • Пишу на Python • Спикер

    PyCon, Moscow Python Conf++ О себе
  3. Как прокачать линтер

  4. • 1 млн уникальных пользователей в сутки • 10x разработка

    за 2014-2019 • 100+ разработчиков • 30 питонистов • Сотни строк нового кода каждый день О ЦИАН
  5. Код-ревью в Циане 1. Одобрение от робота (тесты, линтер etc.)

    2. Одобрение от 2 разработчиков
  6. Проблемы с код-ревью • Есть внутренние договоренности по стилю кода.

    Линтер не покрывает
  7. Проблемы с код-ревью • Есть внутренние договоренности по стилю кода.

    Линтер не покрывает • Линтер не покрывает наши договоренности по стилю.
  8. Проблемы с код-ревью Программист работает вместо линтера

  9. Решение • Инструмент который знает про наши соглашения • И

    проверяет их
  10. Решение

  11. Решение ?

  12. Решение А не написать ли нам свой линтер?

  13. Решение А не написать ли нам свой линтер? Нет, у

    нас уже есть pylint • он удобный • он встроен в наши процессы
  14. Решение Написать плагин для Pylint

  15. Пример №1

  16. 2015 Sergey # TODO: выпилить (не выпилили) 2015 Misha #

    TODO: Убрать после успешного релиза (не убрали) 2017 Vadim # TODO: после эксперимента удалим (не удалили) ¯\_(ツ)_/¯
  17. Пример №1 Проблема: В коде много комментариев «TODO» • Висят

    годами • Не выполняются • Копится техдолг
  18. class TodoIssueChecker(BaseChecker): __implements__ = IRawChecker Будем итерироваться по строкам модуля

    («сырой» чекер) Решение. Пишем свой чекер для Pylint
  19. msgs = { 'С9999': ('Комментарий с TODO без ссылки на

    задачу', 'issue-code-in-todo', 'Длинное описание')} Код и мнемоничеcкое название чекера Пишем свой чекер
  20. С1234 [C]onvention [W]arning Произвольные 4 цифры [E]rror (уникальные в рамках

    pylint) [F]atal [R]efactoring Пишем свой чекер. Код сообщения
  21. msgs = { 'С9999': ('Комментарий с TODO без ссылки на

    задачу', 'issue-code-in-todo', 'Длинное описание')} # pylint: disable=C9999 # pylint: disable=issue-code-in-todo Пишем свой чекер
  22. def process_module(self, node): with node.stream() as stream: for (lineno, line)

    in enumerate(stream): if has_todo_without_issue_code(line): self.add_message( 'issue-code-in-todo', line=lineno ) Пишем свой чекер Выводим предупреждение Ходим по строкам
  23. def register(linter: PyLinter) -> None: linter.register_checker( TodoIssueChecker(linter) ) * Модуль

    чекера должен быть в PYTHONPATH Регистрируем чекер *
  24. $ cat work.py # TODO: Удалю через неделю, честно-честно! $

    pylint work.py --load-plugins todo_checker ... И запускаем!
  25. Тем временем внутри pylint...

  26. 1. Инициализация плагинов 1. Импортируем все модули, где есть плагины

    2. module.register(self) <- Регистрируем + Проверяем, что параметры валидны + Регистрируем опции, сообщения и отчеты
  27. 2. Разбираем пул чекеров Чекер Чекер Чекер Чекер Чекер Чекер

    Raw checker AST checker Token checker
  28. 3. Запуск чекера Запускаем метод process_module, определенный в чекере checker.process_module(module)

  29. $ cat work.py # TODO: Удалю через неделю, честно-честно! $

    pylint work.py --load-plugins todo_checker C: 0, 0: Комментарий с TODO без ссылки на задачу (issue-code-in-todo) Результат
  30. get_offer_by_params( “sale”, True, 859483, ) Пример №2. Keyword-arguments Не всегда

    очевидно что это за аргументы
  31. get_offer_by_params( “sale”, True, 859483, ) Пример №2. Keyword-arguments get_offer_by_params( deal_type=”sale”,

    truncate=True, cian_id=859483, ) Плохо Хорошо
  32. Пример №2. Keyword-arguments Проблема: Сложно написать «сырой» checker

  33. Пример №2. Keyword-arguments Проблема: Сложно написать «сырой» checker Решение: Чекер

    на основе AST
  34. Лирическое отступление про AST

  35. Преобразование кода в AST. Вызов функции get_offer( 112, deal_type=’sale’, offer_type=’flat’,

    ) Call( func=Name(name='get_offer'), args=[Const(value=112)], keywords=[ Keyword( arg='deal_type', value=Const(value='sale') Keyword( arg='offer_type', value=Const(value='flat')) ]))]
  36. Задача. • Найти все ноды Call (вызов функции) • Посчитать

    количество аргументов • Проверить отсутствие позиционных аргументов Call( func=Name(name='get_offer'), args=[Const(value=1298880)], keywords=[ ... ]))]
  37. class NonKeywordArgsChecker(BaseChecker): __implements__ =IAstroidChecker Чекер на основе AST Решение. Пишем

    чекер на основе AST
  38. msgs = { 'С9191': ('Краткое описание', 'keyword-only-args', 'Длинное описание')} Код

    и мнемоничеcкое название чекера Пишем чекер на основе AST
  39. def visit_call(self, node: Call) … visit_<Имя_ноды> вызывается при заходе на

    ноду leave_<Имя_ноды> при уходе с ноды Пишем чекер на основе AST
  40. def visit_call(self, n: Node): if n.args and len(n.args + n.keywords)

    > 2: self.add_message( 'keyword-only-args', node=node ) Пишем чекер на основе AST
  41. def register(linter: PyLinter): linter.register_checker( TodoIssueChecker(linter) ) Регистрируем чекер

  42. $ cat work.py get_offers(1, True, deal_type=”sale”) $ pylint work.py --load-plugins

    non_kwargs_checker ... И запускаем!
  43. Тем временем внутри pylint...

  44. 1. Инициализация плагинов 1. Импортируем все модули, где есть плагины

    2. module.register(self) <- Регистрируем + Проверяем, что параметры валидны + Регистрируем опции, сообщения и отчеты
  45. 2. Разбор модуля на AST Разбор выполняет библиотека Astroid (обертка

    над AST-парсером) github.com/PyCQA/astroid/
  46. Почему Astroid, а не AST (stdlib) 1. Разбирает дерево с

    помощью typed_ast (c поддержкой тайпхинтов из PEP 484)
  47. Почему Astroid, а не AST (stdlib) from module import Entity

    def foo(bar): # type: (Entity) -> None return
  48. Почему Astroid, а не AST (stdlib) 2. Собирает обратно с

    дополнительными плюшками: from foo import * Импортирует и добавит в locals содержимое foo
  49. Почему Astroid, а не AST (stdlib) 3. Transform plugins •

    Поможем Astroid разобраться в динамической природе питона. • Дополняем AST полезной информацией см. pylint-django
  50. 3. Разбираем пул чекеров Чекер Чекер Чекер Чекер Чекер Чекер

    AST checker Raw checker Token checker
  51. 4. Разобрать чекеры по типам нод 1. У каждого чекера

    находим методы visit_<Имя ноды> leave_<Имя ноды>
  52. 4. Разобрать чекеры по типам нод 2. И сохраняем: _visit_methods

    = dict( <Имя ноды>: [checker1, checker2,… ] ) _leave_methods = dict( <Имя ноды>: [checker1, checker2,… ] )
  53. $ cat work.py get_offers(1, True, deal_type=”sale”) $ pylint work.py --load-plugins

    non_kwargs_checker C: 0, 0: Функция с >2 аргументами вызывается с позиционными аргументами (keyword-only-args) Запускаем чекер!
  54. class TestNonKwArgsChecker(CheckerTestCase): CHECKER_CLASS = NonKeywordArgsChecker Чекер, который тестируем A тесты

    написать?
  55. 1. Создаем тестовую AST-ноду node = astroid.extract_node( "get_offers(3, 'magic', 'args')"

    ) А тесты написать?
  56. 2. Проверяем что Pylint бросает предупреждение with self.assertAddsMessages(message): self.checker.visit_call(node) А

    тесты написать?
  57. Еще один тип чекера - TokenChecker Под капотом - tokenize,

    лексический сканер
  58. Лексический анализ кода NAME NAME OP NAME OP NAME OP

    OP NEWLINE def sum(a, b): INDENT NAME NAME OP NAME NEWLINE return a + b DEDENT ENDMARKER
  59. Как Pylint работает с TokenChecker 1. Токенизирует модуль 2. Для

    всех чекеров с интерфейсом ITokenChecker вызывает: process_tokens(tokens)
  60. Применение TokenChecker 1. Проверка правописания 2. Проверка отступов 3. Работа

    со строками. ...
  61. Хочу все знать github.com/PyCQA/Pylint

  62. Выводы Чего добились: • Переложили рутинные проверки на линтер •

    Уменьшили количество отклоненных пулл-реквестов • Повысили качество ревью
  63. Максим Мазаев mazaev.maxim@gmail.com facebook.com/mazaev.maksim