]> git.madduck.net Git - etc/vim.git/commitdiff

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Remove Python 2 support (#2740)
authorRichard Si <63936253+ichard26@users.noreply.github.com>
Mon, 10 Jan 2022 12:16:30 +0000 (07:16 -0500)
committerGitHub <noreply@github.com>
Mon, 10 Jan 2022 12:16:30 +0000 (04:16 -0800)
*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.

27 files changed:
.github/ISSUE_TEMPLATE/bug_report.md
CHANGES.md
README.md
action/main.py
docs/faq.md
docs/getting_started.md
docs/integrations/github_actions.md
docs/the_black_code_style/current_style.md
pyproject.toml
setup.py
src/black/__init__.py
src/black/linegen.py
src/black/mode.py
src/black/nodes.py
src/black/numerics.py
src/black/parsing.py
src/black/strings.py
src/black_primer/primer.json
src/blackd/__init__.py
tests/data/numeric_literals_py2.py [deleted file]
tests/data/python2.py [deleted file]
tests/data/python2_print_function.py [deleted file]
tests/data/python2_unicode_literals.py [deleted file]
tests/test_black.py
tests/test_blackd.py
tests/test_format.py
tox.ini

index cb64cf9325d56d2da3e3335b9d15429482967962..48aa9291b0523359cd34eb3924bd7a724aae4e58 100644 (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.
index ec2f5dc52ab118673dfc895598cfc9e7cc991085..bfee1b6f25949c80b7c6710491f05d48047a3fd7 100644 (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
index 2adf60a783a1cd8c77d0e89010a97ecafa96c122..e2b0d17ecfdf3739cabfed070b321241b0f38322 100644 (file)
--- 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:
 
index fde312553bf3b8554675c42009011af94b55b6fd..d14b10f421de32cdb58935a3d8ac60d8c5050578 100644 (file)
@@ -14,7 +14,7 @@ VERSION = os.getenv("INPUT_VERSION", default="")
 
 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(
index 0a966c99c7f0ec7a07f0b43c2ec16f6fb342476c..c7d5ec33ad9dd1a0f17b21b75a7792b4453b04e8 100644 (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?
 
index c79dc607c4afc6aa744b104f7c9002d62888f6c9..987290ac91fb0655777eaa3e136428eeb4c97730 100644 (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:
 
index e866a3cc616429585a8bcad69e9de10d35ebbfdc..c9697cc05dee89420cfbbc9d302d74246a84f9e9 100644 (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
 
index b9ab350cd12882d6d3bd43f3fc3b010de80324f6..68dff3eef3f0469e73ae85ed26389c6d9248bfa3 100644 (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
 
index aebbc0da29c84b32a77676309f9324bbf13252b3..ec617790039bf290b644b58e00cff9bf78e43f3b 100644 (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",
 ]
index 8ff498e4fef9784aef67eab3ff6095d485c3a94d..57632498debc65d850bc1376fc859e5e44b92e55 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -112,7 +112,6 @@ setup(
     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"],
     },
index 9bc8fc15c49d8136d7da05e10670dac4de793ec5..283c53f0db32d8de42be231b92bc1ad9544319a2 100644 (file)
@@ -1083,20 +1083,8 @@ def format_str(src_contents: str, *, mode: Mode) -> FileContent:
     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
 
 
index dc238c3aee4f293d4dc8786f03ee41e439d85718..6008c773f943f0c22e2fec52eb6f5bfbcea29ed8 100644 (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 @@ class LineGenerator(Visitor[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 @@ class LineGenerator(Visitor[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]
index bd4428add66a22bb6f7db0bca6082a33f28df5bd..5e04525cfc989a5619255e4119581c7c263761d0 100644 (file)
@@ -20,7 +20,6 @@ from black.const import DEFAULT_LINE_LENGTH
 
 
 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 @@ FUTURE_FLAG_TO_FEATURE: Final = {
 
 
 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 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
         Feature.ASYNC_IDENTIFIERS,
     },
     TargetVersion.PY37: {
-        Feature.UNICODE_LITERALS,
         Feature.F_STRINGS,
         Feature.NUMERIC_UNDERSCORES,
         Feature.TRAILING_COMMA_IN_CALL,
@@ -109,7 +76,6 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
         Feature.FUTURE_ANNOTATIONS,
     },
     TargetVersion.PY38: {
-        Feature.UNICODE_LITERALS,
         Feature.F_STRINGS,
         Feature.NUMERIC_UNDERSCORES,
         Feature.TRAILING_COMMA_IN_CALL,
@@ -122,7 +88,6 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
         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 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
         Feature.ANN_ASSIGN_EXTENDED_RHS,
     },
     TargetVersion.PY310: {
-        Feature.UNICODE_LITERALS,
         Feature.F_STRINGS,
         Feature.NUMERIC_UNDERSCORES,
         Feature.TRAILING_COMMA_IN_CALL,
index 75a234740246ec4efed5aedcbfb5eead998abb26..74dfa896295fae424d8bc678f66d06e7094d04c1 100644 (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
index cb1c83e7b7896e92ba16ffc266d0e531a41cea2a..879e5b2cf36a5d37904354668ca965b0ff0d7470 100644 (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
index 76e9de023c7fe1bd53eeccd491c71c6fd79c2ba1..13fa67ee84dabb07f183f8bee0bd8b8742fac490 100644 (file)
@@ -4,7 +4,7 @@ Parse Python code and perform AST validation.
 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.mode import TargetVersion, Feature, supports_feature
 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 @@ except ImportError:
         )
         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)):
index 06a5da01f0cf71f786f3878480e71282731f01c3..262c2ba4313bee09839008f59bf49d4d26543dc6 100644 (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)}"
 
 
index 8c966e346d92c676813ccc7786b0535d4a8a1681..d8e13edeb066ae2cb9811f12932e0a73de7ff800 100644 (file)
       "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,
index cc966404a743a37bd2059c89732e7ad9d3b8b384..0463f169e196518eb7b9d611a321ea4abc9a9216 100644 (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
diff --git a/tests/data/numeric_literals_py2.py b/tests/data/numeric_literals_py2.py
deleted file mode 100644 (file)
index 8f85c43..0000000
+++ /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 (file)
index 4a22f46..0000000
+++ /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 (executable)
index 81b8d8a..0000000
+++ /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 (file)
index 2fe7039..0000000
+++ /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"
index 628647ed9774d18a007e7d0d50538c98f6a715a8..5be4ae8533c249ff03829a0d35a6dcfdcb7f46ac 100644 (file)
@@ -724,24 +724,15 @@ class BlackTestCase(BlackBaseTestCase):
 
         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 @@ class BlackTestCase(BlackBaseTestCase):
         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 @@ class TestFileCollection:
         )
 
 
-@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()
index cc750b40567d7c4c0f393a272780f41115162b4e..37431fcad0000690964ade29403d26a5a056e803 100644 (file)
@@ -77,6 +77,9 @@ class BlackDTestCase(AioHTTPTestCase):
         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 @@ class BlackDTestCase(AioHTTPTestCase):
         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)
index 30099aaf1bc2f175acea55842d10f1768d94cc67..6651272a87cf91ef3a3ea3f04283aa30e44f8616 100644 (file)
@@ -55,12 +55,6 @@ SIMPLE_CASES = [
     "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 683a5439ea98e755b27b42cf0bfeda28bac13ab2..090dc522cadb6662fbe561dced34f0ed39304dc2 100644 (file)
--- 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,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 \