Skip to content

Commit

Permalink
New codemod: remove-future-imports
Browse files Browse the repository at this point in the history
  • Loading branch information
drdavella committed Dec 20, 2023
1 parent 2e4158f commit db7828e
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 0 deletions.
36 changes: 36 additions & 0 deletions integration_tests/test_remove_future_imports.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions src/codemodder/scripts/generate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
),
}


Expand Down
2 changes: 2 additions & 0 deletions src/core_codemods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,6 +58,7 @@
LxmlSafeParsing,
OrderImports,
ProcessSandbox,
RemoveFutureImports,
RemoveUnnecessaryFStr,
RemoveUnusedImports,
RequestsVerify,
Expand Down
11 changes: 11 additions & 0 deletions src/core_codemods/docs/pixee_python_remove-future-imports.md
Original file line number Diff line number Diff line change
@@ -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")
```
59 changes: 59 additions & 0 deletions src/core_codemods/remove_future_imports.py
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions tests/codemods/test_remove_future_imports.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions tests/samples/future_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from __future__ import absolute_import
from __future__ import *

print("HEY")

0 comments on commit db7828e

Please sign in to comment.