Description of the problem:

In Python-2.x, if you had to declare a new-style class , you had to explicitly inherit from object , for example:

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

In Python-3.x, classes are implicitly declared object inheritors, you can write simply:

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

But, both variants of the class declaration will work in 3.x.

Question:

Is it possible, using static code analysis and tools like pylint , to prevent flake8 first, outdated class declaration option?

Please do not judge strictly - this is my first question on ru.SO.

  • 2
    Rarely, users from other lands drop by, and even the renowned ones there. I hope this is a good sign - we are developing, hurray! And welcome, of course :) - user207618

1 answer 1

To begin, check it out manually, in order to get an idea of ​​what exactly PyLint does. An example of a file to check (let tester.py be):

 class MyClass(object): pass class MyAnotherClass(MyClass): pass class MyNormalClass: pass class MyStringInteger(str, int, object): pass 

Now an example of a script that will check the source code:

 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)) 

Result:

 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 

The script opens the module as text, then converts it into an abstract syntax tree (AST) and you can work with this tree already. Pylint works in much the same way, but uses NodeVisitor to travel through a tree. This Visitor calls certain methods when it finds an item. For example, calls visit_classdef when stumbles upon a class definition. Calls visit_callfunc when visit_callfunc into a function call. And so on - constants, assert , mathematical operations and everything that is in a language. In order to attach yourself a fashionable pylint add-on, you need to write such a class (file 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) 

The pylint documentation pylint not shine, so I’m not quite sure about the plugin’s correctness and it’s written “just to work” (alas). In order to write your own you will need the following things (all this is described at https://pylint.readthedocs.io/en/latest/reference_guide/custom_checkers.html ):

  1. The register(linter) function, which will say that you need to use additional checks. In this function, you must call linter.register_checker for all additional checks.
  2. Actually, the classes are verifiers (Visitors). In these classes must be defined:

    • Name (attribute name ) - actually, the name of the check.
    • The priority ( priority attribute) must be less than zero.
    • Message dictionary msgs . This dictionary should have the following structure: msgs = {'message-id': ('displayed-message', 'message-symbol', 'message-help')} . message_id must be unique and not in conflict with existing ones. It consists of an identifier - C, W, E, F, R , meaning Convention - соглашение, Warning - предупреждение, Error - ошибка, Fatal - ужасная ошибка and Refactoring - переделать . Also message_id consists of a 4-digit number. Almost all projects that I have seen use some of their BASE_ID and message number. So message_id limited to 5 characters. What this was done is unthinkable.
    • Actually, the functions themselves that check certain constructions in the code. Already mentioned visit_<node_name> We need to check the definition of a class and its heirs - so do just that. In the function itself, it is necessary to note that pylint is not used by the native ast module, but by the astroid module and, accordingly, all the nodes from astroid .

If all this has been done - the plugin is ready and you can call it something like this:

 pylint --load-plugins "pylint_plugin" tester.py 

At the end there will be a report like this:

 Messages -------- +------------------------+------------+ |message id |occurrences | +========================+============+ |missing-docstring |5 | +------------------------+------------+ |too-few-public-methods |3 | +------------------------+------------+ |bad_classes_with_object |2 | +------------------------+------------+ 

2 bad classes - as described.

  • Thanks for the very detailed description and time spent! I will try to arrange this roll on your instructions. Definitely a bonus from me. - alecxe