#21840 closed Bug (fixed)
LazyObject wrapped instances can no longer be coerced to Bool due to #20924
Reported by: | Gabe Jackson | Owned by: | Baptiste Mispelon |
---|---|---|---|
Component: | Utilities | Version: | dev |
Severity: | Release blocker | Keywords: | LazyObject utils |
Cc: | Triage Stage: | Ready for checkin | |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
https://code.djangoproject.com/ticket/20924 adds proxy methods for __contains__
and __len__
to support lists and dicts wrapped by LazyObject. The problem is that evaluating truth for such LazyObjects will break if __len__
is not defined on the wrapper instance.
This can be demonstrated as follows:
>>> from django.core.files.storage import default_storage >>> if default_storage: print('yes') ... Traceback (most recent call last): File "<console>", line 1, in <module> File "/home/tisba/Programming/testbed/boogz/django/utils/functional.py", line 224, in inner return func(self._wrapped, *args) TypeError: object of type 'FileSystemStorage' has no len()
this is because converting an object to a Bool calls object.__nonzero__
which in turn calls __len__
as stated here:
http://docs.python.org/2/reference/datamodel.html#object.__nonzero
This will break a lot of apps using LazyObject. This problem exists in the current master of imagekit as well:
class JustInTime(object): """ A strategy that ensures the file exists right before it's needed. """ def on_existence_required(self, file): file.generate() def on_content_required(self, file): file.generate() class StrategyWrapper(LazyObject): def __init__(self, strategy): if isinstance(strategy, six.string_types): strategy = get_singleton(strategy, 'cache file strategy') elif isinstance(strategy, dict): strategy = DictStrategy(strategy) elif callable(strategy): strategy = strategy() self._wrapped = strategy def __getstate__(self): return {'_wrapped': self._wrapped} def __setstate__(self, state): self._wrapped = state['_wrapped'] def __unicode__(self): return force_text(self._wrapped) def __str__(self): return str(self._wrapped)
calling the LazyObject in a statement like if lazy_object
will yield:
In [2]: p.avatar_thumbnail --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-2-860e30762713> in <module>() ----> 1 p.avatar_thumbnail /Users/gabejackson/venv/hvad-test/lib/python2.7/site-packages/imagekit/models/fields/utils.pyc in __get__(self, instance, owner) 15 spec = self.field.get_spec(source=source) 16 import ipdb; ipdb.set_trace() ---> 17 file = ImageCacheFile(spec) 18 instance.__dict__[self.attname] = file 19 return file /Users/gabejackson/venv/hvad-test/lib/python2.7/site-packages/imagekit/cachefiles/__init__.pyc in __init__(self, generator, name, storage, cachefile_backend, cachefile_strategy) 55 self.cachefile_strategy = ( 56 cachefile_strategy ---> 57 or getattr(generator, 'cachefile_strategy', None) 58 or get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY, 'cache file strategy') 59 ) /Users/gabejackson/venv/hvad-test/lib/python2.7/site-packages/django/utils/functional.pyc in inner(self, *args) 222 if self._wrapped is empty: 223 self._setup() --> 224 return func(self._wrapped, *args) 225 return inner 226 TypeError: object of type 'JustInTime' has no len()
You can see that the or
statement on line 57 is forcing the coercion of the JustInTime object to a Boolean. Doing this calls __len__
which is now defined on LazyObject and will cause the TypeError stated.
Change History (12)
comment:1 by , 11 years ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:3 by , 11 years ago
further notes:
It appears many of the proxy methods should be on LazyObject instead of on SimpleLazyObject because the latter inherits from the LazyObject.
SimpleLazyObject was introduced here: https://github.com/django/django/commit/a2d8acbacda353248fd191b97155cd4cf27dcd66
Proxies were added here: https://github.com/django/django/commit/60cf3f2f842b6e56132b8880c70acc87bd753c2e
a simple solution seems to just add: __nonzero__ = new_method_proxy(bool)
to LazyObject.
I'm not familiar with these undocumented classes, but wouldn't it be possible to move all the proxying to LazyObject?
comment:4 by , 11 years ago
I left a note on the commit, asking the authors for ideas: https://github.com/django/django/commit/aa01c99f5587b36c354e80c682ad52e1a3b41455#commitcomment-5180010
comment:5 by , 11 years ago
Has patch: | set |
---|
Pull request here: https://github.com/django/django/pull/2221
As far as I can tell, there's no reason for all the dunder methods to exist on SimpleLazyObject
instead of LazyObject
and moving them all to the parent class fixes this particular issue.
I also added a regression test and the full test suite passes (on sqlite at least).
comment:6 by , 11 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
comment:7 by , 11 years ago
Triage Stage: | Ready for checkin → Accepted |
---|
On discussion with bmispelon - we would like to add more tests for this before committing. That said, it can be merged as is if not landed before beta.
comment:8 by , 11 years ago
Needs tests: | set |
---|---|
Owner: | changed from | to
Patch needs improvement: | set |
Status: | new → assigned |
The current PR is broken (which makes me glad I decided to hold off and write some tests) since not all dunder methods can be moved as-is (repr
being one of them for example).
comment:9 by , 11 years ago
Needs tests: | unset |
---|---|
Patch needs improvement: | unset |
Updated the PR. It should be ready for final review now: https://github.com/django/django/pull/2221/
comment:10 by , 11 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
Reviewed by Shai and myself.
comment:11 by , 11 years ago
Resolution: | → fixed |
---|---|
Status: | assigned → closed |
Just to comment why this worked before:
"If a class defines neither len() nor nonzero(), all its instances are considered true."
that is from http://docs.python.org/2/reference/datamodel.html#object.__nonzero