Wed, 31 Mar 2021 19:51:41 +0200
Code Style Checker
- started to implement a checker for potential code simplifications
--- a/eric6.epj Tue Mar 30 19:59:24 2021 +0200 +++ b/eric6.epj Wed Mar 31 19:51:41 2021 +0200 @@ -48,7 +48,7 @@ "CopyrightMinFileSize": 0, "DocstringType": "eric", "EnabledCheckerCategories": "C, D, E, M, N, W", - "ExcludeFiles": "*/ThirdParty/*, */coverage/*, */Ui_*.py, */Examples/*, */*_rc.py,*/pycodestyle.py,*/pyflakes/checker.py,*/mccabe.py,*/eradicate.py", + "ExcludeFiles": "*/ThirdParty/*, */coverage/*, */Ui_*.py, */Examples/*, */*_rc.py,*/pycodestyle.py,*/pyflakes/checker.py,*/mccabe.py,*/eradicate.py,*/ast_unparse.py", "ExcludeMessages": "C101,E265,E266,E305,E402,M201,M301,M302,M303,M304,M305,M306,M307,M308,M311,M312,M313,M314,M315,M321,M701,M702,M811,M834,N802,N803,N807,N808,N821,W293,W504", "FixCodes": "", "FixIssues": false, @@ -2573,7 +2573,11 @@ "eric6/Plugins/CheckerPlugins/CodeStyleChecker/PathLib/PathlibChecker.py", "eric6/Plugins/CheckerPlugins/CodeStyleChecker/PathLib/translations.py", "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/eradicate.py", - "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/mccabe.py" + "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/mccabe.py", + "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/__init__.py", + "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/ast_unparse.py", + "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyChecker.py", + "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/translations.py" ], "SPELLEXCLUDES": "Dictionaries/excludes.dic", "SPELLLANGUAGE": "en_US",
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Tue Mar 30 19:59:24 2021 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Wed Mar 31 19:51:41 2021 +0200 @@ -23,6 +23,7 @@ from Complexity.ComplexityChecker import ComplexityChecker from Security.SecurityChecker import SecurityChecker from PathLib.PathlibChecker import PathlibChecker +from Simplify.SimplifyChecker import SimplifyChecker def initService(): @@ -427,6 +428,7 @@ stats.update(annotationsChecker.counters) errors += annotationsChecker.errors + # check for security issues securityChecker = SecurityChecker( source, filename, select, ignore, [], repeatMessages, securityArgs) @@ -434,11 +436,19 @@ stats.update(securityChecker.counters) errors += securityChecker.errors + # check for pathlib usage pathlibChecker = PathlibChecker( source, filename, select, ignore, [], repeatMessages) pathlibChecker.run() stats.update(pathlibChecker.counters) errors += pathlibChecker.errors + + # check for code simplifications + simplifyChecker =SimplifyChecker( + source, filename, select, ignore, [], repeatMessages) + simplifyChecker.run() + stats.update(simplifyChecker.counters) + errors += simplifyChecker.errors errorsDict = {} for error in errors:
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Tue Mar 30 19:59:24 2021 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Wed Mar 31 19:51:41 2021 +0200 @@ -91,6 +91,9 @@ "W": QCoreApplication.translate( "CheckerCategories", "Warnings"), + "Y": QCoreApplication.translate( + "CheckerCategories", + "Simplify Code"), } noResults = 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyChecker.py Wed Mar 31 19:51:41 2021 +0200 @@ -0,0 +1,249 @@ +# -*- 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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/__init__.py Wed Mar 31 19:51:41 2021 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing a checker to help simplifying the code. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/ast_unparse.py Wed Mar 31 19:51:41 2021 +0200 @@ -0,0 +1,959 @@ +""" + ast_unparse + ~~~~~~~~~~~ + + This module is a backport of the unparse function of the Python 3.9 + ast module. + + Original ast module code is + :copyright: Copyright 2008 by Armin Ronacher. + :license: Python License. +""" + +import ast +import sys +from enum import IntEnum, auto +from contextlib import contextmanager, nullcontext + +# Large float and imaginary literals get turned into infinities in the AST. +# We unparse those infinities to INFSTR. +_INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1) + +class _Precedence(IntEnum): + """Precedence table that originated from python grammar.""" + + TUPLE = auto() + YIELD = auto() # 'yield', 'yield from' + TEST = auto() # 'if'-'else', 'lambda' + OR = auto() # 'or' + AND = auto() # 'and' + NOT = auto() # 'not' + CMP = auto() # '<', '>', '==', '>=', '<=', '!=', + # 'in', 'not in', 'is', 'is not' + EXPR = auto() + BOR = EXPR # '|' + BXOR = auto() # '^' + BAND = auto() # '&' + SHIFT = auto() # '<<', '>>' + ARITH = auto() # '+', '-' + TERM = auto() # '*', '@', '/', '%', '//' + FACTOR = auto() # unary '+', '-', '~' + POWER = auto() # '**' + AWAIT = auto() # 'await' + ATOM = auto() + + def next(self): + try: + return self.__class__(self + 1) + except ValueError: + return self + + +_SINGLE_QUOTES = ("'", '"') +_MULTI_QUOTES = ('"""', "'''") +_ALL_QUOTES = (*_SINGLE_QUOTES, *_MULTI_QUOTES) + +class _Unparser(ast.NodeVisitor): + """Methods in this class recursively traverse an AST and + output source code for the abstract syntax; original formatting + is disregarded.""" + + def __init__(self, *, _avoid_backslashes=False): + self._source = [] + self._buffer = [] + self._precedences = {} + self._type_ignores = {} + self._indent = 0 + self._avoid_backslashes = _avoid_backslashes + + def interleave(self, inter, f, seq): + """Call f on each item in seq, calling inter() in between.""" + seq = iter(seq) + try: + f(next(seq)) + except StopIteration: + pass + else: + for x in seq: + inter() + f(x) + + def items_view(self, traverser, items): + """Traverse and separate the given *items* with a comma and append it to + the buffer. If *items* is a single item sequence, a trailing comma + will be added.""" + if len(items) == 1: + traverser(items[0]) + self.write(",") + else: + self.interleave(lambda: self.write(", "), traverser, items) + + def maybe_newline(self): + """Adds a newline if it isn't the start of generated source""" + if self._source: + self.write("\n") + + def fill(self, text=""): + """Indent a piece of text and append it, according to the current + indentation level""" + self.maybe_newline() + self.write(" " * self._indent + text) + + def write(self, text): + """Append a piece of text""" + self._source.append(text) + + def buffer_writer(self, text): + self._buffer.append(text) + + @property + def buffer(self): + value = "".join(self._buffer) + self._buffer.clear() + return value + + @contextmanager + def block(self, *, extra = None): + """A context manager for preparing the source for blocks. It adds + the character':', increases the indentation on enter and decreases + the indentation on exit. If *extra* is given, it will be directly + appended after the colon character. + """ + self.write(":") + if extra: + self.write(extra) + self._indent += 1 + yield + self._indent -= 1 + + @contextmanager + def delimit(self, start, end): + """A context manager for preparing the source for expressions. It adds + *start* to the buffer and enters, after exit it adds *end*.""" + + self.write(start) + yield + self.write(end) + + def delimit_if(self, start, end, condition): + if condition: + return self.delimit(start, end) + else: + return nullcontext() + + def require_parens(self, precedence, node): + """Shortcut to adding precedence related parens""" + return self.delimit_if("(", ")", self.get_precedence(node) > precedence) + + def get_precedence(self, node): + return self._precedences.get(node, _Precedence.TEST) + + def set_precedence(self, precedence, *nodes): + for node in nodes: + self._precedences[node] = precedence + + def get_raw_docstring(self, node): + """If a docstring node is found in the body of the *node* parameter, + return that docstring node, None otherwise. + + Logic mirrored from ``_PyAST_GetDocString``.""" + if not isinstance( + node, (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, + ast.Module) + ) or len(node.body) < 1: + return None + node = node.body[0] + if not isinstance(node, ast.Expr): + return None + node = node.value + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node + + def get_type_comment(self, node): + comment = self._type_ignores.get(node.lineno) or node.type_comment + if comment is not None: + return f" # type: {comment}" + + def traverse(self, node): + if isinstance(node, list): + for item in node: + self.traverse(item) + else: + super().visit(node) + + def visit(self, node): + """Outputs a source code string that, if converted back to an ast + (using ast.parse) will generate an AST equivalent to *node*""" + self._source = [] + self.traverse(node) + return "".join(self._source) + + def _write_docstring_and_traverse_body(self, node): + if (docstring := self.get_raw_docstring(node)): + self._write_docstring(docstring) + self.traverse(node.body[1:]) + else: + self.traverse(node.body) + + def visit_Module(self, node): + self._type_ignores = { + ignore.lineno: f"ignore{ignore.tag}" + for ignore in node.type_ignores + } + self._write_docstring_and_traverse_body(node) + self._type_ignores.clear() + + def visit_FunctionType(self, node): + with self.delimit("(", ")"): + self.interleave( + lambda: self.write(", "), self.traverse, node.argtypes + ) + + self.write(" -> ") + self.traverse(node.returns) + + def visit_Expr(self, node): + self.fill() + self.set_precedence(_Precedence.YIELD, node.value) + self.traverse(node.value) + + def visit_NamedExpr(self, node): + with self.require_parens(_Precedence.TUPLE, node): + self.set_precedence(_Precedence.ATOM, node.target, node.value) + self.traverse(node.target) + self.write(" := ") + self.traverse(node.value) + + def visit_Import(self, node): + self.fill("import ") + self.interleave(lambda: self.write(", "), self.traverse, node.names) + + def visit_ImportFrom(self, node): + self.fill("from ") + self.write("." * node.level) + if node.module: + self.write(node.module) + self.write(" import ") + self.interleave(lambda: self.write(", "), self.traverse, node.names) + + def visit_Assign(self, node): + self.fill() + for target in node.targets: + self.traverse(target) + self.write(" = ") + self.traverse(node.value) + if type_comment := self.get_type_comment(node): + self.write(type_comment) + + def visit_AugAssign(self, node): + self.fill() + self.traverse(node.target) + self.write(" " + self.binop[node.op.__class__.__name__] + "= ") + self.traverse(node.value) + + def visit_AnnAssign(self, node): + self.fill() + with self.delimit_if("(", ")", not node.simple and isinstance(node.target, ast.Name)): + self.traverse(node.target) + self.write(": ") + self.traverse(node.annotation) + if node.value: + self.write(" = ") + self.traverse(node.value) + + def visit_Return(self, node): + self.fill("return") + if node.value: + self.write(" ") + self.traverse(node.value) + + def visit_Pass(self, node): + self.fill("pass") + + def visit_Break(self, node): + self.fill("break") + + def visit_Continue(self, node): + self.fill("continue") + + def visit_Delete(self, node): + self.fill("del ") + self.interleave(lambda: self.write(", "), self.traverse, node.targets) + + def visit_Assert(self, node): + self.fill("assert ") + self.traverse(node.test) + if node.msg: + self.write(", ") + self.traverse(node.msg) + + def visit_Global(self, node): + self.fill("global ") + self.interleave(lambda: self.write(", "), self.write, node.names) + + def visit_Nonlocal(self, node): + self.fill("nonlocal ") + self.interleave(lambda: self.write(", "), self.write, node.names) + + def visit_Await(self, node): + with self.require_parens(_Precedence.AWAIT, node): + self.write("await") + if node.value: + self.write(" ") + self.set_precedence(_Precedence.ATOM, node.value) + self.traverse(node.value) + + def visit_Yield(self, node): + with self.require_parens(_Precedence.YIELD, node): + self.write("yield") + if node.value: + self.write(" ") + self.set_precedence(_Precedence.ATOM, node.value) + self.traverse(node.value) + + def visit_YieldFrom(self, node): + with self.require_parens(_Precedence.YIELD, node): + self.write("yield from ") + if not node.value: + raise ValueError("Node can't be used without a value attribute.") + self.set_precedence(_Precedence.ATOM, node.value) + self.traverse(node.value) + + def visit_Raise(self, node): + self.fill("raise") + if not node.exc: + if node.cause: + raise ValueError("Node can't use cause without an exception.") + return + self.write(" ") + self.traverse(node.exc) + if node.cause: + self.write(" from ") + self.traverse(node.cause) + + def visit_Try(self, node): + self.fill("try") + with self.block(): + self.traverse(node.body) + for ex in node.handlers: + self.traverse(ex) + if node.orelse: + self.fill("else") + with self.block(): + self.traverse(node.orelse) + if node.finalbody: + self.fill("finally") + with self.block(): + self.traverse(node.finalbody) + + def visit_ExceptHandler(self, node): + self.fill("except") + if node.type: + self.write(" ") + self.traverse(node.type) + if node.name: + self.write(" as ") + self.write(node.name) + with self.block(): + self.traverse(node.body) + + def visit_ClassDef(self, node): + self.maybe_newline() + for deco in node.decorator_list: + self.fill("@") + self.traverse(deco) + self.fill("class " + node.name) + with self.delimit_if("(", ")", condition = node.bases or node.keywords): + comma = False + for e in node.bases: + if comma: + self.write(", ") + else: + comma = True + self.traverse(e) + for e in node.keywords: + if comma: + self.write(", ") + else: + comma = True + self.traverse(e) + + with self.block(): + self._write_docstring_and_traverse_body(node) + + def visit_FunctionDef(self, node): + self._function_helper(node, "def") + + def visit_AsyncFunctionDef(self, node): + self._function_helper(node, "async def") + + def _function_helper(self, node, fill_suffix): + self.maybe_newline() + for deco in node.decorator_list: + self.fill("@") + self.traverse(deco) + def_str = fill_suffix + " " + node.name + self.fill(def_str) + with self.delimit("(", ")"): + self.traverse(node.args) + if node.returns: + self.write(" -> ") + self.traverse(node.returns) + with self.block(extra=self.get_type_comment(node)): + self._write_docstring_and_traverse_body(node) + + def visit_For(self, node): + self._for_helper("for ", node) + + def visit_AsyncFor(self, node): + self._for_helper("async for ", node) + + def _for_helper(self, fill, node): + self.fill(fill) + self.traverse(node.target) + self.write(" in ") + self.traverse(node.iter) + with self.block(extra=self.get_type_comment(node)): + self.traverse(node.body) + if node.orelse: + self.fill("else") + with self.block(): + self.traverse(node.orelse) + + def visit_If(self, node): + self.fill("if ") + self.traverse(node.test) + with self.block(): + self.traverse(node.body) + # collapse nested ifs into equivalent elifs. + while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If): + node = node.orelse[0] + self.fill("elif ") + self.traverse(node.test) + with self.block(): + self.traverse(node.body) + # final else + if node.orelse: + self.fill("else") + with self.block(): + self.traverse(node.orelse) + + def visit_While(self, node): + self.fill("while ") + self.traverse(node.test) + with self.block(): + self.traverse(node.body) + if node.orelse: + self.fill("else") + with self.block(): + self.traverse(node.orelse) + + def visit_With(self, node): + self.fill("with ") + self.interleave(lambda: self.write(", "), self.traverse, node.items) + with self.block(extra=self.get_type_comment(node)): + self.traverse(node.body) + + def visit_AsyncWith(self, node): + self.fill("async with ") + self.interleave(lambda: self.write(", "), self.traverse, node.items) + with self.block(extra=self.get_type_comment(node)): + self.traverse(node.body) + + def _str_literal_helper( + self, string, *, quote_types=_ALL_QUOTES, escape_special_whitespace=False + ): + """Helper for writing string literals, minimizing escapes. + Returns the tuple (string literal to write, possible quote types). + """ + def escape_char(c): + # \n and \t are non-printable, but we only escape them if + # escape_special_whitespace is True + if not escape_special_whitespace and c in "\n\t": + return c + # Always escape backslashes and other non-printable characters + if c == "\\" or not c.isprintable(): + return c.encode("unicode_escape").decode("ascii") + return c + + escaped_string = "".join(map(escape_char, string)) + possible_quotes = quote_types + if "\n" in escaped_string: + possible_quotes = [q for q in possible_quotes if q in _MULTI_QUOTES] + possible_quotes = [q for q in possible_quotes if q not in escaped_string] + if not possible_quotes: + # If there aren't any possible_quotes, fallback to using repr + # on the original string. Try to use a quote from quote_types, + # e.g., so that we use triple quotes for docstrings. + string = repr(string) + quote = next((q for q in quote_types if string[0] in q), string[0]) + return string[1:-1], [quote] + if escaped_string: + # Sort so that we prefer '''"''' over """\"""" + possible_quotes.sort(key=lambda q: q[0] == escaped_string[-1]) + # If we're using triple quotes and we'd need to escape a final + # quote, escape it + if possible_quotes[0][0] == escaped_string[-1]: + assert len(possible_quotes[0]) == 3 + escaped_string = escaped_string[:-1] + "\\" + escaped_string[-1] + return escaped_string, possible_quotes + + def _write_str_avoiding_backslashes(self, string, *, quote_types=_ALL_QUOTES): + """Write string literal value with a best effort attempt to avoid backslashes.""" + string, quote_types = self._str_literal_helper(string, quote_types=quote_types) + quote_type = quote_types[0] + self.write(f"{quote_type}{string}{quote_type}") + + def visit_JoinedStr(self, node): + self.write("f") + if self._avoid_backslashes: + self._fstring_JoinedStr(node, self.buffer_writer) + self._write_str_avoiding_backslashes(self.buffer) + return + + # If we don't need to avoid backslashes globally (i.e., we only need + # to avoid them inside FormattedValues), it's cosmetically preferred + # to use escaped whitespace. That is, it's preferred to use backslashes + # for cases like: f"{x}\n". To accomplish this, we keep track of what + # in our buffer corresponds to FormattedValues and what corresponds to + # Constant parts of the f-string, and allow escapes accordingly. + buffer = [] + for value in node.values: + meth = getattr(self, "_fstring_" + type(value).__name__) + meth(value, self.buffer_writer) + buffer.append((self.buffer, isinstance(value, ast.Constant))) + new_buffer = [] + quote_types = _ALL_QUOTES + for value, is_constant in buffer: + # Repeatedly narrow down the list of possible quote_types + value, quote_types = self._str_literal_helper( + value, quote_types=quote_types, + escape_special_whitespace=is_constant + ) + new_buffer.append(value) + value = "".join(new_buffer) + quote_type = quote_types[0] + self.write(f"{quote_type}{value}{quote_type}") + + def visit_FormattedValue(self, node): + self.write("f") + self._fstring_FormattedValue(node, self.buffer_writer) + self._write_str_avoiding_backslashes(self.buffer) + + def _fstring_JoinedStr(self, node, write): + for value in node.values: + meth = getattr(self, "_fstring_" + type(value).__name__) + meth(value, write) + + def _fstring_Constant(self, node, write): + if not isinstance(node.value, str): + raise ValueError("Constants inside JoinedStr should be a string.") + value = node.value.replace("{", "{{").replace("}", "}}") + write(value) + + def _fstring_FormattedValue(self, node, write): + write("{") + unparser = type(self)(_avoid_backslashes=True) + unparser.set_precedence(_Precedence.TEST.next(), node.value) + expr = unparser.visit(node.value) + if expr.startswith("{"): + write(" ") # Separate pair of opening brackets as "{ {" + if "\\" in expr: + raise ValueError("Unable to avoid backslash in f-string expression part") + write(expr) + if node.conversion != -1: + conversion = chr(node.conversion) + if conversion not in "sra": + raise ValueError("Unknown f-string conversion.") + write(f"!{conversion}") + if node.format_spec: + write(":") + meth = getattr(self, "_fstring_" + type(node.format_spec).__name__) + meth(node.format_spec, write) + write("}") + + def visit_Name(self, node): + self.write(node.id) + + def _write_docstring(self, node): + self.fill() + if node.kind == "u": + self.write("u") + self._write_str_avoiding_backslashes(node.value, quote_types=_MULTI_QUOTES) + + def _write_constant(self, value): + if isinstance(value, (float, complex)): + # Substitute overflowing decimal literal for AST infinities, + # and inf - inf for NaNs. + self.write( + repr(value) + .replace("inf", _INFSTR) + .replace("nan", f"({_INFSTR}-{_INFSTR})") + ) + elif self._avoid_backslashes and isinstance(value, str): + self._write_str_avoiding_backslashes(value) + else: + self.write(repr(value)) + + def visit_Constant(self, node): + value = node.value + if isinstance(value, tuple): + with self.delimit("(", ")"): + self.items_view(self._write_constant, value) + elif value is ...: + self.write("...") + else: + if node.kind == "u": + self.write("u") + self._write_constant(node.value) + + def visit_List(self, node): + with self.delimit("[", "]"): + self.interleave(lambda: self.write(", "), self.traverse, node.elts) + + def visit_ListComp(self, node): + with self.delimit("[", "]"): + self.traverse(node.elt) + for gen in node.generators: + self.traverse(gen) + + def visit_GeneratorExp(self, node): + with self.delimit("(", ")"): + self.traverse(node.elt) + for gen in node.generators: + self.traverse(gen) + + def visit_SetComp(self, node): + with self.delimit("{", "}"): + self.traverse(node.elt) + for gen in node.generators: + self.traverse(gen) + + def visit_DictComp(self, node): + with self.delimit("{", "}"): + self.traverse(node.key) + self.write(": ") + self.traverse(node.value) + for gen in node.generators: + self.traverse(gen) + + def visit_comprehension(self, node): + if node.is_async: + self.write(" async for ") + else: + self.write(" for ") + self.set_precedence(_Precedence.TUPLE, node.target) + self.traverse(node.target) + self.write(" in ") + self.set_precedence(_Precedence.TEST.next(), node.iter, *node.ifs) + self.traverse(node.iter) + for if_clause in node.ifs: + self.write(" if ") + self.traverse(if_clause) + + def visit_IfExp(self, node): + with self.require_parens(_Precedence.TEST, node): + self.set_precedence(_Precedence.TEST.next(), node.body, node.test) + self.traverse(node.body) + self.write(" if ") + self.traverse(node.test) + self.write(" else ") + self.set_precedence(_Precedence.TEST, node.orelse) + self.traverse(node.orelse) + + def visit_Set(self, node): + if node.elts: + with self.delimit("{", "}"): + self.interleave(lambda: self.write(", "), self.traverse, node.elts) + else: + # `{}` would be interpreted as a dictionary literal, and + # `set` might be shadowed. Thus: + self.write('{*()}') + + def visit_Dict(self, node): + def write_key_value_pair(k, v): + self.traverse(k) + self.write(": ") + self.traverse(v) + + def write_item(item): + k, v = item + if k is None: + # for dictionary unpacking operator in dicts {**{'y': 2}} + # see PEP 448 for details + self.write("**") + self.set_precedence(_Precedence.EXPR, v) + self.traverse(v) + else: + write_key_value_pair(k, v) + + with self.delimit("{", "}"): + self.interleave( + lambda: self.write(", "), write_item, zip(node.keys, node.values) + ) + + def visit_Tuple(self, node): + with self.delimit("(", ")"): + self.items_view(self.traverse, node.elts) + + unop = {"Invert": "~", "Not": "not", "UAdd": "+", "USub": "-"} + unop_precedence = { + "not": _Precedence.NOT, + "~": _Precedence.FACTOR, + "+": _Precedence.FACTOR, + "-": _Precedence.FACTOR, + } + + def visit_UnaryOp(self, node): + operator = self.unop[node.op.__class__.__name__] + operator_precedence = self.unop_precedence[operator] + with self.require_parens(operator_precedence, node): + self.write(operator) + # factor prefixes (+, -, ~) shouldn't be seperated + # from the value they belong, (e.g: +1 instead of + 1) + if operator_precedence is not _Precedence.FACTOR: + self.write(" ") + self.set_precedence(operator_precedence, node.operand) + self.traverse(node.operand) + + binop = { + "Add": "+", + "Sub": "-", + "Mult": "*", + "MatMult": "@", + "Div": "/", + "Mod": "%", + "LShift": "<<", + "RShift": ">>", + "BitOr": "|", + "BitXor": "^", + "BitAnd": "&", + "FloorDiv": "//", + "Pow": "**", + } + + binop_precedence = { + "+": _Precedence.ARITH, + "-": _Precedence.ARITH, + "*": _Precedence.TERM, + "@": _Precedence.TERM, + "/": _Precedence.TERM, + "%": _Precedence.TERM, + "<<": _Precedence.SHIFT, + ">>": _Precedence.SHIFT, + "|": _Precedence.BOR, + "^": _Precedence.BXOR, + "&": _Precedence.BAND, + "//": _Precedence.TERM, + "**": _Precedence.POWER, + } + + binop_rassoc = frozenset(("**",)) + def visit_BinOp(self, node): + operator = self.binop[node.op.__class__.__name__] + operator_precedence = self.binop_precedence[operator] + with self.require_parens(operator_precedence, node): + if operator in self.binop_rassoc: + left_precedence = operator_precedence.next() + right_precedence = operator_precedence + else: + left_precedence = operator_precedence + right_precedence = operator_precedence.next() + + self.set_precedence(left_precedence, node.left) + self.traverse(node.left) + self.write(f" {operator} ") + self.set_precedence(right_precedence, node.right) + self.traverse(node.right) + + cmpops = { + "Eq": "==", + "NotEq": "!=", + "Lt": "<", + "LtE": "<=", + "Gt": ">", + "GtE": ">=", + "Is": "is", + "IsNot": "is not", + "In": "in", + "NotIn": "not in", + } + + def visit_Compare(self, node): + with self.require_parens(_Precedence.CMP, node): + self.set_precedence(_Precedence.CMP.next(), node.left, *node.comparators) + self.traverse(node.left) + for o, e in zip(node.ops, node.comparators): + self.write(" " + self.cmpops[o.__class__.__name__] + " ") + self.traverse(e) + + boolops = {"And": "and", "Or": "or"} + boolop_precedence = {"and": _Precedence.AND, "or": _Precedence.OR} + + def visit_BoolOp(self, node): + operator = self.boolops[node.op.__class__.__name__] + operator_precedence = self.boolop_precedence[operator] + + def increasing_level_traverse(node): + nonlocal operator_precedence + operator_precedence = operator_precedence.next() + self.set_precedence(operator_precedence, node) + self.traverse(node) + + with self.require_parens(operator_precedence, node): + s = f" {operator} " + self.interleave(lambda: self.write(s), increasing_level_traverse, node.values) + + def visit_Attribute(self, node): + self.set_precedence(_Precedence.ATOM, node.value) + self.traverse(node.value) + # Special case: 3.__abs__() is a syntax error, so if node.value + # is an integer literal then we need to either parenthesize + # it or add an extra space to get 3 .__abs__(). + if isinstance(node.value, ast.Constant) and isinstance(node.value.value, int): + self.write(" ") + self.write(".") + self.write(node.attr) + + def visit_Call(self, node): + self.set_precedence(_Precedence.ATOM, node.func) + self.traverse(node.func) + with self.delimit("(", ")"): + comma = False + for e in node.args: + if comma: + self.write(", ") + else: + comma = True + self.traverse(e) + for e in node.keywords: + if comma: + self.write(", ") + else: + comma = True + self.traverse(e) + + def visit_Subscript(self, node): + def is_simple_tuple(slice_value): + # when unparsing a non-empty tuple, the parantheses can be safely + # omitted if there aren't any elements that explicitly requires + # parantheses (such as starred expressions). + return ( + isinstance(slice_value, ast.Tuple) + and slice_value.elts + and not any(isinstance(elt, ast.Starred) for elt in slice_value.elts) + ) + + self.set_precedence(_Precedence.ATOM, node.value) + self.traverse(node.value) + with self.delimit("[", "]"): + if is_simple_tuple(node.slice): + self.items_view(self.traverse, node.slice.elts) + else: + self.traverse(node.slice) + + def visit_Starred(self, node): + self.write("*") + self.set_precedence(_Precedence.EXPR, node.value) + self.traverse(node.value) + + def visit_Ellipsis(self, node): + self.write("...") + + def visit_Slice(self, node): + if node.lower: + self.traverse(node.lower) + self.write(":") + if node.upper: + self.traverse(node.upper) + if node.step: + self.write(":") + self.traverse(node.step) + + def visit_arg(self, node): + self.write(node.arg) + if node.annotation: + self.write(": ") + self.traverse(node.annotation) + + def visit_arguments(self, node): + first = True + # normal arguments + all_args = node.posonlyargs + node.args + defaults = [None] * (len(all_args) - len(node.defaults)) + node.defaults + for index, elements in enumerate(zip(all_args, defaults), 1): + a, d = elements + if first: + first = False + else: + self.write(", ") + self.traverse(a) + if d: + self.write("=") + self.traverse(d) + if index == len(node.posonlyargs): + self.write(", /") + + # varargs, or bare '*' if no varargs but keyword-only arguments present + if node.vararg or node.kwonlyargs: + if first: + first = False + else: + self.write(", ") + self.write("*") + if node.vararg: + self.write(node.vararg.arg) + if node.vararg.annotation: + self.write(": ") + self.traverse(node.vararg.annotation) + + # keyword-only arguments + if node.kwonlyargs: + for a, d in zip(node.kwonlyargs, node.kw_defaults): + self.write(", ") + self.traverse(a) + if d: + self.write("=") + self.traverse(d) + + # kwargs + if node.kwarg: + if first: + first = False + else: + self.write(", ") + self.write("**" + node.kwarg.arg) + if node.kwarg.annotation: + self.write(": ") + self.traverse(node.kwarg.annotation) + + def visit_keyword(self, node): + if node.arg is None: + self.write("**") + else: + self.write(node.arg) + self.write("=") + self.traverse(node.value) + + def visit_Lambda(self, node): + with self.require_parens(_Precedence.TEST, node): + self.write("lambda ") + self.traverse(node.args) + self.write(": ") + self.set_precedence(_Precedence.TEST, node.body) + self.traverse(node.body) + + def visit_alias(self, node): + self.write(node.name) + if node.asname: + self.write(" as " + node.asname) + + def visit_withitem(self, node): + self.traverse(node.context_expr) + if node.optional_vars: + self.write(" as ") + self.traverse(node.optional_vars) + +def unparse(ast_obj): + unparser = _Unparser() + return unparser.visit(ast_obj)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/translations.py Wed Mar 31 19:51:41 2021 +0200 @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing message translations for the code style plugin messages +(miscellaneous part). +""" + +from PyQt5.QtCore import QCoreApplication + +_simplifyMessages = { + "Y101": QCoreApplication.translate( + "SimplifyChecker", + "Multiple 'isinstance()' calls which can be merged into a single " + "call for variable '{0}'"), +} + +_simplifyMessagesSampleArgs = { + "Y101": ["foo"], +}
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/translations.py Tue Mar 30 19:59:24 2021 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/translations.py Wed Mar 31 19:51:41 2021 +0200 @@ -28,6 +28,9 @@ from .Security.translations import ( _securityMessages, _securityMessagesSampleArgs ) +from .Simplify.translations import ( + _simplifyMessages, _simplifyMessagesSampleArgs +) ################################################################## ## pycodestyle error messages @@ -256,6 +259,7 @@ "pycodestyle", "{0}"), } +# TODO: change all syntax error reports to report E901 ################################################################## ## pycodestyle warning messages @@ -504,6 +508,7 @@ "P": _pathlibMessages, "S": _securityMessages, "W": _pycodestyleWarningMessages, + "Y": _simplifyMessages, "FIX": _fixMessages, } @@ -516,6 +521,7 @@ "M": _miscellaneousMessagesSampleArgs, "S": _securityMessagesSampleArgs, "W": _pycodestyleWarningMessagesSampleArgs, + "Y": _simplifyMessagesSampleArgs, "FIX": _fixMessagesSampleArgs, }