Skip to content

Checking for __iter__ not valid way to check is iterable #1569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

NyanHelsing
Copy link

Some objects have an __iter__ method even though they are not iterable - SimpleLazyObject for ex.

Hey, thanks for contributing to Haystack. Please review the contributor guidelines and confirm that the tests pass with at least one search engine.

Once your pull request has been submitted, the full test suite will be executed on https://travis-ci.org/django-haystack/django-haystack/pull_requests. Pull requests with passing tests are far more likely to be reviewed and merged.

Some objects have an `__iter__` method even though they are not iterable - `SimpleLazyObject` for ex.
@@ -153,7 +153,9 @@ def get_iterable_objects(cls, current_objects):
return current_objects.all()
return []

elif not hasattr(current_objects, '__iter__'):
try:
current_objects = [obj for obj in current_objects]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any example of iterable without __iter__ method? All basic collections, generators and iterators have it.
I only noticed that 'string' object has no __iter__ in python2 (it is present in python3) but string is an atomic value in terms of fields. Also, checking for __iter__ is quite common practice to check for iteration ability.

>>> list().__iter__
<method-wrapper '__iter__' of list object at 0x7fb27115f908>
>>> set().__iter__
<method-wrapper '__iter__' of set object at 0x7fb271185d00>
>>> dict().__iter__
<method-wrapper '__iter__' of dict object at 0x7fb27116bb40>
>>> tuple().__iter__
<method-wrapper '__iter__' of tuple object at 0x7fb27126d050>
>>> (xrange(10)).__iter__
<method-wrapper '__iter__' of xrange object at 0x7fb271166648>
>>> (xrange(10)).__iter__().__iter__()
<rangeiterator object at 0x7fb271169480>
>>> def gen():
...     for x in xrange(10): yield x
... 
>>> gen().__iter__()
<generator object gen at 0x7fb271155820>
>>> gen().__iter__().__iter__()
<generator object gen at 0x7fb271155a50>

If there is any common case of such a iterable without __iter__, I think that should be shown in test.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue was uncovered a lttle while ago - I can't remember the object that was being given here - if I recall it was a proxy object to a queryset or something to that effect - the proxy itself does not have an iter method, but when passed to a comprehension it behaves as one would expect.

In addition, the above change makes the logic here more pythonic as its a common idiom in python to ask for forgiveness not permission. https://docs.python.org/3.6/glossary.html#term-eafp

@ivan-klass
Copy link

Failing test:
SearchFieldTestCase.test_get_iterable_objects_with_list_stays_the_same in test_haystack.test_fields

@NyanHelsing
Copy link
Author

NyanHelsing commented Mar 19, 2018

Oh I wrote which object in that original comment - looks like it was a SimpleLazyObject.
That fix isn't really ideal- makes a whole list. I'm trying to think of something a bit better.

Not tested:

try:
    for item in items:
        break
except:
    items = [items]

Heres a link to where iter is defined on that object - but the object it's wrapping is not iterable.
https://github.com/django/django/blob/master/django/utils/functional.py#L314

@@ -153,7 +153,9 @@ def get_iterable_objects(cls, current_objects):
return current_objects.all()
return []

elif not hasattr(current_objects, '__iter__'):
try:
_ = [obj for obj in current_objects]
Copy link

@ivan-klass ivan-klass Mar 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will consume all the possible generators:

class Test(object):
    @property
    def test(self):
        for x in '12345': yield x

current_objects = Test().test
_ = [c for c in current_objects]
print ', '.join(current_objects)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — I think the way to handle this would need to be something like this:

try:
    iter(current_objects)
except TypeError:
    current_objects = [current_objects]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't realized this PR already existed, but I used the iter(current_objects) approach suggested by @acdha for PR #1663, back in 2019.

@@ -153,7 +153,9 @@ def get_iterable_objects(cls, current_objects):
return current_objects.all()
return []

elif not hasattr(current_objects, '__iter__'):
try:
_ = [obj for obj in current_objects]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — I think the way to handle this would need to be something like this:

try:
    iter(current_objects)
except TypeError:
    current_objects = [current_objects]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy