Imported Upstream version 1.35.9
[platform/upstream/gobject-introspection.git] / giscanner / docwriter.py
1 #!/usr/bin/env python
2 # -*- Mode: Python -*-
3 # GObject-Introspection - a framework for introspecting GObject libraries
4 # Copyright (C) 2010 Zach Goldberg
5 # Copyright (C) 2011 Johan Dahlin
6 # Copyright (C) 2011 Shaun McCance
7 #
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License
10 # as published by the Free Software Foundation; either version 2
11 # of the License, or (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 # 02110-1301, USA.
22 #
23
24 import os
25 import re
26 import tempfile
27
28 from xml.sax import saxutils
29 from mako.lookup import TemplateLookup
30
31 from . import ast, xmlwriter
32 from .utils import to_underscores
33
34 def make_page_id(node, recursive=False):
35     if isinstance(node, ast.Namespace):
36         if recursive:
37             return node.name
38         else:
39             return 'index'
40
41     if hasattr(node, '_chain') and node._chain:
42         parent = node._chain[-1]
43     else:
44         parent = None
45
46     if parent is None:
47         return '%s.%s' % (node.namespace.name, node.name)
48
49     if isinstance(node, (ast.Property, ast.Signal, ast.VFunction)):
50         return '%s-%s' % (make_page_id(parent, recursive=True), node.name)
51     else:
52         return '%s.%s' % (make_page_id(parent, recursive=True), node.name)
53
54 def get_node_kind(node):
55     if isinstance(node, ast.Namespace):
56         node_kind = 'namespace'
57     elif isinstance(node, (ast.Class, ast.Interface)):
58         node_kind = 'class'
59     elif isinstance(node, ast.Record):
60         node_kind = 'record'
61     elif isinstance(node, ast.Function):
62         if node.is_method:
63             node_kind = 'method'
64         elif node.is_constructor:
65             node_kind = 'constructor'
66         else:
67             node_kind = 'function'
68     elif isinstance(node, ast.Enum):
69         node_kind = 'enum'
70     elif isinstance(node, ast.Property) and node.parent is not None:
71         node_kind = 'property'
72     elif isinstance(node, ast.Signal) and node.parent is not None:
73         node_kind = 'signal'
74     elif isinstance(node, ast.VFunction) and node.parent is not None:
75         node_kind = 'vfunc'
76     else:
77         node_kind = 'default'
78
79     return node_kind
80
81 class TemplatedScanner(object):
82     def __init__(self, specs):
83         self.specs = self.unmangle_specs(specs)
84         self.regex = self.make_regex(self.specs)
85
86     def unmangle_specs(self, specs):
87         mangled = re.compile('<<([a-zA-Z_:]+)>>')
88         specdict = dict((name.lstrip('!'), spec) for name, spec in specs)
89
90         def unmangle(spec, name=None):
91             def replace_func(match):
92                 child_spec_name = match.group(1)
93
94                 if ':' in child_spec_name:
95                     pattern_name, child_spec_name = child_spec_name.split(':', 1)
96                 else:
97                     pattern_name = None
98
99                 child_spec = specdict[child_spec_name]
100                 # Force all child specs of this one to be unnamed
101                 unmangled = unmangle(child_spec, None)
102                 if pattern_name and name:
103                     return '(?P<%s_%s>%s)' % (name, pattern_name, unmangled)
104                 else:
105                     return unmangled
106
107             return mangled.sub(replace_func, spec)
108
109         return [(name, unmangle(spec, name)) for name, spec in specs]
110
111     def make_regex(self, specs):
112         regex = '|'.join('(?P<%s>%s)' % (name, spec) for name, spec in specs
113                          if not name.startswith('!'))
114         return re.compile(regex)
115
116     def get_properties(self, name, match):
117         groupdict = match.groupdict()
118         properties = {name: groupdict.pop(name)}
119         name = name + "_"
120         for group, value in groupdict.iteritems():
121             if group.startswith(name):
122                 key = group[len(name):]
123                 properties[key] = value
124         return properties
125
126     def scan(self, text):
127         pos = 0
128         while True:
129             match = self.regex.search(text, pos)
130             if match is None:
131                 break
132
133             start = match.start()
134             if start > pos:
135                 yield ('other', text[pos:start], None)
136
137             pos = match.end()
138             name = match.lastgroup
139             yield (name, match.group(0), self.get_properties(name, match))
140
141         if pos < len(text):
142             yield ('other', text[pos:], None)
143
144 class DocstringScanner(TemplatedScanner):
145     def __init__(self):
146         specs = [
147             ('!alpha', r'[a-zA-Z0-9_]+'),
148             ('!alpha_dash', r'[a-zA-Z0-9_-]+'),
149             ('property', r'#<<type_name:alpha>>:(<<property_name:alpha_dash>>)'),
150             ('signal', r'#<<type_name:alpha>>::(<<signal_name:alpha_dash>>)'),
151             ('type_name', r'#(<<type_name:alpha>>)'),
152             ('enum_value', r'%(<<member_name:alpha>>)'),
153             ('parameter', r'@<<param_name:alpha>>'),
154             ('function_call', r'<<symbol_name:alpha>>\(\)'),
155         ]
156
157         super(DocstringScanner, self).__init__(specs)
158
159 class DocFormatter(object):
160     def __init__(self, transformer):
161         self._transformer = transformer
162         self._scanner = DocstringScanner()
163
164     def escape(self, text):
165         return saxutils.escape(text)
166
167     def should_render_node(self, node):
168         if isinstance(node, ast.Constant):
169             return False
170
171         return True
172
173     def format(self, node, doc):
174         if doc is None:
175             return ''
176
177         result = ''
178         for para in doc.split('\n\n'):
179             result += '<p>'
180             result += self.format_inline(node, para)
181             result += '</p>'
182         return result
183
184     def _resolve_type(self, ident):
185         try:
186             matches = self._transformer.split_ctype_namespaces(ident)
187         except ValueError:
188             return None
189         for namespace, name in matches:
190             node = namespace.get(name)
191             if node:
192                 return node
193         return None
194
195     def _resolve_symbol(self, symbol):
196         try:
197             matches = self._transformer.split_csymbol_namespaces(symbol)
198         except ValueError:
199             return None
200         for namespace, name in matches:
201             node = namespace.get_by_symbol(symbol)
202             if node:
203                 return node
204         return None
205
206     def _find_thing(self, list_, name):
207         for item in list_:
208             if item.name == name:
209                 return item
210         raise KeyError("Could not find %s" % (name, ))
211
212     def _process_other(self, node, match, props):
213         return self.escape(match)
214
215     def _process_property(self, node, match, props):
216         type_node = self._resolve_type(props['type_name'])
217         if type_node is None:
218             return match
219
220         try:
221             prop = self._find_thing(type_node.properties, props['property_name'])
222         except (AttributeError, KeyError), e:
223             return match
224
225         return self.format_xref(prop)
226
227     def _process_signal(self, node, match, props):
228         type_node = self._resolve_type(props['type_name'])
229         if type_node is None:
230             return match
231
232         try:
233             signal = self._find_thing(type_node.signals, props['signal_name'])
234         except (AttributeError, KeyError), e:
235             return match
236
237         return self.format_xref(signal)
238
239     def _process_type_name(self, node, match, props):
240         type_ = self._resolve_type(props['type_name'])
241         if type_ is None:
242             return match
243
244         return self.format_xref(type_)
245
246     def _process_enum_value(self, node, match, props):
247         member_name = props['member_name']
248
249         try:
250             return '<code>%s</code>' % (self.fundamentals[member_name], )
251         except KeyError:
252             pass
253
254         enum_value = self._resolve_symbol(member_name)
255         if enum_value:
256             return self.format_xref(enum_value)
257
258         return match
259
260     def _process_parameter(self, node, match, props):
261         try:
262             parameter = node.get_parameter(props['param_name'])
263         except (AttributeError, ValueError), e:
264             return match
265
266         return '<code>%s</code>' % (self.format_parameter_name(node, parameter), )
267
268     def _process_function_call(self, node, match, props):
269         func = self._resolve_symbol(props['symbol_name'])
270         if func is None:
271             return match
272
273         return self.format_xref(func)
274
275     def _process_token(self, node, tok):
276         kind, match, props = tok
277
278         dispatch = {
279             'other': self._process_other,
280             'property': self._process_property,
281             'signal': self._process_signal,
282             'type_name': self._process_type_name,
283             'enum_value': self._process_enum_value,
284             'parameter': self._process_parameter,
285             'function_call': self._process_function_call,
286         }
287
288         return dispatch[kind](node, match, props)
289
290     def get_parameters(self, node):
291         raise NotImplementedError
292
293     def format_inline(self, node, para):
294         tokens = self._scanner.scan(para)
295         words = [self._process_token(node, tok) for tok in tokens]
296         return ''.join(words)
297
298     def format_parameter_name(self, node, parameter):
299         if isinstance(parameter.type, ast.Varargs):
300             return "..."
301         else:
302             return parameter.argname
303
304     def format_function_name(self, func):
305         raise NotImplementedError
306
307     def format_type(self, type_):
308         raise NotImplementedError
309
310     def format_page_name(self, node):
311         if isinstance(node, ast.Namespace):
312             return 'Index'
313         elif isinstance(node, ast.Function):
314             return self.format_function_name(node)
315         elif isinstance(node, ast.Property) and node.parent is not None:
316             return '%s:%s' % (self.format_page_name(node.parent), node.name)
317         elif isinstance(node, ast.Signal) and node.parent is not None:
318             return '%s::%s' % (self.format_page_name(node.parent), node.name)
319         elif isinstance(node, ast.VFunction) and node.parent is not None:
320             return '%s::%s' % (self.format_page_name(node.parent), node.name)
321         else:
322             return make_page_id(node)
323
324     def format_xref(self, node, **attrdict):
325         if node is None:
326             attrs = [('xref', 'index')] + attrdict.items()
327             return xmlwriter.build_xml_tag('link', attrs)
328         elif isinstance(node, ast.Member):
329             # Enum/BitField members are linked to the main enum page.
330             return self.format_xref(node.parent, **attrdict) + '.' + node.name
331         else:
332             attrs = [('xref', make_page_id(node))] + attrdict.items()
333             return xmlwriter.build_xml_tag('link', attrs)
334
335     def format_property_flags(self, property_, construct_only=False):
336         flags = []
337         if property_.readable and not construct_only:
338             flags.append("Read")
339         if property_.writable and not construct_only:
340             flags.append("Write")
341         if property_.construct:
342             flags.append("Construct")
343         if property_.construct_only:
344             flags.append("Construct Only")
345
346         return " / ".join(flags)
347
348     def to_underscores(self, string):
349         return to_underscores(string)
350
351     def get_class_hierarchy(self, node):
352         parent_chain = [node]
353
354         while node.parent_type:
355             node = self._transformer.lookup_typenode(node.parent_type)
356             parent_chain.append(node)
357
358         parent_chain.reverse()
359         return parent_chain
360
361 class DocFormatterC(DocFormatter):
362     language = "C"
363     mime_type = "text/x-csrc"
364
365     fundamentals = {
366         "TRUE": "TRUE",
367         "FALSE": "FALSE",
368         "NULL": "NULL",
369     }
370
371     def format_type(self, type_):
372         if isinstance(type_, ast.Array):
373             return self.format_type(type_.element_type) + '*'
374         elif type_.ctype is not None:
375             return type_.ctype
376         elif type_.target_fundamental:
377             return type_.target_fundamental
378         else:
379             node = self._transformer.lookup_typenode(type_)
380             return getattr(node, 'ctype')
381
382     def format_function_name(self, func):
383         if isinstance(func, (ast.Function)):
384             return func.symbol
385         else:
386             return func.name
387
388     def get_parameters(self, node):
389         return node.all_parameters
390
391 class DocFormatterIntrospectableBase(DocFormatter):
392     def should_render_node(self, node):
393         if isinstance(node, ast.Record) and node.is_gtype_struct_for is not None:
394             return False
395
396         if not getattr(node, "introspectable", True):
397             return False
398
399         return super(DocFormatterIntrospectableBase, self).should_render_node(node)
400
401 class DocFormatterPython(DocFormatterIntrospectableBase):
402     language = "Python"
403     mime_type = "text/python"
404
405     fundamentals = {
406         "TRUE": "True",
407         "FALSE": "False",
408         "NULL": "None",
409     }
410
411     def should_render_node(self, node):
412         if getattr(node, "is_constructor", False):
413             return False
414
415         return super(DocFormatterPython, self).should_render_node(node)
416
417     def is_method(self, node):
418         if getattr(node, "is_method", False):
419             return True
420
421         if isinstance(node, (ast.VFunction)):
422             return True
423
424         return False
425
426     def format_parameter_name(self, node, parameter):
427         # Force "self" for the first parameter of a method
428         if self.is_method(node) and parameter is node.instance_parameter:
429             return "self"
430         elif isinstance(parameter.type, ast.Varargs):
431             return "..."
432         else:
433             return parameter.argname
434
435     def format_fundamental_type(self, name):
436         fundamental_types = {
437             "utf8": "unicode",
438             "gunichar": "unicode",
439             "gchar": "str",
440             "guchar": "str",
441             "gboolean": "bool",
442             "gint": "int",
443             "guint": "int",
444             "glong": "int",
445             "gulong": "int",
446             "gint64": "int",
447             "guint64": "int",
448             "gfloat": "float",
449             "gdouble": "float",
450             "gchararray": "str",
451             "GParam": "GLib.Param",
452             "PyObject": "object",
453             "GStrv": "[str]",
454             "GVariant": "GLib.Variant",
455             }
456
457         return fundamental_types.get(name, name)
458
459     def format_type(self, type_):
460         if isinstance(type_, (ast.List, ast.Array)):
461             return '[' + self.format_type(type_.element_type) + ']'
462         elif isinstance(type_, ast.Map):
463             return '{%s: %s}' % (self.format_type(type_.key_type),
464                                  self.format_type(type_.value_type))
465         elif type_.target_giname is not None:
466             return type_.target_giname
467         else:
468             return self.format_fundamental_type(type_.target_fundamental)
469
470     def format_function_name(self, func):
471         if func.parent is not None:
472             return "%s.%s" % (self.format_page_name(func.parent), func.name)
473         else:
474             return func.name
475
476     def get_parameters(self, node):
477         return node.all_parameters
478
479 class DocFormatterGjs(DocFormatterIntrospectableBase):
480     language = "Gjs"
481     mime_type = "text/x-gjs"
482
483     fundamentals = {
484         "TRUE": "true",
485         "FALSE": "false",
486         "NULL": "null",
487     }
488
489     def is_method(self, node):
490         if getattr(node, "is_method", False):
491             return True
492
493         if isinstance(node, (ast.VFunction)):
494             return True
495
496         return False
497
498     def format_fundamental_type(self, name):
499         fundamental_types = {
500             "utf8": "String",
501             "gunichar": "String",
502             "gchar": "String",
503             "guchar": "String",
504             "gboolean": "Boolean",
505             "gint": "Number",
506             "guint": "Number",
507             "glong": "Number",
508             "gulong": "Number",
509             "gint64": "Number",
510             "guint64": "Number",
511             "gfloat": "Number",
512             "gdouble": "Number",
513             "gchararray": "String",
514             "GParam": "GLib.Param",
515             "PyObject": "Object",
516             "GStrv": "[String]",
517             "GVariant": "GLib.Variant",
518             }
519
520         return fundamental_types.get(name, name)
521
522     def format_type(self, type_):
523         if isinstance(type_, (ast.List, ast.Array)):
524             return '[' + self.format_type(type_.element_type) + ']'
525         elif isinstance(type_, ast.Map):
526             return '{%s: %s}' % (self.format_type(type_.key_type),
527                                  self.format_type(type_.value_type))
528         elif type_.target_fundamental == "none":
529             return "void"
530         elif type_.target_giname is not None:
531             return type_.target_giname
532         else:
533             return self.format_fundamental_type(type_.target_fundamental)
534
535     def format_function_name(self, func):
536         if func.is_method:
537             return "%s.prototype.%s" % (self.format_page_name(func.parent), func.name)
538         elif func.is_constructor:
539             return "%s.%s" % (self.format_page_name(func.parent), func.name)
540         else:
541             return func.name
542
543     def get_parameters(self, node):
544         skip = []
545         for param in node.parameters:
546             if param.direction == ast.PARAM_DIRECTION_OUT:
547                 skip.append(param)
548             if param.closure_name is not None:
549                 skip.append(node.get_parameter(param.closure_name))
550             if param.destroy_name is not None:
551                 skip.append(node.get_parameter(param.destroy_name))
552             if isinstance(param.type, ast.Array) and param.type.length_param_name is not None:
553                 skip.append(node.get_parameter(param.type.length_param_name))
554
555         params = []
556         for param in node.parameters:
557             if param not in skip:
558                 params.append(param)
559         return params
560
561 LANGUAGES = {
562     "c": DocFormatterC,
563     "python": DocFormatterPython,
564     "gjs": DocFormatterGjs,
565 }
566
567 class DocWriter(object):
568     def __init__(self, transformer, language):
569         self._transformer = transformer
570
571         try:
572             formatter_class = LANGUAGES[language.lower()]
573         except KeyError:
574             raise SystemExit("Unsupported language: %s" % (language, ))
575
576         self._formatter = formatter_class(self._transformer)
577         self._language = self._formatter.language
578
579         self._lookup = self._get_template_lookup()
580
581     def _get_template_lookup(self):
582         if 'UNINSTALLED_INTROSPECTION_SRCDIR' in os.environ:
583             top_srcdir = os.environ['UNINSTALLED_INTROSPECTION_SRCDIR']
584             srcdir = os.path.join(top_srcdir, 'giscanner')
585         else:
586             srcdir = os.path.dirname(__file__)
587
588         template_dir = os.path.join(srcdir, 'doctemplates')
589
590         return TemplateLookup(directories=[template_dir],
591                               module_directory=tempfile.mkdtemp(),
592                               output_encoding='utf-8')
593
594     def write(self, output):
595         try:
596             os.makedirs(output)
597         except OSError:
598             # directory already made
599             pass
600
601         self._walk_node(output, self._transformer.namespace, [])
602         self._transformer.namespace.walk(lambda node, chain: self._walk_node(output, node, chain))
603
604     def _walk_node(self, output, node, chain):
605         if isinstance(node, ast.Function) and node.moved_to is not None:
606             return False
607         if getattr(node, 'disguised', False):
608             return False
609         if self._formatter.should_render_node(node):
610             self._render_node(node, chain, output)
611             return True
612         return False
613
614     def _render_node(self, node, chain, output):
615         namespace = self._transformer.namespace
616
617         # A bit of a hack...maybe this should be an official API
618         node._chain = list(chain)
619
620         page_kind = get_node_kind(node)
621         template_name = '%s/%s.tmpl' % (self._language, page_kind)
622         page_id = make_page_id(node)
623
624         template = self._lookup.get_template(template_name)
625         result = template.render(namespace=namespace,
626                                  node=node,
627                                  page_id=page_id,
628                                  page_kind=page_kind,
629                                  formatter=self._formatter)
630
631         output_file_name = os.path.join(os.path.abspath(output),
632                                         page_id + '.page')
633         fp = open(output_file_name, 'w')
634         fp.write(result)
635         fp.close()