Using Binding Classes

Python instances corresponding to XML structures can be created in two primary ways: from XML documents, and directly within Python code. Generating XML documents from bindings can also be controlled.

Creating Instances from XML Documents

XML documents are converted into Python bindings by invoking the CreateFromDocument function in a binding module. For example:

import po3

order = po3.CreateFromDocument(file('po3.xml').read())

print '%s is sending %s %d thing(s):' % (order.billTo.name, order.shipTo.name, len(order.items.item))
for item in order.items.item:
    print '  Quantity %d of %s at $%s' % (item.quantity, item.productName, item.USPrice)

The CreateFromDocument function in a given binding module is configured so that documents with no default namespace are assumed to be in the namespace from which the binding was generated.

Locating Invalid Content

If a document does not validate, PyXB will generally through an UnrecognizedContentError exception. You can determine where the problem lies, and what was not recognized, by examining attributes present on the exception as shown in this example:

import pyxb
import po1

xml = file('badcontent.xml').read()
try:
    order = po1.CreateFromDocument(xml, location_base='badcontent.xml')
except pyxb.UnrecognizedContentError, e:
    print 'Unrecognized element "%s" at %s' % (e.content.expanded_name, e.content.location)

which produces:

Unrecognized element "streeet" at badcontent.xml[5:4]

Coping With Wrong xsi:type Attributes

Some web services and binding tools mis-use xsi:type, providing attribute values that either are not types, or do not specify a type that is derived from an abstract type. The pyxb.namespace.builtin.XMLSchema_instance.ProcessTypeAttribute method can be used to relax how PyXB processes those attributes.

Creating Instances in Python Code

Creating bindings from XML documents is straightforward, because the documents contain enough information to identify each element and attribute, locate the corresponding use in the binding class, and store a value that is converted to the appropriate type. Creating values in Python is inherently more complex, because native Python objects like strings and integers do not contain this information.

As described in Binding Model, binding classes corresponding to simple types extend the underlying Python type (such as str or int), and add XML-specific information like the canonical representation of the value in Unicode, which is the natural representation as XML text. These classes also maintain a set of facets that constrain the values that can be stored as instances when validation is active. Binding classes for complex types have constructors that parse positional and keyword parameters to determine the appropriate element or attribute to which the value belongs. Attributes are assigned using keyword parameters. Content is assigned using positional parameters. The order of the positional parameters must be consistent with the order expected by the content model.

Using the schema in the namespace-aware address schema, we can begin to construct the example document in Python:

import address

addr = address.USAddress()
addr.name = 'Robert Smith'
addr.street = '8 Oak Avenue'
addr.city = 'Anytown'
addr.state = 'AK'
addr.zip = 12341

print addr.toxml()

This produces:

<?xml version="1.0" ?><ns1:USAddress xmlns:ns1="URN:address"><name>Robert Smith</name><street>8 Oak Avenue</street><city>Anytown</city><state>AK</state><zip>12341</zip></ns1:USAddress>

Assigning to individual fields like this bypasses the complex type content model, although each field itself is validated. For example, the address schema does not include New York as a state, so the following assignment:

addr.state = 'NY'

will cause a BadTypeValueError exception to be raised:

Traceback (most recent call last):
  File "demo4a1.py", line 7, in <module>
    addr.state = 'NY'
  File "/home/pab/pyxb/dev/pyxb/binding/basis.py", line 62, in __setattr__
    return super(_TypeBinding_mixin, self).__setattr__(name, value)
  File "/home/pab/pyxb/dev/pyxb/binding/content.py", line 399, in set
    value = self.__elementBinding.compatibleValue(value, is_plural=self.isPlural())
  File "/home/pab/pyxb/dev/pyxb/binding/basis.py", line 1331, in compatibleValue
    return self.typeDefinition()._CompatibleValue(value, **kw)
  File "/home/pab/pyxb/dev/pyxb/binding/basis.py", line 270, in _CompatibleValue
    return cls(value)
  File "/home/pab/pyxb/dev/pyxb/binding/basis.py", line 712, in __init__
    self.xsdConstraintsOK()
  File "/home/pab/pyxb/dev/pyxb/binding/basis.py", line 845, in xsdConstraintsOK
    return self.XsdConstraintsOK(self)
  File "/home/pab/pyxb/dev/pyxb/binding/basis.py", line 839, in XsdConstraintsOK
    raise pyxb.BadTypeValueError('%s violation for %s in %s' % (f.Name(), value, cls.__name__))
pyxb.exceptions_.BadTypeValueError: enumeration violation for NY in USState

However, the order of the field assignments does not matter, as long as all required fields are present by the time the XML document is generated.

import address

addr = address.USAddress()
addr.street = '8 Oak Avenue'
addr.state = 'AK'
addr.city = 'Anytown'
addr.zip = 12341
addr.name = 'Robert Smith'

print addr.toxml()

Alternatively, you can provide the content as positional parameters in the object creation call:

# examples/manual/demo4b.py

import address

addr = address.USAddress('Robert Smith', '8 Oak Avenue', 'Anytown', 'AK', 12341)

print addr.toxml()

This has the same effect, and is much more compact, but it does require that the order match the content model.

Attributes are set using keyword parameters:

# examples/manual/demo4c.py

import pyxb
import po4
import address
import pyxb.binding.datatypes as xs

po = po4.purchaseOrder(orderDate=xs.date(1999, 10, 20))
po.shipTo = address.USAddress('Alice Smith', '123 Maple Street', 'Anytown', 'AK', 12341)
po.billTo = address.USAddress('Robert Smith', '8 Oak Avenue', 'Anytown', 'AK', 12341)
                
pyxb.RequireValidWhenGenerating(False)
print po.toxml()

This example produces (after reformatting):

<?xml version="1.0"?>
<ns1:purchaseOrder xmlns:ns1="URN:purchase-order" orderDate="1999-10-20">
  <ns1:billTo>
    <city>Anytown</city>
    <state>AK</state>
    <street>8 Oak Avenue</street>
    <name>Robert Smith</name>
    <zip>12341</zip>
  </ns1:billTo>
  <ns1:shipTo>
    <city>Anytown</city>
    <state>AK</state>
    <street>123 Maple Street</street>
    <name>Alice Smith</name>
    <zip>12341</zip>
  </ns1:shipTo>
</ns1:purchaseOrder>

Note that, because we’re in the middle of the example and have not provided the items element that the content model requires, the code explicitly disables the requirement for validation when generating XML from a binding instance. A consequence of this is that the generated XML is not valid, and validation must be disabled for parsing as well if the resulting document is to be re-converted into a binding with CreateFromDocument.

Creating Instances of Anonymous Types

The style of XML schema used for purchase orders uses anonymous types for the deeper elements of the purchase order:

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
   targetNamespace="URN:purchase-order"
   xmlns:tns="URN:purchase-order"
   xmlns:address="URN:address"
   elementFormDefault="qualified">
  <xsd:import namespace="URN:address" schemaLocation="nsaddress.xsd"/>
  <xsd:element name="purchaseOrder" type="tns:PurchaseOrderType"/>
  <xsd:element name="comment" type="xsd:string"/>
  <xsd:complexType name="PurchaseOrderType">
    <xsd:sequence>
      <xsd:element name="shipTo" type="address:USAddress"/>
      <xsd:element name="billTo" type="address:USAddress"/>
      <xsd:element ref="tns:comment" minOccurs="0"/>
      <xsd:element name="items"  type="tns:Items"/>
    </xsd:sequence>
    <xsd:attribute name="orderDate" type="xsd:date"/>
  </xsd:complexType>
  <xsd:complexType name="Items">
    <xsd:sequence>
      <xsd:element name="item" minOccurs="0" maxOccurs="unbounded">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="productName" type="xsd:string"/>
            <xsd:element name="quantity">
              <xsd:simpleType>
                <xsd:restriction base="xsd:positiveInteger">
                  <xsd:maxExclusive value="100"/>
                </xsd:restriction>
              </xsd:simpleType>
            </xsd:element>
            <xsd:element name="USPrice"  type="xsd:decimal"/>
            <xsd:element ref="tns:comment"   minOccurs="0"/>
            <xsd:element name="shipDate" type="xsd:date" minOccurs="0"/>
          </xsd:sequence>
          <xsd:attribute name="partNum" type="tns:SKU" use="required"/>
        </xsd:complexType>
      </xsd:element>
    </xsd:sequence>
  </xsd:complexType>
  <!-- Stock Keeping Unit, a code for identifying products -->
  <xsd:simpleType name="SKU">
    <xsd:restriction base="xsd:string">
      <xsd:pattern value="\d{3}-[A-Z]{2}"/>
    </xsd:restriction>
  </xsd:simpleType>

</xsd:schema>

In particular, there is no global item element that can be used to create the individual items. For situations like this, we use BIND:

import pyxb
import po4
import address
import pyxb.binding.datatypes as xs

po = po4.purchaseOrder(orderDate=xs.date(1999, 10, 20))
po.shipTo = address.USAddress('Alice Smith', '123 Maple Street', 'Anytown', 'AK', 12341)
po.billTo = address.USAddress('Robert Smith', '8 Oak Avenue', 'Anytown', 'AK', 12341)
po.items = pyxb.BIND(pyxb.BIND('Lapis necklace', 1, 99.95, partNum='833-AA'),
                     pyxb.BIND('Plastic necklace', 4, 3.95, partNum='833-AB'))
                
print po.toxml()

The BIND reference wraps the content of the inner elements, and is a cue to PyXB to attempt to build an instance of whatever type of object would satisfy the content model at that point. The resulting document (after reformatting) is:

<?xml version="1.0"?>
<ns1:purchaseOrder xmlns:ns1="URN:purchase-order" orderDate="1999-10-20">
  <ns1:shipTo>
    <name>Alice Smith</name>
    <street>123 Maple Street</street>
    <city>Anytown</city>
    <state>AK</state>
    <zip>12341</zip>
  </ns1:shipTo>
  <ns1:billTo>
    <name>Robert Smith</name>
    <street>8 Oak Avenue</street>
    <city>Anytown</city>
    <state>AK</state>
    <zip>12341</zip>
  </ns1:billTo>
  <ns1:items>
    <ns1:item partNum="833-AA">
      <ns1:productName>Lapis necklace</ns1:productName>
      <ns1:quantity>1</ns1:quantity>
      <ns1:USPrice>99.95</ns1:USPrice>
    </ns1:item>
    <ns1:item partNum="833-AB">
      <ns1:productName>Plastic necklace</ns1:productName>
      <ns1:quantity>4</ns1:quantity>
      <ns1:USPrice>3.95</ns1:USPrice>
    </ns1:item>
  </ns1:items>
</ns1:purchaseOrder>

The complete document is generated by the following program:

import pyxb
import po4
import address
import pyxb.binding.datatypes as xs
import datetime

po = po4.purchaseOrder(orderDate=xs.date(1999, 10, 20))
po.shipTo = address.USAddress('Alice Smith', '123 Maple Street', 'Anytown', 'AK', 12341)
po.billTo = address.USAddress('Robert Smith', '8 Oak Avenue', 'Anytown', 'AK', 12341)
po.items = pyxb.BIND(pyxb.BIND('Lapis necklace', 1, 99.95, partNum='833-AA'),
                     pyxb.BIND('Plastic necklace', 4, 3.95, partNum='833-AB'))

po.shipTo.country = po.billTo.country = po.shipTo.country

lapis = po.items.item[0]
lapis.shipDate = po.orderDate + datetime.timedelta(days=46)
lapis.comment = 'Want this for the holidays!'
po.items.item[1].shipDate = po.items.item[0].shipDate + datetime.timedelta(days=19)

print po.toxml()

The additional code demonstrates a couple additional features:

  • Fixed attribute values (such as country) are present in the bindings, even though they are only printed if they are set explicitly
  • The PyXB types for representing dates and times are extensions of those used by Python for the same purpose, including the ability to use them in expressions

Creating XML Documents from Binding Instances

All along we’ve been seeing how to generate XML from a binding instance. The toxml method is short-hand for a sequence that converts the binding to a DOM instance using xml.dom.minidom, then uses the DOM interface to generate the XML document.

The BindingDOMSupport class provides ways to control this generation. In particular, you may want to use something more informative than ns# to denote namespaces in the generated documents. This can be done using the following code:

import pyxb.utils.domutils
pyxb.utils.domutils.BindingDOMSupport.DeclareNamespace(address.Namespace, 'addr')
pyxb.utils.domutils.BindingDOMSupport.DeclareNamespace(po4.Namespace, 'po')

print po.toxml()

With this, the final document produced is:

<?xml version="1.0"?>
<po:purchaseOrder xmlns:po="URN:purchase-order" orderDate="1999-10-20">
  <po:shipTo country="US">
    <name>Alice Smith</name>
    <street>123 Maple Street</street>
    <city>Anytown</city>
    <state>AK</state>
    <zip>12341</zip>
  </po:shipTo>
  <po:billTo country="US">
    <name>Robert Smith</name>
    <street>8 Oak Avenue</street>
    <city>Anytown</city>
    <state>AK</state>
    <zip>12341</zip>
  </po:billTo>
  <po:items>
    <po:item partNum="833-AA">
      <po:productName>Lapis necklace</po:productName>
      <po:quantity>1</po:quantity>
      <po:USPrice>99.95</po:USPrice>
      <po:comment>Want this for the holidays!</po:comment>
      <po:shipDate>1999-12-05</po:shipDate>
    </po:item>
    <po:item partNum="833-AB">
      <po:productName>Plastic necklace</po:productName>
      <po:quantity>4</po:quantity>
      <po:USPrice>3.95</po:USPrice>
      <po:shipDate>1999-12-24</po:shipDate>
    </po:item>
  </po:items>
</po:purchaseOrder>

(Surprise: addr does not appear, because the nsaddress.xsd schema uses the default element form unqualified, so none of the address components in the document have a namespace.)