1
2
3
4 """
5 U{PyAMF<http://pyamf.org>} provides Action Message Format (U{AMF
6 <http://en.wikipedia.org/wiki/Action_Message_Format>}) support for Python that
7 is compatible with the Adobe U{Flash Player
8 <http://en.wikipedia.org/wiki/Flash_Player>}.
9
10 @since: October 2007
11 @status: Production/Stable
12 """
13
14 import types
15 import inspect
16
17 from pyamf import util, _version
18 from pyamf.adapters import register_adapters
19 from pyamf import python
20 from pyamf.alias import ClassAlias, UnknownClassAlias
21
22
23 __all__ = [
24 'register_class',
25 'register_class_loader',
26 'encode',
27 'decode',
28 '__version__',
29 'version'
30 ]
31
32
33 __version__ = version = _version.version
34
35
36
37
38
39 CLASS_CACHE = {}
40
41
42
43 CLASS_LOADERS = set()
44
45
46 TYPE_MAP = {}
47
48
49 ERROR_CLASS_MAP = {
50 TypeError.__name__: TypeError,
51 KeyError.__name__: KeyError,
52 LookupError.__name__: LookupError,
53 IndexError.__name__: IndexError,
54 NameError.__name__: NameError,
55 ValueError.__name__: ValueError
56 }
57
58
59 ALIAS_TYPES = {}
60
61
62
63 AMF0 = 0
64
65
66 AMF3 = 3
67
68
69 ENCODING_TYPES = (AMF0, AMF3)
70
71
72 DEFAULT_ENCODING = AMF3
73
74
76 """
77 Represents the C{undefined} value in the Adobe Flash Player client.
78 """
80 return 'pyamf.Undefined'
81
82
83 Undefined = UndefinedType()
84
85
87 """
88 Base AMF Error.
89
90 All AMF related errors should be subclassed from this class.
91 """
92
93
95 """
96 Raised if there is an error in decoding an AMF data stream.
97 """
98
99
101 """
102 Raised if the data stream has come to a natural end.
103 """
104
105
107 """
108 Raised if an AMF data stream refers to a non-existent object or string
109 reference (in the case of AMF3).
110 """
111
112
114 """
115 Raised if the element could not be encoded to AMF.
116 """
117
118
120 """
121 Represents a Flash Actionscript Object (typed or untyped).
122
123 I supply a C{dict} interface to support C{getattr}/C{setattr} calls.
124 """
125
128
130 try:
131 return self[k]
132 except KeyError:
133 raise AttributeError('Unknown attribute \'%s\'' % (k,))
134
137
140
143
144
146 """
147 Used to be able to specify the C{mixedarray} type.
148 """
149
150
152 """
153 This class is used when a strongly typed object is decoded but there is no
154 registered class to apply it to.
155
156 This object can only be used for standard streams - i.e. not externalized
157 data. If encountered, a L{DecodeError} will be raised.
158
159 @ivar alias: The alias of the typed object.
160 @type alias: C{string}
161 @since: 0.4
162 """
163
168
170 raise DecodeError('Unable to decode an externalised stream with '
171 'class alias \'%s\'.\n\nA class alias was found and because '
172 'strict mode is False an attempt was made to decode the object '
173 'automatically. To decode this stream, a registered class with '
174 'the alias and a corresponding __readamf__ method will be '
175 'required.' % (self.alias,))
176
178 raise EncodeError('Unable to encode an externalised stream with '
179 'class alias \'%s\'.\n\nA class alias was found and because '
180 'strict mode is False an attempt was made to encode the object '
181 'automatically. To encode this stream, a registered class with '
182 'the alias and a corresponding __writeamf__ method will be '
183 'required.' % (self.alias,))
184
185
187 """
188 The meta class for L{TypedObject} used to adapt PyAMF.
189
190 @since: 0.4
191 """
192
193 klass = TypedObject
194
197
199 return self.klass(self.alias)
200
203
204
206 """
207 Adapts Python exception objects to Adobe Flash Player error objects.
208
209 @since: 0.5
210 """
211
213 self.exclude_attrs.update(['args'])
214
222
223
225 """
226 Registers a class to be used in the data streaming. This is the equivalent
227 to the C{[RemoteClass(alias="foobar")]} AS3 metatag.
228
229 @return: The registered L{ClassAlias} instance.
230 @see: L{unregister_class}
231 """
232 meta = util.get_class_meta(klass)
233
234 if alias is not None:
235 meta['alias'] = alias
236
237 alias_klass = util.get_class_alias(klass) or ClassAlias
238
239 x = alias_klass(klass, defer=True, **meta)
240
241 if not x.anonymous:
242 CLASS_CACHE[x.alias] = x
243
244 CLASS_CACHE[klass] = x
245
246 return x
247
248
266
267
269 """
270 Finds the L{ClassAlias} that is registered to C{klass_or_alias}.
271
272 If a string is supplied and no related L{ClassAlias} is found, the alias is
273 loaded via L{load_class}.
274
275 @raise UnknownClassAlias: Unknown alias
276 """
277 if isinstance(klass_or_alias, python.str_types):
278 try:
279 return CLASS_CACHE[klass_or_alias]
280 except KeyError:
281 return load_class(klass_or_alias)
282
283 try:
284 return CLASS_CACHE[klass_or_alias]
285 except KeyError:
286 raise UnknownClassAlias('Unknown alias for %r' % (klass_or_alias,))
287
288
290 """
291 Registers a loader that is called to provide the C{class} for a specific
292 alias.
293
294 The C{loader} is provided with one argument, the class alias (as a string).
295 If the loader succeeds in finding a suitable class then it should return
296 that class, otherwise it should return C{None}.
297
298 An example::
299
300 def lazy_load_from_my_module(alias):
301 if not alias.startswith('foo.bar.'):
302 return None
303
304 from foo import bar
305
306 if alias == 'foo.bar.Spam':
307 return bar.Spam
308 elif alias == 'foo.bar.Eggs':
309 return bar.Eggs
310
311 pyamf.register_class_loader(lazy_load_from_my_module)
312
313 @raise TypeError: C{loader} must be callable
314 @see: L{unregister_class_loader}
315 """
316 if not hasattr(loader, '__call__'):
317 raise TypeError("loader must be callable")
318
319 CLASS_LOADERS.update([loader])
320
321
323 """
324 Unregisters a class loader.
325
326 @param loader: The class loader to be unregistered.
327 @raise LookupError: The C{loader} was not registered.
328 @see: L{register_class_loader}
329 """
330 try:
331 CLASS_LOADERS.remove(loader)
332 except KeyError:
333 raise LookupError("loader not found")
334
335
337 """
338 Finds the class registered to the alias.
339
340 The search is done in order:
341 1. Checks if the class name has been registered via L{register_class}
342 or L{register_package}.
343 2. Checks all functions registered via L{register_class_loader}.
344 3. Attempts to load the class via standard module loading techniques.
345
346 @param alias: The class name.
347 @type alias: C{string}
348 @raise UnknownClassAlias: The C{alias} was not found.
349 @raise TypeError: Expecting class type or L{ClassAlias} from loader.
350 @return: Class registered to the alias.
351 @rtype: C{classobj}
352 """
353
354 try:
355 return CLASS_CACHE[alias]
356 except KeyError:
357 pass
358
359 for loader in CLASS_LOADERS:
360 klass = loader(alias)
361
362 if klass is None:
363 continue
364
365 if isinstance(klass, python.class_types):
366 return register_class(klass, alias)
367 elif isinstance(klass, ClassAlias):
368 CLASS_CACHE[klass.alias] = klass
369 CLASS_CACHE[klass.klass] = klass
370
371 return klass
372
373 raise TypeError("Expecting class object or ClassAlias from loader")
374
375 mod_class = alias.split('.')
376
377 if mod_class:
378 module = '.'.join(mod_class[:-1])
379 klass = mod_class[-1]
380
381 try:
382 module = util.get_module(module)
383 except (ImportError, AttributeError):
384 pass
385 else:
386 klass = getattr(module, klass)
387
388 if isinstance(klass, python.class_types):
389 return register_class(klass, alias)
390 elif isinstance(klass, ClassAlias):
391 CLASS_CACHE[klass.alias] = klass
392 CLASS_CACHE[klass.klass] = klass
393
394 return klass.klass
395 else:
396 raise TypeError("Expecting class type or ClassAlias from loader")
397
398
399 raise UnknownClassAlias("Unknown alias for %r" % (alias,))
400
401
402 -def decode(stream, *args, **kwargs):
403 """
404 A generator function to decode a datastream.
405
406 @param stream: AMF data to be decoded.
407 @type stream: byte data.
408 @kwarg encoding: AMF encoding type. One of L{ENCODING_TYPES}.
409 @return: A generator that will decode each element in the stream.
410 """
411 encoding = kwargs.pop('encoding', DEFAULT_ENCODING)
412 decoder = get_decoder(encoding, stream, *args, **kwargs)
413
414 return decoder
415
416
418 """
419 A helper function to encode an element.
420
421 @param args: The python data to be encoded.
422 @kwarg encoding: AMF encoding type. One of L{ENCODING_TYPES}.
423 @return: A L{util.BufferedByteStream} object that contains the data.
424 """
425 encoding = kwargs.pop('encoding', DEFAULT_ENCODING)
426 encoder = get_encoder(encoding, **kwargs)
427
428 [encoder.writeElement(el) for el in args]
429
430 stream = encoder.stream
431 stream.seek(0)
432
433 return stream
434
435
437 """
438 Returns a L{codec.Decoder} capable of decoding AMF[C{encoding}] streams.
439
440 @raise ValueError: Unknown C{encoding}.
441 """
442 def _get_decoder_class():
443 if encoding == AMF0:
444 try:
445 from cpyamf import amf0
446 except ImportError:
447 from pyamf import amf0
448
449 return amf0.Decoder
450 elif encoding == AMF3:
451 try:
452 from cpyamf import amf3
453 except ImportError:
454 from pyamf import amf3
455
456 return amf3.Decoder
457
458 raise ValueError("Unknown encoding %r" % (encoding,))
459
460 return _get_decoder_class()(*args, **kwargs)
461
462
464 """
465 Returns a L{codec.Encoder} capable of encoding AMF[C{encoding}] streams.
466
467 @raise ValueError: Unknown C{encoding}.
468 """
469 def _get_encoder_class():
470 if encoding == AMF0:
471 try:
472 from cpyamf import amf0
473 except ImportError:
474 from pyamf import amf0
475
476 return amf0.Encoder
477 elif encoding == AMF3:
478 try:
479 from cpyamf import amf3
480 except ImportError:
481 from pyamf import amf3
482
483 return amf3.Encoder
484
485 raise ValueError("Unknown encoding %r" % (encoding,))
486
487 return _get_encoder_class()(*args, **kwargs)
488
489
491 """
492 Loader for BlazeDS framework compatibility classes, specifically
493 implementing C{ISmallMessage}.
494
495 @see: U{BlazeDS<http://opensource.adobe.com/wiki/display/blazeds/BlazeDS>}
496 @since: 0.5
497 """
498 if alias not in ['DSC', 'DSK']:
499 return
500
501 import pyamf.flex.messaging
502
503 return CLASS_CACHE[alias]
504
505
507 """
508 Loader for L{Flex<pyamf.flex>} framework compatibility classes.
509
510 @raise UnknownClassAlias: Trying to load an unknown Flex compatibility class.
511 """
512 if not alias.startswith('flex.'):
513 return
514
515 try:
516 if alias.startswith('flex.messaging.messages'):
517 import pyamf.flex.messaging
518 elif alias.startswith('flex.messaging.io'):
519 import pyamf.flex
520 elif alias.startswith('flex.data.messages'):
521 import pyamf.flex.data
522
523 return CLASS_CACHE[alias]
524 except KeyError:
525 raise UnknownClassAlias(alias)
526
527
529 """
530 Adds a custom type to L{TYPE_MAP}. A custom type allows fine grain control
531 of what to encode to an AMF data stream.
532
533 @raise TypeError: Unable to add as a custom type (expected a class or callable).
534 @raise KeyError: Type already exists.
535 @see: L{get_type} and L{remove_type}
536 """
537 def _check_type(type_):
538 if not (isinstance(type_, python.class_types) or
539 hasattr(type_, '__call__')):
540 raise TypeError(r'Unable to add '%r' as a custom type (expected a '
541 'class or callable)' % (type_,))
542
543 if isinstance(type_, list):
544 type_ = tuple(type_)
545
546 if type_ in TYPE_MAP:
547 raise KeyError('Type %r already exists' % (type_,))
548
549 if isinstance(type_, types.TupleType):
550 for x in type_:
551 _check_type(x)
552 else:
553 _check_type(type_)
554
555 TYPE_MAP[type_] = func
556
557
559 """
560 Gets the declaration for the corresponding custom type.
561
562 @raise KeyError: Unknown type.
563 @see: L{add_type} and L{remove_type}
564 """
565 if isinstance(type_, list):
566 type_ = tuple(type_)
567
568 for k, v in TYPE_MAP.iteritems():
569 if k == type_:
570 return v
571
572 raise KeyError("Unknown type %r" % (type_,))
573
574
576 """
577 Removes the custom type declaration.
578
579 @return: Custom type declaration.
580 @see: L{add_type} and L{get_type}
581 """
582 declaration = get_type(type_)
583
584 del TYPE_MAP[type_]
585
586 return declaration
587
588
590 """
591 Maps an exception class to a string code. Used to map remoting C{onStatus}
592 objects to an exception class so that an exception can be built to
593 represent that error.
594
595 An example::
596
597 >>> class AuthenticationError(Exception):
598 ... pass
599 ...
600 >>> pyamf.add_error_class(AuthenticationError, 'Auth.Failed')
601 >>> print pyamf.ERROR_CLASS_MAP
602 {'TypeError': <type 'exceptions.TypeError'>, 'IndexError': <type 'exceptions.IndexError'>,
603 'Auth.Failed': <class '__main__.AuthenticationError'>, 'KeyError': <type 'exceptions.KeyError'>,
604 'NameError': <type 'exceptions.NameError'>, 'LookupError': <type 'exceptions.LookupError'>}
605
606 @param klass: Exception class
607 @param code: Exception code
608 @type code: C{str}
609 @see: L{remove_error_class}
610 """
611 if not isinstance(code, python.str_types):
612 code = code.decode('utf-8')
613
614 if not isinstance(klass, python.class_types):
615 raise TypeError("klass must be a class type")
616
617 mro = inspect.getmro(klass)
618
619 if not Exception in mro:
620 raise TypeError(
621 'Error classes must subclass the __builtin__.Exception class')
622
623 if code in ERROR_CLASS_MAP:
624 raise ValueError('Code %s is already registered' % (code,))
625
626 ERROR_CLASS_MAP[code] = klass
627
628
630 """
631 Removes a class from the L{ERROR_CLASS_MAP}.
632
633 An example::
634
635 >>> class AuthenticationError(Exception):
636 ... pass
637 ...
638 >>> pyamf.add_error_class(AuthenticationError, 'Auth.Failed')
639 >>> pyamf.remove_error_class(AuthenticationError)
640
641 @see: L{add_error_class}
642 """
643 if isinstance(klass, python.str_types):
644 if klass not in ERROR_CLASS_MAP:
645 raise ValueError('Code %s is not registered' % (klass,))
646 elif isinstance(klass, python.class_types):
647 classes = ERROR_CLASS_MAP.values()
648 if klass not in classes:
649 raise ValueError('Class %s is not registered' % (klass,))
650
651 klass = ERROR_CLASS_MAP.keys()[classes.index(klass)]
652 else:
653 raise TypeError("Invalid type, expected class or string")
654
655 del ERROR_CLASS_MAP[klass]
656
657
659 """
660 This function allows you to map subclasses of L{ClassAlias} to classes
661 listed in C{args}.
662
663 When an object is read/written from/to the AMF stream, a paired L{ClassAlias}
664 instance is created (or reused), based on the Python class of that object.
665 L{ClassAlias} provides important metadata for the class and can also control
666 how the equivalent Python object is created, how the attributes are applied
667 etc.
668
669 Use this function if you need to do something non-standard.
670
671 @since: 0.4
672 @see:
673 - L{pyamf.adapters._google_appengine_ext_db.DataStoreClassAlias} for a
674 good example.
675 - L{unregister_alias_type}
676 @raise RuntimeError: alias is already registered
677 @raise TypeError: Value supplied to C{klass} is not a class
678 @raise ValueError:
679 - New aliases must subclass L{pyamf.ClassAlias}
680 - At least one type must be supplied
681 """
682 def check_type_registered(arg):
683 for k, v in ALIAS_TYPES.iteritems():
684 for kl in v:
685 if arg is kl:
686 raise RuntimeError('%r is already registered under %r' % (
687 arg, k))
688
689 if not isinstance(klass, python.class_types):
690 raise TypeError('klass must be class')
691
692 if not issubclass(klass, ClassAlias):
693 raise ValueError('New aliases must subclass pyamf.ClassAlias')
694
695 if len(args) == 0:
696 raise ValueError('At least one type must be supplied')
697
698 if len(args) == 1 and hasattr(args[0], '__call__'):
699 c = args[0]
700
701 check_type_registered(c)
702 else:
703 for arg in args:
704 if not isinstance(arg, python.class_types):
705 raise TypeError('%r must be class' % (arg,))
706
707 check_type_registered(arg)
708
709 ALIAS_TYPES[klass] = args
710
711 for k, v in CLASS_CACHE.copy().iteritems():
712 new_alias = util.get_class_alias(v.klass)
713
714 if new_alias is klass:
715 meta = util.get_class_meta(v.klass)
716 meta['alias'] = v.alias
717
718 alias_klass = klass(v.klass, **meta)
719
720 CLASS_CACHE[k] = alias_klass
721 CLASS_CACHE[v.klass] = alias_klass
722
723
725 """
726 Removes the klass from the L{ALIAS_TYPES} register.
727
728 @see: L{register_alias_type}
729 """
730 return ALIAS_TYPES.pop(klass, None)
731
732
733 -def register_package(module=None, package=None, separator='.', ignore=[],
734 strict=True):
735 """
736 This is a helper function that takes the concept of Actionscript packages
737 and registers all the classes in the supplied Python module under that
738 package. It auto-aliased all classes in C{module} based on the parent
739 C{package}.
740
741 @param module: The Python module that will contain all the classes to
742 auto alias.
743 @type module: C{module} or C{dict}
744 @param package: The base package name. e.g. 'com.example.app'. If this
745 is C{None} then the value is inferred from C{module.__name__}.
746 @type package: C{string} or C{None}
747 @param separator: The separator used to append to C{package} to form the
748 complete alias.
749 @param ignore: To give fine grain control over what gets aliased and what
750 doesn't, supply a list of classes that you B{do not} want to be aliased.
751 @type ignore: C{iterable}
752 @param strict: Whether only classes that originate from C{module} will be
753 registered.
754
755 @return: A dict of all the classes that were registered and their respective
756 L{ClassAlias} counterparts.
757 @since: 0.5
758 @raise TypeError: Cannot get a list of classes from C{module}
759 """
760 if isinstance(module, python.str_types):
761 if module == '':
762 raise TypeError('Cannot get list of classes from %r' % (module,))
763
764 package = module
765 module = None
766
767 if module is None:
768 import inspect
769
770 prev_frame = inspect.stack()[1][0]
771 module = prev_frame.f_locals
772
773 if type(module) is dict:
774 has = lambda x: x in module
775 get = module.__getitem__
776 elif type(module) is list:
777 has = lambda x: x in module
778 get = module.__getitem__
779 strict = False
780 else:
781 has = lambda x: hasattr(module, x)
782 get = lambda x: getattr(module, x)
783
784 if package is None:
785 if has('__name__'):
786 package = get('__name__')
787 else:
788 raise TypeError('Cannot get list of classes from %r' % (module,))
789
790 if has('__all__'):
791 keys = get('__all__')
792 elif hasattr(module, '__dict__'):
793 keys = module.__dict__.keys()
794 elif hasattr(module, 'keys'):
795 keys = module.keys()
796 elif isinstance(module, list):
797 keys = range(len(module))
798 else:
799 raise TypeError('Cannot get list of classes from %r' % (module,))
800
801 def check_attr(attr):
802 if not isinstance(attr, python.class_types):
803 return False
804
805 if attr.__name__ in ignore:
806 return False
807
808 try:
809 if strict and attr.__module__ != get('__name__'):
810 return False
811 except AttributeError:
812 return False
813
814 return True
815
816
817 classes = filter(check_attr, [get(x) for x in keys])
818
819 registered = {}
820
821 for klass in classes:
822 alias = '%s%s%s' % (package, separator, klass.__name__)
823
824 registered[klass] = register_class(klass, alias)
825
826 return registered
827
828
830 """
831 Sets the default interface that will called apon to both de/serialise XML
832 entities. This means providing both C{tostring} and C{fromstring} functions.
833
834 For testing purposes, will return the previous value for this (if any).
835 """
836 from pyamf import xml
837
838 return xml.set_default_interface(etree)
839
840
841
842 register_class(ASObject)
843 register_class_loader(flex_loader)
844 register_class_loader(blaze_loader)
845 register_alias_type(TypedObjectClassAlias, TypedObject)
846 register_alias_type(ErrorAlias, Exception)
847
848 register_adapters()
849