Add back list() when iterating over d.items() in a for statement
Fix #176.
This reverts commit 8a1ef249bf6b75393df60cfcf9615eca915f592e from https://github.com/PyCQA/modernize/pull/120. That commit’s commit message says, “For some reason, 2to3 only treats a for loop as a special context for iter* methods, so it will add a list() call to e.g. for x in d.values().”
There is a good reason that 2to3’s fix_dict considers for statements and for comprehensions a special context (where list() is unnecessary) for iter dict methods but not list dict methods. fix_dict is conservative in omitting list() in d.keys() --> list(d.keys()) and iter() in d.iterkeys() --> iter(d.keys()). For the list functions (python2’s d.keys(), d.items(), d.values()), the list can be safely omitted if the result is consumed immediately by a function (e.g. sorted). For the iterator functions (python2’s d.iterkeys(), d.iteritems(), d.itervalues()), the iter can be safely omitted from the view if the result is converted to an iterator or consumed immediately by a function or by a for statement or for comprehension.
It is often incorrect to omit the list() when converting for x in d.items() to python3 because if you omit list(), then adding or removing elements while iterating will raise “RuntimeError: dictionary changed size during iteration”. Example:
# this is a valid python2 script, but in python3 we must wrap d.keys() in list
d = {'body': '<html>'}
for x in d.keys():
d['statusCode'] = 200 # raises RuntimeError: dictionary changed size during iteration
for x in d.keys():
del d[x] # raises RuntimeError: dictionary changed size during iteration
Furthermore, the overridden in_special_context that considers keys() to be the same as iterkeys() is incorrect because the super method implementation considers iter to be a special context (where adding iter() is not needed) when isiter==True. So iter(d.keys()) should be converted to iter(list(d.keys())), not left unchanged as currently happens.