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 19, 2023
1 parent 2e4158f commit cf75cc4
Show file tree
Hide file tree
Showing 7 changed files with 153 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")
```
57 changes: 57 additions & 0 deletions src/core_codemods/remove_future_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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:
case cst.ImportFrom(module=cst.Name(value="__future__"), names=names):
match 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 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

Check warning on line 57 in src/core_codemods/remove_future_imports.py

View check run for this annotation

Codecov / codecov/patch

src/core_codemods/remove_future_imports.py#L57

Added line #L57 was not covered by tests
39 changes: 39 additions & 0 deletions tests/codemods/test_remove_future_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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)
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 cf75cc4

Please sign in to comment.