# AnnotationParser - parses gtk-doc annotations
-from .ast import (Array, Callback, Class, Enum, Field, Function, Interface,
- List, Map, Parameter, Record, Return, Type, Union, Varargs,
+import re
+import sys
+
+from .ast import (Array, Bitfield, Callback, Class, Enum, Field, Function,
+ Interface, List, Map, Parameter, Record, Return, Type, Union,
+ Varargs,
default_array_types,
BASIC_GIR_TYPES,
PARAM_DIRECTION_INOUT,
_COMMENT_HEADER = '*\n '
# Tags - annotations applyed to comment blocks
-TAG_SINCE = 'Since'
-TAG_DEPRECATED = 'Deprecated'
-TAG_RETURNS = 'Returns'
-TAG_RETURNS_ALT = 'Return value'
+TAG_VFUNC = 'virtual'
+TAG_SINCE = 'since'
+TAG_DEPRECATED = 'deprecated'
+TAG_RETURNS = 'returns'
+TAG_RETURNS_ALT = 'return value'
+TAG_ATTRIBUTES = 'attributes'
+TAG_RENAME_TO = 'rename to'
# Options - annotations for parameters and return values
OPT_ALLOW_NONE = 'allow-none'
OPT_SCOPE = 'scope'
OPT_TRANSFER = 'transfer'
OPT_TYPE = 'type'
+OPT_CLOSURE = 'closure'
+OPT_DESTROY = 'destroy'
+OPT_SKIP = 'skip'
+
+# Specific option values
+OPT_VAL_BITFIELD = 'bitfield'
# Array options - array specific annotations
OPT_ARRAY_FIXED_SIZE = 'fixed-size'
OPT_ARRAY_LENGTH = 'length'
OPT_ARRAY_ZERO_TERMINATED = 'zero-terminated'
+OPT_SCOPE_ASYNC = 'async'
+OPT_SCOPE_CALL = 'call'
+OPT_SCOPE_NOTIFIED = 'notified'
class InvalidAnnotationError(Exception):
pass
class DocBlock(object):
- def __init__(self, name):
+ def __init__(self, name, options):
self.name = name
+ self.options = options
self.value = None
self.tags = odict()
+ self.comment = None
def __repr__(self):
- return '<DocBlock %r>' % (self.name, )
+ return '<DocBlock %r %r>' % (self.name, self.options)
def get(self, name):
if name == TAG_RETURNS:
def __init__(self, name):
self.name = name
- self.options = []
+ self.options = {}
+ self.comment = None
+ def __repr__(self):
+ return '<DocTag %r %r>' % (self.name, self.options)
class Option(object):
# /**
# * symbol:
#
+ # Or, alternatively, with options:
+ # /**
+ # * symbol: (name value) ...
+ #
# symbol is currently one of:
# - function: gtk_widget_show
# - signal: GtkWidget::destroy
pos = comment.find('\n ')
if pos == -1:
return
- block_name = comment[:pos]
- block_name = block_name.strip()
- if not block_name.endswith(':'):
- return
- block = DocBlock(block_name[:-1])
- content = comment[pos+1:]
- for line in content.split('\n'):
+ block_header = comment[:pos]
+ block_header = block_header.strip()
+ cpos = block_header.find(': ')
+ if cpos:
+ block_name = block_header[:cpos]
+ block_options, rest = self._parse_options(block_header[cpos+2:])
+ if rest:
+ return
+ else:
+ block_name, block_options = block_header, {}
+ block = DocBlock(block_name, block_options)
+ comment_lines = []
+ for line in comment[pos+1:].split('\n'):
line = line.lstrip()
line = line[2:].strip() # Skip ' *'
if not line:
continue
-
if line.startswith('@'):
line = line[1:]
elif not ': ' in line:
+ comment_lines.append(line)
continue
- tag = self._parse_tag(line)
- block.tags[tag.name] = tag
-
+ tag_name, value = self._split_tag_namevalue(line)
+ canon_name = tag_name.lower()
+ if canon_name in block.tags:
+ print >> sys.stderr, (
+ "Symbol %s has multiple definition of tag %r" % (
+ block_name, canon_name, ))
+ block.tags[canon_name] = self._create_tag(canon_name, value)
+ block.comment = '\n'.join(comment_lines)
self._blocks[block.name] = block
- def _parse_tag(self, value):
- # Tag: bar
- # Tag: bar opt1 opt2
- parts = value.split(': ', 1)
+ def _split_tag_namevalue(self, raw):
+ """Split a line into tag name and value"""
+ parts = raw.split(': ', 1)
if len(parts) == 1:
tag_name = parts[0]
- options = ''
+ value = ''
+ if tag_name.endswith(':'):
+ tag_name = tag_name[:-1]
else:
- tag_name, options = parts
+ tag_name, value = parts
+ return (tag_name, value)
+
+ def _create_tag(self, tag_name, value):
+ # Tag: bar
+ # Tag: bar opt1 opt2
tag = DocTag(tag_name)
- tag.value = options
- tag.options = self._parse_options(options)
+ tag.value = value
+ options, rest = self._parse_options(tag.value)
+ tag.options = options
+ tag.comment = rest
return tag
def _parse_options(self, value):
# (bar opt1 opt2...)
opened = -1
options = {}
+ last = None
for i, c in enumerate(value):
if c == '(' and opened == -1:
opened = i+1
if option is not None:
option = Option(option)
options[name] = option
+ last = i + 2
opened = -1
- return options
+
+ if last is not None:
+ rest = value[last:].strip()
+ else:
+ rest = None
+ return options, rest
class AnnotationApplier(object):
return block.get(tag_name)
def parse(self, namespace):
- for node in namespace.nodes:
+ self._namespace = namespace
+ for node in namespace.nodes[:]:
self._parse_node(node)
+ del self._namespace
# Boring parsing boilerplate.
self._parse_function(node)
elif isinstance(node, Enum):
self._parse_enum(node)
+ elif isinstance(node, Bitfield):
+ self._parse_bitfield(node)
elif isinstance(node, Class):
self._parse_class(node)
elif isinstance(node, Interface):
self._parse_boxed(node)
def _parse_class(self, class_):
- block = self._blocks.get(class_.name)
- self._parse_version(class_, block)
+ block = self._blocks.get(class_.type_name)
+ self._parse_node_common(class_, block)
self._parse_constructors(class_.constructors)
- self._parse_methods(class_.methods)
- self._parse_methods(class_.static_methods)
+ self._parse_methods(class_, class_.methods)
+ self._parse_vfuncs(class_, class_.virtual_methods)
+ self._parse_methods(class_, class_.static_methods)
self._parse_properties(class_, class_.properties)
self._parse_signals(class_, class_.signals)
self._parse_fields(class_, class_.fields)
+ if block:
+ class_.doc = block.comment
def _parse_interface(self, interface):
- block = self._blocks.get(interface.name)
- self._parse_version(interface, block)
- self._parse_methods(interface.methods)
+ block = self._blocks.get(interface.type_name)
+ self._parse_node_common(interface, block)
+ self._parse_methods(interface, interface.methods)
+ self._parse_vfuncs(interface, interface.virtual_methods)
self._parse_properties(interface, interface.properties)
self._parse_signals(interface, interface.signals)
self._parse_fields(interface, interface.fields)
+ if block:
+ interface.doc = block.comment
def _parse_record(self, record):
block = self._blocks.get(record.symbol)
- self._parse_version(record, block)
+ self._parse_node_common(record, block)
self._parse_constructors(record.constructors)
- self._parse_fields(record, record.fields)
- if isinstance(record, GLibBoxed):
- self._parse_methods(record.methods)
+ self._parse_methods(record, record.methods)
+ self._parse_fields(record, record.fields, block)
+ if block:
+ record.doc = block.comment
def _parse_boxed(self, boxed):
block = self._blocks.get(boxed.name)
- self._parse_version(boxed, block)
+ self._parse_node_common(boxed, block)
self._parse_constructors(boxed.constructors)
- self._parse_methods(boxed.methods)
+ self._parse_methods(boxed, boxed.methods)
+ if block:
+ boxed.doc = block.comment
def _parse_union(self, union):
block = self._blocks.get(union.name)
- self._parse_fields(union, union.fields)
+ self._parse_node_common(union, block)
+ self._parse_fields(union, union.fields, block)
self._parse_constructors(union.constructors)
- if isinstance(union, GLibBoxed):
- self._parse_methods(union.methods)
+ self._parse_methods(union, union.methods)
+ if block:
+ union.doc = block.comment
def _parse_enum(self, enum):
block = self._blocks.get(enum.symbol)
- self._parse_version(enum, block)
+ self._parse_node_common(enum, block)
+ if block:
+ enum.doc = block.comment
+ type_opt = block.options.get(OPT_TYPE)
+ if type_opt and type_opt.one() == OPT_VAL_BITFIELD:
+ # This is hack, but hey, it works :-)
+ enum.__class__ = Bitfield
+
+ def _parse_bitfield(self, bitfield):
+ block = self._blocks.get(bitfield.symbol)
+ self._parse_node_common(bitfield, block)
+ if block:
+ bitfield.doc = block.comment
def _parse_constructors(self, constructors):
for ctor in constructors:
self._parse_function(ctor)
- def _parse_fields(self, parent, fields):
+ def _parse_fields(self, parent, fields, block=None):
for field in fields:
- self._parse_field(parent, field)
+ self._parse_field(parent, field, block)
def _parse_properties(self, parent, properties):
for prop in properties:
self._parse_property(parent, prop)
- def _parse_methods(self, methods):
+ def _parse_methods(self, parent, methods):
for method in methods:
- self._parse_function(method)
+ self._parse_method(parent, method)
+
+ def _parse_vfuncs(self, parent, vfuncs):
+ for vfunc in vfuncs:
+ self._parse_vfunc(parent, vfunc)
def _parse_signals(self, parent, signals):
for signal in signals:
def _parse_property(self, parent, prop):
block = self._blocks.get('%s:%s' % (parent.type_name, prop.name))
- self._parse_version(prop, block)
- self._parse_deprecated(prop, block)
+ self._parse_node_common(prop, block)
+ if block:
+ prop.doc = block.comment
def _parse_callback(self, callback):
block = self._blocks.get(callback.ctype)
- self._parse_version(callback, block)
+ self._parse_node_common(callback, block)
self._parse_params(callback, callback.parameters, block)
self._parse_return(callback, callback.retval, block)
+ if block:
+ callback.doc = block.comment
+
+ def _parse_callable(self, callable, block):
+ self._parse_node_common(callable, block)
+ for i, param in enumerate(callable.parameters):
+ if (param.type.ctype != 'GDestroyNotify' and
+ param.type.name != 'GLib.DestroyNotify'):
+ continue
+ if i < 2:
+ break
+ callback_param = callable.parameters[i-2]
+ if callback_param.closure_index != -1:
+ callback_param.scope = OPT_SCOPE_NOTIFIED
+ callback_param.transfer = PARAM_TRANSFER_NONE
+
+ self._parse_params(callable, callable.parameters, block)
+ self._parse_return(callable, callable.retval, block)
+ if block:
+ callable.doc = block.comment
def _parse_function(self, func):
block = self._blocks.get(func.symbol)
- self._parse_version(func, block)
- self._parse_deprecated(func, block)
- self._parse_params(func, func.parameters, block)
- self._parse_return(func, func.retval, block)
+ self._parse_callable(func, block)
+ self._parse_rename_to_func(func, block)
def _parse_signal(self, parent, signal):
block = self._blocks.get('%s::%s' % (parent.type_name, signal.name))
- self._parse_version(signal, block)
- self._parse_deprecated(signal, block)
+ self._parse_node_common(signal, block)
# We're only attempting to name the signal parameters if
# the number of parameter tags (@foo) is the same or greater
# than the number of signal parameters
options = getattr(tag, 'options', {})
param_type = options.get(OPT_TYPE)
if param_type:
- param.type.name = param_type.one()
+ param.type = self._resolve(param_type.one(), param.type)
else:
tag = None
self._parse_param(signal, param, tag)
self._parse_return(signal, signal.retval, block)
-
- def _parse_field(self, parent, field):
+ if block:
+ signal.doc = block.comment
+
+ def _parse_method(self, parent, meth):
+ block = self._blocks.get(meth.symbol)
+ self._parse_function(meth)
+ virtual = self._get_tag(block, TAG_VFUNC)
+ if virtual:
+ invoker_name = virtual.value
+ matched = False
+ for vfunc in parent.virtual_methods:
+ if vfunc.name == invoker_name:
+ matched = True
+ vfunc.invoker = meth.name
+ break
+ if not matched:
+ print "warning: unmatched virtual invoker %r for method %r" % \
+ (invoker_name, meth.symbol)
+
+ def _parse_vfunc(self, parent, vfunc):
+ key = '%s::%s' % (parent.type_name, vfunc.name)
+ self._parse_callable(vfunc, self._blocks.get(key))
+
+ def _parse_field(self, parent, field, block=None):
if isinstance(field, Callback):
self._parse_callback(field)
+ else:
+ if not block:
+ return
+ tag = block.get(field.name)
+ if not tag:
+ return
+ t = tag.options.get('type')
+ if not t:
+ return
+ field.type.name = self._transformer.resolve_type_name(t.one())
def _parse_params(self, parent, params, block):
for param in params:
def _parse_return(self, parent, return_, block):
tag = self._get_tag(block, TAG_RETURNS)
- options = getattr(tag, 'options', {})
- self._parse_param_ret_common(parent, return_, options)
+ self._parse_param_ret_common(parent, return_, tag)
def _parse_param(self, parent, param, tag):
options = getattr(tag, 'options', {})
-
if isinstance(parent, Function):
scope = options.get(OPT_SCOPE)
if scope:
param.scope = scope.one()
param.transfer = PARAM_TRANSFER_NONE
- self._parse_param_ret_common(parent, param, options)
+ elif (param.type.ctype == 'GAsyncReadyCallback' or
+ param.type.name == 'Gio.AsyncReadyCallback'):
+ param.scope = OPT_SCOPE_ASYNC
+ param.transfer = PARAM_TRANSFER_NONE
- def _parse_param_ret_common(self, parent, node, options):
+ destroy = options.get(OPT_DESTROY)
+ if destroy:
+ param.destroy_index = parent.get_parameter_index(destroy.one())
+ self._fixup_param_destroy(parent, param)
+ closure = options.get(OPT_CLOSURE)
+ if closure:
+ param.closure_index = parent.get_parameter_index(closure.one())
+ self._fixup_param_closure(parent, param)
+ if isinstance(parent, Callback):
+ if OPT_CLOSURE in options:
+ param.closure_index = parent.get_parameter_index(param.name)
+ self._fixup_param_closure(parent, param)
+
+ self._parse_param_ret_common(parent, param, tag)
+
+ def _fixup_param_destroy(self, parent, param):
+ for p in parent.parameters:
+ if p is not param and p.destroy_index == param.destroy_index:
+ p.destroy_index = None
+
+ def _fixup_param_closure(self, parent, param):
+ for p in parent.parameters:
+ if p is not param and p.closure_index == param.closure_index:
+ p.closure_index = None
+
+ def _parse_param_ret_common(self, parent, node, tag):
+ options = getattr(tag, 'options', {})
node.direction = self._extract_direction(node, options)
container_type = self._extract_container_type(
parent, node, options)
if node.direction is None:
node.direction = self._guess_direction(node)
node.transfer = self._extract_transfer(parent, node, options)
- if OPT_ALLOW_NONE in options:
+ param_type = options.get(OPT_TYPE)
+ if param_type:
+ node.type = self._resolve(param_type.one(), node.type)
+
+ if (OPT_ALLOW_NONE in options or
+ node.type.ctype == 'GCancellable*'):
node.allow_none = True
assert node.transfer is not None
+ if tag is not None and tag.comment is not None:
+ node.doc = tag.comment
def _extract_direction(self, node, options):
if (OPT_INOUT in options or
if (not isinstance(node, Field) and
(not has_element_type and
(node.direction is None
+ or isinstance(node, Return)
or node.direction == PARAM_DIRECTION_IN))):
if self._guess_array(node):
has_array = True
element_type = options.get(OPT_ELEMENT_TYPE)
if element_type is not None:
- element_type_name = element_type.one()
+ element_type_node = self._resolve(element_type.one())
else:
- element_type_name = node.type.name
+ element_type_node = Type(node.type.name) # erase ctype
container_type = Array(node.type.ctype,
- element_type_name)
+ element_type_node)
+ container_type.is_const = node.type.is_const
if OPT_ARRAY_ZERO_TERMINATED in array_values:
container_type.zeroterminated = array_values.get(
OPT_ARRAY_ZERO_TERMINATED) == '1'
# is specified.
if (isinstance(node, Parameter) and
node.type.name == 'utf8' and
- self._guess_direction(node) == PARAM_DIRECTION_IN):
+ self._guess_direction(node) == PARAM_DIRECTION_IN and
+ element_type is None):
# FIXME: unsigned char/guchar should be uint8
- container_type.element_type = 'int8'
+ container_type.element_type = Type('int8')
container_type.size = array_values.get(OPT_ARRAY_FIXED_SIZE)
return container_type
+ def _resolve(self, type_str, orig_node=None):
+ def grab_one(type_str, resolver, top_combiner, combiner):
+ """Return a complete type, and the trailing string part after it.
+ Use resolver() on each identifier, and combiner() on the parts of
+ each complete type. (top_combiner is used on the top-most type.)"""
+ bits = re.split(r'([,<>])', type_str, 1)
+ first, sep, rest = [bits[0], '', ''] if (len(bits)==1) else bits
+ args = [resolver(first)]
+ if sep == '<':
+ while sep != '>':
+ next, rest = grab_one(rest, resolver, combiner, combiner)
+ args.append(next)
+ sep, rest = rest[0], rest[1:]
+ else:
+ rest = sep + rest
+ return top_combiner(*args), rest
+ def resolver(ident):
+ return self._transformer.resolve_param_type(Type(ident))
+ def combiner(base, *rest):
+ if not rest:
+ return base
+ if (base.name in ['GLib.List', 'GLib.SList'] or
+ base.ctype in ['GList*', 'GSList*']) and len(rest)==1:
+ return List(base.name, base.ctype, *rest)
+ if (base.name in ['GLib.HashTable'] or
+ base.ctype in ['GHashTable*']) and len(rest)==2:
+ return Map(base.name, base.ctype, *rest)
+ print "WARNING: throwing away type parameters:", type_str
+ return base
+ def top_combiner(base, *rest):
+ """For the top most type, recycle orig_node if possible."""
+ if orig_node is not None:
+ orig_node.name = base.name
+ base = orig_node # preserve other properties of orig_node
+ return combiner(base, *rest)
+
+ result, rest = grab_one(type_str, resolver, top_combiner, combiner)
+ if rest:
+ print "WARNING: throwing away trailing part of type:", type_str
+ return result
+
def _parse_element_type(self, parent, node, options):
element_type_opt = options.get(OPT_ELEMENT_TYPE)
element_type = element_type_opt.flat()
- if node.type.name in ['GLib.List', 'GLib.SList']:
+ if (node.type.name in ['GLib.List', 'GLib.SList'] or
+ node.type.ctype in ['GList*', 'GSList*']):
assert len(element_type) == 1
- etype = Type(element_type[0])
container_type = List(
node.type.name,
node.type.ctype,
- self._transformer.resolve_param_type(etype))
- elif node.type.name in ['GLib.HashTable']:
+ self._resolve(element_type[0]))
+ elif (node.type.name in ['GLib.HashTable'] or
+ node.type.ctype in ['GHashTable*']):
assert len(element_type) == 2
- key_type = Type(element_type[0])
- value_type = Type(element_type[1])
container_type = Map(
node.type.name,
node.type.ctype,
- self._transformer.resolve_param_type(key_type),
- self._transformer.resolve_param_type(value_type))
+ self._resolve(element_type[0]),
+ self._resolve(element_type[1]))
else:
print 'FIXME: unhandled element-type container:', node
return container_type
"none, container, full." % (node, parent.name, transfer))
return transfer
+ def _parse_node_common(self, node, block):
+ self._parse_version(node, block)
+ self._parse_deprecated(node, block)
+ self._parse_attributes(node, block)
+ self._parse_skip(node, block)
+
def _parse_version(self, node, block):
since_tag = self._get_tag(block, TAG_SINCE)
if since_tag is None:
if version is not None:
node.deprecated_version = version
+ def _parse_attributes(self, node, block):
+ annos_tag = self._get_tag(block, TAG_ATTRIBUTES)
+ if annos_tag is None:
+ return
+ for key, value in annos_tag.options.iteritems():
+ if value:
+ node.attributes.append((key, value.one()))
+
+ def _parse_skip(self, node, block):
+ if block is not None:
+ if OPT_SKIP in block.options:
+ node.skip = True
+
+ def _parse_rename_to_func(self, node, block):
+ rename_to_tag = self._get_tag(block, TAG_RENAME_TO)
+ if rename_to_tag is None:
+ return
+ new_name = rename_to_tag.value
+
+ shadowed = []
+
+ def shadowed_filter(n):
+ if isinstance(n, Function) and n.symbol == new_name:
+ shadowed.append(n)
+ return False
+ return True
+
+ self._namespace.remove_matching(shadowed_filter)
+ if len(shadowed) == 1:
+ # method override; use the same (stripped) name as the overloaded
+ # method referenced.
+ # Note that 'g_timeout_add_full' may specify a new_name of
+ # 'g_timeout_add' but the *real* name desired is the stripped name
+ # of 'g_timeout_add' which is 'timeout_add' (for example).
+ node.name = shadowed[0].name
+ elif len(shadowed) == 0:
+ # literal rename, to force a particular prefix strip or whatever
+ # Example: the "nm-utils" package uses a "NM" prefix in most places
+ # but some functions have an "nm_utils_" prefix; the 'Rename To:'
+ # annotation in this case is used to strip the 'utils_' part off.
+ node.name = new_name
+ else:
+ assert False # more than two shadowed methods? Shouldn't happen.
+
def _guess_direction(self, node):
if node.direction:
return node.direction
if node.transfer is not None:
return node.transfer
- if isinstance(node.type, Array):
- return PARAM_TRANSFER_NONE
# Anything with 'const' gets none
if node.type.is_const:
return PARAM_TRANSFER_NONE
-
elif node.type.name in [TYPE_NONE, TYPE_ANY]:
return PARAM_TRANSFER_NONE
elif isinstance(node.type, Varargs):