Skip to content

Purge typing.Text and typing.ContextManager from third-party stubs no longer supporting Python 2 #7469

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 9, 2022

Conversation

AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Mar 9, 2022

I do love a good codemod.

Accomplished using the following script.
#!/usr/bin/env python3

import ast
import re
import subprocess
import sys
from itertools import chain
from pathlib import Path
from typing import NamedTuple


class DeleteableImport(NamedTuple):
    old: str
    replacement: str


FAILURES = []


def fix_bad_syntax(path: Path) -> None:
    with open(path) as f:
        stub = f.read()

    lines = stub.splitlines()
    tree = ast.parse(stub)
    imports_to_delete = {}
    text_from_typing = False

    class BadImportFinder(ast.NodeVisitor):
        def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
            nonlocal text_from_typing

            if node.module != "typing":
                return

            if any(cls.name == "Text" for cls in node.names):
                text_from_typing = True
            else:
                return

            new_import_list = [cls for cls in node.names if cls.name != "Text"]

            ### DEALING WITH EXISTING IMPORT STATEMENTS ###

            # Scenario (1): Now we don't need *any* imports from typing any more.
            if not new_import_list:
                imports_to_delete[node.lineno - 1] = DeleteableImport(old=ast.unparse(node), replacement="")

            # Scenario (2): we still need imports from typing; the existing import statement is only one line
            elif node.lineno == node.end_lineno:
                imports_to_delete[node.lineno - 1] = DeleteableImport(
                    old=ast.unparse(node),
                    replacement=ast.unparse(ast.ImportFrom(module="typing", names=new_import_list, level=0)),
                )

            # Scenario (3): we still need imports from typing; the existing import statement is multiline.
            else:
                for cls in node.names:
                    if cls.name == "Text":
                        imports_to_delete[cls.lineno - 1] = DeleteableImport(
                            old=f"Text," if cls.asname is None else f"Text as {cls.asname},", replacement=""
                        )

    BadImportFinder().visit(tree)

    if not text_from_typing:
        return

    for lineno, (old_syntax, new_syntax) in imports_to_delete.items():
        lines[lineno] = lines[lineno].replace(old_syntax, new_syntax)

    try:
        new_tree = ast.parse("\n".join(lines))
    except SyntaxError:
        sys.stderr.write(f"Error converting new syntax in {path}")
        FAILURES.append(path)
    else:
        lines_with_bad_syntax = []

        class OldSyntaxFinder(ast.NodeVisitor):
            def visit_Name(self, node: ast.Name) -> None:
                if node.id == "Text":
                    lines_with_bad_syntax.append(node.lineno - 1)
                self.generic_visit(node)

        OldSyntaxFinder().visit(new_tree)

        for lineno in lines_with_bad_syntax:
            lines[lineno] = re.sub(r"\bText\b", "str", lines[lineno])

    with open(path, "w") as f:
        f.write("\n".join(lines) + "\n")


def main() -> None:
    print("STARTING RUN: Will attempt to fix new syntax in the stubs directory...\n\n")
    for path in Path(".").rglob("*.pyi"):
        if not any(
            str(path).startswith(stubname) for stubname in {"boto", "cryptography", "paramiko", "pysftp", "pyvmomi", "six"}
        ):
            continue
        print(f"Attempting to convert {path} to new syntax.")
        fix_bad_syntax(path)

    print("\n\nSTARTING ISORT...\n\n")
    subprocess.run([sys.executable, "-m", "isort", "."])

    print("\n\nSTARTING BLACK...\n\n")
    subprocess.run([sys.executable, "-m", "black", "."])

    if FAILURES:
        print("\n\nFAILED to convert the following files to new syntax:\n")
        for path in FAILURES:
            print(f"- {path}")
    else:
        print("\n\nThere were ZERO failures in converting to new syntax. HOORAY!!\n\n")

    print('\n\nRunning "check_new_syntax.py"...\n\n')
    subprocess.run([sys.executable, "../tests/check_new_syntax.py"])


if __name__ == "__main__":
    main()

This PR only modifies the stubs identified as newly Python3-only in #7466 (comment)

@AlexWaygood AlexWaygood changed the title Purge typing.Text from stubs no longer supporting Python 2 Purge typing.Text from third-party stubs no longer supporting Python 2 Mar 9, 2022
@AlexWaygood AlexWaygood marked this pull request as draft March 9, 2022 20:42
@AlexWaygood AlexWaygood changed the title Purge typing.Text from third-party stubs no longer supporting Python 2 Purge typing.Text and typing.ContextManager from third-party stubs no longer supporting Python 2 Mar 9, 2022
@AlexWaygood AlexWaygood marked this pull request as ready for review March 9, 2022 20:45

_T = TypeVar("_T", Text, bytes)
_T = TypeVar("_T", str, bytes)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aka AnyStr

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

@AlexWaygood AlexWaygood merged commit 9a1f5fb into python:master Mar 9, 2022
@AlexWaygood AlexWaygood deleted the python2 branch March 9, 2022 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants