From 7917c6e767b165612f7bcf25774cda77d914ce93 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 15 Dec 2021 00:59:26 +0300 Subject: [PATCH] `from __future__ import annotations` now implies 3.7+ --- CHANGES.md | 1 + src/black/__init__.py | 22 +++++++++++++++++----- src/black/mode.py | 19 +++++++++++++++++++ tests/test_black.py | 18 ++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 097725ec0be..73b807112be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Fix determination of f-string expression spans (#2654) - Fix bad formatting of error messages about EOF in multi-line statements (#2343) - Functions and classes in blocks now have more consistent surrounding spacing (#2472) +- `from __future__ import annotations` statement now implies Python 3.7+ (#2690) #### Jupyter Notebook support diff --git a/src/black/__init__.py b/src/black/__init__.py index 1923c069ede..ce43bcc9bbe 100644 --- 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 index e2417531240..a2b7d9e9e2d 100644 --- 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, diff --git a/tests/test_black.py b/tests/test_black.py index 92598532e2c..2d0a7dfd4e2 100644 --- 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))