~ubuntu-branches/ubuntu/quantal/freeipa/quantal

« back to all changes in this revision

Viewing changes to makeapi

  • Committer: Package Import Robot
  • Author(s): Timo Aaltonen
  • Date: 2012-03-22 00:17:10 UTC
  • Revision ID: package-import@ubuntu.com-20120322001710-pyratwr2bfml6bin
Tags: upstream-2.1.4
ImportĀ upstreamĀ versionĀ 2.1.4

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# Authors:
 
3
#   Rob Crittenden <rcritten@redhat.com>
 
4
#   John Dennis <jdennis@redhat.com>
 
5
#
 
6
# Copyright (C) 2011  Red Hat
 
7
# see file 'COPYING' for use and warranty information
 
8
#
 
9
# This program is free software; you can redistribute it and/or modify
 
10
# it under the terms of the GNU General Public License as published by
 
11
# the Free Software Foundation, either version 3 of the License, or
 
12
# (at your option) any later version.
 
13
#
 
14
# This program is distributed in the hope that it will be useful,
 
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
17
# GNU General Public License for more details.
 
18
#
 
19
# You should have received a copy of the GNU General Public License
 
20
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
21
 
 
22
# Test the API against a known-good API to ensure that changes aren't made
 
23
# lightly.
 
24
 
 
25
import sys
 
26
import os
 
27
import re
 
28
import inspect
 
29
from ipalib import *
 
30
from ipalib.text import Gettext, NGettext
 
31
 
 
32
API_FILE='API.txt'
 
33
 
 
34
API_FILE_DIFFERENCE = 1
 
35
API_NEW_COMMAND = 2
 
36
API_NO_FILE = 4
 
37
API_DOC_ERROR = 8
 
38
 
 
39
def parse_options():
 
40
    from optparse import OptionParser
 
41
 
 
42
    parser = OptionParser()
 
43
    parser.add_option("--validate", dest="validate", action="store_true",
 
44
        default=False, help="Validate the API vs the stored API")
 
45
 
 
46
    parser.add_option("--no-validate-doc", dest="validate_doc", action="store_false",
 
47
        default=True, help="Do not validate documentation")
 
48
 
 
49
    options, args = parser.parse_args()
 
50
    return options, args
 
51
 
 
52
def strip_doc(line):
 
53
    """
 
54
    Remove the doc= part from the repr() of a Parameter.
 
55
    """
 
56
 
 
57
    # this pattern allows up to 2 nested parentheses in doc part
 
58
    newline = re.sub(r', doc=([^(,]+)(\([^()]*(\([^()]+\)[^()]*)?\))?', '', line)
 
59
 
 
60
    return newline
 
61
 
 
62
def validate_doc():
 
63
    """
 
64
    Iterate over all API commands and perform the following validation:
 
65
 
 
66
    * Every command must have documentation
 
67
      and it must be marked for international translation
 
68
 
 
69
    * Every module hosting a command must have documentation
 
70
      and it must be marked for international translation
 
71
 
 
72
    * Every module topic must be marked for international translation
 
73
 
 
74
    For every error found emit a diagnostic.
 
75
    Emit a summary of total errors found.
 
76
 
 
77
    Return error flag if errors found, zero otherwise.
 
78
    """
 
79
 
 
80
    def is_i18n(obj):
 
81
        'Helper utility to determine if object has been internationalized'
 
82
        return isinstance(obj, (Gettext, NGettext))
 
83
 
 
84
    # The return value
 
85
    rval = 0
 
86
 
 
87
    # Used to track if we've processed a module already
 
88
    modules = {}
 
89
 
 
90
    # Initialize error counters
 
91
    n_missing_cmd_doc = 0
 
92
    n_missing_cmd_i18n = 0
 
93
    n_missing_mod_doc = 0
 
94
    n_missing_mod_i18n = 0
 
95
 
 
96
    # Iterate over every command
 
97
    for cmd in api.Command():
 
98
        cmd_class = cmd.__class__
 
99
 
 
100
        # Skip commands marked as NO_CLI
 
101
        if getattr(cmd, 'NO_CLI', False):
 
102
            continue
 
103
 
 
104
        # Have we processed this module yet?
 
105
        if not modules.setdefault(cmd.module, 0):
 
106
            # First time seeing this module, validate the module contents
 
107
            mod = sys.modules[cmd.module]
 
108
 
 
109
            # See if there is a module topic, if so validate it
 
110
            topic = getattr(mod, 'topic', None)
 
111
            if topic is not None:
 
112
                if not is_i18n(topic[1]):
 
113
                    src_file = inspect.getsourcefile(cmd_class)
 
114
                    n_missing_mod_i18n += 1
 
115
                    print "%s: topic in module \"%s\" is not internationalized" % \
 
116
                          (src_file, cmd.module)
 
117
 
 
118
            # Does the module have documentation?
 
119
            if mod.__doc__ is None:
 
120
                src_file = inspect.getsourcefile(mod)
 
121
                n_missing_mod_doc += 1
 
122
                print "%s: module \"%s\" has no doc" % \
 
123
                      (src_file, cmd.module)
 
124
            # Yes the module has doc, but is it internationalized?
 
125
            elif not is_i18n(mod.__doc__):
 
126
                src_file = inspect.getsourcefile(cmd_class)
 
127
                n_missing_mod_i18n += 1
 
128
                print "%s: module \"%s\" doc is not internationalized" % \
 
129
                      (src_file, cmd.module)
 
130
 
 
131
        # Increment the count of how many commands in this module
 
132
        modules[cmd.module] = modules[cmd.module] + 1
 
133
 
 
134
        # Does the command have documentation?
 
135
        if cmd.__doc__ is None:
 
136
            src_file = inspect.getsourcefile(cmd_class)
 
137
            line_num = inspect.getsourcelines(cmd_class)[1]
 
138
            n_missing_cmd_doc += 1
 
139
            print "%s:%d command \"%s\" has no doc" % (src_file, line_num, cmd.name)
 
140
        # Yes the command has doc, but is it internationalized?
 
141
        elif not is_i18n(cmd.__doc__):
 
142
            src_file = inspect.getsourcefile(cmd_class)
 
143
            line_num = inspect.getsourcelines(cmd_class)[1]
 
144
            n_missing_cmd_i18n += 1
 
145
            print "%s:%d command \"%s\" doc is not internationalized" % (src_file, line_num, cmd.name)
 
146
 
 
147
    # If any errors, emit summary information and adjust return value
 
148
    if n_missing_cmd_doc > 0 or n_missing_cmd_i18n > 0:
 
149
        rval = API_DOC_ERROR
 
150
        print "%d commands without doc, %d commands whose doc is not i18n" % \
 
151
              (n_missing_cmd_doc, n_missing_cmd_i18n)
 
152
 
 
153
    if n_missing_mod_doc > 0 or n_missing_mod_i18n > 0:
 
154
        rval = API_DOC_ERROR
 
155
        print "%d modules without doc, %d modules whose doc is not i18n" % \
 
156
              (n_missing_mod_doc, n_missing_mod_i18n)
 
157
 
 
158
    return rval
 
159
 
 
160
def make_api():
 
161
    """
 
162
    Write a new API file from the current tree.
 
163
    """
 
164
    fd = open(API_FILE, 'w')
 
165
    for cmd in api.Command():
 
166
        fd.write('command: %s\n' % cmd.name)
 
167
        fd.write('args: %d,%d,%d\n' % (len(cmd.args), len(cmd.options), len(cmd.output)))
 
168
        for a in cmd.args():
 
169
            fd.write('arg: %s\n' % strip_doc(repr(a)))
 
170
        for o in cmd.options():
 
171
            fd.write('option: %s\n' % strip_doc(repr(o)))
 
172
        for o in cmd.output():
 
173
            fd.write('output: %s\n' % strip_doc(repr(o)))
 
174
    fd.close()
 
175
 
 
176
    return 0
 
177
 
 
178
def find_name(line):
 
179
    """
 
180
    Break apart a Param line and pull out the name. It would be nice if we
 
181
    could just eval() the line but we wouldn't have defined any validators
 
182
    or normalizers it may be using.
 
183
    """
 
184
    m = re.match('^[a-zA-Z0-9]+\(\'([a-z][_a-z0-9?\*\+]*)\'.*', line)
 
185
    if m:
 
186
        name = m.group(1)
 
187
    else:
 
188
        print "Couldn't find name in: %s" % line
 
189
        name = ''
 
190
    return name
 
191
 
 
192
def _finalize_command_validation(cmd, found_args, expected_args,
 
193
                                      found_options, expected_options,
 
194
                                      found_output, expected_output):
 
195
    passed = True
 
196
    # Check the args of the previous command.
 
197
    if len(found_args) != expected_args:
 
198
        print 'Argument count in %s of %d doesn\'t match expected: %d' % (
 
199
            cmd.name, len(found_args), expected_args)
 
200
        passed = False
 
201
    if len(found_options) != expected_options:
 
202
        print 'Options count in %s of %d doesn\'t match expected: %d' % (
 
203
            cmd.name, len(found_options), expected_options)
 
204
        passed = False
 
205
    if len(found_output) != expected_output:
 
206
        print 'Output count in %s of %d doesn\'t match expected: %d' % (
 
207
            cmd.name, len(found_output), expected_output)
 
208
        passed = False
 
209
 
 
210
    # Check if there is not a new arg/opt/output in previous command
 
211
    for a in cmd.args():
 
212
        if a.param_spec not in found_args:
 
213
            print 'Argument %s of command %s in ipalib, not in API file:\n%s' % (
 
214
                a.param_spec, cmd.name, strip_doc(repr(a)))
 
215
            passed = False
 
216
    for o in cmd.options():
 
217
        if o.param_spec not in found_options:
 
218
            print 'Option %s of command %s in ipalib, not in API file:\n%s' % (
 
219
                o.param_spec, cmd.name, strip_doc(repr(o)))
 
220
            passed = False
 
221
    for o in cmd.output():
 
222
        if o.name not in found_output:
 
223
            print 'Output %s of command %s in ipalib, not in API file:\n%s' % (
 
224
                o.name, cmd.name, strip_doc(repr(o)))
 
225
            passed = False
 
226
 
 
227
    return passed
 
228
 
 
229
def validate_api():
 
230
    """
 
231
    Compare the API in the file to the one in ipalib.
 
232
 
 
233
    Return a bitwise return code to identify the types of errors found, if
 
234
    any.
 
235
    """
 
236
    fd = open(API_FILE, 'r')
 
237
    lines = fd.readlines()
 
238
    fd.close()
 
239
 
 
240
    rval = 0
 
241
 
 
242
    expected_args = 0
 
243
    expected_options = 0
 
244
    expected_output = 0
 
245
    found_args = []
 
246
    found_options = []
 
247
    found_output = []
 
248
 
 
249
    # First run through the file and compare it to the API
 
250
    existing_cmds = []
 
251
    cmd = None
 
252
    for line in lines:
 
253
        line = line.strip()
 
254
        if line.startswith('command:'):
 
255
            if cmd:
 
256
                if not _finalize_command_validation(cmd, found_args, expected_args,
 
257
                                      found_options, expected_options,
 
258
                                      found_output, expected_output):
 
259
                    rval |= API_FILE_DIFFERENCE
 
260
 
 
261
            (arg, name) = line.split(': ', 1)
 
262
            if name not in api.Command:
 
263
                print "Command %s in API file, not in ipalib" % name
 
264
                rval |= API_FILE_DIFFERENCE
 
265
                cmd = None
 
266
            else:
 
267
                existing_cmds.append(name)
 
268
                cmd = api.Command[name]
 
269
            found_args = []
 
270
            found_options = []
 
271
            found_output = []
 
272
        if line.startswith('args:') and cmd:
 
273
            line = line.replace('args: ', '')
 
274
            (expected_args, expected_options, expected_output) = line.split(',')
 
275
            expected_args = int(expected_args)
 
276
            expected_options = int(expected_options)
 
277
            expected_output = int(expected_output)
 
278
        if line.startswith('arg:') and cmd:
 
279
            line = line.replace('arg: ', '')
 
280
            found = False
 
281
            arg = find_name(line)
 
282
            for a in cmd.args():
 
283
                if strip_doc(repr(a)) == line:
 
284
                    found = True
 
285
                else:
 
286
                    if a.name == arg:
 
287
                        found = True
 
288
                        print 'Arg in %s doesn\'t match.\nGot      %s\nExpected %s' % (
 
289
                            name, strip_doc(repr(a)), line)
 
290
                        rval |= API_FILE_DIFFERENCE
 
291
            if found:
 
292
                found_args.append(arg)
 
293
            else:
 
294
                arg = find_name(line)
 
295
                print "Argument '%s' in command '%s' in API file not found" % (arg, name)
 
296
                rval |= API_FILE_DIFFERENCE
 
297
        if line.startswith('option:') and cmd:
 
298
            line = line.replace('option: ', '')
 
299
            found = False
 
300
            option = find_name(line)
 
301
            for o in cmd.options():
 
302
                if strip_doc(repr(o)) == line:
 
303
                    found = True
 
304
                else:
 
305
                    if o.name == option:
 
306
                        found = True
 
307
                        print 'Option in %s doesn\'t match. Got %s Expected %s' % (name, o, line)
 
308
                        rval |= API_FILE_DIFFERENCE
 
309
            if found:
 
310
                found_options.append(option)
 
311
            else:
 
312
                option = find_name(line)
 
313
                print "Option '%s' in command '%s' in API file not found" % (option, name)
 
314
                rval |= API_FILE_DIFFERENCE
 
315
        if line.startswith('output:') and cmd:
 
316
            line = line.replace('output: ', '')
 
317
            found = False
 
318
            output = find_name(line)
 
319
            for o in cmd.output():
 
320
                if strip_doc(repr(o)) == line:
 
321
                    found = True
 
322
                else:
 
323
                    if o.name == output:
 
324
                        found = True
 
325
                        print 'Output in %s doesn\'t match. Got %s Expected %s' % (name, o, line)
 
326
                        rval |= API_FILE_DIFFERENCE
 
327
            if found:
 
328
                found_output.append(output)
 
329
            else:
 
330
                output = find_name(line)
 
331
                print "Option '%s' in command '%s' in API file not found" % (output, name)
 
332
                rval |= API_FILE_DIFFERENCE
 
333
 
 
334
    if cmd:
 
335
        if not _finalize_command_validation(cmd, found_args, expected_args,
 
336
                              found_options, expected_options,
 
337
                              found_output, expected_output):
 
338
            rval |= API_FILE_DIFFERENCE
 
339
 
 
340
    # Now look for new commands not in the current API
 
341
    for cmd in api.Command():
 
342
        if cmd.name not in existing_cmds:
 
343
            print "Command %s in ipalib, not in API" % cmd.name
 
344
            rval |= API_NEW_COMMAND
 
345
 
 
346
    return rval
 
347
 
 
348
def main():
 
349
    rval = 0
 
350
    options, args = parse_options()
 
351
 
 
352
    cfg = dict(
 
353
        context='cli',
 
354
        in_server=False,
 
355
        debug=False,
 
356
        verbose=0,
 
357
        validate_api=True,
 
358
        enable_ra=True,
 
359
        mode='developer',
 
360
    )
 
361
 
 
362
    api.bootstrap(**cfg)
 
363
    api.finalize()
 
364
 
 
365
    if options.validate_doc:
 
366
        rval |= validate_doc()
 
367
 
 
368
    if options.validate:
 
369
        if not os.path.exists(API_FILE):
 
370
            print 'No %s to validate' % API_FILE
 
371
            rval |= API_NO_FILE
 
372
        else:
 
373
            rval |= validate_api()
 
374
    else:
 
375
        print "Writing API to API.txt"
 
376
        rval |= make_api()
 
377
 
 
378
    if rval & API_FILE_DIFFERENCE:
 
379
        print ''
 
380
        print 'There are one or more changes to the API.\nEither undo the API changes or update API.txt and increment the major version in VERSION.'
 
381
 
 
382
    if rval & API_NEW_COMMAND:
 
383
        print ''
 
384
        print 'There are one or more new commands defined.\nUpdate API.txt and increment the minor version in VERSION.'
 
385
 
 
386
    if rval & API_DOC_ERROR:
 
387
        print ''
 
388
        print 'There are one or more documentation problems.\nYou must fix these before preceeding'
 
389
 
 
390
    return rval
 
391
 
 
392
sys.exit(main())