From 5e28cd3f2cfc31bf947a747256bc036f8f64888a Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Fri, 24 Nov 2023 11:06:29 +0000 Subject: [PATCH] Fixed #34983 -- Deprecated django.utils.itercompat.is_iterable(). --- django/contrib/auth/models.py | 7 ++++--- django/core/checks/registry.py | 4 ++-- django/db/models/fields/__init__.py | 6 +++--- django/template/defaulttags.py | 4 ++-- django/template/library.py | 6 ++++-- django/test/client.py | 4 ++-- django/utils/hashable.py | 4 ++-- django/utils/itercompat.py | 13 +++++++++++++ docs/internals/deprecation.txt | 3 +++ docs/releases/5.1.txt | 4 ++++ tests/utils_tests/test_itercompat.py | 15 +++++++++++++++ 11 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 tests/utils_tests/test_itercompat.py diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 85330e2c0e6f..e5ef1bb52381 100644 --- a/django/contrib/auth/models.py +++ b/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.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 @@ def has_perms(self, perm_list, obj=None): 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 @@ def has_perm(self, perm, obj=None): 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) diff --git a/django/core/checks/registry.py b/django/core/checks/registry.py index f4bdea8691fc..146b28f65ead 100644 --- a/django/core/checks/registry.py +++ b/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 @@ def run_checks( 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, diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 67e8ddc98650..41735d3b7f18 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -5,6 +5,7 @@ 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.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 @@ def _check_field_name(self): @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 " diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 188bdf8c05ab..40c2917f561e 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -3,6 +3,7 @@ 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 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 diff --git a/django/template/library.py b/django/template/library.py index 16db79e4cd88..4ee96cea893f 100644 --- a/django/template/library.py +++ b/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 @@ def render(self, context): 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) diff --git a/django/test/client.py b/django/test/client.py index 48e058870283..d1fd428ea8cc 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -2,6 +2,7 @@ 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.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 is_file(thing): ) 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)) diff --git a/django/utils/hashable.py b/django/utils/hashable.py index 042e1a4373b7..323dfe74e723 100644 --- a/django/utils/hashable.py +++ b/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 diff --git a/django/utils/itercompat.py b/django/utils/itercompat.py index 9895e3f81657..e4b34cd5342b 100644 --- a/django/utils/itercompat.py +++ b/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: diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index a2968ab625f3..dd6712e936c8 100644 --- a/docs/internals/deprecation.txt +++ b/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 diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index dc48321f5cc8..4c95e4677f68 100644 --- a/docs/releases/5.1.txt +++ b/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 ======================= diff --git a/tests/utils_tests/test_itercompat.py b/tests/utils_tests/test_itercompat.py new file mode 100644 index 000000000000..e6ea278ab402 --- /dev/null +++ b/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([])