Package pyamf

[hide private]
[frames] | no frames]

# Source Code for Package pyamf

       1  # Copyright (c) 2007-2009 The PyAMF Project.
       2  # See LICENSE for details.
       3
       4  """
       5  B{PyAMF} provides B{A}ction B{M}essage B{F}ormat
       6  (U{AMF<http://en.wikipedia.org/wiki/Action_Message_Format>}) support for
       7  Python that is compatible with the
       8  U{Flash Player<http://en.wikipedia.org/wiki/Flash_Player>}.
       9
      10  @copyright: Copyright (c) 2007-2009 The PyAMF Project. All Rights Reserved.
      11  @contact: U{dev@pyamf.org<mailto:dev@pyamf.org>}
      12  @see: U{http://pyamf.org}
      13
      14  @since: October 2007
      15  @version: 0.4.1
      16  @status: Production/Stable
      17  """
      18
      19  import types
      20
      21  from pyamf import util
      22  from pyamf.adapters import register_adapters
      23
      24  __all__ = [
      25      'register_class',
      26      'register_class_loader',
      27      'encode',
      28      'decode',
      29      '__version__'
      30  ]
      31
      32  #: PyAMF version number.
      33  __version__ = (0, 4, 1)
      34
      35  #: Class mapping support.
      36  CLASS_CACHE = {}
      37  #: Class loaders.
      38  CLASS_LOADERS = []
      39  #: Custom type map.
      40  TYPE_MAP = {}
      41  #: Maps error classes to string codes.
      42  ERROR_CLASS_MAP = {}
      43  #: Alias mapping support
      44  ALIAS_TYPES = {}
      45
      46  #: Specifies that objects are serialized using AMF for ActionScript 1.0 and 2.0.
      47  AMF0 = 0
      48  #: Specifies that objects are serialized using AMF for ActionScript 3.0.
      49  AMF3 = 3
      50  #: Supported AMF encoding types.
      51  ENCODING_TYPES = (AMF0, AMF3)
      52
      53 -class ClientTypes:
      54      """
      55      Typecodes used to identify AMF clients and servers.
      56
      57      @see: U{Flash Player on WikiPedia (external)
      58      <http://en.wikipedia.org/wiki/Flash_Player>}
      59      @see: U{Flash Media Server on WikiPedia (external)
      60      <http://en.wikipedia.org/wiki/Adobe_Flash_Media_Server>}
      61      """
      62      #: Specifies a Flash Player 6.0 - 8.0 client.
      63      Flash6   = 0
      64      #: Specifies a FlashCom / Flash Media Server client.
      65      FlashCom = 1
      66      #: Specifies a Flash Player 9.0 client.
      67      Flash9   = 3
      68
      69  #: List of AMF client typecodes.
      70  CLIENT_TYPES = []
      71
      72  for x in ClientTypes.__dict__:
      73      if not x.startswith('_'):
      74          CLIENT_TYPES.append(ClientTypes.__dict__[x])
      75  del x
      76
      77 -class UndefinedType(object):
      78 -    def __repr__(self):
      79          return 'pyamf.Undefined'
      80
      81  #: Represents the C{undefined} value in a Flash client.
      82  Undefined = UndefinedType()
      83
      84 -class BaseError(Exception):
      85      """
      86      Base AMF Error.
      87
      88      All AMF related errors should be subclassed from this class.
      89      """
      90
      91 -class DecodeError(BaseError):
      92      """
      93      Raised if there is an error in decoding an AMF data stream.
      94      """
      95
      96 -class EOStream(BaseError):
      97      """
      98      Raised if the data stream has come to a natural end.
      99      """
     100
     101 -class ReferenceError(BaseError):
     102      """
     103      Raised if an AMF data stream refers to a non-existent object
     104      or string reference.
     105      """
     106
     107 -class EncodeError(BaseError):
     108      """
     109      Raised if the element could not be encoded to the stream.
     110
     111      @bug: See U{Docuverse blog (external)
     112      <http://www.docuverse.com/blog/donpark/2007/05/14/flash-9-amf3-bug>}
     113      for more info about the empty key string array bug.
     114      """
     115
     116 -class UnknownClassAlias(BaseError):
     117      """
     118      Raised if the AMF stream specifies a class that does not
     119      have an alias.
     120
     121      @see: L{register_class}
     122      """
     123
     124 -class BaseContext(object):
     125      """
     126      I hold the AMF context for en/decoding streams.
     127      """
     128
     129 -    def __init__(self):
     130          self.objects = util.IndexedCollection()
     131          self.clear()
     132
     133 -    def clear(self):
     134          """
     135          Completely clears the context.
     136          """
     137          self.objects.clear()
     138          self.class_aliases = {}
     139
     140 -    def reset(self):
     141          """
     142          Resets the context. This is subtly different to the
     143          L{BaseContext.clear} method, which is a hard delete of the context.
     144          This method is mainly used by the L{remoting api<pyamf.remoting>} to
     145          handle context clearing between requests.
     146          """
     147          self.objects.clear()
     148          self.class_aliases = {}
     149
     150 -    def getObject(self, ref):
     151          """
     152          Gets an object based on a reference.
     153
     154          @raise ReferenceError: Unknown object reference.
     155          """
     156          try:
     157              return self.objects.getByReference(ref)
     158          except ReferenceError:
     159              raise ReferenceError("Unknown object reference %r" % (ref,))
     160
     161 -    def getObjectReference(self, obj):
     162          """
     163          Gets a reference for an object.
     164
     165          @raise ReferenceError: Object not a valid reference,
     166          """
     167          try:
     168              return self.objects.getReferenceTo(obj)
     169          except ReferenceError:
     170              raise ReferenceError("Object %r not a valid reference" % (obj,))
     171
     172 -    def addObject(self, obj):
     173          """
     174          Adds a reference to C{obj}.
     175
     176          @type obj: C{mixed}
     177          @param obj: The object to add to the context.
     178          @rtype: C{int}
     179          @return: Reference to C{obj}.
     180          """
     181          return self.objects.append(obj)
     182
     183 -    def getClassAlias(self, klass):
     184          """
     185          Gets a class alias based on the supplied C{klass}.
     186          """
     187          if klass not in self.class_aliases.keys():
     188              try:
     189                  self.class_aliases[klass] = get_class_alias(klass)
     190              except UnknownClassAlias:
     191                  # no alias has been found yet .. check subclasses
     192                  alias = util.get_class_alias(klass)
     193
     194                  if alias is not None:
     195                      self.class_aliases[klass] = alias(klass, None)
     196                  else:
     197                      self.class_aliases[klass] = None
     198
     199          return self.class_aliases[klass]
     200
     201 -    def __copy__(self):
     202          raise NotImplementedError
     203
     204 -class ASObject(dict):
     205      """
     206      This class represents a Flash Actionscript Object (typed or untyped).
     207
     208      I supply a C{__builtin__.dict} interface to support C{get}/C{setattr}
     209      calls.
     210
     211      @raise AttributeError: Unknown attribute.
     212      """
     213
     214 -    def __init__(self, *args, **kwargs):
     215          dict.__init__(self, *args, **kwargs)
     216
     217 -    def __getattr__(self, k):
     218          try:
     219              return self[k]
     220          except KeyError:
     221              raise AttributeError('Unknown attribute \'%s\'' % (k,))
     222
     223 -    def __setattr__(self, k, v):
     224          self[k] = v
     225
     226 -    def __repr__(self):
     227          return dict.__repr__(self)
     228
     229 -    def __hash__(self):
     230          return id(self)
     231
     232 -class MixedArray(dict):
     233      """
     234      Used to be able to specify the C{mixedarray} type.
     235      """
     236
     237 -class ClassMetaData(list):
     238      """
     239      I hold a list of tags relating to the class. The idea behind this is
     240      to emulate the metadata tags you can supply to ActionScript,
     241      e.g. C{static}/C{dynamic}.
     242      """
     243
     244      _allowed_tags = (
     245          ('static', 'dynamic', 'external'),
     246          ('amf3', 'amf0'),
     247          ('anonymous',),
     248      )
     249
     250 -    def __init__(self, *args):
     251          if len(args) == 1 and hasattr(args[0], '__iter__'):
     252              for x in args[0]:
     253                  self.append(x)
     254          else:
     255              for x in args:
     256                  self.append(x)
     257
     258 -    def _is_tag_allowed(self, x):
     259          for y in self._allowed_tags:
     260              if isinstance(y, (types.ListType, types.TupleType)):
     261                  if x in y:
     262                      return (True, y)
     263              else:
     264                  if x == y:
     265                      return (True, None)
     266
     267          return (False, None)
     268
     269 -    def append(self, x):
     270          """
     271          Adds a tag to the metadata.
     272
     273          @param x: Tag.
     274          @raise ValueError: Unknown tag.
     275          """
     276          x = str(x).lower()
     277
     278          allowed, tags = self._is_tag_allowed(x)
     279
     280          if not allowed:
     281              raise ValueError("Unknown tag %s" % (x,))
     282
     283          if tags is not None:
     284              # check to see if a tag in the list is about to be clobbered if so,
     285              # raise a warning
     286              for y in tags:
     287                  if y not in self:
     288                      continue
     289
     290                  if x != y:
     291                      import warnings
     292
     293                      warnings.warn(
     294                          "Previously defined tag %s superceded by %s" % (y, x))
     295
     296                  list.pop(self, self.index(y))
     297                  break
     298
     299          list.append(self, x)
     300
     301 -    def __contains__(self, other):
     302          return list.__contains__(self, str(other).lower())
     303
     304      # TODO nick: deal with slices
     305
     306 -class ClassAlias(object):
     307      """
     308      Class alias.
     309
     310      All classes are initially set to a dynamic state.
     311
     312      @ivar attrs: A list of attributes to encode for this class.
     313      @type attrs: C{list}
     314      @ivar metadata: A list of metadata tags similar to ActionScript tags.
     315      @type metadata: C{list}
     316      """
     317
     318 -    def __init__(self, klass, alias, attrs=None, attr_func=None, metadata=[]):
     319          """
     320          @type klass: C{class}
     321          @param klass: The class to alias.
     322          @type alias: C{str}
     323          @param alias: The alias to the class e.g. C{org.example.Person}. If the
     324              value of this is C{None}, then it is worked out based on the C{klass}.
     325              The anonymous tag is also added to the class.
     326          @type attrs: A list of attributes to encode for this class.
     327          @param attrs: C{list}
     328          @type metadata: A list of metadata tags similar to ActionScript tags.
     329          @param metadata: C{list}
     330
     331          @raise TypeError: The C{klass} must be a class type.
     332          @raise TypeError: The C{attr_func} must be callable.
     333          @raise TypeError: C{__readamf__} must be callable.
     334          @raise TypeError: C{__writeamf__} must be callable.
     335          @raise AttributeError: An externalised class was specified, but no
     336              C{__readamf__} attribute was found.
     337          @raise AttributeError: An externalised class was specified, but no
     338              C{__writeamf__} attribute was found.
     339          @raise ValueError: The C{attrs} keyword must be specified for static
     340              classes.
     341          """
     342          if not isinstance(klass, (type, types.ClassType)):
     343              raise TypeError("klass must be a class type")
     344
     345          self.checkClass(klass)
     346
     347          self.metadata = ClassMetaData(metadata)
     348
     349          if alias is None:
     350              self.metadata.append('anonymous')
     351              alias = "%s.%s" % (klass.__module__, klass.__name__,)
     352
     353          self.klass = klass
     354          self.alias = alias
     355          self.attr_func = attr_func
     356          self.attrs = attrs
     357
     358          if 'external' in self.metadata:
     359              # class is declared as external, lets check
     360              if not hasattr(klass, '__readamf__'):
     361                  raise AttributeError("An externalised class was specified, but"
     362                      " no __readamf__ attribute was found for class %s" % (
     363                          klass.__name__))
     364
     365              if not hasattr(klass, '__writeamf__'):
     366                  raise AttributeError("An externalised class was specified, but"
     367                      " no __writeamf__ attribute was found for class %s" % (
     368                          klass.__name__))
     369
     370              if not isinstance(klass.__readamf__, types.UnboundMethodType):
     371                  raise TypeError("%s.__readamf__ must be callable" % (
     372                      klass.__name__))
     373
     374              if not isinstance(klass.__writeamf__, types.UnboundMethodType):
     375                  raise TypeError("%s.__writeamf__ must be callable" % (
     376                      klass.__name__))
     377
     378          if 'dynamic' in self.metadata:
     379              if attr_func is not None and not callable(attr_func):
     380                  raise TypeError("attr_func must be callable")
     381
     382          if 'static' in self.metadata:
     383              if attrs is None:
     384                  raise ValueError("attrs keyword must be specified for static classes")
     385
     386 -    def __str__(self):
     387          return self.alias
     388
     389 -    def __repr__(self):
     390          return '<ClassAlias alias=%s klass=%s @ %s>' % (
     391              self.alias, self.klass, id(self))
     392
     393 -    def __eq__(self, other):
     394          if isinstance(other, basestring):
     395              return self.alias == other
     396          elif isinstance(other, self.__class__):
     397              return self.klass == other.klass
     398          elif isinstance(other, (type, types.ClassType)):
     399              return self.klass == other
     400          else:
     401              return False
     402
     403 -    def __hash__(self):
     404          return id(self)
     405
     406 -    def checkClass(kls, klass):
     407          """
     408          This function is used to check the class being aliased to fits certain
     409          criteria. The default is to check that the __init__ constructor does
     410          not pass in arguments.
     411
     412          @since: 0.4
     413          @raise TypeError: C{__init__} doesn't support additional arguments
     414          """
     415          # Check that the constructor of the class doesn't require any additonal
     416          # arguments.
     417          if not (hasattr(klass, '__init__') and hasattr(klass.__init__, 'im_func')):
     418              return
     419
     420          klass_func = klass.__init__.im_func
     421
     422          # built-in classes don't have func_code
     423          if hasattr(klass_func, 'func_code') and (
     424             klass_func.func_code.co_argcount - len(klass_func.func_defaults or []) > 1):
     425              args = list(klass_func.func_code.co_varnames)
     426              values = list(klass_func.func_defaults or [])
     427
     428              if not values:
     429                  sign = "%s.__init__(%s)" % (klass.__name__, ", ".join(args))
     430              else:
     431                  named_args = zip(args[len(args) - len(values):], values)
     432                  sign = "%s.__init__(%s, %s)" % (
     433                      klass.__name__,
     434                      ", ".join(args[:0-len(values)]),
     435                      ", ".join(map(lambda x: "%s=%s" % (x,), named_args)))
     436
     437              raise TypeError("__init__ doesn't support additional arguments: %s"
     438                  % sign)
     439
     440      checkClass = classmethod(checkClass)
     441
     442 -    def _getAttrs(self, obj, static_attrs=None, dynamic_attrs=None, traverse=True):
     443          if static_attrs is None:
     444              static_attrs = []
     445
     446          if dynamic_attrs is None:
     447              dynamic_attrs = []
     448
     449          modified_attrs = False
     450
     451          if self.attrs is not None:
     452              modified_attrs = True
     453              static_attrs.extend(self.attrs)
     454          elif traverse is True and hasattr(obj, '__slots__'):
     455              modified_attrs = True
     456              static_attrs.extend(obj.__slots__)
     457
     458          if self.attr_func is not None:
     459              modified_attrs = True
     460              extra_attrs = self.attr_func(obj)
     461
     462              dynamic_attrs.extend([key for key in extra_attrs if key not in static_attrs])
     463
     464          if traverse is True:
     465              for base in util.get_mro(obj.__class__):
     466                  try:
     467                      alias = get_class_alias(base)
     468                  except UnknownClassAlias:
     469                      continue
     470
     471                  x, y = alias._getAttrs(obj, static_attrs, dynamic_attrs, False)
     472
     473                  if x is not None:
     474                      static_attrs.extend(x)
     475                      modified_attrs = True
     476
     477                  if y is not None:
     478                      dynamic_attrs.extend(y)
     479                      modified_attrs = True
     480
     481          if modified_attrs is False:
     482              return None, None
     483
     484          sa = []
     485          da = []
     486
     487          for x in static_attrs:
     488              if x not in sa:
     489                  sa.append(x)
     490
     491          for x in dynamic_attrs:
     492              if x not in da:
     493                  da.append(x)
     494
     495          return (sa, da)
     496
     497 -    def getAttrs(self, obj, codec=None):
     498          """
     499          Returns a tuple of lists, static and dynamic attrs to encode.
     500
     501          @param codec: An optional argument that will contain the en/decoder
     502              instance calling this function.
     503          """
     504          return self._getAttrs(obj)
     505
     506 -    def getAttributes(self, obj, codec=None):
     507          """
     508          Returns a collection of attributes for an object.
     509          Returns a C{tuple} containing a dict of static and dynamic attributes
     510
     511          @param codec: An optional argument that will contain the en/decoder
     512              instance calling this function.
     513          """
     514          dynamic_attrs = {}
     515          static_attrs = {}
     516          static_attr_names, dynamic_attr_names = self.getAttrs(obj, codec=codec)
     517
     518          if static_attr_names is None and dynamic_attr_names is None:
     519              dynamic_attrs = util.get_attrs(obj)
     520
     521          if static_attr_names is not None:
     522              for attr in static_attr_names:
     523                  if hasattr(obj, attr):
     524                      static_attrs[attr] = getattr(obj, attr)
     525                  else:
     526                      static_attrs[attr] = Undefined
     527
     528          if dynamic_attr_names is not None:
     529              for attr in dynamic_attr_names:
     530                  if attr in static_attrs:
     531                      continue
     532
     533                  if hasattr(obj, attr):
     534                      dynamic_attrs[attr] = getattr(obj, attr)
     535
     536          return (static_attrs, dynamic_attrs)
     537
     538 -    def applyAttributes(self, obj, attrs, codec=None):
     539          """
     540          Applies the collection of attributes C{attrs} to aliased object C{obj}.
     541          It is mainly used when reading aliased objects from an AMF byte stream.
     542
     543          @param codec: An optional argument that will contain the en/decoder
     544              instance calling this function.
     545          """
     546          if 'static' in self.metadata:
     547              s, d = self.getAttrs(obj, codec=codec)
     548
     549              if s is not None:
     550                  for k in attrs.keys():
     551                      if k not in s:
     552                          del attrs[k]
     553
     554          util.set_attrs(obj, attrs)
     555
     556 -    def createInstance(self, codec=None, *args, **kwargs):
     557          """
     558          Creates an instance of the klass.
     559
     560          @return: Instance of C{self.klass}.
     561          """
     562          return self.klass(*args, **kwargs)
     563
     564 -class TypedObject(dict):
     565      """
     566      This class is used when a strongly typed object is decoded but there is no
     567      registered class to apply it to.
     568
     569      This object can only be used for 'simple' streams - i.e. not externalized
     570      data. If encountered, a L{DecodeError} will be raised.
     571
     572      @ivar alias: The alias of the typed object.
     573      @type alias: C{unicode}
     574
     575      @since: 0.4
     576      """
     577
     578 -    def __init__(self, alias):
     579          dict.__init__(self)
     580
     581          self.alias = alias
     582
     583 -    def __readamf__(self, o):
     584          raise DecodeError('Unable to decode an externalised stream.\n\nThe '
     585              'class alias \'%s\' was found and because strict mode is False an'
     586              ' attempt was made to decode the object automatically. To decode '
     587              'this stream, a registered class with the alias and a '
     588              'corresponding __readamf__ method will be required.' % (
     589                  self.alias,))
     590
     591 -    def __writeamf__(self, o):
     592          raise EncodeError('Unable to encode an externalised stream.\n\nThe '
     593              'class alias \'%s\' was found and because strict mode is False an'
     594              'attempt was made to encode the object automatically. To encode '
     595              'this stream, a registered class with the alias and a '
     596              'corresponding __readamf__ method will be required.' % (
     597                  self.alias,))
     598
     599 -class TypedObjectClassAlias(ClassAlias):
     600      """
     601      @since: 0.4
     602      """
     603
     604 -    def createInstance(self, codec=None):
     605          return TypedObject(self.alias)
     606
     607 -    def checkClass(kls, klass):
     608          pass
     609
     610 -class BaseDecoder(object):
     611      """
     612      Base AMF decoder.
     613
     614      @ivar context_class: The context for the decoding.
     615      @type context_class: An instance of C{BaseDecoder.context_class}
     616      @ivar type_map:
     617      @type type_map: C{list}
     618      @ivar stream: The underlying data stream.
     619      @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
     620      @ivar strict: Defines how strict the decoding should be. For the time
     621          being this relates to typed objects in the stream that do not have a
     622          registered alias. Introduced in 0.4.
     623      @type strict: C{bool}
     624      """
     625
     626      context_class = BaseContext
     627      type_map = {}
     628
     629 -    def __init__(self, data=None, context=None, strict=False):
     630          """
     631          @type   data: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
     632          @param  data: Data stream.
     633          @type   context: L{Context<pyamf.amf0.Context>}
     634          @param  context: Context.
     635          @raise TypeError: The C{context} parameter must be of
     636          type L{Context<pyamf.amf0.Context>}.
     637          """
     638          # coerce data to BufferedByteStream
     639          if isinstance(data, util.BufferedByteStream):
     640              self.stream = data
     641          else:
     642              self.stream = util.BufferedByteStream(data)
     643
     644          if context == None:
     645              self.context = self.context_class()
     646          elif isinstance(context, self.context_class):
     647              self.context = context
     648          else:
     649              raise TypeError("context must be of type %s.%s" % (
     650                  self.context_class.__module__, self.context_class.__name__))
     651
     652          self.strict = strict
     653
     654 -    def readType(self):
     655          """
     656          @raise NotImplementedError: Override in a subclass.
     657          """
     658          raise NotImplementedError
     659
     660 -    def readElement(self):
     661          """
     662          Reads an AMF3 element from the data stream.
     663
     664          @raise DecodeError: The ActionScript type is unsupported.
     665          @raise EOStream: No more data left to decode.
     666          """
     667          try:
     668              type = self.readType()
     669          except EOFError:
     670              raise EOStream
     671
     672          try:
     673              func = getattr(self, self.type_map[type])
     674          except KeyError:
     675              raise DecodeError("Unsupported ActionScript type 0x%02x" % (type,))
     676
     677          return func()
     678
     679 -    def __iter__(self):
     680          """
     681          @raise StopIteration:
     682          """
     683          try:
     684              while 1:
     685                  yield self.readElement()
     686          except EOFError:
     687              raise StopIteration
     688
     689 -class CustomTypeFunc(object):
     690      """
     691      Custom type mappings.
     692      """
     693 -    def __init__(self, encoder, func):
     694          self.encoder = encoder
     695          self.func = func
     696
     697 -    def __call__(self, data):
     698          self.encoder.writeElement(self.func(data, encoder=self.encoder))
     699
     700 -class BaseEncoder(object):
     701      """
     702      Base AMF encoder.
     703
     704      @ivar type_map: A list of types -> functions. The types is a list of
     705          possible instances or functions to call (that return a C{bool}) to
     706          determine the correct function to call to encode the data.
     707      @type type_map: C{list}
     708      @ivar context_class: Holds the class that will create context objects for
     709          the implementing C{Encoder}.
     710      @type context_class: C{type} or C{types.ClassType}
     711      @ivar stream: The underlying data stream.
     712      @type stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
     713      @ivar context: The context for the encoding.
     714      @type context: An instance of C{BaseEncoder.context_class}
     715      @ivar strict: Whether the encoder should operate in 'strict' mode. Nothing
     716          is really affected by this for the time being - its just here for
     717          flexibility.
     718      @type strict: C{bool}, default is False.
     719      """
     720      context_class = BaseContext
     721      type_map = []
     722
     723 -    def __init__(self, data=None, context=None, strict=False):
     724          """
     725          @type   data: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
     726          @param  data: Data stream.
     727          @type   context: L{Context<pyamf.amf0.Context>}
     728          @param  context: Context.
     729          @raise TypeError: The C{context} parameter must be of type
     730              L{Context<pyamf.amf0.Context>}.
     731          """
     732          # coerce data to BufferedByteStream
     733          if isinstance(data, util.BufferedByteStream):
     734              self.stream = data
     735          else:
     736              self.stream = util.BufferedByteStream(data)
     737
     738          if context == None:
     739              self.context = self.context_class()
     740          elif isinstance(context, self.context_class):
     741              self.context = context
     742          else:
     743              raise TypeError("context must be of type %s.%s" % (
     744                  self.context_class.__module__, self.context_class.__name__))
     745
     746          self._write_elem_func_cache = {}
     747          self.strict = strict
     748
     749 -    def writeFunc(self, obj):
     750          """
     751          Not possible to encode functions.
     752
     753          @raise EncodeError: Unable to encode function/methods.
     754          """
     755          raise EncodeError("Unable to encode function/methods")
     756
     757 -    def _getWriteElementFunc(self, data):
     758          """
     759          Gets a function used to encode the data.
     760
     761          @type   data: C{mixed}
     762          @param  data: Python data.
     763          @rtype: callable or C{None}.
     764          @return: The function used to encode data to the stream.
     765          """
     766          for type_, func in TYPE_MAP.iteritems():
     767              try:
     768                  if isinstance(data, type_):
     769                      return CustomTypeFunc(self, func)
     770              except TypeError:
     771                  if callable(type_) and type_(data):
     772                      return CustomTypeFunc(self, func)
     773
     774          for tlist, method in self.type_map:
     775              for t in tlist:
     776                  try:
     777                      if isinstance(data, t):
     778                          return getattr(self, method)
     779                  except TypeError:
     780                      if callable(t) and t(data):
     781                          return getattr(self, method)
     782
     783          return None
     784
     785 -    def _writeElementFunc(self, data):
     786          """
     787          Gets a function used to encode the data.
     788
     789          @type   data: C{mixed}
     790          @param  data: Python data.
     791          @rtype: callable or C{None}.
     792          @return: The function used to encode data to the stream.
     793          """
     794          try:
     795              key = data.__class__
     796          except AttributeError:
     797              return self._getWriteElementFunc(data)
     798
     799          if key not in self._write_elem_func_cache:
     800              self._write_elem_func_cache[key] = self._getWriteElementFunc(data)
     801
     802          return self._write_elem_func_cache[key]
     803
     804 -    def writeElement(self, data):
     805          """
     806          Writes the data. Overridden in subclass.
     807
     808          @type   data: C{mixed}
     809          @param  data: The data to be encoded to the data stream.
     810          """
     811          raise NotImplementedError
     812
     813 -def register_class(klass, alias=None, attrs=None, attr_func=None, metadata=[]):
     814      """
     815      Registers a class to be used in the data streaming.
     816
     817      @type alias: C{str}
     818      @param alias: The alias of klass, i.e. C{org.example.Person}.
     819      @param attrs: A list of attributes that will be encoded for the class.
     820      @type attrs: C{list} or C{None}
     821      @type attr_func:
     822      @param attr_func:
     823      @type metadata:
     824      @param metadata:
     825      @raise TypeError: PyAMF doesn't support required init arguments.
     826      @raise TypeError: The C{klass} is not callable.
     827      @raise ValueError: The C{klass} or C{alias} is already registered.
     828      @return: The registered L{ClassAlias}.
     829      """
     830      if not callable(klass):
     831          raise TypeError("klass must be callable")
     832
     833      if klass in CLASS_CACHE:
     834          raise ValueError("klass %s already registered" % (klass,))
     835
     836      if alias is not None and alias in CLASS_CACHE.keys():
     837          raise ValueError("alias '%s' already registered" % (alias,))
     838
     839      alias_klass = util.get_class_alias(klass)
     840
     841      if alias_klass is None:
     842          alias_klass = ClassAlias
     843
     844      x = alias_klass(klass, alias, attr_func=attr_func,
     845          attrs=attrs, metadata=metadata)
     846
     847      if alias is None:
     848          alias = "%s.%s" % (klass.__module__, klass.__name__,)
     849
     850      CLASS_CACHE[alias] = x
     851
     852      return x
     853
     854 -def unregister_class(alias):
     855      """
     856      Deletes a class from the cache.
     857
     858      If C{alias} is a class, the matching alias is found.
     859
     860      @type alias: C{class} or C{str}
     861      @param alias: Alias for class to delete.
     862      @raise UnknownClassAlias: Unknown alias.
     863      """
     864      if isinstance(alias, (type, types.ClassType)):
     865          for s, a in CLASS_CACHE.iteritems():
     866              if a.klass == alias:
     867                  alias = s
     868                  break
     869      try:
     870          del CLASS_CACHE[alias]
     871      except KeyError:
     872          raise UnknownClassAlias("Unknown alias %s" % (alias,))
     873
     874 -def register_class_loader(loader):
     875      """
     876      Registers a loader that is called to provide the C{Class} for a specific
     877      alias.
     878
     879      The L{loader} is provided with one argument, the C{Class} alias. If the
     880      loader succeeds in finding a suitable class then it should return that
     881      class, otherwise it should return C{None}.
     882
     883      @type loader: C{callable}
     884      @raise TypeError: The C{loader} is not callable.
     885      @raise ValueError: The C{loader} is already registered.
     886      """
     887      if not callable(loader):
     888          raise TypeError("loader must be callable")
     889
     890      if loader in CLASS_LOADERS:
     891          raise ValueError("loader has already been registered")
     892
     893      CLASS_LOADERS.append(loader)
     894
     895 -def unregister_class_loader(loader):
     896      """
     897      Unregisters a class loader.
     898
     899      @type loader: C{callable}
     900      @param loader: The object to be unregistered
     901
     902      @raise LookupError: The C{loader} was not registered.
     903      """
     904      if loader not in CLASS_LOADERS:
     905          raise LookupError("loader not found")
     906
     907      del CLASS_LOADERS[CLASS_LOADERS.index(loader)]
     908
     909 -def get_module(mod_name):
     910      """
     911      Load a module based on C{mod_name}.
     912
     913      @type mod_name: C{str}
     914      @param mod_name: The module name.
     915      @return: Module.
     916
     917      @raise ImportError: Unable to import an empty module.
     918      """
     919      if mod_name is '':
     920          raise ImportError("Unable to import empty module")
     921
     922      mod = __import__(mod_name)
     923      components = mod_name.split('.')
     924
     925      for comp in components[1:]:
     926          mod = getattr(mod, comp)
     927
     928      return mod
     929
     930 -def load_class(alias):
     931      """
     932      Finds the class registered to the alias.
     933
     934      The search is done in order:
     935        1. Checks if the class name has been registered via L{register_class}.
     936        2. Checks all functions registered via L{register_class_loader}.
     937        3. Attempts to load the class via standard module loading techniques.
     938
     939      @type alias: C{str}
     940      @param alias: The class name.
     941      @raise UnknownClassAlias: The C{alias} was not found.
     942      @raise TypeError: Expecting class type or L{ClassAlias} from loader.
     943      @return: Class registered to the alias.
     944      """
     945      alias = str(alias)
     946
     947      # Try the CLASS_CACHE first
     948      try:
     949          return CLASS_CACHE[alias]
     950      except KeyError:
     951          pass
     952
     953      # Check each CLASS_LOADERS in turn
     954      for loader in CLASS_LOADERS:
     955          klass = loader(alias)
     956
     957          if klass is None:
     958              continue
     959
     960          if isinstance(klass, (type, types.ClassType)):
     961              return register_class(klass, alias)
     962          elif isinstance(klass, ClassAlias):
     963              CLASS_CACHE[str(alias)] = klass
     964          else:
     965              raise TypeError("Expecting class type or ClassAlias from loader")
     966
     967          return klass
     968
     969      # XXX nick: Are there security concerns for loading classes this way?
     970      mod_class = alias.split('.')
     971
     972      if mod_class:
     973          module = '.'.join(mod_class[:-1])
     974          klass = mod_class[-1]
     975
     976          try:
     977              module = get_module(module)
     978          except ImportError, AttributeError:
     979              # XXX What to do here?
     980              pass
     981          else:
     982              klass = getattr(module, klass)
     983
     984              if callable(klass):
     985                  CLASS_CACHE[alias] = klass
     986
     987                  return klass
     988
     989      # All available methods for finding the class have been exhausted
     990      raise UnknownClassAlias("Unknown alias %s" % (alias,))
     991
     992 -def get_class_alias(klass):
     993      """
     994      Finds the alias registered to the class.
     995
     996      @type klass: C{object} or class
     997      @rtype: L{ClassAlias}
     998      @return: The class alias linked to the C{klass}.
     999      @raise UnknownClassAlias: Class not found.
    1000      @raise TypeError: Expecting C{string} or C{class} type.
    1001      """
    1002      if not isinstance(klass, (type, types.ClassType, basestring)):
    1003          if isinstance(klass, types.InstanceType):
    1004              klass = klass.__class__
    1005          elif isinstance(klass, types.ObjectType):
    1006              klass = type(klass)
    1007
    1008      if isinstance(klass, basestring):
    1009          for a, k in CLASS_CACHE.iteritems():
    1010              if klass == a:
    1011                  return k
    1012      else:
    1013          for a, k in CLASS_CACHE.iteritems():
    1014              if klass == k.klass:
    1015                  return k
    1016
    1017      if isinstance(klass, basestring):
    1018          return load_class(klass)
    1019
    1020      raise UnknownClassAlias("Unknown alias %s" % (klass,))
    1021
    1022 -def has_alias(obj):
    1023      """
    1024      @rtype: C{bool}
    1025      @return: Alias is available.
    1026      """
    1027      try:
    1028          alias = get_class_alias(obj)
    1029          return True
    1030      except UnknownClassAlias:
    1031          return False
    1032
    1033 -def decode(stream, encoding=AMF0, context=None, strict=False):
    1034      """
    1035      A generator function to decode a datastream.
    1036
    1037      @type   stream: L{BufferedByteStream<pyamf.util.BufferedByteStream>}
    1038      @param  stream: AMF data.
    1039      @type   encoding: C{int}
    1040      @param  encoding: AMF encoding type.
    1041      @type   context: L{AMF0 Context<pyamf.amf0.Context>} or
    1042      L{AMF3 Context<pyamf.amf3.Context>}
    1043      @param  context: Context.
    1044      @return: Each element in the stream.
    1045      """
    1046      decoder = _get_decoder_class(encoding)(stream, context, strict)
    1047
    1048      while 1:
    1049          try:
    1050              yield decoder.readElement()
    1051          except EOStream:
    1052              break
    1053
    1054 -def encode(*args, **kwargs):
    1055      """
    1056      A helper function to encode an element.
    1057
    1058      @type element: C{mixed}
    1059      @keyword element: Python data.
    1060      @type encoding: C{int}
    1061      @keyword encoding: AMF encoding type.
    1062      @type context: L{amf0.Context<pyamf.amf0.Context>} or
    1063      L{amf3.Context<pyamf.amf3.Context>}
    1064      @keyword context: Context.
    1065
    1066      @rtype: C{StringIO}
    1067      @return: File-like object.
    1068      """
    1069      encoding = kwargs.get('encoding', AMF0)
    1070      context = kwargs.get('context', None)
    1071      strict = kwargs.get('strict', False)
    1072
    1073      stream = util.BufferedByteStream()
    1074      encoder = _get_encoder_class(encoding)(stream, context, strict)
    1075
    1076      for el in args:
    1077          encoder.writeElement(el)
    1078
    1079      stream.seek(0)
    1080
    1081      return stream
    1082
    1083 -def get_decoder(encoding, data=None, context=None, strict=False):
    1084      return _get_decoder_class(encoding)(data=data, context=context, strict=strict)
    1085
    1086 -def _get_decoder_class(encoding):
    1087      """
    1088      Get compatible decoder.
    1089
    1090      @type encoding: C{int}
    1091      @param encoding: AMF encoding version.
    1092      @raise ValueError: AMF encoding version is unknown.
    1093
    1094      @rtype: L{amf0.Decoder<pyamf.amf0.Decoder>} or
    1095      L{amf3.Decoder<pyamf.amf3.Decoder>}
    1096      @return: AMF0 or AMF3 decoder.
    1097      """
    1098      if encoding == AMF0:
    1099          from pyamf import amf0
    1100
    1101          return amf0.Decoder
    1102      elif encoding == AMF3:
    1103          from pyamf import amf3
    1104
    1105          return amf3.Decoder
    1106
    1107      raise ValueError("Unknown encoding %s" % (encoding,))
    1108
    1109 -def get_encoder(encoding, data=None, context=None, strict=False):
    1110      """
    1111      Returns a subclassed instance of L{pyamf.BaseEncoder}, based on C{encoding}
    1112      """
    1113      return _get_encoder_class(encoding)(data=data, context=context,
    1114          strict=strict)
    1115
    1116 -def _get_encoder_class(encoding):
    1117      """
    1118      Get compatible encoder.
    1119
    1120      @type encoding: C{int}
    1121      @param encoding: AMF encoding version.
    1122      @raise ValueError: AMF encoding version is unknown.
    1123
    1124      @rtype: L{amf0.Encoder<pyamf.amf0.Encoder>} or
    1125      L{amf3.Encoder<pyamf.amf3.Encoder>}
    1126      @return: AMF0 or AMF3 encoder.
    1127      """
    1128      if encoding == AMF0:
    1129          from pyamf import amf0
    1130
    1131          return amf0.Encoder
    1132      elif encoding == AMF3:
    1133          from pyamf import amf3
    1134
    1135          return amf3.Encoder
    1136
    1137      raise ValueError("Unknown encoding %s" % (encoding,))
    1138
    1139 -def get_context(encoding):
    1140      return _get_context_class(encoding)()
    1141
    1142 -def _get_context_class(encoding):
    1143      """
    1144      Gets a compatible context class.
    1145
    1146      @type encoding: C{int}
    1147      @param encoding: AMF encoding version.
    1148      @raise ValueError: AMF encoding version is unknown.
    1149
    1150      @rtype: L{amf0.Context<pyamf.amf0.Context>} or
    1151      L{amf3.Context<pyamf.amf3.Context>}
    1152      @return: AMF0 or AMF3 context class.
    1153      """
    1154      if encoding == AMF0:
    1155          from pyamf import amf0
    1156
    1157          return amf0.Context
    1158      elif encoding == AMF3:
    1159          from pyamf import amf3
    1160
    1161          return amf3.Context
    1162
    1163      raise ValueError("Unknown encoding %s" % (encoding,))
    1164
    1165 -def flex_loader(alias):
    1166      """
    1167      Loader for L{Flex<pyamf.flex>} framework compatibility classes.
    1168
    1169      @raise UnknownClassAlias: Trying to load unknown Flex compatibility class.
    1170      """
    1171      if not alias.startswith('flex.'):
    1172          return
    1173
    1174      try:
    1175          if alias.startswith('flex.messaging.messages'):
    1176              import pyamf.flex.messaging
    1177          elif alias.startswith('flex.messaging.io'):
    1178              import pyamf.flex
    1179          elif alias.startswith('flex.data.messages'):
    1180              import pyamf.flex.data
    1181
    1182          return CLASS_CACHE[alias]
    1183      except KeyError:
    1184          raise UnknownClassAlias(alias)
    1185
    1186 -def add_type(type_, func=None):
    1187      """
    1188      Adds a custom type to L{TYPE_MAP}. A custom type allows fine grain control
    1189      of what to encode to an AMF data stream.
    1190
    1191      @raise TypeError: Unable to add as a custom type (expected a class or callable).
    1192      @raise KeyError: Type already exists.
    1193      """
    1194      def _check_type(type_):
    1195          if not (isinstance(type_, (type, types.ClassType)) or callable(type_)):
    1196              raise TypeError('Unable to add \'%r\' as a custom type (expected a class or callable)' % (type_,))
    1197
    1198      if isinstance(type_, list):
    1199          type_ = tuple(type_)
    1200
    1201      if type_ in TYPE_MAP:
    1202          raise KeyError('Type %r already exists' % (type_,))
    1203
    1204      if isinstance(type_, types.TupleType):
    1205          for x in type_:
    1206              _check_type(x)
    1207      else:
    1208          _check_type(type_)
    1209
    1210      TYPE_MAP[type_] = func
    1211
    1212 -def get_type(type_):
    1213      """
    1214      Gets the declaration for the corresponding custom type.
    1215
    1216      @raise KeyError: Unknown type.
    1217      """
    1218      if isinstance(type_, list):
    1219          type_ = tuple(type_)
    1220
    1221      for (k, v) in TYPE_MAP.iteritems():
    1222          if k == type_:
    1223              return v
    1224
    1225      raise KeyError("Unknown type %r" % (type_,))
    1226
    1227 -def remove_type(type_):
    1228      """
    1229      Removes the custom type declaration.
    1230
    1231      @return: Custom type declaration.
    1232      """
    1233      declaration = get_type(type_)
    1234
    1235      del TYPE_MAP[type_]
    1236
    1237      return declaration
    1238
    1239 -def add_error_class(klass, code):
    1240      """
    1241      Maps an exception class to a string code. Used to map remoting C{onStatus}
    1242      objects to an exception class so that an exception can be built to
    1243      represent that error::
    1244
    1245          class AuthenticationError(Exception):
    1246              pass
    1247
    1248      An example: C{add_error_class(AuthenticationError, 'Auth.Failed')}
    1249
    1250      @type code: C{str}
    1251
    1252      @raise TypeError: C{klass} must be a C{class} type.
    1253      @raise TypeError: Error classes must subclass the C{__builtin__.Exception} class.
    1254      @raise ValueError: Code is already registered.
    1255      """
    1256      if not isinstance(code, basestring):
    1257          code = str(code)
    1258
    1259      if not isinstance(klass, (type, types.ClassType)):
    1260          raise TypeError("klass must be a class type")
    1261
    1262      mro = util.get_mro(klass)
    1263
    1264      if not Exception in mro:
    1265          raise TypeError('Error classes must subclass the __builtin__.Exception class')
    1266
    1267      if code in ERROR_CLASS_MAP.keys():
    1268          raise ValueError('Code %s is already registered' % (code,))
    1269
    1270      ERROR_CLASS_MAP[code] = klass
    1271
    1272 -def remove_error_class(klass):
    1273      """
    1274      Removes a class from C{ERROR_CLASS_MAP}.
    1275
    1276      @raise ValueError: Code is not registered.
    1277      @raise ValueError: Class is not registered.
    1278      @raise TypeError: Invalid type, expected C{class} or C{string}.
    1279      """
    1280      if isinstance(klass, basestring):
    1281          if not klass in ERROR_CLASS_MAP.keys():
    1282              raise ValueError('Code %s is not registered' % (klass,))
    1283      elif isinstance(klass, (type, types.ClassType)):
    1284          classes = ERROR_CLASS_MAP.values()
    1285          if not klass in classes:
    1286              raise ValueError('Class %s is not registered' % (klass,))
    1287
    1288          klass = ERROR_CLASS_MAP.keys()[classes.index(klass)]
    1289      else:
    1290          raise TypeError("Invalid type, expected class or string")
    1291
    1292      del ERROR_CLASS_MAP[klass]
    1293
    1294 -def register_alias_type(klass, *args):
    1295      """
    1296      This function allows you to map subclasses of L{ClassAlias} to classes
    1297      listed in C{args}.
    1298
    1299      When an object is read/written from/to the AMF stream, a paired
    1300      L{ClassAlias} instance is created (or reused), based on the Python class
    1301      of that object. L{ClassAlias} provides important metadata for the class
    1302      and can also control how the equivalent Python object is created, how the
    1303      attributes are applied etc.
    1304
    1305      Use this function if you need to do something non-standard.
    1306
    1307      @see: L{pyamf.adapters._google_appengine_ext_db.DataStoreClassAlias} for a
    1308          good example.
    1309      @since: 0.4
    1310      @raise RuntimeError: Type is already registered.
    1311      @raise TypeError: C{klass} must be a class.
    1312      @raise ValueError: New aliases must subclass L{pyamf.ClassAlias}.
    1313      @raise ValueError: At least one type must be supplied.
    1314      """
    1315      def check_type_registered(arg):
    1316          # FIXME: Create a reverse index of registered types and do a quicker lookup
    1317          for k, v in ALIAS_TYPES.iteritems():
    1318              for kl in v:
    1319                  if arg is kl:
    1320                      raise RuntimeError('%r is already registered under %r' % (arg, k))
    1321
    1322      if not isinstance(klass, (type, types.ClassType)):
    1323          raise TypeError('klass must be class')
    1324
    1325      if not issubclass(klass, ClassAlias):
    1326          raise ValueError('New aliases must subclass pyamf.ClassAlias')
    1327
    1328      if len(args) == 0:
    1329          raise ValueError('At least one type must be supplied')
    1330
    1331      if len(args) == 1 and callable(args[0]):
    1332          c = args[0]
    1333
    1334          check_type_registered(c)
    1335      else:
    1336          for arg in args:
    1337              if not isinstance(arg, (type, types.ClassType)):
    1338                  raise TypeError('%r must be class' % (arg,))
    1339
    1340              check_type_registered(arg)
    1341
    1342      ALIAS_TYPES[klass] = args
    1343
    1344  register_class_loader(flex_loader)
    1345  register_adapters()
    1346
    1347  register_alias_type(TypedObjectClassAlias, TypedObject)
    1348