Repository: ambv/black (python)

Issue:
black inserts unnecessary whitespace around exec() with target_version = py27
black is adding whitespace between `exec` and `(` with `--target-version py27`. This also happens with `--target-version py27 --target-version py37`

**Test file:**

```python
exec("1 + 1")
```

**Result:**

```console
$ black --target-version py27 t.py --diff
--- t.py	2019-03-15 07:21:56.222569 +0000
+++ t.py	2019-03-15 07:22:05.901294 +0000
@@ -1,2 +1,2 @@
-exec("1 + 1")
+exec ("1 + 1")
```

Operating system: OS X 
Python version: 3.7.2
*Black* version: 19.3b0
Does also happen on master: yes, as of d6db1c12a8e14833fe22da377cddc2bd1f43dc14

Pull request:
Fix grammar selection involving Python 2
Fixes #757.

Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index 948c53b..6389f45 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -18,9 +18,9 @@ artifacts:
 
 deploy:
   provider: GitHub
-  description: 'Windows binary'
+  description: ''
   auth_token:
     secure: uplI9CJ2dTGcEBCbZuIn+Qb4rC38hOoRSH9lcwuGCr5g9fSnhK1MZdNT6Cjf/mFL
   on:
-    appveyor_repo_tag: true
+    APPVEYOR_REPO_TAG: true
 
diff --git a/black.py b/black.py
index 680b1f4..2dee826 100644
--- a/black.py
+++ b/black.py
@@ -715,24 +715,20 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]:
         return tiow.read(), encoding, newline
 
 
-GRAMMARS = [
-    pygram.python_grammar_no_print_statement_no_exec_statement,
-    pygram.python_grammar_no_print_statement,
-    pygram.python_grammar,
-]
-
-
 def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
     if not target_versions:
-        return GRAMMARS
-    elif all(not version.is_python2() for version in target_versions):
-        # Python 3-compatible code, so don't try Python 2 grammar
+        # No target_version specified, so try all grammars.
         return [
             pygram.python_grammar_no_print_statement_no_exec_statement,
             pygram.python_grammar_no_print_statement,
+            pygram.python_grammar,
         ]
-    else:
+    elif all(version.is_python2() for version in target_versions):
+        # Python 2-only code, so try Python 2 grammars.
         return [pygram.python_grammar_no_print_statement, pygram.python_grammar]
+    else:
+        # Python 3-compatible code, so only try Python 3 grammar.
+        return [pygram.python_grammar_no_print_statement_no_exec_statement]
 
 
 def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node:


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
index 645eec7..a3e2ff8 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -28,7 +28,7 @@
 from click.testing import CliRunner
 
 import black
-from black import Feature
+from black import Feature, TargetVersion
 
 try:
     import blackd
@@ -464,7 +464,7 @@ def test_python2(self) -> None:
     @patch("black.dump_to_file", dump_to_stderr)
     def test_python2_print_function(self) -> None:
         source, expected = read_data("python2_print_function")
-        mode = black.FileMode(target_versions={black.TargetVersion.PY27})
+        mode = black.FileMode(target_versions={TargetVersion.PY27})
         actual = fs(source, mode=mode)
         self.assertFormatEqual(expected, actual)
         black.assert_stable(source, actual, mode)
@@ -828,6 +828,31 @@ def err(msg: str, **kwargs: Any) -> None:
                 "2 files would fail to reformat.",
             )
 
+    def test_lib2to3_parse(self) -> None:
+        with self.assertRaises(black.InvalidInput):
+            black.lib2to3_parse("invalid syntax")
+
+        straddling = "x + y"
+        black.lib2to3_parse(straddling)
+        black.lib2to3_parse(straddling, {TargetVersion.PY27})
+        black.lib2to3_parse(straddling, {TargetVersion.PY36})
+        black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
+
+        py2_only = "print x"
+        black.lib2to3_parse(py2_only)
+        black.lib2to3_parse(py2_only, {TargetVersion.PY27})
+        with self.assertRaises(black.InvalidInput):
+            black.lib2to3_parse(py2_only, {TargetVersion.PY36})
+        with self.assertRaises(black.InvalidInput):
+            black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
+
+        py3_only = "exec(x, end=y)"
+        black.lib2to3_parse(py3_only)
+        with self.assertRaises(black.InvalidInput):
+            black.lib2to3_parse(py3_only, {TargetVersion.PY27})
+        black.lib2to3_parse(py3_only, {TargetVersion.PY36})
+        black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
+
     def test_get_features_used(self) -> None:
         node = black.lib2to3_parse("def f(*, arg): ...\n")
         self.assertEqual(black.get_features_used(node), set())