Index: src/lxml/objectify.pyx =================================================================== --- src/lxml/objectify.pyx (revision 44927) +++ src/lxml/objectify.pyx (working copy) @@ -916,6 +916,15 @@ def __lower_bool(b): return _lower_bool(b) +cdef _get_pytypename(obj): + if isinstance(obj, unicode): + return "str" + else: + return type(obj).__name__ + +def __get_pytypename(obj): + return _get_pytypename(obj) + cdef _registerPyTypes(): pytype = PyType('int', int, IntElement) pytype.xmlSchemaTypes = ("int", "short", "byte", "unsignedShort", @@ -943,7 +952,7 @@ "NMTOKEN", ) pytype.register() - pytype = PyType('none', None, NoneElement) + pytype = PyType('NoneType', None, NoneElement) pytype.register() _registerPyTypes() @@ -1011,6 +1020,53 @@ typemap = _ObjectifyTypemap(typemap) _ElementMaker.__init__(self, typemap, objectify_parser.makeelement) +class TypedElementMaker(_ElementMaker): + def __init__(self, typemap=None): + typemap = _ObjectifyTypemap(typemap) + _ElementMaker.__init__(self, typemap, Element) + + def __call__(self, tag, *children, **attrib): + get = self._typemap.get + + istree = False + for arg in children: + if etree.iselement(arg): + istree = True + break + + typename = None + init_attrib = {} + if not istree: + if len(children) > 1: + # must concatenate values: only str type makes sense + typename = "str" + elif len(children) == 1: + # use python type of the argument value + typename = __get_pytypename(children[0]) + + if typename == "NoneType": + init_attrib = { XML_SCHEMA_INSTANCE_NIL_ATTR: "true" } + + elem = self._makeelement(tag, attrib=init_attrib, _pytype=typename) + if attrib: + get(dict)(elem, attrib) + + for item in children: + if callable(item): + item = item() + t = get(type(item)) + if t is None: + if etree.iselement(item): + elem.append(item) + continue + raise TypeError("bad argument type: %r" % item) + else: + v = t(elem, item) + if v: + get(type(v))(elem, v) + + return elem + cdef class _ObjectifyTypemap: """Type map for the ElementMaker. """ @@ -1041,6 +1097,10 @@ self._typemap[__builtin__.bool] = __add_bool self._typemap[__builtin__.bool.__name__] = __add_bool + NoneType = type(__builtin__.None) + self._typemap[NoneType] = __add_none + self._typemap[NoneType.__name__] = __add_none + def copy(self): return self @@ -1072,7 +1132,7 @@ def __add_bool(_Element elem not None, boolval): _add_text(elem, _lower_bool(boolval)) - + def __add_text(_Element elem not None, text): _add_text(elem, text) @@ -1090,6 +1150,9 @@ text = old + text cetree.setNodeText(elem._c_node, text) +def __add_none(_Element elem not None, noneval): + pass + ################################################################################ # Recursive element dumping @@ -1654,7 +1717,7 @@ empty_pytype = None StrType = _PYTYPE_DICT.get('str') - NoneType = _PYTYPE_DICT.get('none') + NoneType = _PYTYPE_DICT.get('NoneType') c_node = element._c_node tree.BEGIN_FOR_EACH_ELEMENT_FROM(c_node, c_node, 1) if c_node.type == tree.XML_ELEMENT_NODE: @@ -1960,6 +2023,7 @@ return _parse(f, parser) E = ElementMaker() +T = TypedElementMaker() cdef object _DEFAULT_NSMAP _DEFAULT_NSMAP = { "py" : PYTYPE_NAMESPACE, @@ -1985,12 +2049,16 @@ def DataElement(_value, attrib=None, nsmap=None, _pytype=None, _xsi=None, **_attributes): - """Create a new element with a Python value and XML attributes taken from + """Create a new element from a Python value and XML attributes taken from keyword arguments or a dictionary passed as second argument. Automatically adds a 'pytype' attribute for the Python type of the value, if the type can be identified. If '_pytype' or '_xsi' are among the keyword arguments, they will be used instead. + + If the _value argument is an ObjectifiedDataElement instance, its py:pytype, + xsi:type and other attributes and nsmap are reused unless they are redefined + in attrib and/or keyword arguments. """ cdef python.PyObject* dict_result if nsmap is None: @@ -2023,6 +2091,7 @@ dict_result = python.PyDict_GetItem(_attributes, PYTYPE_ATTRIBUTE) if dict_result is not NULL: _pytype = dict_result + if _xsi is not None: if ':' in _xsi: prefix, name = _xsi.split(':', 1) @@ -2059,23 +2128,20 @@ strval = "false" elif _value is None: strval = None + _pytype = "NoneType" else: strval = str(_value) if _pytype is None: - if strval is not None: - for type_check, pytype in _TYPE_CHECKS: - try: - type_check(strval) - _pytype = (pytype).name - break - except IGNORABLE_ERRORS: - pass + for type_check, pytype in _TYPE_CHECKS: + try: + type_check(strval) + _pytype = (pytype).name + break + except IGNORABLE_ERRORS: + pass if _pytype is None: - if _value is None: - python.PyDict_SetItem(_attributes, XML_SCHEMA_INSTANCE_NIL_ATTR, "true") - elif python._isString(_value): - _pytype = "str" + _pytype = "str" else: # check if type information from arguments is valid dict_result = python.PyDict_GetItem(_PYTYPE_DICT, _pytype) @@ -2084,7 +2150,23 @@ if type_check is not None: type_check(strval) - if _pytype is not None: - python.PyDict_SetItem(_attributes, PYTYPE_ATTRIBUTE, _pytype) + if _pytype is not None: + if _pytype == "NoneType": + strval = None + python.PyDict_SetItem(_attributes, XML_SCHEMA_INSTANCE_NIL_ATTR, "true") + else: + python.PyDict_SetItem(_attributes, PYTYPE_ATTRIBUTE, _pytype) return _makeElement("value", strval, _attributes, nsmap) + +def PT(value): + """Create a new pytype-annotated element from a Python value. + + Type annotation uses the Python type name of value. If the argument value is + an ObjectifiedElement instead of a Python value, a copy of the element gets + returned, without trying to add or modify annotation. + """ + if isinstance(value, ObjectifiedElement): + return value.__copy__() + else: + return DataElement(value, _pytype=__get_pytypename(value)) Index: src/lxml/tests/test_objectify.py =================================================================== --- src/lxml/tests/test_objectify.py (revision 44927) +++ src/lxml/tests/test_objectify.py (working copy) @@ -40,6 +40,18 @@ xsitype2objclass = dict(( (v, k) for k in objectclass2xsitype for v in objectclass2xsitype[k] )) +objectclass2pytype = { + # objectify built-in + objectify.IntElement: "int", + objectify.LongElement: "long", + objectify.FloatElement: "float", + objectify.BoolElement: "bool", + objectify.StringElement: "str", + # None: xsi:nil="true" + } + +pytype2objclass = dict(( (objectclass2pytype[k], k) for k in objectclass2pytype)) + xml_str = '''\ @@ -209,6 +221,22 @@ for attr in arg.attrib: self.assertEquals(value.get(attr), arg.get(attr)) + def test_data_element_data_element_arg_pytype_none(self): + # Check that _pytype arg overrides original py:pytype of + # ObjectifiedDataElement + arg = objectify.DataElement(23, _pytype="str", _xsi="foobar", + attrib={"gnu": "muh", "cat": "meeow", + "dog": "wuff"}, + bird="tchilp", dog="grrr") + value = objectify.DataElement(arg, _pytype="NoneType") + self.assert_(isinstance(value, objectify.NoneElement)) + self.assertEquals(value.get(XML_SCHEMA_NIL_ATTR), "true") + self.assertEquals(value.text, None) + self.assertEquals(value.pyval, None) + for attr in arg.attrib: + #if not attr == objectify.PYTYPE_ATTRIBUTE: + self.assertEquals(value.get(attr), arg.get(attr)) + def test_data_element_data_element_arg_pytype(self): # Check that _pytype arg overrides original py:pytype of # ObjectifiedDataElement @@ -485,7 +513,7 @@ self.assert_(isinstance(root.a, objectify.IntElement)) self.assert_(isinstance(root.b, objectify.IntElement)) - def test_type_none(self): + def test_type_NoneType(self): Element = self.Element SubElement = self.etree.SubElement @@ -499,7 +527,7 @@ self.assertEquals(root.none[1], None) self.assertFalse(root.none[1]) - def test_data_element_none(self): + def test_data_element_NoneType(self): value = objectify.DataElement(None) self.assert_(isinstance(value, objectify.NoneElement)) self.assertEquals(value, None) @@ -587,14 +615,20 @@ def test_data_element_xsitypes(self): for xsi, objclass in xsitype2objclass.iteritems(): # 1 is a valid value for all ObjectifiedDataElement classes - value = objectify.DataElement(1, _xsi=xsi) - self.assert_(isinstance(value, objclass)) + pyval = 1 + value = objectify.DataElement(pyval, _xsi=xsi) + self.assert_(isinstance(value, objclass), + "DataElement(%s, _xsi='%s') returns %s, expected %s" + % (pyval, xsi, type(value), objclass)) def test_data_element_xsitypes_xsdprefixed(self): for xsi, objclass in xsitype2objclass.iteritems(): # 1 is a valid value for all ObjectifiedDataElement classes - value = objectify.DataElement(1, _xsi="xsd:%s" % xsi) - self.assert_(isinstance(value, objclass)) + pyval = 1 + value = objectify.DataElement(pyval, _xsi="xsd:%s" % xsi) + self.assert_(isinstance(value, objclass), + "DataElement(%s, _xsi='%s') returns %s, expected %s" + % (pyval, xsi, type(value), objclass)) def test_data_element_xsitypes_prefixed(self): for xsi, objclass in xsitype2objclass.iteritems(): @@ -602,6 +636,26 @@ self.assertRaises(ValueError, objectify.DataElement, 1, _xsi="foo:%s" % xsi) + def test_data_element_pytypes(self): + for pytype, objclass in pytype2objclass.iteritems(): + # 1 is a valid value for all ObjectifiedDataElement classes + pyval = 1 + value = objectify.DataElement(pyval, _pytype=pytype) + self.assert_(isinstance(value, objclass), + "DataElement(%s, _pytype='%s') returns %s, expected %s" + % (pyval, pytype, type(value), objclass)) + + def test_data_element_pytype_none(self): + pyval = 1 + pytype = "NoneType" + objclass = objectify.NoneElement + value = objectify.DataElement(pyval, _pytype=pytype) + self.assert_(isinstance(value, objclass), + "DataElement(%s, _pytype='%s') returns %s, expected %s" + % (pyval, pytype, type(value), objclass)) + self.assertEquals(value.text, None) + self.assertEquals(value.pyval, None) + def test_schema_types(self): XML = self.XML root = XML('''\ @@ -858,7 +912,7 @@ self.assertEquals("float", child_types[ 2]) self.assertEquals("str", child_types[ 3]) self.assertEquals("bool", child_types[ 4]) - self.assertEquals("none", child_types[ 5]) + self.assertEquals("NoneType", child_types[ 5]) self.assertEquals(None, child_types[ 6]) self.assertEquals("float", child_types[ 7]) self.assertEquals("float", child_types[ 8]) @@ -918,7 +972,7 @@ self.assertEquals("float", child_types[ 2]) self.assertEquals("str", child_types[ 3]) self.assertEquals("bool", child_types[ 4]) - self.assertEquals("none", child_types[ 5]) + self.assertEquals("NoneType", child_types[ 5]) self.assertEquals(None, child_types[ 6]) self.assertEquals("float", child_types[ 7]) self.assertEquals("float", child_types[ 8]) @@ -1112,7 +1166,7 @@ self.assertEquals("float", child_types[ 2]) self.assertEquals("str", child_types[ 3]) self.assertEquals("bool", child_types[ 4]) - self.assertEquals("none", child_types[ 5]) + self.assertEquals("NoneType", child_types[ 5]) self.assertEquals(None, child_types[ 6]) self.assertEquals("float", child_types[ 7]) self.assertEquals("float", child_types[ 8]) @@ -1590,6 +1644,226 @@ etree.tostring(new_root), etree.tostring(root)) + # E-Factory tests, need to use sub-elements as root element is always + # type-looked-up as ObjectifiedElement (no annotations) + def test_efactory_int(self): + E = objectify.E + root = E.root(E.val(23)) + self.assert_(isinstance(root.val, objectify.IntElement)) + + def test_efactory_long(self): + E = objectify.E + root = E.root(E.val(23L)) + self.assert_(isinstance(root.val, objectify.IntElement)) + + def test_efactory_float(self): + E = objectify.E + root = E.root(E.val(233.23)) + self.assert_(isinstance(root.val, objectify.FloatElement)) + + def test_efactory_str(self): + E = objectify.E + root = E.root(E.val("what?")) + self.assert_(isinstance(root.val, objectify.StringElement)) + + def test_efactory_unicode(self): + E = objectify.E + root = E.root(E.val(unicode("blöödy häll", encoding="ISO-8859-1"))) + self.assert_(isinstance(root.val, objectify.StringElement)) + + def test_efactory_bool(self): + E = objectify.E + root = E.root(E.val(True)) + self.assert_(isinstance(root.val, objectify.BoolElement)) + + def test_efactory_none(self): + E = objectify.E + root = E.root(E.val(None)) + # E-factory does not special-case None + self.assert_(isinstance(root.val, objectify.StringElement)) + + def test_efactory_value_concatenation(self): + E = objectify.E + root = E.root(E.val(1, "foo", 2.0, "bar ", True, None)) + self.assert_(isinstance(root.val, objectify.StringElement)) + + def test_efactory_attrib(self): + E = objectify.E + root = E.root(foo="bar") + self.assertEquals(root.get("foo"), "bar") + + def test_efactory_nested(self): + E = objectify.E + DataElement = objectify.DataElement + root = E.root("text", E.sub(E.subsub()), "tail", DataElement(1), + DataElement(2.0)) + self.assert_(isinstance(root, objectify.ObjectifiedElement)) + self.assertEquals(root.text, "text") + self.assert_(isinstance(root.sub, objectify.ObjectifiedElement)) + self.assertEquals(root.sub.tail, "tail") + self.assert_(isinstance(root.sub.subsub, objectify.StringElement)) + self.assertEquals(len(root.value), 2) + self.assert_(isinstance(root.value[0], objectify.IntElement)) + self.assert_(isinstance(root.value[1], objectify.FloatElement)) + + # T-Factory tests + def test_tfactory_int(self): + T = objectify.T + val = T.val(23) + self.assert_(isinstance(val, objectify.IntElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "int") + + def test_tfactory_long(self): + T = objectify.T + val = T.val(23L) + self.assert_(isinstance(val, objectify.LongElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "long") + + def test_tfactory_float(self): + T = objectify.T + val = T.val(233.23) + self.assert_(isinstance(val, objectify.FloatElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "float") + + def test_tfactory_str(self): + T = objectify.T + val = T.val("what?") + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + + def test_tfactory_unicode(self): + T = objectify.T + val = T.val(unicode("blöödy häll", encoding="ISO-8859-1")) + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + + def test_tfactory_bool(self): + T = objectify.T + val = T.val(True) + self.assert_(isinstance(val, objectify.BoolElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "bool") + + def test_tfactory_none(self): + T = objectify.T + val = T.val(None) + # T-factory *does* special-case None + self.assert_(isinstance(val, objectify.NoneElement)) + self.assertEquals(val.get(XML_SCHEMA_NIL_ATTR), "true") + + def test_tfactory_str_ambiguous_literals(self): + T = objectify.T + for input in ["1", "2.0", "True", "False", "true", "false"]: + val = T.val(input) + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + + def test_tfactory_unicode_ambiguous_literals(self): + T = objectify.T + for input in [u"1", u"2.0", u"True", u"False", u"true", u"false"]: + val = T.val(input) + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + + def test_tfactory_value_concatenation(self): + T = objectify.T + val = T.val(1, "foo", 2.0, "bar ", True, None) + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + + def test_tfactory_attrib(self): + T = objectify.T + root = T.root(foo="bar") + self.assertEquals(root.get("foo"), "bar") + + def test_tfactory_nested(self): + T = objectify.T + DataElement = objectify.DataElement + root = T.root("text", T.sub(T.subsub()), "tail", DataElement(1), + DataElement(2.0)) + self.assert_(isinstance(root, objectify.ObjectifiedElement)) + self.assertEquals(root.text, "text") + self.assert_(isinstance(root.sub, objectify.ObjectifiedElement)) + self.assertEquals(root.sub.tail, "tail") + self.assert_(isinstance(root.sub.subsub, objectify.ObjectifiedElement)) + self.assertEquals(len(root.value), 2) + self.assert_(isinstance(root.value[0], objectify.IntElement)) + self.assert_(isinstance(root.value[1], objectify.FloatElement)) + + # PT DataElement() wrapper tests + def test_PT_int(self): + PT = objectify.PT + val = PT(23) + self.assert_(isinstance(val, objectify.IntElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "int") + + def test_PT_long(self): + PT = objectify.PT + val = PT(2323872937827979797879797797L) + self.assert_(isinstance(val, objectify.LongElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "long") + + def test_PT_float(self): + PT = objectify.PT + val = PT(3.14159) + self.assert_(isinstance(val, objectify.FloatElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "float") + + def test_PT_str(self): + PT = objectify.PT + val = PT("what?") + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + + def test_PT_unicode(self): + PT = objectify.PT + val = PT(unicode("blöödy häll", encoding="ISO-8859-1")) + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + + def test_PT_bool(self): + PT = objectify.PT + val = PT(False) + self.assert_(isinstance(val, objectify.BoolElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "bool") + + def test_PT_none(self): + PT = objectify.PT + val = PT(None) + self.assert_(isinstance(val, objectify.NoneElement)) + self.assertEquals(val.get(XML_SCHEMA_NIL_ATTR), "true") + + def test_PT_data_element(self): + PT = objectify.PT + DataElement = objectify.DataElement + input = DataElement("I am a data element", foo="bar") + val = PT(input) + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get("foo"), "bar") + self.assertNotEquals(id(input), id(val)) + + def test_PT_element(self): + PT = objectify.PT + Element = objectify.Element + input = Element("root", foo="bar") + val = PT(input) + self.assert_(isinstance(val, objectify.ObjectifiedElement)) + self.assertEquals(val.get("foo"), "bar") + self.assertNotEquals(id(input), id(val)) + + def test_PT_str_ambiguous_literals(self): + PT = objectify.PT + for input in ["1", "2.0", "True", "False", "true", "false"]: + val = PT(input) + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + + def test_PT_unicode_ambiguous_literals(self): + PT = objectify.PT + for input in [u"1", u"2.0", u"True", u"False", u"true", u"false"]: + val = PT(input) + self.assert_(isinstance(val, objectify.StringElement)) + self.assertEquals(val.get(objectify.PYTYPE_ATTRIBUTE), "str") + def test_suite(): suite = unittest.TestSuite() suite.addTests([unittest.makeSuite(ObjectifyTestCase)]) Index: doc/objectify.txt =================================================================== --- doc/objectify.txt (revision 44927) +++ doc/objectify.txt (working copy) @@ -753,7 +753,7 @@ ... ... 5 ... 5 - ... + ... ... ... """ % ns)