Remove Python 2 support (#2740)

*blib2to3's support was left untouched because: 1) I don't want to touch
parsing machinery, and 2) it'll allow us to provide a more useful error
message if someone does try to format Python 2 code.
This commit is contained in:
Richard Si 2022-01-10 07:16:30 -05:00 committed by GitHub
parent e64949ee69
commit e401b6bb1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 73 additions and 391 deletions

View File

@ -16,7 +16,7 @@ current development version. To confirm this, you have three options:
3. Or run _Black_ on your machine:
- create a new virtualenv (make sure it's the same Python version);
- clone this repository;
- run `pip install -e .[d,python2]`;
- run `pip install -e .[d]`;
- run `pip install -r test_requirements.txt`
- make sure it's sane by running `python -m pytest`; and
- run `black` like you did last time.

View File

@ -4,6 +4,7 @@
### _Black_
- **Remove Python 2 support** (#2740)
- Do not accept bare carriage return line endings in pyproject.toml (#2408)
- Improve error message for invalid regular expression (#2678)
- Improve error message when parsing fails during AST safety check by embedding the

View File

@ -40,9 +40,7 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the
### Installation
_Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to
run. If you want to format Python 2 code as well, install with
`pip install black[python2]`. If you want to format Jupyter Notebooks, install with
`pip install black[jupyter]`.
run. If you want to format Jupyter Notebooks, install with `pip install black[jupyter]`.
If you can't wait for the latest _hotness_ and want to install from GitHub, use:

View File

@ -14,7 +14,7 @@
run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True)
req = "black[colorama,python2]"
req = "black[colorama]"
if VERSION:
req += f"=={VERSION}"
pip_proc = run(

View File

@ -75,16 +75,7 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are
## Does Black support Python 2?
```{warning}
Python 2 support has been deprecated since 21.10b0.
This support will be dropped in the first stable release, expected for January 2022.
See [The Black Code Style](the_black_code_style/index.rst) for details.
```
For formatting, yes! [Install](getting_started.md#installation) with the `python2` extra
to format Python 2 files too! In terms of running _Black_ though, Python 3.6 or newer is
required.
Support for formatting Python 2 code was removed in version 22.0.
## Why does my linter or typechecker complain after I format my code?
@ -96,8 +87,7 @@ codebase with _Black_.
## Can I run Black with PyPy?
Yes, there is support for PyPy 3.7 and higher. You cannot format Python 2 files under
PyPy, because PyPy's inbuilt ast module does not support this.
Yes, there is support for PyPy 3.7 and higher.
## Why does Black not detect syntax errors in my code?

View File

@ -17,9 +17,7 @@ Also, you can try out _Black_ online for minimal fuss on the
## Installation
_Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to
run, but can format Python 2 code too. Python 2 support needs the `typed_ast`
dependency, which be installed with `pip install black[python2]`. If you want to format
Jupyter Notebooks, install with `pip install black[jupyter]`.
run. If you want to format Jupyter Notebooks, install with `pip install black[jupyter]`.
If you can't wait for the latest _hotness_ and want to install from GitHub, use:

View File

@ -8,8 +8,8 @@ environment. Great for enforcing that your code matches the _Black_ code style.
This action is known to support all GitHub-hosted runner OSes. In addition, only
published versions of _Black_ are supported (i.e. whatever is available on PyPI).
Finally, this action installs _Black_ with both the `colorama` and `python2` extras so
the `--color` flag and formatting Python 2 code are supported.
Finally, this action installs _Black_ with the `colorama` extra so the `--color` flag
should work fine.
## Usage

View File

@ -281,8 +281,7 @@ removed.
_Black_ standardizes most numeric literals to use lowercase letters for the syntactic
parts and uppercase letters for the digits themselves: `0xAB` instead of `0XAB` and
`1e10` instead of `1E10`. Python 2 long literals are styled as `2L` instead of `2l` to
avoid confusion between `l` and `1`.
`1e10` instead of `1E10`.
### Line breaks & binary operators

View File

@ -29,7 +29,6 @@ build-backend = "setuptools.build_meta"
[tool.pytest.ini_options]
# Option below requires `tests/optional.py`
optional-tests = [
"no_python2: run when `python2` extra NOT installed",
"no_blackd: run when `d` extra NOT installed",
"no_jupyter: run when `jupyter` extra NOT installed",
]

View File

@ -112,7 +112,6 @@ def find_python_files(base: Path) -> List[Path]:
extras_require={
"d": ["aiohttp>=3.7.4"],
"colorama": ["colorama>=0.4.3"],
"python2": ["typed-ast>=1.4.3"],
"uvloop": ["uvloop>=0.15.2"],
"jupyter": ["ipython>=7.8.0", "tokenize-rt>=3.2.0"],
},

View File

@ -1083,20 +1083,8 @@ def f(
else:
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}:
msg = (
"DEPRECATION: Python 2 support will be removed in the first stable release "
"expected in January 2022."
)
err(msg, fg="yellow", bold=True)
normalize_fmt_off(src_node)
lines = LineGenerator(
mode=mode,
remove_u_prefix="unicode_literals" in future_imports
or supports_feature(versions, Feature.UNICODE_LITERALS),
)
lines = LineGenerator(mode=mode)
elt = EmptyLineTracker(is_pyi=mode.is_pyi)
empty_line = Line(mode=mode)
after = 0
@ -1166,14 +1154,6 @@ def get_features_used( # noqa: C901
assert isinstance(n, Leaf)
if "_" in n.value:
features.add(Feature.NUMERIC_UNDERSCORES)
elif n.value.endswith(("L", "l")):
# Python 2: 10L
features.add(Feature.LONG_INT_LITERAL)
elif len(n.value) >= 2 and n.value[0] == "0" and n.value[1].isdigit():
# Python 2: 0123; 00123; ...
if not all(char == "0" for char in n.value):
# although we don't want to match 0000 or similar
features.add(Feature.OCTAL_INT_LITERAL)
elif n.type == token.SLASH:
if n.parent and n.parent.type in {
@ -1226,32 +1206,6 @@ def get_features_used( # noqa: C901
):
features.add(Feature.ANN_ASSIGN_EXTENDED_RHS)
# Python 2 only features (for its deprecation) except for integers, see above
elif n.type == syms.print_stmt:
features.add(Feature.PRINT_STMT)
elif n.type == syms.exec_stmt:
features.add(Feature.EXEC_STMT)
elif n.type == syms.tfpdef:
# def set_position((x, y), value):
# ...
features.add(Feature.AUTOMATIC_PARAMETER_UNPACKING)
elif n.type == syms.except_clause:
# try:
# ...
# except Exception, err:
# ...
if len(n.children) >= 4:
if n.children[-2].type == token.COMMA:
features.add(Feature.COMMA_STYLE_EXCEPT)
elif n.type == syms.raise_stmt:
# raise Exception, "msg"
if len(n.children) >= 4:
if n.children[-2].type == token.COMMA:
features.add(Feature.COMMA_STYLE_RAISE)
elif n.type == token.BACKQUOTE:
# `i'm surprised this ever existed`
features.add(Feature.BACKQUOTE_REPR)
return features

View File

@ -48,9 +48,8 @@ class LineGenerator(Visitor[Line]):
in ways that will no longer stringify to valid Python code on the tree.
"""
def __init__(self, mode: Mode, remove_u_prefix: bool = False) -> None:
def __init__(self, mode: Mode) -> None:
self.mode = mode
self.remove_u_prefix = remove_u_prefix
self.current_line: Line
self.__post_init__()
@ -92,9 +91,7 @@ def visit_default(self, node: LN) -> Iterator[Line]:
normalize_prefix(node, inside_brackets=any_open_brackets)
if self.mode.string_normalization and node.type == token.STRING:
node.value = normalize_string_prefix(
node.value, remove_u_prefix=self.remove_u_prefix
)
node.value = normalize_string_prefix(node.value)
node.value = normalize_string_quotes(node.value)
if node.type == token.NUMBER:
normalize_numeric_literal(node)
@ -236,7 +233,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
if is_docstring(leaf) and "\\\n" not in leaf.value:
# We're ignoring docstrings with backslash newline escapes because changing
# indentation of those changes the AST representation of the code.
docstring = normalize_string_prefix(leaf.value, self.remove_u_prefix)
docstring = normalize_string_prefix(leaf.value)
prefix = get_string_prefix(docstring)
docstring = docstring[len(prefix) :] # Remove the prefix
quote_char = docstring[0]

View File

@ -20,7 +20,6 @@
class TargetVersion(Enum):
PY27 = 2
PY33 = 3
PY34 = 4
PY35 = 5
@ -30,13 +29,8 @@ class TargetVersion(Enum):
PY39 = 9
PY310 = 10
def is_python2(self) -> bool:
return self is TargetVersion.PY27
class Feature(Enum):
# All string literals are unicode
UNICODE_LITERALS = 1
F_STRINGS = 2
NUMERIC_UNDERSCORES = 3
TRAILING_COMMA_IN_CALL = 4
@ -56,16 +50,6 @@ class Feature(Enum):
# __future__ flags
FUTURE_ANNOTATIONS = 51
# temporary for Python 2 deprecation
PRINT_STMT = 200
EXEC_STMT = 201
AUTOMATIC_PARAMETER_UNPACKING = 202
COMMA_STYLE_EXCEPT = 203
COMMA_STYLE_RAISE = 204
LONG_INT_LITERAL = 205
OCTAL_INT_LITERAL = 206
BACKQUOTE_REPR = 207
FUTURE_FLAG_TO_FEATURE: Final = {
"annotations": Feature.FUTURE_ANNOTATIONS,
@ -73,26 +57,10 @@ class Feature(Enum):
VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
TargetVersion.PY27: {
Feature.ASYNC_IDENTIFIERS,
Feature.PRINT_STMT,
Feature.EXEC_STMT,
Feature.AUTOMATIC_PARAMETER_UNPACKING,
Feature.COMMA_STYLE_EXCEPT,
Feature.COMMA_STYLE_RAISE,
Feature.LONG_INT_LITERAL,
Feature.OCTAL_INT_LITERAL,
Feature.BACKQUOTE_REPR,
},
TargetVersion.PY33: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY34: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY35: {
Feature.UNICODE_LITERALS,
Feature.TRAILING_COMMA_IN_CALL,
Feature.ASYNC_IDENTIFIERS,
},
TargetVersion.PY33: {Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY34: {Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY35: {Feature.TRAILING_COMMA_IN_CALL, Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY36: {
Feature.UNICODE_LITERALS,
Feature.F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
@ -100,7 +68,6 @@ class Feature(Enum):
Feature.ASYNC_IDENTIFIERS,
},
TargetVersion.PY37: {
Feature.UNICODE_LITERALS,
Feature.F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
@ -109,7 +76,6 @@ class Feature(Enum):
Feature.FUTURE_ANNOTATIONS,
},
TargetVersion.PY38: {
Feature.UNICODE_LITERALS,
Feature.F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
@ -122,7 +88,6 @@ class Feature(Enum):
Feature.ANN_ASSIGN_EXTENDED_RHS,
},
TargetVersion.PY39: {
Feature.UNICODE_LITERALS,
Feature.F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
@ -136,7 +101,6 @@ class Feature(Enum):
Feature.ANN_ASSIGN_EXTENDED_RHS,
},
TargetVersion.PY310: {
Feature.UNICODE_LITERALS,
Feature.F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,

View File

@ -259,16 +259,6 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901
):
return NO
elif (
prevp.type == token.RIGHTSHIFT
and prevp.parent
and prevp.parent.type == syms.shift_expr
and prevp.prev_sibling
and is_name_token(prevp.prev_sibling)
and prevp.prev_sibling.value == "print"
):
# Python 2 print chevron
return NO
elif prevp.type == token.AT and p.parent and p.parent.type == syms.decorator:
# no space in decorators
return NO

View File

@ -25,13 +25,10 @@ def format_scientific_notation(text: str) -> str:
return f"{before}e{sign}{after}"
def format_long_or_complex_number(text: str) -> str:
"""Formats a long or complex string like `10L` or `10j`"""
def format_complex_number(text: str) -> str:
"""Formats a complex string like `10j`"""
number = text[:-1]
suffix = text[-1]
# Capitalize in "2L" because "l" looks too similar to "1".
if suffix == "l":
suffix = "L"
return f"{format_float_or_int_string(number)}{suffix}"
@ -47,9 +44,7 @@ def format_float_or_int_string(text: str) -> str:
def normalize_numeric_literal(leaf: Leaf) -> None:
"""Normalizes numeric (float, int, and complex) literals.
All letters used in the representation are normalized to lowercase (except
in Python 2 long literals).
"""
All letters used in the representation are normalized to lowercase."""
text = leaf.value.lower()
if text.startswith(("0o", "0b")):
# Leave octal and binary literals alone.
@ -58,8 +53,8 @@ def normalize_numeric_literal(leaf: Leaf) -> None:
text = format_hex(text)
elif "e" in text:
text = format_scientific_notation(text)
elif text.endswith(("j", "l")):
text = format_long_or_complex_number(text)
elif text.endswith("j"):
text = format_complex_number(text)
else:
text = format_float_or_int_string(text)
leaf.value = text

View File

@ -4,7 +4,7 @@
import ast
import platform
import sys
from typing import Any, AnyStr, Iterable, Iterator, List, Set, Tuple, Type, Union
from typing import Any, Iterable, Iterator, List, Set, Tuple, Type, Union
if sys.version_info < (3, 8):
from typing_extensions import Final
@ -23,12 +23,11 @@
from black.nodes import syms
ast3: Any
ast27: Any
_IS_PYPY = platform.python_implementation() == "PyPy"
try:
from typed_ast import ast3, ast27
from typed_ast import ast3
except ImportError:
# Either our python version is too low, or we're on pypy
if sys.version_info < (3, 7) or (sys.version_info < (3, 8) and not _IS_PYPY):
@ -40,12 +39,11 @@
)
sys.exit(1)
else:
ast3 = ast27 = ast
ast3 = ast
PY310_HINT: Final[
str
] = "Consider using --target-version py310 to parse Python 3.10 code."
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."
class InvalidInput(ValueError):
@ -60,22 +58,8 @@ 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 2.7 with future print_function import
pygram.python_grammar_no_print_statement,
# Python 2.7
pygram.python_grammar,
]
if all(version.is_python2() for version in target_versions):
# Python 2-only code, so try Python 2 grammars.
return [
# Python 2.7 with future print_function import
pygram.python_grammar_no_print_statement,
# Python 2.7
pygram.python_grammar,
]
# Python 3-compatible code, so only try Python 3 grammar.
grammars = []
if supports_feature(target_versions, Feature.PATTERN_MATCHING):
# Python 3.10+
@ -129,6 +113,14 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -
original_msg = exc.args[0]
msg = f"{original_msg}\n{PY310_HINT}"
raise InvalidInput(msg) from None
if matches_grammar(src_txt, pygram.python_grammar) or matches_grammar(
src_txt, pygram.python_grammar_no_print_statement
):
original_msg = exc.args[0]
msg = f"{original_msg}\n{PY2_HINT}"
raise InvalidInput(msg) from None
raise exc from None
if isinstance(result, Leaf):
@ -154,7 +146,7 @@ def lib2to3_unparse(node: Node) -> str:
def parse_single_version(
src: str, version: Tuple[int, int]
) -> Union[ast.AST, ast3.AST, ast27.AST]:
) -> Union[ast.AST, ast3.AST]:
filename = "<unknown>"
# typed_ast is needed because of feature version limitations in the builtin ast
if sys.version_info >= (3, 8) and version >= (3,):
@ -164,18 +156,13 @@ def parse_single_version(
return ast3.parse(src, filename)
else:
return ast3.parse(src, filename, feature_version=version[1])
elif version == (2, 7):
return ast27.parse(src)
raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!")
def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]:
def parse_ast(src: str) -> Union[ast.AST, ast3.AST]:
# TODO: support Python 4+ ;)
versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]
if ast27.__name__ != "ast":
versions.append((2, 7))
first_error = ""
for version in sorted(versions, reverse=True):
try:
@ -188,22 +175,19 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]:
ast3_AST: Final[Type[ast3.AST]] = ast3.AST
ast27_AST: Final[Type[ast27.AST]] = ast27.AST
def _normalize(lineend: AnyStr, value: AnyStr) -> AnyStr:
def _normalize(lineend: str, value: str) -> str:
# To normalize, we strip any leading and trailing space from
# each line...
stripped: List[AnyStr] = [i.strip() for i in value.splitlines()]
stripped: List[str] = [i.strip() for i in value.splitlines()]
normalized = lineend.join(stripped)
# ...and remove any blank lines at the beginning and end of
# the whole string
return normalized.strip()
def stringify_ast(
node: Union[ast.AST, ast3.AST, ast27.AST], depth: int = 0
) -> Iterator[str]:
def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[str]:
"""Simple visitor generating strings to compare ASTs by content."""
node = fixup_ast_constants(node)
@ -215,7 +199,7 @@ def stringify_ast(
# TypeIgnore will not be present using pypy < 3.8, so need for this
if not (_IS_PYPY and sys.version_info < (3, 8)):
# TypeIgnore has only one field 'lineno' which breaks this comparison
type_ignore_classes = (ast3.TypeIgnore, ast27.TypeIgnore)
type_ignore_classes = (ast3.TypeIgnore,)
if sys.version_info >= (3, 8):
type_ignore_classes += (ast.TypeIgnore,)
if isinstance(node, type_ignore_classes):
@ -234,40 +218,34 @@ def stringify_ast(
# parentheses and they change the AST.
if (
field == "targets"
and isinstance(node, (ast.Delete, ast3.Delete, ast27.Delete))
and isinstance(item, (ast.Tuple, ast3.Tuple, ast27.Tuple))
and isinstance(node, (ast.Delete, ast3.Delete))
and isinstance(item, (ast.Tuple, ast3.Tuple))
):
for item in item.elts:
yield from stringify_ast(item, depth + 2)
elif isinstance(item, (ast.AST, ast3.AST, ast27.AST)):
elif isinstance(item, (ast.AST, ast3.AST)):
yield from stringify_ast(item, depth + 2)
# Note that we are referencing the typed-ast ASTs via global variables and not
# direct module attribute accesses because that breaks mypyc. It's probably
# something to do with the ast3 / ast27 variables being marked as Any leading
# something to do with the ast3 variables being marked as Any leading
# mypy to think this branch is always taken, leaving the rest of the code
# unanalyzed. Tighting up the types for the typed-ast AST types avoids the
# mypyc crash.
elif isinstance(value, (ast.AST, ast3_AST, ast27_AST)):
elif isinstance(value, (ast.AST, ast3_AST)):
yield from stringify_ast(value, depth + 2)
else:
# Constant strings may be indented across newlines, if they are
# docstrings; fold spaces after newlines when comparing. Similarly,
# trailing and leading space may be removed.
# Note that when formatting Python 2 code, at least with Windows
# line-endings, docstrings can end up here as bytes instead of
# str so make sure that we handle both cases.
if (
isinstance(node, ast.Constant)
and field == "value"
and isinstance(value, (str, bytes))
and isinstance(value, str)
):
if isinstance(value, str):
normalized: Union[str, bytes] = _normalize("\n", value)
else:
normalized = _normalize(b"\n", value)
normalized = _normalize("\n", value)
else:
normalized = value
yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}"
@ -275,14 +253,12 @@ def stringify_ast(
yield f"{' ' * depth}) # /{node.__class__.__name__}"
def fixup_ast_constants(
node: Union[ast.AST, ast3.AST, ast27.AST]
) -> Union[ast.AST, ast3.AST, ast27.AST]:
def fixup_ast_constants(node: Union[ast.AST, ast3.AST]) -> Union[ast.AST, ast3.AST]:
"""Map ast nodes deprecated in 3.8 to Constant."""
if isinstance(node, (ast.Str, ast3.Str, ast27.Str, ast.Bytes, ast3.Bytes)):
if isinstance(node, (ast.Str, ast3.Str, ast.Bytes, ast3.Bytes)):
return ast.Constant(value=node.s)
if isinstance(node, (ast.Num, ast3.Num, ast27.Num)):
if isinstance(node, (ast.Num, ast3.Num)):
return ast.Constant(value=node.n)
if isinstance(node, (ast.NameConstant, ast3.NameConstant)):

View File

@ -138,17 +138,17 @@ def assert_is_leaf_string(string: str) -> None:
), f"{set(string[:quote_idx])} is NOT a subset of {set(STRING_PREFIX_CHARS)}."
def normalize_string_prefix(s: str, remove_u_prefix: bool = False) -> str:
"""Make all string prefixes lowercase.
If remove_u_prefix is given, also removes any u prefix from the string.
"""
def normalize_string_prefix(s: str) -> str:
"""Make all string prefixes lowercase."""
match = STRING_PREFIX_RE.match(s)
assert match is not None, f"failed to match string {s!r}"
orig_prefix = match.group(1)
new_prefix = orig_prefix.replace("F", "f").replace("B", "b").replace("U", "u")
if remove_u_prefix:
new_prefix = new_prefix.replace("u", "")
new_prefix = (
orig_prefix.replace("F", "f")
.replace("B", "b")
.replace("U", "")
.replace("u", "")
)
return f"{new_prefix}{match.group(2)}"

View File

@ -149,17 +149,6 @@
"long_checkout": false,
"py_versions": ["all"]
},
"sqlalchemy": {
"cli_arguments": [
"--experimental-string-processing",
"--extend-exclude",
"/test/orm/test_relationship_criteria.py"
],
"expect_formatting_changes": true,
"git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git",
"long_checkout": false,
"py_versions": ["all"]
},
"tox": {
"cli_arguments": ["--experimental-string-processing"],
"expect_formatting_changes": true,

View File

@ -174,10 +174,8 @@ def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersi
raise InvalidVariantHeader("major version must be 2 or 3")
if len(rest) > 0:
minor = int(rest[0])
if major == 2 and minor != 7:
raise InvalidVariantHeader(
"minor version must be 7 for Python 2"
)
if major == 2:
raise InvalidVariantHeader("Python 2 is not supported")
else:
# Default to lowest supported minor version.
minor = 7 if major == 2 else 3

View File

@ -1,16 +0,0 @@
#!/usr/bin/env python2.7
x = 123456789L
x = 123456789l
x = 123456789
x = 0xb1acc
# output
#!/usr/bin/env python2.7
x = 123456789L
x = 123456789L
x = 123456789
x = 0xB1ACC

View File

@ -1,33 +0,0 @@
#!/usr/bin/env python2
import sys
print >> sys.stderr , "Warning:" ,
print >> sys.stderr , "this is a blast from the past."
print >> sys.stderr , "Look, a repr:", `sys`
def function((_globals, _locals)):
exec ur"print 'hi from exec!'" in _globals, _locals
function((globals(), locals()))
# output
#!/usr/bin/env python2
import sys
print >>sys.stderr, "Warning:",
print >>sys.stderr, "this is a blast from the past."
print >>sys.stderr, "Look, a repr:", ` sys `
def function((_globals, _locals)):
exec ur"print 'hi from exec!'" in _globals, _locals
function((globals(), locals()))

View File

@ -1,16 +0,0 @@
#!/usr/bin/env python2
from __future__ import print_function
print('hello')
print(u'hello')
print(a, file=sys.stderr)
# output
#!/usr/bin/env python2
from __future__ import print_function
print("hello")
print(u"hello")
print(a, file=sys.stderr)

View File

@ -1,20 +0,0 @@
#!/usr/bin/env python2
from __future__ import unicode_literals as _unicode_literals
from __future__ import absolute_import
from __future__ import print_function as lol, with_function
u'hello'
U"hello"
Ur"hello"
# output
#!/usr/bin/env python2
from __future__ import unicode_literals as _unicode_literals
from __future__ import absolute_import
from __future__ import print_function as lol, with_function
"hello"
"hello"
r"hello"

View File

@ -724,24 +724,15 @@ def test_lib2to3_parse(self) -> None:
straddling = "x + y"
black.lib2to3_parse(straddling)
black.lib2to3_parse(straddling, {TargetVersion.PY27})
black.lib2to3_parse(straddling, {TargetVersion.PY36})
black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
py2_only = "print x"
black.lib2to3_parse(py2_only)
black.lib2to3_parse(py2_only, {TargetVersion.PY27})
with self.assertRaises(black.InvalidInput):
black.lib2to3_parse(py2_only, {TargetVersion.PY36})
with self.assertRaises(black.InvalidInput):
black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
py3_only = "exec(x, end=y)"
black.lib2to3_parse(py3_only)
with self.assertRaises(black.InvalidInput):
black.lib2to3_parse(py3_only, {TargetVersion.PY27})
black.lib2to3_parse(py3_only, {TargetVersion.PY36})
black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
def test_get_features_used_decorator(self) -> None:
# Test the feature detection of new decorator syntax
@ -1436,27 +1427,6 @@ def test_bpo_2142_workaround(self) -> None:
actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
self.assertEqual(actual, expected)
@pytest.mark.python2
def test_docstring_reformat_for_py27(self) -> None:
"""
Check that stripping trailing whitespace from Python 2 docstrings
doesn't trigger a "not equivalent to source" error
"""
source = (
b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
)
expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
result = BlackRunner().invoke(
black.main,
["-", "-q", "--target-version=py27"],
input=BytesIO(source),
)
self.assertEqual(result.exit_code, 0)
actual = result.stdout
self.assertFormatEqual(actual, expected)
@staticmethod
def compare_results(
result: click.testing.Result, expected_value: str, expected_exit_code: int
@ -2086,36 +2056,6 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
)
@pytest.mark.python2
@pytest.mark.parametrize("explicit", [True, False], ids=["explicit", "autodetection"])
def test_python_2_deprecation_with_target_version(explicit: bool) -> None:
args = [
"--config",
str(THIS_DIR / "empty.toml"),
str(DATA_DIR / "python2.py"),
"--check",
]
if explicit:
args.append("--target-version=py27")
with cache_dir():
result = BlackRunner().invoke(black.main, args)
assert "DEPRECATION: Python 2 support will be removed" in result.stderr
@pytest.mark.python2
def test_python_2_deprecation_autodetection_extended() -> None:
# this test has a similar construction to test_get_features_used_decorator
python2, non_python2 = read_data("python2_detection")
for python2_case in python2.split("###"):
node = black.lib2to3_parse(python2_case)
assert black.detect_target_versions(node) == {TargetVersion.PY27}, python2_case
for non_python2_case in non_python2.split("###"):
node = black.lib2to3_parse(non_python2_case)
assert black.detect_target_versions(node) != {
TargetVersion.PY27
}, non_python2_case
try:
with open(black.__file__, "r", encoding="utf-8") as _bf:
black_source_lines = _bf.readlines()

View File

@ -77,6 +77,9 @@ async def check(header_value: str, expected_status: int = 400) -> None:
await check("ruby3.5")
await check("pyi3.6")
await check("py1.5")
await check("2")
await check("2.7")
await check("py2.7")
await check("2.8")
await check("py2.8")
await check("3.0")
@ -137,10 +140,6 @@ async def check(header_value: str, expected_status: int) -> None:
await check("py36,py37", 200)
await check("36", 200)
await check("3.6.4", 200)
await check("2", 204)
await check("2.7", 204)
await check("py2.7", 204)
await check("3.4", 204)
await check("py3.4", 204)
await check("py34,py36", 204)

View File

@ -55,12 +55,6 @@
"tupleassign",
]
SIMPLE_CASES_PY2 = [
"numeric_literals_py2",
"python2",
"python2_unicode_literals",
]
EXPERIMENTAL_STRING_PROCESSING_CASES = [
"cantfit",
"comments7",
@ -134,12 +128,6 @@ def check_file(filename: str, mode: black.Mode, *, data: bool = True) -> None:
assert_format(source, expected, mode, fast=False)
@pytest.mark.parametrize("filename", SIMPLE_CASES_PY2)
@pytest.mark.python2
def test_simple_format_py2(filename: str) -> None:
check_file(filename, DEFAULT_MODE)
@pytest.mark.parametrize("filename", SIMPLE_CASES)
def test_simple_format(filename: str) -> None:
check_file(filename, DEFAULT_MODE)
@ -219,6 +207,12 @@ def test_patma_hint() -> None:
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'")
exc_info.match(black.parsing.PY2_HINT)
def test_docstring_no_string_normalization() -> None:
"""Like test_docstring but with string normalization off."""
source, expected = read_data("docstring_no_string_normalization")
@ -245,13 +239,6 @@ def test_numeric_literals_ignoring_underscores() -> None:
assert_format(source, expected, mode)
@pytest.mark.python2
def test_python2_print_function() -> None:
source, expected = read_data("python2_print_function")
mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY27})
assert_format(source, expected, mode)
def test_stub() -> None:
mode = replace(DEFAULT_MODE, is_pyi=True)
source, expected = read_data("stub.pyi")

12
tox.ini
View File

@ -5,7 +5,7 @@ envlist = {,ci-}py{36,37,38,39,310,py3},fuzz
setenv = PYTHONPATH = {toxinidir}/src
skip_install = True
# We use `recreate=True` because otherwise, on the second run of `tox -e py`,
# the `no_python2` tests would run with the Python2 extra dependencies installed.
# the `no_jupyter` tests would run with the jupyter extra dependencies installed.
# See https://github.com/psf/black/issues/2367.
recreate = True
deps =
@ -15,15 +15,9 @@ deps =
commands =
pip install -e .[d]
coverage erase
pytest tests --run-optional no_python2 \
--run-optional no_jupyter \
pytest tests --run-optional no_jupyter \
!ci: --numprocesses auto \
--cov {posargs}
pip install -e .[d,python2]
pytest tests --run-optional python2 \
--run-optional no_jupyter \
!ci: --numprocesses auto \
--cov --cov-append {posargs}
pip install -e .[jupyter]
pytest tests --run-optional jupyter \
-m jupyter \
@ -43,7 +37,7 @@ deps =
commands =
pip install -e .[d]
coverage erase
pytest tests --run-optional no_python2 \
pytest tests \
--run-optional no_jupyter \
!ci: --numprocesses auto \
ci: --numprocesses 1 \