2 TestCmd.py: a testing framework for commands and scripts.
4 The TestCmd module provides a framework for portable automated testing
5 of executable commands and scripts (in any language, not just Python),
6 especially commands and scripts that require file system interaction.
8 In addition to running tests and evaluating conditions, the TestCmd
9 module manages and cleans up one or more temporary workspace
10 directories, and provides methods for creating files and directories in
11 those workspace directories from in-line data, here-documents), allowing
12 tests to be completely self-contained.
14 A TestCmd environment object is created via the usual invocation:
17 test = TestCmd.TestCmd()
19 There are a bunch of keyword arguments that you can use at instantiation
22 test = TestCmd.TestCmd(description = 'string',
23 program = 'program_or_script_to_test',
24 interpreter = 'script_interpreter',
28 match = default_match_function,
31 There are a bunch of methods that let you do a bunch of different
32 things. Here is an overview of them:
36 test.description_set('string')
38 test.program_set('program_or_script_to_test')
40 test.interpreter_set('script_interpreter')
41 test.interpreter_set(['script_interpreter', 'arg'])
43 test.workdir_set('prefix')
47 test.workpath('subdir', 'file')
49 test.subdir('subdir', ...)
51 test.write('file', "contents\n")
52 test.write(['subdir', 'file'], "contents\n")
55 test.read(['subdir', 'file'])
56 test.read('file', mode)
57 test.read(['subdir', 'file'], mode)
59 test.writable('dir', 1)
60 test.writable('dir', None)
62 test.preserve(condition, ...)
64 test.cleanup(condition)
66 test.run(program = 'program_or_script_to_run',
67 interpreter = 'script_interpreter',
68 arguments = 'arguments to pass to program',
69 chdir = 'directory_to_chdir_to',
70 stdin = 'input to feed to the program\n')
73 test.pass_test(condition)
74 test.pass_test(condition, function)
77 test.fail_test(condition)
78 test.fail_test(condition, function)
79 test.fail_test(condition, function, skip)
82 test.no_result(condition)
83 test.no_result(condition, function)
84 test.no_result(condition, function, skip)
92 test.symlink(target, link)
94 test.match(actual, expected)
96 test.match_exact("actual 1\nactual 2\n", "expected 1\nexpected 2\n")
97 test.match_exact(["actual 1\n", "actual 2\n"],
98 ["expected 1\n", "expected 2\n"])
100 test.match_re("actual 1\nactual 2\n", regex_string)
101 test.match_re(["actual 1\n", "actual 2\n"], list_of_regexes)
103 test.match_re_dotall("actual 1\nactual 2\n", regex_string)
104 test.match_re_dotall(["actual 1\n", "actual 2\n"], list_of_regexes)
107 test.tempdir('temporary-directory')
113 test.where_is('foo', 'PATH1:PATH2')
114 test.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
117 test.unlink('subdir', 'file')
119 The TestCmd module provides pass_test(), fail_test(), and no_result()
120 unbound functions that report test results for use with the Aegis change
121 management system. These methods terminate the test immediately,
122 reporting PASSED, FAILED, or NO RESULT respectively, and exiting with
123 status 0 (success), 1 or 2 respectively. This allows for a distinction
124 between an actual failed test and a test that could not be properly
125 evaluated because of an external condition (such as a full file system
126 or incorrect permissions).
131 TestCmd.pass_test(condition)
132 TestCmd.pass_test(condition, function)
135 TestCmd.fail_test(condition)
136 TestCmd.fail_test(condition, function)
137 TestCmd.fail_test(condition, function, skip)
140 TestCmd.no_result(condition)
141 TestCmd.no_result(condition, function)
142 TestCmd.no_result(condition, function, skip)
144 The TestCmd module also provides unbound functions that handle matching
145 in the same way as the match_*() methods described above.
149 test = TestCmd.TestCmd(match = TestCmd.match_exact)
151 test = TestCmd.TestCmd(match = TestCmd.match_re)
153 test = TestCmd.TestCmd(match = TestCmd.match_re_dotall)
155 Lastly, the where_is() method also exists in an unbound function
160 TestCmd.where_is('foo')
161 TestCmd.where_is('foo', 'PATH1:PATH2')
162 TestCmd.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
165 # Copyright 2000, 2001, 2002, 2003, 2004 Steven Knight
166 # This module is free software, and you may redistribute it and/or modify
167 # it under the same terms as Python itself, so long as this copyright message
168 # and disclaimer are retained in their original form.
170 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
171 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
172 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
175 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
176 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
177 # PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
178 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
179 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
181 __author__ = "Steven Knight <knight at baldmt dot com>"
182 __revision__ = "TestCmd.py 0.26.D001 2007/08/20 21:58:58 knight"
212 return type(e) is types.ListType \
213 or isinstance(e, UserList.UserList)
216 from UserString import UserString
221 if hasattr(types, 'UnicodeType'):
223 return type(e) is types.StringType \
224 or type(e) is types.UnicodeType \
225 or isinstance(e, UserString)
228 return type(e) is types.StringType or isinstance(e, UserString)
230 tempfile.template = 'testcmd.'
231 if os.name in ('posix', 'nt'):
232 tempfile.template = 'testcmd.' + str(os.getpid()) + '.'
234 tempfile.template = 'testcmd.'
236 re_space = re.compile('\s')
242 cleanlist = filter(None, _Cleanup)
245 for test in cleanlist:
248 sys.exitfunc = _clean
251 def __init__(self, top):
253 def __call__(self, arg, dirname, names):
254 pathjoin = lambda n, d=dirname: os.path.join(d, n)
255 self.entries.extend(map(pathjoin, names))
257 def _caller(tblist, skip):
260 for file, line, name, text in tblist:
261 if file[-10:] == "TestCmd.py":
263 arr = [(file, line, name, text)] + arr
265 for file, line, name, text in arr[skip:]:
269 name = " (" + name + ")"
270 string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name))
274 def fail_test(self = None, condition = 1, function = None, skip = 0):
275 """Cause the test to fail.
277 By default, the fail_test() method reports that the test FAILED
278 and exits with a status of 1. If a condition argument is supplied,
279 the test fails only if the condition is true.
283 if not function is None:
290 of = " of " + self.program
293 desc = " [" + self.description + "]"
296 at = _caller(traceback.extract_stack(), skip)
297 sys.stderr.write("FAILED test" + of + desc + sep + at)
301 def no_result(self = None, condition = 1, function = None, skip = 0):
302 """Causes a test to exit with no valid result.
304 By default, the no_result() method reports NO RESULT for the test
305 and exits with a status of 2. If a condition argument is supplied,
306 the test fails only if the condition is true.
310 if not function is None:
317 of = " of " + self.program
320 desc = " [" + self.description + "]"
323 at = _caller(traceback.extract_stack(), skip)
324 sys.stderr.write("NO RESULT for test" + of + desc + sep + at)
328 def pass_test(self = None, condition = 1, function = None):
329 """Causes a test to pass.
331 By default, the pass_test() method reports PASSED for the test
332 and exits with a status of 0. If a condition argument is supplied,
333 the test passes only if the condition is true.
337 if not function is None:
339 sys.stderr.write("PASSED\n")
342 def match_exact(lines = None, matches = None):
345 if not is_List(lines):
346 lines = string.split(lines, "\n")
347 if not is_List(matches):
348 matches = string.split(matches, "\n")
349 if len(lines) != len(matches):
351 for i in range(len(lines)):
352 if lines[i] != matches[i]:
356 def match_re(lines = None, res = None):
359 if not is_List(lines):
360 lines = string.split(lines, "\n")
362 res = string.split(res, "\n")
363 if len(lines) != len(res):
365 for i in range(len(lines)):
366 if not re.compile("^" + res[i] + "$").search(lines[i]):
370 def match_re_dotall(lines = None, res = None):
373 if not type(lines) is type(""):
374 lines = string.join(lines, "\n")
375 if not type(res) is type(""):
376 res = string.join(res, "\n")
377 if re.compile("^" + res + "$", re.DOTALL).match(lines):
380 def diff_re(a, b, fromfile='', tofile='',
381 fromfiledate='', tofiledate='', n=3, lineterm='\n'):
383 A simple "diff" of two sets of lines when the expected lines
384 are regular expressions. This is a really dumb thing that
385 just compares each line in turn, so it doesn't look for
386 chunks of matching lines and the like--but at least it lets
387 you know exactly which line first didn't compare correctl...
390 diff = len(a) - len(b)
396 for aline, bline in zip(a, b):
397 if not re.compile("^" + aline + "$").search(bline):
398 result.append("%sc%s" % (i+1, i+1))
399 result.append('< ' + repr(a[i]))
401 result.append('> ' + repr(b[i]))
405 if os.name == 'java':
407 python_executable = os.path.join(sys.prefix, 'jython')
411 python_executable = sys.executable
413 if sys.platform == 'win32':
415 default_sleep_seconds = 2
417 def where_is(file, path=None, pathext=None):
419 path = os.environ['PATH']
421 path = string.split(path, os.pathsep)
423 pathext = os.environ['PATHEXT']
424 if is_String(pathext):
425 pathext = string.split(pathext, os.pathsep)
427 if string.lower(ext) == string.lower(file[-len(ext):]):
431 f = os.path.join(dir, file)
434 if os.path.isfile(fext):
440 def where_is(file, path=None, pathext=None):
442 path = os.environ['PATH']
444 path = string.split(path, os.pathsep)
446 f = os.path.join(dir, file)
447 if os.path.isfile(f):
452 if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
456 default_sleep_seconds = 1
462 def __init__(self, description = None,
470 universal_newlines = 1):
471 self._cwd = os.getcwd()
472 self.description_set(description)
473 self.program_set(program)
474 self.interpreter_set(interpreter)
477 verbose = max( 0, int(os.environ.get('TESTCMD_VERBOSE', 0)) )
480 self.verbose_set(verbose)
481 self.combine = combine
482 self.universal_newlines = universal_newlines
483 if not match is None:
484 self.match_func = match
486 self.match_func = match_re
488 self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0}
489 if os.environ.has_key('PRESERVE') and not os.environ['PRESERVE'] is '':
490 self._preserve['pass_test'] = os.environ['PRESERVE']
491 self._preserve['fail_test'] = os.environ['PRESERVE']
492 self._preserve['no_result'] = os.environ['PRESERVE']
495 self._preserve['pass_test'] = os.environ['PRESERVE_PASS']
499 self._preserve['fail_test'] = os.environ['PRESERVE_FAIL']
503 self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT']
509 self.condition = 'no_result'
510 self.workdir_set(workdir)
517 return "%x" % id(self)
519 if os.name == 'posix':
521 def escape(self, arg):
522 "escape shell special characters"
526 arg = string.replace(arg, slash, slash+slash)
528 arg = string.replace(arg, c, slash+c)
530 if re_space.search(arg):
531 arg = '"' + arg + '"'
536 # Windows does not allow special characters in file names
537 # anyway, so no need for an escape function, we will just quote
539 def escape(self, arg):
540 if re_space.search(arg):
541 arg = '"' + arg + '"'
544 def canonicalize(self, path):
546 path = apply(os.path.join, tuple(path))
547 if not os.path.isabs(path):
548 path = os.path.join(self.workdir, path)
551 def cleanup(self, condition = None):
552 """Removes any temporary working directories for the specified
553 TestCmd environment. If the environment variable PRESERVE was
554 set when the TestCmd environment was created, temporary working
555 directories are not removed. If any of the environment variables
556 PRESERVE_PASS, PRESERVE_FAIL, or PRESERVE_NO_RESULT were set
557 when the TestCmd environment was created, then temporary working
558 directories are not removed if the test passed, failed, or had
559 no result, respectively. Temporary working directories are also
560 preserved for conditions specified via the preserve method.
562 Typically, this method is not called directly, but is used when
563 the script exits to clean up temporary working directories as
564 appropriate for the exit status.
566 if not self._dirlist:
570 if condition is None:
571 condition = self.condition
572 if self._preserve[condition]:
573 for dir in self._dirlist:
574 print "Preserved directory", dir
576 list = self._dirlist[:]
579 self.writable(dir, 1)
580 shutil.rmtree(dir, ignore_errors = 1)
585 _Cleanup.remove(self)
586 except (AttributeError, ValueError):
589 def chmod(self, path, mode):
590 """Changes permissions on the specified file or directory
592 path = self.canonicalize(path)
595 def description_set(self, description):
596 """Set the description of the functionality being tested.
598 self.description = description
601 # """Diff two arrays.
604 def fail_test(self, condition = 1, function = None, skip = 0):
605 """Cause the test to fail.
609 self.condition = 'fail_test'
610 fail_test(self = self,
611 condition = condition,
615 def interpreter_set(self, interpreter):
616 """Set the program to be used to interpret the program
617 under test as a script.
619 self.interpreter = interpreter
621 def match(self, lines, matches):
622 """Compare actual and expected file contents.
624 return self.match_func(lines, matches)
626 def match_exact(self, lines, matches):
627 """Compare actual and expected file contents.
629 return match_exact(lines, matches)
631 def match_re(self, lines, res):
632 """Compare actual and expected file contents.
634 return match_re(lines, res)
636 def match_re_dotall(self, lines, res):
637 """Compare actual and expected file contents.
639 return match_re_dotall(lines, res)
641 def no_result(self, condition = 1, function = None, skip = 0):
642 """Report that the test could not be run.
646 self.condition = 'no_result'
647 no_result(self = self,
648 condition = condition,
652 def pass_test(self, condition = 1, function = None):
653 """Cause the test to pass.
657 self.condition = 'pass_test'
658 pass_test(self = self, condition = condition, function = function)
660 def preserve(self, *conditions):
661 """Arrange for the temporary working directories for the
662 specified TestCmd environment to be preserved for one or more
663 conditions. If no conditions are specified, arranges for
664 the temporary working directories to be preserved for all
668 conditions = ('pass_test', 'fail_test', 'no_result')
669 for cond in conditions:
670 self._preserve[cond] = 1
672 def program_set(self, program):
673 """Set the executable program or script to be tested.
675 if program and not os.path.isabs(program):
676 program = os.path.join(self._cwd, program)
677 self.program = program
679 def read(self, file, mode = 'rb'):
680 """Reads and returns the contents of the specified file name.
681 The file name may be a list, in which case the elements are
682 concatenated with the os.path.join() method. The file is
683 assumed to be under the temporary working directory unless it
684 is an absolute path name. The I/O mode for the file may
685 be specified; it must begin with an 'r'. The default is
688 file = self.canonicalize(file)
690 raise ValueError, "mode must begin with 'r'"
691 return open(file, mode).read()
693 def run(self, program = None,
698 universal_newlines = None):
699 """Runs a test of the program or script for the test
700 environment. Standard output and error output are saved for
701 future retrieval via the stdout() and stderr() methods.
703 The specified program will have the original directory
704 prepending unless it is enclosed in a [list].
708 if not os.path.isabs(chdir):
709 chdir = os.path.join(self.workpath(chdir))
711 sys.stderr.write("chdir(" + chdir + ")\n")
714 if type(program) == type('') and not os.path.isabs(program):
715 program = os.path.join(self._cwd, program)
717 program = self.program
719 interpreter = self.interpreter
720 if not type(program) in [type([]), type(())]:
724 if not type(interpreter) in [type([]), type(())]:
725 interpreter = [interpreter]
726 cmd = list(interpreter) + cmd
728 if type(arguments) == type(''):
729 arguments = string.split(arguments)
730 cmd.extend(arguments)
731 cmd_string = string.join(map(self.escape, cmd), ' ')
733 sys.stderr.write(cmd_string + "\n")
734 if universal_newlines is None:
735 universal_newlines = self.universal_newlines
741 Popen3 = popen2.Popen3
742 except AttributeError:
744 def __init__(self, command):
745 (stdin, stdout, stderr) = os.popen3(' ' + command)
749 def close_output(self):
751 self.resultcode = self.stderr.close()
753 return self.resultcode
754 if sys.platform == 'win32' and cmd_string[0] == '"':
755 cmd_string = '"' + cmd_string + '"'
756 p = Popen3(cmd_string)
760 p.stdout = p.fromchild
761 p.stderr = p.childerr
763 p = subprocess.Popen(cmd,
764 stdin=subprocess.PIPE,
765 stdout=subprocess.PIPE,
766 stderr=subprocess.PIPE,
767 universal_newlines=universal_newlines)
777 out = p.stdout.read()
778 err = p.stderr.read()
781 except AttributeError:
785 self.status = p.wait()
790 self._stdout.append(out + err)
792 self._stdout.append(out)
793 self._stderr.append(err)
797 if self.verbose >= 2:
798 write = sys.stdout.write
799 write('============ STATUS: %d\n' % self.status)
801 if out or self.verbose >= 3:
802 write('============ BEGIN STDOUT (len=%d):\n' % len(out))
804 write('============ END STDOUT\n')
806 if err or self.verbose >= 3:
807 write('============ BEGIN STDERR (len=%d)\n' % len(err))
809 write('============ END STDERR\n')
811 def sleep(self, seconds = default_sleep_seconds):
812 """Sleeps at least the specified number of seconds. If no
813 number is specified, sleeps at least the minimum number of
814 seconds necessary to advance file time stamps on the current
815 system. Sleeping more seconds is all right.
819 def stderr(self, run = None):
820 """Returns the error output from the specified run number.
821 If there is no specified run number, then returns the error
822 output of the last run. If the run number is less than zero,
823 then returns the error output from that many runs back from the
827 run = len(self._stderr)
829 run = len(self._stderr) + run
831 return self._stderr[run]
833 def stdout(self, run = None):
834 """Returns the standard output from the specified run number.
835 If there is no specified run number, then returns the standard
836 output of the last run. If the run number is less than zero,
837 then returns the standard output from that many runs back from
841 run = len(self._stdout)
843 run = len(self._stdout) + run
845 return self._stdout[run]
847 def subdir(self, *subdirs):
848 """Create new subdirectories under the temporary working
849 directory, one for each argument. An argument may be a list,
850 in which case the list elements are concatenated using the
851 os.path.join() method. Subdirectories multiple levels deep
852 must be created using a separate argument for each level:
854 test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory'])
856 Returns the number of subdirectories actually created.
863 sub = apply(os.path.join, tuple(sub))
864 new = os.path.join(self.workdir, sub)
873 def symlink(self, target, link):
874 """Creates a symlink to the specified target.
875 The link name may be a list, in which case the elements are
876 concatenated with the os.path.join() method. The link is
877 assumed to be under the temporary working directory unless it
878 is an absolute path name. The target is *not* assumed to be
879 under the temporary working directory.
881 link = self.canonicalize(link)
882 os.symlink(target, link)
884 def tempdir(self, path=None):
885 """Creates a temporary directory.
886 A unique directory name is generated if no path name is specified.
887 The directory is created, and will be removed when the TestCmd
892 path = tempfile.mktemp(prefix=tempfile.template)
894 path = tempfile.mktemp()
897 # Symlinks in the path will report things
898 # differently from os.getcwd(), so chdir there
899 # and back to fetch the canonical path.
907 # Uppercase the drive letter since the case of drive
908 # letters is pretty much random on win32:
909 drive,rest = os.path.splitdrive(path)
911 path = string.upper(drive) + rest
914 self._dirlist.append(path)
919 _Cleanup.append(self)
923 def touch(self, path, mtime=None):
924 """Updates the modification time on the specified file or
925 directory path name. The default is to update to the
926 current time if no explicit modification time is specified.
928 path = self.canonicalize(path)
929 atime = os.path.getatime(path)
932 os.utime(path, (atime, mtime))
934 def unlink(self, file):
935 """Unlinks the specified file name.
936 The file name may be a list, in which case the elements are
937 concatenated with the os.path.join() method. The file is
938 assumed to be under the temporary working directory unless it
939 is an absolute path name.
941 file = self.canonicalize(file)
944 def verbose_set(self, verbose):
945 """Set the verbose level.
947 self.verbose = verbose
949 def where_is(self, file, path=None, pathext=None):
950 """Find an executable file.
953 file = apply(os.path.join, tuple(file))
954 if not os.path.isabs(file):
955 file = where_is(file, path, pathext)
958 def workdir_set(self, path):
959 """Creates a temporary working directory with the specified
960 path name. If the path is a null string (''), a unique
961 directory name is created.
966 path = self.tempdir(path)
969 def workpath(self, *args):
970 """Returns the absolute path name to a subdirectory or file
971 within the current temporary working directory. Concatenates
972 the temporary working directory name with the specified
973 arguments using the os.path.join() method.
975 return apply(os.path.join, (self.workdir,) + tuple(args))
977 def readable(self, top, read=1):
978 """Make the specified directory tree readable (read == 1)
979 or not (read == None).
984 try: st = os.stat(fname)
986 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0400))
989 try: st = os.stat(fname)
991 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0400))
993 if os.path.isfile(top):
994 # If it's a file, that's easy, just chmod it.
997 # It's a directory and we're trying to turn on read
998 # permission, so it's also pretty easy, just chmod the
999 # directory and then chmod every entry on our walk down the
1000 # tree. Because os.path.walk() is top-down, we'll enable
1001 # read permission on any directories that have it disabled
1002 # before os.path.walk() tries to list their contents.
1005 def chmod_entries(arg, dirname, names, do_chmod=do_chmod):
1006 pathnames = map(lambda n, d=dirname: os.path.join(d, n),
1008 map(lambda p, do=do_chmod: do(p), pathnames)
1010 os.path.walk(top, chmod_entries, None)
1012 # It's a directory and we're trying to turn off read
1013 # permission, which means we have to chmod the directoreis
1014 # in the tree bottom-up, lest disabling read permission from
1015 # the top down get in the way of being able to get at lower
1016 # parts of the tree. But os.path.walk() visits things top
1017 # down, so we just use an object to collect a list of all
1018 # of the entries in the tree, reverse the list, and then
1019 # chmod the reversed (bottom-up) list.
1020 col = Collector(top)
1021 os.path.walk(top, col, None)
1022 col.entries.reverse()
1023 map(lambda d, do=do_chmod: do(d), col.entries)
1025 def writable(self, top, write=1):
1026 """Make the specified directory tree writable (write == 1)
1027 or not (write == None).
1031 def do_chmod(fname):
1032 try: st = os.stat(fname)
1033 except OSError: pass
1034 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0200))
1036 def do_chmod(fname):
1037 try: st = os.stat(fname)
1038 except OSError: pass
1039 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0200))
1041 if os.path.isfile(top):
1044 col = Collector(top)
1045 os.path.walk(top, col, None)
1046 map(lambda d, do=do_chmod: do(d), col.entries)
1048 def executable(self, top, execute=1):
1049 """Make the specified directory tree executable (execute == 1)
1050 or not (execute == None).
1054 def do_chmod(fname):
1055 try: st = os.stat(fname)
1056 except OSError: pass
1057 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0100))
1059 def do_chmod(fname):
1060 try: st = os.stat(fname)
1061 except OSError: pass
1062 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0100))
1064 if os.path.isfile(top):
1065 # If it's a file, that's easy, just chmod it.
1068 # It's a directory and we're trying to turn on execute
1069 # permission, so it's also pretty easy, just chmod the
1070 # directory and then chmod every entry on our walk down the
1071 # tree. Because os.path.walk() is top-down, we'll enable
1072 # execute permission on any directories that have it disabled
1073 # before os.path.walk() tries to list their contents.
1076 def chmod_entries(arg, dirname, names, do_chmod=do_chmod):
1077 pathnames = map(lambda n, d=dirname: os.path.join(d, n),
1079 map(lambda p, do=do_chmod: do(p), pathnames)
1081 os.path.walk(top, chmod_entries, None)
1083 # It's a directory and we're trying to turn off execute
1084 # permission, which means we have to chmod the directories
1085 # in the tree bottom-up, lest disabling execute permission from
1086 # the top down get in the way of being able to get at lower
1087 # parts of the tree. But os.path.walk() visits things top
1088 # down, so we just use an object to collect a list of all
1089 # of the entries in the tree, reverse the list, and then
1090 # chmod the reversed (bottom-up) list.
1091 col = Collector(top)
1092 os.path.walk(top, col, None)
1093 col.entries.reverse()
1094 map(lambda d, do=do_chmod: do(d), col.entries)
1096 def write(self, file, content, mode = 'wb'):
1097 """Writes the specified content text (second argument) to the
1098 specified file name (first argument). The file name may be
1099 a list, in which case the elements are concatenated with the
1100 os.path.join() method. The file is created under the temporary
1101 working directory. Any subdirectories in the path must already
1102 exist. The I/O mode for the file may be specified; it must
1103 begin with a 'w'. The default is 'wb' (binary write).
1105 file = self.canonicalize(file)
1107 raise ValueError, "mode must begin with 'w'"
1108 open(file, mode).write(content)