1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1346 @@ 1.4 +#!/usr/bin/env python 1.5 + 1.6 +# This Source Code Form is subject to the terms of the Mozilla Public 1.7 +# License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 +# You can obtain one at http://mozilla.org/MPL/2.0/. 1.9 + 1.10 +""" 1.11 +Mozilla universal manifest parser 1.12 +""" 1.13 + 1.14 +__all__ = ['read_ini', # .ini reader 1.15 + 'ManifestParser', 'TestManifest', 'convert', # manifest handling 1.16 + 'parse', 'ParseError', 'ExpressionParser'] # conditional expression parser 1.17 + 1.18 +import fnmatch 1.19 +import os 1.20 +import re 1.21 +import shutil 1.22 +import sys 1.23 + 1.24 +from optparse import OptionParser 1.25 +from StringIO import StringIO 1.26 + 1.27 +relpath = os.path.relpath 1.28 +string = (basestring,) 1.29 + 1.30 + 1.31 +# expr.py 1.32 +# from: 1.33 +# http://k0s.org/mozilla/hg/expressionparser 1.34 +# http://hg.mozilla.org/users/tmielczarek_mozilla.com/expressionparser 1.35 + 1.36 +# Implements a top-down parser/evaluator for simple boolean expressions. 1.37 +# ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm 1.38 +# 1.39 +# Rough grammar: 1.40 +# expr := literal 1.41 +# | '(' expr ')' 1.42 +# | expr '&&' expr 1.43 +# | expr '||' expr 1.44 +# | expr '==' expr 1.45 +# | expr '!=' expr 1.46 +# literal := BOOL 1.47 +# | INT 1.48 +# | STRING 1.49 +# | IDENT 1.50 +# BOOL := true|false 1.51 +# INT := [0-9]+ 1.52 +# STRING := "[^"]*" 1.53 +# IDENT := [A-Za-z_]\w* 1.54 + 1.55 +# Identifiers take their values from a mapping dictionary passed as the second 1.56 +# argument. 1.57 + 1.58 +# Glossary (see above URL for details): 1.59 +# - nud: null denotation 1.60 +# - led: left detonation 1.61 +# - lbp: left binding power 1.62 +# - rbp: right binding power 1.63 + 1.64 +class ident_token(object): 1.65 + def __init__(self, value): 1.66 + self.value = value 1.67 + def nud(self, parser): 1.68 + # identifiers take their value from the value mappings passed 1.69 + # to the parser 1.70 + return parser.value(self.value) 1.71 + 1.72 +class literal_token(object): 1.73 + def __init__(self, value): 1.74 + self.value = value 1.75 + def nud(self, parser): 1.76 + return self.value 1.77 + 1.78 +class eq_op_token(object): 1.79 + "==" 1.80 + def led(self, parser, left): 1.81 + return left == parser.expression(self.lbp) 1.82 + 1.83 +class neq_op_token(object): 1.84 + "!=" 1.85 + def led(self, parser, left): 1.86 + return left != parser.expression(self.lbp) 1.87 + 1.88 +class not_op_token(object): 1.89 + "!" 1.90 + def nud(self, parser): 1.91 + return not parser.expression(100) 1.92 + 1.93 +class and_op_token(object): 1.94 + "&&" 1.95 + def led(self, parser, left): 1.96 + right = parser.expression(self.lbp) 1.97 + return left and right 1.98 + 1.99 +class or_op_token(object): 1.100 + "||" 1.101 + def led(self, parser, left): 1.102 + right = parser.expression(self.lbp) 1.103 + return left or right 1.104 + 1.105 +class lparen_token(object): 1.106 + "(" 1.107 + def nud(self, parser): 1.108 + expr = parser.expression() 1.109 + parser.advance(rparen_token) 1.110 + return expr 1.111 + 1.112 +class rparen_token(object): 1.113 + ")" 1.114 + 1.115 +class end_token(object): 1.116 + """always ends parsing""" 1.117 + 1.118 +### derived literal tokens 1.119 + 1.120 +class bool_token(literal_token): 1.121 + def __init__(self, value): 1.122 + value = {'true':True, 'false':False}[value] 1.123 + literal_token.__init__(self, value) 1.124 + 1.125 +class int_token(literal_token): 1.126 + def __init__(self, value): 1.127 + literal_token.__init__(self, int(value)) 1.128 + 1.129 +class string_token(literal_token): 1.130 + def __init__(self, value): 1.131 + literal_token.__init__(self, value[1:-1]) 1.132 + 1.133 +precedence = [(end_token, rparen_token), 1.134 + (or_op_token,), 1.135 + (and_op_token,), 1.136 + (eq_op_token, neq_op_token), 1.137 + (lparen_token,), 1.138 + ] 1.139 +for index, rank in enumerate(precedence): 1.140 + for token in rank: 1.141 + token.lbp = index # lbp = lowest left binding power 1.142 + 1.143 +class ParseError(Exception): 1.144 + """error parsing conditional expression""" 1.145 + 1.146 +class ExpressionParser(object): 1.147 + """ 1.148 + A parser for a simple expression language. 1.149 + 1.150 + The expression language can be described as follows:: 1.151 + 1.152 + EXPRESSION ::= LITERAL | '(' EXPRESSION ')' | '!' EXPRESSION | EXPRESSION OP EXPRESSION 1.153 + OP ::= '==' | '!=' | '&&' | '||' 1.154 + LITERAL ::= BOOL | INT | IDENT | STRING 1.155 + BOOL ::= 'true' | 'false' 1.156 + INT ::= [0-9]+ 1.157 + IDENT ::= [a-zA-Z_]\w* 1.158 + STRING ::= '"' [^\"] '"' | ''' [^\'] ''' 1.159 + 1.160 + At its core, expressions consist of booleans, integers, identifiers and. 1.161 + strings. Booleans are one of *true* or *false*. Integers are a series 1.162 + of digits. Identifiers are a series of English letters and underscores. 1.163 + Strings are a pair of matching quote characters (single or double) with 1.164 + zero or more characters inside. 1.165 + 1.166 + Expressions can be combined with operators: the equals (==) and not 1.167 + equals (!=) operators compare two expressions and produce a boolean. The 1.168 + and (&&) and or (||) operators take two expressions and produce the logical 1.169 + AND or OR value of them, respectively. An expression can also be prefixed 1.170 + with the not (!) operator, which produces its logical negation. 1.171 + 1.172 + Finally, any expression may be contained within parentheses for grouping. 1.173 + 1.174 + Identifiers take their values from the mapping provided. 1.175 + """ 1.176 + def __init__(self, text, valuemapping, strict=False): 1.177 + """ 1.178 + Initialize the parser 1.179 + :param text: The expression to parse as a string. 1.180 + :param valuemapping: A dict mapping identifier names to values. 1.181 + :param strict: If true, referencing an identifier that was not 1.182 + provided in :valuemapping: will raise an error. 1.183 + """ 1.184 + self.text = text 1.185 + self.valuemapping = valuemapping 1.186 + self.strict = strict 1.187 + 1.188 + def _tokenize(self): 1.189 + """ 1.190 + Lex the input text into tokens and yield them in sequence. 1.191 + """ 1.192 + # scanner callbacks 1.193 + def bool_(scanner, t): return bool_token(t) 1.194 + def identifier(scanner, t): return ident_token(t) 1.195 + def integer(scanner, t): return int_token(t) 1.196 + def eq(scanner, t): return eq_op_token() 1.197 + def neq(scanner, t): return neq_op_token() 1.198 + def or_(scanner, t): return or_op_token() 1.199 + def and_(scanner, t): return and_op_token() 1.200 + def lparen(scanner, t): return lparen_token() 1.201 + def rparen(scanner, t): return rparen_token() 1.202 + def string_(scanner, t): return string_token(t) 1.203 + def not_(scanner, t): return not_op_token() 1.204 + 1.205 + scanner = re.Scanner([ 1.206 + # Note: keep these in sync with the class docstring above. 1.207 + (r"true|false", bool_), 1.208 + (r"[a-zA-Z_]\w*", identifier), 1.209 + (r"[0-9]+", integer), 1.210 + (r'("[^"]*")|(\'[^\']*\')', string_), 1.211 + (r"==", eq), 1.212 + (r"!=", neq), 1.213 + (r"\|\|", or_), 1.214 + (r"!", not_), 1.215 + (r"&&", and_), 1.216 + (r"\(", lparen), 1.217 + (r"\)", rparen), 1.218 + (r"\s+", None), # skip whitespace 1.219 + ]) 1.220 + tokens, remainder = scanner.scan(self.text) 1.221 + for t in tokens: 1.222 + yield t 1.223 + yield end_token() 1.224 + 1.225 + def value(self, ident): 1.226 + """ 1.227 + Look up the value of |ident| in the value mapping passed in the 1.228 + constructor. 1.229 + """ 1.230 + if self.strict: 1.231 + return self.valuemapping[ident] 1.232 + else: 1.233 + return self.valuemapping.get(ident, None) 1.234 + 1.235 + def advance(self, expected): 1.236 + """ 1.237 + Assert that the next token is an instance of |expected|, and advance 1.238 + to the next token. 1.239 + """ 1.240 + if not isinstance(self.token, expected): 1.241 + raise Exception, "Unexpected token!" 1.242 + self.token = self.iter.next() 1.243 + 1.244 + def expression(self, rbp=0): 1.245 + """ 1.246 + Parse and return the value of an expression until a token with 1.247 + right binding power greater than rbp is encountered. 1.248 + """ 1.249 + t = self.token 1.250 + self.token = self.iter.next() 1.251 + left = t.nud(self) 1.252 + while rbp < self.token.lbp: 1.253 + t = self.token 1.254 + self.token = self.iter.next() 1.255 + left = t.led(self, left) 1.256 + return left 1.257 + 1.258 + def parse(self): 1.259 + """ 1.260 + Parse and return the value of the expression in the text 1.261 + passed to the constructor. Raises a ParseError if the expression 1.262 + could not be parsed. 1.263 + """ 1.264 + try: 1.265 + self.iter = self._tokenize() 1.266 + self.token = self.iter.next() 1.267 + return self.expression() 1.268 + except: 1.269 + raise ParseError("could not parse: %s; variables: %s" % (self.text, self.valuemapping)) 1.270 + 1.271 + __call__ = parse 1.272 + 1.273 +def parse(text, **values): 1.274 + """ 1.275 + Parse and evaluate a boolean expression. 1.276 + :param text: The expression to parse, as a string. 1.277 + :param values: A dict containing a name to value mapping for identifiers 1.278 + referenced in *text*. 1.279 + :rtype: the final value of the expression. 1.280 + :raises: :py:exc::ParseError: will be raised if parsing fails. 1.281 + """ 1.282 + return ExpressionParser(text, values).parse() 1.283 + 1.284 + 1.285 +### path normalization 1.286 + 1.287 +def normalize_path(path): 1.288 + """normalize a relative path""" 1.289 + if sys.platform.startswith('win'): 1.290 + return path.replace('/', os.path.sep) 1.291 + return path 1.292 + 1.293 +def denormalize_path(path): 1.294 + """denormalize a relative path""" 1.295 + if sys.platform.startswith('win'): 1.296 + return path.replace(os.path.sep, '/') 1.297 + return path 1.298 + 1.299 + 1.300 +### .ini reader 1.301 + 1.302 +def read_ini(fp, variables=None, default='DEFAULT', 1.303 + comments=';#', separators=('=', ':'), 1.304 + strict=True): 1.305 + """ 1.306 + read an .ini file and return a list of [(section, values)] 1.307 + - fp : file pointer or path to read 1.308 + - variables : default set of variables 1.309 + - default : name of the section for the default section 1.310 + - comments : characters that if they start a line denote a comment 1.311 + - separators : strings that denote key, value separation in order 1.312 + - strict : whether to be strict about parsing 1.313 + """ 1.314 + 1.315 + # variables 1.316 + variables = variables or {} 1.317 + sections = [] 1.318 + key = value = None 1.319 + section_names = set() 1.320 + if isinstance(fp, basestring): 1.321 + fp = file(fp) 1.322 + 1.323 + # read the lines 1.324 + for (linenum, line) in enumerate(fp.readlines(), start=1): 1.325 + 1.326 + stripped = line.strip() 1.327 + 1.328 + # ignore blank lines 1.329 + if not stripped: 1.330 + # reset key and value to avoid continuation lines 1.331 + key = value = None 1.332 + continue 1.333 + 1.334 + # ignore comment lines 1.335 + if stripped[0] in comments: 1.336 + continue 1.337 + 1.338 + # check for a new section 1.339 + if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']': 1.340 + section = stripped[1:-1].strip() 1.341 + key = value = None 1.342 + 1.343 + # deal with DEFAULT section 1.344 + if section.lower() == default.lower(): 1.345 + if strict: 1.346 + assert default not in section_names 1.347 + section_names.add(default) 1.348 + current_section = variables 1.349 + continue 1.350 + 1.351 + if strict: 1.352 + # make sure this section doesn't already exist 1.353 + assert section not in section_names, "Section '%s' already found in '%s'" % (section, section_names) 1.354 + 1.355 + section_names.add(section) 1.356 + current_section = {} 1.357 + sections.append((section, current_section)) 1.358 + continue 1.359 + 1.360 + # if there aren't any sections yet, something bad happen 1.361 + if not section_names: 1.362 + raise Exception('No sections found') 1.363 + 1.364 + # (key, value) pair 1.365 + for separator in separators: 1.366 + if separator in stripped: 1.367 + key, value = stripped.split(separator, 1) 1.368 + key = key.strip() 1.369 + value = value.strip() 1.370 + 1.371 + if strict: 1.372 + # make sure this key isn't already in the section or empty 1.373 + assert key 1.374 + if current_section is not variables: 1.375 + assert key not in current_section 1.376 + 1.377 + current_section[key] = value 1.378 + break 1.379 + else: 1.380 + # continuation line ? 1.381 + if line[0].isspace() and key: 1.382 + value = '%s%s%s' % (value, os.linesep, stripped) 1.383 + current_section[key] = value 1.384 + else: 1.385 + # something bad happened! 1.386 + if hasattr(fp, 'name'): 1.387 + filename = fp.name 1.388 + else: 1.389 + filename = 'unknown' 1.390 + raise Exception("Error parsing manifest file '%s', line %s" % 1.391 + (filename, linenum)) 1.392 + 1.393 + # interpret the variables 1.394 + def interpret_variables(global_dict, local_dict): 1.395 + variables = global_dict.copy() 1.396 + if 'skip-if' in local_dict and 'skip-if' in variables: 1.397 + local_dict['skip-if'] = "(%s) || (%s)" % (variables['skip-if'].split('#')[0], local_dict['skip-if'].split('#')[0]) 1.398 + variables.update(local_dict) 1.399 + 1.400 + return variables 1.401 + 1.402 + sections = [(i, interpret_variables(variables, j)) for i, j in sections] 1.403 + return sections 1.404 + 1.405 + 1.406 +### objects for parsing manifests 1.407 + 1.408 +class ManifestParser(object): 1.409 + """read .ini manifests""" 1.410 + 1.411 + def __init__(self, manifests=(), defaults=None, strict=True): 1.412 + self._defaults = defaults or {} 1.413 + self.tests = [] 1.414 + self.manifest_defaults = {} 1.415 + self.strict = strict 1.416 + self.rootdir = None 1.417 + self.relativeRoot = None 1.418 + if manifests: 1.419 + self.read(*manifests) 1.420 + 1.421 + def getRelativeRoot(self, root): 1.422 + return root 1.423 + 1.424 + ### methods for reading manifests 1.425 + 1.426 + def _read(self, root, filename, defaults): 1.427 + 1.428 + # get directory of this file if not file-like object 1.429 + if isinstance(filename, string): 1.430 + filename = os.path.abspath(filename) 1.431 + fp = open(filename) 1.432 + here = os.path.dirname(filename) 1.433 + else: 1.434 + fp = filename 1.435 + filename = here = None 1.436 + defaults['here'] = here 1.437 + 1.438 + # Rootdir is needed for relative path calculation. Precompute it for 1.439 + # the microoptimization used below. 1.440 + if self.rootdir is None: 1.441 + rootdir = "" 1.442 + else: 1.443 + assert os.path.isabs(self.rootdir) 1.444 + rootdir = self.rootdir + os.path.sep 1.445 + 1.446 + # read the configuration 1.447 + sections = read_ini(fp=fp, variables=defaults, strict=self.strict) 1.448 + self.manifest_defaults[filename] = defaults 1.449 + 1.450 + # get the tests 1.451 + for section, data in sections: 1.452 + subsuite = '' 1.453 + if 'subsuite' in data: 1.454 + subsuite = data['subsuite'] 1.455 + 1.456 + # a file to include 1.457 + # TODO: keep track of included file structure: 1.458 + # self.manifests = {'manifest.ini': 'relative/path.ini'} 1.459 + if section.startswith('include:'): 1.460 + include_file = section.split('include:', 1)[-1] 1.461 + include_file = normalize_path(include_file) 1.462 + if not os.path.isabs(include_file): 1.463 + include_file = os.path.join(self.getRelativeRoot(here), include_file) 1.464 + if not os.path.exists(include_file): 1.465 + message = "Included file '%s' does not exist" % include_file 1.466 + if self.strict: 1.467 + raise IOError(message) 1.468 + else: 1.469 + sys.stderr.write("%s\n" % message) 1.470 + continue 1.471 + include_defaults = data.copy() 1.472 + self._read(root, include_file, include_defaults) 1.473 + continue 1.474 + 1.475 + # otherwise an item 1.476 + test = data 1.477 + test['name'] = section 1.478 + 1.479 + # Will be None if the manifest being read is a file-like object. 1.480 + test['manifest'] = filename 1.481 + 1.482 + # determine the path 1.483 + path = test.get('path', section) 1.484 + _relpath = path 1.485 + if '://' not in path: # don't futz with URLs 1.486 + path = normalize_path(path) 1.487 + if here and not os.path.isabs(path): 1.488 + path = os.path.normpath(os.path.join(here, path)) 1.489 + 1.490 + # Microoptimization, because relpath is quite expensive. 1.491 + # We know that rootdir is an absolute path or empty. If path 1.492 + # starts with rootdir, then path is also absolute and the tail 1.493 + # of the path is the relative path (possibly non-normalized, 1.494 + # when here is unknown). 1.495 + # For this to work rootdir needs to be terminated with a path 1.496 + # separator, so that references to sibling directories with 1.497 + # a common prefix don't get misscomputed (e.g. /root and 1.498 + # /rootbeer/file). 1.499 + # When the rootdir is unknown, the relpath needs to be left 1.500 + # unchanged. We use an empty string as rootdir in that case, 1.501 + # which leaves relpath unchanged after slicing. 1.502 + if path.startswith(rootdir): 1.503 + _relpath = path[len(rootdir):] 1.504 + else: 1.505 + _relpath = relpath(path, rootdir) 1.506 + 1.507 + test['subsuite'] = subsuite 1.508 + test['path'] = path 1.509 + test['relpath'] = _relpath 1.510 + 1.511 + # append the item 1.512 + self.tests.append(test) 1.513 + 1.514 + def read(self, *filenames, **defaults): 1.515 + """ 1.516 + read and add manifests from file paths or file-like objects 1.517 + 1.518 + filenames -- file paths or file-like objects to read as manifests 1.519 + defaults -- default variables 1.520 + """ 1.521 + 1.522 + # ensure all files exist 1.523 + missing = [filename for filename in filenames 1.524 + if isinstance(filename, string) and not os.path.exists(filename) ] 1.525 + if missing: 1.526 + raise IOError('Missing files: %s' % ', '.join(missing)) 1.527 + 1.528 + # default variables 1.529 + _defaults = defaults.copy() or self._defaults.copy() 1.530 + _defaults.setdefault('here', None) 1.531 + 1.532 + # process each file 1.533 + for filename in filenames: 1.534 + # set the per file defaults 1.535 + defaults = _defaults.copy() 1.536 + here = None 1.537 + if isinstance(filename, string): 1.538 + here = os.path.dirname(os.path.abspath(filename)) 1.539 + defaults['here'] = here # directory of master .ini file 1.540 + 1.541 + if self.rootdir is None: 1.542 + # set the root directory 1.543 + # == the directory of the first manifest given 1.544 + self.rootdir = here 1.545 + 1.546 + self._read(here, filename, defaults) 1.547 + 1.548 + 1.549 + ### methods for querying manifests 1.550 + 1.551 + def query(self, *checks, **kw): 1.552 + """ 1.553 + general query function for tests 1.554 + - checks : callable conditions to test if the test fulfills the query 1.555 + """ 1.556 + tests = kw.get('tests', None) 1.557 + if tests is None: 1.558 + tests = self.tests 1.559 + retval = [] 1.560 + for test in tests: 1.561 + for check in checks: 1.562 + if not check(test): 1.563 + break 1.564 + else: 1.565 + retval.append(test) 1.566 + return retval 1.567 + 1.568 + def get(self, _key=None, inverse=False, tags=None, tests=None, **kwargs): 1.569 + # TODO: pass a dict instead of kwargs since you might hav 1.570 + # e.g. 'inverse' as a key in the dict 1.571 + 1.572 + # TODO: tags should just be part of kwargs with None values 1.573 + # (None == any is kinda weird, but probably still better) 1.574 + 1.575 + # fix up tags 1.576 + if tags: 1.577 + tags = set(tags) 1.578 + else: 1.579 + tags = set() 1.580 + 1.581 + # make some check functions 1.582 + if inverse: 1.583 + has_tags = lambda test: not tags.intersection(test.keys()) 1.584 + def dict_query(test): 1.585 + for key, value in kwargs.items(): 1.586 + if test.get(key) == value: 1.587 + return False 1.588 + return True 1.589 + else: 1.590 + has_tags = lambda test: tags.issubset(test.keys()) 1.591 + def dict_query(test): 1.592 + for key, value in kwargs.items(): 1.593 + if test.get(key) != value: 1.594 + return False 1.595 + return True 1.596 + 1.597 + # query the tests 1.598 + tests = self.query(has_tags, dict_query, tests=tests) 1.599 + 1.600 + # if a key is given, return only a list of that key 1.601 + # useful for keys like 'name' or 'path' 1.602 + if _key: 1.603 + return [test[_key] for test in tests] 1.604 + 1.605 + # return the tests 1.606 + return tests 1.607 + 1.608 + def manifests(self, tests=None): 1.609 + """ 1.610 + return manifests in order in which they appear in the tests 1.611 + """ 1.612 + if tests is None: 1.613 + # Make sure to return all the manifests, even ones without tests. 1.614 + return self.manifest_defaults.keys() 1.615 + 1.616 + manifests = [] 1.617 + for test in tests: 1.618 + manifest = test.get('manifest') 1.619 + if not manifest: 1.620 + continue 1.621 + if manifest not in manifests: 1.622 + manifests.append(manifest) 1.623 + return manifests 1.624 + 1.625 + def paths(self): 1.626 + return [i['path'] for i in self.tests] 1.627 + 1.628 + 1.629 + ### methods for auditing 1.630 + 1.631 + def missing(self, tests=None): 1.632 + """return list of tests that do not exist on the filesystem""" 1.633 + if tests is None: 1.634 + tests = self.tests 1.635 + return [test for test in tests 1.636 + if not os.path.exists(test['path'])] 1.637 + 1.638 + def verifyDirectory(self, directories, pattern=None, extensions=None): 1.639 + """ 1.640 + checks what is on the filesystem vs what is in a manifest 1.641 + returns a 2-tuple of sets: 1.642 + (missing_from_filesystem, missing_from_manifest) 1.643 + """ 1.644 + 1.645 + files = set([]) 1.646 + if isinstance(directories, basestring): 1.647 + directories = [directories] 1.648 + 1.649 + # get files in directories 1.650 + for directory in directories: 1.651 + for dirpath, dirnames, filenames in os.walk(directory, topdown=True): 1.652 + 1.653 + # only add files that match a pattern 1.654 + if pattern: 1.655 + filenames = fnmatch.filter(filenames, pattern) 1.656 + 1.657 + # only add files that have one of the extensions 1.658 + if extensions: 1.659 + filenames = [filename for filename in filenames 1.660 + if os.path.splitext(filename)[-1] in extensions] 1.661 + 1.662 + files.update([os.path.join(dirpath, filename) for filename in filenames]) 1.663 + 1.664 + paths = set(self.paths()) 1.665 + missing_from_filesystem = paths.difference(files) 1.666 + missing_from_manifest = files.difference(paths) 1.667 + return (missing_from_filesystem, missing_from_manifest) 1.668 + 1.669 + 1.670 + ### methods for output 1.671 + 1.672 + def write(self, fp=sys.stdout, rootdir=None, 1.673 + global_tags=None, global_kwargs=None, 1.674 + local_tags=None, local_kwargs=None): 1.675 + """ 1.676 + write a manifest given a query 1.677 + global and local options will be munged to do the query 1.678 + globals will be written to the top of the file 1.679 + locals (if given) will be written per test 1.680 + """ 1.681 + 1.682 + # open file if `fp` given as string 1.683 + close = False 1.684 + if isinstance(fp, string): 1.685 + fp = file(fp, 'w') 1.686 + close = True 1.687 + 1.688 + # root directory 1.689 + if rootdir is None: 1.690 + rootdir = self.rootdir 1.691 + 1.692 + # sanitize input 1.693 + global_tags = global_tags or set() 1.694 + local_tags = local_tags or set() 1.695 + global_kwargs = global_kwargs or {} 1.696 + local_kwargs = local_kwargs or {} 1.697 + 1.698 + # create the query 1.699 + tags = set([]) 1.700 + tags.update(global_tags) 1.701 + tags.update(local_tags) 1.702 + kwargs = {} 1.703 + kwargs.update(global_kwargs) 1.704 + kwargs.update(local_kwargs) 1.705 + 1.706 + # get matching tests 1.707 + tests = self.get(tags=tags, **kwargs) 1.708 + 1.709 + # print the .ini manifest 1.710 + if global_tags or global_kwargs: 1.711 + print >> fp, '[DEFAULT]' 1.712 + for tag in global_tags: 1.713 + print >> fp, '%s =' % tag 1.714 + for key, value in global_kwargs.items(): 1.715 + print >> fp, '%s = %s' % (key, value) 1.716 + print >> fp 1.717 + 1.718 + for test in tests: 1.719 + test = test.copy() # don't overwrite 1.720 + 1.721 + path = test['name'] 1.722 + if not os.path.isabs(path): 1.723 + path = test['path'] 1.724 + if self.rootdir: 1.725 + path = relpath(test['path'], self.rootdir) 1.726 + path = denormalize_path(path) 1.727 + print >> fp, '[%s]' % path 1.728 + 1.729 + # reserved keywords: 1.730 + reserved = ['path', 'name', 'here', 'manifest', 'relpath'] 1.731 + for key in sorted(test.keys()): 1.732 + if key in reserved: 1.733 + continue 1.734 + if key in global_kwargs: 1.735 + continue 1.736 + if key in global_tags and not test[key]: 1.737 + continue 1.738 + print >> fp, '%s = %s' % (key, test[key]) 1.739 + print >> fp 1.740 + 1.741 + if close: 1.742 + # close the created file 1.743 + fp.close() 1.744 + 1.745 + def __str__(self): 1.746 + fp = StringIO() 1.747 + self.write(fp=fp) 1.748 + value = fp.getvalue() 1.749 + return value 1.750 + 1.751 + def copy(self, directory, rootdir=None, *tags, **kwargs): 1.752 + """ 1.753 + copy the manifests and associated tests 1.754 + - directory : directory to copy to 1.755 + - rootdir : root directory to copy to (if not given from manifests) 1.756 + - tags : keywords the tests must have 1.757 + - kwargs : key, values the tests must match 1.758 + """ 1.759 + # XXX note that copy does *not* filter the tests out of the 1.760 + # resulting manifest; it just stupidly copies them over. 1.761 + # ideally, it would reread the manifests and filter out the 1.762 + # tests that don't match *tags and **kwargs 1.763 + 1.764 + # destination 1.765 + if not os.path.exists(directory): 1.766 + os.path.makedirs(directory) 1.767 + else: 1.768 + # sanity check 1.769 + assert os.path.isdir(directory) 1.770 + 1.771 + # tests to copy 1.772 + tests = self.get(tags=tags, **kwargs) 1.773 + if not tests: 1.774 + return # nothing to do! 1.775 + 1.776 + # root directory 1.777 + if rootdir is None: 1.778 + rootdir = self.rootdir 1.779 + 1.780 + # copy the manifests + tests 1.781 + manifests = [relpath(manifest, rootdir) for manifest in self.manifests()] 1.782 + for manifest in manifests: 1.783 + destination = os.path.join(directory, manifest) 1.784 + dirname = os.path.dirname(destination) 1.785 + if not os.path.exists(dirname): 1.786 + os.makedirs(dirname) 1.787 + else: 1.788 + # sanity check 1.789 + assert os.path.isdir(dirname) 1.790 + shutil.copy(os.path.join(rootdir, manifest), destination) 1.791 + for test in tests: 1.792 + if os.path.isabs(test['name']): 1.793 + continue 1.794 + source = test['path'] 1.795 + if not os.path.exists(source): 1.796 + print >> sys.stderr, "Missing test: '%s' does not exist!" % source 1.797 + continue 1.798 + # TODO: should err on strict 1.799 + destination = os.path.join(directory, relpath(test['path'], rootdir)) 1.800 + shutil.copy(source, destination) 1.801 + # TODO: ensure that all of the tests are below the from_dir 1.802 + 1.803 + def update(self, from_dir, rootdir=None, *tags, **kwargs): 1.804 + """ 1.805 + update the tests as listed in a manifest from a directory 1.806 + - from_dir : directory where the tests live 1.807 + - rootdir : root directory to copy to (if not given from manifests) 1.808 + - tags : keys the tests must have 1.809 + - kwargs : key, values the tests must match 1.810 + """ 1.811 + 1.812 + # get the tests 1.813 + tests = self.get(tags=tags, **kwargs) 1.814 + 1.815 + # get the root directory 1.816 + if not rootdir: 1.817 + rootdir = self.rootdir 1.818 + 1.819 + # copy them! 1.820 + for test in tests: 1.821 + if not os.path.isabs(test['name']): 1.822 + _relpath = relpath(test['path'], rootdir) 1.823 + source = os.path.join(from_dir, _relpath) 1.824 + if not os.path.exists(source): 1.825 + # TODO err on strict 1.826 + print >> sys.stderr, "Missing test: '%s'; skipping" % test['name'] 1.827 + continue 1.828 + destination = os.path.join(rootdir, _relpath) 1.829 + shutil.copy(source, destination) 1.830 + 1.831 + ### directory importers 1.832 + 1.833 + @classmethod 1.834 + def _walk_directories(cls, directories, function, pattern=None, ignore=()): 1.835 + """ 1.836 + internal function to import directories 1.837 + """ 1.838 + 1.839 + class FilteredDirectoryContents(object): 1.840 + """class to filter directory contents""" 1.841 + 1.842 + sort = sorted 1.843 + 1.844 + def __init__(self, pattern=pattern, ignore=ignore, cache=None): 1.845 + if pattern is None: 1.846 + pattern = set() 1.847 + if isinstance(pattern, basestring): 1.848 + pattern = [pattern] 1.849 + self.patterns = pattern 1.850 + self.ignore = set(ignore) 1.851 + 1.852 + # cache of (dirnames, filenames) keyed on directory real path 1.853 + # assumes volume is frozen throughout scope 1.854 + self._cache = cache or {} 1.855 + 1.856 + def __call__(self, directory): 1.857 + """returns 2-tuple: dirnames, filenames""" 1.858 + directory = os.path.realpath(directory) 1.859 + if directory not in self._cache: 1.860 + dirnames, filenames = self.contents(directory) 1.861 + 1.862 + # filter out directories without progeny 1.863 + # XXX recursive: should keep track of seen directories 1.864 + dirnames = [ dirname for dirname in dirnames 1.865 + if not self.empty(os.path.join(directory, dirname)) ] 1.866 + 1.867 + self._cache[directory] = (tuple(dirnames), filenames) 1.868 + 1.869 + # return cached values 1.870 + return self._cache[directory] 1.871 + 1.872 + def empty(self, directory): 1.873 + """ 1.874 + returns if a directory and its descendents are empty 1.875 + """ 1.876 + return self(directory) == ((), ()) 1.877 + 1.878 + def contents(self, directory, sort=None): 1.879 + """ 1.880 + return directory contents as (dirnames, filenames) 1.881 + with `ignore` and `pattern` applied 1.882 + """ 1.883 + 1.884 + if sort is None: 1.885 + sort = self.sort 1.886 + 1.887 + # split directories and files 1.888 + dirnames = [] 1.889 + filenames = [] 1.890 + for item in os.listdir(directory): 1.891 + path = os.path.join(directory, item) 1.892 + if os.path.isdir(path): 1.893 + dirnames.append(item) 1.894 + else: 1.895 + # XXX not sure what to do if neither a file or directory 1.896 + # (if anything) 1.897 + assert os.path.isfile(path) 1.898 + filenames.append(item) 1.899 + 1.900 + # filter contents; 1.901 + # this could be done in situ re the above for loop 1.902 + # but it is really disparate in intent 1.903 + # and could conceivably go to a separate method 1.904 + dirnames = [dirname for dirname in dirnames 1.905 + if dirname not in self.ignore] 1.906 + filenames = set(filenames) 1.907 + # we use set functionality to filter filenames 1.908 + if self.patterns: 1.909 + matches = set() 1.910 + matches.update(*[fnmatch.filter(filenames, pattern) 1.911 + for pattern in self.patterns]) 1.912 + filenames = matches 1.913 + 1.914 + if sort is not None: 1.915 + # sort dirnames, filenames 1.916 + dirnames = sort(dirnames) 1.917 + filenames = sort(filenames) 1.918 + 1.919 + return (tuple(dirnames), tuple(filenames)) 1.920 + 1.921 + # make a filtered directory object 1.922 + directory_contents = FilteredDirectoryContents(pattern=pattern, ignore=ignore) 1.923 + 1.924 + # walk the directories, generating manifests 1.925 + for index, directory in enumerate(directories): 1.926 + 1.927 + for dirpath, dirnames, filenames in os.walk(directory): 1.928 + 1.929 + # get the directory contents from the caching object 1.930 + _dirnames, filenames = directory_contents(dirpath) 1.931 + # filter out directory names 1.932 + dirnames[:] = _dirnames 1.933 + 1.934 + # call callback function 1.935 + function(directory, dirpath, dirnames, filenames) 1.936 + 1.937 + @classmethod 1.938 + def populate_directory_manifests(cls, directories, filename, pattern=None, ignore=(), overwrite=False): 1.939 + """ 1.940 + walks directories and writes manifests of name `filename` in-place; returns `cls` instance populated 1.941 + with the given manifests 1.942 + 1.943 + filename -- filename of manifests to write 1.944 + pattern -- shell pattern (glob) or patterns of filenames to match 1.945 + ignore -- directory names to ignore 1.946 + overwrite -- whether to overwrite existing files of given name 1.947 + """ 1.948 + 1.949 + manifest_dict = {} 1.950 + seen = [] # top-level directories seen 1.951 + 1.952 + if os.path.basename(filename) != filename: 1.953 + raise IOError("filename should not include directory name") 1.954 + 1.955 + # no need to hit directories more than once 1.956 + _directories = directories 1.957 + directories = [] 1.958 + for directory in _directories: 1.959 + if directory not in directories: 1.960 + directories.append(directory) 1.961 + 1.962 + def callback(directory, dirpath, dirnames, filenames): 1.963 + """write a manifest for each directory""" 1.964 + 1.965 + manifest_path = os.path.join(dirpath, filename) 1.966 + if (dirnames or filenames) and not (os.path.exists(manifest_path) and overwrite): 1.967 + with file(manifest_path, 'w') as manifest: 1.968 + for dirname in dirnames: 1.969 + print >> manifest, '[include:%s]' % os.path.join(dirname, filename) 1.970 + for _filename in filenames: 1.971 + print >> manifest, '[%s]' % _filename 1.972 + 1.973 + # add to list of manifests 1.974 + manifest_dict.setdefault(directory, manifest_path) 1.975 + 1.976 + # walk the directories to gather files 1.977 + cls._walk_directories(directories, callback, pattern=pattern, ignore=ignore) 1.978 + # get manifests 1.979 + manifests = [manifest_dict[directory] for directory in _directories] 1.980 + 1.981 + # create a `cls` instance with the manifests 1.982 + return cls(manifests=manifests) 1.983 + 1.984 + @classmethod 1.985 + def from_directories(cls, directories, pattern=None, ignore=(), write=None, relative_to=None): 1.986 + """ 1.987 + convert directories to a simple manifest; returns ManifestParser instance 1.988 + 1.989 + pattern -- shell pattern (glob) or patterns of filenames to match 1.990 + ignore -- directory names to ignore 1.991 + write -- filename or file-like object of manifests to write; 1.992 + if `None` then a StringIO instance will be created 1.993 + relative_to -- write paths relative to this path; 1.994 + if false then the paths are absolute 1.995 + """ 1.996 + 1.997 + 1.998 + # determine output 1.999 + opened_manifest_file = None # name of opened manifest file 1.1000 + absolute = not relative_to # whether to output absolute path names as names 1.1001 + if isinstance(write, string): 1.1002 + opened_manifest_file = write 1.1003 + write = file(write, 'w') 1.1004 + if write is None: 1.1005 + write = StringIO() 1.1006 + 1.1007 + # walk the directories, generating manifests 1.1008 + def callback(directory, dirpath, dirnames, filenames): 1.1009 + 1.1010 + # absolute paths 1.1011 + filenames = [os.path.join(dirpath, filename) 1.1012 + for filename in filenames] 1.1013 + # ensure new manifest isn't added 1.1014 + filenames = [filename for filename in filenames 1.1015 + if filename != opened_manifest_file] 1.1016 + # normalize paths 1.1017 + if not absolute and relative_to: 1.1018 + filenames = [relpath(filename, relative_to) 1.1019 + for filename in filenames] 1.1020 + 1.1021 + # write to manifest 1.1022 + print >> write, '\n'.join(['[%s]' % denormalize_path(filename) 1.1023 + for filename in filenames]) 1.1024 + 1.1025 + 1.1026 + cls._walk_directories(directories, callback, pattern=pattern, ignore=ignore) 1.1027 + 1.1028 + if opened_manifest_file: 1.1029 + # close file 1.1030 + write.close() 1.1031 + manifests = [opened_manifest_file] 1.1032 + else: 1.1033 + # manifests/write is a file-like object; 1.1034 + # rewind buffer 1.1035 + write.flush() 1.1036 + write.seek(0) 1.1037 + manifests = [write] 1.1038 + 1.1039 + 1.1040 + # make a ManifestParser instance 1.1041 + return cls(manifests=manifests) 1.1042 + 1.1043 +convert = ManifestParser.from_directories 1.1044 + 1.1045 + 1.1046 +class TestManifest(ManifestParser): 1.1047 + """ 1.1048 + apply logic to manifests; this is your integration layer :) 1.1049 + specific harnesses may subclass from this if they need more logic 1.1050 + """ 1.1051 + 1.1052 + def filter(self, values, tests): 1.1053 + """ 1.1054 + filter on a specific list tag, e.g.: 1.1055 + run-if = os == win linux 1.1056 + skip-if = os == mac 1.1057 + """ 1.1058 + 1.1059 + # tags: 1.1060 + run_tag = 'run-if' 1.1061 + skip_tag = 'skip-if' 1.1062 + fail_tag = 'fail-if' 1.1063 + 1.1064 + # loop over test 1.1065 + for test in tests: 1.1066 + reason = None # reason to disable 1.1067 + 1.1068 + # tagged-values to run 1.1069 + if run_tag in test: 1.1070 + condition = test[run_tag] 1.1071 + if not parse(condition, **values): 1.1072 + reason = '%s: %s' % (run_tag, condition) 1.1073 + 1.1074 + # tagged-values to skip 1.1075 + if skip_tag in test: 1.1076 + condition = test[skip_tag] 1.1077 + if parse(condition, **values): 1.1078 + reason = '%s: %s' % (skip_tag, condition) 1.1079 + 1.1080 + # mark test as disabled if there's a reason 1.1081 + if reason: 1.1082 + test.setdefault('disabled', reason) 1.1083 + 1.1084 + # mark test as a fail if so indicated 1.1085 + if fail_tag in test: 1.1086 + condition = test[fail_tag] 1.1087 + if parse(condition, **values): 1.1088 + test['expected'] = 'fail' 1.1089 + 1.1090 + def active_tests(self, exists=True, disabled=True, options=None, **values): 1.1091 + """ 1.1092 + - exists : return only existing tests 1.1093 + - disabled : whether to return disabled tests 1.1094 + - tags : keys and values to filter on (e.g. `os = linux mac`) 1.1095 + """ 1.1096 + tests = [i.copy() for i in self.tests] # shallow copy 1.1097 + 1.1098 + # Filter on current subsuite 1.1099 + if options: 1.1100 + if options.subsuite: 1.1101 + tests = [test for test in tests if options.subsuite == test['subsuite']] 1.1102 + else: 1.1103 + tests = [test for test in tests if not test['subsuite']] 1.1104 + 1.1105 + # mark all tests as passing unless indicated otherwise 1.1106 + for test in tests: 1.1107 + test['expected'] = test.get('expected', 'pass') 1.1108 + 1.1109 + # ignore tests that do not exist 1.1110 + if exists: 1.1111 + tests = [test for test in tests if os.path.exists(test['path'])] 1.1112 + 1.1113 + # filter by tags 1.1114 + self.filter(values, tests) 1.1115 + 1.1116 + # ignore disabled tests if specified 1.1117 + if not disabled: 1.1118 + tests = [test for test in tests 1.1119 + if not 'disabled' in test] 1.1120 + 1.1121 + # return active tests 1.1122 + return tests 1.1123 + 1.1124 + def test_paths(self): 1.1125 + return [test['path'] for test in self.active_tests()] 1.1126 + 1.1127 + 1.1128 +### command line attributes 1.1129 + 1.1130 +class ParserError(Exception): 1.1131 + """error for exceptions while parsing the command line""" 1.1132 + 1.1133 +def parse_args(_args): 1.1134 + """ 1.1135 + parse and return: 1.1136 + --keys=value (or --key value) 1.1137 + -tags 1.1138 + args 1.1139 + """ 1.1140 + 1.1141 + # return values 1.1142 + _dict = {} 1.1143 + tags = [] 1.1144 + args = [] 1.1145 + 1.1146 + # parse the arguments 1.1147 + key = None 1.1148 + for arg in _args: 1.1149 + if arg.startswith('---'): 1.1150 + raise ParserError("arguments should start with '-' or '--' only") 1.1151 + elif arg.startswith('--'): 1.1152 + if key: 1.1153 + raise ParserError("Key %s still open" % key) 1.1154 + key = arg[2:] 1.1155 + if '=' in key: 1.1156 + key, value = key.split('=', 1) 1.1157 + _dict[key] = value 1.1158 + key = None 1.1159 + continue 1.1160 + elif arg.startswith('-'): 1.1161 + if key: 1.1162 + raise ParserError("Key %s still open" % key) 1.1163 + tags.append(arg[1:]) 1.1164 + continue 1.1165 + else: 1.1166 + if key: 1.1167 + _dict[key] = arg 1.1168 + continue 1.1169 + args.append(arg) 1.1170 + 1.1171 + # return values 1.1172 + return (_dict, tags, args) 1.1173 + 1.1174 + 1.1175 +### classes for subcommands 1.1176 + 1.1177 +class CLICommand(object): 1.1178 + usage = '%prog [options] command' 1.1179 + def __init__(self, parser): 1.1180 + self._parser = parser # master parser 1.1181 + def parser(self): 1.1182 + return OptionParser(usage=self.usage, description=self.__doc__, 1.1183 + add_help_option=False) 1.1184 + 1.1185 +class Copy(CLICommand): 1.1186 + usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...' 1.1187 + def __call__(self, options, args): 1.1188 + # parse the arguments 1.1189 + try: 1.1190 + kwargs, tags, args = parse_args(args) 1.1191 + except ParserError, e: 1.1192 + self._parser.error(e.message) 1.1193 + 1.1194 + # make sure we have some manifests, otherwise it will 1.1195 + # be quite boring 1.1196 + if not len(args) == 2: 1.1197 + HelpCLI(self._parser)(options, ['copy']) 1.1198 + return 1.1199 + 1.1200 + # read the manifests 1.1201 + # TODO: should probably ensure these exist here 1.1202 + manifests = ManifestParser() 1.1203 + manifests.read(args[0]) 1.1204 + 1.1205 + # print the resultant query 1.1206 + manifests.copy(args[1], None, *tags, **kwargs) 1.1207 + 1.1208 + 1.1209 +class CreateCLI(CLICommand): 1.1210 + """ 1.1211 + create a manifest from a list of directories 1.1212 + """ 1.1213 + usage = '%prog [options] create directory <directory> <...>' 1.1214 + 1.1215 + def parser(self): 1.1216 + parser = CLICommand.parser(self) 1.1217 + parser.add_option('-p', '--pattern', dest='pattern', 1.1218 + help="glob pattern for files") 1.1219 + parser.add_option('-i', '--ignore', dest='ignore', 1.1220 + default=[], action='append', 1.1221 + help='directories to ignore') 1.1222 + parser.add_option('-w', '--in-place', dest='in_place', 1.1223 + help='Write .ini files in place; filename to write to') 1.1224 + return parser 1.1225 + 1.1226 + def __call__(self, _options, args): 1.1227 + parser = self.parser() 1.1228 + options, args = parser.parse_args(args) 1.1229 + 1.1230 + # need some directories 1.1231 + if not len(args): 1.1232 + parser.print_usage() 1.1233 + return 1.1234 + 1.1235 + # add the directories to the manifest 1.1236 + for arg in args: 1.1237 + assert os.path.exists(arg) 1.1238 + assert os.path.isdir(arg) 1.1239 + manifest = convert(args, pattern=options.pattern, ignore=options.ignore, 1.1240 + write=options.in_place) 1.1241 + if manifest: 1.1242 + print manifest 1.1243 + 1.1244 + 1.1245 +class WriteCLI(CLICommand): 1.1246 + """ 1.1247 + write a manifest based on a query 1.1248 + """ 1.1249 + usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...' 1.1250 + def __call__(self, options, args): 1.1251 + 1.1252 + # parse the arguments 1.1253 + try: 1.1254 + kwargs, tags, args = parse_args(args) 1.1255 + except ParserError, e: 1.1256 + self._parser.error(e.message) 1.1257 + 1.1258 + # make sure we have some manifests, otherwise it will 1.1259 + # be quite boring 1.1260 + if not args: 1.1261 + HelpCLI(self._parser)(options, ['write']) 1.1262 + return 1.1263 + 1.1264 + # read the manifests 1.1265 + # TODO: should probably ensure these exist here 1.1266 + manifests = ManifestParser() 1.1267 + manifests.read(*args) 1.1268 + 1.1269 + # print the resultant query 1.1270 + manifests.write(global_tags=tags, global_kwargs=kwargs) 1.1271 + 1.1272 + 1.1273 +class HelpCLI(CLICommand): 1.1274 + """ 1.1275 + get help on a command 1.1276 + """ 1.1277 + usage = '%prog [options] help [command]' 1.1278 + 1.1279 + def __call__(self, options, args): 1.1280 + if len(args) == 1 and args[0] in commands: 1.1281 + commands[args[0]](self._parser).parser().print_help() 1.1282 + else: 1.1283 + self._parser.print_help() 1.1284 + print '\nCommands:' 1.1285 + for command in sorted(commands): 1.1286 + print ' %s : %s' % (command, commands[command].__doc__.strip()) 1.1287 + 1.1288 +class UpdateCLI(CLICommand): 1.1289 + """ 1.1290 + update the tests as listed in a manifest from a directory 1.1291 + """ 1.1292 + usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...' 1.1293 + 1.1294 + def __call__(self, options, args): 1.1295 + # parse the arguments 1.1296 + try: 1.1297 + kwargs, tags, args = parse_args(args) 1.1298 + except ParserError, e: 1.1299 + self._parser.error(e.message) 1.1300 + 1.1301 + # make sure we have some manifests, otherwise it will 1.1302 + # be quite boring 1.1303 + if not len(args) == 2: 1.1304 + HelpCLI(self._parser)(options, ['update']) 1.1305 + return 1.1306 + 1.1307 + # read the manifests 1.1308 + # TODO: should probably ensure these exist here 1.1309 + manifests = ManifestParser() 1.1310 + manifests.read(args[0]) 1.1311 + 1.1312 + # print the resultant query 1.1313 + manifests.update(args[1], None, *tags, **kwargs) 1.1314 + 1.1315 + 1.1316 +# command -> class mapping 1.1317 +commands = { 'create': CreateCLI, 1.1318 + 'help': HelpCLI, 1.1319 + 'update': UpdateCLI, 1.1320 + 'write': WriteCLI } 1.1321 + 1.1322 +def main(args=sys.argv[1:]): 1.1323 + """console_script entry point""" 1.1324 + 1.1325 + # set up an option parser 1.1326 + usage = '%prog [options] [command] ...' 1.1327 + description = "%s. Use `help` to display commands" % __doc__.strip() 1.1328 + parser = OptionParser(usage=usage, description=description) 1.1329 + parser.add_option('-s', '--strict', dest='strict', 1.1330 + action='store_true', default=False, 1.1331 + help='adhere strictly to errors') 1.1332 + parser.disable_interspersed_args() 1.1333 + 1.1334 + options, args = parser.parse_args(args) 1.1335 + 1.1336 + if not args: 1.1337 + HelpCLI(parser)(options, args) 1.1338 + parser.exit() 1.1339 + 1.1340 + # get the command 1.1341 + command = args[0] 1.1342 + if command not in commands: 1.1343 + parser.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands.keys())), command)) 1.1344 + 1.1345 + handler = commands[command](parser) 1.1346 + handler(options, args[1:]) 1.1347 + 1.1348 +if __name__ == '__main__': 1.1349 + main()