Browse Source

Fixed #34983 -- Deprecated django.utils.itercompat.is_iterable().

Nick Pope 1 year ago
parent
commit
5e28cd3f2c

+ 4 - 3
django/contrib/auth/models.py

@@ -1,3 +1,5 @@
+from collections.abc import Iterable
+
 from django.apps import apps
 from django.contrib import auth
 from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
@@ -8,7 +10,6 @@ from django.core.mail import send_mail
 from django.db import models
 from django.db.models.manager import EmptyManager
 from django.utils import timezone
-from django.utils.itercompat import is_iterable
 from django.utils.translation import gettext_lazy as _
 
 from .validators import UnicodeUsernameValidator
@@ -315,7 +316,7 @@ class PermissionsMixin(models.Model):
         Return True if the user has each of the specified permissions. If
         object is passed, check if the user has all required perms for it.
         """
-        if not is_iterable(perm_list) or isinstance(perm_list, str):
+        if not isinstance(perm_list, Iterable) or isinstance(perm_list, str):
             raise ValueError("perm_list must be an iterable of permissions.")
         return all(self.has_perm(perm, obj) for perm in perm_list)
 
@@ -480,7 +481,7 @@ class AnonymousUser:
         return _user_has_perm(self, perm, obj=obj)
 
     def has_perms(self, perm_list, obj=None):
-        if not is_iterable(perm_list) or isinstance(perm_list, str):
+        if not isinstance(perm_list, Iterable) or isinstance(perm_list, str):
             raise ValueError("perm_list must be an iterable of permissions.")
         return all(self.has_perm(perm, obj) for perm in perm_list)
 

+ 2 - 2
django/core/checks/registry.py

@@ -1,7 +1,7 @@
+from collections.abc import Iterable
 from itertools import chain
 
 from django.utils.inspect import func_accepts_kwargs
-from django.utils.itercompat import is_iterable
 
 
 class Tags:
@@ -86,7 +86,7 @@ class CheckRegistry:
 
         for check in checks:
             new_errors = check(app_configs=app_configs, databases=databases)
-            if not is_iterable(new_errors):
+            if not isinstance(new_errors, Iterable):
                 raise TypeError(
                     "The function %r did not return a list. All functions "
                     "registered with the checks registry must return a list." % check,

+ 3 - 3
django/db/models/fields/__init__.py

@@ -5,6 +5,7 @@ import operator
 import uuid
 import warnings
 from base64 import b64decode, b64encode
+from collections.abc import Iterable
 from functools import partialmethod, total_ordering
 
 from django import forms
@@ -31,7 +32,6 @@ from django.utils.dateparse import (
 from django.utils.duration import duration_microseconds, duration_string
 from django.utils.functional import Promise, cached_property
 from django.utils.ipv6 import clean_ipv6_address
-from django.utils.itercompat import is_iterable
 from django.utils.text import capfirst
 from django.utils.translation import gettext_lazy as _
 
@@ -317,13 +317,13 @@ class Field(RegisterLookupMixin):
 
     @classmethod
     def _choices_is_value(cls, value):
-        return isinstance(value, (str, Promise)) or not is_iterable(value)
+        return isinstance(value, (str, Promise)) or not isinstance(value, Iterable)
 
     def _check_choices(self):
         if not self.choices:
             return []
 
-        if not is_iterable(self.choices) or isinstance(self.choices, str):
+        if not isinstance(self.choices, Iterable) or isinstance(self.choices, str):
             return [
                 checks.Error(
                     "'choices' must be a mapping (e.g. a dictionary) or an iterable "

+ 2 - 2
django/template/defaulttags.py

@@ -3,6 +3,7 @@ import re
 import sys
 import warnings
 from collections import namedtuple
+from collections.abc import Iterable
 from datetime import datetime
 from itertools import cycle as itertools_cycle
 from itertools import groupby
@@ -10,7 +11,6 @@ from itertools import groupby
 from django.conf import settings
 from django.utils import timezone
 from django.utils.html import conditional_escape, escape, format_html
-from django.utils.itercompat import is_iterable
 from django.utils.lorem_ipsum import paragraphs, words
 from django.utils.safestring import mark_safe
 
@@ -1198,7 +1198,7 @@ def query_string(context, query_dict=None, **kwargs):
         if value is None:
             if key in query_dict:
                 del query_dict[key]
-        elif is_iterable(value) and not isinstance(value, str):
+        elif isinstance(value, Iterable) and not isinstance(value, str):
             query_dict.setlist(key, value)
         else:
             query_dict[key] = value

+ 4 - 2
django/template/library.py

@@ -1,9 +1,9 @@
+from collections.abc import Iterable
 from functools import wraps
 from importlib import import_module
 from inspect import getfullargspec, unwrap
 
 from django.utils.html import conditional_escape
-from django.utils.itercompat import is_iterable
 
 from .base import Node, Template, token_kwargs
 from .exceptions import TemplateSyntaxError
@@ -263,7 +263,9 @@ class InclusionNode(TagHelperNode):
                 t = self.filename
             elif isinstance(getattr(self.filename, "template", None), Template):
                 t = self.filename.template
-            elif not isinstance(self.filename, str) and is_iterable(self.filename):
+            elif not isinstance(self.filename, str) and isinstance(
+                self.filename, Iterable
+            ):
                 t = context.template.engine.select_template(self.filename)
             else:
                 t = context.template.engine.get_template(self.filename)

+ 2 - 2
django/test/client.py

@@ -2,6 +2,7 @@ import json
 import mimetypes
 import os
 import sys
+from collections.abc import Iterable
 from copy import copy
 from functools import partial
 from http import HTTPStatus
@@ -25,7 +26,6 @@ from django.urls import resolve
 from django.utils.encoding import force_bytes
 from django.utils.functional import SimpleLazyObject
 from django.utils.http import urlencode
-from django.utils.itercompat import is_iterable
 from django.utils.regex_helper import _lazy_re_compile
 
 __all__ = (
@@ -303,7 +303,7 @@ def encode_multipart(boundary, data):
             )
         elif is_file(value):
             lines.extend(encode_file(boundary, key, value))
-        elif not isinstance(value, str) and is_iterable(value):
+        elif not isinstance(value, str) and isinstance(value, Iterable):
             for item in value:
                 if is_file(item):
                     lines.extend(encode_file(boundary, key, item))

+ 2 - 2
django/utils/hashable.py

@@ -1,4 +1,4 @@
-from django.utils.itercompat import is_iterable
+from collections.abc import Iterable
 
 
 def make_hashable(value):
@@ -19,7 +19,7 @@ def make_hashable(value):
     try:
         hash(value)
     except TypeError:
-        if is_iterable(value):
+        if isinstance(value, Iterable):
             return tuple(map(make_hashable, value))
         # Non-hashable, non-iterable.
         raise

+ 13 - 0
django/utils/itercompat.py

@@ -1,5 +1,18 @@
+# RemovedInDjango60Warning: Remove this entire module.
+
+import warnings
+
+from django.utils.deprecation import RemovedInDjango60Warning
+
+
 def is_iterable(x):
     "An implementation independent way of checking for iterables"
+    warnings.warn(
+        "django.utils.itercompat.is_iterable() is deprecated. "
+        "Use isinstance(..., collections.abc.Iterable) instead.",
+        RemovedInDjango60Warning,
+        stacklevel=2,
+    )
     try:
         iter(x)
     except TypeError:

+ 3 - 0
docs/internals/deprecation.txt

@@ -59,6 +59,9 @@ details on these changes.
 * The ``ModelAdmin.log_deletion()`` and ``LogEntryManager.log_action()``
   methods will be removed.
 
+* The undocumented ``django.utils.itercompat.is_iterable()`` function and the
+  ``django.utils.itercompat`` module will be removed.
+
 .. _deprecation-removed-in-5.1:
 
 5.1

+ 4 - 0
docs/releases/5.1.txt

@@ -303,6 +303,10 @@ Miscellaneous
   ``ModelAdmin.log_deletions()`` and  ``LogEntryManager.log_actions()``
   instead.
 
+* The undocumented ``django.utils.itercompat.is_iterable()`` function and the
+  ``django.utils.itercompat`` module are deprecated. Use
+  ``isinstance(..., collections.abc.Iterable)`` instead.
+
 Features removed in 5.1
 =======================
 

+ 15 - 0
tests/utils_tests/test_itercompat.py

@@ -0,0 +1,15 @@
+# RemovedInDjango60Warning: Remove this entire module.
+
+from django.test import SimpleTestCase
+from django.utils.deprecation import RemovedInDjango60Warning
+from django.utils.itercompat import is_iterable
+
+
+class TestIterCompat(SimpleTestCase):
+    def test_is_iterable_deprecation(self):
+        msg = (
+            "django.utils.itercompat.is_iterable() is deprecated. "
+            "Use isinstance(..., collections.abc.Iterable) instead."
+        )
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            is_iterable([])