Thu, 01 Apr 2021 17:23:35 +0200
Code Style Checker
- added icons for the new simplification checks
# -*- coding: utf-8 -*- # Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the checker for simplifying Python code. """ import ast import collections import sys try: from ast import unparse except AttributeError: # Python < 3.9 from .ast_unparse import unparse class SimplifyChecker(object): """ Class implementing a checker for to help simplifying Python code. """ Codes = [ "Y101", ] def __init__(self, source, filename, selected, ignored, expected, repeat): """ Constructor @param source source code to be checked @type list of str @param filename name of the source file @type str @param selected list of selected codes @type list of str @param ignored list of codes to be ignored @type list of str @param expected list of expected codes @type list of str @param repeat flag indicating to report each occurrence of a code @type bool """ self.__select = tuple(selected) self.__ignore = ('',) if selected else tuple(ignored) self.__expected = expected[:] self.__repeat = repeat self.__filename = filename self.__source = source[:] # statistics counters self.counters = {} # collection of detected errors self.errors = [] self.__checkCodes = (code for code in self.Codes if not self.__ignoreCode(code)) def __ignoreCode(self, code): """ Private method to check if the message code should be ignored. @param code message code to check for @type str @return flag indicating to ignore the given code @rtype bool """ return (code.startswith(self.__ignore) and not code.startswith(self.__select)) def __error(self, lineNumber, offset, code, *args): """ Private method to record an issue. @param lineNumber line number of the issue @type int @param offset position within line of the issue @type int @param code message code @type str @param args arguments for the message @type list """ if self.__ignoreCode(code): return if code in self.counters: self.counters[code] += 1 else: self.counters[code] = 1 # Don't care about expected codes if code in self.__expected: return if code and (self.counters[code] == 1 or self.__repeat): # record the issue with one based line number self.errors.append( { "file": self.__filename, "line": lineNumber + 1, "offset": offset, "code": code, "args": args, } ) def __reportInvalidSyntax(self): """ Private method to report a syntax error. """ exc_type, exc = sys.exc_info()[:2] if len(exc.args) > 1: offset = exc.args[1] if len(offset) > 2: offset = offset[1:3] else: offset = (1, 0) self.__error(offset[0] - 1, offset[1] or 0, 'M901', exc_type.__name__, exc.args[0]) def __generateTree(self): """ Private method to generate an AST for our source. @return generated AST @rtype ast.AST """ return ast.parse("".join(self.__source), self.__filename) def run(self): """ Public method to check the given source against functions to be replaced by 'pathlib' equivalents. """ if not self.__filename: # don't do anything, if essential data is missing return if not self.__checkCodes: # don't do anything, if no codes were selected return try: self.__tree = self.__generateTree() except (SyntaxError, TypeError): self.__reportInvalidSyntax() return visitor = SimplifyVisitor(self.__error) visitor.visit(self.__tree) ###################################################################### ## The following code is derived from the flake8-simplify package. ## ## Original License: ## ## MIT License ## ## Copyright (c) 2020 Martin Thoma ## ## Permission is hereby granted, free of charge, to any person obtaining a copy ## of this software and associated documentation files (the "Software"), to ## deal in the Software without restriction, including without limitation the ## rights to use, copy, modify, merge, publish, distribute, sublicense, and/or ## sell copies of the Software, and to permit persons to whom the Software is ## furnished to do so, subject to the following conditions: ## ## The above copyright notice and this permission notice shall be included in ## all copies or substantial portions of the Software. ## ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS ## IN THE SOFTWARE. ###################################################################### class SimplifyVisitor(ast.NodeVisitor): """ Class to traverse the AST node tree and check for code that can be simplified. """ def __init__(self, errorCallback): """ Constructor @param checkCallback callback function taking a reference to the AST node and the resolved name @type func """ super(SimplifyVisitor, self).__init__() self.__error = errorCallback def visit_BoolOp(self, node): """ Public method to process a BoolOp node. @param node reference to the BoolOp node @type ast.BoolOp """ self.__check101(node) ############################################################# ## Methods to check for possible code simplifications below ############################################################# def __getDuplicatedIsinstanceCall(self, node): """ Private method to get a list of isinstance arguments which could be combined. @param node reference to the AST node to be inspected @type ast.BoolOp """ counter = collections.defaultdict(int) for call in node.values: # Ensure this is a call of the built-in isinstance() function. if not isinstance(call, ast.Call) or len(call.args) != 2: continue functionName = call.func.id if functionName != "isinstance": continue arg0Name = unparse(call.args[0]) counter[arg0Name] += 1 return [name for name, count in counter.items() if count > 1] def __check101(self, node): """ Private method to check for duplicate isinstance() calls. @param node reference to the AST node to be checked @type ast.BoolOp """ if not isinstance(node.op, ast.Or): return for variable in self.__getDuplicatedIsinstanceCall(node): self.__error(node.lineno - 1, node.col_offset, "Y101", variable)