Advanced Usage


Annotations are supported as defined in HEP-1: Value Annotations. By default, the parser accepts and validate annotations, but they are not passed to the user. Conversely, dumping values does not write any annotations by default. In order to make use of parsed annotations a “cast callback” must be provided, and to write annotations a “value callback”.

Cast Callbacks

A “cast callback” is any callable object which will be called by the parser every time it has ended parsing a value, and accepts the following arguments:

  • A set of annotations.
  • The text representation of a parsed value.
  • The parsed value, converted to irs corresponding Python type.

The function must return a value, which can be either the same provided by the parser or any other value. The result returned from hipack.load() (or hipack.loads() contains the values returned by the cast function instead of the ones originally seen by the parser. The main use case for a cast callback is to convert HiPack values on-the-fly into objects of the application domain, optinally making use of annotations to help the cast function determine which kind of conversion to perform.

As en example, consider a simple contacts application which stores the information of each contact in a file like the following:

name "Peter"
surname "Parker"
instant-messaging [
    :xmpp ""
    :skype "spideysenses"

Note how the items inside the instant-messaging list are annotated with the kind of instant messaging service they correspond to. When parsing a file like this, we probably want to use the following classes to represent the data above:

class Contact(object):
    def __init__(self, name, surname=u"", im=None): = name
        self.surname = surname = [] if im is None else im

class IMAccount(object):
    kind = None
    def __init__(self, address):
        self.address = address

class XMPPAccount(IMAccount):
    kind = "XMPP"

class SkypeAccount(IMAccount):
    kind = "Skype"

Now, with those classes in place, we can write our cast function as follows:

def contacts_cast(annotations, stringvalue, value):
    if u"xmpp" in annotations:
        return XMPPAccount(value)
    elif u"skype" in annotations:
        return SkypeAccount(value)
        return value

Finally, we can load a contact file as follows:

>>> with open("contact.hipack", "r") as f:
...     contact = Contact(**hipack.load(f, cast=contacts_cast))
>>> isinstance(contact, Contact)
>>> isinstance([0], XMPPAccount)
(u'Peter', u'')

Note that cast callbacks receive a set containing all the annotations attached to a value, including intrinsic implicit annotations. This means that every time the callback is invoked, there will be at least always the intrinsic annotation which informs of the type of the value (.int, .float, .list, etc).

Value Callbacks

A “value callback” performs the opposite operation to cast callbacks: it is called when before serializing a value into its HiPack representation to give the application an opportunity to convert arbitrary Python objects, and attach annotations to the serialized value. Value callbacks must accept a Python object as its first argument, and return two values:

  • A basic value for which HiPack specifies a representation.
  • An iterable which yields the annotations to attach to the value, or None if the value has no annotations associated to it.

Continuing with the contacts example above, we can define a value callback like the following to allow direct serialization of IMAccount objects:

def contact_value(obj):
    if isinstance(obj, IMAccount):
        return obj.address, (obj.kind.lower(),)
        return obj, None

Value callbacks are used in a way similar to cast callbacks, passing them to the hipack.dump() function. For example:

>>> print(hipack.dumps({
...     "work-im": XMPPAccount(""),
...     "home-im": SkypeAccount("spideysenses"),
... }, value=contact_value)
home-im::skype "spideysenses"
work-im::xmpp ""