Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for post processing the payloads as they are decoded. #45

Merged
merged 2 commits into from
Jan 25, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cpyamf/codec.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ cdef class Codec(object):


cdef class Decoder(Codec):
cdef unsigned int depth

cdef object readDate(self)
cpdef object readString(self)
cdef object readObject(self)
Expand All @@ -83,10 +85,12 @@ cdef class Decoder(Codec):
cdef object readList(self)
cdef object readXML(self)

cdef object _readElement(self)
cpdef object readElement(self)
cdef object readConcreteElement(self, char t)

cpdef int send(self, data) except -1
cdef int finalise(self, object payload) except? -1


cdef class Encoder(Codec):
Expand Down
32 changes: 31 additions & 1 deletion cpyamf/codec.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ cdef class Decoder(Codec):
Base AMF decoder.
"""

def __cinit__(self):
self.depth = 0

cdef object readDate(self):
raise NotImplementedError

Expand All @@ -380,7 +383,7 @@ cdef class Decoder(Codec):
cdef object readXML(self):
raise NotImplementedError

cpdef object readElement(self):
cdef object _readElement(self):
"""
Reads an element from the data stream.
"""
Expand All @@ -399,6 +402,21 @@ cdef class Decoder(Codec):

raise

cpdef object readElement(self):
cdef object element

self.depth += 1

try:
element = self._readElement()
finally:
self.depth -= 1

if self.depth == 0:
self.finalise(element)

return element

cdef object readConcreteElement(self, char t):
"""
The workhorse function. Overridden in subclasses
Expand All @@ -424,6 +442,18 @@ cdef class Decoder(Codec):
def __iter__(self):
return self

cdef int finalise(self, object payload) except? -1:
"""
Finalise the payload.

This provides a useful hook to adapters to modify the payload that was
decoded.
"""
for c in pyamf.POST_DECODE_PROCESSORS:
c(payload, self.context.extra)

return 0


cdef class Encoder(Codec):
"""
Expand Down
19 changes: 19 additions & 0 deletions pyamf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
#: L{unregister_alias_type}
ALIAS_TYPES = {}

#: A list of callbacks to execute once a decode has been successful.
POST_DECODE_PROCESSORS = []

#: Specifies that objects are serialized using AMF for ActionScript 1.0
#: and 2.0 that were introduced in the Adobe Flash Player 6.
AMF0 = 0
Expand Down Expand Up @@ -917,6 +920,22 @@ def set_default_etree(etree):
return xml.set_default_interface(etree)


def add_post_decode_processor(func):
"""
Adds a function to be called when a payload has been successfully decoded.

This is useful for adapter as the last chance to modify the Python graph
before it enters user land.

@see: L{pyamf.codec.Decoder.finalise}
@since: 0.7.0
"""
if not python.callable(func):
raise TypeError('%r must be callable' % (func,))

POST_DECODE_PROCESSORS.append(func)


# setup some some standard class registrations and class loaders.
register_class(ASObject)
register_class_loader(flex_loader)
Expand Down
39 changes: 38 additions & 1 deletion pyamf/codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ class Decoder(_Codec):
@type strict: C{bool}
"""

def __init__(self, *args, **kwargs):
_Codec.__init__(self, *args, **kwargs)

self.__depth = 0

def send(self, data):
"""
Add data for the decoder to work on.
Expand All @@ -334,7 +339,20 @@ def next(self):
# all data was successfully decoded from the stream
raise StopIteration

def readElement(self):
def finalise(self, payload):
"""
Finalise the payload.

This provides a useful hook to adapters to modify the payload that was
decoded.

Note that this is an advanced feature and is NOT directly called by the
decoder.
"""
for c in pyamf.POST_DECODE_PROCESSORS:
c(payload, self.context.extra)

def _readElement(self):
"""
Reads an AMF3 element from the data stream.

Expand Down Expand Up @@ -366,6 +384,25 @@ def readElement(self):

raise

def readElement(self):
"""
Reads an AMF3 element from the data stream.

@raise DecodeError: The ActionScript type is unsupported.
@raise EOStream: No more data left to decode.
"""
self.__depth += 1

try:
element = self._readElement()
finally:
self.__depth -= 1

if self.__depth == 0:
self.finalise(element)

return element

def __iter__(self):
return self

Expand Down
9 changes: 0 additions & 9 deletions pyamf/remoting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,6 @@ def _read_args():
status = code
target = target[:0 - len(s)]

if logger:
logger.debug('Remoting target: %r' % (target,))

data_len = stream.read_ulong()
pos = stream.tell()

Expand Down Expand Up @@ -610,9 +607,6 @@ def decode(stream, strict=False, logger=None, timezone_offset=None):
if not isinstance(stream, util.BufferedByteStream):
stream = util.BufferedByteStream(stream)

if logger:
logger.debug('remoting.decode start')

msg = Envelope()
msg.amfVersion = stream.read_ushort()

Expand Down Expand Up @@ -654,9 +648,6 @@ def decode(stream, strict=False, logger=None, timezone_offset=None):
if strict and stream.remaining() > 0:
raise RuntimeError("Unable to fully consume the buffer")

if logger:
logger.debug('remoting.decode end')

return msg


Expand Down
30 changes: 30 additions & 0 deletions pyamf/tests/test_amf0.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,36 @@ def test_numerical_keys_mixed_array(self):

self.assertEqual(d, [{10: u'foobar'}])

def test_post_process(self):
"""
Ensure that postprocessing happens when data has been decoded.
"""
self.executed = False

post_procs = pyamf.POST_DECODE_PROCESSORS[:]

def restore_post_procs():
pyamf.POST_DECODE_PROCESSORS = post_procs

self.addCleanup(restore_post_procs)
pyamf.POST_DECODE_PROCESSORS = []

def postprocess(payload, context):
self.assertEqual(payload, u'foo')
self.assertEqual(context, {})

self.executed = True

pyamf.add_post_decode_processor(postprocess)

# setup complete
bytes = pyamf.encode(u'foo', encoding=pyamf.AMF0).getvalue()

self.decoder.send(bytes)
self.decoder.next()

self.assertTrue(self.executed)


class RecordSetTestCase(unittest.TestCase, EncoderMixIn, DecoderMixIn):
"""
Expand Down
30 changes: 30 additions & 0 deletions pyamf/tests/test_amf3.py
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,36 @@ def f(**kwargs):

f(**kwargs)

def test_post_process(self):
"""
Ensure that postprocessing happens when data has been decoded.
"""
self.executed = False

post_procs = pyamf.POST_DECODE_PROCESSORS[:]

def restore_post_procs():
pyamf.POST_DECODE_PROCESSORS = post_procs

self.addCleanup(restore_post_procs)
pyamf.POST_DECODE_PROCESSORS = []

def postprocess(payload, context):
self.assertEqual(payload, u'foo')
self.assertEqual(context, {})

self.executed = True

pyamf.add_post_decode_processor(postprocess)

# setup complete
bytes = pyamf.encode(u'foo', encoding=pyamf.AMF3).getvalue()

self.decoder.send(bytes)
self.decoder.next()

self.assertTrue(self.executed)


class ObjectEncodingTestCase(ClassCacheClearingTestCase, EncoderMixIn):
"""
Expand Down