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