Compare commits

...

19 Commits

Author SHA1 Message Date
rocky
9d425039a2 Merge branch 'master' into python-2.4 2018-02-17 11:28:45 -05:00
rocky
a5c388c13b Another 2.7 "if" with return fix
This works is in conjunction with the commit before the previous release commit.
2018-02-17 11:11:32 -05:00
rocky
c82095e6ac Get ready for release 2.16.0 2018-02-17 07:30:22 -05:00
rocky
67ad08fd4a Beter 2.7 end_if and COME_FROM determination
Fixes #149

... Add more tests too
2018-02-17 07:16:14 -05:00
rocky
fa4f614295 Wierd comprehension bug seen via new loctraceback 2018-02-15 12:15:49 -05:00
rocky
832f04a486 Merge branch 'master' into python-2.4 2018-02-15 10:47:14 -05:00
rocky
f7f0aa5ea9 Merge branch 'master' of github.com:rocky/python-uncompyle6 2018-02-15 10:42:29 -05:00
rocky
083ae5f3fd Add deparsed_find() used by the trepan debuggers 2018-02-15 10:42:00 -05:00
rocky
657d5ef024 pydisasm fixes 2018-02-15 07:33:51 -05:00
rocky
e92c2503d1 Merge branch 'master' into python-2.4 2018-02-15 07:31:11 -05:00
rocky
a01091a46e Misc pydisasm fixes 2018-02-15 07:30:01 -05:00
rocky
b5a825f4d8 Fix up 3.6+ CALL_FUNCTION_EX 2018-02-12 07:45:20 -05:00
rocky
730b0549d5 Handle 3.6+ FUNCTION_EX a little more generally 2018-02-12 04:26:52 -05:00
rocky
e431e49d77 Isolate Python 3.5 custom parse rules...
are isolated into parse35.py now and removed from parse3.py

This causes some code duplicated from parse3.py into parse3{5,6}.py

We will deal with that later.
2018-02-12 03:59:44 -05:00
rocky
230a38d537 Fix Python 3.5 CALL_FUNCTION_VAR handling 2018-02-12 03:07:03 -05:00
rocky
6d29ed9077 Python 3.5 CALL_FUNCTION_VAR bugs 2018-02-09 16:48:11 -05:00
rocky
bb45be2dc7 Start to handle 3.5+ BUILD_LIST_UNPACK in call ..
to implement multple star arguments
2018-02-09 03:41:13 -05:00
rocky
f7999d2754 Add custom 3.5 handling for f(*a, *b, *c) 2018-02-08 08:42:38 -05:00
rocky
5aeb0424fc Administrivia 2018-02-05 06:37:50 -05:00
21 changed files with 266 additions and 67 deletions

16
NEWS
View File

@@ -1,3 +1,19 @@
uncompyle6 2.16.0 2018-02-17
- API additions:
- add fragments.op_at_code_loc() and
- fragments.deparsed_find()_
- Better 2.7 end_if and COME_FROM determination
- Fix up 3.6+ CALL_FUNCTION_EX
- Misc pydisasm fixes
- Wierd comprehension bug seen via new loctraceback
- Fix Python 3.5+ CALL_FUNCTION_VAR and BUILD_LIST_UNPACK in call; with this
we can can handle 3.5+ f(a, b, *c, *d, *e) now
uncompyle6 2.15.1 2018-02-05
- More bug fixes and revert an improper bug fix in 2.15.0
uncompyle6 2.15.0 2018-02-05 pycon2018.co
- Bug fixes

View File

@@ -45,6 +45,8 @@
$ source admin-tools/setup-python-2.4.sh
$ git merge master
# Add and fix merge conflicts
$ git commit
# Check against older versions

View File

@@ -1,5 +1,5 @@
import pytest
from uncompyle6.semantics.fragments import deparse_code as deparse
from uncompyle6.semantics.fragments import deparse_code as deparse, deparsed_find
from uncompyle6 import PYTHON_VERSION, PYTHON3
def map_stmts(x, y):
@@ -42,6 +42,7 @@ def check_expect(expect, parsed, fn_name):
"%s: ran out if items in testing node" % fn_name)
nodeInfo = parsed.offsets[name, offset]
node = nodeInfo.node
nodeInfo2 = deparsed_find((name, offset), parsed, code)
extractInfo = parsed.extract_node_info(node)
assert expect[i] == extractInfo.selectedLine, \

Binary file not shown.

Binary file not shown.

Binary file not shown.

0
test/bytecode_3.6_run/.gitignore vendored Normal file
View File

View File

@@ -1,9 +1,16 @@
# Bug in Python 3.5 is getting the two star'd arguments right.
def sum(a,b,c,d):
# Bug in 3.5 is detecting when there is more than
# one * in a call. There is a "BUILD_UNPACK_LIST" instruction used
# to unpack star arguments
def sum(a, b, c, d):
return a + b + c + d
args=(1,2)
args, a, b, c = (1, 2), 1, 2, 3
sum(*args, *args)
sum(*args, *args, *args)
sum(a, *args, *args)
sum(a, b, *args)
sum(a, b, *args, *args)
# FIXME: this is handled incorrectly
# (*c,) = (3,4)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# Mode: -*- python -*-
#
# Copyright (c) 2015-2016 by Rocky Bernstein <rb@dustyfeet.com>
# Copyright (c) 2015-2016, 2018 by Rocky Bernstein <rb@dustyfeet.com>
#
import sys, os, getopt
@@ -15,17 +15,24 @@ Usage:
%s [OPTIONS]... FILE
%s [--help | -h | -V | --version]
Disassemble FILE with the instruction mangling that is done to
assist uncompyle6 in parsing the instruction stream. For example
instructions with variable-length arguments like CALL_FUNCTION and
BUILD_LIST have arguement counts appended to the instruction name, and
COME_FROM instructions are inserted into the instruction stream.
Examples:
{0} foo.pyc
{0} foo.py # same thing as above but find the file
{0} foo.pyc bar.pyc # disassemble foo.pyc and bar.pyc
%s foo.pyc
%s foo.py # same thing as above but find the file
%s foo.pyc bar.pyc # disassemble foo.pyc and bar.pyc
See also `pydisasm' from the `xdis' package.
Options:
-U | --uncompyle6 show instructions with uncompyle6 mangling
-V | --version show version and stop
-h | --help show this message
""" % (program, program)
""" % ((program,) * 5)
PATTERNS = ('*.pyc', '*.pyo')
@@ -33,8 +40,6 @@ def main():
Usage_short = """usage: %s FILE...
Type -h for for full help.""" % program
native = True
if len(sys.argv) == 1:
sys.stderr.write("No file(s) given\n")
sys.stderr.write(Usage_short)
@@ -54,8 +59,6 @@ Type -h for for full help.""" % program
elif opt in ('-V', '--version'):
print("%s %s" % (program, VERSION))
sys.exit(0)
elif opt in ('-U', '--uncompyle6'):
native = False
else:
print(opt)
sys.stderr.write(Usage_short)
@@ -63,7 +66,7 @@ Type -h for for full help.""" % program
for file in files:
if os.path.exists(files[0]):
disassemble_file(file, sys.stdout, native)
disassemble_file(file, sys.stdout)
else:
sys.stderr.write("Can't read %s - skipping\n" % files[0])
pass

View File

@@ -130,6 +130,11 @@ class Python27Parser(Python2Parser):
whileelsestmt ::= SETUP_LOOP testexpr l_stmts_opt JUMP_BACK POP_BLOCK
else_suitel COME_FROM
return_stmts ::= _stmts return_stmt
return_stmts ::= return_stmt
return_stmt ::= return
ifstmt ::= testexpr return_stmts COME_FROM
ifstmt ::= testexpr return_if_stmts COME_FROM
ifelsestmt ::= testexpr c_stmts_opt JUMP_FORWARD else_suite COME_FROM
ifelsestmtc ::= testexpr c_stmts_opt JUMP_ABSOLUTE else_suitec

View File

@@ -484,33 +484,15 @@ class Python3Parser(PythonParser):
token.kind = self.call_fn_name(token)
uniq_param = args_kw + args_pos
if self.version == 3.5 and opname.startswith('CALL_FUNCTION_VAR'):
# Python 3.5 changes the stack position of *args. KW args come
# after *args.
# Python 3.6+ replaces CALL_FUNCTION_VAR_KW with CALL_FUNCTION_EX
if opname.endswith('KW'):
kw = 'expr '
else:
kw = ''
rule = ('call ::= expr expr ' +
('pos_arg ' * args_pos) +
('kwarg ' * args_kw) + kw + token.kind)
self.add_unique_rule(rule, token.kind, uniq_param, customize)
# Note: 3.5+ have subclassed this method; so we don't handle
# 'CALL_FUNCTION_VAR' or 'CALL_FUNCTION_EX' here.
rule = ('call ::= expr ' +
('pos_arg ' * args_pos) +
('kwarg ' * args_kw) +
'expr ' * nak + token.kind)
self.add_unique_rule(rule, token.kind, uniq_param, customize)
if self.version >= 3.5 and seen_GET_AWAITABLE_YIELD_FROM:
rule = ('async_call ::= expr ' +
('pos_arg ' * args_pos) +
('kwarg ' * args_kw) +
'expr ' * nak + token.kind +
' GET_AWAITABLE LOAD_CONST YIELD_FROM')
self.add_unique_rule(rule, token.kind, uniq_param, customize)
self.add_unique_rule('expr ::= async_call', token.kind, uniq_param, customize)
'expr ' * nak + token.kind)
self.add_unique_rule(rule, token.kind, uniq_param, customize)
if possible_class_decorator:
if next_token == 'CALL_FUNCTION' and next_token.attr == 1:
@@ -671,6 +653,11 @@ class Python3Parser(PythonParser):
rule = ('build_map_unpack_with_call ::= ' + 'expr1024 ' * int(v//1024) +
'expr32 ' * int((v//32) % 32) +
'expr ' * (v % 32) + opname)
self.addRule(rule, nop_func)
elif opname.startswith('BUILD_TUPLE_UNPACK_WITH_CALL'):
v = token.attr
rule = ('starred ::= %s %s' % ('expr ' * v, opname))
self.addRule(rule, nop_func)
elif opname_base in ('BUILD_LIST', 'BUILD_SET', 'BUILD_TUPLE'):
v = token.attr

View File

@@ -186,6 +186,52 @@ class Python35Parser(Python34Parser):
pass
return
def custom_classfunc_rule(self, opname, token, customize,
seen_LOAD_BUILD_CLASS,
seen_GET_AWAITABLE_YIELD_FROM,
*args):
args_pos, args_kw = self.get_pos_kw(token)
# Additional exprs for * and ** args:
# 0 if neither
# 1 for CALL_FUNCTION_VAR or CALL_FUNCTION_KW
# 2 for * and ** args (CALL_FUNCTION_VAR_KW).
# Yes, this computation based on instruction name is a little bit hoaky.
nak = ( len(opname)-len('CALL_FUNCTION') ) // 3
uniq_param = args_kw + args_pos
if seen_GET_AWAITABLE_YIELD_FROM:
rule = ('async_call ::= expr ' +
('pos_arg ' * args_pos) +
('kwarg ' * args_kw) +
'expr ' * nak + token.kind +
' GET_AWAITABLE LOAD_CONST YIELD_FROM')
self.add_unique_rule(rule, token.kind, uniq_param, customize)
self.add_unique_rule('expr ::= async_call', token.kind, uniq_param, customize)
uniq_param = args_kw + args_pos
if opname.startswith('CALL_FUNCTION_VAR'):
# Python 3.5 changes the stack position of *args. KW args come
# after *args.
# Note: Python 3.6+ replaces CALL_FUNCTION_VAR and
# CALL_FUNCTION_VAR_KW with CALL_FUNCTION_EX
token.kind = self.call_fn_name(token)
if opname.endswith('KW'):
kw = 'expr '
else:
kw = ''
rule = ('call ::= expr expr ' +
('pos_arg ' * args_pos) +
('kwarg ' * args_kw) + kw + token.kind)
self.add_unique_rule(rule, token.kind, uniq_param, customize)
else:
super(Python35Parser, self).custom_classfunc_rule(opname, token, customize,
seen_LOAD_BUILD_CLASS,
seen_GET_AWAITABLE_YIELD_FROM,
*args)
class Python35ParserSingle(Python35Parser, PythonParserSingle):
pass

View File

@@ -131,6 +131,26 @@ class Python36Parser(Python35Parser):
def custom_classfunc_rule(self, opname, token, customize,
possible_class_decorator,
seen_GET_AWAITABLE_YIELD_FROM, next_token):
args_pos, args_kw = self.get_pos_kw(token)
# Additional exprs for * and ** args:
# 0 if neither
# 1 for CALL_FUNCTION_VAR or CALL_FUNCTION_KW
# 2 for * and ** args (CALL_FUNCTION_VAR_KW).
# Yes, this computation based on instruction name is a little bit hoaky.
nak = ( len(opname)-len('CALL_FUNCTION') ) // 3
uniq_param = args_kw + args_pos
if seen_GET_AWAITABLE_YIELD_FROM:
rule = ('async_call ::= expr ' +
('pos_arg ' * args_pos) +
('kwarg ' * args_kw) +
'expr ' * nak + token.kind +
' GET_AWAITABLE LOAD_CONST YIELD_FROM')
self.add_unique_rule(rule, token.kind, uniq_param, customize)
self.add_unique_rule('expr ::= async_call', token.kind, uniq_param, customize)
if opname.startswith('CALL_FUNCTION_KW'):
self.addRule("expr ::= call_kw", nop_func)
values = 'expr ' * token.attr

View File

@@ -962,17 +962,32 @@ class Scanner2(Scanner):
'end': end_offset})
elif code_pre_rtarget == self.opc.RETURN_VALUE:
if self.version == 2.7 or pre_rtarget not in self.ignore_if:
# 10 is exception-match. If there is an exception match in the
# compare, then this is an exception clause not an if-then clause
# Below, 10 is exception-match. If there is an exception
# match in the compare, then this is an exception
# clause not an if-then clause
if (self.code[self.prev[offset]] != self.opc.COMPARE_OP or
self.code[self.prev[offset]+1] != 10):
self.structs.append({'type': 'if-then',
'start': start,
'end': rtarget})
self.thens[start] = rtarget
if self.version == 2.7 or code[pre_rtarget+1] != self.opc.JUMP_FORWARD:
if (self.version == 2.7 or
code[pre_rtarget+1] != self.opc.JUMP_FORWARD):
# The below is a big hack until we get
# better control flow analysis: disallow
# END_IF if the instruction before the
# END_IF instruction happens to be a jump
# target. In this case, probably what's
# gone on is that we messed up on the
# END_IF location and it should be the
# instruction before.
self.fixed_jumps[offset] = rtarget
self.return_end_ifs.add(pre_rtarget)
if (self.version == 2.7 and
self.insts[self.offset2inst_index[pre[pre_rtarget]]].is_jump_target):
self.return_end_ifs.add(pre[pre_rtarget])
pass
else:
self.return_end_ifs.add(pre_rtarget)
pass
pass
pass

View File

@@ -33,6 +33,8 @@ class Scanner36(Scanner3):
t.kind = 'CALL_FUNCTION_KW_%s' % t.attr
elif t.op == self.opc.BUILD_MAP_UNPACK_WITH_CALL:
t.kind = 'BUILD_MAP_UNPACK_WITH_CALL_%d' % t.attr
elif t.op == self.opc.BUILD_TUPLE_UNPACK_WITH_CALL:
t.kind = 'BUILD_TUPLE_UNPACK_WITH_CALL_%d' % t.attr
pass
return tokens, customize

View File

@@ -43,9 +43,9 @@ def customize_for_version(self, is_pypy, version):
if version < 3.0:
TABLE_R.update({
'STORE_SLICE+0': ( '%c[:]', 0 ),
'STORE_SLICE+1': ( '%c[%p:]', 0, (1, 100) ),
'STORE_SLICE+2': ( '%c[:%p]', 0, (1, 100) ),
'STORE_SLICE+3': ( '%c[%p:%p]', 0, (1, 100), (2, 100) ),
'STORE_SLICE+1': ( '%c[%p:]', 0, (1, -1) ),
'STORE_SLICE+2': ( '%c[:%p]', 0, (1, -1) ),
'STORE_SLICE+3': ( '%c[%p:%p]', 0, (1, -1), (2, -1) ),
'DELETE_SLICE+0': ( '%|del %c[:]\n', 0 ),
'DELETE_SLICE+1': ( '%|del %c[%c:]\n', 0, 1 ),
'DELETE_SLICE+2': ( '%|del %c[:%c]\n', 0, 1 ),
@@ -251,8 +251,8 @@ def customize_for_version(self, is_pypy, version):
node.kind == 'call'
p = self.prec
self.prec = 80
self.template_engine(('%c(%P)', 0,
(1, -4, ', ', 100)), node)
self.template_engine(('%c(%P)', 0, (1, -4, ', ',
100)), node)
self.prec = p
node.kind == 'async_call'
self.prune()
@@ -270,7 +270,7 @@ def customize_for_version(self, is_pypy, version):
if key.kind.startswith('CALL_FUNCTION_VAR_KW'):
# Python 3.5 changes the stack position of *args. kwargs come
# after *args whereas in earlier Pythons, *args is at the end
# which simpilfiies things from our perspective.
# which simplifies things from our perspective.
# Python 3.6+ replaces CALL_FUNCTION_VAR_KW with CALL_FUNCTION_EX
# We will just swap the order to make it look like earlier Python 3.
entry = table[key.kind]
@@ -282,6 +282,27 @@ def customize_for_version(self, is_pypy, version):
node[kwarg_pos], node[args_pos] = node[args_pos], node[kwarg_pos]
args_pos = kwarg_pos
kwarg_pos += 1
elif key.kind.startswith('CALL_FUNCTION_VAR'):
nargs = node[-1].attr & 0xFF
if nargs > 0:
template = ('%c(%C, ', 0, (1, nargs+1, ', '))
else:
template = ('%c(', 0)
self.template_engine(template, node)
args_node = node[-2]
if args_node == 'pos_arg':
args_node = args_node[0]
if args_node == 'expr':
args_node = args_node[0]
if args_node == 'build_list_unpack':
template = ('*%P)', (0, len(args_node)-1, ', *', 100))
self.template_engine(template, args_node)
else:
template = ('*%c)', -2)
self.template_engine(template, node)
self.prune()
self.default(node)
self.n_call = n_call
@@ -326,7 +347,6 @@ def customize_for_version(self, is_pypy, version):
'func_args36': ( "%c(**", 0),
'try_except36': ( '%|try:\n%+%c%-%c\n\n', 1, 2 ),
'unpack_list': ( '*%c', (0, 'list') ),
'starred': ( '*%c', (0, 'expr') ),
'call_ex' : (
'%c(%c)',
(0, 'expr'), 1),
@@ -577,6 +597,27 @@ def customize_for_version(self, is_pypy, version):
return
self.n_kwargs_36 = kwargs_36
def starred(node):
l = len(node)
assert l > 0
pos_args = node[0]
if pos_args == 'expr':
pos_args = pos_args[0]
if pos_args == 'tuple':
star_start = 1
template = '%C', (0, -1, ', ')
self.template_engine(template, pos_args)
self.write(', ')
else:
star_start = 0
if l > 1:
template = ( '*%C', (star_start, -1, ', *') )
else:
template = ( '*%c', (star_start, 'expr') )
self.template_engine(template, node)
self.prune()
self.n_starred = starred
def return_closure(node):
# Nothing should be output here

View File

@@ -769,7 +769,8 @@ class FragmentsWalker(pysource.SourceWalker, object):
self.set_pos_info(node[-1], gen_start, len(self.f.getvalue()))
def listcomprehension_walk2(self, node):
"""List comprehensions the way they are done in Python3.
"""List comprehensions the way they are done in Python 2 (and
some Python 3?).
They're more other comprehensions, e.g. set comprehensions
See if we can combine code.
"""
@@ -779,17 +780,24 @@ class FragmentsWalker(pysource.SourceWalker, object):
code = Code(node[1].attr, self.scanner, self.currentclass)
ast = self.build_ast(code._tokens, code._customize)
self.customize(code._customize)
ast = ast[0][0][0][0][0]
if node == 'set_comp':
ast = ast[0][0][0]
else:
ast = ast[0][0][0][0][0]
if ast == 'expr':
ast = ast[0]
n = ast[1]
collection = node[-3]
list_if = None
assert n == 'list_iter'
# find innermost node
# Find the list comprehension body. It is the inner-most
# node that is not list_.. .
while n == 'list_iter':
n = n[0] # recurse one step
if n == 'list_for':
if n == 'list_for':
store = n[2]
n = n[3]
elif n in ('list_if', 'list_if_not'):
@@ -1824,29 +1832,66 @@ def deparse_code_around_offset(name, offset, version, co, out=StringIO(),
return deparsed
def op_at_code_loc(code, loc, opc):
"""Return the instruction name at code[loc] using
opc to look up instruction names. Returns 'got IndexError'
if code[loc] is invalid.
`code` is instruction bytecode, `loc` is an offset (integer) and
`opc` is an opcode module from `xdis`.
"""
try:
op = code[loc]
except IndexError:
return 'got IndexError'
return opc.opname[op]
def deparsed_find(tup, deparsed, code):
"""Return a NodeInfo nametuple for a fragment-deparsed `deparsed` at `tup`.
`tup` is a name and offset tuple, `deparsed` is a fragment object
and `code` is instruction bytecode.
"""
nodeInfo = None
name, last_i = tup
if (name, last_i) in deparsed.offsets.keys():
nodeInfo = deparsed.offsets[name, last_i]
else:
from uncompyle6.scanner import get_scanner
scanner = get_scanner(deparsed.version)
co = code.co_code
if op_at_code_loc(co, last_i, scanner.opc) == 'DUP_TOP':
offset = deparsed.scanner.next_offset(co[last_i], last_i)
if (name, offset) in deparsed.offsets:
nodeInfo = deparsed.offsets[name, offset]
return nodeInfo
# if __name__ == '__main__':
# from uncompyle6 import IS_PYPY
# def deparse_test(co, is_pypy=IS_PYPY):
# from xdis.magics import sysinfo2float
# float_version = sysinfo2float()
# walk = deparse_code(float_version, co, showasm=False, showast=False,
# showgrammar=False, is_pypy=IS_PYPY)
# deparsed = deparse_code(float_version, co, showasm=False, showast=False,
# showgrammar=False, is_pypy=IS_PYPY)
# print("deparsed source")
# print(walk.text, "\n")
# print(deparsed.text, "\n")
# print('------------------------')
# for name, offset in sorted(walk.offsets.keys(),
# for name, offset in sorted(deparsed.offsets.keys(),
# key=lambda x: str(x[0])):
# print("name %s, offset %s" % (name, offset))
# nodeInfo = walk.offsets[name, offset]
# nodeInfo = deparsed.offsets[name, offset]
# nodeInfo2 = deparsed_find((name, offset), deparsed, co)
# assert nodeInfo == nodeInfo2
# node = nodeInfo.node
# extractInfo = walk.extract_node_info(node)
# extractInfo = deparsed.extract_node_info(node)
# print("code: %s" % node.kind)
# # print extractInfo
# print(extractInfo.selectedText)
# print(extractInfo.selectedLine)
# print(extractInfo.markerLine)
# extractInfo, p = walk.extract_parent_info(node)
# extractInfo, p = deparsed.extract_parent_info(node)
# if extractInfo:
# print("Contained in...")
@@ -1862,21 +1907,26 @@ def deparse_code_around_offset(name, offset, version, co, out=StringIO(),
# sys_version = sys.version_info[0] + (sys.version_info[1] / 10.0)
# walk = deparse_code_around_offset(name, offset, sys_version, co, showasm=False, showast=False,
# showgrammar=False, is_pypy=IS_PYPY)
# deparsed = deparse_code_around_offset(name, offset, sys_version, co,
# showasm=False,
# showast=False,
# showgrammar=False,
# is_pypy=IS_PYPY)
# print("deparsed source")
# print(walk.text, "\n")
# print(deparsed.text, "\n")
# print('------------------------')
# for name, offset in sorted(walk.offsets.keys(),
# for name, offset in sorted(deparsed.offsets.keys(),
# key=lambda x: str(x[0])):
# print("name %s, offset %s" % (name, offset))
# nodeInfo = walk.offsets[name, offset]
# nodeInfo = deparsed.offsets[name, offset]
# node = nodeInfo.node
# extractInfo = walk.extract_node_info(node)
# extractInfo = deparsed.extract_node_info(node)
# print("code: %s" % node.kind)
# # print extractInfo
# print(extractInfo.selectedText)
# print(extractInfo.selectedLine)
# print(extractInfo.markerLine)
# extractInfo, p = walk.extract_parent_info(node)
# extractInfo, p = deparsed.extract_parent_info(node)
# if extractInfo:
# print("Contained in...")
# print(extractInfo.selectedLine)

View File

@@ -1620,7 +1620,8 @@ class SourceWalker(GenericASTTraversal, object):
self.prec = p
def listcomprehension_walk2(self, node):
"""List comprehensions the way they are done in Python 2.
"""List comprehensions the way they are done in Python 2 (and
some Python 3?).
They're more other comprehensions, e.g. set comprehensions
See if we can combine code.
"""
@@ -1635,6 +1636,9 @@ class SourceWalker(GenericASTTraversal, object):
else:
ast = ast[0][0][0][0][0]
if ast == 'expr':
ast = ast[0]
n = ast[1]
collection = node[-3]
list_if = None

View File

@@ -1,3 +1,3 @@
# This file is suitable for sourcing inside bash as
# well as importing into Python
VERSION='2.15.1'
VERSION='2.16.0'