Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Pylint изнутри. Как он это делает

Pylint изнутри. Как он это делает

Как написать плагин для Pylint и чем это может быть полезно.
Выступление на Moscow Python Conf++ 2018

Avatar for Maxim Mazaev

Maxim Mazaev

October 22, 2018
Tweet

More Decks by Maxim Mazaev

Other Decks in Programming

Transcript

  1. • 1 млн уникальных пользователей в сутки • Ресурс федерального

    уровня • 10x разработка за 2014-2018 • 90+ разработчиков • 30 питонистов 4 О ЦИАН
  2. Про ревью. Как устроено в Циане 5 Следим за качеством

    кода - ревью каждой задачи Критерий удачного ревью: 1. Одобрение от робота. Робот запускает тесты, линтер, проверяет согласованность АПИ 2. Одобрение от 2 разработчиков
  3. Проблема ревью 6 • Ошибки в бизнес-логике • Проблемы со

    стилем кода • PEP-8 - хорошо, но нам недостаточно • Есть внутренние договоренности по оформлению кода
  4. Внутренние соглашения по стилю кода 7 Decline Cian Proposals Набор

    оснований для деклайна пулл-реквеста
  5. Что мешает продуктивному код-ревью? 8 • Линтер не покрывает наши

    договоренности по стилю. • Нужно постоянно держать в голове все правила.
  6. Что мешает продуктивному код-ревью? 9 В результате: • Снижается скорость

    ревью • Люди делают работу линтера вместо анализа логики кода
  7. Решение 11 А не написать ли нам свой линтер? Нет,

    у нас уже есть pylint он удобный и уже встроен в наши процессы
  8. Пример №1 13 Проблема: В коде много комментариев «TODO» •

    Висят годами • Не выполняются • Копится техдолг
  9. 14 2015 Sergey # TODO: выпилить (не выпилили) 2015 Misha

    # TODO: Убрать после успешного релиза (не убрали) 2017 Vadim # TODO: после эксперимента удалим (не удалили) ¯\_(ツ)_/¯
  10. 15 class TodoIssueChecker(BaseChecker): __implements__ = IRawChecker Будем итерироваться по строкам

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

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

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

    на задачу', 'issue-code-in-todo', 'Длинное описание')} # pylint: disable=C9999 # pylint: disable=issue-code-in-todo Пишем свой чекер
  14. 19 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 ) Пишем свой чекер Выводим предупреждение Ходим по строкам
  15. 20 def register(linter: PyLinter) -> None: linter.register_checker( TodoIssueChecker(linter) ) *

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

    $ pylint work.py --load-plugins todo_checker ... И запускаем!
  17. 23 Фаза 1. Инициализация плагинов 1. Импортируем все модули, где

    есть плагины 2. module.register(self) <- Регистрируем + Проверяем, что параметры валидны + Регистрируем опции, сообщения и отчеты
  18. 26 $ cat work.py # TODO: Удалю через неделю, честно-честно!

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

    deal_type=”sale”, truncate=True, cian_id=859483, ) Плохо Хорошо Если функция принимает >2 аргументов, то вызываем ее только с именованными аргументами
  20. Преобразование кода в AST. Вызов функции 31 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')) ]))]
  21. Задача. 32 • Найти все ноды Call (вызов функции) •

    Посчитать количество аргументов • Проверить отсутствие позиционных аргументов Call( func=Name(name='get_offer'), args=[Const(value=1298880)], keywords=[ ... ]))]
  22. 34 msgs = { 'С9191': ('Краткое описание', 'keyword-only-args', 'Длинное описание')}

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

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

    > 2: self.add_message( 'keyword-only-args', node=node ) Пишем чекер на основе AST
  25. 38 $ cat work.py get_offers(1, True, deal_type=”sale”) $ pylint work.py

    --load-plugins non_kwargs_checker ... И запускаем!
  26. 40 Фаза 1. Инициализация плагинов 1. Импортируем все модули, где

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

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

    Entity def foo(bar): # type: (Entity) -> None return Импорты для тайпхинтов больше не unused
  29. 44 Почему Astroid, а не AST (stdlib) 2. Собирает обратно

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

    • Поможем Astroid разобраться в динамической природе питона. • Дополняем AST полезной информацией см. pylint-django
  31. 47 Фаза 4. Разобрать чекеры по типам нод 1. У

    каждого чекера находим методы visit_<Имя ноды> leave_<Имя ноды>
  32. 48 Фаза 4. Разобрать чекеры по типам нод 2. И

    сохраняем: _visit_methods = dict( <Имя ноды>: [checker1, checker2 … checkerN] ) _leave_methods = dict( <Имя ноды>: [checker1, checker2 … checkerN] )
  33. 49 $ 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) Запускаем чекер!
  34. 51 1. Создаем тестовую AST-ноду node = astroid.extract_node( "get_offers(3, 'magic',

    'args')" ) 2. Проверяем что Pylint бросает предупреждение with self.assertAddsMessages(message): self.checker.visit_call(node) А тесты написать?
  35. 53 Лексический анализ кода NAME NAME OP NAME OP NAME

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

    Для всех чекеров с интерфейсом ITokenChecker: Вызывает process_tokens(tokens)
  37. Выводы 57 Чего добились: • Переложили рутинные проверки на линтер

    • Уменьшили количество отклоненных пулл- реквестов • Повысили качество ревью