You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by kp...@apache.org on 2019/10/16 13:30:01 UTC

[qpid-proton] 01/01: PROTON-2119: Added classes to better handle AMQP restricted types for symbol-only lists and dictionaries with symbol-only keys.

This is an automated email from the ASF dual-hosted git repository.

kpvdr pushed a commit to branch PROTON-2119
in repository https://gitbox.apache.org/repos/asf/qpid-proton.git

commit 5d9ff0017bf4a0057478a9820fd04ab8143620dd
Author: Kim van der Riet <kv...@europa.lan>
AuthorDate: Wed Oct 16 09:28:47 2019 -0400

    PROTON-2119: Added classes to better handle AMQP restricted types for symbol-only lists and dictionaries with symbol-only keys.
---
 python/docs/index.rst                |   6 +
 python/docs/proton.rst               |  30 ++++-
 python/docs/types.rst                | 118 ++++++++++++++++++
 python/proton/__init__.py            |   5 +-
 python/proton/_data.py               | 223 ++++++++++++++++++++++++++++++++++-
 python/proton/_endpoints.py          |  61 +++++++++-
 python/proton/_message.py            |  48 +++++++-
 python/proton/_reactor.py            |   8 +-
 python/tests/proton_tests/codec.py   | 110 +++++++++++++++++
 python/tests/proton_tests/engine.py  |  21 +++-
 python/tests/proton_tests/message.py |   9 ++
 11 files changed, 626 insertions(+), 13 deletions(-)

diff --git a/python/docs/index.rst b/python/docs/index.rst
index ad9fa48..ddf580a 100644
--- a/python/docs/index.rst
+++ b/python/docs/index.rst
@@ -24,6 +24,7 @@ About AMQP and the Qpid Proton Python API
    :maxdepth: 1
 
    overview
+   types
    tutorial
 
 Key API Features
@@ -121,6 +122,11 @@ Several event handlers are provided which provide default behavior for most even
 non-transactional applications. Developers would typically directly inherit from this handler to provide the
 application's event behavior, and override callbacks as needed to provide additional behavior they may require.
 
+AMQP types
+----------
+The types defined by the AMQP specification are mapped to either native Python types or to special proton classes
+which represent the AMQP type. See :ref:`types` for a summary.
+
 Examples
 --------
 
diff --git a/python/docs/proton.rst b/python/docs/proton.rst
index 6d46099..2a92346 100644
--- a/python/docs/proton.rst
+++ b/python/docs/proton.rst
@@ -11,6 +11,7 @@ Module Summary
 
 .. autosummary::
 
+    AnnotationDict
     Condition
     Connection
     Data
@@ -21,6 +22,7 @@ Module Summary
     EventType
     Link
     Message
+    PropertyDict
     Receiver
     SASL
     Sender
@@ -28,6 +30,7 @@ Module Summary
     SSL
     SSLDomain
     SSLSessionDetails
+    SymbolList
     Terminus
     Transport
     Url
@@ -87,9 +90,8 @@ Module Detail
 .. The following classes in the __all__ list are excluded (blacklisted):
    * Collector
 
-|
 
-.. autoclass:: proton.Array
+.. autoclass:: proton.AnnotationDict
     :members:
     :show-inheritance:
     :inherited-members:
@@ -236,6 +238,14 @@ Module Detail
 
 ------------
 
+.. autoclass:: proton.PropertyDict
+    :members:
+    :show-inheritance:
+    :inherited-members:
+    :undoc-members:
+
+------------
+
 .. autoclass:: proton.Receiver
     :members:
     :show-inheritance:
@@ -322,6 +332,14 @@ Module Detail
 
 ------------
 
+.. autoclass:: proton.SymbolList
+    :members:
+    :show-inheritance:
+    :inherited-members:
+    :undoc-members:
+
+------------
+
 .. autoclass:: proton.Terminus
     :members:
     :show-inheritance:
@@ -373,6 +391,14 @@ Module Detail
 
 ------------
 
+.. autoclass:: proton.Array
+    :members:
+    :show-inheritance:
+    :inherited-members:
+    :undoc-members:
+
+------------
+
 .. autoclass:: proton.byte
     :members:
     :show-inheritance:
diff --git a/python/docs/types.rst b/python/docs/types.rst
new file mode 100644
index 0000000..d631c36
--- /dev/null
+++ b/python/docs/types.rst
@@ -0,0 +1,118 @@
+##########
+AMQP Types
+##########
+
+These tables summarize the various AMQP types and their Python API equivalents as used in the API.
+
+|
+
+============
+Scalar Types
+============
+
+|
+
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| AMQP Type  | Proton C API Type | Proton Python API Type    | Description                                                            |
++============+===================+===========================+========================================================================+
+| null       | PN_NONE           | ``None``                  | Indicates an empty value.                                              |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| boolean    | PN_BOOL           | ``bool``                  | Represents a true or false value.                                      |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| ubyte      | PN_UBYTE          | :class:`proton.ubyte`     | Integer in the range :math:`0` to :math:`2^8 - 1` inclusive.           |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| byte       | PN_BYTE           | :class:`proton.byte`      | Integer in the range :math:`-(2^7)` to :math:`2^7 - 1` inclusive.      |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| ushort     | PN_USHORT         | :class:`proton.ushort`    | Integer in the range :math:`0` to :math:`2^{16} - 1` inclusive.        |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| short      | PN_SHORT          | :class:`proton.short`     | Integer in the range :math:`-(2^{15})` to :math:`2^{15} - 1` inclusive.|
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| uint       | PN_UINT           | :class:`proton.uint`      | Integer in the range :math:`0` to :math:`2^{32} - 1` inclusive.        |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| int        | PN_INT            | :class:`proton.int32`     | Integer in the range :math:`-(2^{31})` to :math:`2^{31} - 1` inclusive.|
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| char       | PN_CHAR           | :class:`proton.char`      | A single Unicode character.                                            |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| ulong      | PN_ULONG          | :class:`proton.ulong`     | Integer in the range :math:`0` to :math:`2^{64} - 1` inclusive.        |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| long       | PN_LONG           | ``int`` or ``long``       | Integer in the range :math:`-(2^{63})` to :math:`2^{63} - 1` inclusive.|
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| timestamp  | PN_TIMESTAMP      | :class:`proton.timestamp` | An absolute point in time with millisecond precision.                  |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| float      | PN_FLOAT          | :class:`proton.float32`   | 32-bit floating point number (IEEE 754-2008 binary32).                 |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| double     | PN_DOUBLE         | ``double``                | 64-bit floating point number (IEEE 754-2008 binary64).                 |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| decimal32  | PN_DECIMAL32      | :class:`proton.decimal32` | 32-bit decimal number (IEEE 754-2008 decimal32).                       |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| decimal64  | PN_DECIMAL64      | :class:`proton.decimal64` | 64-bit decimal number (IEEE 754-2008 decimal64).                       |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| decimal128 | PN_DECIMAL128     | :class:`proton.decimal128`| 128-bit decimal number (IEEE 754-2008 decimal128).                     |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| uuid       | PN_UUID           | ``uuid.UUID``             | A universally unique identifier as defined by RFC-4122 section 4.1.2.  |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| binary     | PN_BINARY         | ``bytes``                 | A sequence of octets.                                                  |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| string     | PN_STRING         | ``str``                   | A sequence of Unicode characters.                                      |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+| symbol     | PN_SYMBOL         | :class:`proton.symbol`    | Symbolic values from a constrained domain.                             |
++------------+-------------------+---------------------------+------------------------------------------------------------------------+
+
+|
+
+==============
+Compound Types
+==============
+
+|
+
++-----------+-------------------+---------------------------+-----------------------------------------------------+
+| AMQP Type | Proton C API Type | Proton Python API Type    | Description                                         |
++===========+===================+===========================+=====================================================+
+| array     | PN_ARRAY          | :class:`proton.Array`     | A sequence of values of a single type.              |
++-----------+-------------------+---------------------------+-----------------------------------------------------+
+| list      | PN_LIST           | ``list``                  | A sequence of polymorphic values.                   |
++-----------+-------------------+---------------------------+-----------------------------------------------------+
+| map       | PN_MAP            | ``dict``                  | A polymorphic mapping from distinct keys to values. |
++-----------+-------------------+---------------------------+-----------------------------------------------------+
+
+|
+
+=================
+Specialized Types
+=================
+
+The following classes implement specialized or restricted types to help
+enforce type restrictions in the AMQP specification.
+
+|
+
++-------------------------------+------------------------------------------------------------------------------------+------------------------------------------------+
+| Proton Python API Type        | Description                                                                        | Where used in API                              |
++===============================+====================================================================================+================================================+
+| :class:`proton.SymbolList`    | A ``list`` that only accepts :class:`proton.symbol` elements. However, will        | :attr:`proton.Connection.desired_capabilities` |
+|                               | silently convert strings to symbols.                                               | :attr:`proton.Connection.offered_capabilities` |
++-------------------------------+------------------------------------------------------------------------------------+------------------------------------------------+
+| :class:`proton.PropertyDict`  | A ``dict`` that only accppts :class:`proton.symbol` keys. However, will silently   | :attr:`proton.Connection.properties`           |
+|                               | convert strings to symbols.                                                        |                                                |
++-------------------------------+------------------------------------------------------------------------------------+------------------------------------------------+
+| :class:`proton.AnnotationDict`| A ``dict`` that only accppts :class:`proton.symbol` or :class:`proton.ulong` keys. | :attr:`proton.Message.annotations`             |
+|                               | However, will silently convert strings to symbols.                                 | :attr:`proton.Message.instructions`            |
++-------------------------------+------------------------------------------------------------------------------------+------------------------------------------------+
+
+|
+
+These types would typically be used where the the above attributes are set. They will silently convert strings to symbols,
+but will raise an error if a not-allowed type is used. For example:
+
+        >>> from proton import symbol, ulong, Message, AnnotationDict
+        >>> msg = Message()
+        >>> msg.annotations = AnnotationDict({'one':1, symbol('two'):2, ulong(3):'three'})
+        >>> msg.annotations
+        AnnotationDict({symbol('one'): 1, symbol('two'): 2, ulong(3): 'three'})
+        >>> m.instructions = AnnotationDict({'one':1, symbol('two'):2, ulong(3):'three', 4:'four'})
+          ...
+        KeyError: "invalid non-symbol key: <class 'int'>: 4"
+        >>> m.instructions
+        >>> 
+
diff --git a/python/proton/__init__.py b/python/proton/__init__.py
index 4e6b23f..6c124c5 100644
--- a/python/proton/__init__.py
+++ b/python/proton/__init__.py
@@ -36,7 +36,7 @@ from cproton import PN_VERSION_MAJOR, PN_VERSION_MINOR, PN_VERSION_POINT
 
 from ._condition import Condition
 from ._data import UNDESCRIBED, Array, Data, Described, char, symbol, timestamp, ubyte, ushort, uint, ulong, \
-    byte, short, int32, float32, decimal32, decimal64, decimal128
+    byte, short, int32, float32, decimal32, decimal64, decimal128, AnnotationDict, PropertyDict, SymbolList
 from ._delivery import Delivery, Disposition
 from ._endpoints import Endpoint, Connection, Session, Link, Receiver, Sender, Terminus
 from ._events import Collector, Event, EventType, Handler
@@ -50,6 +50,7 @@ __all__ = [
     "API_LANGUAGE",
     "IMPLEMENTATION_LANGUAGE",
     "UNDESCRIBED",
+    "AnnotationDict",
     "Array",
     "Collector",
     "Condition",
@@ -68,6 +69,7 @@ __all__ = [
     "LinkException",
     "Message",
     "MessageException",
+    "PropertyDict",
     "ProtonException",
     "VERSION_MAJOR",
     "VERSION_MINOR",
@@ -81,6 +83,7 @@ __all__ = [
     "SSLSessionDetails",
     "SSLUnavailable",
     "SSLException",
+    "SymbolList",
     "Terminus",
     "Timeout",
     "Interrupt",
diff --git a/python/proton/_data.py b/python/proton/_data.py
index 75fce93..e324e64 100644
--- a/python/proton/_data.py
+++ b/python/proton/_data.py
@@ -308,6 +308,224 @@ class Array(object):
             return False
 
 
+def _check_type(s, allow_ulong=False, raise_on_error=True):
+    if isinstance(s, symbol):
+        return s
+    if allow_ulong and isinstance(s, ulong):
+        return s
+    if isinstance(s, str):
+        # Must be py2 or py3 str
+        return symbol(s)
+    if isinstance(s, unicode):
+        # This must be python2 unicode as we already detected py3 str above
+        return symbol(s.encode('utf-8'))
+    if raise_on_error:
+        raise TypeError('Non-symbol type %s: %s' % (type(s), s))
+    return s
+
+
+def _check_is_symbol(s, raise_on_error=True):
+    return _check_type(s, allow_ulong=False, raise_on_error=raise_on_error)
+
+
+def _check_is_symbol_or_ulong(s, raise_on_error=True):
+    return _check_type(s, allow_ulong=True, raise_on_error=raise_on_error)
+
+
+class RestrictedKeyDict(dict):
+    """Parent class for :class:`PropertyDict` and :class:`AnnotationDict`"""
+    def __init__(self, validation_fn, e=None, raise_on_error=True, **kwargs):
+        super(RestrictedKeyDict, self).__init__()
+        self.validation_fn = validation_fn
+        self.raise_on_error = raise_on_error
+        self.update(e, **kwargs)
+
+    def __setitem__(self, key, value):
+        """Checks if the key is a :class:`symbol` type before setting the value"""
+        try:
+            return super(RestrictedKeyDict, self).__setitem__(self.validation_fn(key, self.raise_on_error), value)
+        except TypeError:
+            pass
+        # __setitem__() must raise a KeyError, not TypeError
+        raise KeyError('invalid non-symbol key: %s: %s' % (type(key), key))
+
+    def update(self, e=None, **kwargs):
+        """
+        Equivalent to dict.update(), but it was needed to call :meth:`__setitem__()`
+        instead of ``dict.__setitem__()``.
+        """
+        if e:
+            try:
+                for k in e:
+                    self.__setitem__(k, e[k])
+            except TypeError:
+                self.__setitem__(k[0], k[1])  # use tuple consumed from from zip
+                for (k, v) in e:
+                    self.__setitem__(k, v)
+        for k in kwargs:
+            self.__setitem__(k, kwargs[k])
+
+
+class PropertyDict(RestrictedKeyDict):
+    """
+    A dictionary that only takes :class:`symbol` types as a key.
+    However, if a string key is provided, it will be silently converted
+    into a symbol key.
+
+        >>> from proton import symbol, ulong, PropertyDict
+        >>> a = PropertyDict(one=1, two=2)
+        >>> b = PropertyDict({'one':1, symbol('two'):2})
+        >>> c = PropertyDict(zip(['one', symbol('two')], [1, 2]))
+        >>> d = PropertyDict([(symbol('one'), 1), ('two', 2)])
+        >>> e = PropertyDict(a)
+        >>> a == b == c == d == e
+        True
+
+    By default, non-string and non-symbol keys cause a ``KeyError`` to be raised:
+
+        >>> PropertyDict({'one':1, 2:'two'})
+          ...
+        KeyError: "invalid non-symbol key: <type 'int'>: 2"
+
+    but by setting ``raise_on_error=False``, non-string and non-symbol keys will be ignored:
+
+        >>> PropertyDict({'one':1, 2:'two'}, raise_on_error=False)
+        PropertyDict({2: 'two', symbol(u'one'): 1})
+
+    :param e: Initialization for ``dict``
+    :type e: ``dict`` or ``list`` of ``tuple`` or ``zip`` object
+    :param raise_on_error: If ``True``, will raise an ``KeyError`` if a non-string or non-symbol
+        is encountered as a key in the initialization, or in a subsequent operation which
+        adds such an key. If ``False``, non-strings and non-symbols will be added as keys
+        to the dictionary without an error.
+    :type raise_on_error: ``bool``
+    :param kwargs: Keyword args for initializing a ``dict`` of the form key1=val1, key2=val2, ...
+    """
+    def __init__(self, e=None, raise_on_error=True, **kwargs):
+        super(PropertyDict, self).__init__(_check_is_symbol, e, raise_on_error, **kwargs)
+
+    def __repr__(self):
+        """ Representation of PropertyDict """
+        return 'PropertyDict(%s)' % super(PropertyDict, self).__repr__()
+
+
+class AnnotationDict(RestrictedKeyDict):
+    """
+    A dictionary that only takes :class:`symbol` or :class:`ulong` types
+    as a key. However, if a string key is provided, it will be silently
+    converted into a symbol key.
+
+        >>> from proton import symbol, ulong, AnnotationDict
+        >>> a = AnnotationDict(one=1, two=2)
+        >>> a[ulong(3)] = 'three'
+        >>> b = AnnotationDict({'one':1, symbol('two'):2, ulong(3):'three'})
+        >>> c = AnnotationDict(zip([symbol('one'), 'two', ulong(3)], [1, 2, 'three']))
+        >>> d = AnnotationDict([('one', 1), (symbol('two'), 2), (ulong(3), 'three')])
+        >>> e = AnnotationDict(a)
+        >>> a == b == c == d == e
+        True
+
+    By default, non-string, non-symbol and non-ulong keys cause a ``KeyError`` to be raised:
+
+        >>> AnnotationDict({'one': 1, 2: 'two'})
+          ...
+        KeyError: "invalid non-symbol key: <type 'int'>: 2"
+
+    but by setting ``raise_on_error=False``, non-string, non-symbol and non-ulong keys will be ignored:
+
+        >>> AnnotationDict({'one': 1, 2: 'two'}, raise_on_error=False)
+        AnnotationDict({2: 'two', symbol(u'one'): 1})
+
+    :param e: Initialization for ``dict``
+    :type e: ``dict`` or ``list`` of ``tuple`` or ``zip`` object
+    :param raise_on_error: If ``True``, will raise an ``KeyError`` if a non-string, non-symbol or
+        non-ulong is encountered as a key in the initialization, or in a subsequent
+        operation which adds such an key. If ``False``, non-strings, non-ulongs and non-symbols
+        will be added as keys to the dictionary without an error.
+    :type raise_on_error: ``bool``
+    :param kwargs: Keyword args for initializing a ``dict`` of the form key1=val1, key2=val2, ...
+    """
+    def __init__(self, e=None, raise_on_error=True, **kwargs):
+        super(AnnotationDict, self).__init__(_check_is_symbol_or_ulong, e, raise_on_error, **kwargs)
+
+    def __repr__(self):
+        """ Representation of AnnotationDict """
+        return 'AnnotationDict(%s)' % super(AnnotationDict, self).__repr__()
+
+
+class SymbolList(list):
+    """
+    A list that can only hold :class:`symbol` elements. However, if any string elements
+    are present, they will be converted to symbols.
+
+        >>> a = SymbolList(['one', symbol('two'), 'three'])
+        >>> b = SymbolList([symbol('one'), 'two', symbol('three')])
+        >>> c = SymbolList(a)
+        >>> a == b == c
+        True
+
+    By default, using any key other than a symbol or string will result in a ``TypeError``:
+
+        >>> SymbolList(['one', symbol('two'), 3])
+          ...
+        TypeError: Non-symbol type <class 'int'>: 3
+
+    but by setting ``raise_on_error=False``, non-symbol and non-string keys will be ignored:
+
+        >>> SymbolList(['one', symbol('two'), 3], raise_on_error=False)
+        SymbolList([symbol(u'one'), symbol(u'two'), 3])
+
+    :param t: Initialization for list
+    :type t: ``list``
+    :param raise_on_error: If ``True``, will raise an ``TypeError`` if a non-string or non-symbol is
+        encountered in the initialization list, or in a subsequent operation which adds such
+        an element. If ``False``, non-strings and non-symbols will be added to the list without
+        an error.
+    :type raise_on_error: ``bool``
+    """
+    def __init__(self, t=None, raise_on_error=True):
+        super(SymbolList, self).__init__()
+        self.raise_on_error = raise_on_error
+        if t:
+            self.extend(t)
+
+    def _check_list(self, t):
+        """ Check all items in list are :class:`symbol`s (or are converted to symbols). """
+        l = []
+        if t:
+            for v in t:
+                l.append(_check_is_symbol(v, self.raise_on_error))
+        return l
+
+    def append(self, v):
+        """ Add a single value v to the end of the list """
+        return super(SymbolList, self).append(_check_is_symbol(v, self.raise_on_error))
+
+    def extend(self, t):
+        """ Add all elements of an iterable t to the end of the list """
+        return super(SymbolList, self).extend(self._check_list(t))
+
+    def insert(self, i, v):
+        """ Insert a value v at index i """
+        return super(SymbolList, self).insert(i, _check_is_symbol(v, self.raise_on_error))
+
+    def __add__(self, t):
+        """ Handles list1 + list2 """
+        return SymbolList(super(SymbolList, self).__add__(self._check_list(t)), raise_on_error=self.raise_on_error)
+
+    def __iadd__(self, t):
+        """ Handles list1 += list2 """
+        return super(SymbolList, self).__iadd__(self._check_list(t))
+
+    def __setitem__(self, i, t):
+        """ Handles list[i] = v """
+        return super(SymbolList, self).__setitem__(i, _check_is_symbol(t, self.raise_on_error))
+
+    def __repr__(self):
+        """ Representation of SymbolList """
+        return 'SymbolList(%s)' % super(SymbolList, self).__repr__()
+
+
 class Data:
     """
     The :class:`Data` class provides an interface for decoding, extracting,
@@ -1410,7 +1628,10 @@ class Data:
         tuple: put_sequence,
         dict: put_dict,
         Described: put_py_described,
-        Array: put_py_array
+        Array: put_py_array,
+        AnnotationDict: put_dict,
+        PropertyDict: put_dict,
+        SymbolList: put_sequence
     }
     # for Python 3.x, long is merely an alias for int, but for Python 2.x
     # we need to add an explicit int since it is a different type
diff --git a/python/proton/_endpoints.py b/python/proton/_endpoints.py
index f4f68e2..6bbe67a 100644
--- a/python/proton/_endpoints.py
+++ b/python/proton/_endpoints.py
@@ -57,7 +57,7 @@ from cproton import PN_CONFIGURATION, PN_COORDINATOR, PN_DELIVERIES, PN_DIST_MOD
 
 from ._common import unicode2utf8, utf82unicode
 from ._condition import cond2obj, obj2cond
-from ._data import Data, dat2obj, obj2dat
+from ._data import Data, dat2obj, obj2dat, PropertyDict, SymbolList
 from ._delivery import Delivery
 from ._exceptions import ConnectionException, EXCEPTIONS, LinkException, SessionException
 from ._transport import Transport
@@ -500,6 +500,65 @@ class Connection(Wrapper, Endpoint):
         """
         pn_connection_release(self._impl)
 
+    def _get_offered_capabilities(self):
+        return self.offered_capabilities_list
+
+    def _set_offered_capabilities(self, offered_capability_list):
+        if isinstance(offered_capability_list, list):
+            self.offered_capabilities_list = SymbolList(offered_capability_list, raise_on_error=False)
+        else:
+            self.offered_capabilities_list = offered_capability_list
+
+    offered_capabilities = property(_get_offered_capabilities, _set_offered_capabilities, doc="""
+    Offered capabilities as a list of symbols. The AMQP 1.0 specification
+    restricts this list to symbol elements only. It is possible to use
+    the special ``list`` subclass :class:`SymbolList` as it will by
+    default enforce this restriction on construction. In addition, if a
+    string type is used, it will be silently converted into the required
+    symbol.
+
+    :type: ``list`` containing :class:`symbol`.
+    """)
+
+    def _get_desired_capabilities(self):
+        return self.desired_capabilities_list
+
+    def _set_desired_capabilities(self, desired_capability_list):
+        if isinstance(desired_capability_list, list):
+            self.desired_capabilities_list = SymbolList(desired_capability_list, raise_on_error=False)
+        else:
+            self.desired_capabilities_list = desired_capability_list
+
+    desired_capabilities = property(_get_desired_capabilities, _set_desired_capabilities, doc="""
+    Desired capabilities as a list of symbols. The AMQP 1.0 specification
+    restricts this list to symbol elements only. It is possible to use
+    the special ``list`` subclass :class:`SymbolList` which will by
+    default enforce this restriction on construction. In addition, if string
+    types are used, this class will be silently convert them into symbols.
+
+    :type: ``list`` containing :class:`symbol`.
+    """)
+
+    def _get_properties(self):
+        return self.properties_dict
+
+    def _set_properties(self, properties_dict):
+        if isinstance(properties_dict, dict):
+            self.properties_dict = PropertyDict(properties_dict, raise_on_error=False)
+        else:
+            self.properties_dict = properties_dict
+
+    properties = property(_get_properties, _set_properties, doc="""
+    Connection properties as a dictionary of key/values. The AMQP 1.0
+    specification restricts this dictionary to have keys that are only
+    :class:`symbol` types. It is possible to use the special ``dict``
+    subclass :class:`PropertyDict` which will by default enforce this
+    restrictions on construction. In addition, if strings type are used,
+    this will silently convert them into symbols.
+
+    :type: ``dict`` containing :class:`symbol`` keys.
+    """)
+
 
 class Session(Wrapper, Endpoint):
     """A container of links"""
diff --git a/python/proton/_message.py b/python/proton/_message.py
index f89664c..6563935 100644
--- a/python/proton/_message.py
+++ b/python/proton/_message.py
@@ -34,7 +34,7 @@ from cproton import PN_DEFAULT_PRIORITY, PN_OVERFLOW, pn_error_text, pn_message,
 
 from . import _compat
 from ._common import isinteger, millis2secs, secs2millis, unicode2utf8, utf82unicode
-from ._data import Data, symbol, ulong
+from ._data import Data, symbol, ulong, AnnotationDict
 from ._endpoints import Link
 from ._exceptions import EXCEPTIONS, MessageException
 
@@ -49,9 +49,9 @@ except NameError:
 class Message(object):
     """The :py:class:`Message` class is a mutable holder of message content.
 
-    :ivar instructions: delivery instructions for the message
+    :ivar instructions: delivery instructions for the message ("Delivery Annotations" in the AMQP 1.0 spec)
     :vartype instructions: ``dict``
-    :ivar ~.annotations: infrastructure defined message annotations
+    :ivar ~.annotations: infrastructure defined message annotations ("Message Annotations" in the AMQP 1.0 spec)
     :vartype ~.annotations: ``dict``
     :ivar ~.properties: application defined message properties
     :vartype ~.properties: ``dict``
@@ -430,6 +430,48 @@ class Message(object):
         :raise: :exc:`MessageException` if there is any Proton error when using the setter.        
         """)
 
+    def _get_instructions(self):
+        return self.instruction_dict
+
+    def _set_instructions(self, instructions):
+        if isinstance(instructions, dict):
+            self.instruction_dict = AnnotationDict(instructions, raise_on_error=False)
+        else:
+            self.instruction_dict = instructions
+
+    instructions = property(_get_instructions, _set_instructions, doc="""
+    Delivery annotations as a dictionary of key/values. The AMQP 1.0
+    specification restricts this dictionary to have keys that are either
+    :class:`symbol` or :class:`ulong` types. It is possible to use
+    the special ``dict`` subclass :class:`AnnotationDict` which
+    will by default enforce these restrictions on construction. In addition,
+    if string types are used, this class will be silently convert them into
+    symbols.
+
+    :type: :class:`AnnotationDict`. Any ``dict`` with :class:`ulong` or :class:`symbol` keys.
+    """)
+
+    def _get_annotations(self):
+        return self.annotation_dict
+
+    def _set_annotations(self, annotations):
+        if isinstance(annotations, dict):
+            self.annotation_dict = AnnotationDict(annotations, raise_on_error=False)
+        else:
+            self.annotation_dict = annotations
+
+    annotations = property(_get_annotations, _set_annotations, doc="""
+    Message annotations as a dictionary of key/values. The AMQP 1.0
+    specification restricts this dictionary to have keys that are either
+    :class:`symbol` or :class:`ulong` types. It is possible to use
+    the special ``dict`` subclass :class:`AnnotationDict` which
+    will by default enforce these restrictions on construction. In addition,
+    if a string types are used, this class will silently convert them into
+    symbols.
+
+    :type: :class:`AnnotationDict`. Any ``dict`` with :class:`ulong` or :class:`symbol` keys.
+    """)
+
     def encode(self):
         self._pre_encode()
         sz = 16
diff --git a/python/proton/_reactor.py b/python/proton/_reactor.py
index 27c228e..90d41f4 100644
--- a/python/proton/_reactor.py
+++ b/python/proton/_reactor.py
@@ -1188,9 +1188,13 @@ class Container(Reactor):
                 the client; if ``virtual_host`` is not supplied the host field
                 from the URL is used instead.
             *   ``offered_capabilities``, a list of capabilities being offered to the
-                peer.
+                peer. The list must contain symbols (or strings, which will be converted
+                to symbols).
             *   ``desired_capabilities``, a list of capabilities desired from the peer.
-            *   ``properties``, a list of connection properties
+                The list must contain symbols (or strings, which will be converted
+                to symbols).
+            *   ``properties``, a list of connection properties. This must be a map
+                with symbol keys (or string keys, which will be converted to symbol keys).
             *   ``sni`` (``str``), a hostname to use with SSL/TLS Server Name Indication (SNI)
             *   ``max_frame_size`` (``int``), the maximum allowable TCP packet size between the
                 peers.
diff --git a/python/tests/proton_tests/codec.py b/python/tests/proton_tests/codec.py
index 5a81ec5..bc66463 100644
--- a/python/tests/proton_tests/codec.py
+++ b/python/tests/proton_tests/codec.py
@@ -222,6 +222,116 @@ class DataTest(Test):
   def testDescribedEmptyArray(self):
     self._testArray("long", 0, "null")
 
+  def testPropertyDict(self):
+    a = PropertyDict(one=1, two=2, three=3)
+    b = PropertyDict({'one': 1, 'two': 2, 'three': 3})
+    c = PropertyDict(zip(['one', 'two', 'three'], [1, 2, 3]))
+    d = PropertyDict([('two', 2), ('one', 1), ('three', 3)])
+    e = PropertyDict({symbol('three'): 3, symbol('one'): 1, symbol('two'): 2})
+    f = PropertyDict(a)
+    g = PropertyDict()
+    g['one'] = 1
+    g[symbol('two')] = 2
+    g['three'] = 3
+    assert a == b == c == d == e == f == g
+    for k in a.keys():
+        assert isinstance(k, symbol)
+    self.assertRaises(KeyError, AnnotationDict, {'one': 1, None: 'none'})
+    self.assertRaises(KeyError, AnnotationDict, {'one': 1, 1.23: 4})
+
+  def testPropertyDictNoRaiseError(self):
+    a = PropertyDict(one=1, two=2, three=3, raise_on_error=False)
+    a[4] = 'four'
+    b = PropertyDict({'one': 1, 'two': 2, 'three': 3, 4: 'four'}, raise_on_error=False)
+    c = PropertyDict(zip(['one', 'two', 'three', 4], [1, 2, 3, 'four']), raise_on_error=False)
+    d = PropertyDict([('two', 2), ('one', 1), ('three', 3), (4, 'four')], raise_on_error=False)
+    e = PropertyDict({4: 'four', symbol('three'): 3, symbol('one'): 1, symbol('two'): 2}, raise_on_error=False)
+    f = PropertyDict(a, raise_on_error=False)
+    g = PropertyDict(raise_on_error=False)
+    g['one'] = 1
+    g[4] = 'four'
+    g[symbol('two')] = 2
+    g['three'] = 3
+    assert a == b == c == d == e == f == g
+
+  def testAnnotationDict(self):
+    # AnnotationMap c'tor calls update(), so this method is also covered
+    a = AnnotationDict(one=1, two=2, three=3)
+    a[ulong(4)] = 'four'
+    b = AnnotationDict({'one': 1, 'two': 2, 'three': 3, ulong(4): 'four'})
+    c = AnnotationDict(zip(['one', 'two', 'three', ulong(4)], [1, 2, 3, 'four']))
+    d = AnnotationDict([('two', 2), ('one', 1), ('three', 3), (ulong(4), 'four')])
+    e = AnnotationDict({symbol('three'): 3, ulong(4): 'four', symbol('one'): 1, symbol('two'): 2})
+    f = AnnotationDict(a)
+    g = AnnotationDict()
+    g[ulong(4)] = 'four'
+    g['one'] = 1
+    g[symbol('two')] = 2
+    g['three'] = 3
+    assert a == b == c == d == e == f == g
+    for k in a.keys():
+        assert isinstance(k, (symbol, ulong))
+    self.assertRaises(KeyError, AnnotationDict, {'one': 1, None: 'none'})
+    self.assertRaises(KeyError, AnnotationDict, {'one': 1, 1.23: 4})
+
+  def testAnnotationDictNoRaiseError(self):
+    a = AnnotationDict(one=1, two=2, three=3, raise_on_error=False)
+    a[ulong(4)] = 'four'
+    a[5] = 'five'
+    b = AnnotationDict({'one': 1, 'two': 2, 'three': 3, ulong(4): 'four', 5: 'five'}, raise_on_error=False)
+    c = AnnotationDict(zip(['one', 'two', 'three', ulong(4), 5], [1, 2, 3, 'four', 'five']), raise_on_error=False)
+    d = AnnotationDict([('two', 2), ('one', 1), ('three', 3), (ulong(4), 'four'), (5, 'five')], raise_on_error=False)
+    e = AnnotationDict({5: 'five', symbol('three'): 3, ulong(4): 'four', symbol('one'): 1, symbol('two'): 2}, raise_on_error=False)
+    f = AnnotationDict(a, raise_on_error=False)
+    g = AnnotationDict(raise_on_error=False)
+    g[ulong(4)] = 'four'
+    g['one'] = 1
+    g[symbol('two')] = 2
+    g[5] = 'five'
+    g['three'] = 3
+    assert a == b == c == d == e == f == g
+
+  def testSymbolList(self):
+    a = SymbolList(['one', 'two', 'three'])
+    b = SymbolList([symbol('one'), symbol('two'), symbol('three')])
+    c = SymbolList()
+    c.append('one')
+    c.extend([symbol('two'), 'three'])
+    d1 = SymbolList(['one'])
+    d2 = SymbolList(['two', symbol('three')])
+    d = d1 + d2
+    e = SymbolList(['one'])
+    e += SymbolList(['two', symbol('three')])
+    f = SymbolList(['one', 'hello', 'goodbye'])
+    f[1] = symbol('two')
+    f[2] = 'three'
+    g = SymbolList(a)
+    assert a == b == c == d == e == f == g
+    for v in a:
+        assert isinstance(v, symbol)
+    self.assertRaises(TypeError, SymbolList, ['one', None])
+    self.assertRaises(TypeError, SymbolList, ['one', 2])
+    self.assertRaises(TypeError, SymbolList, ['one', ['two']])
+    self.assertRaises(TypeError, SymbolList, ['one', {'two': 3}])
+
+  def testSymbolListNoRaiseError(self):
+    a = SymbolList(['one', 'two', 'three', 4], raise_on_error=False)
+    b = SymbolList([symbol('one'), symbol('two'), symbol('three'), 4], raise_on_error=False)
+    c = SymbolList(raise_on_error=False)
+    c.append('one')
+    c.extend([symbol('two'), 'three', 4])
+    d1 = SymbolList(['one'], raise_on_error=False)
+    d2 = SymbolList(['two', symbol('three'), 4], raise_on_error=False)
+    d = d1 + d2
+    e = SymbolList(['one'], raise_on_error=False)
+    e += SymbolList(['two', symbol('three'), 4], raise_on_error=False)
+    f = SymbolList(['one', 'hello', 'goodbye', 'what?'], raise_on_error=False)
+    f[1] = symbol('two')
+    f[2] = 'three'
+    f[3] = 4
+    g = SymbolList(a, raise_on_error=False)
+    assert a == b == c == d == e == f == g
+
   def _test(self, dtype, *values, **kwargs):
     eq=kwargs.get("eq", lambda x, y: x == y)
     ntype = getattr(Data, dtype.upper())
diff --git a/python/tests/proton_tests/engine.py b/python/tests/proton_tests/engine.py
index 3736cbc..99be7e6 100644
--- a/python/tests/proton_tests/engine.py
+++ b/python/tests/proton_tests/engine.py
@@ -175,7 +175,7 @@ class ConnectionTest(Test):
     assert self.c1.state == Endpoint.LOCAL_CLOSED | Endpoint.REMOTE_CLOSED
     assert self.c2.state == Endpoint.LOCAL_CLOSED | Endpoint.REMOTE_CLOSED
 
-  def test_capabilities(self):
+  def test_capabilities_array(self):
     self.c1.offered_capabilities = Array(UNDESCRIBED, Data.SYMBOL,
                                          symbol("O_one"),
                                          symbol("O_two"),
@@ -197,6 +197,21 @@ class ConnectionTest(Test):
     assert self.c2.remote_desired_capabilities == self.c1.desired_capabilities, \
         (self.c2.remote_desired_capabilities, self.c1.desired_capabilities)
 
+  def test_capabilities_symbol_list(self):
+    self.c1.offered_capabilities = SymbolList(['O_one', 'O_two', symbol('O_three')])
+    self.c1.desired_capabilities = SymbolList([symbol('D_one'), 'D_two', 'D_three'])
+    self.c1.open()
+
+    assert self.c2.remote_offered_capabilities is None
+    assert self.c2.remote_desired_capabilities is None
+
+    self.pump()
+
+    assert self.c2.remote_offered_capabilities == self.c1.offered_capabilities, \
+        (self.c2.remote_offered_capabilities, self.c1.offered_capabilities)
+    assert self.c2.remote_desired_capabilities == self.c1.desired_capabilities, \
+        (self.c2.remote_desired_capabilities, self.c1.desired_capabilities)
+
   def test_condition(self):
     self.c1.open()
     self.c2.open()
@@ -216,7 +231,7 @@ class ConnectionTest(Test):
     rcond = self.c2.remote_condition
     assert rcond == cond, (rcond, cond)
 
-  def test_properties(self, p1={symbol("key"): symbol("value")}, p2=None):
+  def test_properties(self, p1=PropertyDict(key=symbol("value")), p2=None):
     self.c1.properties = p1
     self.c2.properties = p2
     self.c1.open()
@@ -224,7 +239,7 @@ class ConnectionTest(Test):
     self.pump()
 
     assert self.c2.remote_properties == p1, (self.c2.remote_properties, p1)
-    assert self.c1.remote_properties == p2, (self.c2.remote_properties, p2)
+    assert self.c1.remote_properties == p2, (self.c1.remote_properties, p2)
 
   # The proton implementation limits channel_max to 32767.
   # If I set the application's limit lower than that, I should
diff --git a/python/tests/proton_tests/message.py b/python/tests/proton_tests/message.py
index 7f6496e..04413ee 100644
--- a/python/tests/proton_tests/message.py
+++ b/python/tests/proton_tests/message.py
@@ -119,6 +119,15 @@ class CodecTest(Test):
 
     assert msg2.properties['key'] == 'value', msg2.properties['key']
 
+  def testAnnotationsSymbolicAndUlongKey(self, a={symbol('one'): 1, 'two': 2, ulong(3): 'three'}):
+    self.msg.annotations = a
+    data = self.msg.encode()
+
+    msg2 = Message()
+    msg2.decode(data)
+    # both keys must be symbols
+    assert msg2.annotations == a
+
   def testRoundTrip(self):
     self.msg.id = "asdf"
     self.msg.correlation_id = uuid4()


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org