Mon, 24 Feb 2025 15:11:18 +0100
Modified the code style checker such, that the issue category and issue number are separated by a '-' to make up the issue code (e.g E-901).
# -*- coding: utf-8 -*- # Copyright (c) 2021 - 2025 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the checker for functions that can be replaced by use of the pathlib module. """ ##################################################################################### ## adapted from: flake8-use-pathlib v0.3.0 ## ## ## ## Original: Copyright (c) 2021 Rodolphe Pelloux-Prayer ## ## ## ## License: ## ## 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 ## ##################################################################################### import ast import contextlib import copy class PathlibChecker: """ Class implementing a checker for functions that can be replaced by use of the pathlib module. """ Codes = [ ## Replacements for the os module functions "P-101", "P-102", "P-103", "P-104", "P-105", "P-106", "P-107", "P-108", "P-109", "P-110", "P-111", "P-112", "P-113", "P-114", ## Replacements for the os.path module functions "P-201", "P-202", "P-203", "P-204", "P-205", "P-206", "P-207", "P-208", "P-209", "P-210", "P-211", "P-212", "P-213", ## Replacements for some Python standard library functions "P-301", ## Replacements for py.path.local "P-401", ] # map functions to be replaced to error codes Function2Code = { "os.chmod": "P-101", "os.mkdir": "P-102", "os.makedirs": "P-103", "os.rename": "P-104", "os.replace": "P-105", "os.rmdir": "P-106", "os.remove": "P-107", "os.unlink": "P-108", "os.getcwd": "P-109", "os.readlink": "P-110", "os.stat": "P-111", "os.listdir": "P-112", "os.link": "P-113", "os.symlink": "P-114", "os.path.abspath": "P-201", "os.path.exists": "P-202", "os.path.expanduser": "P-203", "os.path.isdir": "P-204", "os.path.isfile": "P-205", "os.path.islink": "P-206", "os.path.isabs": "P-207", "os.path.join": "P-208", "os.path.basename": "P-209", "os.path.dirname": "P-210", "os.path.samefile": "P-211", "os.path.splitext": "P-212", "os.path.relpath": "P-213", "open": "P-301", "py.path.local": "P-401", } def __init__(self, source, filename, tree, 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 tree AST tree of the source code @type ast.Module @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 = tuple(ignored) self.__expected = expected[:] self.__repeat = repeat self.__filename = filename self.__source = source[:] self.__tree = copy.deepcopy(tree) # 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 in self.__ignore or ( 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 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 visitor = PathlibVisitor(self.__checkForReplacement) visitor.visit(self.__tree) def __checkForReplacement(self, node, name): """ Private method to check the given node for the need for a replacement. @param node reference to the AST node to check @type ast.AST @param name resolved name of the node @type str """ with contextlib.suppress(KeyError): errorCode = self.Function2Code[name] self.__error(node.lineno - 1, node.col_offset, errorCode) class PathlibVisitor(ast.NodeVisitor): """ Class to traverse the AST node tree and check for potential issues. """ def __init__(self, checkCallback): """ Constructor @param checkCallback callback function taking a reference to the AST node and the resolved name @type func """ super().__init__() self.__checkCallback = checkCallback self.__importAlias = {} def visit_ImportFrom(self, node): """ Public method handle the ImportFrom AST node. @param node reference to the ImportFrom AST node @type ast.ImportFrom """ for imp in node.names: if imp.asname: self.__importAlias[imp.asname] = f"{node.module}.{imp.name}" else: self.__importAlias[imp.name] = f"{node.module}.{imp.name}" def visit_Import(self, node): """ Public method to handle the Import AST node. @param node reference to the Import AST node @type ast.Import """ for imp in node.names: if imp.asname: self.__importAlias[imp.asname] = imp.name def visit_Call(self, node): """ Public method to handle the Call AST node. @param node reference to the Call AST node @type ast.Call """ nameResolver = NameResolver(self.__importAlias) nameResolver.visit(node.func) self.__checkCallback(node, nameResolver.name()) class NameResolver(ast.NodeVisitor): """ Class to resolve a Name or Attribute node. """ def __init__(self, importAlias): """ Constructor @param importAlias reference to the import aliases dictionary @type dict """ self.__importAlias = importAlias self.__names = [] def name(self): """ Public method to resolve the name. @return resolved name @rtype str """ with contextlib.suppress(KeyError, IndexError): attr = self.__importAlias[self.__names[-1]] self.__names[-1] = attr # do nothing if there is no such name or the names list is empty return ".".join(reversed(self.__names)) def visit_Name(self, node): """ Public method to handle the Name AST node. @param node reference to the Name AST node @type ast.Name """ self.__names.append(node.id) def visit_Attribute(self, node): """ Public method to handle the Attribute AST node. @param node reference to the Attribute AST node @type ast.Attribute """ try: self.__names.append(node.attr) self.__names.append(node.value.id) except AttributeError: self.generic_visit(node)