Opened 11 months ago

Closed 11 months ago

Last modified 6 months ago

#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 Hazho Human, 11 months ago

Cc: Hazho Human added

in reply to:  description comment:2 by Mariusz Felisiak, 11 months ago

Cc: Nick Pope added
Easy pickings: unset
Resolution: invalid
Status: newclosed

Replying to Hazho Human:

The iterators should have method len ...!

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 the BlankChoiceIterator 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.

comment:3 by Nick Pope, 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.)

comment:4 by Yury V. Zaytsev, 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)

in reply to:  4 comment:5 by Matthew Pava, 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 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:6 by Dmytro Litvinov, 6 months ago

Cc: Dmytro Litvinov added
Note: See TracTickets for help on using tickets.
Back to Top