diff options
Diffstat (limited to 'tools/build/v2/test/BoostBuild.py')
-rw-r--r-- | tools/build/v2/test/BoostBuild.py | 921 |
1 files changed, 921 insertions, 0 deletions
diff --git a/tools/build/v2/test/BoostBuild.py b/tools/build/v2/test/BoostBuild.py new file mode 100644 index 0000000000..409a51b6e9 --- /dev/null +++ b/tools/build/v2/test/BoostBuild.py @@ -0,0 +1,921 @@ +# Copyright 2002-2005 Vladimir Prus. +# Copyright 2002-2003 Dave Abrahams. +# Copyright 2006 Rene Rivera. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) + +import TestCmd +import copy +import fnmatch +import glob +import math +import os +import re +import shutil +import string +import StringIO +import sys +import tempfile +import time +import traceback +import tree +import types + +from xml.sax.saxutils import escape + + +annotations = [] + + +def print_annotation(name, value, xml): + """Writes some named bits of information about test run. + """ + if xml: + print escape(name) + " {{{" + print escape(value) + print "}}}" + else: + print name + " {{{" + print value + print "}}}" + +def flush_annotations(xml=0): + global annotations + for ann in annotations: + print_annotation(ann[0], ann[1], xml) + annotations = [] + +def clear_annotations(): + global annotations + annotations = [] + +defer_annotations = 0 + + +def set_defer_annotations(n): + global defer_annotations + defer_annotations = n + + +def annotation(name, value): + """Records an annotation about the test run. + """ + annotations.append((name, value)) + if not defer_annotations: + flush_annotations() + + +def get_toolset(): + toolset = None; + for arg in sys.argv[1:]: + if not arg.startswith('-'): + toolset = arg + return toolset or 'gcc' + + +# Detect the host OS. +windows = False +if os.environ.get('OS', '').lower().startswith('windows') or \ + os.__dict__.has_key('uname') and \ + os.uname()[0].lower().startswith('cygwin'): + windows = True + + +suffixes = {} + + +# Prepare the map of suffixes +def prepare_suffix_map(toolset): + global windows + global suffixes + suffixes = {'.exe': '', '.dll': '.so', '.lib': '.a', '.obj': '.o'} + suffixes['.implib'] = '.no_implib_files_on_this_platform' + if windows: + suffixes = {} + if toolset in ["gcc"]: + suffixes['.lib'] = '.a' # static libs have '.a' suffix with mingw... + suffixes['.obj'] = '.o' + suffixes['.implib'] = '.lib' + if os.__dict__.has_key('uname') and (os.uname()[0] == 'Darwin'): + suffixes['.dll'] = '.dylib' + + +def re_remove(sequence, regex): + me = re.compile(regex) + result = filter(lambda x: me.match(x), sequence) + if 0 == len(result): + raise ValueError() + for r in result: + sequence.remove(r) + + +def glob_remove(sequence, pattern): + result = fnmatch.filter(sequence, pattern) + if 0 == len(result): + raise ValueError() + for r in result: + sequence.remove(r) + + +# Configuration stating whether Boost Build is expected to automatically prepend +# prefixes to built library targets. +lib_prefix = True +dll_prefix = True +if windows: + dll_prefix = False + + +# +# FIXME: this is copy-pasted from TestSCons.py +# Should be moved to TestCmd.py? +# +if os.name == 'posix': + def _failed(self, status=0): + if self.status is None: + return None + return _status(self) != status + def _status(self): + if os.WIFEXITED(self.status): + return os.WEXITSTATUS(self.status) + else: + return -1 +elif os.name == 'nt': + def _failed(self, status=0): + return not self.status is None and self.status != status + def _status(self): + return self.status + + +class Tester(TestCmd.TestCmd): + """Main tester class for Boost Build. + + Optional arguments: + + `arguments` - Arguments passed to the run executable. + `executable` - Name of the executable to invoke. + `match` - Function to use for compating actual and + expected file contents. + `boost_build_path` - Boost build path to be passed to the run + executable. + `translate_suffixes` - Whether to update suffixes on the the file + names passed from the test script so they + match those actually created by the current + toolset. For example, static library files + are specified by using the .lib suffix but + when the 'gcc' toolset is used it actually + creates them using the .a suffix. + `pass_toolset` - Whether the test system should pass the + specified toolset to the run executable. + `use_test_config` - Whether the test system should tell the run + executable to read in the test_config.jam + configuration file. + `ignore_toolset_requirements` - Whether the test system should tell the run + executable to ignore toolset requirements. + `workdir` - indicates an absolute directory where the + test will be run from. + + Optional arguments inherited from the base class: + + `description` - Test description string displayed in case of + a failed test. + `subdir' - List of subdirectories to automatically + create under the working directory. Each + subdirectory needs to be specified + separately parent coming before its child. + `verbose` - Flag that may be used to enable more verbose + test system output. Note that it does not + also enable more verbose build system + output like the --verbose command line + option does. + """ + def __init__(self, arguments="", executable="bjam", + match=TestCmd.match_exact, boost_build_path=None, + translate_suffixes=True, pass_toolset=True, use_test_config=True, + ignore_toolset_requirements=True, workdir="", **keywords): + + self.original_workdir = os.getcwd() + if workdir != '' and not os.path.isabs(workdir): + raise "Parameter workdir <"+workdir+"> must point to an absolute directory: " + + self.last_build_time_start = 0 + self.last_build_time_finish = 0 + self.translate_suffixes = translate_suffixes + self.use_test_config = use_test_config + + self.toolset = get_toolset() + self.pass_toolset = pass_toolset + self.ignore_toolset_requirements = ignore_toolset_requirements + + prepare_suffix_map(pass_toolset and self.toolset or 'gcc') + + if not '--default-bjam' in sys.argv: + jam_build_dir = "" + if os.name == 'nt': + jam_build_dir = "bin.ntx86" + elif (os.name == 'posix') and os.__dict__.has_key('uname'): + if os.uname()[0].lower().startswith('cygwin'): + jam_build_dir = "bin.cygwinx86" + if 'TMP' in os.environ and os.environ['TMP'].find('~') != -1: + print 'Setting $TMP to /tmp to get around problem with short path names' + os.environ['TMP'] = '/tmp' + elif os.uname()[0] == 'Linux': + cpu = os.uname()[4] + if re.match("i.86", cpu): + jam_build_dir = "bin.linuxx86"; + else: + jam_build_dir = "bin.linux" + os.uname()[4] + elif os.uname()[0] == 'SunOS': + jam_build_dir = "bin.solaris" + elif os.uname()[0] == 'Darwin': + if os.uname()[4] == 'i386': + jam_build_dir = "bin.macosxx86" + else: + jam_build_dir = "bin.macosxppc" + elif os.uname()[0] == "AIX": + jam_build_dir = "bin.aix" + elif os.uname()[0] == "IRIX64": + jam_build_dir = "bin.irix" + elif os.uname()[0] == "FreeBSD": + jam_build_dir = "bin.freebsd" + elif os.uname()[0] == "OSF1": + jam_build_dir = "bin.osf" + else: + raise "Don't know directory where Jam is built for this system: " + os.name + "/" + os.uname()[0] + else: + raise "Don't know directory where Jam is built for this system: " + os.name + + # Find where jam_src is located. Try for the debug version if it is + # lying around. + dirs = [os.path.join('../engine', jam_build_dir + '.debug'), + os.path.join('../engine', jam_build_dir), + ] + for d in dirs: + if os.path.exists(d): + jam_build_dir = d + break + else: + print "Cannot find built Boost.Jam" + sys.exit(1) + + verbosity = ['-d0', '--quiet'] + if '--verbose' in sys.argv: + keywords['verbose'] = True + verbosity = ['-d+2'] + + if boost_build_path is None: + boost_build_path = self.original_workdir + "/.." + + program_list = [] + + if '--default-bjam' in sys.argv: + program_list.append(executable) + inpath_bjam = True + else: + program_list.append(os.path.join(jam_build_dir, executable)) + inpath_bjam = None + program_list.append('-sBOOST_BUILD_PATH="' + boost_build_path + '"') + if verbosity: + program_list += verbosity + if arguments: + program_list += arguments.split(" ") + + TestCmd.TestCmd.__init__( + self + , program=program_list + , match=match + , workdir=workdir + , inpath=inpath_bjam + , **keywords) + + os.chdir(self.workdir) + + def cleanup(self): + try: + TestCmd.TestCmd.cleanup(self) + os.chdir(self.original_workdir) + except AttributeError: + # When this is called during TestCmd.TestCmd.__del__ we can have + # both 'TestCmd' and 'os' unavailable in our scope. Do nothing in + # this case. + pass + + # + # Methods that change the working directory's content. + # + def set_tree(self, tree_location): + # It is not possible to remove the current directory. + d = os.getcwd() + os.chdir(os.path.dirname(self.workdir)) + shutil.rmtree(self.workdir, ignore_errors=False) + + if not os.path.isabs(tree_location): + tree_location = os.path.join(self.original_workdir, tree_location) + shutil.copytree(tree_location, self.workdir) + + os.chdir(d) + + def make_writable(unused, dir, entries): + for e in entries: + name = os.path.join(dir, e) + os.chmod(name, os.stat(name)[0] | 0222) + + os.path.walk(".", make_writable, None) + + def write(self, file, content): + self.wait_for_time_change_since_last_build() + nfile = self.native_file_name(file) + try: + os.makedirs(os.path.dirname(nfile)) + except Exception, e: + pass + open(nfile, "wb").write(content) + + def rename(self, old, new): + try: + os.makedirs(os.path.dirname(new)) + except: + pass + + try: + os.remove(new) + except: + pass + + os.rename(old, new) + self.touch(new); + + def copy(self, src, dst): + self.wait_for_time_change_since_last_build() + try: + self.write(dst, self.read(src, 1)) + except: + self.fail_test(1) + + def copy_preserving_timestamp(self, src, dst): + src_name = self.native_file_name(src) + dst_name = self.native_file_name(dst) + stats = os.stat(src_name) + self.write(dst, self.read(src, 1)) + os.utime(dst_name, (stats.st_atime, stats.st_mtime)) + + def touch(self, names): + self.wait_for_time_change_since_last_build() + for name in self.adjust_names(names): + os.utime(self.native_file_name(name), None) + + def rm(self, names): + self.wait_for_time_change_since_last_build() + if not type(names) == types.ListType: + names = [names] + + # Avoid attempts to remove the current directory. + os.chdir(self.original_workdir) + for name in names: + n = self.native_file_name(name) + n = glob.glob(n) + if n: n = n[0] + if not n: + n = self.glob_file(string.replace(name, "$toolset", self.toolset+"*")) + if n: + if os.path.isdir(n): + shutil.rmtree(n, ignore_errors=False) + else: + os.unlink(n) + + # Create working dir root again in case we removed it. + if not os.path.exists(self.workdir): + os.mkdir(self.workdir) + os.chdir(self.workdir) + + def expand_toolset(self, name): + """Expands $toolset in the given file to tested toolset. + """ + content = self.read(name) + content = string.replace(content, "$toolset", self.toolset) + self.write(name, content) + + def dump_stdio(self): + annotation("STDOUT", self.stdout()) + annotation("STDERR", self.stderr()) + + # + # FIXME: Large portion copied from TestSCons.py, should be moved? + # + def run_build_system(self, extra_args="", subdir="", stdout=None, stderr="", + status=0, match=None, pass_toolset=None, use_test_config=None, + ignore_toolset_requirements=None, expected_duration=None, **kw): + + self.last_build_time_start = time.time() + + try: + if os.path.isabs(subdir): + if stderr: + print "You must pass a relative directory to subdir <"+subdir+">." + status = 1 + return + + self.previous_tree = tree.build_tree(self.workdir) + + if match is None: + match = self.match + + if pass_toolset is None: + pass_toolset = self.pass_toolset + + if use_test_config is None: + use_test_config = self.use_test_config + + if ignore_toolset_requirements is None: + ignore_toolset_requirements = self.ignore_toolset_requirements + + try: + kw['program'] = [] + kw['program'] += self.program + if extra_args: + kw['program'] += extra_args.split(" ") + if pass_toolset: + kw['program'].append("toolset=" + self.toolset) + if use_test_config: + kw['program'].append('--test-config="%s"' + % os.path.join(self.original_workdir, "test-config.jam")) + if ignore_toolset_requirements: + kw['program'].append("--ignore-toolset-requirements") + if "--python" in sys.argv: + kw['program'].append("--python") + kw['chdir'] = subdir + self.last_program_invocation = kw['program'] + apply(TestCmd.TestCmd.run, [self], kw) + except: + self.dump_stdio() + raise + finally: + self.last_build_time_finish = time.time() + + if (status != None) and _failed(self, status): + expect = '' + if status != 0: + expect = " (expected %d)" % status + + annotation("failure", '"%s" returned %d%s' + % (kw['program'], _status(self), expect)) + + annotation("reason", "unexpected status returned by bjam") + self.fail_test(1) + + if not (stdout is None) and not match(self.stdout(), stdout): + annotation("failure", "Unexpected stdout") + annotation("Expected STDOUT", stdout) + annotation("Actual STDOUT", self.stdout()) + stderr = self.stderr() + if stderr: + annotation("STDERR", stderr) + self.maybe_do_diff(self.stdout(), stdout) + self.fail_test(1, dump_stdio=False) + + # Intel tends to produce some messages to stderr which make tests fail. + intel_workaround = re.compile("^xi(link|lib): executing.*\n", re.M) + actual_stderr = re.sub(intel_workaround, "", self.stderr()) + + if not (stderr is None) and not match(actual_stderr, stderr): + annotation("failure", "Unexpected stderr") + annotation("Expected STDERR", stderr) + annotation("Actual STDERR", self.stderr()) + annotation("STDOUT", self.stdout()) + self.maybe_do_diff(actual_stderr, stderr) + self.fail_test(1, dump_stdio=False) + + if not expected_duration is None: + actual_duration = self.last_build_time_finish - self.last_build_time_start + if (actual_duration > expected_duration): + print "Test run lasted %f seconds while it was expected to " \ + "finish in under %f seconds." % (actual_duration, + expected_duration) + self.fail_test(1, dump_stdio=False) + + self.tree = tree.build_tree(self.workdir) + self.difference = tree.trees_difference(self.previous_tree, self.tree) + self.difference.ignore_directories() + self.unexpected_difference = copy.deepcopy(self.difference) + + def glob_file(self, name): + result = None + if hasattr(self, 'difference'): + for f in self.difference.added_files+self.difference.modified_files+self.difference.touched_files: + if fnmatch.fnmatch(f, name): + result = self.native_file_name(f) + break + if not result: + result = glob.glob(self.native_file_name(name)) + if result: + result = result[0] + return result + + def read(self, name, binary=False): + try: + if self.toolset: + name = string.replace(name, "$toolset", self.toolset+"*") + name = self.glob_file(name) + openMode = "r" + if binary: + openMode += "b" + else: + openMode += "U" + return open(name, openMode).read() + except: + annotation("failure", "Could not open '%s'" % name) + self.fail_test(1) + return '' + + def read_and_strip(self, name): + if not self.glob_file(name): + return '' + f = open(self.glob_file(name), "rb") + lines = f.readlines() + result = string.join(map(string.rstrip, lines), "\n") + if lines and lines[-1][-1] == '\n': + return result + '\n' + else: + return result + + def fail_test(self, condition, dump_stdio=True, *args): + if not condition: + return + + if hasattr(self, 'difference'): + f = StringIO.StringIO() + self.difference.pprint(f) + annotation("changes caused by the last build command", f.getvalue()) + + if dump_stdio: + self.dump_stdio() + + if '--preserve' in sys.argv: + print + print "*** Copying the state of working dir into 'failed_test' ***" + print + path = os.path.join(self.original_workdir, "failed_test") + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=False) + elif os.path.exists(path): + raise "Path " + path + " already exists and is not a directory"; + shutil.copytree(self.workdir, path) + print "The failed command was:" + print ' '.join(self.last_program_invocation) + + at = TestCmd.caller(traceback.extract_stack(), 0) + annotation("stacktrace", at) + sys.exit(1) + + # A number of methods below check expectations with actual difference + # between directory trees before and after a build. All the 'expect*' + # methods require exact names to be passed. All the 'ignore*' methods allow + # wildcards. + + # All names can be lists, which are taken to be directory components. + def expect_addition(self, names): + for name in self.adjust_names(names): + try: + glob_remove(self.unexpected_difference.added_files, name) + except: + annotation("failure", "File %s not added as expected" % name) + self.fail_test(1) + + def ignore_addition(self, wildcard): + self.ignore_elements(self.unexpected_difference.added_files, wildcard) + + def expect_removal(self, names): + for name in self.adjust_names(names): + try: + glob_remove(self.unexpected_difference.removed_files, name) + except: + annotation("failure", "File %s not removed as expected" % name) + self.fail_test(1) + + def ignore_removal(self, wildcard): + self.ignore_elements(self.unexpected_difference.removed_files, wildcard) + + def expect_modification(self, names): + for name in self.adjust_names(names): + try: + glob_remove(self.unexpected_difference.modified_files, name) + except: + annotation("failure", "File %s not modified as expected" % name) + self.fail_test(1) + + def ignore_modification(self, wildcard): + self.ignore_elements(self.unexpected_difference.modified_files, \ + wildcard) + + def expect_touch(self, names): + d = self.unexpected_difference + for name in self.adjust_names(names): + # We need to check both touched and modified files. The reason is + # that: + # (1) Windows binaries such as obj, exe or dll files have slight + # differences even with identical inputs due to Windows PE + # format headers containing an internal timestamp. + # (2) Intel's compiler for Linux has the same behaviour. + filesets = [d.modified_files, d.touched_files] + + while filesets: + try: + glob_remove(filesets[-1], name) + break + except ValueError: + filesets.pop() + + if not filesets: + annotation("failure", "File %s not touched as expected" % name) + self.fail_test(1) + + def ignore_touch(self, wildcard): + self.ignore_elements(self.unexpected_difference.touched_files, wildcard) + + def ignore(self, wildcard): + self.ignore_elements(self.unexpected_difference.added_files, wildcard) + self.ignore_elements(self.unexpected_difference.removed_files, wildcard) + self.ignore_elements(self.unexpected_difference.modified_files, wildcard) + self.ignore_elements(self.unexpected_difference.touched_files, wildcard) + + def expect_nothing(self, names): + for name in self.adjust_names(names): + if name in self.difference.added_files: + annotation("failure", + "File %s added, but no action was expected" % name) + self.fail_test(1) + if name in self.difference.removed_files: + annotation("failure", + "File %s removed, but no action was expected" % name) + self.fail_test(1) + pass + if name in self.difference.modified_files: + annotation("failure", + "File %s modified, but no action was expected" % name) + self.fail_test(1) + if name in self.difference.touched_files: + annotation("failure", + "File %s touched, but no action was expected" % name) + self.fail_test(1) + + def expect_nothing_more(self): + # Not totally sure about this change, but I do not see a good + # alternative. + if windows: + self.ignore('*.ilk') # MSVC incremental linking files. + self.ignore('*.pdb') # MSVC program database files. + self.ignore('*.rsp') # Response files. + self.ignore('*.tds') # Borland debug symbols. + self.ignore('*.manifest') # MSVC DLL manifests. + + # Debug builds of bjam built with gcc produce this profiling data. + self.ignore('gmon.out') + self.ignore('*/gmon.out') + + self.ignore("bin/config.log") + + self.ignore("*.pyc") + + if not self.unexpected_difference.empty(): + annotation('failure', 'Unexpected changes found') + output = StringIO.StringIO() + self.unexpected_difference.pprint(output) + annotation("unexpected changes", output.getvalue()) + self.fail_test(1) + + def __expect_line(self, content, expected, expected_to_exist): + expected = expected.strip() + lines = content.splitlines() + found = False + for line in lines: + line = line.strip() + if fnmatch.fnmatch(line, expected): + found = True + break + + if expected_to_exist and not found: + annotation("failure", + "Did not find expected line:\n%s\nin output:\n%s" % + (expected, content)) + self.fail_test(1) + if not expected_to_exist and found: + annotation("failure", + "Found an unexpected line:\n%s\nin output:\n%s" % + (expected, content)) + self.fail_test(1) + + def expect_output_line(self, line, expected_to_exist=True): + self.__expect_line(self.stdout(), line, expected_to_exist) + + def expect_content_line(self, name, line, expected_to_exist=True): + content = self.__read_file(name) + self.__expect_line(content, line, expected_to_exist) + + def __read_file(self, name, exact=False): + name = self.adjust_names(name)[0] + result = "" + try: + if exact: + result = self.read(name) + else: + result = string.replace(self.read_and_strip(name), "\\", "/") + except (IOError, IndexError): + print "Note: could not open file", name + self.fail_test(1) + return result + + def expect_content(self, name, content, exact=False): + actual = self.__read_file(name, exact) + content = string.replace(content, "$toolset", self.toolset+"*") + + matched = False + if exact: + matched = fnmatch.fnmatch(actual, content) + else: + def sorted_(x): + x.sort() + return x + actual_ = map(lambda x: sorted_(x.split()), actual.splitlines()) + content_ = map(lambda x: sorted_(x.split()), content.splitlines()) + if len(actual_) == len(content_): + matched = map( + lambda x, y: map(lambda n, p: fnmatch.fnmatch(n, p), x, y), + actual_, content_) + matched = reduce( + lambda x, y: x and reduce( + lambda a, b: a and b, + y), + matched) + + if not matched: + print "Expected:\n" + print content + print "Got:\n" + print actual + self.fail_test(1) + + def maybe_do_diff(self, actual, expected): + if os.environ.has_key("DO_DIFF") and os.environ["DO_DIFF"] != '': + e = tempfile.mktemp("expected") + a = tempfile.mktemp("actual") + open(e, "w").write(expected) + open(a, "w").write(actual) + print "DIFFERENCE" + if os.system("diff -u " + e + " " + a): + print "Unable to compute difference: diff -u %s %s" % (e, a) + os.unlink(e) + os.unlink(a) + else: + print "Set environmental variable 'DO_DIFF' to examine difference." + + # Helpers. + def mul(self, *arguments): + if len(arguments) == 0: + return None + + here = arguments[0] + if type(here) == type(''): + here = [here] + + if len(arguments) > 1: + there = apply(self.mul, arguments[1:]) + result = [] + for i in here: + for j in there: + result.append(i + j) + return result + + return here + + # Internal methods. + def ignore_elements(self, list, wildcard): + """Removes in-place, element of 'list' that match the given wildcard. + """ + list[:] = filter(lambda x, w=wildcard: not fnmatch.fnmatch(x, w), list) + + def adjust_lib_name(self, name): + global lib_prefix + result = name + + pos = string.rfind(name, ".") + if pos != -1: + suffix = name[pos:] + if suffix == ".lib": + (head, tail) = os.path.split(name) + if lib_prefix: + tail = "lib" + tail + result = os.path.join(head, tail) + elif suffix == ".dll": + (head, tail) = os.path.split(name) + if dll_prefix: + tail = "lib" + tail + result = os.path.join(head, tail) + # If we want to use this name in a Jamfile, we better convert \ to /, as + # otherwise we would have to quote \. + result = string.replace(result, "\\", "/") + return result + + def adjust_suffix(self, name): + if not self.translate_suffixes: + return name + + pos = string.rfind(name, ".") + if pos != -1: + suffix = name[pos:] + name = name[:pos] + + if suffixes.has_key(suffix): + suffix = suffixes[suffix] + else: + suffix = '' + + return name + suffix + + # Acceps either a string or a list of strings and returns a list of strings. + # Adjusts suffixes on all names. + def adjust_names(self, names): + if type(names) == types.StringType: + names = [names] + r = map(self.adjust_lib_name, names) + r = map(self.adjust_suffix, r) + r = map(lambda x, t=self.toolset: string.replace(x, "$toolset", t+"*"), r) + return r + + def native_file_name(self, name): + name = self.adjust_names(name)[0] + elements = string.split(name, "/") + return os.path.normpath(apply(os.path.join, [self.workdir]+elements)) + + # Wait while time is no longer equal to the time last "run_build_system" + # call finished. Used to avoid subsequent builds treating existing files as + # 'current'. + def wait_for_time_change_since_last_build(self): + while 1: + # In fact, I'm not sure why "+ 2" as opposed to "+ 1" is needed but + # empirically, "+ 1" sometimes causes 'touch' and other functions + # not to bump the file time enough for a rebuild to happen. + if math.floor(time.time()) < math.floor(self.last_build_time_finish) + 2: + time.sleep(0.1) + else: + break + + +class List: + + def __init__(self, s=""): + elements = [] + if isinstance(s, type("")): + # Have to handle espaced spaces correctly. + s = string.replace(s, "\ ", '\001') + elements = string.split(s) + else: + elements = s; + + self.l = [] + for e in elements: + self.l.append(string.replace(e, '\001', ' ')) + + def __len__(self): + return len(self.l) + + def __getitem__(self, key): + return self.l[key] + + def __setitem__(self, key, value): + self.l[key] = value + + def __delitem__(self, key): + del self.l[key] + + def __str__(self): + return str(self.l) + + def __repr__(self): + return (self.__module__ + '.List(' + + repr(string.join(self.l, ' ')) + + ')') + + def __mul__(self, other): + result = List() + if not isinstance(other, List): + other = List(other) + for f in self: + for s in other: + result.l.append(f + s) + return result + + def __rmul__(self, other): + if not isinstance(other, List): + other = List(other) + return List.__mul__(other, self) + + def __add__(self, other): + result = List() + result.l = self.l[:] + other.l[:] + return result + +# Quickie tests. Should use doctest instead. +if __name__ == '__main__': + assert str(List("foo bar") * "/baz") == "['foo/baz', 'bar/baz']" + assert repr("foo/" * List("bar baz")) == "__main__.List('foo/bar foo/baz')" + print 'tests passed' |