Repository: psf/black

Issue:
use `from __future__ import annotations` to imply `--target-version=py37`
**Is your feature request related to a problem? Please describe.**
I'd like trailing commas after `**kwargs` to be supported in files starting, `from __future__ import annotations`

**Describe the solution you'd like** use `__future__ imports annotations` to imply `--target-version=py37`

**Describe alternatives you've considered** I've not

**Additional context** N/A



Discussion:
Awesome idea actually.
@ambv it also makes me wonder how `from __future__ import print_function` should be interpreted because it's ambiguous:

Is it an explicit declaration of py2 support being required, or is it a legacy that should be removed?
Due to Python 2/Python 3 polyglot code, `print_function` is still accepted as a (nullop) future in Python 3.10. So, what it *could* tell us at best is that "we are 2.7 or 3.0+" which isn't useful :-)

@graingert, care to prepare a PR for this?
@ambv yeah I'll have a look, I'll also add support for generator_stop
@ambv I've added a second issue for the py2 `__future__` implication because it's technically a different problem, but it has the same solution and it's not as clear cut

Resolving patch:
diff --git a/src/black/__init__.py b/src/black/__init__.py
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -40,7 +40,7 @@
 from black.lines import Line, EmptyLineTracker
 from black.linegen import transform_line, LineGenerator, LN
 from black.comments import normalize_fmt_off
-from black.mode import Mode, TargetVersion
+from black.mode import FUTURE_FLAG_TO_FEATURE, Mode, TargetVersion
 from black.mode import Feature, supports_feature, VERSION_TO_FEATURES
 from black.cache import read_cache, write_cache, get_cache_info, filter_cached, Cache
 from black.concurrency import cancel, shutdown, maybe_install_uvloop
@@ -1080,7 +1080,7 @@ def f(
     if mode.target_versions:
         versions = mode.target_versions
     else:
-        versions = detect_target_versions(src_node)
+        versions = detect_target_versions(src_node, future_imports=future_imports)
 
     # TODO: fully drop support and this code hopefully in January 2022 :D
     if TargetVersion.PY27 in mode.target_versions or versions == {TargetVersion.PY27}:
@@ -1132,7 +1132,9 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]:
         return tiow.read(), encoding, newline
 
 
-def get_features_used(node: Node) -> Set[Feature]:  # noqa: C901
+def get_features_used(  # noqa: C901
+    node: Node, *, future_imports: Optional[Set[str]] = None
+) -> Set[Feature]:
     """Return a set of (relatively) new Python features used in this file.
 
     Currently looking for:
@@ -1142,9 +1144,17 @@ def get_features_used(node: Node) -> Set[Feature]:  # noqa: C901
     - positional only arguments in function signatures and lambdas;
     - assignment expression;
     - relaxed decorator syntax;
+    - usage of __future__ flags (annotations);
     - print / exec statements;
     """
     features: Set[Feature] = set()
+    if future_imports:
+        features |= {
+            FUTURE_FLAG_TO_FEATURE[future_import]
+            for future_import in future_imports
+            if future_import in FUTURE_FLAG_TO_FEATURE
+        }
+
     for n in node.pre_order():
         if n.type == token.STRING:
             value_head = n.value[:2]  # type: ignore
@@ -1229,9 +1239,11 @@ def get_features_used(node: Node) -> Set[Feature]:  # noqa: C901
     return features
 
 
-def detect_target_versions(node: Node) -> Set[TargetVersion]:
+def detect_target_versions(
+    node: Node, *, future_imports: Optional[Set[str]] = None
+) -> Set[TargetVersion]:
     """Detect the version to target based on the nodes used."""
-    features = get_features_used(node)
+    features = get_features_used(node, future_imports=future_imports)
     return {
         version for version in TargetVersion if features <= VERSION_TO_FEATURES[version]
     }
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -4,11 +4,18 @@
 chosen by the user.
 """
 
+import sys
+
 from dataclasses import dataclass, field
 from enum import Enum
 from operator import attrgetter
 from typing import Dict, Set
 
+if sys.version_info < (3, 8):
+    from typing_extensions import Final
+else:
+    from typing import Final
+
 from black.const import DEFAULT_LINE_LENGTH
 
 
@@ -44,6 +51,9 @@ class Feature(Enum):
     PATTERN_MATCHING = 11
     FORCE_OPTIONAL_PARENTHESES = 50
 
+    # __future__ flags
+    FUTURE_ANNOTATIONS = 51
+
     # temporary for Python 2 deprecation
     PRINT_STMT = 200
     EXEC_STMT = 201
@@ -55,6 +65,11 @@ class Feature(Enum):
     BACKQUOTE_REPR = 207
 
 
+FUTURE_FLAG_TO_FEATURE: Final = {
+    "annotations": Feature.FUTURE_ANNOTATIONS,
+}
+
+
 VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
     TargetVersion.PY27: {
         Feature.ASYNC_IDENTIFIERS,
@@ -89,6 +104,7 @@ class Feature(Enum):
         Feature.TRAILING_COMMA_IN_CALL,
         Feature.TRAILING_COMMA_IN_DEF,
         Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
     },
     TargetVersion.PY38: {
         Feature.UNICODE_LITERALS,
@@ -97,6 +113,7 @@ class Feature(Enum):
         Feature.TRAILING_COMMA_IN_CALL,
         Feature.TRAILING_COMMA_IN_DEF,
         Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
         Feature.ASSIGNMENT_EXPRESSIONS,
         Feature.POS_ONLY_ARGUMENTS,
     },
@@ -107,6 +124,7 @@ class Feature(Enum):
         Feature.TRAILING_COMMA_IN_CALL,
         Feature.TRAILING_COMMA_IN_DEF,
         Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
         Feature.ASSIGNMENT_EXPRESSIONS,
         Feature.RELAXED_DECORATORS,
         Feature.POS_ONLY_ARGUMENTS,
@@ -118,6 +136,7 @@ class Feature(Enum):
         Feature.TRAILING_COMMA_IN_CALL,
         Feature.TRAILING_COMMA_IN_DEF,
         Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
         Feature.ASSIGNMENT_EXPRESSIONS,
         Feature.RELAXED_DECORATORS,
         Feature.POS_ONLY_ARGUMENTS,


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -811,6 +811,24 @@ def test_get_features_used(self) -> None:
         node = black.lib2to3_parse("def fn(a, /, b): ...")
         self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
 
+    def test_get_features_used_for_future_flags(self) -> None:
+        for src, features in [
+            ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
+            (
+                "from __future__ import (other, annotations)",
+                {Feature.FUTURE_ANNOTATIONS},
+            ),
+            ("a = 1 + 2\nfrom something import annotations", set()),
+            ("from __future__ import x, y", set()),
+        ]:
+            with self.subTest(src=src, features=features):
+                node = black.lib2to3_parse(src)
+                future_imports = black.get_future_imports(node)
+                self.assertEqual(
+                    black.get_features_used(node, future_imports=future_imports),
+                    features,
+                )
+
     def test_get_future_imports(self) -> None:
         node = black.lib2to3_parse("\n")
         self.assertEqual(set(), black.get_future_imports(node))