# # specparser.py # # Reads in a spec document and generates pretty data structures from it. # # Copyright (C) 2009 Collabora Ltd. # # This library is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 2.1 of the License, or (at # your option) any later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # Authors: Davyd Madeley # import sys import xml.dom.minidom import xincludator XMLNS_TP = 'http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0' class UnknownAccess(Exception): pass class UnknownDirection(Exception): pass class UnknownType(Exception): pass class UnnamedItem(Exception): pass class UntypedItem(Exception): pass class UnsupportedArray(Exception): pass class BadNameForBindings(Exception): pass def getText(dom): try: if dom.childNodes[0].nodeType == dom.TEXT_NODE: return dom.childNodes[0].data else: return '' except IndexError: return '' def getChildrenByName(dom, namespace, name): return filter(lambda n: n.nodeType == n.ELEMENT_NODE and \ n.namespaceURI == namespace and \ n.localName == name, dom.childNodes) def build_name(namespace, name): """Returns a name by appending `name' to the namespace of this object. """ return '.'.join( filter(lambda n: n is not None and n != '', [namespace, name.replace(' ', '')]) ) class Base(object): """The base class for any type of XML node in the spec that implements the 'name' attribute. Don't instantiate this class directly. """ devhelp_name = "" def __init__(self, parent, namespace, dom): self.short_name = name = dom.getAttribute('name') self.namespace = namespace self.name = build_name(namespace, name) self.parent = parent try: self.docstring = getChildrenByName(dom, XMLNS_TP, 'docstring')[0] except IndexError: self.docstring = None try: self.added = getChildrenByName(dom, XMLNS_TP, 'added')[0] except IndexError: self.added = None try: self.deprecated = getChildrenByName(dom, XMLNS_TP, 'deprecated')[0] except IndexError: self.deprecated = None self.changed = getChildrenByName(dom, XMLNS_TP, 'changed') self.validate() def validate(self): if self.short_name == '': raise UnnamedItem("Node %s of %s has no name" % ( self.__class__.__name__, self.parent)) def get_type_name(self): return self.__class__.__name__ def get_spec(self): return self.parent.get_spec() def get_root_namespace(self): return self.get_interface().name def get_interface(self): return self.parent.get_interface() def get_url(self): return "%s#%s" % (self.get_interface().get_url(), self.name) def _get_generic_with_ver(self, nnode, htmlclass, txt): if nnode is None: return '' else: # make a copy of this node, turn it into a HTML
tag node = nnode.cloneNode(True) node.tagName = 'div' node.baseURI = None node.setAttribute('class', htmlclass) try: node.removeAttribute('version') span = xml.dom.minidom.parseString( ('%s\n' % txt) % nnode.getAttribute('version')).firstChild node.insertBefore(span, node.firstChild) except xml.dom.NotFoundErr: print >> sys.stderr, \ 'WARNING: %s was %s, but gives no version' % (self, htmlclass) self._convert_to_html(node) return node.toxml().encode('ascii', 'xmlcharrefreplace') def get_added(self): return self._get_generic_with_ver(self.added, 'added', "Added in %s.") def get_deprecated(self): return self._get_generic_with_ver(self.deprecated, 'deprecated', "Deprecated since %s.") def get_changed(self): return '\n'.join(map(lambda n: self._get_generic_with_ver(n, 'changed', "Changed in %s."), self.changed)) def get_docstring(self): """Get the docstring for this node, but do node substitution to rewrite types, interfaces, etc. as links. """ if self.docstring is None: return '' else: # make a copy of this node, turn it into a HTML
tag node = self.docstring.cloneNode(True) node.tagName = 'div' node.baseURI = None node.setAttribute('class', 'docstring') self._convert_to_html(node) return node.toxml().encode('ascii', 'xmlcharrefreplace') def _convert_to_html(self, node): spec = self.get_spec() namespace = self.get_root_namespace() # rewrite for n in node.getElementsByTagNameNS(XMLNS_TP, 'rationale'): n.tagName = 'div' n.namespaceURI = None n.setAttribute('class', 'rationale') # rewrite for n in node.getElementsByTagNameNS(XMLNS_TP, 'type'): t = spec.lookup_type(getText(n)) n.tagName = 'a' n.namespaceURI = None n.setAttribute('href', t.get_url()) # rewrite for n in node.getElementsByTagNameNS(XMLNS_TP, 'member-ref'): key = getText(n) try: o = spec.lookup(key, namespace=namespace) except KeyError: print >> sys.stderr, \ "WARNING: Key '%s' not known in namespace '%s'" % ( key, namespace) continue n.tagName = 'a' n.namespaceURI = None n.setAttribute('href', o.get_url()) n.setAttribute('title', o.get_title()) # rewrite for n in node.getElementsByTagNameNS(XMLNS_TP, 'dbus-ref'): namespace = n.getAttribute('namespace') key = getText(n) try: o = spec.lookup(key, namespace=namespace) except KeyError: print >> sys.stderr, \ "WARNING: Key '%s' not known in namespace '%s'" % ( key, namespace) continue n.tagName = 'a' n.namespaceURI = None n.setAttribute('href', o.get_url()) n.setAttribute('title', o.get_title()) def get_title(self): return '%s %s' % (self.get_type_name(), self.name) def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self.name) class DBusConstruct(Base): """Base class for signals, methods and properties.""" def __init__(self, parent, namespace, dom): super(DBusConstruct, self).__init__(parent, namespace, dom) self.name_for_bindings = dom.getAttributeNS(XMLNS_TP, 'name-for-bindings') if not self.name_for_bindings: raise BadNameForBindings('%s has no name-for-bindings' % self) if self.name_for_bindings.replace('_', '') != self.short_name: raise BadNameForBindings('%s name-for-bindings = %s does not ' 'match short_name = %s' % (self, self.name_for_bindings, self.short_name)) class PossibleError(Base): def __init__(self, parent, namespace, dom): super(PossibleError, self).__init__(parent, namespace, dom) def get_error(self): spec = self.get_spec() try: return spec.errors[self.name] except KeyError: return External(self.name) def get_url(self): return self.get_error().get_url() def get_title(self): return self.get_error().get_title() def get_docstring(self): d = super(PossibleError, self).get_docstring() if d == '': return self.get_error().get_docstring() else: return d class Method(DBusConstruct): devhelp_name = "function" def __init__(self, parent, namespace, dom): super(Method, self).__init__(parent, namespace, dom) args = build_list(self, Arg, self.name, dom.getElementsByTagName('arg')) # separate arguments as input and output arguments self.in_args = filter(lambda a: a.direction == Arg.DIRECTION_IN, args) self.out_args = filter(lambda a: a.direction == Arg.DIRECTION_OUT, args) for arg in args: if arg.direction == Arg.DIRECTION_IN or \ arg.direction == Arg.DIRECTION_OUT: continue print >> sys.stderr, "WARNING: '%s' of method '%s' does not specify a suitable direction" % (arg, self) self.possible_errors = build_list(self, PossibleError, None, dom.getElementsByTagNameNS(XMLNS_TP, 'error')) def get_in_args(self): return ', '.join(map(lambda a: a.spec_name(), self.in_args)) def get_out_args(self): if len(self.out_args) > 0: return ', '.join(map(lambda a: a.spec_name(), self.out_args)) else: return 'nothing' class Typed(Base): """The base class for all typed nodes (i.e. Arg and Property). Don't instantiate this class directly. """ def __init__(self, parent, namespace, dom): super(Typed, self).__init__(parent, namespace, dom) self.type = dom.getAttributeNS(XMLNS_TP, 'type') self.dbus_type = dom.getAttribute('type') # check we have a dbus type if self.dbus_type == '': raise UntypedItem("Node referred to by '%s' has no type" % dom.toxml()) def get_type(self): return self.get_spec().lookup_type(self.type) def get_type_url(self): t = self.get_type() if t is None: return '' else: return t.get_url() def get_type_title(self): t = self.get_type() if t is None: return '' else: return t.get_title() def spec_name(self): return '%s: %s' % (self.dbus_type, self.short_name) def __repr__(self): return '%s(%s:%s)' % (self.__class__.__name__, self.name, self.dbus_type) class Property(DBusConstruct, Typed): ACCESS_READ = 1 ACCESS_WRITE = 2 ACCESS_READWRITE = ACCESS_READ | ACCESS_WRITE def __init__(self, parent, namespace, dom): super(Property, self).__init__(parent, namespace, dom) access = dom.getAttribute('access') if access == 'read': self.access = self.ACCESS_READ elif access == 'write': self.access = self.ACCESS_WRITE elif access == 'readwrite': self.access = self.ACCESS_READWRITE else: raise UnknownAccess("Unknown access '%s' on %s" % (access, self)) def get_access(self): if self.access & self.ACCESS_READ and self.access & self.ACCESS_WRITE: return 'Read/Write' elif self.access & self.ACCESS_READ: return 'Read only' elif self.access & self.ACCESS_WRITE: return 'Write only' class AwkwardTelepathyProperty(Typed): def get_type_name(self): return 'Telepathy Property' class Arg(Typed): DIRECTION_IN, DIRECTION_OUT, DIRECTION_UNSPECIFIED = range(3) def __init__(self, parent, namespace, dom): super(Arg, self).__init__(parent, namespace, dom) direction = dom.getAttribute('direction') if direction == 'in': self.direction = self.DIRECTION_IN elif direction == 'out': self.direction = self.DIRECTION_OUT elif direction == '': self.direction = self.DIRECTION_UNSPECIFIED else: raise UnknownDirection("Unknown direction '%s' on %s" % ( direction, self.parent)) class Signal(DBusConstruct): def __init__(self, parent, namespace, dom): super(Signal, self).__init__(parent, namespace, dom) self.args = build_list(self, Arg, self.name, dom.getElementsByTagName('arg')) for arg in self.args: if arg.direction == Arg.DIRECTION_UNSPECIFIED: continue print >> sys.stderr, "WARNING: '%s' of signal '%s' does not specify a suitable direction" % (arg, self) def get_args(self): return ', '.join(map(lambda a: a.spec_name(), self.args)) class External(object): """External objects are objects that are referred to in another spec. We have to attempt to look them up if at all possible. """ def __init__(self, name): self.name = self.short_name = name def get_url(self): return None def get_title(self): return 'External %s' % self.name def get_docstring(self): return None def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self.name) class Interface(Base): def __init__(self, parent, namespace, dom, spec_namespace): super(Interface, self).__init__(parent, namespace, dom) # If you're writing a spec with more than one top-level namespace, you # probably want to replace spec_namespace with a list. if self.name.startswith(spec_namespace + "."): self.short_name = self.name[len(spec_namespace) + 1:] else: self.short_name = self.name # build lists of methods, etc., in this interface self.methods = build_list(self, Method, self.name, dom.getElementsByTagName('method')) self.properties = build_list(self, Property, self.name, dom.getElementsByTagName('property')) self.signals = build_list(self, Signal, self.name, dom.getElementsByTagName('signal')) self.tpproperties = build_list(self, AwkwardTelepathyProperty, self.name, dom.getElementsByTagNameNS(XMLNS_TP, 'property')) self.handler_capability_tokens = build_list(self, HandlerCapabilityToken, self.name, dom.getElementsByTagNameNS(XMLNS_TP, 'handler-capability-token')) self.contact_attributes = build_list(self, ContactAttribute, self.name, dom.getElementsByTagNameNS(XMLNS_TP, 'contact-attribute')) # build a list of types in this interface self.types = parse_types(self, dom, self.name) # find out if this interface causes havoc self.causes_havoc = dom.getAttributeNS(XMLNS_TP, 'causes-havoc') if self.causes_havoc == '': self.causes_havoc = None # find out what we're required to also implement self.requires = map(lambda n: n.getAttribute('interface'), getChildrenByName(dom, XMLNS_TP, 'requires')) def get_interface(self): return self def get_requires(self): spec = self.get_spec() def lookup(r): try: return spec.lookup(r) except KeyError: return External(r) return map(lookup, self.requires) def get_url(self): return '%s.html' % self.name class Error(Base): def get_url(self): return 'errors.html#%s' % self.name def get_root_namespace(self): return self.namespace class DBusList(object): """Stores a list of a given DBusType. Provides some basic validation to determine whether or not the type is sane. """ def __init__(self, child): self.child = child if isinstance(child, DBusType): self.ultimate = child self.depth = 1 if self.child.array_name == '': raise UnsupportedArray("Type '%s' does not support being " "used in an array" % self.child.name) else: self.name = build_name(self.child.namespace, self.child.array_name) self.short_name = self.child.array_name elif isinstance(child, DBusList): self.ultimate = child.ultimate self.depth = child.depth + 1 self.name = self.child.name + '_List' self.short_name = self.child.short_name + '_List' # check that our child can operate at this depth maxdepth = int(self.ultimate.array_depth) if self.depth > maxdepth: raise TypeError("Type '%s' has exceeded its maximum depth (%i)" % (self, maxdepth)) else: raise TypeError("DBusList can contain only a DBusType or DBusList not '%s'" % child) self.dbus_type = 'a' + self.child.dbus_type def get_url(self): return self.ultimate.get_url() def get_title(self): return "Array of %s" % self.child.get_title() def __repr__(self): return 'Array(%s)' % self.child class DBusType(Base): """The base class for all D-Bus types referred to in the spec. Don't instantiate this class directly. """ devhelp_name = "typedef" def __init__(self, parent, namespace, dom): super(DBusType, self).__init__(parent, namespace, dom) self.dbus_type = dom.getAttribute('type') self.array_name = dom.getAttribute('array-name') self.array_depth = dom.getAttribute('array-depth') def get_root_namespace(self): return self.namespace def get_breakdown(self): return '' def get_url(self): if isinstance(self.parent, Interface): html = self.parent.get_url() else: html = 'generic-types.html' return '%s#%s' % (html, self.name) class SimpleType(DBusType): def get_type_name(self): return 'Simple Type' class ExternalType(DBusType): def __init__(self, parent, namespace, dom): super(ExternalType, self).__init__(parent, namespace, dom) # FIXME: until we are able to cross reference external types to learn # about their array names, we're just going to assume they work like # this self.array_name = self.short_name + '_List' def get_type_name(self): return 'External Type' class StructLike(DBusType): """Base class for all D-Bus types that look kind of like Structs Don't instantiate this class directly. """ class StructMember(Typed): def get_root_namespace(self): return self.parent.get_root_namespace() def __init__(self, parent, namespace, dom): super(StructLike, self).__init__(parent, namespace, dom) self.members = build_list(self, StructLike.StructMember, None, dom.getElementsByTagNameNS(XMLNS_TP, 'member')) def get_breakdown(self): str = '' str += '
    \n' for member in self.members: # attempt to lookup the member up in the type system t = member.get_type() str += '
  • %s — %s' % (member.name, member.dbus_type) if t: str += ' (%s)' % ( t.get_url(), t.get_title(), t.short_name) str += '
  • \n' str += member.get_docstring() str += '
\n' return str class Mapping(StructLike): def __init__(self, parent, namespace, dom): super(Mapping, self).__init__(parent, namespace, dom) # rewrite the D-Bus type self.dbus_type = 'a{%s}' % ''.join(map(lambda m: m.dbus_type, self.members)) class Struct(StructLike): devhelp_name = "struct" def __init__(self, parent, namespace, dom): super(Struct, self).__init__(parent, namespace, dom) # rewrite the D-Bus type self.dbus_type = '(%s)' % ''.join(map(lambda m: m.dbus_type, self.members)) class EnumLike(DBusType): """Base class for all D-Bus types that look kind of like Enums Don't instantiate this class directly. """ class EnumValue(Base): def __init__(self, parent, namespace, dom): super(EnumLike.EnumValue, self).__init__(parent, namespace, dom) # rewrite self.name self.short_name = dom.getAttribute('suffix') self.name = build_name(namespace, self.short_name) self.value = dom.getAttribute('value') super(EnumLike.EnumValue, self).validate() def validate(self): pass def get_root_namespace(self): return self.parent.get_root_namespace() def get_breakdown(self): str = '' str += '
    \n' for value in self.values: # attempt to lookup the member.name as a type in the type system str += '
  • %s (%s)
  • \n' % (value.short_name, value.value) str += value.get_added() str += value.get_changed() str += value.get_deprecated() str += value.get_docstring() str += '
\n' return str class Enum(EnumLike): devhelp_name = "enum" def __init__(self, parent, namespace, dom): super(Enum, self).__init__(parent, namespace, dom) self.values = build_list(self, EnumLike.EnumValue, self.name, dom.getElementsByTagNameNS(XMLNS_TP, 'enumvalue')) class Flags(EnumLike): def __init__(self, parent, namespace, dom): super(Flags, self).__init__(parent, namespace, dom) self.values = build_list(self, EnumLike.EnumValue, self.name, dom.getElementsByTagNameNS(XMLNS_TP, 'flag')) self.flags = self.values # in case you're looking for it class TokenBase(Base): devhelp_name = "macro" # it's a constant, which is near enough... separator = '/' def __init__(self, parent, namespace, dom): super(TokenBase, self).__init__(parent, namespace, dom) self.name = namespace + '/' + self.short_name class ContactAttribute(TokenBase, Typed): def get_type_name(self): return 'Contact Attribute' class HandlerCapabilityToken(TokenBase): def get_type_name(self): return 'Handler Capability Token' def __init__(self, parent, namespace, dom): super(HandlerCapabilityToken, self).__init__(parent, namespace, dom) is_family = dom.getAttribute('is-family') assert is_family in ('yes', 'no', '') self.is_family = (is_family == 'yes') class SectionBase(object): """A SectionBase is an abstract base class for any type of node that can contain a , which means the top-level Spec object, or any Section object. It should not be instantiated directly. """ def __init__(self, dom, spec_namespace): self.items = [] def recurse(nodes): # iterate through the list of child nodes for node in nodes: if node.nodeType != node.ELEMENT_NODE: continue if node.tagName == 'node': # recurse into this level for interesting items recurse(node.childNodes) elif node.namespaceURI == XMLNS_TP and \ node.localName == 'section': self.items.append(Section(self, None, node, spec_namespace)) elif node.tagName == 'interface': self.items.append(Interface(self, None, node, spec_namespace)) recurse(dom.childNodes) class Section(Base, SectionBase): def __init__(self, parent, namespace, dom, spec_namespace): Base.__init__(self, parent, namespace, dom) SectionBase.__init__(self, dom, spec_namespace) def get_root_namespace(self): return None class Spec(SectionBase): def __init__(self, dom, spec_namespace): # build a dictionary of errors in this spec try: errorsnode = dom.getElementsByTagNameNS(XMLNS_TP, 'errors')[0] self.errors = build_dict(self, Error, errorsnode.getAttribute('namespace'), errorsnode.getElementsByTagNameNS(XMLNS_TP, 'error')) except IndexError: self.errors = {} # build a list of generic types self.generic_types = reduce (lambda a, b: a + b, map(lambda l: parse_types(self, l), dom.getElementsByTagNameNS(XMLNS_TP, 'generic-types')), []) # create a top-level section for this Spec SectionBase.__init__(self, dom.documentElement, spec_namespace) # build a list of interfaces in this spec self.interfaces = [] def recurse(items): for item in items: if isinstance(item, Section): recurse(item.items) elif isinstance(item, Interface): self.interfaces.append(item) recurse(self.items) # build a giant dictionary of everything (interfaces, methods, signals # and properties); also build a dictionary of types self.everything = {} self.types = {} for type in self.generic_types: self.types[type.short_name] = type for interface in self.interfaces: self.everything[interface.name] = interface for method in interface.methods: self.everything[method.name] = method for signal in interface.signals: self.everything[signal.name] = signal for property in interface.properties: self.everything[property.name] = property for property in interface.tpproperties: self.everything[property.name] = property for token in interface.contact_attributes: self.everything[token.name] = token for token in interface.handler_capability_tokens: self.everything[token.name] = token for type in interface.types: self.types[type.short_name] = type # get some extra bits for the HTML node = dom.getElementsByTagNameNS(XMLNS_TP, 'spec')[0] self.title = getText(getChildrenByName(node, XMLNS_TP, 'title')[0]) try: self.version = getText(getChildrenByName(node, XMLNS_TP, 'version')[0]) except IndexError: self.version = None self.copyrights = map(getText, getChildrenByName(node, XMLNS_TP, 'copyright')) try: license = getChildrenByName(node, XMLNS_TP, 'license')[0] license.tagName = 'div' license.namespaceURI = None license.setAttribute('class', 'license') self.license = license.toxml() except IndexError: self.license = '' # FIXME: we need to check all args for type correctness def get_spec(self): return self def lookup(self, name, namespace=None): key = build_name(namespace, name) return self.everything[key] def lookup_type(self, type_): if type_.endswith('[]'): return DBusList(self.lookup_type(type_[:-2])) if type_ == '': return None elif type_ in self.types: return self.types[type_] raise UnknownType("Type '%s' is unknown" % type_) def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self.title) def build_dict(parent, type_, namespace, nodes): """Build a dictionary of D-Bus names to Python objects representing that name using the XML node for that item in the spec. e.g. 'org.freedesktop.Telepathy.Channel' : Interface(Channel) Works for any Python object inheriting from 'Base' whose XML node implements the 'name' attribute. """ def build_tuple(node): o = type_(parent, namespace, node) return(o.name, o) return dict(build_tuple(n) for n in nodes) def build_list(parent, type_, namespace, nodes): return map(lambda node: type_(parent, namespace, node), nodes) def parse_types(parent, dom, namespace = None): """Parse all of the types of type nodes mentioned in 't' from the node 'dom' and insert them into the dictionary 'd'. """ t = [ (SimpleType, 'simple-type'), (Enum, 'enum'), (Flags, 'flags'), (Mapping, 'mapping'), (Struct, 'struct'), (ExternalType, 'external-type'), ] types = [] for (type_, tagname) in t: types += build_list(parent, type_, namespace, dom.getElementsByTagNameNS(XMLNS_TP, tagname)) return types def parse(filename, spec_namespace): dom = xml.dom.minidom.parse(filename) xincludator.xincludate(dom, filename) spec = Spec(dom, spec_namespace) return spec if __name__ == '__main__': parse(sys.argv[1])