[Django] #35046: BlankChoiceIterator causes AttributeError for some existing packages and projects

36 views
Skip to first unread message

Django

unread,
Dec 17, 2023, 6:51:13 PM12/17/23
to django-...@googlegroups.com
#35046: BlankChoiceIterator causes AttributeError for some existing packages and
projects
--------------------------------------+------------------------
Reporter: hazho | Owner: nobody
Type: Bug | Status: new
Component: Utilities | Version: 5.0
Severity: Normal | Keywords:
Triage Stage: Unreviewed | Has patch: 0
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 1
UI/UX: 0 |
--------------------------------------+------------------------
The iterators should have method __len__ ...!
while this is not the case for (BlankChoiceIterator) in the current
version of Django (5.0), this is why the following error raised:
AttributeError: 'BlankChoiceIterator' object has no attribute '__len__'.
Did you mean: '__le__'?


to solve this, simply the BlankChoiceIterator class should have the method
__len__ returning 0 as indication for emptiness.

this is important because the package maintainer or project author may not
find the way to update their code accordingly, for example the projects
depend on django-countries would have the following trace raised (note
there is no obvious indication where in the project code is the main
causer that calls __len__ of BlankChoiceIterator objects:


{{{

Traceback (most recent call last):
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\core\handlers\exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\core\handlers\base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args,
**callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\django_simple_payment_system\wallets\views.py", line 38, in
index
return render(request, template_path, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\shortcuts.py", line 24, in render
content = loader.render_to_string(template_name, context, request,
using=using)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\loader.py", line 62, in render_to_string
return template.render(context, request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\backends\django.py", line 61, in render
return self.template.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 171, in render
return self._render(context)
^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 163, in _render
return self.nodelist.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 1000, in render
return SafeString("".join([node.render_annotated(context) for node in
self]))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 961, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\loader_tags.py", line 210, in render
return template.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 173, in render
return self._render(context)
^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 163, in _render
return self.nodelist.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 1000, in render
return SafeString("".join([node.render_annotated(context) for node in
self]))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 961, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\defaulttags.py", line 241, in render
nodelist.append(node.render_annotated(context))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 961, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 1065, in render
return render_value_in_context(output, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\template\base.py", line 1042, in render_value_in_context
value = str(value)
^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\forms\utils.py", line 79, in __str__
return self.as_widget()
^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\forms\boundfield.py", line 95, in as_widget
attrs = self.build_widget_attrs(attrs, widget)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\forms\boundfield.py", line 270, in build_widget_attrs
widget.use_required_attribute(self.initial)
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\forms\widgets.py", line 781, in use_required_attribute
first_choice = next(iter(self.choices), None)
^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django_countries\widgets.py", line 29, in get_choices
self._choices: ChoiceList = list(self._choices)
^^^^^^^^^^^^^^^^^^^
File "C:\env_django_simple_payment_system\Lib\site-
packages\django\utils\functional.py", line 188, in __wrapper__
return getattr(result, __method_name)(*args, **kw)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'BlankChoiceIterator' object has no attribute '__len__'.
Did you mean: '__le__'?
}}}


the solution can be as the following:

{{{

# django/utils/choices.py
class BlankChoiceIterator(BaseChoiceIterator):
"""Iterator to lazily inject a blank choice."""
# existing code

def __len__(self):
return 0
}}}

--
Ticket URL: <https://code.djangoproject.com/ticket/35046>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Dec 17, 2023, 6:52:34 PM12/17/23
to django-...@googlegroups.com
#35046: BlankChoiceIterator causes AttributeError for some existing packages and
projects
---------------------------+--------------------------------------

Reporter: hazho | Owner: nobody
Type: Bug | Status: new
Component: Utilities | Version: 5.0
Severity: Normal | Resolution:

Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
---------------------------+--------------------------------------
Changes (by hazho):

* cc: hazho (added)


--
Ticket URL: <https://code.djangoproject.com/ticket/35046#comment:1>

Django

unread,
Dec 17, 2023, 11:47:28 PM12/17/23
to django-...@googlegroups.com
#35046: BlankChoiceIterator causes AttributeError for some existing packages and
projects
-----------------------------+--------------------------------------
Reporter: Hazho Human | Owner: nobody
Type: Bug | Status: closed
Component: Utilities | Version: 5.0
Severity: Normal | Resolution: invalid

Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------+--------------------------------------
Changes (by Mariusz Felisiak):

* cc: Nick Pope (added)
* status: new => closed
* resolution: => invalid
* easy: 1 => 0


Comment:

Replying to [ticket:35046 Hazho Human]:


> The iterators should have method __len__ ...!

That's not true, and there is no need to shout. According to
[https://docs.python.org/3/glossary.html#term-iterator the Python's
documentation]: ''"Iterators are required to have an `__iter__()`
method...".'' and that's it so the `BlankChoiceIterator` implementation is
correct. Also, `django-countries` doesn't support Django 4.2+, but there
are efforts to change this, check out [https://github.com/SmileyChris
/django-countries/pull/424 PR424] and [https://github.com/SmileyChris
/django-countries/pull/438 PR 438].

--
Ticket URL: <https://code.djangoproject.com/ticket/35046#comment:2>

Django

unread,
Dec 18, 2023, 3:12:51 AM12/18/23
to django-...@googlegroups.com
#35046: BlankChoiceIterator causes AttributeError for some existing packages and
projects
-----------------------------+--------------------------------------
Reporter: Hazho Human | Owner: nobody
Type: Bug | Status: closed
Component: Utilities | Version: 5.0
Severity: Normal | Resolution: invalid
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------+--------------------------------------

Comment (by Nick Pope):

Agreed. Also, returning `0` is incorrect and would likely break things
somewhere else. The true length would be `n+0` or `n+1` depending on the
length of the wrapped iterable and whether it already contains a blank
value. The whole purpose of this is to keep it lazy for certain use cases,
e.g. callable support, so it makes no sense to consume everything early to
determine a length.

The good news is that, from looking at the later pull request above, the
new functionality in Django seems to satisfy the needs of `django-
countries` w.r.t. making choices lazy for which it had to implement its
own solution based on undocumented/internal behaviours. Some problems also
arose in `django-filters`, but these were caught early and fixed. In the
long term, with something now standardised in core this will be more
robust going forward. (Many potential bugs internally were also ironed out
by normalizing consistently - lots of things around choices had been
bolted on over time.)

--
Ticket URL: <https://code.djangoproject.com/ticket/35046#comment:3>

Django

unread,
Dec 25, 2023, 12:43:36 PM12/25/23
to django-...@googlegroups.com
#35046: BlankChoiceIterator causes AttributeError for some existing packages and
projects
-----------------------------+--------------------------------------
Reporter: Hazho Human | Owner: nobody
Type: Bug | Status: closed
Component: Utilities | Version: 5.0
Severity: Normal | Resolution: invalid
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------+--------------------------------------

Comment (by Yury V. Zaytsev):

Is there any workaround we could use until `django-countries` is finally
updated to support Django 5? I've got the admin pages to work again with
the following monkey patch in `settings.py`, but I haven't tested it, and
didn't look deep enough into the issue to rate how disgusting it is on a
scale from 1 to 10:

{{{
from django_countries.widgets import LazyChoicesMixin

LazyChoicesMixin.get_choices = lambda self: self._choices
LazyChoicesMixin.choices = property(LazyChoicesMixin.get_choices,
LazyChoicesMixin.set_choices)
}}}

--
Ticket URL: <https://code.djangoproject.com/ticket/35046#comment:4>

Django

unread,
Dec 28, 2023, 10:16:59 AM12/28/23
to django-...@googlegroups.com
#35046: BlankChoiceIterator causes AttributeError for some existing packages and
projects
-----------------------------+--------------------------------------
Reporter: Hazho Human | Owner: nobody
Type: Bug | Status: closed
Component: Utilities | Version: 5.0
Severity: Normal | Resolution: invalid
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------+--------------------------------------

Comment (by Matthew Pava):

The monkey patch is the workaround until the update is released. It
appears that they have already fixed the issue; they just haven't released
it. You can check their issue tracker here: https://github.com/SmileyChris
/django-countries/issues/447

Replying to [comment:4 Yury V. Zaytsev]:


> Is there any workaround we could use until `django-countries` is finally
updated to support Django 5? I've got the admin pages to work again with
the following monkey patch in `settings.py`, but I haven't tested it, and
didn't look deep enough into the issue to rate how disgusting it is on a
scale from 1 to 10:
>
> {{{
> from django_countries.widgets import LazyChoicesMixin
>
> LazyChoicesMixin.get_choices = lambda self: self._choices
> LazyChoicesMixin.choices = property(LazyChoicesMixin.get_choices,
LazyChoicesMixin.set_choices)
> }}}

--
Ticket URL: <https://code.djangoproject.com/ticket/35046#comment:5>

Django

unread,
Jun 6, 2024, 2:41:34 PM6/6/24
to django-...@googlegroups.com
#35046: BlankChoiceIterator causes AttributeError for some existing packages and
projects
-----------------------------+--------------------------------------
Reporter: Hazho Human | Owner: nobody
Type: Bug | Status: closed
Component: Utilities | Version: 5.0
Severity: Normal | Resolution: invalid
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-----------------------------+--------------------------------------
Changes (by Dmytro Litvinov):

* cc: Dmytro Litvinov (added)

--
Ticket URL: <https://code.djangoproject.com/ticket/35046#comment:6>
Reply all
Reply to author
Forward
0 new messages