From 38023a7bc37843801350b4723ee3ab81b00e8f84 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 25 Sep 2021 13:57:24 -0400 Subject: [PATCH 1/8] Remove Python 2 support (code* & docs) *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. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- README.md | 4 +- action/main.py | 2 +- docs/faq.md | 15 +--- docs/getting_started.md | 4 +- docs/integrations/github_actions.md | 4 +- docs/the_black_code_style/current_style.md | 3 +- pyproject.toml | 1 - setup.py | 1 - src/black/__init__.py | 48 +------------ src/black/linegen.py | 9 +-- src/black/mode.py | 66 ++++------------- src/black/nodes.py | 10 --- src/black/numerics.py | 15 ++-- src/black/parsing.py | 82 ++++++++-------------- src/black/strings.py | 16 ++--- src/blackd/__init__.py | 6 +- tests/data/numeric_literals_py2.py | 16 ----- tests/data/python2.py | 33 --------- tests/data/python2_print_function.py | 16 ----- tests/data/python2_unicode_literals.py | 20 ------ tests/test_black.py | 60 ---------------- tests/test_blackd.py | 7 +- tests/test_format.py | 25 ++----- tox.ini | 11 +-- 25 files changed, 85 insertions(+), 391 deletions(-) delete mode 100644 tests/data/numeric_literals_py2.py delete mode 100644 tests/data/python2.py delete mode 100755 tests/data/python2_print_function.py delete mode 100644 tests/data/python2_unicode_literals.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cb64cf9325d..48aa9291b05 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/README.md b/README.md index 2adf60a783a..e2b0d17ecfd 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/action/main.py b/action/main.py index fde312553bf..d14b10f421d 100644 --- a/action/main.py +++ b/action/main.py @@ -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( diff --git a/docs/faq.md b/docs/faq.md index 0a966c99c7f..c7814e34f10 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -75,16 +75,8 @@ 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. +Formatting Python 2 code support was removed in version 22.0. In terms of running +_Black_, Python 3.6 or newer is required since the first release. ## Why does my linter or typechecker complain after I format my code? @@ -96,8 +88,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? diff --git a/docs/getting_started.md b/docs/getting_started.md index c79dc607c4a..987290ac91f 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -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: diff --git a/docs/integrations/github_actions.md b/docs/integrations/github_actions.md index e866a3cc616..c9697cc05de 100644 --- a/docs/integrations/github_actions.md +++ b/docs/integrations/github_actions.md @@ -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 diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index b9ab350cd12..68dff3eef3f 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index aebbc0da29c..ec617790039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/setup.py b/setup.py index d314bb283f2..e23bc21caac 100644 --- a/setup.py +++ b/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"], }, diff --git a/src/black/__init__.py b/src/black/__init__.py index 9bc8fc15c49..283c53f0db3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -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 diff --git a/src/black/linegen.py b/src/black/linegen.py index dc238c3aee4..6008c773f94 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -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] diff --git a/src/black/mode.py b/src/black/mode.py index bd4428add66..1151bc481ad 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -20,7 +20,6 @@ class TargetVersion(Enum): - PY27 = 2 PY33 = 3 PY34 = 4 PY35 = 5 @@ -30,42 +29,27 @@ 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 - TRAILING_COMMA_IN_DEF = 5 + F_STRINGS = 1 + NUMERIC_UNDERSCORES = 2 + TRAILING_COMMA_IN_CALL = 3 + TRAILING_COMMA_IN_DEF = 4 # The following two feature-flags are mutually exclusive, and exactly one should be # set for every version of python. - ASYNC_IDENTIFIERS = 6 - ASYNC_KEYWORDS = 7 - ASSIGNMENT_EXPRESSIONS = 8 - POS_ONLY_ARGUMENTS = 9 - RELAXED_DECORATORS = 10 - PATTERN_MATCHING = 11 - UNPACKING_ON_FLOW = 12 - ANN_ASSIGN_EXTENDED_RHS = 13 + ASYNC_IDENTIFIERS = 5 + ASYNC_KEYWORDS = 6 + ASSIGNMENT_EXPRESSIONS = 7 + POS_ONLY_ARGUMENTS = 8 + RELAXED_DECORATORS = 9 + PATTERN_MATCHING = 10 + UNPACKING_ON_FLOW = 11 + ANN_ASSIGN_EXTENDED_RHS = 12 FORCE_OPTIONAL_PARENTHESES = 50 # __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, diff --git a/src/black/nodes.py b/src/black/nodes.py index 75a23474024..74dfa896295 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -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 diff --git a/src/black/numerics.py b/src/black/numerics.py index cb1c83e7b78..879e5b2cf36 100644 --- a/src/black/numerics.py +++ b/src/black/numerics.py @@ -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 diff --git a/src/black/parsing.py b/src/black/parsing.py index 76e9de023c7..13fa67ee84d 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -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 = "" # 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)): diff --git a/src/black/strings.py b/src/black/strings.py index 06a5da01f0c..2d3e4e6fcf1 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -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", "u") + .replace("u", "") + ) return f"{new_prefix}{match.group(2)}" diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index cc966404a74..cfcc2774211 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -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 unsupported") else: # Default to lowest supported minor version. minor = 7 if major == 2 else 3 diff --git a/tests/data/numeric_literals_py2.py b/tests/data/numeric_literals_py2.py deleted file mode 100644 index 8f85c43f265..00000000000 --- a/tests/data/numeric_literals_py2.py +++ /dev/null @@ -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 diff --git a/tests/data/python2.py b/tests/data/python2.py deleted file mode 100644 index 4a22f46de42..00000000000 --- a/tests/data/python2.py +++ /dev/null @@ -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())) diff --git a/tests/data/python2_print_function.py b/tests/data/python2_print_function.py deleted file mode 100755 index 81b8d8a70ce..00000000000 --- a/tests/data/python2_print_function.py +++ /dev/null @@ -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) diff --git a/tests/data/python2_unicode_literals.py b/tests/data/python2_unicode_literals.py deleted file mode 100644 index 2fe70392af6..00000000000 --- a/tests/data/python2_unicode_literals.py +++ /dev/null @@ -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" diff --git a/tests/test_black.py b/tests/test_black.py index 628647ed977..5be4ae8533c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -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() diff --git a/tests/test_blackd.py b/tests/test_blackd.py index cc750b40567..37431fcad00 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -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) diff --git a/tests/test_format.py b/tests/test_format.py index 30099aaf1bc..6651272a87c 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -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") diff --git a/tox.ini b/tox.ini index 683a5439ea9..89e8c9feeb9 100644 --- a/tox.ini +++ b/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,10 @@ deps = commands = pip install -e .[d] coverage erase - pytest tests --run-optional no_python2 \ + pytest tests --run-optional \ --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 +38,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 \ From 36d6cee5951e655ddf8cc320ee89453551a5394e Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 1 Jan 2022 16:44:26 -0500 Subject: [PATCH 2/8] Primer: drop sqlalchemy as it supports Python 2 --- src/black_primer/primer.json | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index 8c966e346d9..d8e13edeb06 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -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, From 99aed029666610e5514be908226379c3fbd5a494 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 1 Jan 2022 16:50:07 -0500 Subject: [PATCH 3/8] Add changelog entry --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index cb637d94c11..0da265cb9ab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 From f424a59489fb7ae318cf606fcc50913a83a64f2c Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 1 Jan 2022 16:58:35 -0500 Subject: [PATCH 4/8] Fix tox --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 89e8c9feeb9..090dc522cad 100644 --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,7 @@ deps = commands = pip install -e .[d] coverage erase - pytest tests --run-optional \ - --run-optional no_jupyter \ + pytest tests --run-optional no_jupyter \ !ci: --numprocesses auto \ --cov {posargs} pip install -e .[jupyter] From 9fe7ce24bbb2c7a302459661dd86f4c5bc603b84 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 6 Jan 2022 18:20:55 -0500 Subject: [PATCH 5/8] Drop unnecessary phrase in FAQ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Hildén --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index c7814e34f10..56d060190f5 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -76,7 +76,7 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are ## Does Black support Python 2? Formatting Python 2 code support was removed in version 22.0. In terms of running -_Black_, Python 3.6 or newer is required since the first release. +_Black_, Python 3.6 or newer is required. ## Why does my linter or typechecker complain after I format my code? From da34d76bc266d9e49ec44309dcd78a369895ae9f Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sun, 9 Jan 2022 10:38:40 -0500 Subject: [PATCH 6/8] Reduce diff noise and improve wording Co-authored-by: Jelle Zijlstra --- docs/faq.md | 3 +-- src/black/mode.py | 24 ++++++++++++------------ src/black/strings.py | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 56d060190f5..7f72da971af 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -75,8 +75,7 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are ## Does Black support Python 2? -Formatting Python 2 code support was removed in version 22.0. In terms of running -_Black_, Python 3.6 or newer is required. +Formatting Python 2 code support was removed in version 22.0. ## Why does my linter or typechecker complain after I format my code? diff --git a/src/black/mode.py b/src/black/mode.py index 1151bc481ad..5e04525cfc9 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -31,20 +31,20 @@ class TargetVersion(Enum): class Feature(Enum): - F_STRINGS = 1 - NUMERIC_UNDERSCORES = 2 - TRAILING_COMMA_IN_CALL = 3 - TRAILING_COMMA_IN_DEF = 4 + F_STRINGS = 2 + NUMERIC_UNDERSCORES = 3 + TRAILING_COMMA_IN_CALL = 4 + TRAILING_COMMA_IN_DEF = 5 # The following two feature-flags are mutually exclusive, and exactly one should be # set for every version of python. - ASYNC_IDENTIFIERS = 5 - ASYNC_KEYWORDS = 6 - ASSIGNMENT_EXPRESSIONS = 7 - POS_ONLY_ARGUMENTS = 8 - RELAXED_DECORATORS = 9 - PATTERN_MATCHING = 10 - UNPACKING_ON_FLOW = 11 - ANN_ASSIGN_EXTENDED_RHS = 12 + ASYNC_IDENTIFIERS = 6 + ASYNC_KEYWORDS = 7 + ASSIGNMENT_EXPRESSIONS = 8 + POS_ONLY_ARGUMENTS = 9 + RELAXED_DECORATORS = 10 + PATTERN_MATCHING = 11 + UNPACKING_ON_FLOW = 12 + ANN_ASSIGN_EXTENDED_RHS = 13 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags diff --git a/src/black/strings.py b/src/black/strings.py index 2d3e4e6fcf1..262c2ba4313 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -146,7 +146,7 @@ def normalize_string_prefix(s: str) -> str: new_prefix = ( orig_prefix.replace("F", "f") .replace("B", "b") - .replace("U", "u") + .replace("U", "") .replace("u", "") ) return f"{new_prefix}{match.group(2)}" From 4c638f8b7bb7463c7e3fa2c645635f20560f1c14 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 10 Jan 2022 03:53:38 -0800 Subject: [PATCH 7/8] Update docs/faq.md --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 7f72da971af..c7d5ec33ad9 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -75,7 +75,7 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are ## Does Black support Python 2? -Formatting Python 2 code support was removed in version 22.0. +Support for formatting Python 2 code was removed in version 22.0. ## Why does my linter or typechecker complain after I format my code? From 79669d99289cdc13698f96194753c2c53ab94069 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 10 Jan 2022 03:53:45 -0800 Subject: [PATCH 8/8] Update src/blackd/__init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Hildén --- src/blackd/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index cfcc2774211..0463f169e19 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -175,7 +175,7 @@ def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersi if len(rest) > 0: minor = int(rest[0]) if major == 2: - raise InvalidVariantHeader("Python 2 is unsupported") + raise InvalidVariantHeader("Python 2 is not supported") else: # Default to lowest supported minor version. minor = 7 if major == 2 else 3