X Tutup
Skip to content

Commit 1528537

Browse files
authored
Merge pull request rocky#80 from moagstar/BUILD_CONST_KEY_MAP
Build const key map
2 parents 9c49b5d + 6b8ae29 commit 1528537

File tree

4 files changed

+177
-1
lines changed

4 files changed

+177
-1
lines changed

pytest/test_build_const_key_map.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
# uncompyle6
3+
from uncompyle6 import PYTHON_VERSION
4+
from validate import validate_uncompyle
5+
6+
7+
@pytest.mark.skipif(PYTHON_VERSION < 3.6, reason='need at least python 3.6')
8+
@pytest.mark.parametrize('text', (
9+
"{0.: 'a', -1: 'b'}", # BUILD_MAP
10+
"{'a':'b'}", # BUILD_MAP
11+
"{0: 1}", # BUILD_MAP
12+
"{b'0':1, b'2':3}", # BUILD_CONST_KEY_MAP
13+
"{0: 1, 2: 3}", # BUILD_CONST_KEY_MAP
14+
"{'a':'b','c':'d'}", # BUILD_CONST_KEY_MAP
15+
"{0: 1, 2: 3}", # BUILD_CONST_KEY_MAP
16+
"{'a': 1, 'b': 2}", # BUILD_CONST_KEY_MAP
17+
"{'a':'b','c':'d'}", # BUILD_CONST_KEY_MAP
18+
"{0.0:'b',0.1:'d'}", # BUILD_CONST_KEY_MAP
19+
))
20+
def test_build_const_key_map(text):
21+
validate_uncompyle(text)

pytest/validate.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# future
2+
from __future__ import print_function
3+
# std
4+
import os
5+
import dis
6+
import difflib
7+
import subprocess
8+
import tempfile
9+
# compatability
10+
import six
11+
# uncompyle6 / xdis
12+
from uncompyle6 import PYTHON_VERSION, deparse_code
13+
14+
15+
def _dis_to_text(co):
16+
return dis.Bytecode(co).dis()
17+
18+
19+
def print_diff(original, uncompyled):
20+
"""
21+
Try and display a pretty html line difference between the original and
22+
uncompyled code and bytecode if elinks and BeautifulSoup are installed
23+
otherwise just show the diff.
24+
25+
:param original: Text describing the original code object.
26+
:param uncompyled: Text describing the uncompyled code object.
27+
"""
28+
original_lines = original.split('\n')
29+
uncompyled_lines = uncompyled.split('\n')
30+
args = original_lines, uncompyled_lines, 'original', 'uncompyled'
31+
try:
32+
from bs4 import BeautifulSoup
33+
diff = difflib.HtmlDiff().make_file(*args)
34+
diff = BeautifulSoup(diff, "html.parser")
35+
diff.select_one('table[summary="Legends"]').extract()
36+
except ImportError:
37+
print('\nTo display diff highlighting run:\n pip install BeautifulSoup4')
38+
diff = difflib.HtmlDiff().make_table(*args)
39+
40+
with tempfile.NamedTemporaryFile(delete=False) as f:
41+
f.write(str(diff).encode('utf-8'))
42+
43+
try:
44+
print()
45+
html = subprocess.check_output([
46+
'elinks',
47+
'-dump',
48+
'-no-references',
49+
'-dump-color-mode',
50+
'1',
51+
f.name,
52+
]).decode('utf-8')
53+
print(html)
54+
except:
55+
print('\nFor side by side diff install elinks')
56+
diff = difflib.Differ().compare(original_lines, uncompyled_lines)
57+
print('\n'.join(diff))
58+
finally:
59+
os.unlink(f.name)
60+
61+
62+
def are_instructions_equal(i1, i2):
63+
"""
64+
Determine if two instructions are approximately equal,
65+
ignoring certain fields which we allow to differ, namely:
66+
67+
* code objects are ignore (should probaby be checked) due to address
68+
* line numbers
69+
70+
:param i1: left instruction to compare
71+
:param i2: right instruction to compare
72+
73+
:return: True if the two instructions are approximately equal, otherwise False.
74+
"""
75+
result = (1==1
76+
and i1.opname == i2.opname
77+
and i1.opcode == i2.opcode
78+
and i1.arg == i2.arg
79+
# ignore differences due to code objects
80+
# TODO : Better way of ignoring address
81+
and (i1.argval == i2.argval or '<code object' in str(i1.argval))
82+
# TODO : Should probably recurse to check code objects
83+
and (i1.argrepr == i2.argrepr or '<code object' in i1.argrepr)
84+
and i1.offset == i2.offset
85+
# ignore differences in line numbers
86+
#and i1.starts_line
87+
and i1.is_jump_target == i2.is_jump_target
88+
)
89+
return result
90+
91+
92+
def are_code_objects_equal(co1, co2):
93+
"""
94+
Determine if two code objects are approximately equal,
95+
see are_instructions_equal for more information.
96+
97+
:param i1: left code object to compare
98+
:param i2: right code object to compare
99+
100+
:return: True if the two code objects are approximately equal, otherwise False.
101+
"""
102+
# TODO : Use xdis for python2 compatability
103+
instructions1 = dis.Bytecode(co1)
104+
instructions2 = dis.Bytecode(co2)
105+
for opcode1, opcode2 in zip(instructions1, instructions2):
106+
if not are_instructions_equal(opcode1, opcode2):
107+
return False
108+
return True
109+
110+
111+
def validate_uncompyle(text, mode='exec'):
112+
"""
113+
Validate decompilation of the given source code.
114+
115+
:param text: Source to validate decompilation of.
116+
"""
117+
original_code = compile(text, '<string>', mode)
118+
original_dis = _dis_to_text(original_code)
119+
original_text = text
120+
121+
deparsed = deparse_code(PYTHON_VERSION, original_code,
122+
compile_mode=mode, out=six.StringIO())
123+
uncompyled_text = deparsed.text
124+
uncompyled_code = compile(uncompyled_text, '<string>', 'exec')
125+
126+
if not are_code_objects_equal(uncompyled_code, original_code):
127+
128+
uncompyled_dis = _dis_to_text(uncompyled_text)
129+
130+
def output(text, dis):
131+
width = 60
132+
return '\n\n'.join([
133+
' SOURCE CODE '.center(width, '#'),
134+
text.strip(),
135+
' BYTECODE '.center(width, '#'),
136+
dis
137+
])
138+
139+
original = output(original_text, original_dis)
140+
uncompyled = output(uncompyled_text, uncompyled_dis)
141+
print_diff(original, uncompyled)
142+
143+
assert 'original' == 'uncompyled'

requirements-dev.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pytest
22
flake8
3-
hypothesis
3+
hypothesis
4+
six

uncompyle6/semantics/pysource.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,17 @@ def n_mapexpr(self, node):
14261426
i += 3
14271427
pass
14281428
pass
1429+
elif node[-1].type == 'BUILD_CONST_KEY_MAP':
1430+
# Python 3.6+ style const map
1431+
keys = node[-2].pattr
1432+
values = node[:-2]
1433+
# FIXME: Line numbers?
1434+
for key, value in zip(keys, values):
1435+
self.write(repr(key))
1436+
self.write(':')
1437+
self.write(self.traverse(value[0]))
1438+
self.write(',')
1439+
pass
14291440
pass
14301441
else:
14311442
# Python 2 style kvlist

0 commit comments

Comments
 (0)
X Tutup