Skip to content

Commit 0adae95

Browse files
Merge pull request #5 from secondlife/sl-19314
SL-19314: Recast llsd serialization to write to a stream.
2 parents c4035fb + 2a19caf commit 0adae95

File tree

5 files changed

+266
-192
lines changed

5 files changed

+266
-192
lines changed

llsd/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
BINARY_HEADER, NOTATION_HEADER, XML_HEADER,
1212
LLSDBaseParser, LLSDParseError, LLSDSerializationError,
1313
LongType, UnicodeType, binary, undef, uri)
14-
from llsd.serde_binary import LLSDBinaryParser, format_binary, parse_binary, parse_binary_nohdr
15-
from llsd.serde_notation import LLSDNotationFormatter, LLSDNotationParser, format_notation, parse_notation, parse_notation_nohdr
16-
from llsd.serde_xml import LLSDXMLFormatter, LLSDXMLPrettyFormatter, format_pretty_xml, format_xml, parse_xml, parse_xml_nohdr
14+
from llsd.serde_binary import (LLSDBinaryParser, format_binary, parse_binary, parse_binary_nohdr,
15+
write_binary)
16+
from llsd.serde_notation import (LLSDNotationFormatter, write_notation, format_notation,
17+
LLSDNotationParser, parse_notation, parse_notation_nohdr)
18+
from llsd.serde_xml import (LLSDXMLFormatter, LLSDXMLPrettyFormatter,
19+
write_pretty_xml, write_xml, format_pretty_xml, format_xml,
20+
parse_xml, parse_xml_nohdr)
1721

1822

1923
def parse(something, mime_type = None):

llsd/base.py

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def __init__(self, thing=None):
4242
undef = _LLSD(None)
4343

4444

45+
# 'binary' only exists so that a Python 2 caller can distinguish binary data
46+
# from str data - since in Python 2, (bytes is str).
4547
if PY2:
4648
class binary(str):
4749
"Simple wrapper for llsd.binary data."
@@ -174,7 +176,7 @@ def _format_datestr(v):
174176
xml and notation serializations.
175177
"""
176178
if not isinstance(v, datetime.date) and not isinstance(v, datetime.datetime):
177-
raise LLSDParseError("invalid date string %s passed to date formatter" % v)
179+
raise LLSDSerializationError("invalid date string %s passed to date formatter" % v)
178180

179181
if not isinstance(v, datetime.datetime):
180182
v = datetime.datetime.combine(v, datetime.time(0))
@@ -322,31 +324,61 @@ class LLSDBaseFormatter(object):
322324
role of this base class is to provide self.type_map based on the methods
323325
defined in its subclass.
324326
"""
327+
__slots__ = ['stream', 'type_map']
328+
325329
def __init__(self):
326330
"Construct a new formatter dispatch table."
331+
self.stream = None
327332
self.type_map = {
328-
type(None): self.UNDEF,
329-
undef: self.UNDEF,
330-
bool: self.BOOLEAN,
331-
int: self.INTEGER,
332-
LongType: self.INTEGER,
333-
float: self.REAL,
334-
uuid.UUID: self.UUID,
335-
binary: self.BINARY,
336-
str: self.STRING,
337-
UnicodeType: self.STRING,
338-
newstr: self.STRING,
339-
uri: self.URI,
340-
datetime.datetime: self.DATE,
341-
datetime.date: self.DATE,
342-
list: self.ARRAY,
343-
tuple: self.ARRAY,
344-
types.GeneratorType: self.ARRAY,
345-
dict: self.MAP,
346-
_LLSD: self.LLSD,
333+
type(None): self._UNDEF,
334+
undef: self._UNDEF,
335+
bool: self._BOOLEAN,
336+
int: self._INTEGER,
337+
LongType: self._INTEGER,
338+
float: self._REAL,
339+
uuid.UUID: self._UUID,
340+
binary: self._BINARY,
341+
str: self._STRING,
342+
UnicodeType: self._STRING,
343+
newstr: self._STRING,
344+
uri: self._URI,
345+
datetime.datetime: self._DATE,
346+
datetime.date: self._DATE,
347+
list: self._ARRAY,
348+
tuple: self._ARRAY,
349+
types.GeneratorType: self._ARRAY,
350+
dict: self._MAP,
351+
_LLSD: self._LLSD,
347352
}
348353

349354

355+
def format(self, something):
356+
"""
357+
Pure Python implementation of the formatter.
358+
Format a python object according to subclass formatting.
359+
360+
:param something: A python object (typically a dict) to be serialized.
361+
:returns: A serialized bytes object.
362+
"""
363+
stream = io.BytesIO()
364+
self.write(stream, something)
365+
return stream.getvalue()
366+
367+
def write(self, stream, something):
368+
"""
369+
Serialize a python object to the passed binary 'stream' according to
370+
subclass formatting.
371+
372+
:param stream: A binary file-like object to which to serialize 'something'.
373+
:param something: A python object (typically a dict) to be serialized.
374+
"""
375+
self.stream = stream
376+
try:
377+
return self._write(something)
378+
finally:
379+
self.stream = None
380+
381+
350382
_X_ORD = ord(b'x')
351383
_BACKSLASH_ORD = ord(b'\\')
352384
_DECODE_BUFF_ALLOC_SIZE = 1024

llsd/serde_binary.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import calendar
22
import datetime
3+
import io
34
import struct
45
import uuid
56

@@ -152,74 +153,75 @@ def format_binary(something):
152153
:param something: a python object (typically a dict) to be serialized.
153154
:returns: Returns a LLSD binary formatted string.
154155
"""
155-
return BINARY_HEADER + b'\n' + _format_binary_recurse(something)
156+
stream = io.BytesIO()
157+
write_binary(stream, something)
158+
return stream.getvalue()
156159

157160

158-
def _format_binary_recurse(something):
159-
"Binary formatter workhorse."
160-
def _format_list(something):
161-
array_builder = []
162-
array_builder.append(b'[' + struct.pack('!i', len(something)))
163-
for item in something:
164-
array_builder.append(_format_binary_recurse(item))
165-
array_builder.append(b']')
166-
return b''.join(array_builder)
161+
def write_binary(stream, something):
162+
stream.write(b'<?llsd/binary?>\n')
163+
_write_binary_recurse(stream, something)
164+
167165

166+
def _write_binary_recurse(stream, something):
167+
"Binary formatter workhorse."
168168
if something is None:
169-
return b'!'
169+
stream.write(b'!')
170170
elif isinstance(something, _LLSD):
171-
return _format_binary_recurse(something.thing)
171+
_write_binary_recurse(stream, something.thing)
172172
elif isinstance(something, bool):
173-
if something:
174-
return b'1'
175-
else:
176-
return b'0'
173+
stream.write(b'1' if something else b'0')
177174
elif is_integer(something):
178175
try:
179-
return b'i' + struct.pack('!i', something)
176+
stream.writelines([b'i', struct.pack('!i', something)])
180177
except (OverflowError, struct.error) as exc:
181178
raise LLSDSerializationError(str(exc), something)
182179
elif isinstance(something, float):
183180
try:
184-
return b'r' + struct.pack('!d', something)
181+
stream.writelines([b'r', struct.pack('!d', something)])
185182
except SystemError as exc:
186183
raise LLSDSerializationError(str(exc), something)
187184
elif isinstance(something, uuid.UUID):
188-
return b'u' + something.bytes
185+
stream.writelines([b'u', something.bytes])
189186
elif isinstance(something, binary):
190-
return b'b' + struct.pack('!i', len(something)) + something
187+
stream.writelines([b'b', struct.pack('!i', len(something)), something])
191188
elif is_string(something):
192189
something = _str_to_bytes(something)
193-
return b's' + struct.pack('!i', len(something)) + something
190+
stream.writelines([b's', struct.pack('!i', len(something)), something])
194191
elif isinstance(something, uri):
195-
return b'l' + struct.pack('!i', len(something)) + something
192+
stream.writelines([b'l', struct.pack('!i', len(something)), something])
196193
elif isinstance(something, datetime.datetime):
197194
seconds_since_epoch = calendar.timegm(something.utctimetuple()) \
198195
+ something.microsecond // 1e6
199-
return b'd' + struct.pack('<d', seconds_since_epoch)
196+
stream.writelines([b'd', struct.pack('<d', seconds_since_epoch)])
200197
elif isinstance(something, datetime.date):
201198
seconds_since_epoch = calendar.timegm(something.timetuple())
202-
return b'd' + struct.pack('<d', seconds_since_epoch)
199+
stream.writelines([b'd', struct.pack('<d', seconds_since_epoch)])
203200
elif isinstance(something, (list, tuple)):
204-
return _format_list(something)
201+
_write_list(stream, something)
205202
elif isinstance(something, dict):
206-
map_builder = []
207-
map_builder.append(b'{' + struct.pack('!i', len(something)))
203+
stream.writelines([b'{', struct.pack('!i', len(something))])
208204
for key, value in something.items():
209205
key = _str_to_bytes(key)
210-
map_builder.append(b'k' + struct.pack('!i', len(key)) + key)
211-
map_builder.append(_format_binary_recurse(value))
212-
map_builder.append(b'}')
213-
return b''.join(map_builder)
206+
stream.writelines([b'k', struct.pack('!i', len(key)), key])
207+
_write_binary_recurse(stream, value)
208+
stream.write(b'}')
214209
else:
215210
try:
216-
return _format_list(list(something))
211+
return _write_list(stream, list(something))
217212
except TypeError:
218213
raise LLSDSerializationError(
219214
"Cannot serialize unknown type: %s (%s)" %
220215
(type(something), something))
221216

222217

218+
def _write_list(stream, something):
219+
stream.writelines([b'[', struct.pack('!i', len(something))])
220+
for item in something:
221+
_write_binary_recurse(stream, item)
222+
stream.write(b']')
223+
224+
223225
def parse_binary(something):
224226
"""
225227
This is the basic public interface for parsing llsd+binary.

llsd/serde_notation.py

Lines changed: 61 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def _parse_array(self):
267267
def _parse_uuid(self):
268268
"Parse a uuid."
269269
self._getc() # eat the beginning 'u'
270-
# see comment on LLSDNotationFormatter.UUID() re use of latin-1
270+
# see comment on LLSDNotationFormatter._UUID() re use of latin-1
271271
return uuid.UUID(hex=self._getc(36).decode('latin-1'))
272272

273273
def _parse_uri(self):
@@ -346,22 +346,17 @@ class LLSDNotationFormatter(LLSDBaseFormatter):
346346
347347
See http://wiki.secondlife.com/wiki/LLSD#Notation_Serialization
348348
"""
349-
__slots__ = []
350-
351-
def LLSD(self, v):
349+
def _LLSD(self, v):
352350
return self._generate(v.thing)
353-
def UNDEF(self, v):
354-
return b'!'
355-
def BOOLEAN(self, v):
356-
if v:
357-
return b'true'
358-
else:
359-
return b'false'
360-
def INTEGER(self, v):
361-
return B("i%d") % v
362-
def REAL(self, v):
363-
return B("r%r") % v
364-
def UUID(self, v):
351+
def _UNDEF(self, v):
352+
self.stream.write(b'!')
353+
def _BOOLEAN(self, v):
354+
self.stream.write(b'true' if v else b'false')
355+
def _INTEGER(self, v):
356+
self.stream.write(B("i%d") % v)
357+
def _REAL(self, v):
358+
self.stream.write(B("r%r") % v)
359+
def _UUID(self, v):
365360
# latin-1 is the byte-to-byte encoding, mapping \x00-\xFF ->
366361
# \u0000-\u00FF. It's also the fastest encoding, I believe, from
367362
# https://docs.python.org/3/library/codecs.html#encodings-and-unicode
@@ -370,24 +365,42 @@ def UUID(self, v):
370365
# error behavior in case someone passes an invalid hex string, with
371366
# things other than 0-9a-fA-F, so that they will fail in the UUID
372367
# decode, rather than with a UnicodeError.
373-
return B("u%s") % str(v).encode('latin-1')
374-
def BINARY(self, v):
375-
return b'b64"' + base64.b64encode(v).strip() + b'"'
376-
377-
def STRING(self, v):
378-
return B("'%s'") % _str_to_bytes(v).replace(b"\\", b"\\\\").replace(b"'", b"\\'")
379-
def URI(self, v):
380-
return B('l"%s"') % _str_to_bytes(v).replace(b"\\", b"\\\\").replace(b'"', b'\\"')
381-
def DATE(self, v):
382-
return B('d"%s"') % _format_datestr(v)
383-
def ARRAY(self, v):
384-
return B("[%s]") % b','.join([self._generate(item) for item in v])
385-
def MAP(self, v):
386-
return B("{%s}") % b','.join([B("'%s':%s") % (_str_to_bytes(UnicodeType(key)).replace(b"\\", b"\\\\").replace(b"'", b"\\'"), self._generate(value))
387-
for key, value in v.items()])
368+
self.stream.writelines([b"u", str(v).encode('latin-1')])
369+
def _BINARY(self, v):
370+
self.stream.writelines([b'b64"', base64.b64encode(v).strip(), b'"'])
371+
372+
def _STRING(self, v):
373+
self.stream.writelines([b"'", self._esc(v), b"'"])
374+
def _URI(self, v):
375+
self.stream.writelines([b'l"', self._esc(v, b'"'), b'"'])
376+
def _DATE(self, v):
377+
self.stream.writelines([b'd"', _format_datestr(v), b'"'])
378+
def _ARRAY(self, v):
379+
self.stream.write(b'[')
380+
delim = b''
381+
for item in v:
382+
self.stream.write(delim)
383+
self._generate(item)
384+
delim = b','
385+
self.stream.write(b']')
386+
def _MAP(self, v):
387+
self.stream.write(b'{')
388+
delim = b''
389+
for key, value in v.items():
390+
self.stream.writelines([delim, b"'", self._esc(UnicodeType(key)), b"':"])
391+
self._generate(value)
392+
delim = b','
393+
self.stream.write(b'}')
394+
395+
def _esc(self, data, quote=b"'"):
396+
return _str_to_bytes(data).replace(b"\\", b"\\\\").replace(quote, b'\\'+quote)
388397

389398
def _generate(self, something):
390-
"Generate notation from a single python object."
399+
"""
400+
Serialize a python object to self.stream as application/llsd+notation
401+
402+
:param something: a python object (typically a dict) to be serialized.
403+
"""
391404
t = type(something)
392405
handler = self.type_map.get(t)
393406
if handler:
@@ -396,19 +409,13 @@ def _generate(self, something):
396409
return self.type_map[_LLSD](something)
397410
else:
398411
try:
399-
return self.ARRAY(iter(something))
412+
return self._ARRAY(iter(something))
400413
except TypeError:
401414
raise LLSDSerializationError(
402415
"Cannot serialize unknown type: %s (%s)" % (t, something))
403416

404-
def format(self, something):
405-
"""
406-
Format a python object as application/llsd+notation
407-
408-
:param something: a python object (typically a dict) to be serialized.
409-
:returns: Returns a LLSD notation formatted string.
410-
"""
411-
return self._generate(something)
417+
# _write() method is an alias for _generate()
418+
_write = _generate
412419

413420

414421
def format_notation(something):
@@ -423,6 +430,19 @@ def format_notation(something):
423430
return LLSDNotationFormatter().format(something)
424431

425432

433+
def write_notation(stream, something):
434+
"""
435+
Serialize to passed binary 'stream' a python object 'something' as
436+
application/llsd+notation.
437+
438+
:param stream: a binary stream open for writing.
439+
:param something: a python object (typically a dict) to be serialized.
440+
441+
See http://wiki.secondlife.com/wiki/LLSD#Notation_Serialization
442+
"""
443+
return LLSDNotationFormatter().write(stream, something)
444+
445+
426446
def parse_notation(something):
427447
"""
428448
This is the basic public interface for parsing llsd+notation.

0 commit comments

Comments
 (0)