Страницы

Поиск по вопросам

пятница, 29 ноября 2019 г.

Запрещать явно наследоваться от object

#python #python_3x #статический_анализ #pylint


Описание проблемы:

В Python-2.x, если нужно было объявить new-style класс, приходилось явно наследоваться
от object, например:

class A(object):
    def __init__(self, prop):
        self.prop = prop 


В Python-3.x классы неявно объявляются наследниками object, можно написать просто:

class A:
    def __init__(self, prop):
        self.prop = prop 


Но, оба варианта объявления класса будут работать в 3.x.

Вопрос:

Можно ли, с помощью статического анализа кода и инструментов вроде pylint, flake8
запретить первый, устаревший вариант объявления класса?

Пожалуйста, не судите строго - это мой первый вопрос на ru.SO.
    


Ответы

Ответ 1



Для начала проверим это вручную, дабы примерно понять, что именно делает PyLint. Пример файла для проверки (пускай будет tester.py): class MyClass(object): pass class MyAnotherClass(MyClass): pass class MyNormalClass: pass class MyStringInteger(str, int, object): pass Теперь пример скрипта, который и будет заниматься проверкой исходника: import ast MODULE_PATH = "tester.py" with open(MODULE_PATH, "r", encoding="utf8") as f: MODULE_AS_STRING = f.read() root = ast.parse(source=MODULE_AS_STRING) nodes_gen = ast.walk(root) for node in nodes_gen: if type(node) is ast.ClassDef: print("Yo! Found class definition. Classname: ", node.name) bases_list = [] for bc in node.bases: bases_list.append(bc.id) print("Base classes:", ', '.join(bases_list)) Результат: Yo! Found class definition. Classname: MyClass Base classes: object Yo! Found class definition. Classname: MyAnotherClass Base classes: MyClass Yo! Found class definition. Classname: MyNormalClass Base classes: Yo! Found class definition. Classname: MyStringInteger Base classes: str, int, object Скрипт открывает модуль как текст, затем преобразовывает в абстрактное синтаксическое дерево (AST) и уже с этим деревом можно работать. Pylint работает примерно таким же образом, однако использует NodeVisitor для путешествия по дереву. Этот Visitor вызывает определнные методы, когда находит элемент. Например, вызывает visit_classdef, когда натыкается на определение класса. Вызывает visit_callfunc, когда натыкается на вызов функции. И так далее - константы, assert, математические операции и все, что только есть в языке. Для того, чтобы приделать себе модное pylint дополнение нужно написать такой класс (файл pylint_plugin.py): from pylint.checkers import BaseChecker, utils from pylint.interfaces import IAstroidChecker BASE_ID = 56 def register(linter): print("Registering pylint plugin...") linter.register_checker(MyClassChecker(linter)) print("Yo! Registered!") class MyClassChecker(BaseChecker): __implements__ = IAstroidChecker MESSAGE_ID = "bad_classes_with_object" msgs = { 'W%d01' % BASE_ID: ("%s classname has some problems with object...", MESSAGE_ID, "Please rewrite") } # Обязательно необходимо определить этот аттрибут name = "object_inheritance_checker" @utils.check_messages(MESSAGE_ID) def visit_classdef(self, node): for bc in node.bases: # Внимание - это уже класс astroid.Name if bc.name == 'object': print("MY CLASSES PRINT! YO!", node) self.add_message(msg_id=self.MESSAGE_ID, node=node, args=node.name) Документация pylint не блистает, поэтому я не совсем уверен в правильности плагина и написан он "лишь бы работало" (увы). Для того, чтобы написать свой собственный понадобятся следующие вещи (все это описано по адресу https://pylint.readthedocs.io/en/latest/reference_guide/custom_checkers.html): Функция register(linter), которая скажет, что необходимо использовать дополнительные проверки. В этой функции необходимо вызывать linter.register_checker для всех дополнительных проверок. Собственно, классы проверяльщики (Visitor'ы). В этих классах обязательно должны быть определены: Имя (аттрибут name) - собственно, имя проверки. Приоритет (аттрибут priority) должен быть меньше нуля. Словарь сообщений msgs. Этот словарь должен иметь такую структуру: msgs = {'message-id': ('displayed-message', 'message-symbol', 'message-help')}. message_id должен быть уникален и не конфликтовать с существующими. Он состоит из идентификатора - C, W, E, F, R, означающих Convention - соглашение, Warning - предупреждение, Error - ошибка, Fatal - ужасная ошибка and Refactoring - переделать. Также message_id состоит из 4-хзначного номера. Почти все проекты, что я видел используют какой-нибудь свой BASE_ID и номер сообщения. Т.о. message_id ограничен 5 символами. К чему это было сделано - немыслимо. Собственно, сами функции, проверяющие определенные конструкции в коде. Уже упоминавшиеся visit_ Нам нужно проверять определение класса и его наследников - так сделаем же именно это. В самой функции необходимо обратить внимание, что pylint использует не родной модуль ast, а модуль astroid и соответственно все узлы именно из astroid. Если все это было сделано - плагин готов и его можно вызвать как-то так: pylint --load-plugins "pylint_plugin" tester.py В конце будет отчет вот такого вида: Messages -------- +------------------------+------------+ |message id |occurrences | +========================+============+ |missing-docstring |5 | +------------------------+------------+ |too-few-public-methods |3 | +------------------------+------------+ |bad_classes_with_object |2 | +------------------------+------------+ 2 плохих класса - как и было описано.

Комментариев нет:

Отправить комментарий