瀏覽代碼

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

Nick Pope 1 年之前
父節點
當前提交
5e28cd3f2c

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

@@ -1,3 +1,5 @@
+from collections.abc import Iterable
+
 from django.apps import apps
 from django.apps import apps
 from django.contrib import auth
 from django.contrib import auth
 from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
 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 import models
 from django.db.models.manager import EmptyManager
 from django.db.models.manager import EmptyManager
 from django.utils import timezone
 from django.utils import timezone
-from django.utils.itercompat import is_iterable
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from .validators import UnicodeUsernameValidator
 from .validators import UnicodeUsernameValidator
@@ -315,7 +316,7 @@ class PermissionsMixin(models.Model):
         Return True if the user has each of the specified permissions. If
         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.
         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.")
             raise ValueError("perm_list must be an iterable of permissions.")
         return all(self.has_perm(perm, obj) for perm in perm_list)
         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)
         return _user_has_perm(self, perm, obj=obj)
 
 
     def has_perms(self, perm_list, obj=None):
     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.")
             raise ValueError("perm_list must be an iterable of permissions.")
         return all(self.has_perm(perm, obj) for perm in perm_list)
         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 itertools import chain
 
 
 from django.utils.inspect import func_accepts_kwargs
 from django.utils.inspect import func_accepts_kwargs
-from django.utils.itercompat import is_iterable
 
 
 
 
 class Tags:
 class Tags:
@@ -86,7 +86,7 @@ class CheckRegistry:
 
 
         for check in checks:
         for check in checks:
             new_errors = check(app_configs=app_configs, databases=databases)
             new_errors = check(app_configs=app_configs, databases=databases)
-            if not is_iterable(new_errors):
+            if not isinstance(new_errors, Iterable):
                 raise TypeError(
                 raise TypeError(
                     "The function %r did not return a list. All functions "
                     "The function %r did not return a list. All functions "
                     "registered with the checks registry must return a list." % check,
                     "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 uuid
 import warnings
 import warnings
 from base64 import b64decode, b64encode
 from base64 import b64decode, b64encode
+from collections.abc import Iterable
 from functools import partialmethod, total_ordering
 from functools import partialmethod, total_ordering
 
 
 from django import forms
 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.duration import duration_microseconds, duration_string
 from django.utils.functional import Promise, cached_property
 from django.utils.functional import Promise, cached_property
 from django.utils.ipv6 import clean_ipv6_address
 from django.utils.ipv6 import clean_ipv6_address
-from django.utils.itercompat import is_iterable
 from django.utils.text import capfirst
 from django.utils.text import capfirst
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
@@ -317,13 +317,13 @@ class Field(RegisterLookupMixin):
 
 
     @classmethod
     @classmethod
     def _choices_is_value(cls, value):
     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):
     def _check_choices(self):
         if not self.choices:
         if not self.choices:
             return []
             return []
 
 
-        if not is_iterable(self.choices) or isinstance(self.choices, str):
+        if not isinstance(self.choices, Iterable) or isinstance(self.choices, str):
             return [
             return [
                 checks.Error(
                 checks.Error(
                     "'choices' must be a mapping (e.g. a dictionary) or an iterable "
                     "'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 sys
 import warnings
 import warnings
 from collections import namedtuple
 from collections import namedtuple
+from collections.abc import Iterable
 from datetime import datetime
 from datetime import datetime
 from itertools import cycle as itertools_cycle
 from itertools import cycle as itertools_cycle
 from itertools import groupby
 from itertools import groupby
@@ -10,7 +11,6 @@ from itertools import groupby
 from django.conf import settings
 from django.conf import settings
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.html import conditional_escape, escape, format_html
 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.lorem_ipsum import paragraphs, words
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
@@ -1198,7 +1198,7 @@ def query_string(context, query_dict=None, **kwargs):
         if value is None:
         if value is None:
             if key in query_dict:
             if key in query_dict:
                 del query_dict[key]
                 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)
             query_dict.setlist(key, value)
         else:
         else:
             query_dict[key] = value
             query_dict[key] = value

+ 4 - 2
django/template/library.py

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

+ 2 - 2
django/test/client.py

@@ -2,6 +2,7 @@ import json
 import mimetypes
 import mimetypes
 import os
 import os
 import sys
 import sys
+from collections.abc import Iterable
 from copy import copy
 from copy import copy
 from functools import partial
 from functools import partial
 from http import HTTPStatus
 from http import HTTPStatus
@@ -25,7 +26,6 @@ from django.urls import resolve
 from django.utils.encoding import force_bytes
 from django.utils.encoding import force_bytes
 from django.utils.functional import SimpleLazyObject
 from django.utils.functional import SimpleLazyObject
 from django.utils.http import urlencode
 from django.utils.http import urlencode
-from django.utils.itercompat import is_iterable
 from django.utils.regex_helper import _lazy_re_compile
 from django.utils.regex_helper import _lazy_re_compile
 
 
 __all__ = (
 __all__ = (
@@ -303,7 +303,7 @@ def encode_multipart(boundary, data):
             )
             )
         elif is_file(value):
         elif is_file(value):
             lines.extend(encode_file(boundary, key, 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:
             for item in value:
                 if is_file(item):
                 if is_file(item):
                     lines.extend(encode_file(boundary, key, 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):
 def make_hashable(value):
@@ -19,7 +19,7 @@ def make_hashable(value):
     try:
     try:
         hash(value)
         hash(value)
     except TypeError:
     except TypeError:
-        if is_iterable(value):
+        if isinstance(value, Iterable):
             return tuple(map(make_hashable, value))
             return tuple(map(make_hashable, value))
         # Non-hashable, non-iterable.
         # Non-hashable, non-iterable.
         raise
         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):
 def is_iterable(x):
     "An implementation independent way of checking for iterables"
     "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:
     try:
         iter(x)
         iter(x)
     except TypeError:
     except TypeError:

+ 3 - 0
docs/internals/deprecation.txt

@@ -59,6 +59,9 @@ details on these changes.
 * The ``ModelAdmin.log_deletion()`` and ``LogEntryManager.log_action()``
 * The ``ModelAdmin.log_deletion()`` and ``LogEntryManager.log_action()``
   methods will be removed.
   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:
 .. _deprecation-removed-in-5.1:
 
 
 5.1
 5.1

+ 4 - 0
docs/releases/5.1.txt

@@ -303,6 +303,10 @@ Miscellaneous
   ``ModelAdmin.log_deletions()`` and  ``LogEntryManager.log_actions()``
   ``ModelAdmin.log_deletions()`` and  ``LogEntryManager.log_actions()``
   instead.
   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
 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([])