diff options
author | Lars Wirzenius <lars.wirzenius@codethink.co.uk> | 2013-09-04 16:13:21 +0000 |
---|---|---|
committer | Lars Wirzenius <lars.wirzenius@codethink.co.uk> | 2013-09-04 16:13:21 +0000 |
commit | a194122be492fbddd8748fe323a648434a866c36 (patch) | |
tree | f39293588b7922faa40f0c6b985a031f8f5cf0ae /giscanner/docwriter.py | |
parent | 8a2a9f9db169eaaf2a7eede18e0ceba0af44345f (diff) | |
parent | 85d02d59f101f9eb37829b16a81890ef9801824a (diff) | |
download | gobject-introspection-baserock/jonathan/fix-giscanner.tar.gz |
merge with GOBJECT_INTROSPECTION_1_37_6baserock/jonathan/fix-giscanner
Diffstat (limited to 'giscanner/docwriter.py')
-rw-r--r-- | giscanner/docwriter.py | 644 |
1 files changed, 644 insertions, 0 deletions
diff --git a/giscanner/docwriter.py b/giscanner/docwriter.py new file mode 100644 index 00000000..982ab37c --- /dev/null +++ b/giscanner/docwriter.py @@ -0,0 +1,644 @@ +#!/usr/bin/env python +# -*- Mode: Python -*- +# GObject-Introspection - a framework for introspecting GObject libraries +# Copyright (C) 2010 Zach Goldberg +# Copyright (C) 2011 Johan Dahlin +# Copyright (C) 2011 Shaun McCance +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. +# + +import os +import re +import tempfile + +from xml.sax import saxutils +from mako.lookup import TemplateLookup + +from . import ast, xmlwriter +from .utils import to_underscores + + +def make_page_id(node, recursive=False): + if isinstance(node, ast.Namespace): + if recursive: + return node.name + else: + return 'index' + + if hasattr(node, '_chain') and node._chain: + parent = node._chain[-1] + else: + parent = None + + if parent is None: + return '%s.%s' % (node.namespace.name, node.name) + + if isinstance(node, (ast.Property, ast.Signal, ast.VFunction)): + return '%s-%s' % (make_page_id(parent, recursive=True), node.name) + else: + return '%s.%s' % (make_page_id(parent, recursive=True), node.name) + + +def get_node_kind(node): + if isinstance(node, ast.Namespace): + node_kind = 'namespace' + elif isinstance(node, (ast.Class, ast.Interface)): + node_kind = 'class' + elif isinstance(node, ast.Record): + node_kind = 'record' + elif isinstance(node, ast.Function): + if node.is_method: + node_kind = 'method' + elif node.is_constructor: + node_kind = 'constructor' + else: + node_kind = 'function' + elif isinstance(node, ast.Enum): + node_kind = 'enum' + elif isinstance(node, ast.Property) and node.parent is not None: + node_kind = 'property' + elif isinstance(node, ast.Signal) and node.parent is not None: + node_kind = 'signal' + elif isinstance(node, ast.VFunction) and node.parent is not None: + node_kind = 'vfunc' + else: + node_kind = 'default' + + return node_kind + + +class TemplatedScanner(object): + def __init__(self, specs): + self.specs = self.unmangle_specs(specs) + self.regex = self.make_regex(self.specs) + + def unmangle_specs(self, specs): + mangled = re.compile('<<([a-zA-Z_:]+)>>') + specdict = dict((name.lstrip('!'), spec) for name, spec in specs) + + def unmangle(spec, name=None): + def replace_func(match): + child_spec_name = match.group(1) + + if ':' in child_spec_name: + pattern_name, child_spec_name = child_spec_name.split(':', 1) + else: + pattern_name = None + + child_spec = specdict[child_spec_name] + # Force all child specs of this one to be unnamed + unmangled = unmangle(child_spec, None) + if pattern_name and name: + return '(?P<%s_%s>%s)' % (name, pattern_name, unmangled) + else: + return unmangled + + return mangled.sub(replace_func, spec) + + return [(name, unmangle(spec, name)) for name, spec in specs] + + def make_regex(self, specs): + regex = '|'.join('(?P<%s>%s)' % (name, spec) for name, spec in specs + if not name.startswith('!')) + return re.compile(regex) + + def get_properties(self, name, match): + groupdict = match.groupdict() + properties = {name: groupdict.pop(name)} + name = name + "_" + for group, value in groupdict.iteritems(): + if group.startswith(name): + key = group[len(name):] + properties[key] = value + return properties + + def scan(self, text): + pos = 0 + while True: + match = self.regex.search(text, pos) + if match is None: + break + + start = match.start() + if start > pos: + yield ('other', text[pos:start], None) + + pos = match.end() + name = match.lastgroup + yield (name, match.group(0), self.get_properties(name, match)) + + if pos < len(text): + yield ('other', text[pos:], None) + + +class DocstringScanner(TemplatedScanner): + def __init__(self): + specs = [ + ('!alpha', r'[a-zA-Z0-9_]+'), + ('!alpha_dash', r'[a-zA-Z0-9_-]+'), + ('property', r'#<<type_name:alpha>>:(<<property_name:alpha_dash>>)'), + ('signal', r'#<<type_name:alpha>>::(<<signal_name:alpha_dash>>)'), + ('type_name', r'#(<<type_name:alpha>>)'), + ('enum_value', r'%(<<member_name:alpha>>)'), + ('parameter', r'@<<param_name:alpha>>'), + ('function_call', r'<<symbol_name:alpha>>\(\)'), + ] + + super(DocstringScanner, self).__init__(specs) + + +class DocFormatter(object): + def __init__(self, transformer): + self._transformer = transformer + self._scanner = DocstringScanner() + + def escape(self, text): + return saxutils.escape(text) + + def should_render_node(self, node): + if isinstance(node, ast.Constant): + return False + + return True + + def format(self, node, doc): + if doc is None: + return '' + + result = '' + for para in doc.split('\n\n'): + result += '<p>' + result += self.format_inline(node, para) + result += '</p>' + return result + + def _resolve_type(self, ident): + try: + matches = self._transformer.split_ctype_namespaces(ident) + except ValueError: + return None + for namespace, name in matches: + node = namespace.get(name) + if node: + return node + return None + + def _resolve_symbol(self, symbol): + try: + matches = self._transformer.split_csymbol_namespaces(symbol) + except ValueError: + return None + for namespace, name in matches: + node = namespace.get_by_symbol(symbol) + if node: + return node + return None + + def _find_thing(self, list_, name): + for item in list_: + if item.name == name: + return item + raise KeyError("Could not find %s" % (name, )) + + def _process_other(self, node, match, props): + return self.escape(match) + + def _process_property(self, node, match, props): + type_node = self._resolve_type(props['type_name']) + if type_node is None: + return match + + try: + prop = self._find_thing(type_node.properties, props['property_name']) + except (AttributeError, KeyError): + return match + + return self.format_xref(prop) + + def _process_signal(self, node, match, props): + type_node = self._resolve_type(props['type_name']) + if type_node is None: + return match + + try: + signal = self._find_thing(type_node.signals, props['signal_name']) + except (AttributeError, KeyError): + return match + + return self.format_xref(signal) + + def _process_type_name(self, node, match, props): + type_ = self._resolve_type(props['type_name']) + if type_ is None: + return match + + return self.format_xref(type_) + + def _process_enum_value(self, node, match, props): + member_name = props['member_name'] + + try: + return '<code>%s</code>' % (self.fundamentals[member_name], ) + except KeyError: + pass + + enum_value = self._resolve_symbol(member_name) + if enum_value: + return self.format_xref(enum_value) + + return match + + def _process_parameter(self, node, match, props): + try: + parameter = node.get_parameter(props['param_name']) + except (AttributeError, ValueError): + return match + + return '<code>%s</code>' % (self.format_parameter_name(node, parameter), ) + + def _process_function_call(self, node, match, props): + func = self._resolve_symbol(props['symbol_name']) + if func is None: + return match + + return self.format_xref(func) + + def _process_token(self, node, tok): + kind, match, props = tok + + dispatch = { + 'other': self._process_other, + 'property': self._process_property, + 'signal': self._process_signal, + 'type_name': self._process_type_name, + 'enum_value': self._process_enum_value, + 'parameter': self._process_parameter, + 'function_call': self._process_function_call, + } + + return dispatch[kind](node, match, props) + + def get_parameters(self, node): + raise NotImplementedError + + def format_inline(self, node, para): + tokens = self._scanner.scan(para) + words = [self._process_token(node, tok) for tok in tokens] + return ''.join(words) + + def format_parameter_name(self, node, parameter): + if isinstance(parameter.type, ast.Varargs): + return "..." + else: + return parameter.argname + + def format_function_name(self, func): + raise NotImplementedError + + def format_type(self, type_): + raise NotImplementedError + + def format_page_name(self, node): + if isinstance(node, ast.Namespace): + return 'Index' + elif isinstance(node, ast.Function): + return self.format_function_name(node) + elif isinstance(node, ast.Property) and node.parent is not None: + return '%s:%s' % (self.format_page_name(node.parent), node.name) + elif isinstance(node, ast.Signal) and node.parent is not None: + return '%s::%s' % (self.format_page_name(node.parent), node.name) + elif isinstance(node, ast.VFunction) and node.parent is not None: + return '%s::%s' % (self.format_page_name(node.parent), node.name) + else: + return make_page_id(node) + + def format_xref(self, node, **attrdict): + if node is None: + attrs = [('xref', 'index')] + attrdict.items() + return xmlwriter.build_xml_tag('link', attrs) + elif isinstance(node, ast.Member): + # Enum/BitField members are linked to the main enum page. + return self.format_xref(node.parent, **attrdict) + '.' + node.name + else: + attrs = [('xref', make_page_id(node))] + attrdict.items() + return xmlwriter.build_xml_tag('link', attrs) + + def format_property_flags(self, property_, construct_only=False): + flags = [] + if property_.readable and not construct_only: + flags.append("Read") + if property_.writable and not construct_only: + flags.append("Write") + if property_.construct: + flags.append("Construct") + if property_.construct_only: + flags.append("Construct Only") + + return " / ".join(flags) + + def to_underscores(self, string): + return to_underscores(string) + + def get_class_hierarchy(self, node): + parent_chain = [node] + + while node.parent_type: + node = self._transformer.lookup_typenode(node.parent_type) + parent_chain.append(node) + + parent_chain.reverse() + return parent_chain + + +class DocFormatterC(DocFormatter): + language = "C" + mime_type = "text/x-csrc" + + fundamentals = { + "TRUE": "TRUE", + "FALSE": "FALSE", + "NULL": "NULL", + } + + def format_type(self, type_): + if isinstance(type_, ast.Array): + return self.format_type(type_.element_type) + '*' + elif type_.ctype is not None: + return type_.ctype + elif type_.target_fundamental: + return type_.target_fundamental + else: + node = self._transformer.lookup_typenode(type_) + return getattr(node, 'ctype') + + def format_function_name(self, func): + if isinstance(func, ast.Function): + return func.symbol + else: + return func.name + + def get_parameters(self, node): + return node.all_parameters + + +class DocFormatterIntrospectableBase(DocFormatter): + def should_render_node(self, node): + if isinstance(node, ast.Record) and node.is_gtype_struct_for is not None: + return False + + if not getattr(node, "introspectable", True): + return False + + return super(DocFormatterIntrospectableBase, self).should_render_node(node) + + +class DocFormatterPython(DocFormatterIntrospectableBase): + language = "Python" + mime_type = "text/python" + + fundamentals = { + "TRUE": "True", + "FALSE": "False", + "NULL": "None", + } + + def should_render_node(self, node): + if getattr(node, "is_constructor", False): + return False + + return super(DocFormatterPython, self).should_render_node(node) + + def is_method(self, node): + if getattr(node, "is_method", False): + return True + + if isinstance(node, ast.VFunction): + return True + + return False + + def format_parameter_name(self, node, parameter): + # Force "self" for the first parameter of a method + if self.is_method(node) and parameter is node.instance_parameter: + return "self" + elif isinstance(parameter.type, ast.Varargs): + return "..." + else: + return parameter.argname + + def format_fundamental_type(self, name): + fundamental_types = { + "utf8": "unicode", + "gunichar": "unicode", + "gchar": "str", + "guchar": "str", + "gboolean": "bool", + "gint": "int", + "guint": "int", + "glong": "int", + "gulong": "int", + "gint64": "int", + "guint64": "int", + "gfloat": "float", + "gdouble": "float", + "gchararray": "str", + "GParam": "GLib.Param", + "PyObject": "object", + "GStrv": "[str]", + "GVariant": "GLib.Variant"} + + return fundamental_types.get(name, name) + + def format_type(self, type_): + if isinstance(type_, (ast.List, ast.Array)): + return '[' + self.format_type(type_.element_type) + ']' + elif isinstance(type_, ast.Map): + return '{%s: %s}' % (self.format_type(type_.key_type), + self.format_type(type_.value_type)) + elif type_.target_giname is not None: + return type_.target_giname + else: + return self.format_fundamental_type(type_.target_fundamental) + + def format_function_name(self, func): + if func.parent is not None: + return "%s.%s" % (self.format_page_name(func.parent), func.name) + else: + return func.name + + def get_parameters(self, node): + return node.all_parameters + + +class DocFormatterGjs(DocFormatterIntrospectableBase): + language = "Gjs" + mime_type = "text/x-gjs" + + fundamentals = { + "TRUE": "true", + "FALSE": "false", + "NULL": "null", + } + + def is_method(self, node): + if getattr(node, "is_method", False): + return True + + if isinstance(node, ast.VFunction): + return True + + return False + + def format_fundamental_type(self, name): + fundamental_types = { + "utf8": "String", + "gunichar": "String", + "gchar": "String", + "guchar": "String", + "gboolean": "Boolean", + "gint": "Number", + "guint": "Number", + "glong": "Number", + "gulong": "Number", + "gint64": "Number", + "guint64": "Number", + "gfloat": "Number", + "gdouble": "Number", + "gchararray": "String", + "GParam": "GLib.Param", + "PyObject": "Object", + "GStrv": "[String]", + "GVariant": "GLib.Variant"} + + return fundamental_types.get(name, name) + + def format_type(self, type_): + if isinstance(type_, (ast.List, ast.Array)): + return '[' + self.format_type(type_.element_type) + ']' + elif isinstance(type_, ast.Map): + return '{%s: %s}' % (self.format_type(type_.key_type), + self.format_type(type_.value_type)) + elif type_.target_fundamental == "none": + return "void" + elif type_.target_giname is not None: + return type_.target_giname + else: + return self.format_fundamental_type(type_.target_fundamental) + + def format_function_name(self, func): + if func.is_method: + return "%s.prototype.%s" % (self.format_page_name(func.parent), func.name) + elif func.is_constructor: + return "%s.%s" % (self.format_page_name(func.parent), func.name) + else: + return func.name + + def get_parameters(self, node): + skip = [] + for param in node.parameters: + if param.direction == ast.PARAM_DIRECTION_OUT: + skip.append(param) + if param.closure_name is not None: + skip.append(node.get_parameter(param.closure_name)) + if param.destroy_name is not None: + skip.append(node.get_parameter(param.destroy_name)) + if isinstance(param.type, ast.Array) and param.type.length_param_name is not None: + skip.append(node.get_parameter(param.type.length_param_name)) + + params = [] + for param in node.parameters: + if param not in skip: + params.append(param) + return params + + +LANGUAGES = { + "c": DocFormatterC, + "python": DocFormatterPython, + "gjs": DocFormatterGjs, +} + + +class DocWriter(object): + def __init__(self, transformer, language): + self._transformer = transformer + + try: + formatter_class = LANGUAGES[language.lower()] + except KeyError: + raise SystemExit("Unsupported language: %s" % (language, )) + + self._formatter = formatter_class(self._transformer) + self._language = self._formatter.language + + self._lookup = self._get_template_lookup() + + def _get_template_lookup(self): + if 'UNINSTALLED_INTROSPECTION_SRCDIR' in os.environ: + top_srcdir = os.environ['UNINSTALLED_INTROSPECTION_SRCDIR'] + srcdir = os.path.join(top_srcdir, 'giscanner') + else: + srcdir = os.path.dirname(__file__) + + template_dir = os.path.join(srcdir, 'doctemplates') + + return TemplateLookup(directories=[template_dir], + module_directory=tempfile.mkdtemp(), + output_encoding='utf-8') + + def write(self, output): + try: + os.makedirs(output) + except OSError: + # directory already made + pass + + self._walk_node(output, self._transformer.namespace, []) + self._transformer.namespace.walk(lambda node, chain: self._walk_node(output, node, chain)) + + def _walk_node(self, output, node, chain): + if isinstance(node, ast.Function) and node.moved_to is not None: + return False + if getattr(node, 'disguised', False): + return False + if self._formatter.should_render_node(node): + self._render_node(node, chain, output) + return True + return False + + def _render_node(self, node, chain, output): + namespace = self._transformer.namespace + + # A bit of a hack...maybe this should be an official API + node._chain = list(chain) + + page_kind = get_node_kind(node) + template_name = '%s/%s.tmpl' % (self._language, page_kind) + page_id = make_page_id(node) + + template = self._lookup.get_template(template_name) + result = template.render(namespace=namespace, + node=node, + page_id=page_id, + page_kind=page_kind, + formatter=self._formatter) + + output_file_name = os.path.join(os.path.abspath(output), + page_id + '.page') + fp = open(output_file_name, 'w') + fp.write(result) + fp.close() |