#35046 closed Bug (invalid)
BlankChoiceIterator causes AttributeError for some existing packages and projects
| Reported by: | Hazho Human | Owned by: | nobody |
|---|---|---|---|
| Component: | Utilities | Version: | 5.0 |
| Severity: | Normal | Keywords: | |
| Cc: | Hazho Human, Nick Pope, Dmytro Litvinov | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
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
Change History (6)
comment:1 by , 23 months ago
| Cc: | added |
|---|
comment:2 by , 23 months ago
| Cc: | added |
|---|---|
| Easy pickings: | unset |
| Resolution: | → invalid |
| Status: | new → closed |
comment:3 by , 23 months ago
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.)
follow-up: 5 comment:4 by , 23 months ago
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)
comment:5 by , 23 months ago
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 Yury V. Zaytsev:
Is there any workaround we could use until
django-countriesis finally updated to support Django 5? I've got the admin pages to work again with the following monkey patch insettings.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)
comment:6 by , 17 months ago
| Cc: | added |
|---|
Replying to Hazho Human:
That's not true, and there is no need to shout. According to the Python's documentation: "Iterators are required to have an
__iter__()method...". and that's it so theBlankChoiceIteratorimplementation is correct. Also,django-countriesdoesn't support Django 4.2+, but there are efforts to change this, check out PR424 and PR 438.