Opened 10 years ago
Closed 10 years ago
#23555 closed Bug (fixed)
QuerySet.first suppresses internal IndexError exceptions
Reported by: | Artem Rizhov | Owned by: | nobody |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | dev |
Severity: | Normal | Keywords: | QuerySet first IndexError |
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
Current implementation of this method relies on IndexError
when checking the item existence. However this exception may be raised somewhere inside of own __getitem__()
which does a lot of work. This way the potential errors are hidden. The method will return None
in case of a bug that raises IndexError
internally. The Zen of Python says "Errors should never pass silently."
Change History (8)
comment:1 by , 10 years ago
Has patch: | set |
---|
comment:2 by , 10 years ago
Created second pull request with alternate solution for those who prefer to use try/catch - https://github.com/django/django/pull/3277
comment:3 by , 10 years ago
I don't think there is a problem with the current implementation. There is no reason why QuerySet.__getitem__()
shouldn't raise an IndexError
as it aims to provide a similar interface to a list.
Note that this is a common thing in Python to raise such an IndexError in similar situation, see those examples in cPython itself :
https://github.com/python/cpython/blob/c7688b44387d116522ff53c0927169db45969f0e/Lib/xml/dom/pulldom.py#L219
https://github.com/python/cpython/blob/c7688b44387d116522ff53c0927169db45969f0e/Lib/ipaddress.py#L666
comment:4 by , 10 years ago
Sorry for my English, my explanation is probably not too clear. But if you see the pull request and the test that I've added to the branch, you probably will understand the problem. There is also a lot of comments on the test case code.
Let me try to explain one more time.
You are right. There is no reason why __getitem__
shouldn't raise an IndexError
. This is correct and normal behaviour. I'm talking about another case.
IndexError
from QuerySet.__getitem__
is not always what you think, because it may be produced by nested calls, not by this method itself.
Let's remember that QuerySet is a lazy request to DB. So when you write qs[i]
, it makes actual query to the DB, retrieves a list of items and returns i-th element. IndexError
maybe raised by some another method that is called from __getitem__
, especially by __iter__
and any nested call. For example by the iterator implementation, or by database adaptor. For example if internal list of db connections is empty, but the adaptor tries to get one from the list. Such abnormal situation should not be treated as "no such element in the queryset". IndexError
in nested calls may mean anything, but probably not what you mean. If any nested call produces IndexError
, the caller (_getitem__
, __iter__
and another nested methods) is responsible to handle this right way, because it is aware of the source of error. Your outer code can't handle such error just because it's not aware of the source.
Hope this is clear now. If you are in doubt, please review the test in the pull request.
So what is the problem?
You can use try/except if you access i-th element of a list. Because you are sure IndexError
means "no such element" in this certain list. But you should not rely on try/except for IndexError
when accessing i-th element of QuerySet. You should not. But the current implementation of first()
method does this. It uses "standard" try/except in the case where it is not safe:
try: return qs[0] except IndexError: return None
This would work if qs
as plain list
. But qs
is QuerySet
with all it's magic. It's __getitem__
is not simple and rely on successful execution of a lot of nested calls. If any of those calls produces unexpected exception (either IndexError
or another one), this should not be treated as "empty query set", and should not be suppressed. The outer code should receive this exception as a singal that something goes wrong. But first()
method suppresses such errors.
Hope you understand the problem now. If no then please review the pull request, especially my tests, and the comments on the PR.
Thanks for the attention.
comment:5 by , 10 years ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:6 by , 10 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
See the PR for my reservation about adding a test for this (I perceive it to be mostly a code cleanup). But I'll let the committer make a decision.
comment:7 by , 10 years ago
If a code cleanup has a specific purpose other than readability/style, it's worth capturing that purpose in a test if possible, to help prevent regression. Same reason we'd add a test for any other code change.
comment:8 by , 10 years ago
Resolution: | → fixed |
---|---|
Status: | new → closed |
A fix is available from branch ticket_23555 of my github repo - https://github.com/artemrizhov/django/tree/ticket_23555
Also created a pull request - https://github.com/django/django/pull/3274