Repository: browniebroke/django-codemod (language: python)

Issue:
Removed compatibility imports in 3.1
**Link to Django docs**
Link to release note section where the change and suitable replacement is mentioned:

https://docs.djangoproject.com/en/3.1/releases/3.1/#id1

**Quote of the relevant paragraph**

In case links gets outdated/removed:

> The compatibility imports of `django.core.exceptions.EmptyResultSet` in `django.db.models.query`, `django.db.models.sql`, and `django.db.models.sql.datastructures` are removed.

> The compatibility import of `django.core.exceptions.FieldDoesNotExist` in `django.db.models.fields` is removed.

> The compatibility imports of `django.forms.utils.pretty_name()` and `django.forms.boundfield.BoundField` in `django.forms.forms` are removed.

> The compatibility imports of `Context`, `ContextPopException`, and `RequestContext` in `django.template.base` are removed.

> The compatibility import of `django.contrib.admin.helpers.ACTION_CHECKBOX_NAME` in `django.contrib.admin` is removed.

**Deprecation timeline**

- Removed in: 3.1
- EmptyResultSet - deprecated in: 1.11
- FieldDoesNotExist - deprecated in: 1.8
- pretty_name - deprecated in: 1.9
- BoundField - deprecated in: 1.9
- Context - deprecated in: 1.7
- ContextPopException - deprecated in: 1.7
- RequestContext - deprecated in: 1.7
- ACTION_CHECKBOX_NAME - deprecated in: 1.3


Discussion / hints:

Fix patch:
diff --git a/commitlint.config.js b/commitlint.config.js
index f451df78..7d55e4dc 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -2,5 +2,6 @@ module.exports = {
   extends: ["@commitlint/config-conventional"],
   rules: {
     "body-max-line-length": [0, "always", Infinity],
+    "header-max-length": [0, "always", Infinity],
   },
 };
diff --git a/django_codemod/visitors/__init__.py b/django_codemod/visitors/__init__.py
index df9ae67c..394ddabd 100644
--- a/django_codemod/visitors/__init__.py
+++ b/django_codemod/visitors/__init__.py
@@ -1,4 +1,4 @@
-from .admin import InlineHasAddPermissionsTransformer
+from .admin import ActionCheckboxNameTransformer, InlineHasAddPermissionsTransformer
 from .core import URLResolversTransformer
 from .crypto import GetRandomStringTransformer
 from .decorators import AvailableAttrsTransformer, ContextDecoratorTransformer
@@ -7,6 +7,13 @@
     SmartTextTransformer,
     UnicodeCompatibleTransformer,
 )
+from .exceptions import (
+    DatastructuresEmptyResultSetTransformer,
+    FieldDoesNotExistTransformer,
+    QueryEmptyResultSetTransformer,
+    SqlEmptyResultSetTransformer,
+)
+from .forms import BoundFieldTransformer, PrettyNameTransformer
 from .html import UnescapeEntitiesTransformer
 from .http import (
     CookieDateTransformer,
@@ -32,6 +39,12 @@
 )
 from .shortcuts import RenderToResponseTransformer
 from .signals import SignalDisconnectWeakTransformer
+from .template_context import (
+    BaseContextTransformer,
+    ContextPopExceptionTransformer,
+    ContextTransformer,
+    RequestContextTransformer,
+)
 from .template_tags import AssignmentTagTransformer
 from .timezone import FixedOffsetTransformer
 from .translations import (
@@ -45,10 +58,17 @@
 
 __all__ = (
     "AbsPathTransformer",
+    "ActionCheckboxNameTransformer",
     "AssignmentTagTransformer",
     "AvailableAttrsTransformer",
+    "BaseContextTransformer",
+    "BoundFieldTransformer",
     "ContextDecoratorTransformer",
+    "ContextPopExceptionTransformer",
+    "ContextTransformer",
     "CookieDateTransformer",
+    "DatastructuresEmptyResultSetTransformer",
+    "FieldDoesNotExistTransformer",
     "FixedOffsetTransformer",
     "FloatRangeFormFieldTransformer",
     "FloatRangeModelFieldTransformer",
@@ -66,17 +86,21 @@
     "ModelsPermalinkTransformer",
     "NullBooleanFieldTransformer",
     "OnDeleteTransformer",
+    "PrettyNameTransformer",
+    "QueryEmptyResultSetTransformer",
     "QuerySetPaginatorTransformer",
     "RenderToResponseTransformer",
+    "RequestContextTransformer",
     "SignalDisconnectWeakTransformer",
     "SmartTextTransformer",
+    "SqlEmptyResultSetTransformer",
     "UGetTextLazyTransformer",
     "UGetTextNoopTransformer",
     "UGetTextTransformer",
-    "UnescapeEntitiesTransformer",
     "UNGetTextLazyTransformer",
     "UNGetTextTransformer",
-    "UnicodeCompatibleTransformer",
     "URLResolversTransformer",
     "URLTransformer",
+    "UnescapeEntitiesTransformer",
+    "UnicodeCompatibleTransformer",
 )
diff --git a/django_codemod/visitors/admin.py b/django_codemod/visitors/admin.py
index 483fb2af..10ef1ea9 100644
--- a/django_codemod/visitors/admin.py
+++ b/django_codemod/visitors/admin.py
@@ -15,9 +15,13 @@
 )
 from libcst import matchers as m
 
-from django_codemod.constants import DJANGO_2_1, DJANGO_3_0
+from django_codemod.constants import DJANGO_1_3, DJANGO_2_1, DJANGO_3_0, DJANGO_3_1
 from django_codemod.utils.calls import parse_arg, parse_param
-from django_codemod.visitors.base import BaseDjCodemodTransformer, module_matcher
+from django_codemod.visitors.base import (
+    BaseDjCodemodTransformer,
+    BaseRenameTransformer,
+    module_matcher,
+)
 
 
 class InlineHasAddPermissionsTransformer(BaseDjCodemodTransformer):
@@ -143,3 +147,12 @@ def leave_Call(self, original_node: Call, updated_node: Call) -> BaseExpression:
             )
             return updated_node.with_changes(args=updated_args)
         return super().leave_Call(original_node, updated_node)
+
+
+class ActionCheckboxNameTransformer(BaseRenameTransformer):
+    """Replace `django.contrib.admin.ACTION_CHECKBOX_NAME` compatibility import"""
+
+    deprecated_in = DJANGO_1_3
+    removed_in = DJANGO_3_1
+    rename_from = "django.contrib.admin.ACTION_CHECKBOX_NAME"
+    rename_to = "django.contrib.admin.helpers.ACTION_CHECKBOX_NAME"
diff --git a/django_codemod/visitors/exceptions.py b/django_codemod/visitors/exceptions.py
new file mode 100644
index 00000000..6d81070d
--- /dev/null
+++ b/django_codemod/visitors/exceptions.py
@@ -0,0 +1,41 @@
+from django_codemod.constants import DJANGO_1_8, DJANGO_1_11, DJANGO_3_1
+from django_codemod.visitors.base import BaseRenameTransformer
+
+
+class QueryEmptyResultSetTransformer(BaseRenameTransformer):
+    """Replace `django.db.models.query.EmptyResultSet` compatibility import."""
+
+    deprecated_in = DJANGO_1_11
+    removed_in = DJANGO_3_1
+    rename_from = "django.db.models.query.EmptyResultSet"
+    rename_to = "django.core.exceptions.EmptyResultSet"
+
+
+class SqlEmptyResultSetTransformer(BaseRenameTransformer):
+    """Replace `django.db.models.sql.EmptyResultSet` compatibility import."""
+
+    deprecated_in = DJANGO_1_11
+    removed_in = DJANGO_3_1
+    rename_from = "django.db.models.sql.EmptyResultSet"
+    rename_to = "django.core.exceptions.EmptyResultSet"
+
+
+class DatastructuresEmptyResultSetTransformer(BaseRenameTransformer):
+    """
+    Replace `django.db.models.sql.datastructures.EmptyResultSet` compatibility
+    import.
+    """
+
+    deprecated_in = DJANGO_1_11
+    removed_in = DJANGO_3_1
+    rename_from = "django.db.models.sql.datastructures.EmptyResultSet"
+    rename_to = "django.core.exceptions.EmptyResultSet"
+
+
+class FieldDoesNotExistTransformer(BaseRenameTransformer):
+    """Replace `django.db.models.fields.FieldDoesNotExist` compatibility import."""
+
+    deprecated_in = DJANGO_1_8
+    removed_in = DJANGO_3_1
+    rename_from = "django.db.models.fields.FieldDoesNotExist"
+    rename_to = "django.core.exceptions.FieldDoesNotExist"
diff --git a/django_codemod/visitors/forms.py b/django_codemod/visitors/forms.py
new file mode 100644
index 00000000..95bfa52f
--- /dev/null
+++ b/django_codemod/visitors/forms.py
@@ -0,0 +1,20 @@
+from django_codemod.constants import DJANGO_1_9, DJANGO_3_1
+from django_codemod.visitors.base import BaseRenameTransformer
+
+
+class PrettyNameTransformer(BaseRenameTransformer):
+    """Replace `django.forms.forms.pretty_name` compatibility import."""
+
+    deprecated_in = DJANGO_1_9
+    removed_in = DJANGO_3_1
+    rename_from = "django.forms.forms.pretty_name"
+    rename_to = "django.forms.utils.pretty_name"
+
+
+class BoundFieldTransformer(BaseRenameTransformer):
+    """Replace `django.forms.forms.BoundField` compatibility import."""
+
+    deprecated_in = DJANGO_1_9
+    removed_in = DJANGO_3_1
+    rename_from = "django.forms.forms.BoundField"
+    rename_to = "django.forms.boundfield.BoundField"
diff --git a/django_codemod/visitors/template_context.py b/django_codemod/visitors/template_context.py
new file mode 100644
index 00000000..dd8d804f
--- /dev/null
+++ b/django_codemod/visitors/template_context.py
@@ -0,0 +1,38 @@
+from django_codemod.constants import DJANGO_1_7, DJANGO_3_1
+from django_codemod.visitors.base import BaseRenameTransformer
+
+
+class BaseContextTransformer(BaseRenameTransformer):
+    """Replace `django.template.base.BaseContext` compatibility import."""
+
+    deprecated_in = DJANGO_1_7
+    removed_in = DJANGO_3_1
+    rename_from = "django.template.base.BaseContext"
+    rename_to = "django.template.context.BaseContext"
+
+
+class ContextTransformer(BaseRenameTransformer):
+    """Replace `django.template.base.Context` compatibility import."""
+
+    deprecated_in = DJANGO_1_7
+    removed_in = DJANGO_3_1
+    rename_from = "django.template.base.Context"
+    rename_to = "django.template.context.Context"
+
+
+class RequestContextTransformer(BaseRenameTransformer):
+    """Replace `django.template.base.RequestContext` compatibility import."""
+
+    deprecated_in = DJANGO_1_7
+    removed_in = DJANGO_3_1
+    rename_from = "django.template.base.RequestContext"
+    rename_to = "django.template.context.RequestContext"
+
+
+class ContextPopExceptionTransformer(BaseRenameTransformer):
+    """Replace `django.template.base.ContextPopException` compatibility import."""
+
+    deprecated_in = DJANGO_1_7
+    removed_in = DJANGO_3_1
+    rename_from = "django.template.base.ContextPopException"
+    rename_to = "django.template.context.ContextPopException"
diff --git a/docs/codemods.md b/docs/codemods.md
index 2399384a..239a9866 100644
--- a/docs/codemods.md
+++ b/docs/codemods.md
@@ -69,6 +69,35 @@ Applied by passing the `--removed-in 3.1` or `--deprecated-in 2.2` option:
 - Replace the `FloatRangeField` model and form fields in
   `django.contrib.postgres` by `DecimalRangeField`.
 
+Applied by passing the `--removed-in 3.1` or `--deprecated-in 1.11` option:
+
+- Replace compatibility imports of `EmptyResultSet`
+  in `django.db.models.query`, `django.db.models.sql`, and
+  `django.db.models.sql.datastructures` by import from `django.core.exceptions`.
+
+Applied by passing the `--removed-in 3.1` or `--deprecated-in 1.9` option:
+
+- Replace compatibility imports of `pretty_name`
+  in `django.forms.forms` by import from `django.forms.utils`.
+- Replace compatibility imports of `BoundField`
+  in `django.forms.forms` by import from `django.forms.boundfield`.
+
+Applied by passing the `--removed-in 3.1` or `--deprecated-in 1.8` option:
+
+- Replace compatibility imports of `FieldDoesNotExist`
+  in `django.db.models.fields` by import from `django.core.exceptions`.
+
+Applied by passing the `--removed-in 3.1` or `--deprecated-in 1.7` option:
+
+- Replace compatibility imports of `BaseContext`, `Context`,
+  `ContextPopException` and `RequestContext` in `django.template.base`
+  by import from `django.template.context`.
+
+Applied by passing the `--removed-in 3.1` or `--deprecated-in 1.3` option:
+
+- Replace compatibility imports of `ACTION_CHECKBOX_NAME` in
+  `django.contrib.admin` by import from `django.contrib.admin.helpers`.
+
 ## Removed in Django 4.0
 
 Applied by passing the `--removed-in 4.0` or `--deprecated-in 3.0` option:


Test patch:
diff --git a/tests/visitors/test_admin.py b/tests/visitors/test_admin.py
index 6fea859c..90d50f1a 100644
--- a/tests/visitors/test_admin.py
+++ b/tests/visitors/test_admin.py
@@ -1,6 +1,9 @@
 from parameterized import parameterized
 
-from django_codemod.visitors import InlineHasAddPermissionsTransformer
+from django_codemod.visitors import (
+    ActionCheckboxNameTransformer,
+    InlineHasAddPermissionsTransformer,
+)
 from tests.visitors.base import BaseVisitorTest
 
 
@@ -194,3 +197,17 @@ def has_add_permission(self, request):
                     return False
         """
         self.assertCodemod(before, after)
+
+
+class TestActionCheckboxNameTransformer(BaseVisitorTest):
+
+    transformer = ActionCheckboxNameTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.contrib.admin import ACTION_CHECKBOX_NAME
+        """
+        after = """
+            from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
+        """
+        self.assertCodemod(before, after)
diff --git a/tests/visitors/test_exceptions.py b/tests/visitors/test_exceptions.py
new file mode 100644
index 00000000..a8e1fa1c
--- /dev/null
+++ b/tests/visitors/test_exceptions.py
@@ -0,0 +1,79 @@
+from django_codemod.visitors import (
+    DatastructuresEmptyResultSetTransformer,
+    FieldDoesNotExistTransformer,
+    QueryEmptyResultSetTransformer,
+    SqlEmptyResultSetTransformer,
+)
+from tests.visitors.base import BaseVisitorTest
+
+
+class TestQueryEmptyResultSetTransformer(BaseVisitorTest):
+
+    transformer = QueryEmptyResultSetTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.db.models.query import EmptyResultSet
+
+            EmptyResultSet()
+        """
+        after = """
+            from django.core.exceptions import EmptyResultSet
+
+            EmptyResultSet()
+        """
+        self.assertCodemod(before, after)
+
+
+class TestSqlEmptyResultSetTransformer(BaseVisitorTest):
+
+    transformer = SqlEmptyResultSetTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.db.models.sql import EmptyResultSet
+
+            EmptyResultSet()
+        """
+        after = """
+            from django.core.exceptions import EmptyResultSet
+
+            EmptyResultSet()
+        """
+        self.assertCodemod(before, after)
+
+
+class TestDatastructuresEmptyResultSetTransformer(BaseVisitorTest):
+
+    transformer = DatastructuresEmptyResultSetTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.db.models.sql.datastructures import EmptyResultSet
+
+            EmptyResultSet()
+        """
+        after = """
+            from django.core.exceptions import EmptyResultSet
+
+            EmptyResultSet()
+        """
+        self.assertCodemod(before, after)
+
+
+class TestFieldDoesNotExistTransformer(BaseVisitorTest):
+
+    transformer = FieldDoesNotExistTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.db.models.fields import FieldDoesNotExist
+
+            FieldDoesNotExist()
+        """
+        after = """
+            from django.core.exceptions import FieldDoesNotExist
+
+            FieldDoesNotExist()
+        """
+        self.assertCodemod(before, after)
diff --git a/tests/visitors/test_forms.py b/tests/visitors/test_forms.py
new file mode 100644
index 00000000..b97cad45
--- /dev/null
+++ b/tests/visitors/test_forms.py
@@ -0,0 +1,38 @@
+from django_codemod.visitors import BoundFieldTransformer, PrettyNameTransformer
+from tests.visitors.base import BaseVisitorTest
+
+
+class TestPrettyNameTransformer(BaseVisitorTest):
+
+    transformer = PrettyNameTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.forms.forms import pretty_name
+
+            pretty_name()
+        """
+        after = """
+            from django.forms.utils import pretty_name
+
+            pretty_name()
+        """
+        self.assertCodemod(before, after)
+
+
+class TestBoundFieldTransformer(BaseVisitorTest):
+
+    transformer = BoundFieldTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.forms.forms import BoundField
+
+            BoundField()
+        """
+        after = """
+            from django.forms.boundfield import BoundField
+
+            BoundField()
+        """
+        self.assertCodemod(before, after)
diff --git a/tests/visitors/test_template_context.py b/tests/visitors/test_template_context.py
new file mode 100644
index 00000000..5243256f
--- /dev/null
+++ b/tests/visitors/test_template_context.py
@@ -0,0 +1,79 @@
+from django_codemod.visitors import (
+    BaseContextTransformer,
+    ContextPopExceptionTransformer,
+    ContextTransformer,
+    RequestContextTransformer,
+)
+from tests.visitors.base import BaseVisitorTest
+
+
+class TestBaseContextTransformer(BaseVisitorTest):
+
+    transformer = BaseContextTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.template.base import BaseContext
+
+            BaseContext({})
+        """
+        after = """
+            from django.template.context import BaseContext
+
+            BaseContext({})
+        """
+        self.assertCodemod(before, after)
+
+
+class TestContextTransformer(BaseVisitorTest):
+
+    transformer = ContextTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.template.base import Context
+
+            Context({})
+        """
+        after = """
+            from django.template.context import Context
+
+            Context({})
+        """
+        self.assertCodemod(before, after)
+
+
+class TestRequestContextTransformer(BaseVisitorTest):
+
+    transformer = RequestContextTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.template.base import RequestContext
+
+            RequestContext({})
+        """
+        after = """
+            from django.template.context import RequestContext
+
+            RequestContext({})
+        """
+        self.assertCodemod(before, after)
+
+
+class TestContextPopExceptionTransformer(BaseVisitorTest):
+
+    transformer = ContextPopExceptionTransformer
+
+    def test_simple_substitution(self) -> None:
+        before = """
+            from django.template.base import ContextPopException
+
+            raise ContextPopException()
+        """
+        after = """
+            from django.template.context import ContextPopException
+
+            raise ContextPopException()
+        """
+        self.assertCodemod(before, after)