Skip to content

Commit e0022dc

Browse files
committed
Make futurize --stage1 leave next() method alone (issue #81)
1 parent dc895f7 commit e0022dc

File tree

5 files changed

+200
-4
lines changed

5 files changed

+200
-4
lines changed

docs/futurize.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ The complete set of fixers applied by ``futurize --stage1`` is::
131131
lib2to3.fixes.fix_isinstance
132132
lib2to3.fixes.fix_methodattrs
133133
lib2to3.fixes.fix_ne
134-
lib2to3.fixes.fix_next
134+
libfuturize.fixes.fix_next_call
135135
lib2to3.fixes.fix_numliterals
136136
lib2to3.fixes.fix_paren
137137
lib2to3.fixes.fix_reduce
@@ -161,6 +161,17 @@ this. The new fixer both makes implicit relative imports explicit and
161161
adds the declaration ``from __future__ import absolute_import`` at the top
162162
of each relevant module.
163163

164+
.. code-block:: python
165+
166+
lib2to3.fixes.fix_next
167+
168+
The ``fix_next_call`` fixer in ``libfuturize.fixes`` is applied instead of
169+
``fix_next`` in stage 1. The new fixer changes any ``obj.next()`` calls to
170+
``next(obj)``, which is Py2/3 compatible, but doesn't change any ``next`` method
171+
names to ``__next__``, which would break Py2 compatibility.
172+
173+
``fix_next`` is applied in stage 2.
174+
164175
.. code-block:: python
165176
166177
lib2to3.fixes.fix_print

docs/whatsnew.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ What's new in version 0.12.5
99

1010
- Use a private logger instead of the global logger in
1111
``future.standard_library`` (issue #82).
12+
- Stage 1 of ``futurize`` no longer renames ``next`` methods to ``__next__``
13+
(issue #81). It still converts ``obj.next()`` method calls to
14+
``next(obj)`` correctly.
1215

1316

1417
.. whats-new-0.12.4:

future/tests/test_futurize.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def test_oldstyle_classes_iterator(self):
156156
class Upper:
157157
def __init__(self, iterable):
158158
self._iter = iter(iterable)
159-
def next(self): # note the Py3 interface
159+
def next(self): # note the Py2 interface
160160
return next(self._iter).upper()
161161
def __iter__(self):
162162
return self
@@ -183,7 +183,7 @@ def __iter__(self):
183183
class Upper():
184184
def __init__(self, iterable):
185185
self._iter = iter(iterable)
186-
def next(self): # note the Py3 interface
186+
def next(self): # note the Py2 interface
187187
return next(self._iter).upper()
188188
def __iter__(self):
189189
return self
@@ -677,6 +677,79 @@ def f(a, b):
677677
'''
678678
self.convert_check(before, after, stages=[1])
679679

680+
def test_next_1(self):
681+
"""
682+
Custom next methods should not be converted to __next__ in stage1, but
683+
any obj.next() calls should be converted to next(obj).
684+
"""
685+
before = """
686+
class Upper:
687+
def __init__(self, iterable):
688+
self._iter = iter(iterable)
689+
def next(self): # note the Py2 interface
690+
return next(self._iter).upper()
691+
def __iter__(self):
692+
return self
693+
694+
itr = Upper('hello')
695+
assert itr.next() == 'H'
696+
assert next(itr) == 'E'
697+
assert list(itr) == list('LLO')
698+
"""
699+
700+
after = """
701+
class Upper:
702+
def __init__(self, iterable):
703+
self._iter = iter(iterable)
704+
def next(self): # note the Py2 interface
705+
return next(self._iter).upper()
706+
def __iter__(self):
707+
return self
708+
709+
itr = Upper('hello')
710+
assert next(itr) == 'H'
711+
assert next(itr) == 'E'
712+
assert list(itr) == list('LLO')
713+
"""
714+
self.convert_check(before, after, stages=[1])
715+
716+
@unittest.expectedFailure
717+
def test_next_2(self):
718+
"""
719+
This version of the above doesn't currently work: the self._iter.next() call in
720+
line 5 isn't converted to next(self._iter).
721+
"""
722+
before = """
723+
class Upper:
724+
def __init__(self, iterable):
725+
self._iter = iter(iterable)
726+
def next(self): # note the Py2 interface
727+
return self._iter.next().upper()
728+
def __iter__(self):
729+
return self
730+
731+
itr = Upper('hello')
732+
assert itr.next() == 'H'
733+
assert next(itr) == 'E'
734+
assert list(itr) == list('LLO')
735+
"""
736+
737+
after = """
738+
class Upper(object):
739+
def __init__(self, iterable):
740+
self._iter = iter(iterable)
741+
def next(self): # note the Py2 interface
742+
return next(self._iter).upper()
743+
def __iter__(self):
744+
return self
745+
746+
itr = Upper('hello')
747+
assert next(itr) == 'H'
748+
assert next(itr) == 'E'
749+
assert list(itr) == list('LLO')
750+
"""
751+
self.convert_check(before, after, stages=[1])
752+
680753
def test_xrange(self):
681754
"""
682755
xrange should not be changed by futurize --stage1

libfuturize/fixes/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
'lib2to3.fixes.fix_isinstance',
1818
'lib2to3.fixes.fix_methodattrs',
1919
'lib2to3.fixes.fix_ne',
20-
'lib2to3.fixes.fix_next',
20+
# 'lib2to3.fixes.fix_next', # would replace ``next`` method names
21+
# with ``__next__``.
2122
'lib2to3.fixes.fix_numliterals', # turns 1L into 1, 0755 into 0o755
2223
'lib2to3.fixes.fix_paren',
2324
# 'lib2to3.fixes.fix_print', # see the libfuturize fixer that also
@@ -57,6 +58,7 @@
5758
'lib2to3.fixes.fix_long',
5859
'lib2to3.fixes.fix_map',
5960
# 'lib2to3.fixes.fix_metaclass', # causes SyntaxError in Py2! Use the one from ``six`` instead
61+
'lib2to3.fixes.fix_next',
6062
'lib2to3.fixes.fix_nonzero', # TODO: cause this to import ``object`` and/or add a decorator for mapping __bool__ to __nonzero__
6163
'lib2to3.fixes.fix_operator', # we will need support for this by e.g. extending the Py2 operator module to provide those functions in Py3
6264
'lib2to3.fixes.fix_raw_input',
@@ -69,6 +71,9 @@
6971
libfuturize_fix_names_stage1 = set([
7072
'libfuturize.fixes.fix_absolute_import',
7173
'libfuturize.fixes.fix_division',
74+
'libfuturize.fixes.fix_next_call', # obj.next() -> next(obj). Unlike
75+
# lib2to3.fixes.fix_next, doesn't change
76+
# the ``next`` method to ``__next__``.
7277
'libfuturize.fixes.fix_print_with_import',
7378
'libfuturize.fixes.fix_raise',
7479
'libfuturize.fixes.fix_order___future__imports', # TODO: consolidate to a single line to simplify testing

libfuturize/fixes/fix_next_call.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
Based on fix_next.py by Collin Winter.
3+
4+
Replaces it.next() -> next(it), per PEP 3114.
5+
6+
Unlike fix_next.py, this fixer doesn't replace the name of a next method with __next__,
7+
which would break Python 2 compatibility without further help from fixers in
8+
stage 2.
9+
"""
10+
11+
# Local imports
12+
from lib2to3.pgen2 import token
13+
from lib2to3.pygram import python_symbols as syms
14+
from lib2to3 import fixer_base
15+
from lib2to3.fixer_util import Name, Call, find_binding
16+
17+
bind_warning = "Calls to builtin next() possibly shadowed by global binding"
18+
19+
20+
class FixNextCall(fixer_base.BaseFix):
21+
BM_compatible = True
22+
PATTERN = """
23+
power< base=any+ trailer< '.' attr='next' > trailer< '(' ')' > >
24+
|
25+
power< head=any+ trailer< '.' attr='next' > not trailer< '(' ')' > >
26+
|
27+
global=global_stmt< 'global' any* 'next' any* >
28+
"""
29+
30+
order = "pre" # Pre-order tree traversal
31+
32+
def start_tree(self, tree, filename):
33+
super(FixNextCall, self).start_tree(tree, filename)
34+
35+
n = find_binding('next', tree)
36+
if n:
37+
self.warning(n, bind_warning)
38+
self.shadowed_next = True
39+
else:
40+
self.shadowed_next = False
41+
42+
def transform(self, node, results):
43+
assert results
44+
45+
base = results.get("base")
46+
attr = results.get("attr")
47+
name = results.get("name")
48+
49+
if base:
50+
if self.shadowed_next:
51+
# Omit this:
52+
# attr.replace(Name("__next__", prefix=attr.prefix))
53+
pass
54+
else:
55+
base = [n.clone() for n in base]
56+
base[0].prefix = ""
57+
node.replace(Call(Name("next", prefix=node.prefix), base))
58+
elif name:
59+
# Omit this:
60+
# n = Name("__next__", prefix=name.prefix)
61+
# name.replace(n)
62+
pass
63+
elif attr:
64+
# We don't do this transformation if we're assigning to "x.next".
65+
# Unfortunately, it doesn't seem possible to do this in PATTERN,
66+
# so it's being done here.
67+
if is_assign_target(node):
68+
head = results["head"]
69+
if "".join([str(n) for n in head]).strip() == '__builtin__':
70+
self.warning(node, bind_warning)
71+
return
72+
# Omit this:
73+
# attr.replace(Name("__next__"))
74+
elif "global" in results:
75+
self.warning(node, bind_warning)
76+
self.shadowed_next = True
77+
78+
79+
### The following functions help test if node is part of an assignment
80+
### target.
81+
82+
def is_assign_target(node):
83+
assign = find_assign(node)
84+
if assign is None:
85+
return False
86+
87+
for child in assign.children:
88+
if child.type == token.EQUAL:
89+
return False
90+
elif is_subtree(child, node):
91+
return True
92+
return False
93+
94+
def find_assign(node):
95+
if node.type == syms.expr_stmt:
96+
return node
97+
if node.type == syms.simple_stmt or node.parent is None:
98+
return None
99+
return find_assign(node.parent)
100+
101+
def is_subtree(root, node):
102+
if root == node:
103+
return True
104+
return any(is_subtree(c, node) for c in root.children)

0 commit comments

Comments
 (0)