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