But you can reformat Python 2 code with it, too. *Black* is able to parse
all of the new syntax supported on Python 3.6 but also *effectively all*
-the Python 2 syntax at the same time, as long as you're not using print
-statements.
+the Python 2 syntax at the same time.
By making the code exclusively Python 3.6+, I'm able to focus on the
quality of the formatting and re-use all the nice features of the new
### 18.3a4 (unreleased)
+* automatic detection of deprecated Python 2 forms of print statements
+ and exec statements in the formatted file (#49)
+
* only return exit code 1 when --check is used (#50)
* don't remove single trailing commas from square bracket indexing
return dst_contents
+GRAMMARS = [
+ pygram.python_grammar_no_print_statement_no_exec_statement,
+ pygram.python_grammar_no_print_statement,
+ pygram.python_grammar_no_exec_statement,
+ pygram.python_grammar,
+]
+
+
def lib2to3_parse(src_txt: str) -> Node:
"""Given a string with source, return the lib2to3 Node."""
grammar = pygram.python_grammar_no_print_statement
- drv = driver.Driver(grammar, pytree.convert)
if src_txt[-1] != '\n':
nl = '\r\n' if '\r\n' in src_txt[:1024] else '\n'
src_txt += nl
- try:
- result = drv.parse_string(src_txt, True)
- except ParseError as pe:
- lineno, column = pe.context[1]
- lines = src_txt.splitlines()
+ for grammar in GRAMMARS:
+ drv = driver.Driver(grammar, pytree.convert)
try:
- faulty_line = lines[lineno - 1]
- except IndexError:
- faulty_line = "<line number missing in source>"
- raise ValueError(f"Cannot parse: {lineno}:{column}: {faulty_line}") from None
+ result = drv.parse_string(src_txt, True)
+ break
+
+ except ParseError as pe:
+ lineno, column = pe.context[1]
+ lines = src_txt.splitlines()
+ try:
+ faulty_line = lines[lineno - 1]
+ except IndexError:
+ faulty_line = "<line number missing in source>"
+ exc = ValueError(f"Cannot parse: {lineno}:{column}: {faulty_line}")
+ else:
+ raise exc from None
if isinstance(result, Leaf):
result = Node(syms.file_input, [result])
):
return NO
+ elif (
+ prevp.type == token.RIGHTSHIFT
+ and prevp.parent
+ and prevp.parent.type == syms.shift_expr
+ and prevp.prev_sibling
+ and prevp.prev_sibling.type == token.NAME
+ and prevp.prev_sibling.value == 'print'
+ ):
+ # Python 2 print chevron
+ return NO
+
elif prev.type in OPENING_BRACKETS:
return NO
try:
src_ast = ast.parse(src)
except Exception as exc:
- raise AssertionError(f"cannot parse source: {exc}") from None
+ major, minor = sys.version_info[:2]
+ raise AssertionError(
+ f"cannot use --safe with this file; failed to parse source file "
+ f"with Python {major}.{minor}'s builtin AST. Re-run with --fast "
+ f"or stop using deprecated Python 2 syntax. AST error message: {exc}"
+ )
try:
dst_ast = ast.parse(dst)
python_grammar_no_print_statement = python_grammar.copy()
del python_grammar_no_print_statement.keywords["print"]
+python_grammar_no_exec_statement = python_grammar.copy()
+del python_grammar_no_exec_statement.keywords["exec"]
+
+python_grammar_no_print_statement_no_exec_statement = python_grammar.copy()
+del python_grammar_no_print_statement_no_exec_statement.keywords["print"]
+del python_grammar_no_print_statement_no_exec_statement.keywords["exec"]
+
pattern_grammar = driver.load_packaged_grammar("blib2to3", _PATTERN_GRAMMAR_FILE)
pattern_symbols = Symbols(pattern_grammar)
python_grammar: Grammar
python_grammar_no_print_statement: Grammar
+python_grammar_no_print_statement_no_exec_statement: Grammar
+python_grammar_no_exec_statement: Grammar
pattern_grammar: Grammar
for i in range(10):
print(i)
continue
+ exec("new-style exec", {}, {})
return None
-async def coroutine(arg):
+async def coroutine(arg, exec=False):
"Single-line docstring. Multiline is harder to reformat."
async with some_connection() as conn:
await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
print(i)
continue
+ exec("new-style exec", {}, {})
return None
-async def coroutine(arg):
+async def coroutine(arg, exec=False):
"Single-line docstring. Multiline is harder to reformat."
async with some_connection() as conn:
await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
--- /dev/null
+#!/usr/bin/env python2
+
+import sys
+
+print >> sys.stderr , "Warning:" ,
+print >> sys.stderr , "this is a blast from the past."
+print >> sys.stderr , "Look, a repr:", `sys`
+
+
+def function((_globals, _locals)):
+ exec "print 'hi from exec!'" in _globals, _locals
+
+
+function((globals(), locals()))
+
+
+# output
+
+
+#!/usr/bin/env python2
+
+import sys
+
+print >>sys.stderr, "Warning:",
+print >>sys.stderr, "this is a blast from the past."
+print >>sys.stderr, "Look, a repr:", ` sys `
+
+
+def function((_globals, _locals)):
+ exec "print 'hi from exec!'" in _globals, _locals
+
+
+function((globals(), locals()))
black.assert_equivalent(source, actual)
black.assert_stable(source, actual, line_length=ll)
+ @patch("black.dump_to_file", dump_to_stderr)
+ def test_python2(self) -> None:
+ source, expected = read_data('python2')
+ actual = fs(source)
+ self.assertFormatEqual(expected, actual)
+ # black.assert_equivalent(source, actual)
+ black.assert_stable(source, actual, line_length=ll)
+
def test_report(self) -> None:
report = black.Report()
out_lines = []