Repository: browniebroke/django-codemod

Issue:
Django 3.0: HttpRequest.xreadlines()
Removed in Django 3.0:

> HttpRequest.xreadlines() will be removed.

Deprecated [since Django 2.0](https://docs.djangoproject.com/en/2.0/releases/2.0/#deprecated-features-2-0):

> HttpRequest.xreadlines() is deprecated in favor of iterating over the request.


Discussion:

Resolving patch:
diff --git a/django_codemod/visitors/__init__.py b/django_codemod/visitors/__init__.py
--- a/django_codemod/visitors/__init__.py
+++ b/django_codemod/visitors/__init__.py
@@ -8,6 +8,7 @@
 )
 from .html import UnescapeEntitiesTransformer
 from .http import (
+    HttpRequestXReadLinesTransformer,
     HttpUrlQuotePlusTransformer,
     HttpUrlQuoteTransformer,
     HttpUrlUnQuotePlusTransformer,
@@ -41,6 +42,7 @@
     "FloatRangeFormFieldTransformer",
     "FloatRangeModelFieldTransformer",
     "ForceTextTransformer",
+    "HttpRequestXReadLinesTransformer",
     "HttpUrlQuotePlusTransformer",
     "HttpUrlQuoteTransformer",
     "HttpUrlUnQuotePlusTransformer",
diff --git a/django_codemod/visitors/http.py b/django_codemod/visitors/http.py
--- a/django_codemod/visitors/http.py
+++ b/django_codemod/visitors/http.py
@@ -1,5 +1,11 @@
-from django_codemod.constants import DJANGO_3_0, DJANGO_4_0
-from django_codemod.visitors.base import BaseFuncRenameTransformer
+from libcst import BaseExpression, Call
+from libcst import matchers as m
+
+from django_codemod.constants import DJANGO_2_0, DJANGO_3_0, DJANGO_4_0
+from django_codemod.visitors.base import (
+    BaseDjCodemodTransformer,
+    BaseFuncRenameTransformer,
+)
 
 
 class HttpUrlQuoteTransformer(BaseFuncRenameTransformer):
@@ -45,3 +51,30 @@ class IsSafeUrlTransformer(BaseFuncRenameTransformer):
     removed_in = DJANGO_4_0
     rename_from = "django.utils.http.is_safe_url"
     rename_to = "django.utils.http.url_has_allowed_host_and_scheme"
+
+
+class HttpRequestXReadLinesTransformer(BaseDjCodemodTransformer):
+    """Replace `HttpRequest.xreadlines()` by iterating over the request."""
+
+    deprecated_in = DJANGO_2_0
+    removed_in = DJANGO_3_0
+
+    # This should be conservative and only apply changes to:
+    # - variables called `request`/`req`
+    # - `request`/`req` attributes (e.g `self.request`/`view.req`...)
+    matcher = m.Call(
+        func=m.Attribute(
+            value=m.OneOf(
+                m.Name(value="request"),
+                m.Name(value="req"),
+                m.Attribute(attr=m.Name(value="request")),
+                m.Attribute(attr=m.Name(value="req")),
+            ),
+            attr=m.Name(value="xreadlines"),
+        )
+    )
+
+    def leave_Call(self, original_node: Call, updated_node: Call) -> BaseExpression:
+        if m.matches(updated_node, self.matcher):
+            return updated_node.func.value
+        return super().leave_Call(original_node, updated_node)


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -209,6 +209,7 @@ def test_deprecated_in_mapping():
             "AbsPathTransformer",
             "AvailableAttrsTransformer",
             "ContextDecoratorTransformer",
+            "HttpRequestXReadLinesTransformer",
             "LRUCacheTransformer",
             "RenderToResponseTransformer",
             "UnicodeCompatibleTransformer",
@@ -254,6 +255,7 @@ def test_removed_in_mapping():
             "AbsPathTransformer",
             "AvailableAttrsTransformer",
             "ContextDecoratorTransformer",
+            "HttpRequestXReadLinesTransformer",
             "InlineHasAddPermissionsTransformer",
             "LRUCacheTransformer",
             "RenderToResponseTransformer",
diff --git a/tests/visitors/test_http.py b/tests/visitors/test_http.py
--- a/tests/visitors/test_http.py
+++ b/tests/visitors/test_http.py
@@ -1,4 +1,7 @@
+from parameterized import parameterized
+
 from django_codemod.visitors import (
+    HttpRequestXReadLinesTransformer,
     HttpUrlQuotePlusTransformer,
     HttpUrlQuoteTransformer,
     HttpUrlUnQuotePlusTransformer,
@@ -96,3 +99,64 @@ def test_simple_substitution(self) -> None:
             result = url_has_allowed_host_and_scheme('http://test.com/some-path', None)
         """
         self.assertCodemod(before, after)
+
+
+class TestHttpRequestXReadLinesTransformer(BaseVisitorTest):
+
+    transformer = HttpRequestXReadLinesTransformer
+
+    def test_noop_wrong_name(self) -> None:
+        """Don't replace calls for name other than `request` or `req`."""
+        before = after = """
+            for line in r.xreadlines():
+                print(line)
+        """
+        self.assertCodemod(before, after)
+
+    @parameterized.expand(["req", "request"])
+    def test_simple_substitution(self, variable_name) -> None:
+        before = f"""
+            for line in {variable_name}.xreadlines():
+                print(line)
+        """
+        after = f"""
+            for line in {variable_name}:
+                print(line)
+        """
+        self.assertCodemod(before, after)
+
+    @parameterized.expand(["req", "request"])
+    def test_substitution_class_attribute(self, attribute_name) -> None:
+        before = f"""
+            for line in self.{attribute_name}.xreadlines():
+                print(line)
+        """
+        after = f"""
+            for line in self.{attribute_name}:
+                print(line)
+        """
+        self.assertCodemod(before, after)
+
+    @parameterized.expand(["req", "request"])
+    def test_substitution_view_attribute(self, attribute_name) -> None:
+        before = f"""
+            for line in view.{attribute_name}.xreadlines():
+                print(line)
+        """
+        after = f"""
+            for line in view.{attribute_name}:
+                print(line)
+        """
+        self.assertCodemod(before, after)
+
+    @parameterized.expand(["req", "request"])
+    def test_substitution_nested_attribute(self, attribute_name) -> None:
+        before = f"""
+            for line in self.view.{attribute_name}.xreadlines():
+                print(line)
+        """
+        after = f"""
+            for line in self.view.{attribute_name}:
+                print(line)
+        """
+        self.assertCodemod(before, after)