1#!/usr/bin/env python3
  2
  3"""This module outputs a skeleton Wiki page describing either:
  4
  5 - An activity which writes to a derived table
  6 - An event class
  7 - A view
  8
  9It only produces the framework of a wiki page with a few sections filled in, not a final page.
 10The result should be edited by hand.
 11The environment variables CHART_WEB and CHART_PREFIX affect the generated URLs.
 12"""
 13
 14
 15
 16import sys
 17import ast
 18
 19from chart.project import settings
 20from chart.common.args import ArgumentParser
 21from chart.common.util import nvl
 22from chart.events.eventclass import EventClass
 23from chart.db.model.table import TableInfo
 24from chart.alg.views import constants_in_file
 25from chart.alg.constants import get_constant_description
 26
 27# disable all pylint T-B-D warnings for the module because we print lots of strings containing
 28# the acronym
 29# pylint: disable=W0511
 30
 31
 32def italic(text):
 33    """Markup `text` as wiki italic text."""
 34    return "''{text}''".format(text=text)  # italic
 35
 36
 37def bold(text):
 38    """Mark up `text` as wiki bold text."""
 39    return "'''{text}'''".format(text=text)  # bold
 40
 41
 42def table_header(*args):
 43    """Emit a line of wiki code marking a table header.
 44    `args` should be a list of strings giving the column names."""
 45    return '||=' + '=||='.join(bold(h) for h in args) + '=||\n'
 46
 47
 48def table_row(*args):
 49    """Emit a table row."""
 50    return '||' + '||'.join(args) + '||\n'
 51
 52
 53def tobedone(text):
 54    """Markup `text` as a T-B-D."""
 55    # (we are avoiding triggering pylint errors here)
 56    return "''{t}{b}{d}: {text}''".format(t='T', b='B', d='D', text=text)
 57
 58
 59def list_formatter(items, common_delimiter=', ', final_delimiter=' and '):
 60    """Return a string giving `items` separated by `common_delimiter.
 61    The final item uses `final_delimiter`.
 62
 63    >>> list_formatter(['apple', 'banana', 'orange'])
 64    'apple, banana and orange'
 65    """
 66    items = list(items)
 67    result = ''
 68    for i, item in enumerate(items):
 69        if i == 0:
 70            result += item
 71
 72        elif i != (len(items) - 1):
 73            result += common_delimiter + item
 74
 75        else:
 76            result += final_delimiter + item
 77
 78    return result
 79
 80
 81def sentance(text):
 82    """Ensure `text` ends in a full stop.
 83    """
 84
 85    if text.endswith('.'):
 86        return text
 87
 88    else:
 89        return text + '.'
 90
 91
 92def wiki_tabledef(tableinfo, target=sys.stdout):
 93    """Describe `table` in wiki format.
 94    """
 95
 96    if tableinfo.type == 'view':
 97        typ = 'View'
 98
 99    else:
100        typ = 'Table'
101
102    has_units = False
103    for f in tableinfo.fields.values():
104        if f.unit is not None and len(f.unit) > 0:
105            has_units = True
106
107    target.write("{typ} [{link} {name}]\n\n||='''Field'''=||={unit}'''Type'''"
108                 "=||='''Description'''=||\n".format(
109            typ=typ,
110            link=tableinfo.browse_url,
111            name=tableinfo.name,
112            unit="'''Unit'''=||=" if has_units else ''))
113
114    for f in tableinfo.fields.values():
115        # note the trac wiki engine will treat '||||' as a cell joined to the next cell to the
116        # right, so we use '|| ||' for an empty cell.
117        target.write('||{name}||{unit}{type}||{description}||\n'.format(
118                name=f.name,
119                unit=nvl(f.unit, ' ') + '||' if has_units else '',
120                type=f.datatype,
121                description=nvl(f.description)))
122
123    target.write('\n')
124
125
126def wiki_title(thing, target=sys.stdout):
127    """Write a title section"""
128    if isinstance(thing, EventClass):
129        target.write('= {name} class =\n\n'.format(name=thing.name))
130
131    else:
132        target.write('= {name} =\n\n'.format(name=thing.name))
133
134
135def wiki_introduction_activity(activity, target=sys.stdout):
136    """Write an introduction section using the <description> of the activity"""
137
138    # heading
139    target.write('== Introduction ==\n\n')
140
141    if len(activity.eventnames) > 0:
142        target.write("""Warning: Since this Activity can generate Events, you probably wanted to
143run the wiki skeleton tool with the "--event xxx" option instead.\n\n""")
144
145    # simple summary of the nature of this page and the purpose of the thing it describes
146    if len(activity.output_tables) > 0:
147        target.write('This page describes the activity {act} which writes to the table{plural} '
148                     '{outputs}.\n\n'.format(
149                plural='s' if len(activity.output_tables) != 1 else '',
150                act=activity.name,
151                outputs=list_formatter(o.name for o in activity.output_tables)))
152
153    # now try to find more information from the activity.xml ...
154    if activity.description is not None:
155        target.write('{description}\n\n'.format(description=activity.description))
156
157    # ... and code docstring
158    docstring = activity.docstring
159    if docstring is not None and len(docstring) > 0:
160        target.write('{docstring}\n\n'.format(docstring=docstring))
161
162    # remind the author to check the results
163    target.write(tobedone(
164            'This section is autogenerated from the activity <description> and the code '
165            'docstring. It should be checked and edited.') + '\n\n')
166
167
168def wiki_introduction_view(viewinfo, target=sys.stdout):
169    """Write an introduction section using the <description> of the activity"""
170
171    # heading
172    target.write('== Introduction ==\n\n')
173
174    # simple summary of the nature of this page and the purpose of the thing it describes
175    target.write('This page describes the view {view}. A view is a database object which '
176                 'contains computed values derived from other tables or views.'.format(
177            view=viewinfo.name))
178
179    target.write('{description}\n\n'.format(description=sentance(viewinfo.description)))
180
181    # remind the author to check the results
182    # disable pylint T-B-D warning
183    target.write("''TBD: This section is autogenerated from the view "
184                 "<description>. It should be checked and edited.''\n\n")
185
186
187def wiki_introduction_eventclass(eventclass, target=sys.stdout):
188    """Write an introduction section using the <description> of the activity"""
189
190    # heading
191    target.write('== Introduction ==\n\n')
192
193    # remind the author to check the results
194    # disable pylint T-B-D warning
195    target.write(tobedone('This section is autogenerated from the event class description and '
196                          'the activity <description>. It should be checked and edited') + '\n\n')
197
198    if eventclass.description is not None and len(eventclass.description) > 0:
199        target.write(sentance(eventclass.description) + '\n\n')
200
201    else:
202        target.write(tobedone('Add a <description> element to the event class description') +
203                     '\n\n')
204
205    activities = list(eventclass.raised_by())
206    if len(activities) == 1:
207        target.write('This event is raised by the activity {act}.\n\n'.format(
208                act=activities[0].name))
209
210    elif len(activities) > 1:
211        target.write('This event is raised by the activities: {acts}.\n\n'.format(
212                acts=', '.join(a.name for a in activities)))
213
214    else:
215        target.write('This event is not raised by any activities.\n\n')
216
217    for a in activities:
218        if a.description is not None and len(a.description) > 0:
219            target.write(' {act}::\n  {desc}\n\n'.format(
220                    act=a.name, desc=sentance(a.description)))
221
222
223def wiki_definition_view(viewinfo, target=sys.stdout):
224    """Show a table definition for a view."""
225    target.write('== Definition ==\n\n')
226
227    wiki_tabledef(viewinfo, target)
228
229
230def wiki_definition_eventclass(eventclass, target=sys.stdout):
231    """List the instance properties of an event class."""
232    target.write('== Event class definition ==\n\n')
233
234    non_comments = len(list(e for e in eventclass.instance_properties.keys() if e != 'comment'))
235
236    if non_comments == 0:
237        target.write('This event class only uses the standard properties (start time etc.).\n\n')
238
239    else:
240        target.write(
241            "In addition to the standard properties (start time etc.) this event class "
242            "has the following instance properties defined:\n\n||='''Name'''=||='''Unit'''=||"
243            "='''Datatype'''=||='''Optional'''=||='''Choices'''=||='''Description'''=||\n")
244
245        for i in eventclass.instance_properties.values():
246            if i['name'] == 'comment':
247                continue
248
249            if 'choices' not in i:
250                choices = ' '
251
252            else:
253                choices = str(i['choices'])
254
255            target.write('||{name}||{unit}||{type}||{optional}||{choices}||{desc}||\n'.format(
256                    name=i['name'],
257                    unit=i.get('unit'),
258                    type=i['type'],
259                    optional=nvl(i.get('optional', ' ')),
260                    choices=choices,
261                    desc=nvl(i.get('description'), ' ')))
262
263        target.write('\n')
264
265
266def wiki_inputs_activity(activity, target=sys.stdout):
267    """Write Inputs section by scanning the source."""
268    target.write('== Inputs ==\n\n')
269
270    target.write('Inputs from timeseries tables:\n\n')
271
272    # for t in activity.triggers:
273        # target.write(' * Table [{link} {table}]\n   * Field SENSING_TIME\n'.format(
274                # table=t['table'].name,
275                # link=t['table'].browse_url))
276
277    for t in ts_in_file(activity.abs_executable):
278        # target.write(str(t))
279        target.write(' * Table [{link} {table}]\n'.format(
280                table=t['table'].name,
281                link=t['table'].browse_url))
282        for f in t['fields']:
283            target.write('   * {f}\n'.format(f=f))
284
285    target.write('\n')
286
287
288def wiki_inputs_view(viewinfo, target=sys.stdout):
289    """Write Inputs section using the view inputs elements."""
290    target.write('== Inputs ==\n\n')
291
292    target.write('The following tables are inputs to this view:\n\n')
293
294    for s in viewinfo.source_tables:
295        target.write(" * Table [{link} {name}]\n   * Field SENSING_TIME\n    ''TBD: add any other "
296                     "input fields here''\n".format(
297                link=s.browse_url,
298                name=s.name))
299
300    target.write('\n')
301
302
303def wiki_inputs_eventclass(eventclass, target=sys.stdout):
304    """Show a sample Inputs section."""
305    target.write('== Inputs ==\n\n')
306
307    for a in eventclass.raised_by():
308        target.write('This event is raised by the activity {act} which uses as inputs:\n\n'.format(
309                act=a.name))
310
311        # for t in a.triggers:
312            # target.write(' * Table [{link} {table}]\n   * Field SENSING_TIME\n'.format(
313                    # table=t['table'].name,
314                    # link=t['table'].browse_url))
315
316        for t in ts_in_file(a.abs_executable):
317            target.write(' * Table [{link} {table}]\n'.format(
318                    table=t['table'].name,
319                    link=t['table'].browse_url))
320            for f in t['fields']:
321                target.write('   * {f}\n'.format(f=f))
322
323        target.write('\n')
324
325    target.write("''TBD: This list may input inputs which are not "  # pylint: disable=W0611
326                 "relevant to this event and should be edited by hand.''\n\n")
327
328
329def wiki_outputs(activity, target=sys.stdout):
330    """Write outputs section (either derived table, view desc or event class desc)."""
331    target.write('== Outputs ==\n\n')
332
333    for t in activity.output_tables:
334        wiki_tabledef(t)
335
336
337def wiki_constants(activity, target=sys.stdout):  # (unused arg) pylint: disable=W0613
338    """Write a Constants section.
339    This should list all values used from constants.py.
340    Auto-scan is possible but not implemented.
341    """
342    target.write('== Constants ==\n\n')
343    target.write('The following constants are read from the constants file:\n\n')
344
345    for constant in constants_in_file(activity.abs_executable):
346        desc = get_constant_description(constant)
347        if desc is None:
348            target.write(' * {name}\n'.format(name=constant))
349
350        else:
351            target.write(' * {name} ({desc})\n'.format(name=constant,
352                                                       desc=desc))
353
354    target.write('\n')
355
356
357def wiki_method_activity(activity, target=sys.stdout):
358    """This section must be hand written.
359    To help the author we include a blank template section listing fields and properties."""
360    target.write('== Method ==\n\n')
361
362    # switch off pylint T-B-D warning
363    target.write(tobedone('This section should be rewritten to describe the algorithm processing') +
364                 '\n\n')
365
366    for t in activity.output_tables:
367        target.write(tobedone('Describe how each field in {name} is set').format(name=t.name) +
368                     '\n\n')
369
370        target.write(table_header('Field', 'Algorithm'))
371
372        for f in t.fields.values():
373            target.write(table_row(f.name, ' '))
374
375        target.write('\n')
376
377
378def wiki_method_view(viewinfo, target=sys.stdout):
379    """Write a method section."""
380    target.write('== Method ==\n\n')
381    target.write(tobedone(
382            'In this section describe the overall algorithm used to compute the '
383            'view members, and fill in below the specific method for each field.') + '\n\n')
384    target.write(table_header('Field', 'Algorithm'))
385    for f in viewinfo.fields.values():
386        target.write(table_row(f.name, ' '))
387
388    target.write('\n')
389
390
391def wiki_method_eventclass(eventclass, target=sys.stdout):
392    """Add a fairly empty Method section for an event page."""
393    target.write('== Method ==\n\n')
394
395    # switch off pylint T-B-D warning
396    target.write("''TBD: This section should be rewritten to describe the "
397                 "algorithm processing.")
398
399    if len(eventclass.instance_properties) > 0:
400        target.write(" Include a description of how each property is set.''\n\n")
401
402        target.write(table_header('Name', 'Method'))
403
404        for i in eventclass.instance_properties.values():
405            if i['name'] == 'comment':
406                continue
407
408            target.write('||{name}|| ||\n'.format(
409                    name=i['name']))
410
411        target.write('\n')
412
413    else:
414        target.write("''\n\n")
415
416
417def wiki_implementation_activity(activity, target=sys.stdout):
418    """Write an implementation section."""
419    target.write('== Implementation ==\n\n')
420
421    activity_path = settings.ACTIVITY_DIR.relative_to(settings.PROJ_HOME_DIR)
422
423    target.write('This algorithm is implemented in the file [{exe}] '
424                 'and configured in the activity [{act}].'.format(
425                     exe=activity.browse_executable_url,
426                     act=activity.browse_source_url))
427
428    if len(activity.output_tables) > 0:
429        target.write('The output table{plural2} {plural1} defined in the file{plural2} '
430                     '{files}.'.format(
431                plural1='are' if len(activity.output_tables) != 1 else 'is',
432                plural2='s:' if len(activity.output_tables) != 1 else '',
433                files=', '.join(t.browse_source_url for t in activity.output_tables)))
434
435    target.write('\n\n')
436
437
438def wiki_implementation_view(viewinfo, target=sys.stdout):
439    """Write an implementation section."""
440    target.write('== Implementation ==\n\n')
441
442    ts_path = settings.TS_TABLE_DIR.relative_to(settings.PROJ_HOME_DIR)
443    target.write('This view is defined and implemented in the file '
444                 '[{base}/{rel}/{name}.xml].\n\n'.format(
445            base=settings.PROJ_WIKI_BROWSE_URL,
446            rel=ts_path,
447            name=viewinfo.name))
448
449
450def wiki_implementation_eventclass(eventclass, target=sys.stdout):
451    """Write an implementation section."""
452    target.write('== Implementation ==\n\n')
453
454    for a in eventclass.raise_by():
455        activity_path = settings.ACTIVITY_DIR.relative_to(settings.PROJ_HOME_DIR)
456        target.write("This algorithm is implemented by the function ''TBD'' in the file "
457                     "[{base}/{exe}] and configured in the activity "
458                     "[{base}/{rel}/{act}].".format(
459                base=settings.PROJ_WIKI_BROWSE_URL,
460                exe=a.executable,
461                rel=activity_path,
462                act=a.name + '.xml'))
463
464        if len(a.output_tables) > 0:
465            ts_path = settings.TS_TABLE_DIR.relative_to(settings.PROJ_HOME_DIR)
466            target.write(' The output tables {plural1} defined in the file{plural2} '
467                         '{files}.'.format(
468                    plural1='are' if len(a.output_tables) != 1 else 'is',
469                    plural2='s:' if len(a.output_tables) != 1 else '',
470                    files=', '.join('[{base}/{rel}/{name}.xml]'.format(
471                            base=settings.PROJ_WIKI_BROWSE_URL,
472                            rel=ts_path,
473                            name=t.name) for t in a.output_tables)))
474
475        target.write('\n\n')
476
477
478def wiki_triggering(activity, target=sys.stdout):
479    """Should be created fully automatically."""
480    target.write('== Triggering ==\n\n')
481
482    # test for presence of any orbital triggers
483    orbital_triggers = []
484    for t in activity.triggers:
485        if t['type'] == 'orbital':
486            orbital_triggers.append(t)
487
488    # have we automatically detected any triggers?
489    auto_trigger = False
490    if len(orbital_triggers) > 0:
491        target.write('This algorithm is triggered on a per-orbit basis by changes to the following '
492                     'tables: {triggers}.'.format(
493                triggers=', '.join(t['table'].name for t in orbital_triggers)))
494        auto_trigger = True
495
496    if not auto_trigger:
497        target.write(tobedone('Describe how the algorithm is triggered.') + '\n\n')
498
499    target.write('\n\n')
500
501
502def wiki_testing(activity=None, target=sys.stdout):  # pylint: disable=W0613
503    """Could possibly auto generate this by scanning the tests directory
504    or by adding meta data to the activity file."""
505    target.write('== Testing ==\n\n')
506
507    target.write(tobedone('describe any unit tests here.') + '\n\n')
508
509
510def wiki_testing_eventclass(eventclass, target=sys.stdout):
511    """Generate a wiki section describing tests written for `eventclass`."""
512    target.write('== Testing ==\n\n')
513
514    if eventclass.test is None:
515        target.write(tobedone('write an automated test and describe it here.'))
516
517    else:
518        target.write('This event is covered by the automated test [{base}/{test}].'.format(
519                base=settings.PROJ_WIKI_BROWSE_URL,
520                test=eventclass.test))
521
522    target.write('\n\n')
523
524
525def wiki_gallery(target=sys.stdout):
526    """Gallery section."""
527    target.write('== Gallery ==\n\n')
528
529    target.write(tobedone('Link to one or more sample plots showing the algorithm results.') +
530                 '\n\n')
531
532
533def wiki_tickets(target=sys.stdout):
534    """Maybe possibly could set this automatically be scanning the tickets."""
535    target.write('== Tickets ==\n\n')
536
537    target.write(tobedone('List any related tickets.') + '\n\n')
538
539
540def wiki_page_activity(activity, target=sys.stdout):
541    """Given an `activity` object, output a skeleton Wiki page describing that activity.
542    URLs are adjusted according to the current environment so setting i.e. CHART_URL
543    may help.
544
545    Different sections are output depending on if this page describes:
546
547    - An algorithm which writes to a derived table
548    - An algorithm which creates events
549    - A view
550    """
551    wiki_title(activity, target)
552    wiki_introduction_activity(activity=activity, target=target)
553    wiki_inputs_activity(activity, target)
554    wiki_outputs(activity, target)
555    wiki_constants(activity, target)
556    wiki_method_activity(activity, target)
557    wiki_implementation_activity(activity, target)
558    wiki_triggering(activity, target)
559    wiki_testing(target=target)
560    wiki_gallery(target)
561    wiki_tickets(target)
562
563
564def wiki_page_view(viewinfo, target=sys.stdout):
565    """Generate a skeleton wiki page describing a database view."""
566    wiki_title(viewinfo, target)
567    wiki_introduction_view(viewinfo=viewinfo, target=target)
568    wiki_definition_view(viewinfo, target)
569    wiki_inputs_view(viewinfo, target)
570    wiki_method_view(viewinfo, target)
571    wiki_implementation_view(viewinfo, target)
572    wiki_testing(target)
573    wiki_gallery(target)
574    wiki_tickets(target)
575
576
577def wiki_page_eventclass(eventclass, target=sys.stdout):
578    """Generate a skeleton wiki page describing an event class."""
579    wiki_title(eventclass, target)
580    wiki_introduction_eventclass(eventclass, target)
581    wiki_definition_eventclass(eventclass, target)
582    wiki_inputs_eventclass(eventclass, target)
583    wiki_method_eventclass(eventclass, target)
584    wiki_implementation_eventclass(eventclass, target)
585    wiki_testing_eventclass(eventclass, target)
586    wiki_gallery(target)
587    wiki_tickets(target)
588
589
590def ts_in_file(filename):
591    """
592    Yield a series of dictionaries describing calls to db.ts.select.
593    Each contains keys:
594    `tablename` (str):
595    `fields` (list of str)
596    """
597    for fn in functions_in_file(filename):
598        if fn['name'] == 'db.ts.select':
599            ts = {}
600            if 'table' in fn['kwargs']:
601                ts['table'] = fn['kwargs']['table']
602
603            elif len(fn['args']) >= 1:
604                ts['table'] = fn['args'][0]
605
606            ts['table'] = TableInfo(ts['table'])
607
608            if 'fields' in fn['kwargs']:
609                ts['fields'] = fn['kwargs']['fields']
610
611            elif len(fn['args']) >= 2:
612                ts['fields'] = fn['args'][1]
613
614            # clients can pass in fields as a single string
615            if isinstance(ts['fields'], str):
616                ts['fields'] = [ts['fields']]
617
618            yield ts
619
620
621def strings(elem):
622    """Assuming `elem` is a list representing element, return the strings it contains.
623    Not very accurate, this is not recursive and can omit loads of things.
624    """
625    res = []
626    for elt in elem.elts:
627        if isinstance(elt, ast.Str):
628            res.append(elt.s)
629
630    return res
631
632
633def functions_in_file(filename):
634    """Given a Python file name yield a series of dictionaries containing keys:
635    `name` (str): Qualified function name
636    `args` (list of str): Numeric function arguments
637    `kwargs` (str vs list of str): Keyword function arguments.
638
639    This probably only works where all the arguments are fixed strings.
640    """
641    for fn, elem in function_elem_in_file(filename):
642        # print('fn ' + str(fn))
643        res = {'name': fn,
644               'args': [],
645               'kwargs': {}}
646        for kw in elem.keywords:
647            # walk through the keyword args to the function
648            if isinstance(kw.value, ast.Str):
649                # plain string
650                res['kwargs'][kw.arg] = kw.value.s
651
652            elif isinstance(kw.value, (ast.Tuple, ast.List)):
653                # a list of things
654                tup = []
655                for elt in kw.value.elts:
656                    # print(type(elt))
657                    if isinstance(elt, ast.Dict):
658                        # if the thing is complicated we give up
659                        pass
660
661                    elif isinstance(elt, ast.Str):
662                        # if the thing is simple, assume its a string and return it
663                        tup.append(elt.s)
664
665                    else:
666                        pass
667
668                res['kwargs'][kw.arg] = tup
669
670        for a in elem.args:
671            # walk through the positional arguments
672            # print('arg ' + str(a))
673            if isinstance(a, ast.Str):
674                # plain string
675                res['args'].append(a.s)
676
677            elif isinstance(a, (ast.Tuple, ast.List)):
678                # list/tuple
679                tup = []
680                for elt in a.elts:
681                    if isinstance(elt, ast.Str):
682                        tup.append(elt.s)
683
684                res['args'].append(tup)
685
686            elif isinstance(a, ast.BinOp):
687                # probably something clever like computing a list of fields
688                if isinstance(a.right, ast.List):
689                    res['args'].append(strings(a.right))
690
691            # else:
692                # print(type(a))
693
694        # if fn == 'db.ts.select':
695            # a = 1 / 0
696
697        yield res
698
699
700def function_elem_in_file(filename):
701    """Yield tuples of (name, elem) for function calls in file.
702    Name is fully qualified dot delimited function name e.g. "db.ts.select".
703      It could be a single name "fn()" or a module function "db.fn()" or a submodule function.
704    Elem is the ast elem object.
705    """
706    tree = ast.parse(filename.open('r').read())
707    for elem in ast.walk(tree):
708        # print(elem)
709        if isinstance(elem, ast.Call):
710            if isinstance(elem.func, ast.Name):
711                # free function
712                # print('free function ' + elem.func.id)
713                yield elem.func.id, elem
714
715            elif isinstance(elem.func, ast.Attribute):
716                # function in module
717                if isinstance(elem.func.value, ast.Name):
718                    # print('module function ' + elem.func.attr + '.' + elem.func.value.id)
719                    yield '{mod}.{fn}'.format(mod=elem.func.value.id, fn=elem.func.attr), elem
720
721                elif isinstance(elem.func.value, ast.Attribute):
722                    # function in module in module
723                    if isinstance(elem.func.value.value, ast.Name):
724                        # print('nested module function ' + elem.func.value.value.id + ' . ' + \
725                            # elem.func.value.attr + ' .' + elem.func.attr)
726                        yield '{mod}.{submod}.{fn}'.format(mod=elem.func.value.value.id,
727                                                           submod=elem.func.value.attr,
728                                                           fn=elem.func.attr), elem
729
730                    else:
731                        # function in module in module in module
732                        pass
733
734            # else:
735                # pass
736
737
738def main():
739    """Command line entry point."""
740
741    parser = ArgumentParser(__doc__)
742    parser.add_argument('--view',
743                        type=ArgumentParser.table,
744                        help='XML view description')
745    parser.add_argument('--activity', '-a',
746                        type=ArgumentParser.activity,
747                        help='Activity XML file name')
748    parser.add_argument('--event', '--eventclass', '-e',
749                        type=ArgumentParser.eventclass,
750                        help='Name of event class to document')
751    parser.add_argument('--ts',
752                        metavar='FILE',
753                        help='Show db.ts.select() calls in FILE')
754    parser.add_argument('--tabledef-fragment',
755                        metavar='TABLE',
756                        help='Output only a wiki fragment describing TABLE')
757    parser.add_argument('--prefix',
758                        help='Override PREFIX setting',
759                        default=settings.PREFIX)
760    args = parser.parse_args()
761
762    if args.prefix:
763        settings.PREFIX = args.prefix
764
765    if args.tabledef_fragment:
766        print(wiki_tabledef(TableInfo(args.tabledef_fragment)))
767        parser.exit()
768
769    if args.ts:
770        for row in ts_in_file(args.ts):
771            print(row)
772
773        parser.exit()
774
775    if sum((args.view is not None, args.activity is not None, args.event is not None)) != 1:
776        parser.error('Select one of --activity, --event, --view')
777
778    if args.activity:
779        wiki_page_activity(args.activity)
780        parser.exit()
781
782    elif args.view:
783        wiki_page_view(args.view)
784        parser.exit()
785
786    elif args.event:
787        # print(str(args.event) + ' ' + str(type(args.event)))
788        wiki_page_eventclass(args.event)
789
790    else:
791        parser.error('No actions specified')
792
793if __name__ == '__main__':
794    main()