From 47e9179e60a1ba7afecaccd5ea51944abfd0e7f5 Mon Sep 17 00:00:00 2001 From: Daniel D'Avella Date: Tue, 19 Dec 2023 16:16:15 -0500 Subject: [PATCH] New codemod: remove-future-imports --- .../test_remove_future_imports.py | 36 +++++++++++ src/codemodder/scripts/generate_docs.py | 4 ++ src/core_codemods/__init__.py | 2 + .../pixee_python_remove-future-imports.md | 11 ++++ src/core_codemods/remove_future_imports.py | 59 +++++++++++++++++++ tests/codemods/test_remove_future_imports.py | 47 +++++++++++++++ tests/samples/future_imports.py | 4 ++ 7 files changed, 163 insertions(+) create mode 100644 integration_tests/test_remove_future_imports.py create mode 100644 src/core_codemods/docs/pixee_python_remove-future-imports.md create mode 100644 src/core_codemods/remove_future_imports.py create mode 100644 tests/codemods/test_remove_future_imports.py create mode 100644 tests/samples/future_imports.py diff --git a/integration_tests/test_remove_future_imports.py b/integration_tests/test_remove_future_imports.py new file mode 100644 index 00000000..8e779074 --- /dev/null +++ b/integration_tests/test_remove_future_imports.py @@ -0,0 +1,36 @@ +from textwrap import dedent + +from core_codemods.remove_future_imports import RemoveFutureImports +from integration_tests.base_test import ( + BaseIntegrationTest, + original_and_expected_from_code_path, +) + + +class TestRemoveFutureImports(BaseIntegrationTest): + codemod = RemoveFutureImports + code_path = "tests/samples/future_imports.py" + + original_code, _ = original_and_expected_from_code_path(code_path, []) + expected_new_code = dedent( + """\ + from __future__ import annotations + + print("HEY") + """ + ) + + expected_diff = """\ +--- ++++ +@@ -1,4 +1,3 @@ +-from __future__ import absolute_import +-from __future__ import * ++from __future__ import annotations + + print("HEY") +""" + + num_changes = 2 + expected_line_change = "1" + change_description = RemoveFutureImports.CHANGE_DESCRIPTION diff --git a/src/codemodder/scripts/generate_docs.py b/src/codemodder/scripts/generate_docs.py index 0375edf3..1786b03e 100644 --- a/src/codemodder/scripts/generate_docs.py +++ b/src/codemodder/scripts/generate_docs.py @@ -170,6 +170,10 @@ class DocMetadata: importance="Low", guidance_explained="A statement with an exception by itself has no effect. Raising the exception is most likely the intended effect and thus we deem it safe.", ), + "remove-future-imports": DocMetadata( + importance="Low", + guidance_explained="Removing future imports is safe and will not cause any issues.", + ), } diff --git a/src/core_codemods/__init__.py b/src/core_codemods/__init__.py index c198b74d..b371f659 100644 --- a/src/core_codemods/__init__.py +++ b/src/core_codemods/__init__.py @@ -15,6 +15,7 @@ from .lxml_safe_parsing import LxmlSafeParsing from .order_imports import OrderImports from .process_creation_sandbox import ProcessSandbox +from .remove_future_imports import RemoveFutureImports from .remove_unnecessary_f_str import RemoveUnnecessaryFStr from .remove_unused_imports import RemoveUnusedImports from .requests_verify import RequestsVerify @@ -57,6 +58,7 @@ LxmlSafeParsing, OrderImports, ProcessSandbox, + RemoveFutureImports, RemoveUnnecessaryFStr, RemoveUnusedImports, RequestsVerify, diff --git a/src/core_codemods/docs/pixee_python_remove-future-imports.md b/src/core_codemods/docs/pixee_python_remove-future-imports.md new file mode 100644 index 00000000..332defc3 --- /dev/null +++ b/src/core_codemods/docs/pixee_python_remove-future-imports.md @@ -0,0 +1,11 @@ +Many older codebases have `__future__` imports for forwards compatibility with features. As of this writing, all but one of those features is now stable in all currently supported versions of Python and so the imports are no longer needed. While such imports are harmless, they are also unnecessary and in most cases you probably just forgot to remove them. + +This codemod removes all such `__future__` imports, preserving only those that are still necessary for forwards compatibility. + +Our changes look like the following: +```diff + import os +-from __future__ import print_function + + print("HELLO") +``` diff --git a/src/core_codemods/remove_future_imports.py b/src/core_codemods/remove_future_imports.py new file mode 100644 index 00000000..1823bcf1 --- /dev/null +++ b/src/core_codemods/remove_future_imports.py @@ -0,0 +1,59 @@ +import libcst as cst + +from codemodder.codemods.api import BaseCodemod, ReviewGuidance + + +DEPRECATED_NAMES = [ + "print_function", + "unicode_literals", + "division", + "absolute_import", + "generators", + "nested_scopes", + "with_statement", + "generator_stop", +] +CURRENT_NAMES = [ + "annotations", +] + + +class RemoveFutureImports(BaseCodemod): + NAME = "remove-future-imports" + SUMMARY = "Remove deprecated `__future__` imports" + REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW + DESCRIPTION = "Remove deprecated `__future__` imports" + REFERENCES = [ + { + "url": "https://docs.python.org/3/library/__future__.html", + "description": "", + }, + ] + + def leave_ImportFrom( + self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom + ): + match original_node.module: + case cst.Name(value="__future__"): + match original_node.names: + case cst.ImportStar(): + names = [ + cst.ImportAlias(name=cst.Name(value=name)) + for name in CURRENT_NAMES + ] + self.add_change(original_node, self.CHANGE_DESCRIPTION) + return original_node.with_changes(names=names) + + updated_names: list[cst.ImportAlias] = [ + name + for name in original_node.names + if name.name.value not in DEPRECATED_NAMES + ] + self.add_change(original_node, self.CHANGE_DESCRIPTION) + return ( + updated_node.with_changes(names=updated_names) + if updated_names + else cst.RemoveFromParent() + ) + + return updated_node diff --git a/tests/codemods/test_remove_future_imports.py b/tests/codemods/test_remove_future_imports.py new file mode 100644 index 00000000..27c78e43 --- /dev/null +++ b/tests/codemods/test_remove_future_imports.py @@ -0,0 +1,47 @@ +import pytest + +from core_codemods.remove_future_imports import RemoveFutureImports, DEPRECATED_NAMES +from tests.codemods.base_codemod_test import BaseCodemodTest + + +class TestRemoveFutureImports(BaseCodemodTest): + codemod = RemoveFutureImports + + @pytest.mark.parametrize("name", DEPRECATED_NAMES) + def test_remove_future_imports(self, tmpdir, name): + original_code = f""" + import os + from __future__ import {name} + print("HEY") + """ + expected_code = """ + import os + print("HEY") + """ + self.run_and_assert(tmpdir, original_code, expected_code) + + def test_update_import_star(self, tmpdir): + original_code = """ + from __future__ import * + """ + expected_code = """ + from __future__ import annotations + """ + self.run_and_assert(tmpdir, original_code, expected_code) + + def test_update_import_deprecated_and_annotations(self, tmpdir): + original_code = """ + from __future__ import print_function, annotations + """ + expected_code = """ + from __future__ import annotations + """ + self.run_and_assert(tmpdir, original_code, expected_code) + + def test_not_from_future(self, tmpdir): + original_code = """ + import os + from __footure__ import print_function + print("HEY") + """ + self.run_and_assert(tmpdir, original_code, original_code) diff --git a/tests/samples/future_imports.py b/tests/samples/future_imports.py new file mode 100644 index 00000000..d1e51200 --- /dev/null +++ b/tests/samples/future_imports.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import +from __future__ import * + +print("HEY")