bpo-28837: Fix lib2to3 handling of map/zip/filter calls when followed with a 'trailer', e.g. zip()[x] (#24)

This commit is contained in:
Stuart Berg 2017-04-06 01:19:40 -04:00 committed by Benjamin Peterson
parent 01fa9ae546
commit 93b4b47e3a
5 changed files with 110 additions and 26 deletions

View File

@ -15,7 +15,10 @@
# Local imports # Local imports
from .. import fixer_base from .. import fixer_base
from ..fixer_util import Name, Call, ListComp, in_special_context from ..pytree import Node
from ..pygram import python_symbols as syms
from ..fixer_util import Name, ArgList, ListComp, in_special_context
class FixFilter(fixer_base.ConditionalFix): class FixFilter(fixer_base.ConditionalFix):
BM_compatible = True BM_compatible = True
@ -34,16 +37,19 @@ class FixFilter(fixer_base.ConditionalFix):
> >
')' ')'
> >
[extra_trailers=trailer*]
> >
| |
power< power<
'filter' 'filter'
trailer< '(' arglist< none='None' ',' seq=any > ')' > trailer< '(' arglist< none='None' ',' seq=any > ')' >
[extra_trailers=trailer*]
> >
| |
power< power<
'filter' 'filter'
args=trailer< '(' [any] ')' > args=trailer< '(' [any] ')' >
[extra_trailers=trailer*]
> >
""" """
@ -53,23 +59,32 @@ def transform(self, node, results):
if self.should_skip(node): if self.should_skip(node):
return return
trailers = []
if 'extra_trailers' in results:
for t in results['extra_trailers']:
trailers.append(t.clone())
if "filter_lambda" in results: if "filter_lambda" in results:
new = ListComp(results.get("fp").clone(), new = ListComp(results.get("fp").clone(),
results.get("fp").clone(), results.get("fp").clone(),
results.get("it").clone(), results.get("it").clone(),
results.get("xp").clone()) results.get("xp").clone())
new = Node(syms.power, [new] + trailers, prefix="")
elif "none" in results: elif "none" in results:
new = ListComp(Name("_f"), new = ListComp(Name("_f"),
Name("_f"), Name("_f"),
results["seq"].clone(), results["seq"].clone(),
Name("_f")) Name("_f"))
new = Node(syms.power, [new] + trailers, prefix="")
else: else:
if in_special_context(node): if in_special_context(node):
return None return None
new = node.clone()
args = results['args'].clone()
new = Node(syms.power, [Name("filter"), args], prefix="")
new = Node(syms.power, [Name("list"), ArgList([new])] + trailers)
new.prefix = "" new.prefix = ""
new = Call(Name("list"), [new])
new.prefix = node.prefix new.prefix = node.prefix
return new return new

View File

@ -22,8 +22,10 @@
# Local imports # Local imports
from ..pgen2 import token from ..pgen2 import token
from .. import fixer_base from .. import fixer_base
from ..fixer_util import Name, Call, ListComp, in_special_context from ..fixer_util import Name, ArgList, Call, ListComp, in_special_context
from ..pygram import python_symbols as syms from ..pygram import python_symbols as syms
from ..pytree import Node
class FixMap(fixer_base.ConditionalFix): class FixMap(fixer_base.ConditionalFix):
BM_compatible = True BM_compatible = True
@ -32,6 +34,7 @@ class FixMap(fixer_base.ConditionalFix):
map_none=power< map_none=power<
'map' 'map'
trailer< '(' arglist< 'None' ',' arg=any [','] > ')' > trailer< '(' arglist< 'None' ',' arg=any [','] > ')' >
[extra_trailers=trailer*]
> >
| |
map_lambda=power< map_lambda=power<
@ -47,10 +50,12 @@ class FixMap(fixer_base.ConditionalFix):
> >
')' ')'
> >
[extra_trailers=trailer*]
> >
| |
power< power<
'map' trailer< '(' [arglist=any] ')' > 'map' args=trailer< '(' [any] ')' >
[extra_trailers=trailer*]
> >
""" """
@ -60,6 +65,11 @@ def transform(self, node, results):
if self.should_skip(node): if self.should_skip(node):
return return
trailers = []
if 'extra_trailers' in results:
for t in results['extra_trailers']:
trailers.append(t.clone())
if node.parent.type == syms.simple_stmt: if node.parent.type == syms.simple_stmt:
self.warning(node, "You should use a for loop here") self.warning(node, "You should use a for loop here")
new = node.clone() new = node.clone()
@ -69,23 +79,32 @@ def transform(self, node, results):
new = ListComp(results["xp"].clone(), new = ListComp(results["xp"].clone(),
results["fp"].clone(), results["fp"].clone(),
results["it"].clone()) results["it"].clone())
new = Node(syms.power, [new] + trailers, prefix="")
else: else:
if "map_none" in results: if "map_none" in results:
new = results["arg"].clone() new = results["arg"].clone()
new.prefix = ""
else: else:
if "arglist" in results: if "args" in results:
args = results["arglist"] args = results["args"]
if args.type == syms.arglist and \ if args.type == syms.trailer and \
args.children[0].type == token.NAME and \ args.children[1].type == syms.arglist and \
args.children[0].value == "None": args.children[1].children[0].type == token.NAME and \
args.children[1].children[0].value == "None":
self.warning(node, "cannot convert map(None, ...) " self.warning(node, "cannot convert map(None, ...) "
"with multiple arguments because map() " "with multiple arguments because map() "
"now truncates to the shortest sequence") "now truncates to the shortest sequence")
return return
new = Node(syms.power, [Name("map"), args.clone()])
new.prefix = ""
if in_special_context(node): if in_special_context(node):
return None return None
new = node.clone()
new = Node(syms.power, [Name("list"), ArgList([new])] + trailers)
new.prefix = "" new.prefix = ""
new = Call(Name("list"), [new])
new.prefix = node.prefix new.prefix = node.prefix
return new return new

View File

@ -9,13 +9,16 @@
# Local imports # Local imports
from .. import fixer_base from .. import fixer_base
from ..fixer_util import Name, Call, in_special_context from ..pytree import Node
from ..pygram import python_symbols as syms
from ..fixer_util import Name, ArgList, in_special_context
class FixZip(fixer_base.ConditionalFix): class FixZip(fixer_base.ConditionalFix):
BM_compatible = True BM_compatible = True
PATTERN = """ PATTERN = """
power< 'zip' args=trailer< '(' [any] ')' > power< 'zip' args=trailer< '(' [any] ')' > [trailers=trailer*]
> >
""" """
@ -28,8 +31,16 @@ def transform(self, node, results):
if in_special_context(node): if in_special_context(node):
return None return None
new = node.clone() args = results['args'].clone()
new.prefix = "" args.prefix = ""
new = Call(Name("list"), [new])
trailers = []
if 'trailers' in results:
trailers = [n.clone() for n in results['trailers']]
for n in trailers:
n.prefix = ""
new = Node(syms.power, [Name("zip"), args], prefix="")
new = Node(syms.power, [Name("list"), ArgList([new])] + trailers)
new.prefix = node.prefix new.prefix = node.prefix
return new return new

View File

@ -2954,10 +2954,23 @@ def test_filter_basic(self):
a = """x = [x for x in range(10) if x%2 == 0]""" a = """x = [x for x in range(10) if x%2 == 0]"""
self.check(b, a) self.check(b, a)
# XXX This (rare) case is not supported def test_filter_trailers(self):
## b = """x = filter(f, 'abc')[0]""" b = """x = filter(None, 'abc')[0]"""
## a = """x = list(filter(f, 'abc'))[0]""" a = """x = [_f for _f in 'abc' if _f][0]"""
## self.check(b, a) self.check(b, a)
b = """x = len(filter(f, 'abc')[0])"""
a = """x = len(list(filter(f, 'abc'))[0])"""
self.check(b, a)
b = """x = filter(lambda x: x%2 == 0, range(10))[0]"""
a = """x = [x for x in range(10) if x%2 == 0][0]"""
self.check(b, a)
# Note the parens around x
b = """x = filter(lambda (x): x%2 == 0, range(10))[0]"""
a = """x = [x for x in range(10) if x%2 == 0][0]"""
self.check(b, a)
def test_filter_nochange(self): def test_filter_nochange(self):
a = """b.join(filter(f, 'abc'))""" a = """b.join(filter(f, 'abc'))"""
@ -3022,6 +3035,23 @@ def test_prefix_preservation(self):
a = """x = list(map( f, 'abc' ))""" a = """x = list(map( f, 'abc' ))"""
self.check(b, a) self.check(b, a)
def test_map_trailers(self):
b = """x = map(f, 'abc')[0]"""
a = """x = list(map(f, 'abc'))[0]"""
self.check(b, a)
b = """x = map(None, l)[0]"""
a = """x = list(l)[0]"""
self.check(b, a)
b = """x = map(lambda x:x, l)[0]"""
a = """x = [x for x in l][0]"""
self.check(b, a)
b = """x = map(f, 'abc')[0][1]"""
a = """x = list(map(f, 'abc'))[0][1]"""
self.check(b, a)
def test_trailing_comment(self): def test_trailing_comment(self):
b = """x = map(f, 'abc') # foo""" b = """x = map(f, 'abc') # foo"""
a = """x = list(map(f, 'abc')) # foo""" a = """x = list(map(f, 'abc')) # foo"""
@ -3066,11 +3096,6 @@ def test_map_basic(self):
""" """
self.warns(b, a, "You should use a for loop here") self.warns(b, a, "You should use a for loop here")
# XXX This (rare) case is not supported
## b = """x = map(f, 'abc')[0]"""
## a = """x = list(map(f, 'abc'))[0]"""
## self.check(b, a)
def test_map_nochange(self): def test_map_nochange(self):
a = """b.join(map(f, 'abc'))""" a = """b.join(map(f, 'abc'))"""
self.unchanged(a) self.unchanged(a)
@ -3130,6 +3155,10 @@ def check(self, b, a):
super(Test_zip, self).check(b, a) super(Test_zip, self).check(b, a)
def test_zip_basic(self): def test_zip_basic(self):
b = """x = zip()"""
a = """x = list(zip())"""
self.check(b, a)
b = """x = zip(a, b, c)""" b = """x = zip(a, b, c)"""
a = """x = list(zip(a, b, c))""" a = """x = list(zip(a, b, c))"""
self.check(b, a) self.check(b, a)
@ -3138,6 +3167,15 @@ def test_zip_basic(self):
a = """x = len(list(zip(a, b)))""" a = """x = len(list(zip(a, b)))"""
self.check(b, a) self.check(b, a)
def test_zip_trailers(self):
b = """x = zip(a, b, c)[0]"""
a = """x = list(zip(a, b, c))[0]"""
self.check(b, a)
b = """x = zip(a, b, c)[0][1]"""
a = """x = list(zip(a, b, c))[0][1]"""
self.check(b, a)
def test_zip_nochange(self): def test_zip_nochange(self):
a = """b.join(zip(a, b))""" a = """b.join(zip(a, b))"""
self.unchanged(a) self.unchanged(a)

View File

@ -128,6 +128,7 @@ Andrew Bennetts
Andy Bensky Andy Bensky
Bennett Benson Bennett Benson
Ezra Berch Ezra Berch
Stuart Berg
Michel Van den Bergh Michel Van den Bergh
Julian Berman Julian Berman
Brice Berna Brice Berna