Skip to content

Commit f6d8fc2

Browse files
committed
Eliminate code generation for Struct methods
Struct.to_binary, Struct.parse_value and Struct.parse_binary were doing code generation to produce and store a specialised method for each struct instance. Apparently (see #12) this was a necessary optimisation in the distant past, but it's almost certainly no longer needed, and it makes the code considerably harder to follow. On my machine the tests run slightly but noticeably faster with this change, but of course the tests are probably not a realistic sample for performance testing. This doesn't directly impact Python 3 support, but it simplifies things.
1 parent 6b3e1bd commit f6d8fc2

File tree

1 file changed

+73
-195
lines changed

1 file changed

+73
-195
lines changed

Xlib/protocol/rq.py

Lines changed: 73 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -959,157 +959,85 @@ def to_binary(self, *varargs, **keys):
959959
960960
"""
961961

962-
code = ''
963-
total_length = str(self.static_size)
964-
joins = []
965-
args = []
966-
defargs = []
967-
kwarg = 0
962+
# Emulate Python function argument handling with our field names
963+
names = [f.name for f in self.fields \
964+
if isinstance(f, ValueField) and f.name]
965+
field_args = dict(zip(names, varargs))
966+
if set(field_args).intersection(keys):
967+
dupes = ", ".join(set(field_args).intersection(keys))
968+
raise TypeError("%s arguments were passed both positionally and by keyword" % dupes)
969+
field_args.update(keys)
970+
for f in self.fields:
971+
if f.name and (f.name not in field_args):
972+
if f.default is None:
973+
raise TypeError("Missing required argument %s" % f.name)
974+
field_args[f.name] = f.default
975+
# /argument handling
968976

969977
# First pack all varfields so their lengths and formats are
970978
# available when we pack their static LengthFields and
971979
# FormatFields
972980

973-
i = 0
981+
total_length = self.static_size
982+
var_vals = {}
983+
lengths = {}
984+
formats = {}
985+
974986
for f in self.var_fields:
975987
if f.keyword_args:
976-
kwarg = 1
977-
kw = ', _keyword_args'
988+
v, l, fm = f.pack_value(field_args[f.name], keys)
978989
else:
979-
kw = ''
980-
981-
# Call pack_value method for each field, storing
982-
# the return values for later use
983-
code = code + (' _%(name)s, _%(name)s_length, _%(name)s_format'
984-
' = self.var_fields[%(fno)d].pack_value(%(name)s%(kw)s)\n'
985-
% { 'name': f.name,
986-
'fno': i,
987-
'kw': kw })
988-
989-
total_length = total_length + ' + len(_%s)' % f.name
990-
joins.append('_%s' % f.name)
990+
v, l, fm = f.pack_value(field_args[f.name])
991+
var_vals[f.name] = v
992+
lengths[f.name] = l
993+
formats[f.name] = fm
991994

992-
i = i + 1
995+
total_length += len(v)
993996

994997

995-
# Construct argument list for struct.pack call, packing all
996-
# static fields. First argument is the structcode, the
997-
# remaining are values.
998+
# Construct item list for struct.pack call, packing all static fields.
999+
pack_items = []
9981000

999-
pack_args = ['"%s"' % self.static_codes]
1000-
1001-
i = 0
10021001
for f in self.static_fields:
10031002
if isinstance(f, LengthField):
10041003

10051004
# If this is a total length field, insert
10061005
# the calculated field value here
10071006
if isinstance(f, TotalLengthField):
1008-
if self.var_fields:
1009-
pack_args.append('self.static_fields[%d].calc_length(%s)'
1010-
% (i, total_length))
1011-
else:
1012-
pack_args.append(str(f.calc_length(self.static_size)))
1007+
pack_items.append(f.calc_length(total_length))
10131008
else:
1014-
pack_args.append('self.static_fields[%d].calc_length(_%s_length)'
1015-
% (i, f.name))
1009+
pack_items.append(f.calc_length(lengths[f.name]))
10161010

10171011
# Format field, just insert the value we got previously
10181012
elif isinstance(f, FormatField):
1019-
pack_args.append('_%s_format' % f.name)
1013+
pack_items.append(formats[f.name])
10201014

10211015
# A constant field, insert its value directly
10221016
elif isinstance(f, ConstantField):
1023-
pack_args.append(str(f.value))
1017+
pack_items.append(f.value)
10241018

10251019
# Value fields
10261020
else:
10271021
if f.structvalues == 1:
1028-
10291022
# If there's a value check/convert function, call it
10301023
if f.check_value is not None:
1031-
pack_args.append('self.static_fields[%d].check_value(%s)'
1032-
% (i, f.name))
1033-
1024+
pack_items.append(f.check_value(field_args[f.name]))
10341025
# Else just use the argument as provided
10351026
else:
1036-
pack_args.append(f.name)
1027+
pack_items.append(field_args[f.name])
10371028

10381029
# Multivalue field. Handled like single valuefield,
10391030
# but the value are tuple unpacked into seperate arguments
1040-
# which are appended to pack_args
1031+
# which are appended to pack_items
10411032
else:
1042-
a = []
1043-
for j in range(f.structvalues):
1044-
a.append('_%s_%d' % (f.name, j))
1045-
10461033
if f.check_value is not None:
1047-
code = code + (' %s = self.static_fields[%d].check_value(%s)\n'
1048-
% (string.join(a, ', '), i, f.name))
1034+
pack_items.extend(f.check_value(field_args[f.name]))
10491035
else:
1050-
code = code + ' %s = %s\n' % (string.join(a, ', '), f.name)
1051-
1052-
pack_args = pack_args + a
1053-
1054-
# Add field to argument list
1055-
if f.name:
1056-
if f.default is None:
1057-
args.append(f.name)
1058-
else:
1059-
defargs.append('%s = %s' % (f.name, repr(f.default)))
1060-
1061-
i = i + 1
1062-
1063-
1064-
# Construct call to struct.pack
1065-
pack = 'struct.pack(%s)' % ', '.join(pack_args)
1066-
1067-
# If there are any varfields, we append the packed strings to build
1068-
# the resulting binary value
1069-
if self.var_fields:
1070-
code = code + ' return %s + %s\n' % (pack, ' + '.join(joins))
1071-
1072-
# If there's only static fields, return the packed value
1073-
else:
1074-
code = code + ' return %s\n' % pack
1075-
1076-
# Add all varsize fields to argument list. We do it here
1077-
# to ensure that they appear after the static fields.
1078-
for f in self.var_fields:
1079-
if f.name:
1080-
if f.default is None:
1081-
args.append(f.name)
1082-
else:
1083-
defargs.append('%s = %s' % (f.name, repr(f.default)))
1084-
1085-
args = args + defargs
1086-
if kwarg:
1087-
args.append('**_keyword_args')
1088-
1089-
# Add function header
1090-
code = 'def to_binary(self, %s):\n' % ', '.join(args) + code
1091-
1092-
# self._pack_code = code
1093-
1094-
# print
1095-
# print code
1096-
# print
1097-
1098-
# Finally, compile function by evaluating it. This will store
1099-
# the function in the local variable to_binary, thanks to the
1100-
# def: line. Convert it into a instance metod bound to self,
1101-
# and store it in self.
1102-
1103-
# Unfortunately, this creates a circular reference. However,
1104-
# Structs are not really created dynamically so the potential
1105-
# memory leak isn't that serious. Besides, Python 2.0 has
1106-
# real garbage collect.
1107-
ns = {'struct': struct}
1108-
exec(code, ns)
1109-
self.to_binary = _method(ns['to_binary'], self)
1036+
pack_items.extend(field_args[f.name])
11101037

1111-
# Finally call it manually
1112-
return self.to_binary(*varargs, **keys)
1038+
static_part = struct.pack(self.static_codes, *pack_items)
1039+
var_parts = [var_vals[f.name] for f in self.var_fields]
1040+
return static_part + b''.join(var_parts)
11131041

11141042

11151043
def pack_value(self, value):
@@ -1120,9 +1048,9 @@ def pack_value(self, value):
11201048
11211049
"""
11221050

1123-
if type(value) is types.TupleType:
1051+
if type(value) is tuple:
11241052
return self.to_binary(*value)
1125-
elif type(value) is types.DictionaryType:
1053+
elif type(value) is dict:
11261054
return self.to_binary(**value)
11271055
elif isinstance(value, DictWrapper):
11281056
return self.to_binary(**value._data)
@@ -1136,12 +1064,8 @@ def parse_value(self, val, display, rawdict = 0):
11361064
Struct objects with no var_fields into Python values.
11371065
11381066
"""
1139-
1140-
code = ('def parse_value(self, val, display, rawdict = 0):\n'
1141-
' ret = {}\n')
1142-
1067+
ret = {}
11431068
vno = 0
1144-
fno = 0
11451069
for f in self.static_fields:
11461070
# Fields without names should be ignored, and there should
11471071
# not be any length or format fields if this function
@@ -1160,42 +1084,22 @@ def parse_value(self, val, display, rawdict = 0):
11601084

11611085
# Value fields
11621086
else:
1163-
1164-
# Get the index or range in val representing this field.
1165-
1166-
if f.structvalues == 1:
1167-
vrange = str(vno)
1168-
else:
1169-
vrange = '%d:%d' % (vno, vno + f.structvalues)
1170-
11711087
# If this field has a parse_value method, call it, otherwise
11721088
# use the unpacked value as is.
1173-
1174-
if f.parse_value is None:
1175-
code = code + ' ret["%s"] = val[%s]\n' % (f.name, vrange)
1089+
if f.structvalues == 1:
1090+
field_val = val[vno]
11761091
else:
1177-
code = code + (' ret["%s"] = self.static_fields[%d].'
1178-
'parse_value(val[%s], display)\n'
1179-
% (f.name, fno, vrange))
1180-
1181-
fno = fno + 1
1182-
vno = vno + f.structvalues
1092+
field_val = val[vno:vno+f.structvalues]
11831093

1184-
code = code + ' if not rawdict: return DictWrapper(ret)\n'
1185-
code = code + ' return ret\n'
1094+
if f.parse_value is not None:
1095+
field_val = f.parse_value(field_val, display, rawdict=rawdict)
1096+
ret[f.name] = field_val
11861097

1187-
# print
1188-
# print code
1189-
# print
1190-
1191-
# Finally, compile function as for to_binary.
1192-
ns = {'DictWrapper': DictWrapper}
1193-
exec(code, ns)
1194-
self.parse_value = types.MethodType(ns['parse_value'], self, self.__class__)
1195-
1196-
# Call it manually
1197-
return self.parse_value(val, display, rawdict)
1098+
vno = vno + f.structvalues
11981099

1100+
if not rawdict:
1101+
return DictWrapper(ret)
1102+
return ret
11991103

12001104
def parse_binary(self, data, display, rawdict = 0):
12011105

@@ -1216,17 +1120,12 @@ def parse_binary(self, data, display, rawdict = 0):
12161120
REMDATA are the remaining binary data, unused by the Struct object.
12171121
12181122
"""
1219-
1220-
code = ('def parse_binary(self, data, display, rawdict = 0):\n'
1221-
' ret = {}\n'
1222-
' val = struct.unpack("%s", data[:%d])\n'
1223-
% (self.static_codes, self.static_size))
1224-
1123+
ret = {}
1124+
val = struct.unpack(self.static_codes, data[:self.static_size])
12251125
lengths = {}
12261126
formats = {}
12271127

12281128
vno = 0
1229-
fno = 0
12301129
for f in self.static_fields:
12311130

12321131
# Fields without name should be ignored. This is typically
@@ -1242,63 +1141,42 @@ def parse_binary(self, data, display, rawdict = 0):
12421141
f_names = [f.name]
12431142
if f.other_fields:
12441143
f_names.extend(f.other_fields)
1144+
field_val = val[vno]
1145+
if f.parse_value is not None:
1146+
field_val = f.parse_value(field_val, display)
12451147
for f_name in f_names:
1246-
if f.parse_value is None:
1247-
lengths[f_name] = 'val[%d]' % vno
1248-
else:
1249-
lengths[f_name] = ('self.static_fields[%d].'
1250-
'parse_value(val[%d], display)'
1251-
% (fno, vno))
1148+
lengths[f_name] = field_val
12521149

12531150
elif isinstance(f, FormatField):
1254-
formats[f.name] = 'val[%d]' % vno
1151+
formats[f.name] = val[vno]
12551152

12561153
# Treat value fields the same was as in parse_value.
12571154
else:
12581155
if f.structvalues == 1:
1259-
vrange = str(vno)
1156+
field_val = val[vno]
12601157
else:
1261-
vrange = '%d:%d' % (vno, vno + f.structvalues)
1158+
field_val = val[vno:vno+f.structvalues]
12621159

1263-
if f.parse_value is None:
1264-
code = code + ' ret["%s"] = val[%s]\n' % (f.name, vrange)
1265-
else:
1266-
code = code + (' ret["%s"] = self.static_fields[%d].'
1267-
'parse_value(val[%s], display)\n'
1268-
% (f.name, fno, vrange))
1160+
if f.parse_value is not None:
1161+
field_val = f.parse_value(field_val, display)
1162+
ret[f.name] = field_val
12691163

1270-
fno = fno + 1
12711164
vno = vno + f.structvalues
12721165

1273-
code = code + ' data = data[%d:]\n' % self.static_size
1166+
data = data[self.static_size:]
12741167

12751168
# Call parse_binary_value for each var_field, passing the
12761169
# length and format values from the unpacked val.
12771170

1278-
fno = 0
12791171
for f in self.var_fields:
1280-
code = code + (' ret["%s"], data = '
1281-
'self.var_fields[%d].parse_binary_value'
1282-
'(data, display, %s, %s)\n'
1283-
% (f.name, fno,
1284-
lengths.get(f.name, 'None'),
1285-
formats.get(f.name, 'None')))
1286-
fno = fno + 1
1287-
1288-
code = code + ' if not rawdict: ret = DictWrapper(ret)\n'
1289-
code = code + ' return ret, data\n'
1290-
1291-
# print
1292-
# print code
1293-
# print
1294-
1295-
# Finally, compile function as for to_binary.
1296-
ns = {'struct': struct, 'DictWrapper': DictWrapper}
1297-
exec(code, ns)
1298-
self.parse_binary = _method(ns['parse_binary'], self)
1299-
1300-
# Call it manually
1301-
return self.parse_binary(data, display, rawdict)
1172+
ret[f.name], data = f.parse_binary_value(data, display,
1173+
lengths.get(f.name, None),
1174+
formats.get(f.name, None),
1175+
)
1176+
1177+
if not rawdict:
1178+
ret = DictWrapper(ret)
1179+
return ret, data
13021180

13031181

13041182
class TextElements8(ValueField):

0 commit comments

Comments
 (0)