--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/pycodestyle.py Tue Aug 29 16:50:17 2023 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/pycodestyle.py Tue Aug 29 16:55:01 2023 +0200 @@ -82,16 +82,16 @@ sys.version_info < (3, 10) and callable(getattr(tokenize, '_compile', None)) ): # pragma: no cover (<py310) - tokenize._compile = lru_cache()(tokenize._compile) # type: ignore + tokenize._compile = lru_cache(tokenize._compile) # type: ignore -__version__ = '2.10.0-eric' +__version__ = '2.11.0-eric' DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__,.tox' DEFAULT_IGNORE = 'E121,E123,E126,E226,E24,E704,W503,W504' try: - if sys.platform == 'win32': + if sys.platform == 'win32': # pragma: win32 cover USER_CONFIG = os.path.expanduser(r'~\.pycodestyle') - else: + else: # pragma: win32 no cover USER_CONFIG = os.path.join( os.getenv('XDG_CONFIG_HOME') or os.path.expanduser('~/.config'), 'pycodestyle' @@ -100,7 +100,6 @@ USER_CONFIG = None PROJECT_CONFIG = ('setup.cfg', 'tox.ini') -TESTSUITE_PATH = os.path.join(os.path.dirname(__file__), 'testsuite') MAX_LINE_LENGTH = 79 # Number of blank lines between various code parts. BLANK_LINES_CONFIG = { @@ -122,12 +121,10 @@ UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) ARITHMETIC_OP = frozenset(['**', '*', '/', '//', '+', '-', '@']) WS_OPTIONAL_OPERATORS = ARITHMETIC_OP.union(['^', '&', '|', '<<', '>>', '%']) -ASSIGNMENT_EXPRESSION_OP = [':='] if sys.version_info >= (3, 8) else [] WS_NEEDED_OPERATORS = frozenset([ - '**=', '*=', '/=', '//=', '+=', '-=', '!=', '<>', '<', '>', + '**=', '*=', '/=', '//=', '+=', '-=', '!=', '<', '>', '%=', '^=', '&=', '|=', '==', '<=', '>=', '<<=', '>>=', '=', - 'and', 'in', 'is', 'or', '->'] + - ASSIGNMENT_EXPRESSION_OP) + 'and', 'in', 'is', 'or', '->', ':=']) WHITESPACE = frozenset(' \t\xa0') NEWLINE = frozenset([tokenize.NL, tokenize.NEWLINE]) SKIP_TOKENS = NEWLINE.union([tokenize.INDENT, tokenize.DEDENT]) @@ -136,8 +133,6 @@ BENCHMARK_KEYS = ['directories', 'files', 'logical lines', 'physical lines'] INDENT_REGEX = re.compile(r'([ \t]*)') -RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,') -RERAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,.*,\s*\w+\s*$') ERRORCODE_REGEX = re.compile(r'\b[A-Z]\d{3}\b') DOCSTRING_REGEX = re.compile(r'u?r?["\']') EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[\[({][ \t]|[ \t][\]}),;:](?!=)') @@ -146,8 +141,10 @@ r'\s*(?(1)|(None|False|True))\b') COMPARE_NEGATIVE_REGEX = re.compile(r'\b(?<!is\s)(not)\s+[^][)(}{ ]+\s+' r'(in|is)\s') -COMPARE_TYPE_REGEX = re.compile(r'(?:[=!]=|is(?:\s+not)?)\s+type(?:s.\w+Type' - r'|\s*\(\s*([^)]*[^ )])\s*\))') +COMPARE_TYPE_REGEX = re.compile( + r'[=!]=\s+type(?:\s*\(\s*([^)]*[^ )])\s*\))' + r'|\btype(?:\s*\(\s*([^)]*[^ )])\s*\))\s+[=!]=' +) KEYWORD_REGEX = re.compile(r'(\s*)\b(?:%s)\b(\s*)' % r'|'.join(KEYWORDS)) OPERATOR_REGEX = re.compile(r'(?:[^,\s])(\s*)(?:[-+*/|!<=>%&^]+|:=)(\s*)') LAMBDA_REGEX = re.compile(r'\blambda\b') @@ -168,6 +165,13 @@ DUNDER_REGEX = re.compile(r"^__([^\s]+)__(?::\s*[a-zA-Z.0-9_\[\]\"]+)? = ") BLANK_EXCEPT_REGEX = re.compile(r"except\s*:") +if sys.version_info >= (3, 12): # pragma: >=3.12 cover + FSTRING_START = tokenize.FSTRING_START + FSTRING_MIDDLE = tokenize.FSTRING_MIDDLE + FSTRING_END = tokenize.FSTRING_END +else: # pragma: <3.12 cover + FSTRING_START = FSTRING_MIDDLE = FSTRING_END = -1 + _checks = {'physical_line': {}, 'logical_line': {}, 'tree': {}} @@ -214,7 +218,6 @@ These options are highly recommended! Okay: if a == 0:\n a = 1\n b = 1 - E101: if a == 0:\n a = 1\n\tb = 1 """ indent = INDENT_REGEX.match(physical_line).group(1) for offset, char in enumerate(indent): @@ -517,7 +520,6 @@ if (tok0.end == tok1.start and keyword.iskeyword(tok0.string) and tok0.string not in SINGLETONS and - tok0.string not in ('async', 'await') and not (tok0.string == 'except' and tok1.string == '*') and not (tok0.string == 'yield' and tok1.string == ')') and tok1.string not in ':\n'): @@ -525,37 +527,6 @@ @register_check -def missing_whitespace(logical_line): - r"""Each comma, semicolon or colon should be followed by whitespace. - - Okay: [a, b] - Okay: (3,) - Okay: a[3,] = 1 - Okay: a[1:4] - Okay: a[:4] - Okay: a[1:] - Okay: a[1:4:2] - E231: ['a','b'] - E231: foo(bar,baz) - E231: [{'a':'b'}] - """ - line = logical_line - for index in range(len(line) - 1): - char = line[index] - next_char = line[index + 1] - if char in ',;:' and next_char not in WHITESPACE: - before = line[:index] - if char == ':' and before.count('[') > before.count(']') and \ - before.rfind('{') < before.rfind('['): - continue # Slice syntax, no space required - if char == ',' and next_char in ')]': - continue # Allow tuple with only one element: (3,) - if char == ':' and next_char == '=' and sys.version_info >= (3, 8): - continue # Allow assignment expression - yield index, "E231 missing whitespace after '%s'", char - - -@register_check def indentation(logical_line, previous_logical, indent_char, indent_level, previous_indent_level, indent_size): @@ -743,8 +714,7 @@ if verbose >= 4: print(f"bracket depth {depth} indent to {start[1]}") # deal with implicit string concatenation - elif (token_type in (tokenize.STRING, tokenize.COMMENT) or - text in ('u', 'ur', 'b', 'br')): + elif token_type in (tokenize.STRING, tokenize.COMMENT, FSTRING_START): indent_chances[start[1]] = str # visual indent after assert/raise/with elif not row and not depth and text in ["assert", "raise", "with"]: @@ -832,9 +802,10 @@ (index < 2 or tokens[index - 2][1] != 'class') and # Allow "return (a.foo for a in range(5))" not keyword.iskeyword(prev_text) and - # 'match' and 'case' are only soft keywords ( sys.version_info < (3, 9) or + # 3.12+: type is a soft keyword but no braces after + prev_text == 'type' or not keyword.issoftkeyword(prev_text) ) ): @@ -869,14 +840,16 @@ @register_check -def missing_whitespace_around_operator(logical_line, tokens): - r"""Surround operators with a single space on either side. +def missing_whitespace(logical_line, tokens): + r"""Surround operators with the correct amount of whitespace. - Always surround these binary operators with a single space on either side: assignment (=), augmented assignment (+=, -= etc.), comparisons (==, <, >, !=, <=, >=, in, not in, is, is not), Booleans (and, or, not). + - Each comma, semicolon or colon should be followed by whitespace. + - If operators with different priorities are used, consider adding whitespace around the operators with the lowest priorities. @@ -887,6 +860,13 @@ Okay: c = (a + b) * (a - b) Okay: foo(bar, key='word', *args, **kwargs) Okay: alpha[:-i] + Okay: [a, b] + Okay: (3,) + Okay: a[3,] = 1 + Okay: a[1:4] + Okay: a[:4] + Okay: a[1:] + Okay: a[1:4:2] E225: i=i+1 E225: submitted +=1 @@ -897,19 +877,52 @@ E226: hypot2 = x*x + y*y E227: c = a|b E228: msg = fmt%(errno, errmsg) + E231: ['a','b'] + E231: foo(bar,baz) + E231: [{'a':'b'}] """ - parens = 0 need_space = False prev_type = tokenize.OP prev_text = prev_end = None operator_types = (tokenize.OP, tokenize.NAME) + brace_stack = [] for token_type, text, start, end, line in tokens: + if token_type == tokenize.OP and text in {'[', '(', '{'}: + brace_stack.append(text) + elif token_type == FSTRING_START: # pragma: >=3.12 cover + brace_stack.append('f') + elif token_type == tokenize.NAME and text == 'lambda': + brace_stack.append('l') + elif brace_stack: + if token_type == tokenize.OP and text in {']', ')', '}'}: + brace_stack.pop() + elif token_type == FSTRING_END: # pragma: >=3.12 cover + brace_stack.pop() + elif ( + brace_stack[-1] == 'l' and + token_type == tokenize.OP and + text == ':' + ): + brace_stack.pop() + if token_type in SKIP_COMMENTS: continue - if text in ('(', 'lambda'): - parens += 1 - elif text == ')': - parens -= 1 + + if token_type == tokenize.OP and text in {',', ';', ':'}: + next_char = line[end[1]:end[1] + 1] + if next_char not in WHITESPACE and next_char not in '\r\n': + # slice + if text == ':' and brace_stack[-1:] == ['[']: + pass + # 3.12+ fstring format specifier + elif text == ':' and brace_stack[-2:] == ['f', '{']: # pragma: >=3.12 cover # noqa: E501 + pass + # tuple (and list for some reason?) + elif text == ',' and next_char in ')]': + pass + else: + yield start, f'E231 missing whitespace after {text!r}', text + if need_space: if start != prev_end: # Found a (probably) needed space @@ -917,10 +930,6 @@ yield (need_space[0], "E225 missing whitespace around operator") need_space = False - elif text == '>' and prev_text in ('<', '-'): - # Tolerate the "<>" operator, even if running Python 3 - # Deal with Python 3's annotated return value "->" - pass elif ( # def f(a, /, b): # ^ @@ -950,8 +959,16 @@ "around %s operator" % (code, optype)) need_space = False elif token_type in operator_types and prev_end is not None: - if text == '=' and parens: - # Allow keyword args or defaults: foo(bar=None). + if ( + text == '=' and ( + # allow lambda default args: lambda x=None: None + brace_stack[-1:] == ['l'] or + # allow keyword args or defaults: foo(bar=None). + brace_stack[-1:] == ['('] or + # allow python 3.8 fstring repr specifier + brace_stack[-2:] == ['f', '{'] + ) + ): pass elif text in WS_NEEDED_OPERATORS: need_space = True @@ -1145,10 +1162,6 @@ Okay: # this is a comment\nimport os Okay: '''this is a module docstring'''\nimport os Okay: r'''this is a module docstring'''\nimport os - Okay: - try:\n\timport x\nexcept ImportError:\n\tpass\nelse:\n\tpass\nimport y - Okay: - try:\n\timport x\nexcept ImportError:\n\tpass\nfinally:\n\tpass\nimport y E402: a=1\nimport os E402: 'One string'\n"Two string"\nimport os E402: a=1\nfrom sys import x @@ -1229,11 +1242,12 @@ counts = {char: 0 for char in '{}[]()'} while -1 < found < last_char: update_counts(line[prev_found:found], counts) - if ((counts['{'] <= counts['}'] and # {'a': 1} (dict) - counts['['] <= counts[']'] and # [1:2] (slice) - counts['('] <= counts[')']) and # (annotation) - not (sys.version_info >= (3, 8) and - line[found + 1] == '=')): # assignment expression + if ( + counts['{'] <= counts['}'] and # {'a': 1} (dict) + counts['['] <= counts[']'] and # [1:2] (slice) + counts['('] <= counts[')'] and # (annotation) + line[found + 1] != '=' # assignment expression + ): lambda_kw = LAMBDA_REGEX.search(line, 0, found) if lambda_kw: before = line[:lambda_kw.start()].rstrip() @@ -1469,21 +1483,19 @@ Do not compare types directly. Okay: if isinstance(obj, int): - E721: if type(obj) is type(1): - - When checking if an object is a string, keep in mind that it might - be a unicode string too! In Python 2.3, str and unicode have a - common base class, basestring, so you can do: - - Okay: if isinstance(obj, basestring): - Okay: if type(a1) is type(b1): + Okay: if type(obj) is int: + E721: if type(obj) == type(1): """ match = COMPARE_TYPE_REGEX.search(logical_line) if match and not noqa: inst = match.group(1) if inst and inst.isidentifier() and inst not in SINGLETONS: return # Allow comparison for types which are not obvious - yield match.start(), "E721 do not compare types, use 'isinstance()'" + yield ( + match.start(), + "E721 do not compare types, for exact checks use `is` / `is not`, " + "for instance checks use `isinstance()`", + ) @register_check @@ -1604,59 +1616,6 @@ @register_check -def python_3000_has_key(logical_line, noqa): - r"""The {}.has_key() method is removed in Python 3: use the 'in' - operator. - - Okay: if "alph" in d:\n print d["alph"] - W601: assert d.has_key('alph') - """ - pos = logical_line.find('.has_key(') - if pos > -1 and not noqa: - yield pos, "W601 .has_key() is deprecated, use 'in'" - - -@register_check -def python_3000_raise_comma(logical_line): - r"""When raising an exception, use "raise ValueError('message')". - - The older form is removed in Python 3. - - Okay: raise DummyError("Message") - W602: raise DummyError, "Message" - """ - match = RAISE_COMMA_REGEX.match(logical_line) - if match and not RERAISE_COMMA_REGEX.match(logical_line): - yield match.end() - 1, "W602 deprecated form of raising exception" - - -@register_check -def python_3000_not_equal(logical_line): - r"""New code should always use != instead of <>. - - The older syntax is removed in Python 3. - - Okay: if a != 'no': - W603: if a <> 'no': - """ - pos = logical_line.find('<>') - if pos > -1: - yield pos, "W603 '<>' is deprecated, use '!='" - - -@register_check -def python_3000_backticks(logical_line): - r"""Use repr() instead of backticks in Python 3. - - Okay: val = repr(1 + 2) - W604: val = `1 + 2` - """ - pos = logical_line.find('`') - if pos > -1: - yield pos, "W604 backticks are deprecated, use 'repr()'" - - -@register_check def python_3000_invalid_escape_sequence(logical_line, tokens, noqa): r"""Invalid escape sequences are deprecated in Python 3.6. @@ -1688,102 +1647,33 @@ 'U', ] - for token_type, text, start, end, line in tokens: - if token_type == tokenize.STRING: - start_line, start_col = start - quote = text[-3:] if text[-3:] in ('"""', "'''") else text[-1] + prefixes = [] + for token_type, text, start, _, _ in tokens: + if token_type in {tokenize.STRING, FSTRING_START}: # Extract string modifiers (e.g. u or r) - quote_pos = text.index(quote) - prefix = text[:quote_pos].lower() - start = quote_pos + len(quote) - string = text[start:-len(quote)] + prefixes.append(text[:text.index(text[-1])].lower()) - if 'r' not in prefix: - pos = string.find('\\') + if token_type in {tokenize.STRING, FSTRING_MIDDLE}: + if 'r' not in prefixes[-1]: + start_line, start_col = start + pos = text.find('\\') while pos >= 0: pos += 1 - if string[pos] not in valid: - line = start_line + string.count('\n', 0, pos) + if text[pos] not in valid: + line = start_line + text.count('\n', 0, pos) if line == start_line: - col = start_col + len(prefix) + len(quote) + pos + col = start_col + pos else: - col = pos - string.rfind('\n', 0, pos) - 1 + col = pos - text.rfind('\n', 0, pos) - 1 yield ( (line, col - 1), - "W605 invalid escape sequence '\\%s'", - string[pos], + f"W605 invalid escape sequence '\\{text[pos]}'", + text[pos], ) - pos = string.find('\\', pos + 1) - - -@register_check -def python_3000_async_await_keywords(logical_line, tokens): - """'async' and 'await' are reserved keywords starting at Python 3.7. - - W606: async = 42 - W606: await = 42 - Okay: async def read(db):\n data = await db.fetch('SELECT ...') - """ - # The Python tokenize library before Python 3.5 recognizes - # async/await as a NAME token. Therefore, use a state machine to - # look for the possible async/await constructs as defined by the - # Python grammar: - # https://docs.python.org/3/reference/grammar.html - - state = None - for token_type, text, start, end, line in tokens: - error = False - - if token_type == tokenize.NL: - continue - - if state is None: - if token_type == tokenize.NAME: - if text == 'async': - state = ('async_stmt', start) - elif text == 'await': - state = ('await', start) - elif (token_type == tokenize.NAME and - text in ('def', 'for')): - state = ('define', start) + pos = text.find('\\', pos + 1) - elif state[0] == 'async_stmt': - if token_type == tokenize.NAME and text in ('def', 'with', 'for'): - # One of funcdef, with_stmt, or for_stmt. Return to - # looking for async/await names. - state = None - else: - error = True - elif state[0] == 'await': - if token_type == tokenize.NAME: - # An await expression. Return to looking for async/await - # names. - state = None - elif token_type == tokenize.OP and text == '(': - state = None - else: - error = True - elif state[0] == 'define': - if token_type == tokenize.NAME and text in ('async', 'await'): - error = True - else: - state = None - - if error: - yield ( - state[1], - "W606 'async' and 'await' are reserved keywords starting with " - "Python 3.7", - ) - state = None - - # Last token - if state is not None: - yield ( - state[1], - "W606 'async' and 'await' are reserved keywords starting with " - "Python 3.7", - ) + if token_type in {tokenize.STRING, FSTRING_END}: + prefixes.pop() ######################################################################## @@ -1861,15 +1751,6 @@ r"""Return the amount of indentation. Tabs are expanded to the next multiple of 8. - - >>> expand_indent(' ') - 4 - >>> expand_indent('\t') - 8 - >>> expand_indent(' \t') - 8 - >>> expand_indent(' \t') - 16 """ line = line.rstrip('\n\r') if '\t' not in line: @@ -1886,15 +1767,7 @@ def mute_string(text): - """Replace contents with 'xxx' to prevent syntax matching. - - >>> mute_string('"abc"') - '"xxx"' - >>> mute_string("'''abc'''") - "'''xxx'''" - >>> mute_string("r'abc'") - "r'xxx'" - """ + """Replace contents with 'xxx' to prevent syntax matching.""" # String modifiers (e.g. u or r) start = text.index(text[-1]) + 1 end = len(text) - 1 @@ -1995,6 +1868,7 @@ self.max_line_length = options.max_line_length self.max_doc_length = options.max_doc_length self.indent_size = options.indent_size + self.fstring_start = 0 self.multiline = False # in a multiline string? self.hang_closing = options.hang_closing self.indent_size = options.indent_size @@ -2097,6 +1971,8 @@ continue if token_type == tokenize.STRING: text = mute_string(text) + elif token_type == FSTRING_MIDDLE: # pragma: >=3.12 cover + text = 'x' * len(text) if prev_row: (start_row, start_col) = start if prev_row != start_row: # different row @@ -2187,16 +2063,21 @@ """If appropriate for token, check current physical line(s).""" # Called after every token, but act only on end of line. + if token.type == FSTRING_START: # pragma: >=3.12 cover + self.fstring_start = token.start[0] # a newline token ends a single physical line. - if _is_eol_token(token): + elif _is_eol_token(token): # if the file does not end with a newline, the NEWLINE # token is inserted by the parser, but it does not contain # the previous physical line in `token[4]` - if token[4] == '': + if token.line == '': self.check_physical(prev_physical) else: - self.check_physical(token[4]) - elif token[0] == tokenize.STRING and '\n' in token[1]: + self.check_physical(token.line) + elif ( + token.type == tokenize.STRING and '\n' in token.string or + token.type == FSTRING_END + ): # Less obviously, a string that contains newlines is a # multiline string, either triple-quoted or with internal # newlines backslash-escaped. Check every physical line in @@ -2212,14 +2093,18 @@ # - have to wind self.line_number back because initially it # points to the last line of the string, and we want # check_physical() to give accurate feedback - if noqa(token[4]): + if noqa(token.line): return + if token.type == FSTRING_END: # pragma: >=3.12 cover + start = self.fstring_start + else: + start = token.start[0] + end = token.end[0] + self.multiline = True - self.line_number = token[2][0] - _, src, (_, offset), _, _ = token - src = self.lines[self.line_number - 1][:offset] + src - for line in src.split('\n')[:-1]: - self.check_physical(line + '\n') + self.line_number = start + for line_number in range(start, end): + self.check_physical(self.lines[line_number - 1] + '\n') self.line_number += 1 self.multiline = False @@ -2487,15 +2372,13 @@ options.reporter = BaseReport if options.quiet else StandardReport options.select = tuple(options.select or ()) -# if not (options.select or options.ignore or -# options.testsuite or options.doctest) and DEFAULT_IGNORE: +# if not (options.select or options.ignore) and DEFAULT_IGNORE: # # The default choice: ignore controversial checks # options.ignore = tuple(DEFAULT_IGNORE.split(',')) # else: -# # Ignore all checks which are not explicitly selected or all if no +# # Ignore all checks which are not explicitly selected # options.ignore = ('',) if options.select else tuple(options.ignore) - # check is ignored or explicitly selected options.ignore = ('',) if options.select else tuple(options.ignore) options.benchmark_keys = BENCHMARK_KEYS[:] options.ignore_code = self.ignore_code @@ -2661,11 +2544,6 @@ help="report changes only within line number ranges in " "the unified diff received on STDIN") group = parser.add_option_group("Testing Options") - if os.path.exists(TESTSUITE_PATH): - group.add_option('--testsuite', metavar='dir', - help="run regression tests from dir") - group.add_option('--doctest', action='store_true', - help="run doctest on myself") group.add_option('--benchmark', action='store_true', help="measure processing speed") return parser @@ -2742,7 +2620,6 @@ # Third, overwrite with the command-line options (options, __) = parser.parse_args(arglist, values=new_options) - options.doctest = options.testsuite = False return options @@ -2775,17 +2652,14 @@ if verbose is not None: options.verbose = verbose - if options.ensure_value('testsuite', False): - args.append(options.testsuite) - elif not options.ensure_value('doctest', False): - if parse_argv and not args: - if options.diff or any(os.path.exists(name) - for name in PROJECT_CONFIG): - args = ['.'] - else: - parser.error('input not specified') - options = read_config(options, args, arglist, parser) - options.reporter = parse_argv and options.quiet == 1 and FileReport + if parse_argv and not args: + if options.diff or any(os.path.exists(name) + for name in PROJECT_CONFIG): + args = ['.'] + else: + parser.error('input not specified') + options = read_config(options, args, arglist, parser) + options.reporter = parse_argv and options.quiet == 1 and FileReport options.filename = _parse_multi_options(options.filename) options.exclude = normalize_paths(options.exclude) @@ -2830,11 +2704,7 @@ style_guide = StyleGuide(parse_argv=True) options = style_guide.options - if options.doctest or options.testsuite: - from testsuite.support import run_tests - report = run_tests(style_guide) - else: - report = style_guide.check_files() + report = style_guide.check_files() if options.statistics: report.print_statistics() @@ -2842,9 +2712,6 @@ if options.benchmark: report.print_benchmark() - if options.testsuite and not options.quiet: - report.print_results() - if report.total_errors: if options.count: sys.stderr.write(str(report.total_errors) + '\n')