3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 Mozilla universal manifest parser
12 # http://hg.mozilla.org/automation/ManifestDestiny/raw-file/tip/manifestparser.py
14 __all__
= ['read_ini', # .ini reader
15 'ManifestParser', 'TestManifest', 'convert', # manifest handling
16 'parse', 'ParseError', 'ExpressionParser'] # conditional expression parser
22 from fnmatch
import fnmatch
23 from optparse
import OptionParser
25 version
= '0.5.3' # package version
27 from setuptools
import setup
31 # we need relpath, but it is introduced in python 2.6
32 # http://docs.python.org/library/os.path.html
34 relpath
= os
.path
.relpath
35 except AttributeError:
36 def relpath(path
, start
):
38 Return a relative version of a path
39 from /usr/lib/python2.6/posixpath.py
43 raise ValueError("no path specified")
45 start_list
= os
.path
.abspath(start
).split(os
.path
.sep
)
46 path_list
= os
.path
.abspath(path
).split(os
.path
.sep
)
48 # Work out how much of the filepath is shared by start and path.
49 i
= len(os
.path
.commonprefix([start_list
, path_list
]))
51 rel_list
= [os
.path
.pardir
] * (len(start_list
)-i
) + path_list
[i
:]
54 return os
.path
.join(*rel_list
)
58 # http://k0s.org/mozilla/hg/expressionparser
59 # http://hg.mozilla.org/users/tmielczarek_mozilla.com/expressionparser
61 # Implements a top-down parser/evaluator for simple boolean expressions.
62 # ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm
78 # IDENT := [A-Za-z_]\w*
80 # Identifiers take their values from a mapping dictionary passed as the second
83 # Glossary (see above URL for details):
84 # - nud: null denotation
85 # - led: left detonation
86 # - lbp: left binding power
87 # - rbp: right binding power
89 class ident_token(object):
90 def __init__(self
, value
):
92 def nud(self
, parser
):
93 # identifiers take their value from the value mappings passed
95 return parser
.value(self
.value
)
97 class literal_token(object):
98 def __init__(self
, value
):
100 def nud(self
, parser
):
103 class eq_op_token(object):
105 def led(self
, parser
, left
):
106 return left
== parser
.expression(self
.lbp
)
108 class neq_op_token(object):
110 def led(self
, parser
, left
):
111 return left
!= parser
.expression(self
.lbp
)
113 class not_op_token(object):
115 def nud(self
, parser
):
116 return not parser
.expression()
118 class and_op_token(object):
120 def led(self
, parser
, left
):
121 right
= parser
.expression(self
.lbp
)
122 return left
and right
124 class or_op_token(object):
126 def led(self
, parser
, left
):
127 right
= parser
.expression(self
.lbp
)
130 class lparen_token(object):
132 def nud(self
, parser
):
133 expr
= parser
.expression()
134 parser
.advance(rparen_token
)
137 class rparen_token(object):
140 class end_token(object):
141 """always ends parsing"""
143 ### derived literal tokens
145 class bool_token(literal_token
):
146 def __init__(self
, value
):
147 value
= {'true':True, 'false':False}[value
]
148 literal_token
.__init
__(self
, value
)
150 class int_token(literal_token
):
151 def __init__(self
, value
):
152 literal_token
.__init
__(self
, int(value
))
154 class string_token(literal_token
):
155 def __init__(self
, value
):
156 literal_token
.__init
__(self
, value
[1:-1])
158 precedence
= [(end_token
, rparen_token
),
161 (eq_op_token
, neq_op_token
),
164 for index
, rank
in enumerate(precedence
):
166 token
.lbp
= index
# lbp = lowest left binding power
168 class ParseError(Exception):
169 """errror parsing conditional expression"""
171 class ExpressionParser(object):
172 def __init__(self
, text
, valuemapping
, strict
=False):
174 Initialize the parser with input |text|, and |valuemapping| as
175 a dict mapping identifier names to values.
178 self
.valuemapping
= valuemapping
183 Lex the input text into tokens and yield them in sequence.
186 def bool_(scanner
, t
): return bool_token(t
)
187 def identifier(scanner
, t
): return ident_token(t
)
188 def integer(scanner
, t
): return int_token(t
)
189 def eq(scanner
, t
): return eq_op_token()
190 def neq(scanner
, t
): return neq_op_token()
191 def or_(scanner
, t
): return or_op_token()
192 def and_(scanner
, t
): return and_op_token()
193 def lparen(scanner
, t
): return lparen_token()
194 def rparen(scanner
, t
): return rparen_token()
195 def string_(scanner
, t
): return string_token(t
)
196 def not_(scanner
, t
): return not_op_token()
198 scanner
= re
.Scanner([
199 (r
"true|false", bool_
),
200 (r
"[a-zA-Z_]\w*", identifier
),
201 (r
"[0-9]+", integer
),
202 (r
'("[^"]*")|(\'[^
\']*\')', string_),
210 (r"\s+", None), # skip whitespace
212 tokens, remainder = scanner.scan(self.text)
217 def value(self, ident):
219 Look up the value of |ident| in the value mapping passed in the
223 return self.valuemapping[ident]
225 return self.valuemapping.get(ident, None)
227 def advance(self, expected):
229 Assert that the next token is an instance of |expected|, and advance
232 if not isinstance(self.token, expected):
233 raise Exception, "Unexpected token!"
234 self.token = self.iter.next()
236 def expression(self, rbp=0):
238 Parse and return the value of an expression until a token with
239 right binding power greater than rbp is encountered.
242 self.token = self.iter.next()
244 while rbp < self.token.lbp:
246 self.token = self.iter.next()
247 left = t.led(self, left)
252 Parse and return the value of the expression in the text
253 passed to the constructor. Raises a ParseError if the expression
257 self.iter = self._tokenize()
258 self.token = self.iter.next()
259 return self.expression()
261 raise ParseError("could not parse: %s; variables: %s" % (self.text, self.valuemapping))
265 def parse(text, **values):
267 Parse and evaluate a boolean expression in |text|. Use |values| to look
268 up the value of identifiers referenced in the expression. Returns the final
269 value of the expression. A ParseError will be raised if parsing fails.
271 return ExpressionParser(text, values).parse()
273 def normalize_path(path):
274 """normalize a relative path"""
275 if sys.platform.startswith('win
'):
276 return path.replace('/', os.path.sep)
279 def denormalize_path(path):
280 """denormalize a relative path"""
281 if sys.platform.startswith('win
'):
282 return path.replace(os.path.sep, '/')
286 def read_ini(fp, variables=None, default='DEFAULT
',
287 comments=';#', separators=('=', ':'),
290 read an .ini file and return a list of [(section, values)]
291 - fp : file pointer or path to read
292 - variables : default set of variables
293 - default : name of the section for the default section
294 - comments : characters that if they start a line denote a comment
295 - separators : strings that denote key, value separation in order
296 - strict : whether to be strict about parsing
299 if variables
is None:
302 if isinstance(fp
, basestring
):
307 section_names
= set([])
310 for line
in fp
.readlines():
312 stripped
= line
.strip()
316 # reset key and value to avoid continuation lines
320 # ignore comment lines
321 if stripped
[0] in comments
:
324 # check for a new section
325 if len(stripped
) > 2 and stripped
[0] == '[' and stripped
[-1] == ']':
326 section
= stripped
[1:-1].strip()
329 # deal with DEFAULT section
330 if section
.lower() == default
.lower():
332 assert default
not in section_names
333 section_names
.add(default
)
334 current_section
= variables
338 # make sure this section doesn't already exist
339 assert section
not in section_names
341 section_names
.add(section
)
343 sections
.append((section
, current_section
))
346 # if there aren't any sections yet, something bad happen
347 if not section_names
:
348 raise Exception('No sections found')
351 for separator
in separators
:
352 if separator
in stripped
:
353 key
, value
= stripped
.split(separator
, 1)
355 value
= value
.strip()
358 # make sure this key isn't already in the section or empty
360 if current_section
is not variables
:
361 assert key
not in current_section
363 current_section
[key
] = value
366 # continuation line ?
367 if line
[0].isspace() and key
:
368 value
= '%s%s%s' % (value
, os
.linesep
, stripped
)
369 current_section
[key
] = value
371 # something bad happen!
372 raise Exception("Not sure what you're trying to do")
374 # interpret the variables
375 def interpret_variables(global_dict
, local_dict
):
376 variables
= global_dict
.copy()
377 variables
.update(local_dict
)
380 sections
= [(i
, interpret_variables(variables
, j
)) for i
, j
in sections
]
384 ### objects for parsing manifests
386 class ManifestParser(object):
387 """read .ini manifests"""
389 ### methods for reading manifests
391 def __init__(self
, manifests
=(), defaults
=None, strict
=True):
392 self
._defaults
= defaults
or {}
396 self
.relativeRoot
= None
398 self
.read(*manifests
)
400 def getRelativeRoot(self
, root
):
403 def read(self
, *filenames
, **defaults
):
405 # ensure all files exist
406 missing
= [ filename
for filename
in filenames
407 if not os
.path
.exists(filename
) ]
409 raise IOError('Missing files: %s' % ', '.join(missing
))
412 for filename
in filenames
:
414 # set the per file defaults
415 defaults
= defaults
.copy() or self
._defaults
.copy()
416 here
= os
.path
.dirname(os
.path
.abspath(filename
))
417 defaults
['here'] = here
419 if self
.rootdir
is None:
420 # set the root directory
421 # == the directory of the first manifest given
424 # read the configuration
425 sections
= read_ini(fp
=filename
, variables
=defaults
, strict
=self
.strict
)
428 for section
, data
in sections
:
431 # TODO: keep track of included file structure:
432 # self.manifests = {'manifest.ini': 'relative/path.ini'}
433 if section
.startswith('include:'):
434 include_file
= section
.split('include:', 1)[-1]
435 include_file
= normalize_path(include_file
)
436 if not os
.path
.isabs(include_file
):
437 include_file
= os
.path
.join(self
.getRelativeRoot(here
), include_file
)
438 if not os
.path
.exists(include_file
):
440 raise IOError("File '%s' does not exist" % include_file
)
443 include_defaults
= data
.copy()
444 self
.read(include_file
, **include_defaults
)
449 test
['name'] = section
450 test
['manifest'] = os
.path
.abspath(filename
)
453 path
= test
.get('path', section
)
454 if '://' not in path
: # don't futz with URLs
455 path
= normalize_path(path
)
456 if not os
.path
.isabs(path
):
457 path
= os
.path
.join(here
, path
)
461 self
.tests
.append(test
)
463 ### methods for querying manifests
465 def query(self
, *checks
, **kw
):
467 general query function for tests
468 - checks : callable conditions to test if the test fulfills the query
470 tests
= kw
.get('tests', None)
482 def get(self
, _key
=None, inverse
=False, tags
=None, tests
=None, **kwargs
):
483 # TODO: pass a dict instead of kwargs since you might hav
484 # e.g. 'inverse' as a key in the dict
486 # TODO: tags should just be part of kwargs with None values
487 # (None == any is kinda weird, but probably still better)
495 # make some check functions
497 has_tags
= lambda test
: not tags
.intersection(test
.keys())
498 def dict_query(test
):
499 for key
, value
in kwargs
.items():
500 if test
.get(key
) == value
:
504 has_tags
= lambda test
: tags
.issubset(test
.keys())
505 def dict_query(test
):
506 for key
, value
in kwargs
.items():
507 if test
.get(key
) != value
:
512 tests
= self
.query(has_tags
, dict_query
, tests
=tests
)
514 # if a key is given, return only a list of that key
515 # useful for keys like 'name' or 'path'
517 return [test
[_key
] for test
in tests
]
522 def missing(self
, tests
=None):
523 """return list of tests that do not exist on the filesystem"""
526 return [test
for test
in tests
527 if not os
.path
.exists(test
['path'])]
529 def manifests(self
, tests
=None):
531 return manifests in order in which they appear in the tests
537 manifest
= test
.get('manifest')
540 if manifest
not in manifests
:
541 manifests
.append(manifest
)
544 ### methods for outputting from manifests
546 def write(self
, fp
=sys
.stdout
, rootdir
=None,
547 global_tags
=None, global_kwargs
=None,
548 local_tags
=None, local_kwargs
=None):
550 write a manifest given a query
551 global and local options will be munged to do the query
552 globals will be written to the top of the file
553 locals (if given) will be written per test
558 rootdir
= self
.rootdir
561 global_tags
= global_tags
or set()
562 local_tags
= local_tags
or set()
563 global_kwargs
= global_kwargs
or {}
564 local_kwargs
= local_kwargs
or {}
568 tags
.update(global_tags
)
569 tags
.update(local_tags
)
571 kwargs
.update(global_kwargs
)
572 kwargs
.update(local_kwargs
)
575 tests
= self
.get(tags
=tags
, **kwargs
)
577 # print the .ini manifest
578 if global_tags
or global_kwargs
:
579 print >> fp
, '[DEFAULT]'
580 for tag
in global_tags
:
581 print >> fp
, '%s =' % tag
582 for key
, value
in global_kwargs
.items():
583 print >> fp
, '%s = %s' % (key
, value
)
587 test
= test
.copy() # don't overwrite
590 if not os
.path
.isabs(path
):
591 path
= denormalize_path(relpath(test
['path'], self
.rootdir
))
592 print >> fp
, '[%s]' % path
595 reserved
= ['path', 'name', 'here', 'manifest']
596 for key
in sorted(test
.keys()):
599 if key
in global_kwargs
:
601 if key
in global_tags
and not test
[key
]:
603 print >> fp
, '%s = %s' % (key
, test
[key
])
606 def copy(self
, directory
, rootdir
=None, *tags
, **kwargs
):
608 copy the manifests and associated tests
609 - directory : directory to copy to
610 - rootdir : root directory to copy to (if not given from manifests)
611 - tags : keywords the tests must have
612 - kwargs : key, values the tests must match
614 # XXX note that copy does *not* filter the tests out of the
615 # resulting manifest; it just stupidly copies them over.
616 # ideally, it would reread the manifests and filter out the
617 # tests that don't match *tags and **kwargs
620 if not os
.path
.exists(directory
):
621 os
.path
.makedirs(directory
)
624 assert os
.path
.isdir(directory
)
627 tests
= self
.get(tags
=tags
, **kwargs
)
629 return # nothing to do!
633 rootdir
= self
.rootdir
635 # copy the manifests + tests
636 manifests
= [relpath(manifest
, rootdir
) for manifest
in self
.manifests()]
637 for manifest
in manifests
:
638 destination
= os
.path
.join(directory
, manifest
)
639 dirname
= os
.path
.dirname(destination
)
640 if not os
.path
.exists(dirname
):
644 assert os
.path
.isdir(dirname
)
645 shutil
.copy(os
.path
.join(rootdir
, manifest
), destination
)
647 if os
.path
.isabs(test
['name']):
649 source
= test
['path']
650 if not os
.path
.exists(source
):
651 print >> sys
.stderr
, "Missing test: '%s' does not exist!" % source
653 # TODO: should err on strict
654 destination
= os
.path
.join(directory
, relpath(test
['path'], rootdir
))
655 shutil
.copy(source
, destination
)
656 # TODO: ensure that all of the tests are below the from_dir
658 def update(self
, from_dir
, rootdir
=None, *tags
, **kwargs
):
660 update the tests as listed in a manifest from a directory
661 - from_dir : directory where the tests live
662 - rootdir : root directory to copy to (if not given from manifests)
663 - tags : keys the tests must have
664 - kwargs : key, values the tests must match
668 tests
= self
.get(tags
=tags
, **kwargs
)
670 # get the root directory
672 rootdir
= self
.rootdir
676 if not os
.path
.isabs(test
['name']):
677 _relpath
= relpath(test
['path'], rootdir
)
678 source
= os
.path
.join(from_dir
, _relpath
)
679 if not os
.path
.exists(source
):
681 print >> sys
.stderr
, "Missing test: '%s'; skipping" % test
['name']
683 destination
= os
.path
.join(rootdir
, _relpath
)
684 shutil
.copy(source
, destination
)
687 class TestManifest(ManifestParser
):
689 apply logic to manifests; this is your integration layer :)
690 specific harnesses may subclass from this if they need more logic
693 def filter(self
, values
, tests
):
695 filter on a specific list tag, e.g.:
696 run-if.os = win linux
707 reason
= None # reason to disable
709 # tagged-values to run
711 condition
= test
[run_tag
]
712 if not parse(condition
, **values
):
713 reason
= '%s: %s' % (run_tag
, condition
)
715 # tagged-values to skip
717 condition
= test
[skip_tag
]
718 if parse(condition
, **values
):
719 reason
= '%s: %s' % (skip_tag
, condition
)
721 # mark test as disabled if there's a reason
723 test
.setdefault('disabled', reason
)
725 # mark test as a fail if so indicated
727 condition
= test
[fail_tag
]
728 if parse(condition
, **values
):
729 test
['expected'] = 'fail'
731 def active_tests(self
, exists
=True, disabled
=True, **values
):
733 - exists : return only existing tests
734 - disabled : whether to return disabled tests
735 - tags : keys and values to filter on (e.g. `os = linux mac`)
738 tests
= [i
.copy() for i
in self
.tests
] # shallow copy
740 # mark all tests as passing unless indicated otherwise
742 test
['expected'] = test
.get('expected', 'pass')
744 # ignore tests that do not exist
746 tests
= [test
for test
in tests
if os
.path
.exists(test
['path'])]
749 self
.filter(values
, tests
)
751 # ignore disabled tests if specified
753 tests
= [test
for test
in tests
754 if not 'disabled' in test
]
756 # return active tests
759 def test_paths(self
):
760 return [test
['path'] for test
in self
.active_tests()]
763 ### utility function(s); probably belongs elsewhere
765 def convert(directories
, pattern
=None, ignore
=(), write
=None):
767 convert directories to a simple manifest
772 for directory
in directories
:
773 for dirpath
, dirnames
, filenames
in os
.walk(directory
):
775 # filter out directory names
776 dirnames
= [ i
for i
in dirnames
if i
not in ignore
]
779 # reference only the subdirectory
781 dirpath
= dirpath
.split(directory
, 1)[-1].strip(os
.path
.sep
)
783 if dirpath
.split(os
.path
.sep
)[0] in ignore
:
788 filenames
= [filename
for filename
in filenames
789 if fnmatch(filename
, pattern
)]
793 # write a manifest for each directory
794 if write
and (dirnames
or filenames
):
795 manifest
= file(os
.path
.join(_dirpath
, write
), 'w')
796 for dirname
in dirnames
:
797 print >> manifest
, '[include:%s]' % os
.path
.join(dirname
, write
)
798 for filename
in filenames
:
799 print >> manifest
, '[%s]' % filename
803 retval
.extend([denormalize_path(os
.path
.join(dirpath
, filename
))
804 for filename
in filenames
])
807 return # the manifests have already been written!
810 retval
= ['[%s]' % filename
for filename
in retval
]
811 return '\n'.join(retval
)
813 ### command line attributes
815 class ParserError(Exception):
816 """error for exceptions while parsing the command line"""
818 def parse_args(_args
):
821 --keys=value (or --key value)
831 # parse the arguments
834 if arg
.startswith('---'):
835 raise ParserError("arguments should start with '-' or '--' only")
836 elif arg
.startswith('--'):
838 raise ParserError("Key %s still open" % key
)
841 key
, value
= key
.split('=', 1)
845 elif arg
.startswith('-'):
847 raise ParserError("Key %s still open" % key
)
857 return (_dict
, tags
, args
)
860 ### classes for subcommands
862 class CLICommand(object):
863 usage
= '%prog [options] command'
864 def __init__(self
, parser
):
865 self
._parser
= parser
# master parser
867 return OptionParser(usage
=self
.usage
, description
=self
.__doc
__,
868 add_help_option
=False)
870 class Copy(CLICommand
):
871 usage
= '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
872 def __call__(self
, options
, args
):
873 # parse the arguments
875 kwargs
, tags
, args
= parse_args(args
)
876 except ParserError
, e
:
877 self
._parser
.error(e
.message
)
879 # make sure we have some manifests, otherwise it will
881 if not len(args
) == 2:
882 HelpCLI(self
._parser
)(options
, ['copy'])
886 # TODO: should probably ensure these exist here
887 manifests
= ManifestParser()
888 manifests
.read(args
[0])
890 # print the resultant query
891 manifests
.copy(args
[1], None, *tags
, **kwargs
)
894 class CreateCLI(CLICommand
):
896 create a manifest from a list of directories
898 usage
= '%prog [options] create directory <directory> <...>'
901 parser
= CLICommand
.parser(self
)
902 parser
.add_option('-p', '--pattern', dest
='pattern',
903 help="glob pattern for files")
904 parser
.add_option('-i', '--ignore', dest
='ignore',
905 default
=[], action
='append',
906 help='directories to ignore')
907 parser
.add_option('-w', '--in-place', dest
='in_place',
908 help='Write .ini files in place; filename to write to')
911 def __call__(self
, _options
, args
):
912 parser
= self
.parser()
913 options
, args
= parser
.parse_args(args
)
915 # need some directories
920 # add the directories to the manifest
922 assert os
.path
.exists(arg
)
923 assert os
.path
.isdir(arg
)
924 manifest
= convert(args
, pattern
=options
.pattern
, ignore
=options
.ignore
,
925 write
=options
.in_place
)
930 class WriteCLI(CLICommand
):
932 write a manifest based on a query
934 usage
= '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...'
935 def __call__(self
, options
, args
):
937 # parse the arguments
939 kwargs
, tags
, args
= parse_args(args
)
940 except ParserError
, e
:
941 self
._parser
.error(e
.message
)
943 # make sure we have some manifests, otherwise it will
946 HelpCLI(self
._parser
)(options
, ['write'])
950 # TODO: should probably ensure these exist here
951 manifests
= ManifestParser()
952 manifests
.read(*args
)
954 # print the resultant query
955 manifests
.write(global_tags
=tags
, global_kwargs
=kwargs
)
958 class HelpCLI(CLICommand
):
960 get help on a command
962 usage
= '%prog [options] help [command]'
964 def __call__(self
, options
, args
):
965 if len(args
) == 1 and args
[0] in commands
:
966 commands
[args
[0]](self
._parser
).parser().print_help()
968 self
._parser
.print_help()
970 for command
in sorted(commands
):
971 print ' %s : %s' % (command
, commands
[command
].__doc
__.strip())
973 class SetupCLI(CLICommand
):
975 setup using setuptools
977 # use setup.py from the repo when you want to distribute to python!
978 # otherwise setuptools will complain that it can't find setup.py
979 # and result in a useless package
981 usage
= '%prog [options] setup [setuptools options]'
983 def __call__(self
, options
, args
):
984 sys
.argv
= [sys
.argv
[0]] + args
985 assert setup
is not None, "You must have setuptools installed to use SetupCLI"
986 here
= os
.path
.dirname(os
.path
.abspath(__file__
))
988 filename
= os
.path
.join(here
, 'README.txt')
989 description
= file(filename
).read()
994 setup(name
='ManifestDestiny',
996 description
="Universal manifests for Mozilla test harnesses",
997 long_description
=description
,
998 classifiers
=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
999 keywords
='mozilla manifests',
1000 author
='Jeff Hammel',
1001 author_email
='jhammel@mozilla.com',
1002 url
='https://wiki.mozilla.org/Auto-tools/Projects/ManifestDestiny',
1005 py_modules
=['manifestparser'],
1007 # -*- Extra requirements: -*-
1011 manifestparser = manifestparser:main
1016 class UpdateCLI(CLICommand
):
1018 update the tests as listed in a manifest from a directory
1020 usage
= '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
1022 def __call__(self
, options
, args
):
1023 # parse the arguments
1025 kwargs
, tags
, args
= parse_args(args
)
1026 except ParserError
, e
:
1027 self
._parser
.error(e
.message
)
1029 # make sure we have some manifests, otherwise it will
1031 if not len(args
) == 2:
1032 HelpCLI(self
._parser
)(options
, ['update'])
1035 # read the manifests
1036 # TODO: should probably ensure these exist here
1037 manifests
= ManifestParser()
1038 manifests
.read(args
[0])
1040 # print the resultant query
1041 manifests
.update(args
[1], None, *tags
, **kwargs
)
1044 # command -> class mapping
1045 commands
= { 'create': CreateCLI
,
1047 'update': UpdateCLI
,
1049 if setup
is not None:
1050 commands
['setup'] = SetupCLI
1052 def main(args
=sys
.argv
[1:]):
1053 """console_script entry point"""
1055 # set up an option parser
1056 usage
= '%prog [options] [command] ...'
1057 description
= __doc__
1058 parser
= OptionParser(usage
=usage
, description
=description
)
1059 parser
.add_option('-s', '--strict', dest
='strict',
1060 action
='store_true', default
=False,
1061 help='adhere strictly to errors')
1062 parser
.disable_interspersed_args()
1064 options
, args
= parser
.parse_args(args
)
1067 HelpCLI(parser
)(options
, args
)
1072 if command
not in commands
:
1073 parser
.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands
.keys())), command
))
1075 handler
= commands
[command
](parser
)
1076 handler(options
, args
[1:])
1078 if __name__
== '__main__':