Skip to content

Commit 30483db

Browse files
authored
Merge pull request #2146 from RasmusWL/python-improve-iter-returns-non-iterator
Python: improve py/iter-returns-non-iterator
2 parents 41969a3 + 5c5eaac commit 30483db

File tree

10 files changed

+101
-58
lines changed

10 files changed

+101
-58
lines changed

change-notes/1.23/analysis-python.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919
| **Query** | **Expected impact** | **Change** |
2020
|----------------------------|------------------------|------------|
2121
| Unreachable code | Fewer false positives | Analysis now accounts for uses of `contextlib.suppress` to suppress exceptions. |
22-
22+
| `__iter__` method returns a non-iterator | Better alert message | Alert now highlights which class is expected to be an iterator. |
Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
class MyRange(object):
22
def __init__(self, low, high):
3-
self.current = low
3+
self.low = low
44
self.high = high
55

66
def __iter__(self):
7-
return self
7+
return MyRangeIterator(self.low, self.high)
88

9-
#Fixed version
10-
class MyRange(object):
11-
def __init__(self, low, high):
9+
def skip(self, to_skip):
10+
return MyRangeIterator(self.low, self.high, to_skip)
11+
12+
class MyRangeIterator(object):
13+
def __init__(self, low, high, skip=None):
1214
self.current = low
1315
self.high = high
16+
self.skip = skip
1417

15-
def __iter__(self):
16-
while self.current < self.high:
17-
yield self.current
18-
self.current += 1
18+
def __next__(self):
19+
if self.current >= self.high:
20+
raise StopIteration
21+
to_return = self.current
22+
self.current += 1
23+
if self.skip and to_return in self.skip:
24+
return self.__next__()
25+
return to_return
26+
27+
# Problem is fixed by uncommenting these lines
28+
# def __iter__(self):
29+
# return self
30+
31+
my_range = MyRange(0,10)
32+
x = sum(my_range) # x = 45
33+
y = sum(my_range.skip({6,9})) # TypeError: 'MyRangeIterator' object is not iterable

python/ql/src/Functions/IterReturnsNonIterator.qhelp

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,33 @@
33
"qhelp.dtd">
44
<qhelp>
55
<overview>
6-
<p>The <code>__iter__</code> method of a class should return an iterator.
6+
<p>The <code>__iter__</code> method of a class should always return an iterator.</p>
77

8-
Iteration in Python relies on this behavior and attempting to iterate over an
9-
instance of a class with an incorrect <code>__iter__</code> method will raise a TypeError.
10-
</p>
8+
<p>Iterators must implement both <code>__next__</code> and <code>__iter__</code> for Python 3, or both <code>next</code> and <code>__iter__</code> for Python 2. The <code>__iter__</code> method of the iterator must return the iterator object itself.</p>
119

10+
<p>Iteration in Python relies on this behavior and attempting to iterate over an instance of a class with an incorrect <code>__iter__</code> method can raise a <code>TypeError</code>.
11+
</p>
1212

1313
</overview>
1414
<recommendation>
15-
<p>Make the <code>__iter__</code> return a new iterator, either as an instance of
16-
a separate class or as a generator.</p>
17-
15+
<p>Make sure the value returned by <code>__iter__</code> implements the full iterator protocol.</p>
1816
</recommendation>
1917
<example>
20-
<p>In this example the <code>MyRange</code> class's <code>__iter__</code> method does not
21-
return an iterator. This will cause the program to fail when anyone attempts
22-
to use the iterator in a <code>for</code> loop or <code>in</code> statement.
23-
</p>
18+
<p>In this example, we have implemented our own version of <code>range</code>, extending the normal functionality with the ability to skip some elements by using the <code>skip</code> method. However, the iterator <code>MyRangeIterator</code> does not fully implement the iterator protocol (namely it is missing <code>__iter__</code>).</p>
2419

25-
<p>The fixed version implements the <code>__iter__</code> method as a generator function.</p>
20+
<p>Iterating over the elements in the range seems to work on the surface, for example the code <code>x = sum(my_range)</code> gives the expected result. However, if we run <code>sum(iter(my_range))</code> we get a <code>TypeError: 'MyRangeIterator' object is not iterable</code>.</p>
21+
22+
<p>If we try to skip some elements using our custom method, for example <code>y = sum(my_range.skip({6,9}))</code>, this also raises a <code>TypeError</code>.</p>
23+
24+
<p>The fix is to implement the <code>__iter__</code> method in <code>MyRangeIterator</code>.</p>
2625

2726
<sample src="IterReturnsNonIterator.py" />
2827

2928
</example>
3029
<references>
3130

32-
<li>Python Language Reference: <a href="http://docs.python.org/2.7/reference/datamodel.html#object.__iter__">object.__iter__</a>.</li>
33-
<li>Python Standard Library: <a href="http://docs.python.org/2/library/stdtypes.html#typeiter">Iterator Types</a>.</li>
34-
31+
<li>Python Language Reference: <a href="https://docs.python.org/3/reference/datamodel.html#object.__iter__">object.__iter__</a>.</li>
32+
<li>Python Standard Library: <a href="https://docs.python.org/3/library/stdtypes.html#typeiter">Iterator Types</a>.</li>
3533

3634
</references>
3735
</qhelp>

python/ql/src/Functions/IterReturnsNonIterator.ql

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,20 @@
1212

1313
import python
1414

15-
FunctionObject iter_method(ClassObject t) {
16-
result = t.lookupAttribute("__iter__")
17-
}
18-
19-
cached ClassObject return_type(FunctionObject f) {
15+
ClassObject return_type(FunctionObject f) {
2016
exists(ControlFlowNode n, Return ret |
21-
ret.getScope() = f.getFunction() and ret.getValue() = n.getNode() and
17+
ret.getScope() = f.getFunction() and
18+
ret.getValue() = n.getNode() and
2219
n.refersTo(_, result, _)
2320
)
2421
}
2522

26-
from ClassObject t, FunctionObject iter
27-
where exists(ClassObject ret_t | iter = iter_method(t) and
28-
ret_t = return_type(iter) and
29-
not ret_t.isIterator()
30-
)
31-
32-
select iter, "The '__iter__' method of iterable class $@ does not return an iterator.", t, t.getName()
23+
from ClassObject iterable, FunctionObject iter, ClassObject iterator
24+
where
25+
iter = iterable.lookupAttribute("__iter__") and
26+
iterator = return_type(iter) and
27+
not iterator.isIterator()
28+
select iterator,
29+
"Class " + iterator.getName() +
30+
" is returned as an iterator (by $@) but does not fully implement the iterator interface.",
31+
iter, iter.getName()
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
| protocols.py:66:5:66:33 | Function __getitem__ | Function always raises $@; raise LookupError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError |
2-
| protocols.py:69:5:69:26 | Function __getattr__ | Function always raises $@; raise AttributeError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError |
1+
| protocols.py:98:5:98:33 | Function __getitem__ | Function always raises $@; raise LookupError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError |
2+
| protocols.py:101:5:101:26 | Function __getattr__ | Function always raises $@; raise AttributeError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError |
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
| protocols.py:16:5:16:23 | Function __iter__ | The '__iter__' method of iterable class $@ does not return an iterator. | protocols.py:14:1:14:16 | class X | X |
1+
| file://:Compiled Code:0:0:0:0 | builtin-class object | Class object is returned as an iterator (by $@) but does not fully implement the iterator interface. | protocols.py:16:5:16:23 | Function __iter__ | __iter__ |
2+
| protocols.py:20:1:20:26 | class IteratorMissingNext | Class IteratorMissingNext is returned as an iterator (by $@) but does not fully implement the iterator interface. | protocols.py:22:5:22:23 | Function __iter__ | __iter__ |
3+
| protocols.py:20:1:20:26 | class IteratorMissingNext | Class IteratorMissingNext is returned as an iterator (by $@) but does not fully implement the iterator interface. | protocols.py:27:5:27:23 | Function __iter__ | __iter__ |
4+
| protocols.py:30:1:30:26 | class IteratorMissingIter | Class IteratorMissingIter is returned as an iterator (by $@) but does not fully implement the iterator interface. | protocols.py:40:5:40:23 | Function __iter__ | __iter__ |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
| protocols.py:22:1:22:29 | class AlmostIterator | Class AlmostIterator is an iterator but its $@ method does not return 'self'. | protocols.py:30:5:30:23 | Function __iter__ | __iter__ |
1+
| protocols.py:54:1:54:29 | class AlmostIterator | Class AlmostIterator is an iterator but its $@ method does not return 'self'. | protocols.py:62:5:62:23 | Function __iter__ | __iter__ |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
| protocols.py:42:5:42:22 | Function __del__ | Overly complex '__del__' method. |
1+
| protocols.py:74:5:74:22 | Function __del__ | Overly complex '__del__' method. |

python/ql/test/query-tests/Functions/general/SignatureSpecialMethods.expected

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
| om_test.py:71:5:71:19 | Function __repr__ | Too few parameters for special method __repr__, which has no parameters, but should have 1, in class $@. | om_test.py:57:1:57:28 | class WrongSpecials | WrongSpecials |
66
| om_test.py:74:5:74:46 | Function __add__ | 1 default values(s) will never be used for special method __add__, in class $@. | om_test.py:57:1:57:28 | class WrongSpecials | WrongSpecials |
77
| om_test.py:97:15:97:34 | Function lambda | Too few parameters for special method __sub__, which has 1 parameter, but should have 2, in class $@. | om_test.py:95:1:95:28 | class NotOKSpecials | NotOKSpecials |
8-
| protocols.py:72:1:72:12 | Function f | Too few parameters for special method __add__, which has 1 parameter, but should have 2, in class $@. | protocols.py:75:1:75:29 | class MissingMethods | MissingMethods |
9-
| protocols.py:72:1:72:12 | Function f | Too few parameters for special method __set__, which has 1 parameter, but should have 3, in class $@. | protocols.py:75:1:75:29 | class MissingMethods | MissingMethods |
8+
| protocols.py:104:1:104:12 | Function f | Too few parameters for special method __add__, which has 1 parameter, but should have 2, in class $@. | protocols.py:107:1:107:29 | class MissingMethods | MissingMethods |
9+
| protocols.py:104:1:104:12 | Function f | Too few parameters for special method __set__, which has 1 parameter, but should have 3, in class $@. | protocols.py:107:1:107:29 | class MissingMethods | MissingMethods |

python/ql/test/query-tests/Functions/general/protocols.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,38 @@ def __iter__(self):
1717
return object()
1818

1919

20+
class IteratorMissingNext:
21+
22+
def __iter__(self):
23+
return self
24+
25+
class IterableMissingNext:
26+
27+
def __iter__(self):
28+
return IteratorMissingNext()
29+
30+
class IteratorMissingIter:
31+
32+
def next(self):
33+
pass
34+
35+
def __next__(self):
36+
pass
37+
38+
class IterableMissingIter:
39+
40+
def __iter__(self):
41+
return IteratorMissingIter()
42+
43+
class IterableWithGenerator:
44+
# returning a generator from __iter__ in an iterable is ok
45+
46+
def __iter__(self):
47+
i = 0
48+
while True:
49+
yield i
50+
i += 1
51+
2052
#Iterator not returning self
2153

2254
class AlmostIterator(object):
@@ -57,34 +89,30 @@ def close(self):
5789

5890
def __del__(self):
5991
self.close()
60-
92+
6193
class IncorrectSpecialMethods(object):
62-
94+
6395
def __add__(self, other):
6496
raise NotImplementedError()
65-
97+
6698
def __getitem__(self, index):
6799
raise ZeroDivisionError()
68-
100+
69101
def __getattr__(self):
70102
raise ZeroDivisionError()
71-
103+
72104
def f(self):
73105
pass
74-
106+
75107
class MissingMethods(object):
76-
108+
77109
__repr__ = f # This should be OK
78110
__add__ = f # But not this
79111
__set__ = f # or this
80-
112+
81113
#OK Special method
82114
class OK(object):
83-
115+
84116
def __call__(self):
85117
yield 0
86118
raise StopIteration
87-
88-
89-
90-

0 commit comments

Comments
 (0)