Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

from __future__ import annotations now implies 3.7+ #2690

Merged
merged 1 commit into from Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -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

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