Repository: psf/black

Issue:
Enable pattern matching by default
Now that the speed-up PR landed, it should be safe for us to enable pattern matching by default as the last grammar case to try. This would still make us initially test the 3.7+ and 3.0-3.6 grammars first before we try to run with the backtracking.


Discussion:
(this was actually supposed be a feature request not a bug)

Resolving patch:
diff --git a/src/black/parsing.py b/src/black/parsing.py
--- a/src/black/parsing.py
+++ b/src/black/parsing.py
@@ -42,7 +42,6 @@
         ast3 = ast
 
 
-PY310_HINT: Final = "Consider using --target-version py310 to parse Python 3.10 code."
 PY2_HINT: Final = "Python 2 support was removed in version 22.0."
 
 
@@ -58,12 +57,11 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
             pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords,
             # Python 3.0-3.6
             pygram.python_grammar_no_print_statement_no_exec_statement,
+            # Python 3.10+
+            pygram.python_grammar_soft_keywords,
         ]
 
     grammars = []
-    if supports_feature(target_versions, Feature.PATTERN_MATCHING):
-        # Python 3.10+
-        grammars.append(pygram.python_grammar_soft_keywords)
     # If we have to parse both, try to parse async as a keyword first
     if not supports_feature(
         target_versions, Feature.ASYNC_IDENTIFIERS
@@ -75,6 +73,10 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
     if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):
         # Python 3.0-3.6
         grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement)
+    if supports_feature(target_versions, Feature.PATTERN_MATCHING):
+        # Python 3.10+
+        grammars.append(pygram.python_grammar_soft_keywords)
+
     # At least one of the above branches must have been taken, because every Python
     # version has exactly one of the two 'ASYNC_*' flags
     return grammars
@@ -86,6 +88,7 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -
         src_txt += "\n"
 
     grammars = get_grammars(set(target_versions))
+    errors = {}
     for grammar in grammars:
         drv = driver.Driver(grammar)
         try:
@@ -99,20 +102,21 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -
                 faulty_line = lines[lineno - 1]
             except IndexError:
                 faulty_line = "<line number missing in source>"
-            exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {faulty_line}")
+            errors[grammar.version] = InvalidInput(
+                f"Cannot parse: {lineno}:{column}: {faulty_line}"
+            )
 
         except TokenError as te:
             # In edge cases these are raised; and typically don't have a "faulty_line".
             lineno, column = te.args[1]
-            exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {te.args[0]}")
+            errors[grammar.version] = InvalidInput(
+                f"Cannot parse: {lineno}:{column}: {te.args[0]}"
+            )
 
     else:
-        if pygram.python_grammar_soft_keywords not in grammars and matches_grammar(
-            src_txt, pygram.python_grammar_soft_keywords
-        ):
-            original_msg = exc.args[0]
-            msg = f"{original_msg}\n{PY310_HINT}"
-            raise InvalidInput(msg) from None
+        # Choose the latest version when raising the actual parsing error.
+        assert len(errors) >= 1
+        exc = errors[max(errors)]
 
         if matches_grammar(src_txt, pygram.python_grammar) or matches_grammar(
             src_txt, pygram.python_grammar_no_print_statement
diff --git a/src/blib2to3/pgen2/grammar.py b/src/blib2to3/pgen2/grammar.py
--- a/src/blib2to3/pgen2/grammar.py
+++ b/src/blib2to3/pgen2/grammar.py
@@ -92,6 +92,7 @@ def __init__(self) -> None:
         self.soft_keywords: Dict[str, int] = {}
         self.tokens: Dict[int, int] = {}
         self.symbol2label: Dict[str, int] = {}
+        self.version: Tuple[int, int] = (0, 0)
         self.start = 256
         # Python 3.7+ parses async as a keyword, not an identifier
         self.async_keywords = False
@@ -145,6 +146,7 @@ def copy(self: _P) -> _P:
         new.labels = self.labels[:]
         new.states = self.states[:]
         new.start = self.start
+        new.version = self.version
         new.async_keywords = self.async_keywords
         return new
 
diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py
--- a/src/blib2to3/pygram.py
+++ b/src/blib2to3/pygram.py
@@ -178,6 +178,8 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None:
 
     # Python 2
     python_grammar = driver.load_packaged_grammar("blib2to3", _GRAMMAR_FILE, cache_dir)
+    python_grammar.version = (2, 0)
+
     soft_keywords = python_grammar.soft_keywords.copy()
     python_grammar.soft_keywords.clear()
 
@@ -191,6 +193,7 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None:
     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"]
+    python_grammar_no_print_statement_no_exec_statement.version = (3, 0)
 
     # Python 3.7+
     python_grammar_no_print_statement_no_exec_statement_async_keywords = (
@@ -199,12 +202,14 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None:
     python_grammar_no_print_statement_no_exec_statement_async_keywords.async_keywords = (
         True
     )
+    python_grammar_no_print_statement_no_exec_statement_async_keywords.version = (3, 7)
 
     # Python 3.10+
     python_grammar_soft_keywords = (
         python_grammar_no_print_statement_no_exec_statement_async_keywords.copy()
     )
     python_grammar_soft_keywords.soft_keywords = soft_keywords
+    python_grammar_soft_keywords.version = (3, 10)
 
     pattern_grammar = driver.load_packaged_grammar(
         "blib2to3", _PATTERN_GRAMMAR_FILE, cache_dir


Test patch:
diff --git a/tests/test_format.py b/tests/test_format.py
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -191,6 +191,12 @@ def test_python_310(filename: str) -> None:
     assert_format(source, expected, mode, minimum_version=(3, 10))
 
 
+def test_python_310_without_target_version() -> None:
+    source, expected = read_data("pattern_matching_simple")
+    mode = black.Mode()
+    assert_format(source, expected, mode, minimum_version=(3, 10))
+
+
 def test_patma_invalid() -> None:
     source, expected = read_data("pattern_matching_invalid")
     mode = black.Mode(target_versions={black.TargetVersion.PY310})
@@ -200,15 +206,6 @@ def test_patma_invalid() -> None:
     exc_info.match("Cannot parse: 10:11")
 
 
-def test_patma_hint() -> None:
-    source, expected = read_data("pattern_matching_simple")
-    mode = black.Mode(target_versions={black.TargetVersion.PY39})
-    with pytest.raises(black.parsing.InvalidInput) as exc_info:
-        assert_format(source, expected, mode, minimum_version=(3, 10))
-
-    exc_info.match(black.parsing.PY310_HINT)
-
-
 def test_python_2_hint() -> None:
     with pytest.raises(black.parsing.InvalidInput) as exc_info:
         assert_format("print 'daylily'", "print 'daylily'")