OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 """ | 2 """ |
3 KVM test configuration file parser | 3 KVM test configuration file parser |
4 | 4 |
5 @copyright: Red Hat 2008-2011 | 5 @copyright: Red Hat 2008-2011 |
6 """ | 6 """ |
7 | 7 |
8 import re, os, sys, optparse, collections, string | 8 import re, os, sys, optparse, collections |
9 | 9 |
10 | 10 |
11 # Filter syntax: | 11 # Filter syntax: |
12 # , means OR | 12 # , means OR |
13 # .. means AND | 13 # .. means AND |
14 # . means IMMEDIATELY-FOLLOWED-BY | 14 # . means IMMEDIATELY-FOLLOWED-BY |
15 | 15 |
16 # Example: | 16 # Example: |
17 # qcow2..Fedora.14, RHEL.6..raw..boot, smp2..qcow2..migrate..ide | 17 # qcow2..Fedora.14, RHEL.6..raw..boot, smp2..qcow2..migrate..ide |
18 # means match all dicts whose names have: | 18 # means match all dicts whose names have: |
(...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
130 return False | 130 return False |
131 | 131 |
132 | 132 |
133 class NoOnlyFilter(Filter): | 133 class NoOnlyFilter(Filter): |
134 def __init__(self, line): | 134 def __init__(self, line): |
135 Filter.__init__(self, line.split(None, 1)[1]) | 135 Filter.__init__(self, line.split(None, 1)[1]) |
136 self.line = line | 136 self.line = line |
137 | 137 |
138 | 138 |
139 class OnlyFilter(NoOnlyFilter): | 139 class OnlyFilter(NoOnlyFilter): |
| 140 def is_irrelevant(self, ctx, ctx_set, descendant_labels): |
| 141 return self.match(ctx, ctx_set) |
| 142 |
| 143 |
| 144 def requires_action(self, ctx, ctx_set, descendant_labels): |
| 145 return not self.might_match(ctx, ctx_set, descendant_labels) |
| 146 |
| 147 |
140 def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set, | 148 def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set, |
141 descendant_labels): | 149 descendant_labels): |
142 for word in self.filter: | 150 for word in self.filter: |
143 for block in word: | 151 for block in word: |
144 if (_match_adjacent(block, ctx, ctx_set) > | 152 if (_match_adjacent(block, ctx, ctx_set) > |
145 _match_adjacent(block, failed_ctx, failed_ctx_set)): | 153 _match_adjacent(block, failed_ctx, failed_ctx_set)): |
146 return self.might_match(ctx, ctx_set, descendant_labels) | 154 return self.might_match(ctx, ctx_set, descendant_labels) |
147 return False | 155 return False |
148 | 156 |
149 | 157 |
150 class NoFilter(NoOnlyFilter): | 158 class NoFilter(NoOnlyFilter): |
| 159 def is_irrelevant(self, ctx, ctx_set, descendant_labels): |
| 160 return not self.might_match(ctx, ctx_set, descendant_labels) |
| 161 |
| 162 |
| 163 def requires_action(self, ctx, ctx_set, descendant_labels): |
| 164 return self.match(ctx, ctx_set) |
| 165 |
| 166 |
151 def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set, | 167 def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set, |
152 descendant_labels): | 168 descendant_labels): |
153 for word in self.filter: | 169 for word in self.filter: |
154 for block in word: | 170 for block in word: |
155 if (_match_adjacent(block, ctx, ctx_set) < | 171 if (_match_adjacent(block, ctx, ctx_set) < |
156 _match_adjacent(block, failed_ctx, failed_ctx_set)): | 172 _match_adjacent(block, failed_ctx, failed_ctx_set)): |
157 return not self.match(ctx, ctx_set) | 173 return not self.match(ctx, ctx_set) |
158 return False | 174 return False |
159 | 175 |
160 | 176 |
161 class Condition(NoFilter): | 177 class Condition(NoFilter): |
162 def __init__(self, line): | 178 def __init__(self, line): |
163 Filter.__init__(self, line.rstrip(":")) | 179 Filter.__init__(self, line.rstrip(":")) |
164 self.line = line | 180 self.line = line |
165 self.content = [] | 181 self.content = [] |
166 | 182 |
167 | 183 |
| 184 class NegativeCondition(OnlyFilter): |
| 185 def __init__(self, line): |
| 186 Filter.__init__(self, line.lstrip("!").rstrip(":")) |
| 187 self.line = line |
| 188 self.content = [] |
| 189 |
| 190 |
168 class Parser(object): | 191 class Parser(object): |
169 """ | 192 """ |
170 Parse an input file or string that follows the KVM Test Config File format | 193 Parse an input file or string that follows the KVM Test Config File format |
171 and generate a list of dicts that will be later used as configuration | 194 and generate a list of dicts that will be later used as configuration |
172 parameters by the KVM tests. | 195 parameters by the KVM tests. |
173 | 196 |
174 @see: http://www.linux-kvm.org/page/KVM-Autotest/Test_Config_File | 197 @see: http://www.linux-kvm.org/page/KVM-Autotest/Test_Config_File |
175 """ | 198 """ |
176 | 199 |
177 def __init__(self, filename=None, debug=False): | 200 def __init__(self, filename=None, debug=False): |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
219 # new_content and unpack conditional blocks if appropriate. | 242 # new_content and unpack conditional blocks if appropriate. |
220 # For example, if an 'only' statement fully matches ctx, it | 243 # For example, if an 'only' statement fully matches ctx, it |
221 # becomes irrelevant and is not appended to new_content. | 244 # becomes irrelevant and is not appended to new_content. |
222 # If a conditional block fully matches, its contents are | 245 # If a conditional block fully matches, its contents are |
223 # unpacked into new_content. | 246 # unpacked into new_content. |
224 # 3. Move failed filters into failed_filters, so that next time we | 247 # 3. Move failed filters into failed_filters, so that next time we |
225 # reach this node or one of its ancestors, we'll check those | 248 # reach this node or one of its ancestors, we'll check those |
226 # filters first. | 249 # filters first. |
227 for t in content: | 250 for t in content: |
228 filename, linenum, obj = t | 251 filename, linenum, obj = t |
229 if type(obj) is str: | 252 if type(obj) is Op: |
230 new_content.append(t) | 253 new_content.append(t) |
231 continue | 254 continue |
232 elif type(obj) is OnlyFilter: | 255 # obj is an OnlyFilter/NoFilter/Condition/NegativeCondition |
233 if not obj.might_match(ctx, ctx_set, labels): | 256 if obj.requires_action(ctx, ctx_set, labels): |
| 257 # This filter requires action now |
| 258 if type(obj) is OnlyFilter or type(obj) is NoFilter: |
234 self._debug(" filter did not pass: %r (%s:%s)", | 259 self._debug(" filter did not pass: %r (%s:%s)", |
235 obj.line, filename, linenum) | 260 obj.line, filename, linenum) |
236 failed_filters.append(t) | 261 failed_filters.append(t) |
237 return False | 262 return False |
238 elif obj.match(ctx, ctx_set): | 263 else: |
239 continue | |
240 elif type(obj) is NoFilter: | |
241 if obj.match(ctx, ctx_set): | |
242 self._debug(" filter did not pass: %r (%s:%s)", | |
243 obj.line, filename, linenum) | |
244 failed_filters.append(t) | |
245 return False | |
246 elif not obj.might_match(ctx, ctx_set, labels): | |
247 continue | |
248 elif type(obj) is Condition: | |
249 if obj.match(ctx, ctx_set): | |
250 self._debug(" conditional block matches: %r (%s:%s)", | 264 self._debug(" conditional block matches: %r (%s:%s)", |
251 obj.line, filename, linenum) | 265 obj.line, filename, linenum) |
252 # Check and unpack the content inside this Condition | 266 # Check and unpack the content inside this Condition |
253 # object (note: the failed filters should go into | 267 # object (note: the failed filters should go into |
254 # new_internal_filters because we don't expect them to | 268 # new_internal_filters because we don't expect them to |
255 # come from outside this node, even if the Condition | 269 # come from outside this node, even if the Condition |
256 # itself was external) | 270 # itself was external) |
257 if not process_content(obj.content, | 271 if not process_content(obj.content, |
258 new_internal_filters): | 272 new_internal_filters): |
259 failed_filters.append(t) | 273 failed_filters.append(t) |
260 return False | 274 return False |
261 continue | 275 continue |
262 elif not obj.might_match(ctx, ctx_set, labels): | 276 elif obj.is_irrelevant(ctx, ctx_set, labels): |
263 continue | 277 # This filter is no longer relevant and can be removed |
264 new_content.append(t) | 278 continue |
| 279 else: |
| 280 # Keep the filter and check it again later |
| 281 new_content.append(t) |
265 return True | 282 return True |
266 | 283 |
267 def might_pass(failed_ctx, | 284 def might_pass(failed_ctx, |
268 failed_ctx_set, | 285 failed_ctx_set, |
269 failed_external_filters, | 286 failed_external_filters, |
270 failed_internal_filters): | 287 failed_internal_filters): |
271 for t in failed_external_filters: | 288 for t in failed_external_filters: |
272 if t not in content: | 289 if t not in content: |
273 return True | 290 return True |
274 filename, linenum, filter = t | 291 filename, linenum, filter = t |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
323 count = 0 | 340 count = 0 |
324 for n in node.children: | 341 for n in node.children: |
325 for d in self.get_dicts(n, ctx, new_content, shortname, dep): | 342 for d in self.get_dicts(n, ctx, new_content, shortname, dep): |
326 count += 1 | 343 count += 1 |
327 yield d | 344 yield d |
328 # Reached leaf? | 345 # Reached leaf? |
329 if not node.children: | 346 if not node.children: |
330 self._debug(" reached leaf, returning it") | 347 self._debug(" reached leaf, returning it") |
331 d = {"name": name, "dep": dep, "shortname": ".".join(shortname)} | 348 d = {"name": name, "dep": dep, "shortname": ".".join(shortname)} |
332 for filename, linenum, op in new_content: | 349 for filename, linenum, op in new_content: |
333 op.apply_to_dict(d, ctx, ctx_set) | 350 op.apply_to_dict(d) |
334 yield d | 351 yield d |
335 # If this node did not produce any dicts, remember the failed filters | 352 # If this node did not produce any dicts, remember the failed filters |
336 # of its descendants | 353 # of its descendants |
337 elif not count: | 354 elif not count: |
338 new_external_filters = [] | 355 new_external_filters = [] |
339 new_internal_filters = [] | 356 new_internal_filters = [] |
340 for n in node.children: | 357 for n in node.children: |
341 (failed_ctx, | 358 (failed_ctx, |
342 failed_ctx_set, | 359 failed_ctx_set, |
343 failed_external_filters, | 360 failed_external_filters, |
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
422 while True: | 439 while True: |
423 line, indent, linenum = cr.get_next_line(prev_indent) | 440 line, indent, linenum = cr.get_next_line(prev_indent) |
424 if not line: | 441 if not line: |
425 break | 442 break |
426 | 443 |
427 words = line.split(None, 1) | 444 words = line.split(None, 1) |
428 | 445 |
429 # Parse 'variants' | 446 # Parse 'variants' |
430 if line == "variants:": | 447 if line == "variants:": |
431 # 'variants' is not allowed inside a conditional block | 448 # 'variants' is not allowed inside a conditional block |
432 if isinstance(node, Condition): | 449 if (isinstance(node, Condition) or |
| 450 isinstance(node, NegativeCondition)): |
433 raise ParserError("'variants' is not allowed inside a " | 451 raise ParserError("'variants' is not allowed inside a " |
434 "conditional block", | 452 "conditional block", |
435 None, cr.filename, linenum) | 453 None, cr.filename, linenum) |
436 node = self._parse_variants(cr, node, prev_indent=indent) | 454 node = self._parse_variants(cr, node, prev_indent=indent) |
437 continue | 455 continue |
438 | 456 |
439 # Parse 'include' statements | 457 # Parse 'include' statements |
440 if words[0] == "include": | 458 if words[0] == "include": |
441 if len(words) < 2: | 459 if len(words) < 2: |
442 raise ParserError("Syntax error: missing parameter", | 460 raise ParserError("Syntax error: missing parameter", |
443 line, cr.filename, linenum) | 461 line, cr.filename, linenum) |
444 if not isinstance(cr, FileReader): | 462 filename = os.path.expanduser(words[1]) |
445 raise ParserError("Cannot include because no file is " | 463 if isinstance(cr, FileReader) and not os.path.isabs(filename): |
446 "currently open", | 464 filename = os.path.join(os.path.dirname(cr.filename), |
447 line, cr.filename, linenum) | 465 filename) |
448 filename = os.path.join(os.path.dirname(cr.filename), words[1]) | |
449 if not os.path.isfile(filename): | 466 if not os.path.isfile(filename): |
450 self._warn("%r (%s:%s): file doesn't exist or is not a " | 467 self._warn("%r (%s:%s): file doesn't exist or is not a " |
451 "regular file", line, cr.filename, linenum) | 468 "regular file", line, cr.filename, linenum) |
452 continue | 469 continue |
453 node = self._parse(FileReader(filename), node) | 470 node = self._parse(FileReader(filename), node) |
454 continue | 471 continue |
455 | 472 |
456 # Parse 'only' and 'no' filters | 473 # Parse 'only' and 'no' filters |
457 if words[0] in ("only", "no"): | 474 if words[0] in ("only", "no"): |
458 if len(words) < 2: | 475 if len(words) < 2: |
459 raise ParserError("Syntax error: missing parameter", | 476 raise ParserError("Syntax error: missing parameter", |
460 line, cr.filename, linenum) | 477 line, cr.filename, linenum) |
461 try: | 478 try: |
462 if words[0] == "only": | 479 if words[0] == "only": |
463 f = OnlyFilter(line) | 480 f = OnlyFilter(line) |
464 elif words[0] == "no": | 481 elif words[0] == "no": |
465 f = NoFilter(line) | 482 f = NoFilter(line) |
466 except ParserError, e: | 483 except ParserError, e: |
467 e.line = line | 484 e.line = line |
468 e.filename = cr.filename | 485 e.filename = cr.filename |
469 e.linenum = linenum | 486 e.linenum = linenum |
470 raise | 487 raise |
471 node.content += [(cr.filename, linenum, f)] | 488 node.content += [(cr.filename, linenum, f)] |
472 continue | 489 continue |
473 | 490 |
| 491 # Look for operators |
| 492 op_match = _ops_exp.search(line) |
| 493 |
474 # Parse conditional blocks | 494 # Parse conditional blocks |
475 if line.endswith(":"): | 495 if ":" in line: |
476 try: | 496 index = line.index(":") |
477 cond = Condition(line) | 497 if not op_match or index < op_match.start(): |
478 except ParserError, e: | 498 index += 1 |
479 e.line = line | 499 cr.set_next_line(line[index:], indent, linenum) |
480 e.filename = cr.filename | 500 line = line[:index] |
481 e.linenum = linenum | 501 try: |
482 raise | 502 if line.startswith("!"): |
483 self._parse(cr, cond, prev_indent=indent) | 503 cond = NegativeCondition(line) |
484 node.content += [(cr.filename, linenum, cond)] | 504 else: |
485 continue | 505 cond = Condition(line) |
| 506 except ParserError, e: |
| 507 e.line = line |
| 508 e.filename = cr.filename |
| 509 e.linenum = linenum |
| 510 raise |
| 511 self._parse(cr, cond, prev_indent=indent) |
| 512 node.content += [(cr.filename, linenum, cond)] |
| 513 continue |
486 | 514 |
487 # Parse regular operators | 515 # Parse regular operators |
488 try: | 516 if not op_match: |
489 op = Op(line) | 517 raise ParserError("Syntax error", line, cr.filename, linenum) |
490 except ParserError, e: | 518 node.content += [(cr.filename, linenum, Op(line, op_match))] |
491 e.line = line | |
492 e.filename = cr.filename | |
493 e.linenum = linenum | |
494 raise | |
495 node.content += [(cr.filename, linenum, op)] | |
496 | 519 |
497 return node | 520 return node |
498 | 521 |
499 | 522 |
500 # Assignment operators | 523 # Assignment operators |
501 | 524 |
502 _reserved_keys = set(("name", "shortname", "dep")) | 525 _reserved_keys = set(("name", "shortname", "dep")) |
503 | 526 |
504 | 527 |
505 def _op_set(d, key, value): | 528 def _op_set(d, key, value): |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
550 "<=": (r"\<\=", _op_prepend), | 573 "<=": (r"\<\=", _op_prepend), |
551 "?=": (r"\?\=", _op_regex_set), | 574 "?=": (r"\?\=", _op_regex_set), |
552 "?+=": (r"\?\+\=", _op_regex_append), | 575 "?+=": (r"\?\+\=", _op_regex_append), |
553 "?<=": (r"\?\<\=", _op_regex_prepend), | 576 "?<=": (r"\?\<\=", _op_regex_prepend), |
554 "del": (r"^del\b", _op_regex_del)} | 577 "del": (r"^del\b", _op_regex_del)} |
555 | 578 |
556 _ops_exp = re.compile("|".join([op[0] for op in _ops.values()])) | 579 _ops_exp = re.compile("|".join([op[0] for op in _ops.values()])) |
557 | 580 |
558 | 581 |
559 class Op(object): | 582 class Op(object): |
560 def __init__(self, line): | 583 def __init__(self, line, m): |
561 m = re.search(_ops_exp, line) | 584 self.func = _ops[m.group()][1] |
562 if not m: | 585 self.key = line[:m.start()].strip() |
563 raise ParserError("Syntax error: missing operator") | |
564 left = line[:m.start()].strip() | |
565 value = line[m.end():].strip() | 586 value = line[m.end():].strip() |
566 if value and ((value[0] == '"' and value[-1] == '"') or | 587 if value and (value[0] == value[-1] == '"' or |
567 (value[0] == "'" and value[-1] == "'")): | 588 value[0] == value[-1] == "'"): |
568 value = value[1:-1] | 589 value = value[1:-1] |
569 filters_and_key = map(str.strip, left.split(":")) | |
570 self.filters = [Filter(f) for f in filters_and_key[:-1]] | |
571 self.key = filters_and_key[-1] | |
572 self.value = value | 590 self.value = value |
573 self.func = _ops[m.group()][1] | |
574 | 591 |
575 | 592 |
576 def apply_to_dict(self, d, ctx, ctx_set): | 593 def apply_to_dict(self, d): |
577 for f in self.filters: | |
578 if not f.match(ctx, ctx_set): | |
579 return | |
580 self.func(d, self.key, self.value) | 594 self.func(d, self.key, self.value) |
581 | 595 |
582 | 596 |
583 # StrReader and FileReader | 597 # StrReader and FileReader |
584 | 598 |
585 class StrReader(object): | 599 class StrReader(object): |
586 """ | 600 """ |
587 Preprocess an input string for easy reading. | 601 Preprocess an input string for easy reading. |
588 """ | 602 """ |
589 def __init__(self, s): | 603 def __init__(self, s): |
590 """ | 604 """ |
591 Initialize the reader. | 605 Initialize the reader. |
592 | 606 |
593 @param s: The string to parse. | 607 @param s: The string to parse. |
594 """ | 608 """ |
595 self.filename = "<string>" | 609 self.filename = "<string>" |
596 self._lines = [] | 610 self._lines = [] |
597 self._line_index = 0 | 611 self._line_index = 0 |
| 612 self._stored_line = None |
598 for linenum, line in enumerate(s.splitlines()): | 613 for linenum, line in enumerate(s.splitlines()): |
599 line = line.rstrip().expandtabs() | 614 line = line.rstrip().expandtabs() |
600 stripped_line = line.lstrip() | 615 stripped_line = line.lstrip() |
601 indent = len(line) - len(stripped_line) | 616 indent = len(line) - len(stripped_line) |
602 if (not stripped_line | 617 if (not stripped_line |
603 or stripped_line.startswith("#") | 618 or stripped_line.startswith("#") |
604 or stripped_line.startswith("//")): | 619 or stripped_line.startswith("//")): |
605 continue | 620 continue |
606 self._lines.append((stripped_line, indent, linenum + 1)) | 621 self._lines.append((stripped_line, indent, linenum + 1)) |
607 | 622 |
608 | 623 |
609 def get_next_line(self, prev_indent): | 624 def get_next_line(self, prev_indent): |
610 """ | 625 """ |
611 Get the next non-empty, non-comment line in the string, whose | 626 Get the next line in the current block. |
612 indentation level is higher than prev_indent. | |
613 | 627 |
614 @param prev_indent: The indentation level of the previous block. | 628 @param prev_indent: The indentation level of the previous block. |
615 @return: (line, indent, linenum), where indent is the line's | 629 @return: (line, indent, linenum), where indent is the line's |
616 indentation level. If no line is available, (None, -1, -1) is | 630 indentation level. If no line is available, (None, -1, -1) is |
617 returned. | 631 returned. |
618 """ | 632 """ |
| 633 if self._stored_line: |
| 634 ret = self._stored_line |
| 635 self._stored_line = None |
| 636 return ret |
619 if self._line_index >= len(self._lines): | 637 if self._line_index >= len(self._lines): |
620 return None, -1, -1 | 638 return None, -1, -1 |
621 line, indent, linenum = self._lines[self._line_index] | 639 line, indent, linenum = self._lines[self._line_index] |
622 if indent <= prev_indent: | 640 if indent <= prev_indent: |
623 return None, -1, -1 | 641 return None, -1, -1 |
624 self._line_index += 1 | 642 self._line_index += 1 |
625 return line, indent, linenum | 643 return line, indent, linenum |
626 | 644 |
627 | 645 |
| 646 def set_next_line(self, line, indent, linenum): |
| 647 """ |
| 648 Make the next call to get_next_line() return the given line instead of |
| 649 the real next line. |
| 650 """ |
| 651 line = line.strip() |
| 652 if line: |
| 653 self._stored_line = line, indent, linenum |
| 654 |
| 655 |
628 class FileReader(StrReader): | 656 class FileReader(StrReader): |
629 """ | 657 """ |
630 Preprocess an input file for easy reading. | 658 Preprocess an input file for easy reading. |
631 """ | 659 """ |
632 def __init__(self, filename): | 660 def __init__(self, filename): |
633 """ | 661 """ |
634 Initialize the reader. | 662 Initialize the reader. |
635 | 663 |
636 @parse filename: The name of the input file. | 664 @parse filename: The name of the input file. |
637 """ | 665 """ |
638 StrReader.__init__(self, open(filename).read()) | 666 StrReader.__init__(self, open(filename).read()) |
639 self.filename = filename | 667 self.filename = filename |
640 | 668 |
641 | 669 |
642 if __name__ == "__main__": | 670 if __name__ == "__main__": |
643 parser = optparse.OptionParser("usage: %prog [options] <filename>") | 671 parser = optparse.OptionParser('usage: %prog [options] filename ' |
| 672 '[extra code] ...\n\nExample:\n\n ' |
| 673 '%prog tests.cfg "only my_set" "no qcow2"') |
644 parser.add_option("-v", "--verbose", dest="debug", action="store_true", | 674 parser.add_option("-v", "--verbose", dest="debug", action="store_true", |
645 help="include debug messages in console output") | 675 help="include debug messages in console output") |
646 parser.add_option("-f", "--fullname", dest="fullname", action="store_true", | 676 parser.add_option("-f", "--fullname", dest="fullname", action="store_true", |
647 help="show full dict names instead of short names") | 677 help="show full dict names instead of short names") |
648 parser.add_option("-c", "--contents", dest="contents", action="store_true", | 678 parser.add_option("-c", "--contents", dest="contents", action="store_true", |
649 help="show dict contents") | 679 help="show dict contents") |
650 | 680 |
651 options, args = parser.parse_args() | 681 options, args = parser.parse_args() |
652 if not args: | 682 if not args: |
653 parser.error("filename required") | 683 parser.error("filename required") |
654 | 684 |
655 c = Parser(args[0], debug=options.debug) | 685 c = Parser(args[0], debug=options.debug) |
| 686 for s in args[1:]: |
| 687 c.parse_string(s) |
| 688 |
656 for i, d in enumerate(c.get_dicts()): | 689 for i, d in enumerate(c.get_dicts()): |
657 if options.fullname: | 690 if options.fullname: |
658 print "dict %4d: %s" % (i + 1, d["name"]) | 691 print "dict %4d: %s" % (i + 1, d["name"]) |
659 else: | 692 else: |
660 print "dict %4d: %s" % (i + 1, d["shortname"]) | 693 print "dict %4d: %s" % (i + 1, d["shortname"]) |
661 if options.contents: | 694 if options.contents: |
662 keys = d.keys() | 695 keys = d.keys() |
663 keys.sort() | 696 keys.sort() |
664 for key in keys: | 697 for key in keys: |
665 print " %s = %s" % (key, d[key]) | 698 print " %s = %s" % (key, d[key]) |
OLD | NEW |