Skip to content

Commit

Permalink
from __future__ import annotations now implies 3.7+ (#2690)
Browse files Browse the repository at this point in the history
  • Loading branch information
isidentical committed Dec 14, 2021
1 parent 1c6b3a3 commit ab86513
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -15,6 +15,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

Expand Down
22 changes: 17 additions & 5 deletions src/black/__init__.py
Expand Up @@ -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
Expand Down Expand Up @@ -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}:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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]
}
Expand Down
19 changes: 19 additions & 0 deletions src/black/mode.py
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
},
Expand All @@ -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,
Expand All @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions tests/test_black.py
Expand Up @@ -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))
Expand Down

0 comments on commit ab86513

Please sign in to comment.