Skip to content

Commit e2ff084

Browse files
committed
Adds width attribute
Adds width attribute which allows user to specify number of bytes printed per line
1 parent 1e4f7b5 commit e2ff084

8 files changed

Lines changed: 261 additions & 22 deletions

File tree

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ python:
44
- 3.5
55
- 3.4.2
66
install:
7-
- pip install git+https://github.com/juhakivekas/multidiff
7+
- pip install .
88
script:
99
- python -m pytest

multidiff/Render.py

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from multidiff.Ansi import Ansi
22
import binascii
33
import html
4+
import textwrap
5+
import re
46

57
class Render():
6-
def __init__(self, encoder='hexdump', color='ansi'):
8+
def __init__(self, encoder='hexdump', color='ansi', bytes=16, width=None):
79
'''Configure the output encoding and coloring method of this rendering object'''
810
if color == 'ansi':
911
self.highligther = ansi_colored
@@ -16,17 +18,22 @@ def __init__(self, encoder='hexdump', color='ansi'):
1618
self.encoder = HexEncoder
1719
elif encoder == 'utf8':
1820
self.encoder = Utf8Encoder
19-
21+
22+
self.width = width
23+
self.bytes = bytes
24+
2025
def render(self, model, diff):
2126
'''Render the diff in the given model into a UTF-8 String'''
2227
result = self.encoder(self.highligther)
2328
obj = model.objects[diff.target]
2429
for op in diff.opcodes:
2530
data = obj.data[op[3]:op[4]]
2631
if type(data) == bytes:
27-
result.append(data, op[0])
32+
result.append(data, op[0], self.width, self.bytes)
2833
elif type(data) == str:
29-
result.append(bytes(data, "utf8"), op[0])
34+
result.append(bytes(data, "utf8"), op[0], self.width, self.bytes)
35+
if self.bytes != 16:
36+
return result.reformat(result.final(), int(self.bytes))
3037
return result.final()
3138

3239
def dumps(self, model):
@@ -42,9 +49,12 @@ def __init__(self, highligther):
4249
self.highligther = highligther
4350
self.output = ''
4451

45-
def append(self, data, color):
52+
def append(self, data, color, width=None, bytes=16):
4653
self.output += self.highligther(str(data, 'utf8'), color)
47-
54+
if width:
55+
if len(self.output) > int(width):
56+
self.output = textwrap.fill(self.output, int(width))
57+
4858
def final(self):
4959
return self.output
5060

@@ -53,10 +63,14 @@ class HexEncoder():
5363
def __init__(self, highligther):
5464
self.highligther = highligther
5565
self.output = ''
56-
def append(self, data, color):
66+
67+
def append(self, data, color, width=None, bytes=16):
5768
data = str(binascii.hexlify(data),'utf8')
5869
self.output += self.highligther(data, color)
59-
70+
if width:
71+
if len(self.output) > int(width):
72+
self.output = textwrap.fill(self.output, int(width))
73+
6074
def final(self):
6175
return self.output
6276

@@ -71,21 +85,21 @@ def __init__(self, highligther):
7185
self.skipspace = False
7286
self.asciirow = ''
7387

74-
def append(self, data, color):
88+
def append(self, data, color, width=None, bytes=16):
7589
if len(data) == 0:
76-
self._append(data, color)
90+
self._append(data, color, width)
7791
while len(data) > 0:
7892
if self.rowlen == 16:
7993
self._newrow()
80-
consumed = self._append(data[:16 - self.rowlen], color)
94+
consumed = self._append(data[:16 - self.rowlen], color, width)
8195
data = data[consumed:]
8296

83-
def _append(self, data, color):
97+
def _append(self, data, color, width):
8498
if len(data) == 0:
8599
#in the case of highlightig a deletion in a target or an
86100
#addition in the source, print a highlighted space and mark
87101
#it skippanble for the next append
88-
hexs = ' '
102+
hexs = ' '
89103
self.skipspace = True
90104
else:
91105
self._add_hex_space()
@@ -101,7 +115,10 @@ def _append(self, data, color):
101115
asciis += '.'
102116
self.asciirow += self.highligther(asciis, color)
103117

104-
self.hexrow += self.highligther(hexs, color)
118+
self.hexrow += self.highligther(hexs, color)
119+
if width:
120+
if len(self.hexrow) > int(width):
121+
self.hexrow = textwrap.fill(self.hexrow, int(width))
105122
self.rowlen += len(data)
106123
return len(data)
107124

@@ -121,14 +138,42 @@ def _add_hex_space(self):
121138
self.skipspace = False
122139
else:
123140
self.hexrow += ' '
124-
141+
125142

126143
def final(self):
127144
self.hexrow += 3*(16 - self.rowlen) * ' '
128145
self.asciirow += (16 - self.rowlen) * ' '
129146
self._newrow()
130147
return self.body
131148

149+
def reformat(self, body, n=16):
150+
instring = ''
151+
foo = body.split('\n')
152+
for line in foo:
153+
line.rstrip()
154+
line = line[line.find(':')+1:line.find('|')]
155+
instring += line
156+
instring += '\n'
157+
outstring = ''
158+
# Remove line numbers and newlines.
159+
clean_string = instring.replace(r'\d+:|\n', '')
160+
# Split on spaces that are not in tags.
161+
elements = re.split(r'\s+(?![^<]+>)', clean_string)
162+
# Omit first tag so that everything else can be chunked by n.
163+
clean_elements = elements[1:]
164+
# Chunk by n.
165+
chunks = [' '.join(clean_elements[i:i+n])
166+
for i in range(0, len(clean_elements), n)]
167+
# Concatenate the chunks as a line in outstring, with a line number.
168+
for i, chunk in enumerate(chunks):
169+
line = '{:06x}'.format(i*16)
170+
if i == 0:
171+
outstring += '{}:{} {}\n'.format(line, elements[0], chunk)
172+
else:
173+
outstring += '{}: {}\n'.format(line, chunk)
174+
outstring.rstrip()
175+
return outstring
176+
132177
def ansi_colored(string, op):
133178
if op == 'equal':
134179
return string

multidiff/StreamView.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
class StreamView():
55
'''A class for building UIs. Has some pretty serious side effects.
66
Use Render instead if you're not making a long-running UI'''
7-
def __init__(self, model, encoding='hexdump', mode='sequence', color='ansi'):
7+
def __init__(self, model, encoding='hexdump', mode='sequence', color='ansi', bytes=16, width=None):
88
self.color = color
9-
self.render = Render(color=color, encoder=encoding)
9+
self.render = Render(color=color, encoder=encoding, bytes=bytes, width=width)
1010
self.mode = mode
1111
self.model = model
12+
self.bytes = bytes
1213
model.add_listener(self)
1314

1415
def object_added(self, index):

multidiff/command_line_interface.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
#!/usr/bin/python3
22
import argparse
33
from multidiff import MultidiffModel, StreamView, SocketController, FileController, StdinController
4+
import shutil
5+
import sys
46

5-
def main():
6-
args = make_parser().parse_args()
7+
def main(args=None):
8+
9+
if args is None:
10+
args = sys.argv[1:]
11+
args = make_parser().parse_args(args)
712
m = MultidiffModel()
8-
v = StreamView(m, encoding=args.outformat, mode=args.mode, color=args.color)
9-
13+
14+
if args.width == 'max':
15+
args.width = get_max_width(args)
16+
17+
v = StreamView(m, encoding=args.outformat, mode=args.mode, color=args.color, bytes=args.bytes, width=args.width)
18+
1019
if len(args.file) > 0:
1120
informat = args.informat if args.informat else 'raw'
1221
files = FileController(m, informat)
@@ -20,6 +29,11 @@ def main():
2029
server = SocketController(('127.0.0.1', args.port), m, informat)
2130
server.serve_forever()
2231

32+
def get_max_width(args):
33+
columns = int(shutil.get_terminal_size((120,30)).columns)
34+
args.width = columns
35+
return args.width
36+
2337
def make_parser():
2438
parser = argparse.ArgumentParser(
2539
formatter_class=argparse.RawTextHelpFormatter,
@@ -75,6 +89,16 @@ def make_parser():
7589
const='html',
7690
default='ansi',
7791
help='use html for colors instead of ansi codes')
92+
93+
parser.add_argument('-w', '--width',
94+
dest='width',
95+
default='82',
96+
help='number of bytes printed per line, either an integer or max(width of console)')
97+
98+
parser.add_argument('-b', '--bytes',
99+
dest='bytes',
100+
default=16,
101+
help='number of hexs printed per line, either an integer or max(width of console)')
78102
return parser
79103

80104
if __name__ == '__main__':

test/bin_file1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0123456789abcdef012345678

test/bin_file2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0123456789abcdef

test/cli_test.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from multidiff.Render import *
2+
from multidiff import Ansi
3+
4+
import unittest
5+
from pathlib import Path
6+
import subprocess
7+
8+
9+
class MainCLITests(unittest.TestCase):
10+
11+
def call_run(self, expected_stdout, args):
12+
got_stdout = '(no stdout)'
13+
cmd = ['multidiff'] + args
14+
try:
15+
got_stdout = subprocess.check_output(cmd, universal_newlines=True)
16+
except subprocess.CalledProcessError as err:
17+
print('Got stderr: `{err_message}`'.format(err_message=err))
18+
finally:
19+
print('Got stdout: `{stdout}`'.format(stdout=got_stdout))
20+
21+
self.assertEqual(expected_stdout, got_stdout)
22+
23+
def test_diff_cli_no_args(self):
24+
expected_output = ''
25+
self.call_run(expected_output, [])
26+
27+
def test_diff_cli_simple(self):
28+
p = Path(".")
29+
res = list(p.glob("**/bin_file*"))
30+
res = [str(x) for x in res]
31+
32+
dump = Ansi.bold + res[1] + Ansi.reset
33+
dump += "\n000000: "
34+
dump += "30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66"
35+
dump += Ansi.delete + " " + Ansi.reset
36+
dump += "|0123456789abcdef|\n"
37+
38+
expected_output = dump
39+
self.call_run(expected_output, res)
40+
41+
def test_diff_cli_with_width_flag(self):
42+
p = Path('.')
43+
res = res = list(p.glob('**/bin_file*'))
44+
res = [str(x) for x in res]
45+
res += ['--width', '25']
46+
47+
dump = Ansi.bold + res[1] + Ansi.reset
48+
dump += "\n000000: "
49+
dump += "30 31 32 33 34 35 36 37"
50+
dump += "\n38 39 61 62 63 64 65"
51+
dump += "\n66"
52+
dump += Ansi.delete + " " + Ansi.reset
53+
dump += "|0123456789abcdef|\n"
54+
55+
expected_output = dump
56+
self.call_run(expected_output, res)
57+
58+
def test_diff_cli_with_bytes_flag(self):
59+
p = Path('.')
60+
res = res = list(p.glob('**/bin_file*'))
61+
res = [str(x) for x in res]
62+
res += ['--bytes', '6']
63+
64+
dump = Ansi.bold + res[1] + Ansi.reset
65+
dump += "\n000000: "
66+
dump += "30 31 32 33 34 35"
67+
dump += "\n000010: "
68+
dump += "36 37 38 39 61 62"
69+
dump += "\n000020: "
70+
dump += "63 64 65 66"
71+
dump += Ansi.delete + " " + Ansi.reset
72+
dump += '\n'
73+
print(dump)
74+
75+
expected_output = dump
76+
self.call_run(expected_output, res)
77+
78+
if __name__ == '__main__':
79+
unittest.main()

0 commit comments

Comments
 (0)