class Command(xso.XSO): TAG = (namespaces.xep0050_commands, "command") actions = xso.Child([Actions]) notes = xso.ChildList([Note]) action = xso.Attr( "action", type_=xso.EnumType(ActionType), default=ActionType.EXECUTE, ) status = xso.Attr( "status", type_=xso.EnumType(CommandStatus), default=None, ) sessionid = xso.Attr( "sessionid", default=None, ) node = xso.Attr( "node", ) payload = xso.ChildList([ aioxmpp.forms.Data, ]) def __init__(self, node, *, action=ActionType.EXECUTE, status=None, sessionid=None, payload=[], notes=[], actions=None): super().__init__() self.node = node self.action = action self.status = status self.sessionid = sessionid if not isinstance(payload, collections.abc.Iterable): self.payload[:] = [payload] else: self.payload[:] = payload self.notes[:] = notes self.actions = actions @property def first_payload(self): try: return self.payload[0] except IndexError: return
def test_parse_works_with_actual_enum(self): e = xso.EnumType(self.SomeEnum, xso.Integer()) for enum_value in self.SomeEnum: self.assertEqual( e.parse(str(enum_value.value)), enum_value, )
def test_init_with_enum(self): e = xso.EnumType(self.SomeEnum) self.assertIs(e.enum_class, self.SomeEnum) self.assertIsInstance( e.nested_type, xso.String, )
def test_format_works_with_actual_enums(self): e = xso.EnumType(self.SomeEnum, xso.Integer()) for enum_value in self.SomeEnum: self.assertEqual( e.format(enum_value), str(enum_value.value), )
def test_init_with_custom_nested_type(self): e = xso.EnumType(self.SomeEnum, nested_type=unittest.mock.sentinel.nested_type) self.assertIs(e.enum_class, self.SomeEnum) self.assertIs( e.nested_type, unittest.mock.sentinel.nested_type, )
def test_reject_non_Enum_values_on_coerce(self): wrong = [1, "10", 10.2, object()] e = xso.EnumType(self.SomeEnum) for thing in wrong: with self.assertRaises(TypeError): e.coerce(thing)
def test_allow_unknown_can_be_turned_off(self): enum_class = self.SomeEnum e = xso.EnumType( enum_class, xso.Integer(), allow_unknown=False, ) with self.assertRaisesRegex(ValueError, r"10 is not a valid SomeEnum"): e.parse(10)
def test_allow_unknown_by_default(self): enum_class = self.SomeEnum e = xso.EnumType( enum_class, xso.Integer(), ) value = e.parse("10") self.assertIsInstance(value, xso.Unknown) self.assertEqual(xso.Unknown(10), value)
def test_accept_unknown_by_default(self): enum_class = self.SomeEnum e = xso.EnumType( enum_class, xso.Integer(), ) value = e.coerce(xso.Unknown(10)) self.assertIsInstance(value, xso.Unknown) self.assertEqual(xso.Unknown(10), value)
def test_value_error_propagates(self): exc = ValueError() enum_class = unittest.mock.Mock() enum_class.side_effect = exc e = xso.EnumType(enum_class, allow_coerce=True) with self.assertRaises(ValueError) as ctx: e.coerce(unittest.mock.sentinel.value) self.assertIs(ctx.exception, exc)
def test_format_works_with_unknown(self): enum_class = self.SomeEnum e = xso.EnumType( enum_class, xso.Integer(), ) self.assertEqual( e.format(xso.Unknown(10)), "10", )
def test_accept_unknown_can_be_turned_off(self): enum_class = self.SomeEnum e = xso.EnumType( enum_class, xso.Integer(), accept_unknown=False, ) with self.assertRaisesRegex(TypeError, r"not a valid .* value: <Unknown: 10>"): e.coerce(xso.Unknown(10))
def test_get_formatted_type_delegates_to_nested_type(self): nested_type = unittest.mock.Mock() e = xso.EnumType( unittest.mock.sentinel.enum_class, nested_type, ) result = e.get_formatted_type() nested_type.get_formatted_type.assert_called_with() self.assertEqual( result, nested_type.get_formatted_type(), )
def test_format_uses_enum_value_and_nested_type(self): enum_class = unittest.mock.Mock() enum_value = unittest.mock.Mock() nested_type = unittest.mock.Mock() e = xso.EnumType(enum_class, nested_type) result = e.format(enum_value) nested_type.format.assert_called_with(enum_value.value, ) self.assertEqual( result, nested_type.format(), )
def test_parse_uses_enum_and_nested_type(self): enum_class = unittest.mock.Mock() nested_type = unittest.mock.Mock() e = xso.EnumType(enum_class, nested_type) result = e.parse(unittest.mock.sentinel.value) nested_type.parse.assert_called_with(unittest.mock.sentinel.value, ) enum_class.assert_called_with(nested_type.parse(), ) self.assertEqual( result, enum_class(), )
class Actions(xso.XSO): TAG = (namespaces.xep0050_commands, "actions") next_is_allowed = xso.ChildFlag( (namespaces.xep0050_commands, "next"), ) prev_is_allowed = xso.ChildFlag( (namespaces.xep0050_commands, "prev"), ) complete_is_allowed = xso.ChildFlag( (namespaces.xep0050_commands, "complete"), ) execute = xso.Attr( "execute", type_=xso.EnumType(ActionType), validator=xso.RestrictToSet({ ActionType.NEXT, ActionType.PREV, ActionType.COMPLETE, }), default=None, ) @property def allowed_actions(self): result = [ActionType.EXECUTE, ActionType.CANCEL] if self.prev_is_allowed: result.append(ActionType.PREV) if self.next_is_allowed: result.append(ActionType.NEXT) if self.complete_is_allowed: result.append(ActionType.COMPLETE) return frozenset(result) @allowed_actions.setter def allowed_actions(self, values): values = frozenset(values) if ActionType.EXECUTE not in values: raise ValueError("EXECUTE must always be allowed") if ActionType.CANCEL not in values: raise ValueError("CANCEL must always be allowed") self.prev_is_allowed = ActionType.PREV in values self.next_is_allowed = ActionType.NEXT in values self.complete_is_allowed = ActionType.COMPLETE in values
def test_try_to_coerce_if_allow_coerce_is_set(self): enum_class = unittest.mock.Mock() e = xso.EnumType( enum_class, allow_coerce=True, ) with warnings.catch_warnings() as w: result = e.coerce(unittest.mock.sentinel.value) enum_class.assert_called_with(unittest.mock.sentinel.value, ) self.assertEqual( result, enum_class(), ) self.assertFalse(w)
class Note(xso.XSO): TAG = (namespaces.xep0050_commands, "note") body = xso.Text( default=None, ) type_ = xso.Attr( "type", type_=xso.EnumType( NoteType, ), default=NoteType.INFO, ) def __init__(self, type_, body): super().__init__() self.type_ = type_ self.body = body
def test_deprecate_coerce_does_not_emit_warning_for_enum_value(self): enum_class = self.SomeEnum e = xso.EnumType( enum_class, allow_coerce=True, deprecate_coerce=True, ) value = enum_class.X with contextlib.ExitStack() as stack: warn = stack.enter_context(unittest.mock.patch("warnings.warn", )) result = e.coerce(value) self.assertFalse(warn.mock_calls) self.assertIs( value, result, )
def test_deprecate_coerce_custom_stacklevel(self): enum_class = self.SomeEnum e = xso.EnumType( enum_class, allow_coerce=True, deprecate_coerce=unittest.mock.sentinel.stacklevel, ) with contextlib.ExitStack() as stack: warn = stack.enter_context(unittest.mock.patch("warnings.warn", )) result = e.coerce(1) warn.assert_called_with( "assignment of non-enum values to this descriptor is deprecated", DeprecationWarning, stacklevel=unittest.mock.sentinel.stacklevel) self.assertEqual( result, enum_class(1), )
class Data(AbstractItem): """ A :xep:`4` ``x`` element, that is, a Data Form. :param type_: Initial value for the :attr:`type_` attribute. .. attribute:: type_ The ``type`` attribute of the form, represented by one of the members of the :class:`DataType` enumeration. .. attribute:: title The (optional) title of the form. Either a :class:`str` or :data:`None`. .. attribute:: instructions A sequence of strings which represent the instructions elements on the form. .. attribute:: fields If the :class:`Data` is a form, this is a sequence of :class:`Field` elements which represent the fields to be filled in. This does not make sense on :attr:`.DataType.RESULT` typed objects. .. attribute:: items If the :class:`Data` is a table, this is a sequence of :class:`Item` instances which represent the table rows. This only makes sense on :attr:`.DataType.RESULT` typed objects. .. attribute:: reported If the :class:`Data` is a table, this is a :class:`Reported` object representing the table header. This only makes sense on :attr:`.DataType.RESULT` typed objects. """ TAG = (namespaces.xep0004_data, "x") type_ = xso.Attr( "type", type_=xso.EnumType(DataType) ) title = xso.ChildText( (namespaces.xep0004_data, "title"), default=None, ) instructions = xso.ChildValueList( type_=InstructionsElement() ) items = xso.ChildList([Item]) reported = xso.Child([Reported], required=False) def __init__(self, type_): super().__init__() self.type_ = type_ def _validate_result(self): if self.fields: raise ValueError("field in report result") fieldvars = {field.var for field in self.reported.fields} if not fieldvars: raise ValueError("empty report header") for item in self.items: itemvars = {field.var for field in item.fields} if itemvars != fieldvars: raise ValueError("field mismatch between row and header") def validate(self): super().validate() if (self.type_ != DataType.RESULT and (self.reported is not None or self.items)): raise ValueError("report in non-result") if (self.type_ == DataType.RESULT and (self.reported is not None or self.items)): self._validate_result()
class Field(xso.XSO): """ Represent a single field in a Data Form. :param type_: Field type, must be one of the valid field types specified in :xep:`4`. :type type_: :class:`FieldType` :param options: A mapping of values to labels defining the options in a ``list-*`` field. :type options: :class:`dict` mapping :class:`str` to :class:`str` :param values: A sequence of values currently given for the field. Having more than one value is only valid in ``*-multi`` fields. :type values: :class:`list` of :class:`str` :param desc: Description which can be shown in a tool-tip or similar, without newlines. :type desc: :class:`str` or :data:`None`s :param label: Human-readable label to be shown next to the field input :type label: :class:`str` or :data:`None` :param required: Flag to indicate that the field is required :type required: :class:`bool` :param var: "ID" identifying the field uniquely inside the form. Only required for fields carrying a meaning (thus, not for ``fixed``). :type var: :class:`str` or :data:`None` The semantics of a :class:`Field` are different depending on where it occurs: in a :class:`Data`, it is a form field to be filled in, in a :class:`Item` it is a cell of a row and in a :class:`Reported` it represents a column header. .. attribute:: required A boolean flag indicating whether the field is required. If true, the XML serialisation will contain the corresponding ``<required/>`` tag. .. attribute:: desc Single line of description for the field. This attribute represents the ``<desc/>`` element from :xep:`4`. .. attribute:: values A sequence of strings representing the ``<value/>`` elements of the field, one string for each value. .. note:: Since the requirements on the sequence of strings in :attr:`values` change depending on the :attr:`type_` attribute, validation and type conversion on assignment is very lax. The attribute accepts all sequences of strings, even if the field is for example a :attr:`FieldType.BOOLEAN` field, which allows for at most one string of a well-defined format (see the documentation there for the details). This makes it easy to inadvertendly generate invalid forms, which is why you should be using :class:`Form` subclasses when accessing forms from within normal code and some other, generic mechanism taking care of these details when showing forms in a UI framework to users. Note that devising such a mechanism is out of scope for :mod:`aioxmpp`, as every UI framework has different requirements. .. attribute:: options A dictionary mapping values to human-readable labels, representing the ``<option/>`` elements of the field. .. attribute:: var The uniquely identifying string of the (valued, that is, non-:attr:`FieldType.FIXED` field). Represents the ``var`` attribute of the field. .. attribute:: type_ The type of the field. The :attr:`type_` must be a :class:`FieldType` enumeration value and determines restrictions and constraints on other attributes. See the :class:`FieldType` enumeration and :xep:`4` for details. .. attribute:: label The human-readable label for the field, representing the ``label`` attribute of the field. May be :data:`None` if the label is omitted. """ TAG = (namespaces.xep0004_data, "field") required = xso.ChildFlag( (namespaces.xep0004_data, "required"), ) desc = xso.ChildText( (namespaces.xep0004_data, "desc"), default=None ) values = xso.ChildValueList( type_=ValueElement() ) options = xso.ChildValueMap( type_=OptionElement(), mapping_type=collections.OrderedDict, ) var = xso.Attr( (None, "var"), default=None ) type_ = xso.Attr( (None, "type"), type_=xso.EnumType( FieldType, ), default=FieldType.TEXT_SINGLE, ) label = xso.Attr( (None, "label"), default=None ) def __init__(self, *, type_=FieldType.TEXT_SINGLE, options={}, values=[], desc=None, label=None, required=False, var=None): super().__init__() self.type_ = type_ self.options.update(options) self.values[:] = values self.desc = desc self.label = label self.required = required self.var = var def validate(self): super().validate() if self.type_ != FieldType.FIXED and not self.var: raise ValueError("missing attribute var") if not self.type_.has_options and self.options: raise ValueError("unexpected option on non-list field") if not self.type_.is_multivalued and len(self.values) > 1: raise ValueError("too many values on non-multi field") values_list = [opt for opt in self.options.values()] values_set = set(values_list) if len(values_list) != len(values_set): raise ValueError("duplicate option value")
def test_pass_Enum_values_through_coerce(self): e = xso.EnumType(self.SomeEnum) for enum_value in self.SomeEnum: self.assertIs(enum_value, e.coerce(enum_value))
def test_init_default(self): with self.assertRaises(TypeError): xso.EnumType()