#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 , 11 months ago
Cc: | added |
---|
comment:2 by , 11 months ago
Cc: | added |
---|---|
Easy pickings: | unset |
Resolution: | → invalid |
Status: | new → closed |
comment:3 by , 11 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 , 11 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 , 11 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-countries
is 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 , 5 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 theBlankChoiceIterator
implementation is correct. Also,django-countries
doesn't support Django 4.2+, but there are efforts to change this, check out PR424 and PR 438.