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:
parent
e64949ee69
commit
e401b6bb1e
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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(
|
||||
|
14
docs/faq.md
14
docs/faq.md
@ -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?
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
]
|
||||
|
1
setup.py
1
setup.py
@ -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"],
|
||||
},
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)):
|
||||
|
@ -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)}"
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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()))
|
@ -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)
|
@ -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"
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
12
tox.ini
@ -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 \
|
||||
|
Loading…
Reference in New Issue
Block a user