From 7b11f04d54d6da117ed5e640fd9420727f07ef81 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 14 Oct 2019 18:15:18 -0700 Subject: [PATCH 01/16] Fix type: ignore line breaking when there is a destructuring assignment (#1065) It turns out we also need to handle invisible *left* parens added at the *start* of a line. Refactor `contains_unsplittable_type_ignore` to handle this more cleanly. --- black.py | 22 +++++++++------------- tests/data/comments6.py | 2 ++ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/black.py b/black.py index ff373c8..0776983 100644 --- a/black.py +++ b/black.py @@ -1341,19 +1341,15 @@ class Line: # only report an unsplittable 'type: ignore' if this line was # one line in the original code. - # Like in the type comment check above, we need to skip a black added - # trailing comma or invisible paren, since it will be the original leaf - # before it that has the original line number. - last_idx = -1 - last_leaf = self.leaves[-1] - if len(self.leaves) > 2 and ( - last_leaf.type == token.COMMA - or (last_leaf.type == token.RPAR and not last_leaf.value) - ): - last_idx = -2 - - if self.leaves[0].lineno == self.leaves[last_idx].lineno: - for node in self.leaves[last_idx:]: + # Grab the first and last line numbers, skipping generated leaves + first_line = next((l.lineno for l in self.leaves if l.lineno != 0), 0) + last_line = next((l.lineno for l in reversed(self.leaves) if l.lineno != 0), 0) + + if first_line == last_line: + # We look at the last two leaves since a comma or an + # invisible paren could have been added at the end of the + # line. + for node in self.leaves[-2:]: for comment in self.comments.get(id(node), []): if is_type_comment(comment, " ignore"): return True diff --git a/tests/data/comments6.py b/tests/data/comments6.py index 3de8b2e..abfd96e 100644 --- a/tests/data/comments6.py +++ b/tests/data/comments6.py @@ -112,3 +112,5 @@ call_to_some_function_asdf( foo, [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore ) + +aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] -- 2.39.2 From be35b1ed2d1f9453090bb29ac24abe997815ff70 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 15 Oct 2019 20:08:44 +0300 Subject: [PATCH 02/16] Declare support for Python 3.8 (#1069) --- CONTRIBUTING.md | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be7c505..72ffa56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ to get accepted. You can still try but prepare to be disappointed. ## Technicalities Development on the latest version of Python is preferred. As of this -writing it's 3.7 You can use any operating system. I am using macOS +writing it's 3.8. You can use any operating system. I am using macOS myself and CentOS at work. Install all development dependencies using: diff --git a/setup.py b/setup.py index 1ba7a00..8c9672e 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ setup( "Programming Language :: Python", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", -- 2.39.2 From 3bfb66971f03da39ae1f4c98c30d55e60f63d33b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 15 Oct 2019 21:56:28 +0300 Subject: [PATCH 03/16] Test on Python 3.8 final (#1070) --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c2b8d05..a4eb998 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,8 +31,8 @@ matrix: python: 3.6 - name: "3.7" python: 3.7 - - name: "3.8-dev" - python: 3.8-dev + - name: "3.8" + python: 3.8 before_deploy: - pip install pyinstaller - pyinstaller --clean -F --add-data blib2to3/:blib2to3 black.py -- 2.39.2 From 0ff718e1e2b434477bca134e6c8aa0f67c898cbc Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Sun, 20 Oct 2019 06:55:31 -0700 Subject: [PATCH 04/16] Blacken .py files in blib2to3 (#1011) * Blacken .py files in blib2to3 This is in preparation for adding type annotations to blib2to3 in order to compiling it with mypyc (#1009, which I can rebase on top of this). To enforce that it stays blackened, I just cargo-culted the existing test code used for validating formatting. It feels pretty clunky now, though, so I can abstract the common logic out into a helper if that seems better. (But error messages might be less clear then?) * Tidy up the tests --- blib2to3/__init__.py | 2 +- blib2to3/pgen2/conv.py | 61 +++---- blib2to3/pgen2/driver.py | 38 ++-- blib2to3/pgen2/grammar.py | 15 +- blib2to3/pgen2/literals.py | 35 ++-- blib2to3/pgen2/parse.py | 14 +- blib2to3/pgen2/pgen.py | 67 +++---- blib2to3/pgen2/token.py | 6 +- blib2to3/pgen2/tokenize.py | 355 +++++++++++++++++++++++-------------- blib2to3/pygram.py | 16 +- blib2to3/pytree.py | 99 +++++++---- tests/test_black.py | 68 ++++--- 12 files changed, 457 insertions(+), 319 deletions(-) diff --git a/blib2to3/__init__.py b/blib2to3/__init__.py index ea30561..1bb8bf6 100644 --- a/blib2to3/__init__.py +++ b/blib2to3/__init__.py @@ -1 +1 @@ -#empty +# empty diff --git a/blib2to3/pgen2/conv.py b/blib2to3/pgen2/conv.py index dd70752..728b152 100644 --- a/blib2to3/pgen2/conv.py +++ b/blib2to3/pgen2/conv.py @@ -70,8 +70,7 @@ class Converter(grammar.Grammar): lineno += 1 mo = re.match(r"^#define\s+(\w+)\s+(\d+)$", line) if not mo and line.strip(): - print("%s(%s): can't parse %s" % (filename, lineno, - line.strip())) + print("%s(%s): can't parse %s" % (filename, lineno, line.strip())) else: symbol, number = mo.groups() number = int(number) @@ -118,39 +117,38 @@ class Converter(grammar.Grammar): lineno = 0 # Expect the two #include lines - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) assert line == '#include "pgenheaders.h"\n', (lineno, line) - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) assert line == '#include "grammar.h"\n', (lineno, line) # Parse the state definitions - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) allarcs = {} states = [] while line.startswith("static arc "): while line.startswith("static arc "): - mo = re.match(r"static arc arcs_(\d+)_(\d+)\[(\d+)\] = {$", - line) + mo = re.match(r"static arc arcs_(\d+)_(\d+)\[(\d+)\] = {$", line) assert mo, (lineno, line) n, m, k = list(map(int, mo.groups())) arcs = [] for _ in range(k): - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) mo = re.match(r"\s+{(\d+), (\d+)},$", line) assert mo, (lineno, line) i, j = list(map(int, mo.groups())) arcs.append((i, j)) - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) assert line == "};\n", (lineno, line) allarcs[(n, m)] = arcs - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) mo = re.match(r"static state states_(\d+)\[(\d+)\] = {$", line) assert mo, (lineno, line) s, t = list(map(int, mo.groups())) assert s == len(states), (lineno, line) state = [] for _ in range(t): - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) mo = re.match(r"\s+{(\d+), arcs_(\d+)_(\d+)},$", line) assert mo, (lineno, line) k, n, m = list(map(int, mo.groups())) @@ -158,9 +156,9 @@ class Converter(grammar.Grammar): assert k == len(arcs), (lineno, line) state.append(arcs) states.append(state) - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) assert line == "};\n", (lineno, line) - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) self.states = states # Parse the dfas @@ -169,9 +167,8 @@ class Converter(grammar.Grammar): assert mo, (lineno, line) ndfas = int(mo.group(1)) for i in range(ndfas): - lineno, line = lineno+1, next(f) - mo = re.match(r'\s+{(\d+), "(\w+)", (\d+), (\d+), states_(\d+),$', - line) + lineno, line = lineno + 1, next(f) + mo = re.match(r'\s+{(\d+), "(\w+)", (\d+), (\d+), states_(\d+),$', line) assert mo, (lineno, line) symbol = mo.group(2) number, x, y, z = list(map(int, mo.group(1, 3, 4, 5))) @@ -180,7 +177,7 @@ class Converter(grammar.Grammar): assert x == 0, (lineno, line) state = states[z] assert y == len(state), (lineno, line) - lineno, line = lineno+1, next(f) + lineno, line = lineno + 1, next(f) mo = re.match(r'\s+("(?:\\\d\d\d)*")},$', line) assert mo, (lineno, line) first = {} @@ -188,21 +185,21 @@ class Converter(grammar.Grammar): for i, c in enumerate(rawbitset): byte = ord(c) for j in range(8): - if byte & (1<= 2*len(q) - s = s[len(q):-len(q)] + if s[:3] == q * 3: + q = q * 3 + assert s.endswith(q), repr(s[-len(q) :]) + assert len(s) >= 2 * len(q) + s = s[len(q) : -len(q)] return re.sub(r"\\(\'|\"|\\|[abfnrtv]|x.{0,2}|[0-7]{1,3})", escape, s) + def test(): for i in range(256): c = chr(i) diff --git a/blib2to3/pgen2/parse.py b/blib2to3/pgen2/parse.py index 6bebdbb..22f14c8 100644 --- a/blib2to3/pgen2/parse.py +++ b/blib2to3/pgen2/parse.py @@ -13,17 +13,20 @@ how this parsing engine works. # Local imports from . import token + class ParseError(Exception): """Exception to signal the parser is stuck.""" def __init__(self, msg, type, value, context): - Exception.__init__(self, "%s: type=%r, value=%r, context=%r" % - (msg, type, value, context)) + Exception.__init__( + self, "%s: type=%r, value=%r, context=%r" % (msg, type, value, context) + ) self.msg = msg self.type = type self.value = value self.context = context + class Parser(object): """Parser engine. @@ -108,7 +111,7 @@ class Parser(object): stackentry = (self.grammar.dfas[start], 0, newnode) self.stack = [stackentry] self.rootnode = None - self.used_names = set() # Aliased to self.rootnode.used_names in pop() + self.used_names = set() # Aliased to self.rootnode.used_names in pop() def addtoken(self, type, value, context): """Add a token; return True iff this is the end of the program.""" @@ -145,15 +148,14 @@ class Parser(object): if ilabel in itsfirst: # Push a symbol self.push(t, self.grammar.dfas[t], newstate, context) - break # To continue the outer while loop + break # To continue the outer while loop else: if (0, state) in arcs: # An accepting state, pop it and try something else self.pop() if not self.stack: # Done parsing, but another token is input - raise ParseError("too much input", - type, value, context) + raise ParseError("too much input", type, value, context) else: # No success finding a transition raise ParseError("bad input", type, value, context) diff --git a/blib2to3/pgen2/pgen.py b/blib2to3/pgen2/pgen.py index b0cbd16..1da6925 100644 --- a/blib2to3/pgen2/pgen.py +++ b/blib2to3/pgen2/pgen.py @@ -4,11 +4,12 @@ # Pgen imports from . import grammar, token, tokenize + class PgenGrammar(grammar.Grammar): pass -class ParserGenerator(object): +class ParserGenerator(object): def __init__(self, filename, stream=None): close_stream = None if stream is None: @@ -17,11 +18,11 @@ class ParserGenerator(object): self.filename = filename self.stream = stream self.generator = tokenize.generate_tokens(stream.readline) - self.gettoken() # Initialize lookahead + self.gettoken() # Initialize lookahead self.dfas, self.startsymbol = self.parse() if close_stream is not None: close_stream() - self.first = {} # map from symbol name to set of tokens + self.first = {} # map from symbol name to set of tokens self.addfirstsets() def make_grammar(self): @@ -96,7 +97,7 @@ class ParserGenerator(object): return ilabel else: # An operator (any non-numeric token) - itoken = grammar.opmap[value] # Fails if unknown token + itoken = grammar.opmap[value] # Fails if unknown token if itoken in c.tokens: return c.tokens[itoken] else: @@ -110,11 +111,11 @@ class ParserGenerator(object): for name in names: if name not in self.first: self.calcfirst(name) - #print name, self.first[name].keys() + # print name, self.first[name].keys() def calcfirst(self, name): dfa = self.dfas[name] - self.first[name] = None # dummy to detect left recursion + self.first[name] = None # dummy to detect left recursion state = dfa[0] totalset = {} overlapcheck = {} @@ -136,9 +137,11 @@ class ParserGenerator(object): for label, itsfirst in overlapcheck.items(): for symbol in itsfirst: if symbol in inverse: - raise ValueError("rule %s is ambiguous; %s is in the" - " first sets of %s as well as %s" % - (name, symbol, label, inverse[symbol])) + raise ValueError( + "rule %s is ambiguous; %s is in the" + " first sets of %s as well as %s" + % (name, symbol, label, inverse[symbol]) + ) inverse[symbol] = label self.first[name] = totalset @@ -154,14 +157,14 @@ class ParserGenerator(object): self.expect(token.OP, ":") a, z = self.parse_rhs() self.expect(token.NEWLINE) - #self.dump_nfa(name, a, z) + # self.dump_nfa(name, a, z) dfa = self.make_dfa(a, z) - #self.dump_dfa(name, dfa) + # self.dump_dfa(name, dfa) oldlen = len(dfa) self.simplify_dfa(dfa) newlen = len(dfa) dfas[name] = dfa - #print name, oldlen, newlen + # print name, oldlen, newlen if startsymbol is None: startsymbol = name return dfas, startsymbol @@ -173,10 +176,12 @@ class ParserGenerator(object): # values. assert isinstance(start, NFAState) assert isinstance(finish, NFAState) + def closure(state): base = {} addclosure(state, base) return base + def addclosure(state, base): assert isinstance(state, NFAState) if state in base: @@ -185,8 +190,9 @@ class ParserGenerator(object): for label, next in state.arcs: if label is None: addclosure(next, base) + states = [DFAState(closure(start), finish)] - for state in states: # NB states grows while we're iterating + for state in states: # NB states grows while we're iterating arcs = {} for nfastate in state.nfaset: for label, next in nfastate.arcs: @@ -200,7 +206,7 @@ class ParserGenerator(object): st = DFAState(nfaset, finish) states.append(st) state.addarc(st, label) - return states # List of DFAState instances; first one is start + return states # List of DFAState instances; first one is start def dump_nfa(self, name, start, finish): print("Dump of NFA for", name) @@ -236,10 +242,10 @@ class ParserGenerator(object): while changes: changes = False for i, state_i in enumerate(dfa): - for j in range(i+1, len(dfa)): + for j in range(i + 1, len(dfa)): state_j = dfa[j] if state_i == state_j: - #print " unify", i, j + # print " unify", i, j del dfa[j] for state in dfa: state.unifystate(state_j, state_i) @@ -266,8 +272,7 @@ class ParserGenerator(object): def parse_alt(self): # ALT: ITEM+ a, b = self.parse_item() - while (self.value in ("(", "[") or - self.type in (token.NAME, token.STRING)): + while self.value in ("(", "[") or self.type in (token.NAME, token.STRING): c, d = self.parse_item() b.addarc(c) b = d @@ -307,13 +312,15 @@ class ParserGenerator(object): self.gettoken() return a, z else: - self.raise_error("expected (...) or NAME or STRING, got %s/%s", - self.type, self.value) + self.raise_error( + "expected (...) or NAME or STRING, got %s/%s", self.type, self.value + ) def expect(self, type, value=None): if self.type != type or (value is not None and self.value != value): - self.raise_error("expected %s/%s, got %s/%s", - type, value, self.type, self.value) + self.raise_error( + "expected %s/%s, got %s/%s", type, value, self.type, self.value + ) value = self.value self.gettoken() return value @@ -323,7 +330,7 @@ class ParserGenerator(object): while tup[0] in (tokenize.COMMENT, tokenize.NL): tup = next(self.generator) self.type, self.value, self.begin, self.end, self.line = tup - #print token.tok_name[self.type], repr(self.value) + # print token.tok_name[self.type], repr(self.value) def raise_error(self, msg, *args): if args: @@ -331,28 +338,27 @@ class ParserGenerator(object): msg = msg % args except: msg = " ".join([msg] + list(map(str, args))) - raise SyntaxError(msg, (self.filename, self.end[0], - self.end[1], self.line)) + raise SyntaxError(msg, (self.filename, self.end[0], self.end[1], self.line)) -class NFAState(object): +class NFAState(object): def __init__(self): - self.arcs = [] # list of (label, NFAState) pairs + self.arcs = [] # list of (label, NFAState) pairs def addarc(self, next, label=None): assert label is None or isinstance(label, str) assert isinstance(next, NFAState) self.arcs.append((label, next)) -class DFAState(object): +class DFAState(object): def __init__(self, nfaset, final): assert isinstance(nfaset, dict) assert isinstance(next(iter(nfaset)), NFAState) assert isinstance(final, NFAState) self.nfaset = nfaset self.isfinal = final in nfaset - self.arcs = {} # map from label to DFAState + self.arcs = {} # map from label to DFAState def addarc(self, next, label): assert isinstance(label, str) @@ -379,7 +385,8 @@ class DFAState(object): return False return True - __hash__ = None # For Py3 compatibility. + __hash__ = None # For Py3 compatibility. + def generate_grammar(filename="Grammar.txt"): p = ParserGenerator(filename) diff --git a/blib2to3/pgen2/token.py b/blib2to3/pgen2/token.py index 40aa89d..583a8a1 100644 --- a/blib2to3/pgen2/token.py +++ b/blib2to3/pgen2/token.py @@ -3,7 +3,7 @@ # Taken from Python (r53757) and modified to include some tokens # originally monkeypatched in by pgen2.tokenize -#--start constants-- +# --start constants-- ENDMARKER = 0 NAME = 1 NUMBER = 2 @@ -66,7 +66,7 @@ ERRORTOKEN = 58 COLONEQUAL = 59 N_TOKENS = 60 NT_OFFSET = 256 -#--end constants-- +# --end constants-- tok_name = {} for _name, _value in list(globals().items()): @@ -77,8 +77,10 @@ for _name, _value in list(globals().items()): def ISTERMINAL(x): return x < NT_OFFSET + def ISNONTERMINAL(x): return x >= NT_OFFSET + def ISEOF(x): return x == ENDMARKER diff --git a/blib2to3/pgen2/tokenize.py b/blib2to3/pgen2/tokenize.py index 9775489..8c1c4cf 100644 --- a/blib2to3/pgen2/tokenize.py +++ b/blib2to3/pgen2/tokenize.py @@ -25,17 +25,20 @@ are the same, except instead of generating tokens, tokeneater is a callback function to which the 5 fields described above are passed as 5 arguments, each time a new token is found.""" -__author__ = 'Ka-Ping Yee ' -__credits__ = \ - 'GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, Skip Montanaro' +__author__ = "Ka-Ping Yee " +__credits__ = "GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, Skip Montanaro" import regex as re from codecs import BOM_UTF8, lookup from blib2to3.pgen2.token import * from . import token -__all__ = [x for x in dir(token) if x[0] != '_'] + ["tokenize", - "generate_tokens", "untokenize"] + +__all__ = [x for x in dir(token) if x[0] != "_"] + [ + "tokenize", + "generate_tokens", + "untokenize", +] del token try: @@ -45,29 +48,40 @@ except NameError: # valid Python 3 code. bytes = str -def group(*choices): return '(' + '|'.join(choices) + ')' -def any(*choices): return group(*choices) + '*' -def maybe(*choices): return group(*choices) + '?' + +def group(*choices): + return "(" + "|".join(choices) + ")" + + +def any(*choices): + return group(*choices) + "*" + + +def maybe(*choices): + return group(*choices) + "?" + + def _combinations(*l): - return set( - x + y for x in l for y in l + ("",) if x.casefold() != y.casefold() - ) + return set(x + y for x in l for y in l + ("",) if x.casefold() != y.casefold()) -Whitespace = r'[ \f\t]*' -Comment = r'#[^\r\n]*' -Ignore = Whitespace + any(r'\\\r?\n' + Whitespace) + maybe(Comment) -Name = r'\w+' # this is invalid but it's fine because Name comes after Number in all groups -Binnumber = r'0[bB]_?[01]+(?:_[01]+)*' -Hexnumber = r'0[xX]_?[\da-fA-F]+(?:_[\da-fA-F]+)*[lL]?' -Octnumber = r'0[oO]?_?[0-7]+(?:_[0-7]+)*[lL]?' -Decnumber = group(r'[1-9]\d*(?:_\d+)*[lL]?', '0[lL]?') +Whitespace = r"[ \f\t]*" +Comment = r"#[^\r\n]*" +Ignore = Whitespace + any(r"\\\r?\n" + Whitespace) + maybe(Comment) +Name = r"\w+" # this is invalid but it's fine because Name comes after Number in all groups + +Binnumber = r"0[bB]_?[01]+(?:_[01]+)*" +Hexnumber = r"0[xX]_?[\da-fA-F]+(?:_[\da-fA-F]+)*[lL]?" +Octnumber = r"0[oO]?_?[0-7]+(?:_[0-7]+)*[lL]?" +Decnumber = group(r"[1-9]\d*(?:_\d+)*[lL]?", "0[lL]?") Intnumber = group(Binnumber, Hexnumber, Octnumber, Decnumber) -Exponent = r'[eE][-+]?\d+(?:_\d+)*' -Pointfloat = group(r'\d+(?:_\d+)*\.(?:\d+(?:_\d+)*)?', r'\.\d+(?:_\d+)*') + maybe(Exponent) -Expfloat = r'\d+(?:_\d+)*' + Exponent +Exponent = r"[eE][-+]?\d+(?:_\d+)*" +Pointfloat = group(r"\d+(?:_\d+)*\.(?:\d+(?:_\d+)*)?", r"\.\d+(?:_\d+)*") + maybe( + Exponent +) +Expfloat = r"\d+(?:_\d+)*" + Exponent Floatnumber = group(Pointfloat, Expfloat) -Imagnumber = group(r'\d+(?:_\d+)*[jJ]', Floatnumber + r'[jJ]') +Imagnumber = group(r"\d+(?:_\d+)*[jJ]", Floatnumber + r"[jJ]") Number = group(Imagnumber, Floatnumber, Intnumber) # Tail end of ' string. @@ -81,30 +95,39 @@ Double3 = r'[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*"""' _litprefix = r"(?:[uUrRbBfF]|[rR][fFbB]|[fFbBuU][rR])?" Triple = group(_litprefix + "'''", _litprefix + '"""') # Single-line ' or " string. -String = group(_litprefix + r"'[^\n'\\]*(?:\\.[^\n'\\]*)*'", - _litprefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*"') +String = group( + _litprefix + r"'[^\n'\\]*(?:\\.[^\n'\\]*)*'", + _litprefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*"', +) # Because of leftmost-then-longest match semantics, be sure to put the # longest operators first (e.g., if = came before ==, == would get # recognized as two instances of =). -Operator = group(r"\*\*=?", r">>=?", r"<<=?", r"<>", r"!=", - r"//=?", r"->", - r"[+\-*/%&@|^=<>:]=?", - r"~") +Operator = group( + r"\*\*=?", + r">>=?", + r"<<=?", + r"<>", + r"!=", + r"//=?", + r"->", + r"[+\-*/%&@|^=<>:]=?", + r"~", +) -Bracket = '[][(){}]' -Special = group(r'\r?\n', r'[:;.,`@]') +Bracket = "[][(){}]" +Special = group(r"\r?\n", r"[:;.,`@]") Funny = group(Operator, Bracket, Special) PlainToken = group(Number, Funny, String, Name) Token = Ignore + PlainToken # First (or only) line of ' or " string. -ContStr = group(_litprefix + r"'[^\n'\\]*(?:\\.[^\n'\\]*)*" + - group("'", r'\\\r?\n'), - _litprefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*' + - group('"', r'\\\r?\n')) -PseudoExtras = group(r'\\\r?\n', Comment, Triple) +ContStr = group( + _litprefix + r"'[^\n'\\]*(?:\\.[^\n'\\]*)*" + group("'", r"\\\r?\n"), + _litprefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*' + group('"', r"\\\r?\n"), +) +PseudoExtras = group(r"\\\r?\n", Comment, Triple) PseudoToken = Whitespace + group(PseudoExtras, Number, Funny, ContStr, Name) tokenprog = re.compile(Token, re.UNICODE) @@ -113,39 +136,50 @@ single3prog = re.compile(Single3) double3prog = re.compile(Double3) _strprefixes = ( - _combinations('r', 'R', 'f', 'F') | - _combinations('r', 'R', 'b', 'B') | - {'u', 'U', 'ur', 'uR', 'Ur', 'UR'} + _combinations("r", "R", "f", "F") + | _combinations("r", "R", "b", "B") + | {"u", "U", "ur", "uR", "Ur", "UR"} ) -endprogs = {"'": re.compile(Single), '"': re.compile(Double), - "'''": single3prog, '"""': double3prog, - **{f"{prefix}'''": single3prog for prefix in _strprefixes}, - **{f'{prefix}"""': double3prog for prefix in _strprefixes}, - **{prefix: None for prefix in _strprefixes}} +endprogs = { + "'": re.compile(Single), + '"': re.compile(Double), + "'''": single3prog, + '"""': double3prog, + **{f"{prefix}'''": single3prog for prefix in _strprefixes}, + **{f'{prefix}"""': double3prog for prefix in _strprefixes}, + **{prefix: None for prefix in _strprefixes}, +} triple_quoted = ( - {"'''", '"""'} | - {f"{prefix}'''" for prefix in _strprefixes} | - {f'{prefix}"""' for prefix in _strprefixes} + {"'''", '"""'} + | {f"{prefix}'''" for prefix in _strprefixes} + | {f'{prefix}"""' for prefix in _strprefixes} ) single_quoted = ( - {"'", '"'} | - {f"{prefix}'" for prefix in _strprefixes} | - {f'{prefix}"' for prefix in _strprefixes} + {"'", '"'} + | {f"{prefix}'" for prefix in _strprefixes} + | {f'{prefix}"' for prefix in _strprefixes} ) tabsize = 8 -class TokenError(Exception): pass -class StopTokenizing(Exception): pass +class TokenError(Exception): + pass + + +class StopTokenizing(Exception): + pass + -def printtoken(type, token, xxx_todo_changeme, xxx_todo_changeme1, line): # for testing +def printtoken(type, token, xxx_todo_changeme, xxx_todo_changeme1, line): # for testing (srow, scol) = xxx_todo_changeme (erow, ecol) = xxx_todo_changeme1 - print("%d,%d-%d,%d:\t%s\t%s" % \ - (srow, scol, erow, ecol, tok_name[type], repr(token))) + print( + "%d,%d-%d,%d:\t%s\t%s" % (srow, scol, erow, ecol, tok_name[type], repr(token)) + ) + def tokenize(readline, tokeneater=printtoken): """ @@ -165,13 +199,14 @@ def tokenize(readline, tokeneater=printtoken): except StopTokenizing: pass + # backwards compatible interface def tokenize_loop(readline, tokeneater): for token_info in generate_tokens(readline): tokeneater(*token_info) -class Untokenizer: +class Untokenizer: def __init__(self): self.tokens = [] self.prev_row = 1 @@ -204,14 +239,14 @@ class Untokenizer: toks_append = self.tokens.append toknum, tokval = token if toknum in (NAME, NUMBER): - tokval += ' ' + tokval += " " if toknum in (NEWLINE, NL): startline = True for tok in iterable: toknum, tokval = tok[:2] if toknum in (NAME, NUMBER, ASYNC, AWAIT): - tokval += ' ' + tokval += " " if toknum == INDENT: indents.append(tokval) @@ -226,8 +261,10 @@ class Untokenizer: startline = False toks_append(tokval) -cookie_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII) -blank_re = re.compile(br'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII) + +cookie_re = re.compile(r"^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)", re.ASCII) +blank_re = re.compile(br"^[ \t\f]*(?:[#\r\n]|$)", re.ASCII) + def _get_normal_name(orig_enc): """Imitates get_normal_name in tokenizer.c.""" @@ -235,11 +272,13 @@ def _get_normal_name(orig_enc): enc = orig_enc[:12].lower().replace("_", "-") if enc == "utf-8" or enc.startswith("utf-8-"): return "utf-8" - if enc in ("latin-1", "iso-8859-1", "iso-latin-1") or \ - enc.startswith(("latin-1-", "iso-8859-1-", "iso-latin-1-")): + if enc in ("latin-1", "iso-8859-1", "iso-latin-1") or enc.startswith( + ("latin-1-", "iso-8859-1-", "iso-latin-1-") + ): return "iso-8859-1" return orig_enc + def detect_encoding(readline): """ The detect_encoding() function is used to detect the encoding that should @@ -260,7 +299,8 @@ def detect_encoding(readline): """ bom_found = False encoding = None - default = 'utf-8' + default = "utf-8" + def read_or_stop(): try: return readline() @@ -269,7 +309,7 @@ def detect_encoding(readline): def find_cookie(line): try: - line_string = line.decode('ascii') + line_string = line.decode("ascii") except UnicodeDecodeError: return None match = cookie_re.match(line_string) @@ -283,17 +323,17 @@ def detect_encoding(readline): raise SyntaxError("unknown encoding: " + encoding) if bom_found: - if codec.name != 'utf-8': + if codec.name != "utf-8": # This behaviour mimics the Python interpreter - raise SyntaxError('encoding problem: utf-8') - encoding += '-sig' + raise SyntaxError("encoding problem: utf-8") + encoding += "-sig" return encoding first = read_or_stop() if first.startswith(BOM_UTF8): bom_found = True first = first[3:] - default = 'utf-8-sig' + default = "utf-8-sig" if not first: return default, [] @@ -313,6 +353,7 @@ def detect_encoding(readline): return default, [first, second] + def untokenize(iterable): """Transform tokens back into Python source code. @@ -334,6 +375,7 @@ def untokenize(iterable): ut = Untokenizer() return ut.untokenize(iterable) + def generate_tokens(readline, grammar=None): """ The generate_tokens() generator requires one argument, readline, which @@ -351,8 +393,8 @@ def generate_tokens(readline, grammar=None): logical line; continuation lines are included. """ lnum = parenlev = continued = 0 - numchars = '0123456789' - contstr, needcont = '', 0 + numchars = "0123456789" + contstr, needcont = "", 0 contline = None indents = [0] @@ -365,28 +407,38 @@ def generate_tokens(readline, grammar=None): async_def_indent = 0 async_def_nl = False - while 1: # loop over lines in stream + while 1: # loop over lines in stream try: line = readline() except StopIteration: - line = '' + line = "" lnum = lnum + 1 pos, max = 0, len(line) - if contstr: # continued string + if contstr: # continued string if not line: raise TokenError("EOF in multi-line string", strstart) endmatch = endprog.match(line) if endmatch: pos = end = endmatch.end(0) - yield (STRING, contstr + line[:end], - strstart, (lnum, end), contline + line) - contstr, needcont = '', 0 + yield ( + STRING, + contstr + line[:end], + strstart, + (lnum, end), + contline + line, + ) + contstr, needcont = "", 0 contline = None - elif needcont and line[-2:] != '\\\n' and line[-3:] != '\\\r\n': - yield (ERRORTOKEN, contstr + line, - strstart, (lnum, len(line)), contline) - contstr = '' + elif needcont and line[-2:] != "\\\n" and line[-3:] != "\\\r\n": + yield ( + ERRORTOKEN, + contstr + line, + strstart, + (lnum, len(line)), + contline, + ) + contstr = "" contline = None continue else: @@ -395,42 +447,53 @@ def generate_tokens(readline, grammar=None): continue elif parenlev == 0 and not continued: # new statement - if not line: break + if not line: + break column = 0 - while pos < max: # measure leading whitespace - if line[pos] == ' ': column = column + 1 - elif line[pos] == '\t': column = (column//tabsize + 1)*tabsize - elif line[pos] == '\f': column = 0 - else: break + while pos < max: # measure leading whitespace + if line[pos] == " ": + column = column + 1 + elif line[pos] == "\t": + column = (column // tabsize + 1) * tabsize + elif line[pos] == "\f": + column = 0 + else: + break pos = pos + 1 - if pos == max: break + if pos == max: + break if stashed: yield stashed stashed = None - if line[pos] in '\r\n': # skip blank lines + if line[pos] in "\r\n": # skip blank lines yield (NL, line[pos:], (lnum, pos), (lnum, len(line)), line) continue - if line[pos] == '#': # skip comments - comment_token = line[pos:].rstrip('\r\n') + if line[pos] == "#": # skip comments + comment_token = line[pos:].rstrip("\r\n") nl_pos = pos + len(comment_token) - yield (COMMENT, comment_token, - (lnum, pos), (lnum, pos + len(comment_token)), line) - yield (NL, line[nl_pos:], - (lnum, nl_pos), (lnum, len(line)), line) + yield ( + COMMENT, + comment_token, + (lnum, pos), + (lnum, pos + len(comment_token)), + line, + ) + yield (NL, line[nl_pos:], (lnum, nl_pos), (lnum, len(line)), line) continue - if column > indents[-1]: # count indents + if column > indents[-1]: # count indents indents.append(column) yield (INDENT, line[:pos], (lnum, 0), (lnum, pos), line) - while column < indents[-1]: # count dedents + while column < indents[-1]: # count dedents if column not in indents: raise IndentationError( "unindent does not match any outer indentation level", - ("", lnum, pos, line)) + ("", lnum, pos, line), + ) indents = indents[:-1] if async_def and async_def_indent >= indents[-1]: @@ -438,29 +501,30 @@ def generate_tokens(readline, grammar=None): async_def_nl = False async_def_indent = 0 - yield (DEDENT, '', (lnum, pos), (lnum, pos), line) + yield (DEDENT, "", (lnum, pos), (lnum, pos), line) if async_def and async_def_nl and async_def_indent >= indents[-1]: async_def = False async_def_nl = False async_def_indent = 0 - else: # continued statement + else: # continued statement if not line: raise TokenError("EOF in multi-line statement", (lnum, 0)) continued = 0 while pos < max: pseudomatch = pseudoprog.match(line, pos) - if pseudomatch: # scan for tokens + if pseudomatch: # scan for tokens start, end = pseudomatch.span(1) spos, epos, pos = (lnum, start), (lnum, end), end token, initial = line[start:end], line[start] - if initial in numchars or \ - (initial == '.' and token != '.'): # ordinary number + if initial in numchars or ( + initial == "." and token != "." + ): # ordinary number yield (NUMBER, token, spos, epos, line) - elif initial in '\r\n': + elif initial in "\r\n": newline = NEWLINE if parenlev > 0: newline = NL @@ -471,7 +535,7 @@ def generate_tokens(readline, grammar=None): stashed = None yield (newline, token, spos, epos, line) - elif initial == '#': + elif initial == "#": assert not token.endswith("\n") if stashed: yield stashed @@ -480,7 +544,7 @@ def generate_tokens(readline, grammar=None): elif token in triple_quoted: endprog = endprogs[token] endmatch = endprog.match(line, pos) - if endmatch: # all on one line + if endmatch: # all on one line pos = endmatch.end(0) token = line[start:pos] if stashed: @@ -488,49 +552,61 @@ def generate_tokens(readline, grammar=None): stashed = None yield (STRING, token, spos, (lnum, pos), line) else: - strstart = (lnum, start) # multiple lines + strstart = (lnum, start) # multiple lines contstr = line[start:] contline = line break - elif initial in single_quoted or \ - token[:2] in single_quoted or \ - token[:3] in single_quoted: - if token[-1] == '\n': # continued string + elif ( + initial in single_quoted + or token[:2] in single_quoted + or token[:3] in single_quoted + ): + if token[-1] == "\n": # continued string strstart = (lnum, start) - endprog = (endprogs[initial] or endprogs[token[1]] or - endprogs[token[2]]) + endprog = ( + endprogs[initial] + or endprogs[token[1]] + or endprogs[token[2]] + ) contstr, needcont = line[start:], 1 contline = line break - else: # ordinary string + else: # ordinary string if stashed: yield stashed stashed = None yield (STRING, token, spos, epos, line) - elif initial.isidentifier(): # ordinary name - if token in ('async', 'await'): + elif initial.isidentifier(): # ordinary name + if token in ("async", "await"): if async_keywords or async_def: - yield (ASYNC if token == 'async' else AWAIT, - token, spos, epos, line) + yield ( + ASYNC if token == "async" else AWAIT, + token, + spos, + epos, + line, + ) continue tok = (NAME, token, spos, epos, line) - if token == 'async' and not stashed: + if token == "async" and not stashed: stashed = tok continue - if token in ('def', 'for'): - if (stashed - and stashed[0] == NAME - and stashed[1] == 'async'): + if token in ("def", "for"): + if stashed and stashed[0] == NAME and stashed[1] == "async": - if token == 'def': + if token == "def": async_def = True async_def_indent = indents[-1] - yield (ASYNC, stashed[1], - stashed[2], stashed[3], - stashed[4]) + yield ( + ASYNC, + stashed[1], + stashed[2], + stashed[3], + stashed[4], + ) stashed = None if stashed: @@ -538,7 +614,7 @@ def generate_tokens(readline, grammar=None): stashed = None yield tok - elif initial == '\\': # continued stmt + elif initial == "\\": # continued stmt # This yield is new; needed for better idempotency: if stashed: yield stashed @@ -546,26 +622,31 @@ def generate_tokens(readline, grammar=None): yield (NL, token, spos, (lnum, pos), line) continued = 1 else: - if initial in '([{': parenlev = parenlev + 1 - elif initial in ')]}': parenlev = parenlev - 1 + if initial in "([{": + parenlev = parenlev + 1 + elif initial in ")]}": + parenlev = parenlev - 1 if stashed: yield stashed stashed = None yield (OP, token, spos, epos, line) else: - yield (ERRORTOKEN, line[pos], - (lnum, pos), (lnum, pos+1), line) + yield (ERRORTOKEN, line[pos], (lnum, pos), (lnum, pos + 1), line) pos = pos + 1 if stashed: yield stashed stashed = None - for indent in indents[1:]: # pop remaining indent levels - yield (DEDENT, '', (lnum, 0), (lnum, 0), '') - yield (ENDMARKER, '', (lnum, 0), (lnum, 0), '') + for indent in indents[1:]: # pop remaining indent levels + yield (DEDENT, "", (lnum, 0), (lnum, 0), "") + yield (ENDMARKER, "", (lnum, 0), (lnum, 0), "") -if __name__ == '__main__': # testing + +if __name__ == "__main__": # testing import sys - if len(sys.argv) > 1: tokenize(open(sys.argv[1]).readline) - else: tokenize(sys.stdin.readline) + + if len(sys.argv) > 1: + tokenize(open(sys.argv[1]).readline) + else: + tokenize(sys.stdin.readline) diff --git a/blib2to3/pygram.py b/blib2to3/pygram.py index 0c916a9..7614af7 100644 --- a/blib2to3/pygram.py +++ b/blib2to3/pygram.py @@ -12,12 +12,10 @@ from .pgen2 import driver # The grammar file _GRAMMAR_FILE = os.path.join(os.path.dirname(__file__), "Grammar.txt") -_PATTERN_GRAMMAR_FILE = os.path.join(os.path.dirname(__file__), - "PatternGrammar.txt") +_PATTERN_GRAMMAR_FILE = os.path.join(os.path.dirname(__file__), "PatternGrammar.txt") class Symbols(object): - def __init__(self, grammar): """Initializer. @@ -38,8 +36,7 @@ def initialize(cache_dir=None): global pattern_symbols # Python 2 - python_grammar = driver.load_packaged_grammar("blib2to3", _GRAMMAR_FILE, - cache_dir) + python_grammar = driver.load_packaged_grammar("blib2to3", _GRAMMAR_FILE, cache_dir) python_symbols = Symbols(python_grammar) @@ -56,8 +53,11 @@ def initialize(cache_dir=None): python_grammar_no_print_statement_no_exec_statement_async_keywords = ( python_grammar_no_print_statement_no_exec_statement.copy() ) - python_grammar_no_print_statement_no_exec_statement_async_keywords.async_keywords = True + python_grammar_no_print_statement_no_exec_statement_async_keywords.async_keywords = ( + True + ) - pattern_grammar = driver.load_packaged_grammar("blib2to3", _PATTERN_GRAMMAR_FILE, - cache_dir) + pattern_grammar = driver.load_packaged_grammar( + "blib2to3", _PATTERN_GRAMMAR_FILE, cache_dir + ) pattern_symbols = Symbols(pattern_grammar) diff --git a/blib2to3/pytree.py b/blib2to3/pytree.py index 4da5700..6776491 100644 --- a/blib2to3/pytree.py +++ b/blib2to3/pytree.py @@ -18,16 +18,21 @@ from io import StringIO HUGE = 0x7FFFFFFF # maximum repeat count, default max _type_reprs = {} + + def type_repr(type_num): global _type_reprs if not _type_reprs: from .pygram import python_symbols + # printing tokens is possible but not as useful # from .pgen2 import token // token.__dict__.items(): for name, val in python_symbols.__dict__.items(): - if type(val) == int: _type_reprs[val] = name + if type(val) == int: + _type_reprs[val] = name return _type_reprs.setdefault(type_num, type_num) + class Base(object): """ @@ -40,7 +45,7 @@ class Base(object): """ # Default values for instance variables - type = None # int: token number (< 256) or symbol number (>= 256) + type = None # int: token number (< 256) or symbol number (>= 256) parent = None # Parent node pointer, or None children = () # Tuple of subnodes was_changed = False @@ -61,7 +66,7 @@ class Base(object): return NotImplemented return self._eq(other) - __hash__ = None # For Py3 compatibility. + __hash__ = None # For Py3 compatibility. def _eq(self, other): """ @@ -198,17 +203,16 @@ class Base(object): return next_sib.prefix if sys.version_info < (3, 0): + def __str__(self): return str(self).encode("ascii") + class Node(Base): """Concrete implementation for interior nodes.""" - def __init__(self,type, children, - context=None, - prefix=None, - fixers_applied=None): + def __init__(self, type, children, context=None, prefix=None, fixers_applied=None): """ Initializer. @@ -233,9 +237,11 @@ class Node(Base): def __repr__(self): """Return a canonical string representation.""" - return "%s(%s, %r)" % (self.__class__.__name__, - type_repr(self.type), - self.children) + return "%s(%s, %r)" % ( + self.__class__.__name__, + type_repr(self.type), + self.children, + ) def __unicode__(self): """ @@ -254,8 +260,11 @@ class Node(Base): def clone(self): """Return a cloned (deep) copy of self.""" - return Node(self.type, [ch.clone() for ch in self.children], - fixers_applied=self.fixers_applied) + return Node( + self.type, + [ch.clone() for ch in self.children], + fixers_applied=self.fixers_applied, + ) def post_order(self): """Return a post-order iterator for the tree.""" @@ -328,19 +337,17 @@ class Node(Base): previous = current _next[id(current)] = None + class Leaf(Base): """Concrete implementation for leaf nodes.""" # Default values for instance variables _prefix = "" # Whitespace and comments preceding this token in the input - lineno = 0 # Line where this token starts in the input - column = 0 # Column where this token starts in the input + lineno = 0 # Line where this token starts in the input + column = 0 # Column where this token starts in the input - def __init__(self, type, value, - context=None, - prefix=None, - fixers_applied=[]): + def __init__(self, type, value, context=None, prefix=None, fixers_applied=[]): """ Initializer. @@ -359,9 +366,12 @@ class Leaf(Base): def __repr__(self): """Return a canonical string representation.""" from .pgen2.token import tok_name - return "%s(%s, %r)" % (self.__class__.__name__, - tok_name.get(self.type, self.type), - self.value) + + return "%s(%s, %r)" % ( + self.__class__.__name__, + tok_name.get(self.type, self.type), + self.value, + ) def __unicode__(self): """ @@ -380,9 +390,12 @@ class Leaf(Base): def clone(self): """Return a cloned (deep) copy of self.""" - return Leaf(self.type, self.value, - (self.prefix, (self.lineno, self.column)), - fixers_applied=self.fixers_applied) + return Leaf( + self.type, + self.value, + (self.prefix, (self.lineno, self.column)), + fixers_applied=self.fixers_applied, + ) def leaves(self): yield self @@ -407,6 +420,7 @@ class Leaf(Base): self.changed() self._prefix = prefix + def convert(gr, raw_node): """ Convert raw node information to a Node or Leaf instance. @@ -443,9 +457,9 @@ class BasePattern(object): """ # Defaults for instance variables - type = None # Node type (token if < 256, symbol if >= 256) + type = None # Node type (token if < 256, symbol if >= 256) content = None # Optional content matching pattern - name = None # Optional name used to store match in results dict + name = None # Optional name used to store match in results dict def __new__(cls, *args, **kwds): """Constructor that prevents BasePattern from being instantiated.""" @@ -513,7 +527,6 @@ class BasePattern(object): class LeafPattern(BasePattern): - def __init__(self, type=None, content=None, name=None): """ Initializer. Takes optional type, content, and name. @@ -660,7 +673,7 @@ class WildcardPattern(BasePattern): # Check sanity of alternatives assert len(content), repr(content) # Can't have zero alternatives for alt in content: - assert len(alt), repr(alt) # Can have empty alternatives + assert len(alt), repr(alt) # Can have empty alternatives self.content = content self.min = min self.max = max @@ -669,20 +682,29 @@ class WildcardPattern(BasePattern): def optimize(self): """Optimize certain stacked wildcard patterns.""" subpattern = None - if (self.content is not None and - len(self.content) == 1 and len(self.content[0]) == 1): + if ( + self.content is not None + and len(self.content) == 1 + and len(self.content[0]) == 1 + ): subpattern = self.content[0][0] if self.min == 1 and self.max == 1: if self.content is None: return NodePattern(name=self.name) - if subpattern is not None and self.name == subpattern.name: + if subpattern is not None and self.name == subpattern.name: return subpattern.optimize() - if (self.min <= 1 and isinstance(subpattern, WildcardPattern) and - subpattern.min <= 1 and self.name == subpattern.name): - return WildcardPattern(subpattern.content, - self.min*subpattern.min, - self.max*subpattern.max, - subpattern.name) + if ( + self.min <= 1 + and isinstance(subpattern, WildcardPattern) + and subpattern.min <= 1 + and self.name == subpattern.name + ): + return WildcardPattern( + subpattern.content, + self.min * subpattern.min, + self.max * subpattern.max, + subpattern.name, + ) return self def match(self, node, results=None): @@ -798,7 +820,7 @@ class WildcardPattern(BasePattern): if count < self.max: for alt in self.content: for c0, r0 in generate_matches(alt, nodes): - for c1, r1 in self._recursive_matches(nodes[c0:], count+1): + for c1, r1 in self._recursive_matches(nodes[c0:], count + 1): r = {} r.update(r0) r.update(r1) @@ -806,7 +828,6 @@ class WildcardPattern(BasePattern): class NegatedPattern(BasePattern): - def __init__(self, content=None): """ Initializer. diff --git a/tests/test_black.py b/tests/test_black.py index 10efbcf..107f77d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -158,6 +158,16 @@ class BlackTestCase(unittest.TestCase): result = runner.invoke(black.main, args) self.assertEqual(result.exit_code, exit_code, msg=runner.stderr_bytes.decode()) + @patch("black.dump_to_file", dump_to_stderr) + def checkSourceFile(self, name: str) -> None: + path = THIS_DIR.parent / name + source, expected = read_data(str(path), data=False) + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + self.assertFalse(ff(path)) + @patch("black.dump_to_file", dump_to_stderr) def test_empty(self) -> None: source = expected = "" @@ -177,23 +187,44 @@ class BlackTestCase(unittest.TestCase): os.unlink(tmp_file) self.assertFormatEqual(expected, actual) - @patch("black.dump_to_file", dump_to_stderr) def test_self(self) -> None: - source, expected = read_data("test_black", data=False) - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) - self.assertFalse(ff(THIS_FILE)) + self.checkSourceFile("tests/test_black.py") - @patch("black.dump_to_file", dump_to_stderr) def test_black(self) -> None: - source, expected = read_data("../black", data=False) - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) - self.assertFalse(ff(THIS_DIR / ".." / "black.py")) + self.checkSourceFile("black.py") + + def test_pygram(self) -> None: + self.checkSourceFile("blib2to3/pygram.py") + + def test_pytree(self) -> None: + self.checkSourceFile("blib2to3/pytree.py") + + def test_conv(self) -> None: + self.checkSourceFile("blib2to3/pgen2/conv.py") + + def test_driver(self) -> None: + self.checkSourceFile("blib2to3/pgen2/driver.py") + + def test_grammar(self) -> None: + self.checkSourceFile("blib2to3/pgen2/grammar.py") + + def test_literals(self) -> None: + self.checkSourceFile("blib2to3/pgen2/literals.py") + + def test_parse(self) -> None: + self.checkSourceFile("blib2to3/pgen2/parse.py") + + def test_pgen(self) -> None: + self.checkSourceFile("blib2to3/pgen2/pgen.py") + + def test_tokenize(self) -> None: + self.checkSourceFile("blib2to3/pgen2/tokenize.py") + + def test_token(self) -> None: + self.checkSourceFile("blib2to3/pgen2/token.py") + + def test_setup(self) -> None: + self.checkSourceFile("setup.py") def test_piping(self) -> None: source, expected = read_data("../black", data=False) @@ -230,15 +261,6 @@ class BlackTestCase(unittest.TestCase): actual = actual.rstrip() + "\n" # the diff output has a trailing space self.assertEqual(expected, actual) - @patch("black.dump_to_file", dump_to_stderr) - def test_setup(self) -> None: - source, expected = read_data("../setup", data=False) - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) - self.assertFalse(ff(THIS_DIR / ".." / "setup.py")) - @patch("black.dump_to_file", dump_to_stderr) def test_function(self) -> None: source, expected = read_data("function") -- 2.39.2 From 65c5a0d9f180c4b36ea98917cb3b569f8e4f892f Mon Sep 17 00:00:00 2001 From: Charles Reid <53452777+chmreid@users.noreply.github.com> Date: Sun, 20 Oct 2019 07:00:48 -0700 Subject: [PATCH 05/16] restore cursor to same line of code, not same line of buffer (#989) --- plugin/black.vim | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/plugin/black.vim b/plugin/black.vim index 15a13d1..29b2992 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -110,7 +110,29 @@ def Black(): string_normalization=not bool(int(vim.eval("g:black_skip_string_normalization"))), is_pyi=vim.current.buffer.name.endswith('.pyi'), ) - buffer_str = '\n'.join(vim.current.buffer) + '\n' + (cursor_line, cursor_column) = vim.current.window.cursor + cb = vim.current.buffer[:] + cb_bc = cb[0:cursor_line] + # Format all code before the cursor. + # Detect unclosed blocks, close them with pass. + last_line = cb_bc[-1] + if last_line.rstrip().endswith(":"): + cb_bc[-1] = last_line + " pass" + # Determine old:new cursor location mapping + buffer_str_before = '\n'.join(cb_bc)+'\n' + try: + new_buffer_str_before = black.format_file_contents(buffer_str_before, fast=fast, mode=mode) + new_cb = new_buffer_str_before.split('\n')[:-1] + new_cursor_line = len(new_cb) + new_cursor = (new_cursor_line, cursor_column) + except black.NothingChanged: + new_cursor_line = cursor_line + new_cursor = (new_cursor_line, cursor_column) + except Exception as exc: + print(exc) + # Now we know where the cursor should be + # when we format the entire buffer. Do it: + buffer_str = '\n'.join(cb) + '\n' try: new_buffer_str = black.format_file_contents(buffer_str, fast=fast, mode=mode) except black.NothingChanged: @@ -118,10 +140,12 @@ def Black(): except Exception as exc: print(exc) else: - cursor = vim.current.window.cursor - vim.current.buffer[:] = new_buffer_str.split('\n')[:-1] + # Replace the buffer + new_cb = new_buffer_str.split('\n')[:-1] + vim.current.buffer[:] = new_cb + # Restore the cursor to its rightful place try: - vim.current.window.cursor = cursor + vim.current.window.cursor = new_cursor except vim.error: vim.current.window.cursor = (len(vim.current.buffer), 0) print(f'Reformatted in {time.time() - start:.4f}s.') -- 2.39.2 From 84e22b75c6baeb231dee52b7bd086f547063e0e4 Mon Sep 17 00:00:00 2001 From: Florent Thiery Date: Sun, 20 Oct 2019 16:07:07 +0200 Subject: [PATCH 06/16] add instructions to Readme for installing vim plugin using vim native package loading, and how to map a key to run it manually (#944) --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 76ae9cf..0d14b68 100644 --- a/README.md +++ b/README.md @@ -740,6 +740,12 @@ Plugin 'psf/black' ``` or you can copy the plugin from [plugin/black.vim](https://github.com/psf/black/tree/master/plugin/black.vim). + +``` +mkdir -p ~/.vim/pack/python/start/black/plugin +curl https://raw.githubusercontent.com/psf/black/master/plugin/black.vim -o ~/.vim/pack/python/start/black/plugin/black.vim +``` + Let me know if this requires any changes to work with Vim 8's builtin `packadd`, or Pathogen, and so on. @@ -762,6 +768,12 @@ To run *Black* on save, add the following line to `.vimrc` or `init.vim`: autocmd BufWritePre *.py execute ':Black' ``` +To run *Black* on a key press (e.g. F9 below), add this: + +``` +nnoremap :Black +``` + **How to get Vim with Python 3.6?** On Ubuntu 17.10 Vim comes with Python 3.6 by default. On macOS with Homebrew run: `brew install vim --with-python3`. -- 2.39.2 From 9854d4b375ef84d651892785165a6c7b0e9f391b Mon Sep 17 00:00:00 2001 From: Augie Fackler Date: Sun, 20 Oct 2019 10:08:34 -0400 Subject: [PATCH 07/16] Tweak collection literals to explode with trailing comma (#826) --- black.py | 129 +++++++++++++-------- tests/data/collections.py | 160 ++++++++++++++++++++++++++ tests/data/comments2.py | 4 +- tests/data/comments7.py | 2 +- tests/data/expression.diff | 31 +++-- tests/data/expression.py | 24 +++- tests/data/fmtonoff.py | 2 +- tests/data/function.py | 2 +- tests/data/function2.py | 2 +- tests/data/function_trailing_comma.py | 4 +- tests/test_black.py | 7 ++ 11 files changed, 299 insertions(+), 68 deletions(-) create mode 100644 tests/data/collections.py diff --git a/black.py b/black.py index 0776983..40b4d2b 100644 --- a/black.py +++ b/black.py @@ -1240,6 +1240,61 @@ class Line: Leaf(token.DOT, ".") for _ in range(3) ] + @property + def is_collection_with_optional_trailing_comma(self) -> bool: + """Is this line a collection literal with a trailing comma that's optional? + + Note that the trailing comma in a 1-tuple is not optional. + """ + if not self.leaves or len(self.leaves) < 4: + return False + # Look for and address a trailing colon. + if self.leaves[-1].type == token.COLON: + closer = self.leaves[-2] + close_index = -2 + else: + closer = self.leaves[-1] + close_index = -1 + if closer.type not in CLOSING_BRACKETS or self.inside_brackets: + return False + if closer.type == token.RPAR: + # Tuples require an extra check, because if there's only + # one element in the tuple removing the comma unmakes the + # tuple. + # + # We also check for parens before looking for the trailing + # comma because in some cases (eg assigning a dict + # literal) the literal gets wrapped in temporary parens + # during parsing. This case is covered by the + # collections.py test data. + opener = closer.opening_bracket + for _open_index, leaf in enumerate(self.leaves): + if leaf is opener: + break + else: + # Couldn't find the matching opening paren, play it safe. + return False + commas = 0 + comma_depth = self.leaves[close_index - 1].bracket_depth + for leaf in self.leaves[_open_index + 1 : close_index]: + if leaf.bracket_depth == comma_depth and leaf.type == token.COMMA: + commas += 1 + if commas > 1: + # We haven't looked yet for the trailing comma because + # we might also have caught noop parens. + return self.leaves[close_index - 1].type == token.COMMA + elif commas == 1: + return False # it's either a one-tuple or didn't have a trailing comma + if self.leaves[close_index - 1].type in CLOSING_BRACKETS: + close_index -= 1 + closer = self.leaves[close_index] + if closer.type == token.RPAR: + # TODO: this is a gut feeling. Will we ever see this? + return False + if self.leaves[close_index - 1].type != token.COMMA: + return False + return True + @property def is_def(self) -> bool: """Is this a function definition? (Also returns True for async defs.)""" @@ -1365,60 +1420,39 @@ class Line: def maybe_remove_trailing_comma(self, closing: Leaf) -> bool: """Remove trailing comma if there is one and it's safe.""" + if not (self.leaves and self.leaves[-1].type == token.COMMA): + return False + # We remove trailing commas only in the case of importing a + # single name from a module. if not ( self.leaves + and self.is_import + and len(self.leaves) > 4 and self.leaves[-1].type == token.COMMA and closing.type in CLOSING_BRACKETS + and self.leaves[-4].type == token.NAME + and ( + # regular `from foo import bar,` + self.leaves[-4].value == "import" + # `from foo import (bar as baz,) + or ( + len(self.leaves) > 6 + and self.leaves[-6].value == "import" + and self.leaves[-3].value == "as" + ) + # `from foo import bar as baz,` + or ( + len(self.leaves) > 5 + and self.leaves[-5].value == "import" + and self.leaves[-3].value == "as" + ) + ) + and closing.type == token.RPAR ): return False - if closing.type == token.RBRACE: - self.remove_trailing_comma() - return True - - if closing.type == token.RSQB: - comma = self.leaves[-1] - if comma.parent and comma.parent.type == syms.listmaker: - self.remove_trailing_comma() - return True - - # For parens let's check if it's safe to remove the comma. - # Imports are always safe. - if self.is_import: - self.remove_trailing_comma() - return True - - # Otherwise, if the trailing one is the only one, we might mistakenly - # change a tuple into a different type by removing the comma. - depth = closing.bracket_depth + 1 - commas = 0 - opening = closing.opening_bracket - for _opening_index, leaf in enumerate(self.leaves): - if leaf is opening: - break - - else: - return False - - for leaf in self.leaves[_opening_index + 1 :]: - if leaf is closing: - break - - bracket_depth = leaf.bracket_depth - if bracket_depth == depth and leaf.type == token.COMMA: - commas += 1 - if leaf.parent and leaf.parent.type in { - syms.arglist, - syms.typedargslist, - }: - commas += 1 - break - - if commas > 1: - self.remove_trailing_comma() - return True - - return False + self.remove_trailing_comma() + return True def append_comment(self, comment: Leaf) -> bool: """Add an inline or standalone comment to the line.""" @@ -2363,6 +2397,7 @@ def split_line( if ( not line.contains_uncollapsable_type_comments() and not line.should_explode + and not line.is_collection_with_optional_trailing_comma and ( is_line_short_enough(line, line_length=line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() diff --git a/tests/data/collections.py b/tests/data/collections.py new file mode 100644 index 0000000..d408bf9 --- /dev/null +++ b/tests/data/collections.py @@ -0,0 +1,160 @@ +import core, time, a + +from . import A, B, C + +# unwraps +from foo import ( + bar, +) + +# stays wrapped +from foo import ( + baz, + qux, +) + +# as doesn't get confusing when unwrapped +from foo import ( + xyzzy as magic, +) + +a = {1,2,3,} +b = { +1,2, + 3} +c = { + 1, + 2, + 3, +} +x = 1, +y = narf(), +nested = {(1,2,3),(4,5,6),} +nested_no_trailing_comma = {(1,2,3),(4,5,6)} +nested_long_lines = ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "cccccccccccccccccccccccccccccccccccccccc", (1, 2, 3), "dddddddddddddddddddddddddddddddddddddddd"] +{"oneple": (1,),} +{"oneple": (1,)} +['ls', 'lsoneple/%s' % (foo,)] +x = {"oneple": (1,)} +y = {"oneple": (1,),} +assert False, ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" % bar) + +# looping over a 1-tuple should also not get wrapped +for x in (1,): + pass +for (x,) in (1,), (2,), (3,): + pass + +[1, 2, 3,] + +division_result_tuple = (6/2,) +print("foo %r", (foo.bar,)) + +if True: + IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( + Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING + | {pylons.controllers.WSGIController} + ) + +if True: + ec2client.get_waiter('instance_stopped').wait( + InstanceIds=[instance.id], + WaiterConfig={ + 'Delay': 5, + }) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], + WaiterConfig={"Delay": 5,}, + ) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], WaiterConfig={"Delay": 5,}, + ) + +# output + + +import core, time, a + +from . import A, B, C + +# unwraps +from foo import bar + +# stays wrapped +from foo import ( + baz, + qux, +) + +# as doesn't get confusing when unwrapped +from foo import xyzzy as magic + +a = { + 1, + 2, + 3, +} +b = {1, 2, 3} +c = { + 1, + 2, + 3, +} +x = (1,) +y = (narf(),) +nested = { + (1, 2, 3), + (4, 5, 6), +} +nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} +nested_long_lines = [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccc", + (1, 2, 3), + "dddddddddddddddddddddddddddddddddddddddd", +] +{ + "oneple": (1,), +} +{"oneple": (1,)} +["ls", "lsoneple/%s" % (foo,)] +x = {"oneple": (1,)} +y = { + "oneple": (1,), +} +assert False, ( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" + % bar +) + +# looping over a 1-tuple should also not get wrapped +for x in (1,): + pass +for (x,) in (1,), (2,), (3,): + pass + +[ + 1, + 2, + 3, +] + +division_result_tuple = (6 / 2,) +print("foo %r", (foo.bar,)) + +if True: + IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING | { + pylons.controllers.WSGIController + } + +if True: + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], WaiterConfig={"Delay": 5,} + ) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], WaiterConfig={"Delay": 5,}, + ) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], WaiterConfig={"Delay": 5,}, + ) diff --git a/tests/data/comments2.py b/tests/data/comments2.py index e928f6d..489661e 100644 --- a/tests/data/comments2.py +++ b/tests/data/comments2.py @@ -63,7 +63,7 @@ def inline_comments_in_brackets_ruin_everything(): parameters.children = [ children[0], # (1 body, - children[-1], # )1 + children[-1] # )1 ] parameters.children = [ children[0], @@ -142,7 +142,7 @@ short syms.simple_stmt, [ Node(statement, result), - Leaf(token.NEWLINE, '\n'), # FIXME: \r\n? + Leaf(token.NEWLINE, '\n') # FIXME: \r\n? ], ) diff --git a/tests/data/comments7.py b/tests/data/comments7.py index 088dc99..948b3b0 100644 --- a/tests/data/comments7.py +++ b/tests/data/comments7.py @@ -94,7 +94,7 @@ result = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx def func(): c = call( - 0.0123, 0.0456, 0.0789, 0.0123, 0.0789, a[-1] # type: ignore + 0.0123, 0.0456, 0.0789, 0.0123, 0.0789, a[-1], # type: ignore ) # The type: ignore exception only applies to line length, not diff --git a/tests/data/expression.diff b/tests/data/expression.diff index eff98f9..da20094 100644 --- a/tests/data/expression.diff +++ b/tests/data/expression.diff @@ -11,7 +11,7 @@ True False 1 -@@ -29,63 +29,84 @@ +@@ -29,63 +29,96 @@ ~great +value -1 @@ -61,14 +61,26 @@ [] [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] -[1, 2, 3,] -+[1, 2, 3] ++[ ++ 1, ++ 2, ++ 3, ++] [*a] [*range(10)] -[*a, 4, 5,] -[4, *a, 5,] -[this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, *more] -+[*a, 4, 5] -+[4, *a, 5] ++[ ++ *a, ++ 4, ++ 5, ++] ++[ ++ 4, ++ *a, ++ 5, ++] +[ + this_is_a_very_long_variable_which_will_force_a_delimiter_split, + element, @@ -118,14 +130,16 @@ call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl -@@ -94,23 +115,23 @@ +@@ -94,23 +127,25 @@ 1.0 .real ....__class__ list[str] dict[str, int] tuple[str, ...] -tuple[str, int, float, dict[str, int],] -+tuple[str, int, float, dict[str, int]] ++tuple[ ++ str, int, float, dict[str, int], ++] very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], ] @@ -146,7 +160,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -134,113 +155,171 @@ +@@ -134,113 +169,171 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -199,7 +213,7 @@ + .filter( + models.Customer.account_id == account_id, models.Customer.email == email_address + ) -+ .order_by(models.Customer.id.asc()) ++ .order_by(models.Customer.id.asc(),) + .all() +) Ø = set() @@ -391,3 +405,4 @@ return True last_call() # standalone comment at ENDMARKER + diff --git a/tests/data/expression.py b/tests/data/expression.py index 3851249..c9b149f 100644 --- a/tests/data/expression.py +++ b/tests/data/expression.py @@ -314,11 +314,23 @@ str or None if (1 if True else 2) else str or bytes or None (1, 2, 3) [] [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] -[1, 2, 3] +[ + 1, + 2, + 3, +] [*a] [*range(10)] -[*a, 4, 5] -[4, *a, 5] +[ + *a, + 4, + 5, +] +[ + 4, + *a, + 5, +] [ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, @@ -367,7 +379,9 @@ call.me(maybe) list[str] dict[str, int] tuple[str, ...] -tuple[str, int, float, dict[str, int]] +tuple[ + str, int, float, dict[str, int], +] very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], ] @@ -445,7 +459,7 @@ result = ( .filter( models.Customer.account_id == account_id, models.Customer.email == email_address ) - .order_by(models.Customer.id.asc()) + .order_by(models.Customer.id.asc(),) .all() ) Ø = set() diff --git a/tests/data/fmtonoff.py b/tests/data/fmtonoff.py index 5f62af0..fff751e 100644 --- a/tests/data/fmtonoff.py +++ b/tests/data/fmtonoff.py @@ -150,7 +150,7 @@ def single_literal_yapf_disable(): BAZ = { (1, 2, 3, 4), (5, 6, 7, 8), - (9, 10, 11, 12), + (9, 10, 11, 12) } # yapf: disable cfg.rule( "Default", "address", diff --git a/tests/data/function.py b/tests/data/function.py index 4754588..51234a1 100644 --- a/tests/data/function.py +++ b/tests/data/function.py @@ -230,7 +230,7 @@ def trailing_comma(): } -def f(a, **kwargs) -> A: +def f(a, **kwargs,) -> A: return ( yield from A( very_long_argument_name1=very_long_value_for_the_argument, diff --git a/tests/data/function2.py b/tests/data/function2.py index 0c9da12..f57a3f5 100644 --- a/tests/data/function2.py +++ b/tests/data/function2.py @@ -24,7 +24,7 @@ def h(): # output -def f(a, **kwargs) -> A: +def f(a, **kwargs,) -> A: with cache_dir(): if something: result = CliRunner().invoke( diff --git a/tests/data/function_trailing_comma.py b/tests/data/function_trailing_comma.py index 29fd99b..f2594c6 100644 --- a/tests/data/function_trailing_comma.py +++ b/tests/data/function_trailing_comma.py @@ -6,9 +6,9 @@ def f(a:int=1,): # output -def f(a): +def f(a,): ... -def f(a: int = 1): +def f(a: int = 1,): ... diff --git a/tests/test_black.py b/tests/test_black.py index 107f77d..a92798d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1369,6 +1369,13 @@ class BlackTestCase(unittest.TestCase): self.assertIn(path, pyi_cache) self.assertNotIn(path, normal_cache) + def test_collections(self) -> None: + source, expected = read_data("collections") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + def test_pipe_force_py36(self) -> None: source, expected = read_data("force_py36") result = CliRunner().invoke( -- 2.39.2 From b73ec93fa71ae8980d0743555a9f49f46528e6e1 Mon Sep 17 00:00:00 2001 From: Joe Antonakakis Date: Sun, 20 Oct 2019 07:16:26 -0700 Subject: [PATCH 08/16] Add .svn to default exclusion list (#965) --- black.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/black.py b/black.py index 40b4d2b..65545e9 100644 --- a/black.py +++ b/black.py @@ -54,9 +54,7 @@ from blib2to3.pgen2.parse import ParseError from _version import version as __version__ DEFAULT_LINE_LENGTH = 88 -DEFAULT_EXCLUDES = ( - r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist)/" -) +DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950 DEFAULT_INCLUDES = r"\.pyi?$" CACHE_DIR = Path(user_cache_dir("black", version=__version__)) -- 2.39.2 From 9027ca63cac2b5ed24117ea6f5b6e760f1f2e11d Mon Sep 17 00:00:00 2001 From: Josh Bode Date: Sun, 20 Oct 2019 16:24:50 +0200 Subject: [PATCH 09/16] Change how venv path is modified in vim plugin (#804) - Check if black venv path is not already in `sys.path` - Append (not insert) path so that black doesn't incorrectly import backports (e.g. `typing`) Avoids this error if `typing` is present in venv: ``` Traceback (most recent call last): File "", line 56, in File "/home/josh/.virtualenvs/default/lib/python3.7/site-packages/black.py", line 19, in from typing import ( File "/home/josh/.virtualenvs/default/lib/python3.7/site-packages/typing.py", line 1356, in class Callable(extra=collections_abc.Callable, metaclass=CallableMeta): File "/home/josh/.virtualenvs/default/lib/python3.7/site-packages/typing.py", line 1004, in __new__ self._abc_registry = extra._abc_registry AttributeError: type object 'Callable' has no attribute '_abc_registry' ``` --- plugin/black.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/black.vim b/plugin/black.vim index 29b2992..030c18f 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -94,8 +94,8 @@ def _initialize_black_env(upgrade=False): print('DONE! You are all set, thanks for waiting ✨ 🍰 ✨') if first_install: print('Pro-tip: to upgrade Black in the future, use the :BlackUpgrade command and restart Vim.\n') - if sys.path[0] != virtualenv_site_packages: - sys.path.insert(0, virtualenv_site_packages) + if virtualenv_site_packages not in sys.path: + sys.path.append(virtualenv_site_packages) return True if _initialize_black_env(): -- 2.39.2 From 893dd952a5e128ae71d9e9c93504d9a1092388a6 Mon Sep 17 00:00:00 2001 From: Andrew Thorp Date: Sun, 20 Oct 2019 10:26:17 -0400 Subject: [PATCH 10/16] Update README.md (#906) Add Kakoune integration instructions --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0d14b68..ddca8a4 100644 --- a/README.md +++ b/README.md @@ -810,6 +810,14 @@ the [Python Language Server](https://github.com/palantir/python-language-server) Use [python-black](https://atom.io/packages/python-black). +### Kakoune + +Add the following hook to your kakrc, then run black with `:format`. +``` +hook global WinSetOption filetype=python %{ + set-option window formatcmd 'black -q -' +} +``` ### Other editors -- 2.39.2 From 51f1e0a8734195aab4b70b32908cdb9607d8f82c Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Sun, 20 Oct 2019 07:30:44 -0700 Subject: [PATCH 11/16] Create new issue templates (#934) * Create new issue templates * style -> design * Apply suggestions from code review Co-Authored-By: Hugo van Kemenade --- .github/ISSUE_TEMPLATE.md | 16 ---------- .github/ISSUE_TEMPLATE/bug_report.md | 37 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ .github/ISSUE_TEMPLATE/style_issue.md | 30 ++++++++++++++++++ 4 files changed, 87 insertions(+), 16 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/style_issue.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 2dfd64b..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ -Howdy! Sorry you're having trouble. To expedite your experience, -provide some basics for me: - -Operating system: -Python version: -*Black* version: -Does also happen on master: - -To answer the last question, you have two options: -1. Use the online formatter at https://black.now.sh/?version=master, which will use the latest master branch. -2. Or run black on your machine: - * create a new virtualenv (make sure it's the same Python version); - * clone this repository; - * run `pip install -e .`; - * make sure it's sane by running `python setup.py test`; and - * run `black` like you did last time. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4c51d30 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Take this file '...' +2. Run *Black* on it with these arguments '....' +3. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** +- Version: [e.g. master] +- OS and Python version: [e.g. Linux/Python 3.7.4rc1] + +**Does this bug also happen on master?** +To answer this, you have two options: +1. Use the online formatter at https://black.now.sh/?version=master, which will use the latest master branch. +2. Or run *Black* on your machine: + * create a new virtualenv (make sure it's the same Python version); + * clone this repository; + * run `pip install -e .`; + * make sure it's sane by running `python setup.py test`; and + * run `black` like you did last time. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6532412 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/style_issue.md b/.github/ISSUE_TEMPLATE/style_issue.md new file mode 100644 index 0000000..a1f7c48 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/style_issue.md @@ -0,0 +1,30 @@ +--- +name: Style issue +about: Help us improve the Black style +title: '' +labels: design +assignees: '' + +--- + +**Describe the style change** +A clear and concise description of how the style can be improved. + +**Examples in the current *Black* style** +Think of some short code snippets that show how the current *Black* style is not great: +``` +def f(): + "Make sure this code is blackened""" + pass +``` + +**Desired style** +How do you think *Black* should format the above snippets: +``` +def f( + ): + pass +``` + +**Additional context** +Add any other context about the problem here. -- 2.39.2 From 4bcae4cf839aeba828bcdc4764369ae790f81d0d Mon Sep 17 00:00:00 2001 From: Asger Hautop Drewsen Date: Sun, 20 Oct 2019 16:43:02 +0200 Subject: [PATCH 12/16] Use better default venv dir when using neovim (#937) --- README.md | 2 +- plugin/black.vim | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ddca8a4..3ef758f 100644 --- a/README.md +++ b/README.md @@ -725,7 +725,7 @@ Configuration: * `g:black_fast` (defaults to `0`) * `g:black_linelength` (defaults to `88`) * `g:black_skip_string_normalization` (defaults to `0`) -* `g:black_virtualenv` (defaults to `~/.vim/black`) +* `g:black_virtualenv` (defaults to `~/.vim/black` or `~/.local/share/nvim/black`) To install with [vim-plug](https://github.com/junegunn/vim-plug): diff --git a/plugin/black.vim b/plugin/black.vim index 030c18f..9def61a 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -24,7 +24,11 @@ endif let g:load_black = "py1.0" if !exists("g:black_virtualenv") - let g:black_virtualenv = "~/.vim/black" + if has("nvim") + let g:black_virtualenv = "~/.local/share/nvim/black" + else + let g:black_virtualenv = "~/.vim/black" + endif endif if !exists("g:black_fast") let g:black_fast = 0 -- 2.39.2 From 4b5852e44ac9d2d2aa10fbbfa66a5f02421e7768 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 20 Oct 2019 17:52:07 +0300 Subject: [PATCH 13/16] Set correct return statement for `is_type_comment` function (#929) --- black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/black.py b/black.py index 65545e9..d1e87a9 100644 --- a/black.py +++ b/black.py @@ -2777,7 +2777,7 @@ def is_type_comment(leaf: Leaf, suffix: str = "") -> bool: Only returns true for type comments for now.""" t = leaf.type v = leaf.value - return t in {token.COMMENT, t == STANDALONE_COMMENT} and v.startswith( + return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith( "# type:" + suffix ) -- 2.39.2 From a73d25883aaaa3fa3a10ea2ddfcb70ac1efd448e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 20 Oct 2019 08:35:57 -0700 Subject: [PATCH 14/16] fix CI (#1078) --- black.py | 4 +--- tests/data/comments2.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/black.py b/black.py index d1e87a9..3842be9 100644 --- a/black.py +++ b/black.py @@ -2777,9 +2777,7 @@ def is_type_comment(leaf: Leaf, suffix: str = "") -> bool: Only returns true for type comments for now.""" t = leaf.type v = leaf.value - return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith( - "# type:" + suffix - ) + return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix) def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: diff --git a/tests/data/comments2.py b/tests/data/comments2.py index 489661e..f4a30b8 100644 --- a/tests/data/comments2.py +++ b/tests/data/comments2.py @@ -310,7 +310,7 @@ short ) -CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final +CONFIG_FILES = [CONFIG_FILE,] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final ####################### ### SECTION COMMENT ### -- 2.39.2 From 14b28c89c22659e1f935bc0ac22ee03d90bcc290 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 20 Oct 2019 09:02:17 -0700 Subject: [PATCH 15/16] Back out #850 (#1079) Fixes #1042 (and probably #1044 which looks like the same thing). The issue with the "obviously unnecessary" parentheses that #850 removed is that sometimes they're necessary to help Black fit something in one line. I didn't see an obvious solution that still removes the parens #850 was intended to remove, so let's back out this change for now in the interest of unblocking a release. This PR also adds a test adapted from the failing example in #1042, so that if we try to reapply the #850 change we don't break the same case again. --- black.py | 20 -------------------- tests/data/expression.diff | 3 +-- tests/data/expression.py | 2 +- tests/data/remove_parens.py | 32 ++++++++++++++++++++++++++++---- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/black.py b/black.py index 3842be9..151c1a6 100644 --- a/black.py +++ b/black.py @@ -1714,26 +1714,6 @@ class LineGenerator(Visitor[Line]): self.current_line.append(node) yield from super().visit_default(node) - def visit_atom(self, node: Node) -> Iterator[Line]: - # Always make parentheses invisible around a single node, because it should - # not be needed (except in the case of yield, where removing the parentheses - # produces a SyntaxError). - if ( - len(node.children) == 3 - and isinstance(node.children[0], Leaf) - and node.children[0].type == token.LPAR - and isinstance(node.children[2], Leaf) - and node.children[2].type == token.RPAR - and isinstance(node.children[1], Leaf) - and not ( - node.children[1].type == token.NAME - and node.children[1].value == "yield" - ) - ): - node.children[0].value = "" - node.children[2].value = "" - yield from super().visit_default(node) - def visit_factor(self, node: Node) -> Iterator[Line]: """Force parentheses between a unary op and a binary power: diff --git a/tests/data/expression.diff b/tests/data/expression.diff index da20094..629e101 100644 --- a/tests/data/expression.diff +++ b/tests/data/expression.diff @@ -171,8 +171,7 @@ +{"2.7": dead, "3.7": long_live or die_hard} +{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] --(SomeName) -+SomeName + (SomeName) SomeName (Good, Bad, Ugly) (i for i in (1, 2, 3)) diff --git a/tests/data/expression.py b/tests/data/expression.py index c9b149f..3bcf52b 100644 --- a/tests/data/expression.py +++ b/tests/data/expression.py @@ -424,7 +424,7 @@ numpy[np.newaxis, :] {"2.7": dead, "3.7": long_live or die_hard} {"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] -SomeName +(SomeName) SomeName (Good, Bad, Ugly) (i for i in (1, 2, 3)) diff --git a/tests/data/remove_parens.py b/tests/data/remove_parens.py index e128f59..afc3401 100644 --- a/tests/data/remove_parens.py +++ b/tests/data/remove_parens.py @@ -1,8 +1,19 @@ -print((1)) x = (1) x = (1.2) -(x) = (3) +data = ( + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +).encode() + +async def show_status(): + while True: + try: + if report_host: + data = ( + f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ).encode() + except Exception as e: + pass def example(): return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) @@ -45,10 +56,23 @@ def example8(): # output -print(1) x = 1 x = 1.2 -x = 3 + +data = ( + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +).encode() + + +async def show_status(): + while True: + try: + if report_host: + data = ( + f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ).encode() + except Exception as e: + pass def example(): -- 2.39.2 From 394edc388755eff01cc2ea155918bc4379ce933d Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C5=81ukasz=20Langa?= Date: Mon, 21 Oct 2019 11:16:34 +0200 Subject: [PATCH 16/16] Revert "restore cursor to same line of code, not same line of buffer (#989)" This reverts commit 65c5a0d9f180c4b36ea98917cb3b569f8e4f892f. Edge cases were discovered on the pull request post merge. --- plugin/black.vim | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/plugin/black.vim b/plugin/black.vim index 9def61a..8e05c2a 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -114,29 +114,7 @@ def Black(): string_normalization=not bool(int(vim.eval("g:black_skip_string_normalization"))), is_pyi=vim.current.buffer.name.endswith('.pyi'), ) - (cursor_line, cursor_column) = vim.current.window.cursor - cb = vim.current.buffer[:] - cb_bc = cb[0:cursor_line] - # Format all code before the cursor. - # Detect unclosed blocks, close them with pass. - last_line = cb_bc[-1] - if last_line.rstrip().endswith(":"): - cb_bc[-1] = last_line + " pass" - # Determine old:new cursor location mapping - buffer_str_before = '\n'.join(cb_bc)+'\n' - try: - new_buffer_str_before = black.format_file_contents(buffer_str_before, fast=fast, mode=mode) - new_cb = new_buffer_str_before.split('\n')[:-1] - new_cursor_line = len(new_cb) - new_cursor = (new_cursor_line, cursor_column) - except black.NothingChanged: - new_cursor_line = cursor_line - new_cursor = (new_cursor_line, cursor_column) - except Exception as exc: - print(exc) - # Now we know where the cursor should be - # when we format the entire buffer. Do it: - buffer_str = '\n'.join(cb) + '\n' + buffer_str = '\n'.join(vim.current.buffer) + '\n' try: new_buffer_str = black.format_file_contents(buffer_str, fast=fast, mode=mode) except black.NothingChanged: @@ -144,12 +122,10 @@ def Black(): except Exception as exc: print(exc) else: - # Replace the buffer - new_cb = new_buffer_str.split('\n')[:-1] - vim.current.buffer[:] = new_cb - # Restore the cursor to its rightful place + cursor = vim.current.window.cursor + vim.current.buffer[:] = new_buffer_str.split('\n')[:-1] try: - vim.current.window.cursor = new_cursor + vim.current.window.cursor = cursor except vim.error: vim.current.window.cursor = (len(vim.current.buffer), 0) print(f'Reformatted in {time.time() - start:.4f}s.') -- 2.39.2