Source code for dbusapi.ast

# -*- coding: utf-8 -*-
#
# Copyright © 2015, 2016 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, see <http://www.gnu.org/licenses/>.


"""
An implementation of the abstract syntax tree (AST) for a D-Bus introspection
document, which fully describes a D-Bus API.

An AST can be built by parsing an XML file (using
`interfaceparser.InterfaceParser`) or by building the tree of objects manually.
"""


from abc import ABCMeta
from collections import OrderedDict
# pylint: disable=no-member
from lxml import etree
from re import match
from dbusapi.log import Log
from dbusapi.typeparser import TypeParser


TP_DTD = 'http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0'
FDO_DTD = 'http://www.freedesktop.org/dbus/1.0/doc.dtd'


[docs]class AstLog(Log): """Specialized Log subclass for AST messages""" def __init__(self): """Construct a new AstLog""" super(AstLog, self).__init__() self.register_issue_code('unknown-node') self.register_issue_code('empty-root') self.register_issue_code('missing-attribute') self.register_issue_code('duplicate-node') self.register_issue_code('duplicate-interface') self.register_issue_code('duplicate-method') self.register_issue_code('duplicate-signal') self.register_issue_code('duplicate-property') self.register_issue_code('node-name') self.register_issue_code('interface-name') self.register_issue_code('method-name') self.register_issue_code('signal-name') self.register_issue_code('property-type') self.register_issue_code('argument-type') self.domain = 'ast'
[docs]def ignore_node(node): """Decide whether to ignore the given node when parsing.""" return node.tag[0] == '{' # in a namespace
# pylint: disable=too-many-instance-attributes
[docs]class BaseNode(object): """Base class for all D-Bus AST nodes.""" __metaclass__ = ABCMeta DOCSTRING_TAGS = ['{%s}docstring' % TP_DTD, '{%s}doc' % FDO_DTD] required_attributes = ['name'] optional_attributes = [] def __init__(self, name, annotations=None, log=None): """Construct a new ast.BaseNode Args: name: str, the name of the node. annotations: potentially empty dict of annotations applied to the node, mapping annotation name to an `ast.Annotation` instance log: subclass of `Log`, used to store log messages; can be None """ self.name = name self.children = [] self.parent = None self._comment = None self.log = log or AstLog() self.annotations = OrderedDict() self.line_number = -1 self.comment_line_number = -1 self._children_types = { 'annotation': Annotation, } self._type_containers = { Annotation: self.annotations, } for annotation in (annotations or {}).values(): self._add_child(annotation) @classmethod
[docs] def from_xml(cls, node, comment, log, parent=None): """Return a new ast.BaseNode instance from an XML node.""" attrs = {} valid = True for attr_name in cls.required_attributes: # Avoid redefining a builtin member_name = attr_name if member_name == 'type': member_name = 'type_' try: attrs[member_name] = node.attrib[attr_name] except KeyError: log.log_issue('missing-attribute', 'Missing required attribute ‘%s’ in %s.' % (attr_name, node.tag)) valid = False if not valid: return None for attr_name in cls.optional_attributes: attrs[attr_name] = node.attrib.get(attr_name) # FIXME: Hack for the fact that Node.name and Argument.name are not # actually required, but is the first attribute in the constructor, and # hence must be specified. This can be removed when we break API. if (cls == Node or cls == Argument) and 'name' not in attrs: attrs['name'] = None elif issubclass(cls, Callable) and 'args' not in attrs: attrs['args'] = [] attrs['log'] = log res = cls(**attrs) res.line_number = node.sourceline if comment is not None: res.comment = comment.text # lxml reports the last source line for xml comments as # being the actual source line, fix this. # Also report line numbers starting from 1, consistent # with node.line_number res.comment_line_number = (comment.sourceline - len(res.comment.split('\n')) + 1) if parent: parent.add_child(res) res.parse_xml_children(node) return res
[docs] def add_child(self, child): """Add a child to the node""" return self._add_child(child)
[docs] def parse_xml_children(self, node): """Parse the XML node's children.""" xml_comment = None for elem in node: if elem.tag == etree.Comment: xml_comment = elem continue elif elem.tag in BaseNode.DOCSTRING_TAGS: self.comment_line_number = elem.sourceline self.comment = elem.text continue elif ignore_node(elem): xml_comment = None continue try: ctype = self._children_types[elem.tag] except KeyError: xml_comment = None if isinstance(self, Node) and not self.pretty_name: # Special handling for root nodes to allow more meaningful # error messages. self.__log_issue('unknown-node', "Unknown node ‘%s’ in root." % elem.tag) else: self.__log_issue('unknown-node', "Unknown node ‘%s’ in %s%s’." % (elem.tag, type(self).__name__.lower(), self.pretty_name)) continue ctype.from_xml(elem, xml_comment, parent=self, log=self.log) xml_comment = None
[docs] def walk(self): """Traverse this node's children in pre-order.""" for child in self.children: yield child for grandchild in child.walk(): yield grandchild
# Backward compat
[docs] def format_name(self): """Format this node's name as a human-readable string""" return self.pretty_name
@property def comment(self): """ Get the comment for this node. Returns: str: If the node was annotated with `org.gtk.GDBus.DocString`, the value of the annotation, otherwise one of: * A tp:docstring child node * A doc:doc child node * An XML comment immediately preceding the XML node. , whichever is seen last. """ try: doc_annotation = self.annotations['org.gtk.GDBus.DocString'] return doc_annotation.value except KeyError: return self._comment @comment.setter def comment(self, value): """Set the comment for this node.""" self._comment = value @property def pretty_name(self): """Format the node's name as a human-readable string.""" return self.name def __log_issue(self, code, message): self.log.log_issue(code, message) def _child_is_duplicate(self, child): return child.name in self._type_containers[type(child)] def _add_child(self, child): child.parent = self if self._child_is_duplicate(child): self.__log_issue('duplicate-%s' % type(child).__name__.lower(), 'Duplicate %s definition ‘%s’.' % (type(child).__name__.lower(), child.pretty_name)) return False self.children.append(child) container = self._type_containers[type(child)] if isinstance(container, list): container.append(child) else: container[child.name] = child return True
[docs]class Node(BaseNode): """ AST representation of a <node> element. This represents the top level of a D-Bus API. """ required_attributes = [] optional_attributes = ['name'] # pylint: disable=too-many-arguments def __init__(self, name=None, interfaces=None, nodes=None, annotations=None, log=None): """ Construct a new ast.Node. Args: name: node name; a non-empty string; The root <node> should either have no name or should have a name that is a valid absolute object path. Child <node> names must be valid relative paths. interfaces: potentially empty dict of interfaces in the node, mapping interface name to an `ast.Interface` instance nodes: potentially empty dict of properties in the node, mapping node name to an `ast.Node` instance annotations: potentially empty dict of annotations applied to the node, mapping annotation name to an `ast.Annotation` instance log: subclass of `Log`, used to store log messages; can be None """ super(Node, self).__init__(name, annotations, log) self._children_types.update({'interface': Interface, 'node': Node}) self.interfaces = OrderedDict() self.nodes = OrderedDict() self._type_containers.update({Interface: self.interfaces, Node: self.nodes}) for child in (interfaces or {}).values(): self._add_child(child) for child in (nodes or {}).values(): self._add_child(child) def _add_child(self, child): if isinstance(child, Node): if not child.name: self.log.log_issue('missing-attribute', 'Missing required attribute ‘name’ in ' 'non-root node.') elif not Node.is_valid_relative_object_path(child.name): self.log.log_issue('node-name', 'Non-root node name is not a relative ' 'object path ‘%s’.' % child.name) child.node = self return super(Node, self)._add_child(child) @staticmethod
[docs] def is_valid_absolute_object_path(path): """ Validate an absolute D-Bus object path. https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path Args: path: object path """ return path == '/' or match(r'(/[A-Za-z0-9_]+)+', path) is not None
@staticmethod
[docs] def is_valid_relative_object_path(path): """ Validate a relative D-Bus object path. https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path Args: path: object path """ return match(r'[A-Za-z0-9_]+(/[A-Za-z0-9_]+)*', path) is not None
[docs]class Interface(BaseNode): """ AST representation of an <interface> element. This represents the most commonly used node of a D-Bus API. """ # pylint: disable=too-many-arguments def __init__(self, name, methods=None, properties=None, signals=None, annotations=None, log=None): """ Construct a new ast.Interface. Args: name: interface name; a non-empty string methods: potentially empty dict of methods in the interface, mapping method name to an `ast.Method` instance properties: potentially empty dict of properties in the interface, mapping property name to an `ast.Property` instance signals: potentially empty dict of signals in the interface, mapping signal name to an `ast.Signal` instance annotations: potentially empty dict of annotations applied to the interface, mapping annotation name to an `ast.Annotation` instance log: subclass of `Log`, used to store log messages; can be None """ super(Interface, self).__init__(name, annotations, log) if name and not Interface.is_valid_interface_name(name): self.log.log_issue('interface-name', 'Invalid interface name ‘%s’.' % name) self._children_types.update({'signal': Signal, 'method': Method, 'property': Property}) self.methods = OrderedDict() self.signals = OrderedDict() self.properties = OrderedDict() self._type_containers.update({Method: self.methods, Signal: self.signals, Property: self.properties}) for child in (methods or {}).values(): self._add_child(child) for child in (signals or {}).values(): self._add_child(child) for child in (properties or {}).values(): self._add_child(child) def _add_child(self, child): child.interface = self return super(Interface, self)._add_child(child) @staticmethod
[docs] def is_valid_interface_name(name): """ Validate a D-Bus interface name. http://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface Args: name: interface name """ return len(name) <= 255 and \ match(r'[A-Za-z_][A-Za-z0-9_]*' r'(\.[A-Za-z_][A-Za-z0-9_]*)+', name) is not None
def _dotted_name(elem): if elem.parent: return elem.parent.format_name() + '.' + elem.name return elem.name
[docs]class Property(BaseNode): """ AST representation of a <property> element. This represents a readable or writable property of an interface. """ ACCESS_READ = 'read' ACCESS_WRITE = 'write' ACCESS_READWRITE = 'readwrite' required_attributes = BaseNode.required_attributes + ['access', 'type'] # pylint: disable=too-many-arguments def __init__(self, name, type_, access, annotations=None, log=None): """ Construct a new ast.Property. Args: name: property name; a non-empty string, not including the parent interface name type_: type string for the property; see http://goo.gl/uCpa5A access: ACCESS_READ, ACCESS_WRITE, or ACCESS_READWRITE annotations: potentially empty dict of annotations applied to the property, mapping annotation name to an `ast.Annotation` instance log: subclass of `Log`, used to store log messages; can be None """ super(Property, self).__init__(name, annotations, log) type_parser = TypeParser(type_) self.type = type_parser.parse() if self.type is None: message = type_parser.get_output()[0][3] self.log.log_issue('property-type', 'Error when parsing type ‘%s’ for property ' '‘%s’: %s' % (type_, name, message)) self.access = access self.interface = None @property def pretty_name(self): """Format the property's name as a human-readable string""" return _dotted_name(self)
[docs]class Callable(BaseNode): u""" AST representation of a callable element. This represents a ‘callable’, such as a method or a signal. All callables contain a list of in and out arguments. """ def __init__(self, name, args, annotations=None, log=None): """ Construct a new ast.Callable. Args: name: callable name; a non-empty string, not including the parent interface name args: potentially empty ordered list of ast.Arguments accepted and returned by the callable annotations: potentially empty dict of annotations applied to the callable, mapping annotation name to an ast.Annotation instance log: subclass of `Log`, used to store log messages; can be None """ super(Callable, self).__init__(name, annotations, log) self.arguments = [] self.interface = None self._children_types.update({'arg': Argument}) self._type_containers.update({Argument: self.arguments}) for arg in args: self._add_child(arg) def _child_is_duplicate(self, child): if isinstance(child, Argument): return False return super(Callable, self)._child_is_duplicate(child) @property def pretty_name(self): """Format the callable's name as a human-readable string""" return _dotted_name(self) @staticmethod
[docs] def is_valid_name(name): """ Validate a D-Bus member name. https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-member Args: name: callable name """ return len(name) <= 255 and \ match(r'[A-Za-z_][A-Za-z0-9_]*', name) is not None
[docs]class Method(Callable): """ AST representation of a <method> element. This represents a callable method of an interface. """ def __init__(self, name, args, annotations=None, log=None): """ Construct a new ast.Method. Args: name: method name; a non-empty string, not including the parent interface name args: potentially empty ordered list of ast.Arguments accepted and returned by the method annotations: potentially empty dict of annotations applied to the method, mapping annotation name to an ast.Annotation instance log: subclass of `Log`, used to store log messages; can be None """ super(Method, self).__init__(name, args, annotations, log) if name and not Callable.is_valid_name(name): self.log.log_issue('method-name', 'Invalid method name ‘%s’.' % name)
[docs]class Signal(Callable): """ AST representation of a <signal> element. This represents an emittable signal on an interface. """ def __init__(self, name, args, annotations=None, log=None): """ Construct a new ast.Signal. Args: name: signal name; a non-empty string, not including the parent interface name args: potentially empty ordered list of ast.Arguments accepted and returned by the signal annotations: potentially empty dict of annotations applied to the signal, mapping annotation name to an ast.Annotation instance log: subclass of `Log`, used to store log messages; can be None """ super(Signal, self).__init__(name, args, annotations, log) if name and not Callable.is_valid_name(name): self.log.log_issue('signal-name', 'Invalid signal name ‘%s’.' % name)
[docs]class Argument(BaseNode): """ AST representation of an <arg> element. This represents an argument to an `ast.Signal` or `ast.Method`. """ DIRECTION_IN = 'in' DIRECTION_OUT = 'out' required_attributes = ['type'] optional_attributes = ['direction', 'name'] # pylint: disable=too-many-arguments def __init__(self, name, direction, type_, annotations=None, log=None): """ Construct a new ast.Argument. Args: name: argument name; may be empty direction: DIRECTION_IN or DIRECTION_OUT type_: type string for the argument; see http://goo.gl/uCpa5A annotations: potentially empty dict of annotations applied to the argument, mapping annotation name to an ast.Annotation instance log: subclass of `Log`, used to store log messages; can be None """ super(Argument, self).__init__(name, annotations, log) type_parser = TypeParser(type_) self.type = type_parser.parse() if self.type is None: message = type_parser.get_output()[0][3] self.log.log_issue('argument-type', 'Error when parsing type ‘%s’ for argument ' '‘%s’: %s' % (type_, name, message)) self.direction = direction or Argument.DIRECTION_IN self._index = -1 @property def pretty_name(self): """Format the argument's name as a human-readable string""" if self.index == -1 and self.name is None: res = 'unnamed' elif self.index == -1: res = '‘%s’' % self.name elif self.name is None: res = '%u' % self.index else: res = '%u (‘%s’)' % (self.index, self.name) if self.parent: parent_type = type(self.parent).__name__.lower() res += ' of %s%s’' % (parent_type, self.parent.pretty_name) return res @property def index(self): """The index of this argument in its parent's list of arguments""" # Slight optimization, assumes arguments cannot be reparented if self._index != -1: return self._index if not self.parent: return -1 else: self._index = self.parent.arguments.index(self) return self._index
[docs]class Annotation(BaseNode): """ AST representation of an <annotation> element. This represents an arbitrary key-value metadata annotation attached to one of the nodes in an interface. The annotation name can be one of the well-known ones described at http://goo.gl/LgmNUe, or could be something else. """ optional_attributes = ['value'] def __init__(self, name, value=None, log=None): """ Construct a new ast.Annotation. Args: name: annotation name; a non-empty string value: annotation value; any string is permitted log: subclass of `Log`, used to store log messages; can be None """ super(Annotation, self).__init__(name, log=log) self.value = value @property def pretty_name(self): """Format the annotation's name as a human-readable string""" if not self.parent: return self.name return '%s of ‘%s’' % (self.name, self.parent.pretty_name)